download
raw
15.8 kB
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}
}

Xet Storage Details

Size:
15.8 kB
·
Xet hash:
b876cf69f6ce486ab197fe86dc67ffa734be53b2f9b192ded3b40e0771a1d933

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