Initial project import
This commit is contained in:
218
node_modules/troika-three-text/src/GlyphsGeometry.js
generated
vendored
Normal file
218
node_modules/troika-three-text/src/GlyphsGeometry.js
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user