Skip to content

Commit

Permalink
feat(BatchedText): Support remaining visual properties
Browse files Browse the repository at this point in the history
  • Loading branch information
lojjic committed Nov 11, 2024
1 parent 6d94024 commit 60c5e93
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 62 deletions.
12 changes: 10 additions & 2 deletions packages/troika-examples/text-batched/BatchedTextExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function BatchedTextExample ({ stats, width, height }) {
"Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen",
"Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty"
]
const subset = all.slice(0, Math.max(8, Math.floor(Math.random() * all.length)))
const subset = all.slice(0, Math.max(16, Math.floor(Math.random() * all.length)))
return subset.map((text, i) => ({
facade: Text3DFacade,
text,
Expand All @@ -27,7 +27,15 @@ export default function BatchedTextExample ({ stats, width, height }) {
anchorX: "50%",
anchorY: "50%",
color: randColor(),
// fillOpacity: Math.random() < 0.5 ? 0.2 : 1,
// fillOpacity: Math.random() < 0.5 ? 0.1 : 1,
// strokeWidth: Math.random() < 0.5 ? '3%' : 0,
// strokeColor: randColor(),
outlineWidth: Math.random() < 0.3 ? '10%' : 0,
// outlineBlur: Math.random() < 0.3 ? '20%' : 0,
// outlineColor: randColor(),
// outlineOpacity: Math.random(),
// clipRect: Math.random() < 0.5 ? [0, 0, 999, 999] : null,
// curveRadius: Math.random() < 0.5 ? 0.1 : 0,
animation: {
from: { rotateY: 0 },
to: { rotateY: Math.PI * 2 },
Expand Down
207 changes: 157 additions & 50 deletions packages/troika-three-text/src/BatchedText.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,30 @@ const syncStartEvent = { type: "syncstart" };
const syncCompleteEvent = { type: "synccomplete" };
const memberIndexAttrName = "aTroikaTextBatchMemberIndex";

// 0-15: matrix
// 16: color
// 17: fillOpacity
// TODO:
// total bounds for uv
// outlineWidth/Color/Opacity/Blur/Offset
// strokeWidth/Color/Opacity
// clipRect
let floatsPerMember = 18;
floatsPerMember = Math.ceil(floatsPerMember / 4) * 4; // whole texels

/*
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();
Expand All @@ -31,9 +45,7 @@ const tempColor = new Color();
* The `material` of each child `Text` will be ignored, and the `material` of the
* `BatchedText` will be used for all of them instead.
*
* @todo Support for WebGL1 without OES_texture_float extension (pack floats into 4 ints)
* @todo Handle more visual uniforms: uv bounds, outlines, etc.
* @todo Handle things that can't vary between members like sdfGlyphSize - separate batches, or throw?
* NOTE: This only works in WebGL2 or where the OES_texture_float extension is available.
*/
export class BatchedText extends Text {
constructor () {
Expand All @@ -49,6 +61,7 @@ export class BatchedText extends Text {
* @type {Map<Text, PackingInfo>}
*/
this._members = new Map();
this._dataTextures = {}

this._onMemberSynced = (e) => {
this._members.get(e.target).dirty = true;
Expand Down Expand Up @@ -123,21 +136,27 @@ export class BatchedText extends Text {
bbox.getBoundingSphere(this.geometry.boundingSphere);
}

/** @override */
hasOutline() {
return this._members.keys().some(m => m.hasOutline())
}

/**
* @override
* Copy member matrices and uniform values into the data texture
*/
_prepareForRender (material) {
// Copy member matrices to the texture
// TODO only do this once, not once per material
const isOutline = material.isTextOutlineMaterial
material.uniforms.uTroikaIsOutline.value = isOutline

// Resize the texture to fit in powers of 2
let texture = this._mat4Texture;
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._mat4Texture = new DataTexture(
texture = this[isOutline ? 'outline' : 'main'] = new DataTexture(
new Float32Array(dataLength),
width,
dataLength / 4 / width,
Expand All @@ -163,23 +182,62 @@ export class BatchedText extends Text {
setTexData(startIndex + i, matrix[i])
}

// Color + opacity
let color = text.color;
// 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 + 16, tempColor.set(color).getHex());
setTexData(startIndex + 17, text.fillOpacity == null ? 1 : text.fillOpacity)

// TODO:
// outlineWidth/Color/Opacity/Blur/Offset
// strokeWidth/Color/Opacity
// fillOpacity
// clipRect
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);
}

Expand Down Expand Up @@ -270,7 +328,7 @@ export class BatchedText extends Text {

dispose () {
super.dispose();
this._mat4Texture.dispose();
Object.values(this._dataTextures).forEach(tex => tex.dispose())
}
}

Expand All @@ -283,7 +341,6 @@ function cloneAndResize (source, newLength) {
function createBatchedTextMaterial (baseMaterial) {
const texUniformName = "uTroikaMatricesTexture";
const texSizeUniformName = "uTroikaMatricesTextureSize";
const fillOpacityVaryingName = "vTroikaFillOpacity";

// Due to how vertexTransform gets injected, the matrix transforms must happen
// in the base material of TextDerivedMaterial, but other transforms to its
Expand Down Expand Up @@ -327,36 +384,60 @@ function createBatchedTextMaterial (baseMaterial) {
// Now make other changes to the derived text shader code
batchMaterial = createDerivedMaterial(batchMaterial, {
chained: true,
customRewriter({vertexShader, fragmentShader}) {
uniforms: {
uTroikaIsOutline: {value: false},
},
customRewriter(shaders) {
// Convert some text shader uniforms to varyings
function uniformToVarying(shader, uniformName, varyingName) {
return shader.replace(
new RegExp(`(uniform\\s+(float|vec[234])\\s+)?\\b${uniformName}\\b`, 'g'),
(_, isDeclaration, type) => {
return (isDeclaration ? `varying ${type} ` : '') + varyingName
}
);
}
fragmentShader = uniformToVarying(fragmentShader, 'uTroikaFillOpacity', fillOpacityVaryingName);

// Strip out diffuse uniform from the vertex shader, if it was added by
// TextDerivedMaterial, and replace with a writeable var
vertexShader = 'vec3 diffuse;\n' + vertexShader.replace(/uniform vec3 diffuse;/, '')

return {vertexShader, fragmentShader}
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: `
varying float ${fillOpacityVaryingName};
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="}"
vertexMainIntro: `
vec4 colorData = troikaBatchTexel(4.0);
diffuse = troikaFloatToColor(colorData.r);
${fillOpacityVaryingName} = colorData.g;
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;
}
`,
});

Expand All @@ -367,3 +448,29 @@ function createBatchedTextMaterial (baseMaterial) {
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}
}

8 changes: 6 additions & 2 deletions packages/troika-three-text/src/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ class Text extends Mesh {
// feature (see GlyphsGeometry which sets up `groups` for this purpose) Doing it with multi
// materials ensures the layers are always rendered consecutively in a consistent order.
// Each layer will trigger onBeforeRender with the appropriate material.
if (this.outlineWidth || this.outlineBlur || this.outlineOffsetX || this.outlineOffsetY) {
if (this.hasOutline()) {
let outlineMaterial = derivedMaterial._outlineMtl
if (!outlineMaterial) {
outlineMaterial = derivedMaterial._outlineMtl = Object.create(derivedMaterial, {
Expand Down Expand Up @@ -567,6 +567,10 @@ class Text extends Mesh {
}
}

hasOutline() {
return !!(this.outlineWidth || this.outlineBlur || this.outlineOffsetX || this.outlineOffsetY)
}

get glyphGeometryDetail() {
return this.geometry.detail
}
Expand Down Expand Up @@ -629,7 +633,7 @@ class Text extends Mesh {
fillOpacity = this.fillOpacity
}

uniforms.uTroikaDistanceOffset.value = distanceOffset
uniforms.uTroikaEdgeOffset.value = distanceOffset
uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY)
uniforms.uTroikaBlurRadius.value = blurRadius
uniforms.uTroikaStrokeWidth.value = strokeWidth
Expand Down
14 changes: 6 additions & 8 deletions packages/troika-three-text/src/TextDerivedMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ uniform vec4 uTroikaTotalBounds;
uniform vec4 uTroikaClipRect;
uniform mat3 uTroikaOrient;
uniform bool uTroikaUseGlyphColors;
uniform float uTroikaDistanceOffset;
uniform float uTroikaEdgeOffset;
uniform float uTroikaBlurRadius;
uniform vec2 uTroikaPositionOffset;
uniform float uTroikaCurveRadius;
Expand All @@ -30,8 +30,8 @@ bounds.xz += uTroikaPositionOffset.x;
bounds.yw -= uTroikaPositionOffset.y;
vec4 outlineBounds = vec4(
bounds.xy - uTroikaDistanceOffset - uTroikaBlurRadius,
bounds.zw + uTroikaDistanceOffset + uTroikaBlurRadius
bounds.xy - uTroikaEdgeOffset - uTroikaBlurRadius,
bounds.zw + uTroikaEdgeOffset + uTroikaBlurRadius
);
vec4 clippedBounds = vec4(
clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw),
Expand Down Expand Up @@ -77,9 +77,8 @@ uniform sampler2D uTroikaSDFTexture;
uniform vec2 uTroikaSDFTextureSize;
uniform float uTroikaSDFGlyphSize;
uniform float uTroikaSDFExponent;
uniform float uTroikaDistanceOffset;
uniform float uTroikaEdgeOffset;
uniform float uTroikaFillOpacity;
uniform float uTroikaOutlineOpacity;
uniform float uTroikaBlurRadius;
uniform vec3 uTroikaStrokeColor;
uniform float uTroikaStrokeWidth;
Expand Down Expand Up @@ -182,7 +181,7 @@ float aaDist = troikaGetAADist();
float fragDistance = troikaGetFragDistValue();
float edgeAlpha = uTroikaSDFDebug ?
troikaGlyphUvToSdfValue(vTroikaGlyphUV) :
troikaGetEdgeAlpha(fragDistance, uTroikaDistanceOffset, max(aaDist, uTroikaBlurRadius));
troikaGetEdgeAlpha(fragDistance, uTroikaEdgeOffset, max(aaDist, uTroikaBlurRadius));
#if !defined(IS_DEPTH_MATERIAL) && !defined(IS_DISTANCE_MATERIAL)
vec4 fillRGBA = gl_FragColor;
Expand Down Expand Up @@ -219,8 +218,7 @@ export function createTextDerivedMaterial(baseMaterial) {
uTroikaSDFExponent: {value: 0},
uTroikaTotalBounds: {value: new Vector4(0,0,0,0)},
uTroikaClipRect: {value: new Vector4(0,0,0,0)},
uTroikaDistanceOffset: {value: 0},
uTroikaOutlineOpacity: {value: 0},
uTroikaEdgeOffset: {value: 0},
uTroikaFillOpacity: {value: 1},
uTroikaPositionOffset: {value: new Vector2()},
uTroikaCurveRadius: {value: 0},
Expand Down

0 comments on commit 60c5e93

Please sign in to comment.