download
raw
7.8 kB
import {
PlaneGeometry,
InstancedBufferGeometry,
InstancedBufferAttribute,
Sphere,
Box3,
} from 'three'
const templateGeometries = {}
function getTemplateGeometry(detail) {
let geom = templateGeometries[detail]
if (!geom) {
geom = templateGeometries[detail] = new PlaneGeometry(1, 1, detail, detail).translate(0.5, 0.5, 0)
}
return geom
}
const glyphBoundsAttrName = 'aTroikaGlyphBounds'
const glyphIndexAttrName = 'aTroikaGlyphIndex'
const glyphColorAttrName = 'aTroikaGlyphColor'
/**
@class GlyphsGeometry
A specialized Geometry for rendering a set of text glyphs. Uses InstancedBufferGeometry to
render the glyphs using GPU instancing of a single quad, rather than constructing a whole
geometry with vertices, for much smaller attribute arraybuffers according to this math:
Where N = number of glyphs...
Instanced:
- position: 4 * 3
- index: 2 * 3
- normal: 4 * 3
- uv: 4 * 2
- glyph x/y bounds: N * 4
- glyph indices: N * 1
= 5N + 38
Non-instanced:
- position: N * 4 * 3
- index: N * 2 * 3
- normal: N * 4 * 3
- uv: N * 4 * 2
- glyph indices: N * 1
= 39N
A downside of this is the rare-but-possible lack of the instanced arrays extension,
which we could potentially work around with a fallback non-instanced implementation.
*/
class GlyphsGeometry extends InstancedBufferGeometry {
constructor() {
super()
this.detail = 1
this.curveRadius = 0
// Define groups for rendering text outline as a separate pass; these will only
// be used when the `material` getter returns an array, i.e. outlineWidth > 0.
this.groups = [
{start: 0, count: Infinity, materialIndex: 0},
{start: 0, count: Infinity, materialIndex: 1}
]
// Preallocate empty bounding objects
this.boundingSphere = new Sphere()
this.boundingBox = new Box3()
}
computeBoundingSphere () {
// No-op; we'll sync the boundingSphere proactively when needed.
}
computeBoundingBox() {
// No-op; we'll sync the boundingBox proactively when needed.
}
set detail(detail) {
if (detail !== this._detail) {
this._detail = detail
if (typeof detail !== 'number' || detail < 1) {
detail = 1
}
let tpl = getTemplateGeometry(detail)
;['position', 'normal', 'uv'].forEach(attr => {
this.attributes[attr] = tpl.attributes[attr].clone()
})
this.setIndex(tpl.getIndex().clone())
}
}
get detail() {
return this._detail
}
set curveRadius(r) {
if (r !== this._curveRadius) {
this._curveRadius = r
this._updateBounds()
}
}
get curveRadius() {
return this._curveRadius
}
/**
* Update the geometry for a new set of glyphs.
* @param {Float32Array} glyphBounds - An array holding the planar bounds for all glyphs
* to be rendered, 4 entries for each glyph: x1,x2,y1,y1
* @param {Float32Array} glyphAtlasIndices - An array holding the index of each glyph within
* the SDF atlas texture.
* @param {Array} blockBounds - An array holding the [minX, minY, maxX, maxY] across all glyphs
* @param {Array} [chunkedBounds] - An array of objects describing bounds for each chunk of N
* consecutive glyphs: `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`. This can be
* used with `applyClipRect` to choose an optimized `instanceCount`.
* @param {Uint8Array} [glyphColors] - An array holding r,g,b values for each glyph.
*/
updateGlyphs(glyphBounds, glyphAtlasIndices, blockBounds, chunkedBounds, glyphColors) {
// Update the instance attributes
this.updateAttributeData(glyphBoundsAttrName, glyphBounds, 4)
this.updateAttributeData(glyphIndexAttrName, glyphAtlasIndices, 1)
this.updateAttributeData(glyphColorAttrName, glyphColors, 3)
this._blockBounds = blockBounds
this._chunkedBounds = chunkedBounds
this.instanceCount = glyphAtlasIndices.length
this._updateBounds()
}
_updateBounds() {
const bounds = this._blockBounds
if (bounds) {
const { curveRadius, boundingBox: bbox } = this
if (curveRadius) {
const { PI, floor, min, max, sin, cos } = Math
const halfPi = PI / 2
const twoPi = PI * 2
const absR = Math.abs(curveRadius)
const leftAngle = bounds[0] / absR
const rightAngle = bounds[2] / absR
const minX = floor((leftAngle + halfPi) / twoPi) !== floor((rightAngle + halfPi) / twoPi)
? -absR : min(sin(leftAngle) * absR, sin(rightAngle) * absR)
const maxX = floor((leftAngle - halfPi) / twoPi) !== floor((rightAngle - halfPi) / twoPi)
? absR : max(sin(leftAngle) * absR, sin(rightAngle) * absR)
const maxZ = floor((leftAngle + PI) / twoPi) !== floor((rightAngle + PI) / twoPi)
? absR * 2 : max(absR - cos(leftAngle) * absR, absR - cos(rightAngle) * absR)
bbox.min.set(minX, bounds[1], curveRadius < 0 ? -maxZ : 0)
bbox.max.set(maxX, bounds[3], curveRadius < 0 ? 0 : maxZ)
} else {
bbox.min.set(bounds[0], bounds[1], 0)
bbox.max.set(bounds[2], bounds[3], 0)
}
bbox.getBoundingSphere(this.boundingSphere)
}
}
/**
* Given a clipping rect, and the chunkedBounds from the last updateGlyphs call, choose the lowest
* `instanceCount` that will show all glyphs within the clipped view. This is an optimization
* for long blocks of text that are clipped, to skip vertex shader evaluation for glyphs that would
* be clipped anyway.
*
* Note that since `drawElementsInstanced[ANGLE]` only accepts an instance count and not a starting
* offset, this optimization becomes less effective as the clipRect moves closer to the end of the
* text block. We could fix that by switching from instancing to a full geometry with a drawRange,
* but at the expense of much larger attribute buffers (see classdoc above.)
*
* @param {Vector4} clipRect
*/
applyClipRect(clipRect) {
let count = this.getAttribute(glyphIndexAttrName).count
let chunks = this._chunkedBounds
if (chunks) {
for (let i = chunks.length; i--;) {
count = chunks[i].end
let rect = chunks[i].rect
// note: both rects are l-b-r-t
if (rect[1] < clipRect.w && rect[3] > clipRect.y && rect[0] < clipRect.z && rect[2] > clipRect.x) {
break
}
}
}
this.instanceCount = count
}
/**
* Utility for updating instance attributes with automatic resizing
*/
updateAttributeData(attrName, newArray, itemSize) {
const attr = this.getAttribute(attrName)
if (newArray) {
// If length isn't changing, just update the attribute's array data
if (attr && attr.array.length === newArray.length) {
attr.array.set(newArray)
attr.needsUpdate = true
} else {
this.setAttribute(attrName, new InstancedBufferAttribute(newArray, itemSize))
// If the new attribute has a different size, we also have to (as of r117) manually clear the
// internal cached max instance count. See https://github.com/mrdoob/three.js/issues/19706
// It's unclear if this is a threejs bug or a truly unsupported scenario; discussion in
// that ticket is ambiguous as to whether replacing a BufferAttribute with one of a
// different size is supported, but https://github.com/mrdoob/three.js/pull/17418 strongly
// implies it should be supported. It's possible we need to
delete this._maxInstanceCount //for r117+, could be fragile
this.dispose() //for r118+, more robust feeling, but more heavy-handed than I'd like
}
} else if (attr) {
this.deleteAttribute(attrName)
}
}
}
export {
GlyphsGeometry,
glyphBoundsAttrName,
glyphColorAttrName,
glyphIndexAttrName,
}

Xet Storage Details

Size:
7.8 kB
·
Xet hash:
5ef9d8f0f93b58a2d51f58aaa222aecd647342e3b9ee09a36dac723c0a4db261

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.