From a042c3809435832e637ce824c20dceb119c43b62 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Fri, 19 Feb 2021 13:51:27 -0700 Subject: [PATCH] wip: track and add glsl attributes for char/word/line indices --- packages/troika-examples/text/TextExample.jsx | 27 ++++++- .../troika-three-text/src/GlyphsGeometry.js | 12 ++- packages/troika-three-text/src/Text.js | 7 +- packages/troika-three-text/src/TextBuilder.js | 51 +++++++------ .../src/TextDerivedMaterial.js | 61 +++++++++++++-- .../src/worker/FontProcessor.js | 76 +++++++++++++++++++ 6 files changed, 198 insertions(+), 36 deletions(-) diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index ec14b596..1422a0e1 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -80,8 +80,33 @@ const MATERIALS = { normal.xyz = normalize(vec3(-cos(waveX) * waveAmplitude, 0.0, 1.0)); position.z += waveZ; ` + }), + 'charIndex': indexAnimatorMaterial('charIndex', 'totalChars'), + 'charInWordIndex': indexAnimatorMaterial('charInWordIndex', 'totalCharsInWord'), + 'charInLineIndex': indexAnimatorMaterial('charInLineIndex', 'totalCharsInLine'), + 'wordIndex': indexAnimatorMaterial('wordIndex', 'totalWords'), + 'wordInLineIndex': indexAnimatorMaterial('wordInLineIndex', 'totalWordsInLine'), + 'lineIndex': indexAnimatorMaterial('lineIndex', 'totalLines'), +} + +function indexAnimatorMaterial(indexVar, totalVar) { + return createDerivedMaterial(new MeshBasicMaterial(), { + timeUniform: 'elapsed', + // vertexTransform: ` + // // float angle = mix(0.0, PI / 2.0, ${indexVar} / ${totalVar}); + // // position.z = position.y * cos(angle); + // // position.y *= sin(angle); + // // + // // position *= clamp(0.0, 1.0, (elapsed / 10.0 - ${indexVar}) / 10.0); + // `, + fragmentColorTransform: ` + float angle = ${indexVar} / ${totalVar} * PI * 2.0; + gl_FragColor = vec4( sin(angle) / 2.0 + 0.5, cos(angle) / 2.0 + 0.5, tan(angle) / 2.0 + 0.5, 1.0 ); + ` }) } + + const MATERIAL_OPTS = Object.keys(MATERIALS) Object.keys(MATERIALS).forEach(name => { MATERIALS[name + '+Texture'] = MATERIALS[name].clone() @@ -116,7 +141,7 @@ class TextExample extends React.Component { curveRadius: 0, fog: false, animTextColor: true, - animTilt: true, + animTilt: false, animRotate: false, material: 'MeshStandardMaterial', useTexture: false, diff --git a/packages/troika-three-text/src/GlyphsGeometry.js b/packages/troika-three-text/src/GlyphsGeometry.js index d4290c1a..7fc81b6c 100644 --- a/packages/troika-three-text/src/GlyphsGeometry.js +++ b/packages/troika-three-text/src/GlyphsGeometry.js @@ -20,8 +20,10 @@ const GlyphsGeometry = /*#__PURE__*/(() => { const tempVec3 = new Vector3() const glyphBoundsAttrName = 'aTroikaGlyphBounds' - const glyphIndexAttrName = 'aTroikaGlyphIndex' + const atlasIndexAttrName = 'aTroikaGlyphAtlasIndex' const glyphColorAttrName = 'aTroikaGlyphColor' + const charIndicesAttrName = 'aTroikaCharIndices' + const wordAndLineIndicesAttrName = 'aTroikaWordLineIndices' /** @class GlyphsGeometry @@ -108,11 +110,13 @@ const GlyphsGeometry = /*#__PURE__*/(() => { * 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) { + updateGlyphs(glyphBounds, glyphAtlasIndices, blockBounds, chunkedBounds, glyphColors, charIndices, wordAndLineIndices) { // Update the instance attributes updateBufferAttr(this, glyphBoundsAttrName, glyphBounds, 4) - updateBufferAttr(this, glyphIndexAttrName, glyphAtlasIndices, 1) + updateBufferAttr(this, atlasIndexAttrName, glyphAtlasIndices, 1) updateBufferAttr(this, glyphColorAttrName, glyphColors, 3) + updateBufferAttr(this, charIndicesAttrName, charIndices, 4) + updateBufferAttr(this, wordAndLineIndicesAttrName, wordAndLineIndices, 4) this._chunkedBounds = chunkedBounds setInstanceCount(this, glyphAtlasIndices.length) @@ -145,7 +149,7 @@ const GlyphsGeometry = /*#__PURE__*/(() => { * @param {Vector4} clipRect */ applyClipRect(clipRect) { - let count = this.getAttribute(glyphIndexAttrName).count + let count = this.getAttribute(atlasIndexAttrName).count let chunks = this._chunkedBounds if (chunks) { for (let i = chunks.length; i--;) { diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index ca2946e5..d1dd1f41 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -393,6 +393,8 @@ const Text = /*#__PURE__*/(() => { anchorY: this.anchorY, colorRanges: this.colorRanges, includeCaretPositions: true, //TODO parameterize + includeCharIndices: true, //TODO parameterize + includeWordAndLineIndices: true, //TODO parameterize sdfGlyphSize: this.sdfGlyphSize }, textRenderInfo => { this._isSyncing = false @@ -406,7 +408,9 @@ const Text = /*#__PURE__*/(() => { textRenderInfo.glyphAtlasIndices, textRenderInfo.blockBounds, textRenderInfo.chunkedBounds, - textRenderInfo.glyphColors + textRenderInfo.glyphColors, + textRenderInfo.charIndices, + textRenderInfo.wordAndLineIndices, ) // If we had extra sync requests queued up, kick it off @@ -540,6 +544,7 @@ const Text = /*#__PURE__*/(() => { uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent uniforms.uTroikaTotalBounds.value.fromArray(blockBounds) uniforms.uTroikaUseGlyphColors.value = !isOutline && !!textInfo.glyphColors + uniforms.uTroikaCharWordLineTotals.value.set(textInfo.totalChars, textInfo.totalWords, textInfo.totalLines) let distanceOffset = 0 let blurRadius = 0 diff --git a/packages/troika-three-text/src/TextBuilder.js b/packages/troika-three-text/src/TextBuilder.js index 4418143e..f08c326d 100644 --- a/packages/troika-three-text/src/TextBuilder.js +++ b/packages/troika-three-text/src/TextBuilder.js @@ -194,34 +194,35 @@ function getTextRenderInfo(args, callback) { } // Invoke callback with the text layout arrays and updated texture - callback(Object.freeze({ + const textRenderInfo = { parameters: args, sdfTexture: atlas.sdfTexture, sdfGlyphSize, - sdfExponent, - glyphBounds: result.glyphBounds, - glyphAtlasIndices: result.glyphAtlasIndices, - glyphColors: result.glyphColors, - caretPositions: result.caretPositions, - caretHeight: result.caretHeight, - chunkedBounds: result.chunkedBounds, - ascender: result.ascender, - descender: result.descender, - lineHeight: result.lineHeight, - topBaseline: result.topBaseline, - blockBounds: result.blockBounds, - visibleBounds: result.visibleBounds, - timings: result.timings, - get totalBounds() { - console.log('totalBounds deprecated, use blockBounds instead') - return result.blockBounds - }, - get totalBlockSize() { - console.log('totalBlockSize deprecated, use blockBounds instead') - const [x0, y0, x1, y1] = result.blockBounds - return [x1 - x0, y1 - y0] - } - })) + sdfExponent + } + ;[ //copy specific props from result object + 'glyphBounds', + 'glyphAtlasIndices', + 'glyphColors', + 'caretPositions', + 'caretHeight', + 'chunkedBounds', + 'charIndices', + 'wordAndLineIndices', + 'totalChars', + 'totalWords', + 'totalLines', + 'ascender', + 'descender', + 'lineHeight', + 'topBaseline', + 'blockBounds', + 'visibleBounds', + 'timings' + ].forEach(prop => { + textRenderInfo[prop] = result[prop] + }) + callback(Object.freeze(textRenderInfo)) }) } diff --git a/packages/troika-three-text/src/TextDerivedMaterial.js b/packages/troika-three-text/src/TextDerivedMaterial.js index 5bade4ec..d31c5e93 100644 --- a/packages/troika-three-text/src/TextDerivedMaterial.js +++ b/packages/troika-three-text/src/TextDerivedMaterial.js @@ -1,5 +1,37 @@ import { createDerivedMaterial, voidMainRegExp } from 'troika-three-utils' -import { Color, Vector2, Vector4, Matrix3 } from 'three' +import { Color, Vector2, Vector4, Matrix3, Vector3 } from 'three' + +const CHAR_WORD_LINE_VAR_DECLS = ` +float charIndex; +float totalChars; +float charInWordIndex; +float totalCharsInWord; +float charInLineIndex; +float totalCharsInLine; +float wordIndex; +float totalWords; +float wordInLineIndex; +float totalWordsInLine; +float lineIndex; +float totalLines; +` + +const CHAR_WORD_LINE_VAR_ASSIGNMENTS = ` +totalChars = uTroikaCharWordLineTotals.x; +totalWords = uTroikaCharWordLineTotals.y; +totalLines = uTroikaCharWordLineTotals.z; + +charIndex = vTroikaCharIndices.x; +charInLineIndex = vTroikaCharIndices.y; +totalCharsInLine = vTroikaCharIndices.z; +charInWordIndex = floor(vTroikaCharIndices.w / 256.0 + 0.5); +totalCharsInWord = mod(vTroikaCharIndices.w, 256.0); + +wordIndex = vTroikaWordLineIndices.x; +wordInLineIndex = vTroikaWordLineIndices.y; +totalWordsInLine = vTroikaWordLineIndices.z; +lineIndex = vTroikaWordLineIndices.w; +` // language=GLSL const VERTEX_DEFS = ` @@ -13,14 +45,19 @@ uniform float uTroikaDistanceOffset; uniform float uTroikaBlurRadius; uniform vec2 uTroikaPositionOffset; uniform float uTroikaCurveRadius; +uniform vec3 uTroikaCharWordLineTotals; attribute vec4 aTroikaGlyphBounds; -attribute float aTroikaGlyphIndex; +attribute float aTroikaGlyphAtlasIndex; attribute vec3 aTroikaGlyphColor; +attribute vec4 aTroikaCharIndices; +attribute vec4 aTroikaWordLineIndices; varying vec2 vTroikaGlyphUV; varying vec4 vTroikaTextureUVBounds; varying float vTroikaTextureChannel; varying vec3 vTroikaGlyphColor; varying vec2 vTroikaGlyphDimensions; +varying vec4 vTroikaCharIndices; +varying vec4 vTroikaWordLineIndices; ` // language=GLSL prefix="void main() {" suffix="}" @@ -64,11 +101,11 @@ ${''/* NOTE: it seems important to calculate the glyph's bounding texture UVs he float txCols = uTroikaSDFTextureSize.x / uTroikaSDFGlyphSize; vec2 txUvPerSquare = uTroikaSDFGlyphSize / uTroikaSDFTextureSize; vec2 txStartUV = txUvPerSquare * vec2( - mod(floor(aTroikaGlyphIndex / 4.0), txCols), - floor(floor(aTroikaGlyphIndex / 4.0) / txCols) + mod(floor(aTroikaGlyphAtlasIndex / 4.0), txCols), + floor(floor(aTroikaGlyphAtlasIndex / 4.0) / txCols) ); vTroikaTextureUVBounds = vec4(txStartUV, vec2(txStartUV) + txUvPerSquare); -vTroikaTextureChannel = mod(aTroikaGlyphIndex, 4.0); +vTroikaTextureChannel = mod(aTroikaGlyphAtlasIndex, 4.0); ` // language=GLSL @@ -84,11 +121,14 @@ uniform float uTroikaBlurRadius; uniform vec3 uTroikaStrokeColor; uniform float uTroikaStrokeWidth; uniform float uTroikaStrokeOpacity; +uniform vec3 uTroikaCharWordLineTotals; uniform bool uTroikaSDFDebug; varying vec2 vTroikaGlyphUV; varying vec4 vTroikaTextureUVBounds; varying float vTroikaTextureChannel; varying vec2 vTroikaGlyphDimensions; +varying vec4 vTroikaCharIndices; +varying vec4 vTroikaWordLineIndices; float troikaSdfValueToSignedDistance(float alpha) { // Inverse of encoding in SDFGenerator.js @@ -230,11 +270,18 @@ export function createTextDerivedMaterial(baseMaterial) { uTroikaStrokeOpacity: {value: 1}, uTroikaOrient: {value: new Matrix3()}, uTroikaUseGlyphColors: {value: true}, + uTroikaCharWordLineTotals: {value: new Vector3()}, uTroikaSDFDebug: {value: false} }, vertexDefs: VERTEX_DEFS, + vertexMainIntro: ` + vTroikaCharIndices = aTroikaCharIndices; + vTroikaWordLineIndices = aTroikaWordLineIndices; + ${CHAR_WORD_LINE_VAR_ASSIGNMENTS} + `, vertexTransform: VERTEX_TRANSFORM, fragmentDefs: FRAGMENT_DEFS, + fragmentMainIntro: CHAR_WORD_LINE_VAR_ASSIGNMENTS, fragmentColorTransform: FRAGMENT_TRANSFORM, customRewriter({vertexShader, fragmentShader}) { let uDiffuseRE = /\buniform\s+vec3\s+diffuse\b/ @@ -251,6 +298,10 @@ export function createTextDerivedMaterial(baseMaterial) { ) } } + + vertexShader = `${CHAR_WORD_LINE_VAR_DECLS}\n${vertexShader}` + fragmentShader = `${CHAR_WORD_LINE_VAR_DECLS}\n${fragmentShader}` + return { vertexShader, fragmentShader } } }) diff --git a/packages/troika-three-text/src/worker/FontProcessor.js b/packages/troika-three-text/src/worker/FontProcessor.js index 7477787c..dc8bc20c 100644 --- a/packages/troika-three-text/src/worker/FontProcessor.js +++ b/packages/troika-three-text/src/worker/FontProcessor.js @@ -180,6 +180,8 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { anchorX = 0, anchorY = 0, includeCaretPositions=false, + includeCharIndices=false, + includeWordAndLineIndices=false, chunkedBoundsSize=8192, colorRanges=null }, @@ -209,6 +211,9 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { let glyphBounds = null let glyphAtlasIndices = null let glyphColors = null + let charIndices = null + let wordAndLineIndices = null + let totalChars = 0, totalWords = 0, totalLines = 0 let caretPositions = null let visibleBounds = null let chunkedBounds = null @@ -354,6 +359,8 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { // collecting all renderable glyphs into a single collection. glyphBounds = new Float32Array(renderableGlyphCount * 4) glyphAtlasIndices = new Float32Array(renderableGlyphCount) + charIndices = includeCharIndices && new Float32Array(renderableGlyphCount * 4) //index-overall, index-in-line, total-in-line, index-in-word+total-in-word + wordAndLineIndices = includeWordAndLineIndices && new Float32Array(renderableGlyphCount * 4) //word-overall, word-in-line, total-in-line, line visibleBounds = [INF, INF, -INF, -INF] chunkedBounds = [] let lineYOffset = topBaseline @@ -368,11 +375,20 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { let colorCharIndex = -1 let chunk let currentColor + let charIndex = 0 + let charInWordIndex = 0 + let charsInWordCount = 0 + let charInLineIndex = 0 + let wordIndex = 0 + let wordInLineIndex = 0 + let lineIndex = 0 lines.forEach(line => { const {count:lineGlyphCount, width:lineWidth} = line // Ignore empty lines if (lineGlyphCount > 0) { + totalLines++ + // Find x offset for horizontal alignment let lineXOffset = 0 let justifyAdjust = 0 @@ -400,6 +416,21 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { justifyAdjust = (maxLineWidth - lineWidth) / whitespaceCount } + // Count visible chars and words in full line + let charsInLineCount = 0 + let wordsInLineCount = 0 + if (includeCharIndices || includeWordAndLineIndices) { + for (let i = 0; i < lineGlyphCount; i++) { + const glyphInfo = line.glyphAt(i) + if (!glyphInfo.glyphObj.isWhitespace) { + charsInLineCount++ + if (i === 0 || line.glyphAt(i - 1).glyphObj.isWhitespace) { + wordsInLineCount++ + } + } + } + } + for (let i = 0; i < lineGlyphCount; i++) { let glyphInfo = line.glyphAt(i) const glyphObj = glyphInfo.glyphObj @@ -456,6 +487,7 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { // Get atlas data for renderable glyphs if (!glyphObj.isWhitespace && !glyphObj.isEmpty) { + totalChars++ const idx = renderableGlyphIndex++ // If we haven't seen this glyph yet, generate its SDF @@ -515,6 +547,34 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { // Add to atlas indices array glyphAtlasIndices[idx] = glyphAtlasInfo.atlasIndex + // Char/word/line indices + if (includeCharIndices) { + // If this is the start of a word, step forward to count total chars in the word + if (charInWordIndex === 0) { + totalWords++ + for (let j = i; j < lineGlyphCount; j++) { + if (line.glyphAt(j).glyphObj.isWhitespace) { + break + } + charsInWordCount++ + } + glyphInfo = line.glyphAt(i) //restore flyweight + } + + // index-overall, index-in-line, total-in-line, index-in-word+total-in-word + charIndices[idx * 4] = charIndex++ + charIndices[idx * 4 + 1] = charInLineIndex++ + charIndices[idx * 4 + 2] = charsInLineCount + charIndices[idx * 4 + 3] = (Math.min(256, charInWordIndex++) << 8) | Math.min(256, charsInWordCount) + } + if (includeWordAndLineIndices) { + //word-overall, word-in-line, total-words-in-line, line + wordAndLineIndices[idx * 4] = wordIndex + wordAndLineIndices[idx * 4 + 1] = wordInLineIndex + wordAndLineIndices[idx * 4 + 2] = wordsInLineCount + wordAndLineIndices[idx * 4 + 3] = lineIndex + } + // Add colors if (colorRanges) { const start = idx * 3 @@ -522,12 +582,23 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { glyphColors[start + 1] = currentColor >> 8 & 255 glyphColors[start + 2] = currentColor & 255 } + } else { + // End of word + wordIndex++ + wordInLineIndex++ + charInWordIndex = 0 + charsInWordCount = 0 } } } // Increment y offset for next line lineYOffset -= lineHeight + + lineIndex++ + wordInLineIndex = 0 + charInLineIndex = 0 + charInWordIndex = 0 }) } @@ -545,6 +616,11 @@ export function createFontProcessor(fontParser, sdfGenerator, config) { caretHeight, //height of cursor from bottom to top glyphColors, //color for each glyph, if color ranges supplied chunkedBounds, //total rects per (n=chunkedBoundsSize) consecutive glyphs + charIndices, + wordAndLineIndices, + totalChars, + totalWords, + totalLines, ascender: ascender * fontSizeMult, //font ascender descender: descender * fontSizeMult, //font descender lineHeight, //computed line height