Initial project import
This commit is contained in:
491
node_modules/troika-three-text/src/BatchedText.js
generated
vendored
Normal file
491
node_modules/troika-three-text/src/BatchedText.js
generated
vendored
Normal file
@@ -0,0 +1,491 @@
|
||||
import { Text } from "./Text.js";
|
||||
import { DataTexture, FloatType, RGBAFormat, Vector2, Box3, Color, DynamicDrawUsage } from "three";
|
||||
import { glyphBoundsAttrName, glyphIndexAttrName } from "./GlyphsGeometry";
|
||||
import { createDerivedMaterial } from "troika-three-utils";
|
||||
import { createTextDerivedMaterial } from "./TextDerivedMaterial";
|
||||
|
||||
const syncStartEvent = { type: "syncstart" };
|
||||
const syncCompleteEvent = { type: "synccomplete" };
|
||||
const memberIndexAttrName = "aTroikaTextBatchMemberIndex";
|
||||
|
||||
|
||||
/*
|
||||
Data texture packing strategy:
|
||||
|
||||
# Common:
|
||||
0-15: matrix
|
||||
16-19: uTroikaTotalBounds
|
||||
20-23: uTroikaClipRect
|
||||
24: diffuse (color/outlineColor)
|
||||
25: uTroikaFillOpacity (fillOpacity/outlineOpacity)
|
||||
26: uTroikaCurveRadius
|
||||
27: <blank>
|
||||
|
||||
# Main:
|
||||
28: uTroikaStrokeWidth
|
||||
29: uTroikaStrokeColor
|
||||
30: uTroikaStrokeOpacity
|
||||
|
||||
# Outline:
|
||||
28-29: uTroikaPositionOffset
|
||||
30: uTroikaEdgeOffset
|
||||
31: uTroikaBlurRadius
|
||||
*/
|
||||
const floatsPerMember = 32;
|
||||
|
||||
const tempBox3 = new Box3();
|
||||
const tempColor = new Color();
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*
|
||||
* A specialized `Text` implementation that accepts any number of `Text` children
|
||||
* and automatically batches them together to render in a single draw call.
|
||||
*
|
||||
* The `material` of each child `Text` will be ignored, and the `material` of the
|
||||
* `BatchedText` will be used for all of them instead.
|
||||
*
|
||||
* NOTE: This only works in WebGL2 or where the OES_texture_float extension is available.
|
||||
*/
|
||||
export class BatchedText extends Text {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackingInfo
|
||||
* @property {number} index - the packing order index when last packed, or -1
|
||||
* @property {boolean} dirty - whether it has synced since last pack
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<Text, PackingInfo>}
|
||||
*/
|
||||
this._members = new Map();
|
||||
this._dataTextures = {}
|
||||
|
||||
this._onMemberSynced = (e) => {
|
||||
this._members.get(e.target).dirty = true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Batch any Text objects added as children
|
||||
*/
|
||||
add (...objects) {
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
if (objects[i] instanceof Text) {
|
||||
this.addText(objects[i]);
|
||||
} else {
|
||||
super.add(objects[i]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
remove (...objects) {
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
if (objects[i] instanceof Text) {
|
||||
this.removeText(objects[i]);
|
||||
} else {
|
||||
super.remove(objects[i]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Text} text
|
||||
*/
|
||||
addText (text) {
|
||||
if (!this._members.has(text)) {
|
||||
this._members.set(text, {
|
||||
index: -1,
|
||||
glyphCount: -1,
|
||||
dirty: true
|
||||
});
|
||||
text.addEventListener("synccomplete", this._onMemberSynced);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Text} text
|
||||
*/
|
||||
removeText (text) {
|
||||
this._needsRepack = true
|
||||
text.removeEventListener("synccomplete", this._onMemberSynced);
|
||||
this._members.delete(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the custom derivation with extra batching logic
|
||||
*/
|
||||
createDerivedMaterial (baseMaterial) {
|
||||
return createBatchedTextMaterial(baseMaterial);
|
||||
}
|
||||
|
||||
updateMatrixWorld (force) {
|
||||
super.updateMatrixWorld(force);
|
||||
this.updateBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the batched geometry bounds to hold all members
|
||||
*/
|
||||
updateBounds () {
|
||||
// Update member local matrices and the overall bounds
|
||||
const bbox = this.geometry.boundingBox.makeEmpty();
|
||||
this._members.forEach((_, text) => {
|
||||
if (text.matrixAutoUpdate) text.updateMatrix(); // ignore world matrix
|
||||
tempBox3.copy(text.geometry.boundingBox).applyMatrix4(text.matrix);
|
||||
bbox.union(tempBox3);
|
||||
});
|
||||
bbox.getBoundingSphere(this.geometry.boundingSphere);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
hasOutline() {
|
||||
// Iterator.some() not supported in Safari
|
||||
for (let member of this._members.keys()) {
|
||||
if (member.hasOutline()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Copy member matrices and uniform values into the data texture
|
||||
*/
|
||||
_prepareForRender (material) {
|
||||
const isOutline = material.isTextOutlineMaterial
|
||||
material.uniforms.uTroikaIsOutline.value = isOutline
|
||||
|
||||
// Resize the texture to fit in powers of 2
|
||||
let texture = this._dataTextures[isOutline ? 'outline' : 'main'];
|
||||
const dataLength = Math.pow(2, Math.ceil(Math.log2(this._members.size * floatsPerMember)));
|
||||
if (!texture || dataLength !== texture.image.data.length) {
|
||||
// console.log(`resizing: ${dataLength}`);
|
||||
if (texture) texture.dispose();
|
||||
const width = Math.min(dataLength / 4, 1024);
|
||||
texture = this._dataTextures[isOutline ? 'outline' : 'main'] = new DataTexture(
|
||||
new Float32Array(dataLength),
|
||||
width,
|
||||
dataLength / 4 / width,
|
||||
RGBAFormat,
|
||||
FloatType
|
||||
);
|
||||
}
|
||||
|
||||
const texData = texture.image.data;
|
||||
const setTexData = (index, value) => {
|
||||
if (value !== texData[index]) {
|
||||
texData[index] = value;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
this._members.forEach(({ index, dirty }, text) => {
|
||||
if (index > -1) {
|
||||
const startIndex = index * floatsPerMember
|
||||
|
||||
// Matrix
|
||||
const matrix = text.matrix.elements;
|
||||
for (let i = 0; i < 16; i++) {
|
||||
setTexData(startIndex + i, matrix[i])
|
||||
}
|
||||
|
||||
// Let the member populate the uniforms, since that does all the appropriate
|
||||
// logic and handling of defaults, and we'll just grab the results from there
|
||||
text._prepareForRender(material)
|
||||
const {
|
||||
uTroikaTotalBounds,
|
||||
uTroikaClipRect,
|
||||
uTroikaPositionOffset,
|
||||
uTroikaEdgeOffset,
|
||||
uTroikaBlurRadius,
|
||||
uTroikaStrokeWidth,
|
||||
uTroikaStrokeColor,
|
||||
uTroikaStrokeOpacity,
|
||||
uTroikaFillOpacity,
|
||||
uTroikaCurveRadius,
|
||||
} = material.uniforms;
|
||||
|
||||
// Total bounds for uv
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTexData(startIndex + 16 + i, uTroikaTotalBounds.value.getComponent(i));
|
||||
}
|
||||
|
||||
// Clip rect
|
||||
for (let i = 0; i < 4; i++) {
|
||||
setTexData(startIndex + 20 + i, uTroikaClipRect.value.getComponent(i));
|
||||
}
|
||||
|
||||
// Color
|
||||
let color = isOutline ? (text.outlineColor || 0) : text.color;
|
||||
if (color == null) color = this.color;
|
||||
if (color == null) color = this.material.color;
|
||||
if (color == null) color = 0xffffff;
|
||||
setTexData(startIndex + 24, tempColor.set(color).getHex());
|
||||
|
||||
// Fill opacity / outline opacity
|
||||
setTexData(startIndex + 25, uTroikaFillOpacity.value)
|
||||
|
||||
// Curve radius
|
||||
setTexData(startIndex + 26, uTroikaCurveRadius.value)
|
||||
|
||||
if (isOutline) {
|
||||
// Outline properties
|
||||
setTexData(startIndex + 28, uTroikaPositionOffset.value.x);
|
||||
setTexData(startIndex + 29, uTroikaPositionOffset.value.y);
|
||||
setTexData(startIndex + 30, uTroikaEdgeOffset.value);
|
||||
setTexData(startIndex + 31, uTroikaBlurRadius.value);
|
||||
} else {
|
||||
// Stroke properties
|
||||
setTexData(startIndex + 28, uTroikaStrokeWidth.value);
|
||||
setTexData(startIndex + 29, tempColor.set(uTroikaStrokeColor.value).getHex());
|
||||
setTexData(startIndex + 30, uTroikaStrokeOpacity.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
material.setMatrixTexture(texture);
|
||||
|
||||
// For the non-member-specific uniforms:
|
||||
super._prepareForRender(material);
|
||||
}
|
||||
|
||||
sync (callback) {
|
||||
// TODO: skip members updating their geometries, just use textRenderInfo directly
|
||||
|
||||
// Trigger sync on all members that need it
|
||||
let syncPromises = this._needsRepack ? [] : null;
|
||||
this._needsRepack = false;
|
||||
this._members.forEach((packingInfo, text) => {
|
||||
if (packingInfo.dirty || text._needsSync) {
|
||||
packingInfo.dirty = false;
|
||||
(syncPromises || (syncPromises = [])).push(new Promise(resolve => {
|
||||
if (text._needsSync) {
|
||||
text.sync(resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// If any needed syncing, wait for them and then repack the batched geometry
|
||||
if (syncPromises) {
|
||||
this.dispatchEvent(syncStartEvent);
|
||||
|
||||
Promise.all(syncPromises).then(() => {
|
||||
const { geometry } = this;
|
||||
const batchedAttributes = geometry.attributes;
|
||||
let memberIndexes = batchedAttributes[memberIndexAttrName] && batchedAttributes[memberIndexAttrName].array || new Uint16Array(0);
|
||||
let batchedGlyphIndexes = batchedAttributes[glyphIndexAttrName] && batchedAttributes[glyphIndexAttrName].array || new Float32Array(0);
|
||||
let batchedGlyphBounds = batchedAttributes[glyphBoundsAttrName] && batchedAttributes[glyphBoundsAttrName].array || new Float32Array(0);
|
||||
|
||||
// Initial pass to collect total glyph count and resize the arrays if needed
|
||||
let totalGlyphCount = 0;
|
||||
this._members.forEach((packingInfo, { textRenderInfo }) => {
|
||||
if (textRenderInfo) {
|
||||
totalGlyphCount += textRenderInfo.glyphAtlasIndices.length;
|
||||
this._textRenderInfo = textRenderInfo; // TODO - need this, but be smarter
|
||||
}
|
||||
});
|
||||
if (totalGlyphCount !== memberIndexes.length) {
|
||||
memberIndexes = cloneAndResize(memberIndexes, totalGlyphCount);
|
||||
batchedGlyphIndexes = cloneAndResize(batchedGlyphIndexes, totalGlyphCount);
|
||||
batchedGlyphBounds = cloneAndResize(batchedGlyphBounds, totalGlyphCount * 4);
|
||||
}
|
||||
|
||||
// Populate batch arrays
|
||||
let memberIndex = 0;
|
||||
let glyphIndex = 0;
|
||||
this._members.forEach((packingInfo, { textRenderInfo }) => {
|
||||
if (textRenderInfo) {
|
||||
const glyphCount = textRenderInfo.glyphAtlasIndices.length;
|
||||
memberIndexes.fill(memberIndex, glyphIndex, glyphIndex + glyphCount);
|
||||
|
||||
// TODO can skip these for members that are not dirty or shifting overall position:
|
||||
batchedGlyphIndexes.set(textRenderInfo.glyphAtlasIndices, glyphIndex, glyphIndex + glyphCount);
|
||||
batchedGlyphBounds.set(textRenderInfo.glyphBounds, glyphIndex * 4, (glyphIndex + glyphCount) * 4);
|
||||
|
||||
glyphIndex += glyphCount;
|
||||
packingInfo.index = memberIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the geometry attributes
|
||||
geometry.updateAttributeData(memberIndexAttrName, memberIndexes, 1);
|
||||
geometry.getAttribute(memberIndexAttrName).setUsage(DynamicDrawUsage);
|
||||
geometry.updateAttributeData(glyphIndexAttrName, batchedGlyphIndexes, 1);
|
||||
geometry.updateAttributeData(glyphBoundsAttrName, batchedGlyphBounds, 4);
|
||||
|
||||
this.updateBounds();
|
||||
|
||||
this.dispatchEvent(syncCompleteEvent);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
copy (source) {
|
||||
if (source instanceof BatchedText) {
|
||||
super.copy(source);
|
||||
this._members.forEach((_, text) => this.removeText(text));
|
||||
source._members.forEach((_, text) => this.addText(text));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
dispose () {
|
||||
super.dispose();
|
||||
Object.values(this._dataTextures).forEach(tex => tex.dispose())
|
||||
}
|
||||
}
|
||||
|
||||
function cloneAndResize (source, newLength) {
|
||||
const copy = new source.constructor(newLength);
|
||||
copy.set(source.subarray(0, newLength));
|
||||
return copy;
|
||||
}
|
||||
|
||||
function createBatchedTextMaterial (baseMaterial) {
|
||||
const texUniformName = "uTroikaMatricesTexture";
|
||||
const texSizeUniformName = "uTroikaMatricesTextureSize";
|
||||
|
||||
// Due to how vertexTransform gets injected, the matrix transforms must happen
|
||||
// in the base material of TextDerivedMaterial, but other transforms to its
|
||||
// shader must come after, so we sandwich it between two derivations.
|
||||
|
||||
// Transform the vertex position
|
||||
let batchMaterial = createDerivedMaterial(baseMaterial, {
|
||||
chained: true,
|
||||
uniforms: {
|
||||
[texSizeUniformName]: { value: new Vector2() },
|
||||
[texUniformName]: { value: null }
|
||||
},
|
||||
// language=GLSL
|
||||
vertexDefs: `
|
||||
uniform highp sampler2D ${texUniformName};
|
||||
uniform vec2 ${texSizeUniformName};
|
||||
attribute float ${memberIndexAttrName};
|
||||
|
||||
vec4 troikaBatchTexel(float offset) {
|
||||
offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0;
|
||||
float w = ${texSizeUniformName}.x;
|
||||
vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName};
|
||||
return texture2D(${texUniformName}, uv);
|
||||
}
|
||||
`,
|
||||
// language=GLSL prefix="void main() {" suffix="}"
|
||||
vertexTransform: `
|
||||
mat4 matrix = mat4(
|
||||
troikaBatchTexel(0.0),
|
||||
troikaBatchTexel(1.0),
|
||||
troikaBatchTexel(2.0),
|
||||
troikaBatchTexel(3.0)
|
||||
);
|
||||
position.xyz = (matrix * vec4(position, 1.0)).xyz;
|
||||
`,
|
||||
});
|
||||
|
||||
// Add the text shaders
|
||||
batchMaterial = createTextDerivedMaterial(batchMaterial);
|
||||
|
||||
// Now make other changes to the derived text shader code
|
||||
batchMaterial = createDerivedMaterial(batchMaterial, {
|
||||
chained: true,
|
||||
uniforms: {
|
||||
uTroikaIsOutline: {value: false},
|
||||
},
|
||||
customRewriter(shaders) {
|
||||
// Convert some text shader uniforms to varyings
|
||||
const varyingUniforms = [
|
||||
'uTroikaTotalBounds',
|
||||
'uTroikaClipRect',
|
||||
'uTroikaPositionOffset',
|
||||
'uTroikaEdgeOffset',
|
||||
'uTroikaBlurRadius',
|
||||
'uTroikaStrokeWidth',
|
||||
'uTroikaStrokeColor',
|
||||
'uTroikaStrokeOpacity',
|
||||
'uTroikaFillOpacity',
|
||||
'uTroikaCurveRadius',
|
||||
'diffuse'
|
||||
]
|
||||
varyingUniforms.forEach(uniformName => {
|
||||
shaders = uniformToVarying(shaders, uniformName)
|
||||
})
|
||||
return shaders
|
||||
},
|
||||
// language=GLSL
|
||||
vertexDefs: `
|
||||
uniform bool uTroikaIsOutline;
|
||||
vec3 troikaFloatToColor(float v) {
|
||||
return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;
|
||||
}
|
||||
`,
|
||||
// language=GLSL prefix="void main() {" suffix="}"
|
||||
vertexTransform: `
|
||||
uTroikaTotalBounds = troikaBatchTexel(4.0);
|
||||
uTroikaClipRect = troikaBatchTexel(5.0);
|
||||
|
||||
vec4 data = troikaBatchTexel(6.0);
|
||||
diffuse = troikaFloatToColor(data.x);
|
||||
uTroikaFillOpacity = data.y;
|
||||
uTroikaCurveRadius = data.z;
|
||||
|
||||
data = troikaBatchTexel(7.0);
|
||||
if (uTroikaIsOutline) {
|
||||
if (data == vec4(0.0)) { // degenerate if zero outline
|
||||
position = vec3(0.0);
|
||||
} else {
|
||||
uTroikaPositionOffset = data.xy;
|
||||
uTroikaEdgeOffset = data.z;
|
||||
uTroikaBlurRadius = data.w;
|
||||
}
|
||||
} else {
|
||||
uTroikaStrokeWidth = data.x;
|
||||
uTroikaStrokeColor = troikaFloatToColor(data.y);
|
||||
uTroikaStrokeOpacity = data.z;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
batchMaterial.setMatrixTexture = (texture) => {
|
||||
batchMaterial.uniforms[texUniformName].value = texture;
|
||||
batchMaterial.uniforms[texSizeUniformName].value.set(texture.image.width, texture.image.height);
|
||||
};
|
||||
return batchMaterial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a uniform into a varying/writeable value.
|
||||
* - If the uniform was used in the fragment shader, it will become a varying in both shaders.
|
||||
* - If the uniform was only used in the vertex shader, it will become a writeable var.
|
||||
*/
|
||||
export function uniformToVarying({vertexShader, fragmentShader}, uniformName, varyingName = uniformName) {
|
||||
const uniformRE = new RegExp(`uniform\\s+(bool|float|vec[234]|mat[34])\\s+${uniformName}\\b`)
|
||||
|
||||
let type
|
||||
let hadFragmentUniform = false
|
||||
fragmentShader = fragmentShader.replace(uniformRE, ($0, $1) => {
|
||||
hadFragmentUniform = true
|
||||
return `varying ${type = $1} ${varyingName}`
|
||||
})
|
||||
|
||||
let hadVertexUniform = false
|
||||
vertexShader = vertexShader.replace(uniformRE, (_, $1) => {
|
||||
hadVertexUniform = true
|
||||
return `${hadFragmentUniform ? 'varying' : ''} ${type = $1} ${varyingName}`
|
||||
})
|
||||
if (!hadVertexUniform) {
|
||||
vertexShader = `${hadFragmentUniform ? 'varying' : ''} ${type} ${varyingName};\n${vertexShader}`
|
||||
}
|
||||
return {vertexShader, fragmentShader}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user