Skip to content

Commit b5c9138

Browse files
committed
feat(troika-three-text): integrate webgl-sdf-generator for GPU-accelerated SDF generation
This gives a massive performance increase where supported.
1 parent 7bf9c5c commit b5c9138

File tree

8 files changed

+158
-532
lines changed

8 files changed

+158
-532
lines changed

packages/troika-examples/text-rtl/TextExample.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const FONTS = {
2121
'Cairo': 'https://fonts.gstatic.com/s/cairo/v10/SLXGc1nY6HkvamIl.woff',
2222
'Lemonada': 'https://fonts.gstatic.com/s/lemonada/v12/0QI-MXFD9oygTWy_R-FFlwV-bgfR7QJGeut2mg.woff',
2323
'Mirza': 'https://fonts.gstatic.com/s/mirza/v10/co3ImWlikiN5Eure.woff',
24+
'Noto Sans Arabic': 'https://fonts.gstatic.com/s/notosansarabic/v13/nwpxtLGrOAZMl5nJ_wfgRg3DrWFZWsnVBJ_sS6tlqHHFlhQ5l3sQWIHPqzCfyGyvuA.woff',
2425
//'Reem Kufi': 'https://fonts.gstatic.com/s/reemkufi/v10/2sDcZGJLip7W2J7v7wQDbA.woff',
2526
'Scheherazade': 'https://fonts.gstatic.com/s/scheherazade/v20/YA9Ur0yF4ETZN60keViq1kQgtA.woff',
2627
'Tinos (Hebrew)': 'https://fonts.gstatic.com/s/tinos/v16/buE4poGnedXvwgX_.woff',

packages/troika-three-text/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"dependencies": {
1717
"bidi-js": "^1.0.2",
1818
"troika-three-utils": "^0.45.0",
19-
"troika-worker-utils": "^0.45.0"
19+
"troika-worker-utils": "^0.45.0",
20+
"webgl-sdf-generator": "1.1.1"
2021
},
2122
"peerDependencies": {
2223
"three": ">=0.103.0"

packages/troika-three-text/src/TextBuilder.js

+40-103
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Color, DataTexture, LinearFilter, RGBAFormat } from 'three'
2-
import { defineWorkerModule, terminateWorker, ThenableWorkerModule, Thenable } from 'troika-worker-utils'
3-
import { createSDFGenerator } from './worker/SDFGenerator.js'
1+
import { Color, CanvasTexture, DataTexture, LinearFilter } from 'three'
2+
import { defineWorkerModule, ThenableWorkerModule, Thenable } from 'troika-worker-utils'
43
import { createTypesetter } from './worker/Typesetter.js'
5-
import { createGlyphSegmentsIndex } from './worker/GlyphSegmentsIndex.js'
4+
import { generateSDF, warmUpSDFCanvas, resizeWebGLCanvasWithoutClearing } from './worker/SDFGenerator.js'
5+
66
import bidiFactory from 'bidi-js'
77

88
// Choose parser impl:
@@ -150,16 +150,16 @@ function getTextRenderInfo(args, callback) {
150150
// Init the atlas if needed
151151
const {textureWidth, sdfExponent} = CONFIG
152152
const {sdfGlyphSize} = args
153+
const glyphsPerRow = (textureWidth / sdfGlyphSize * 4)
153154
let atlas = atlases[sdfGlyphSize]
154155
if (!atlas) {
156+
const canvas = document.createElement('canvas')
157+
canvas.width = textureWidth
158+
canvas.height = sdfGlyphSize * 256 / glyphsPerRow // start tall enough to fit 256 glyphs
155159
atlas = atlases[sdfGlyphSize] = {
156160
glyphCount: 0,
157-
sdfTexture: new DataTexture(
158-
new Uint8Array(sdfGlyphSize * textureWidth * 4),
159-
textureWidth,
160-
sdfGlyphSize,
161-
RGBAFormat,
162-
undefined,
161+
sdfTexture: new CanvasTexture(
162+
canvas,
163163
undefined,
164164
undefined,
165165
undefined,
@@ -170,6 +170,7 @@ function getTextRenderInfo(args, callback) {
170170
}
171171
}
172172

173+
const {sdfTexture} = atlas
173174
let fontGlyphs = atlas.glyphsByFont.get(args.font)
174175
if (!fontGlyphs) {
175176
atlas.glyphsByFont.set(args.font, fontGlyphs = new Map())
@@ -228,52 +229,37 @@ function getTextRenderInfo(args, callback) {
228229
const sdfStart = now()
229230
timings.sdf = {}
230231

232+
// Grow the texture height by power of 2 if needed
233+
const currentHeight = sdfTexture.image.height
234+
const neededRows = Math.ceil(atlas.glyphCount / glyphsPerRow)
235+
const neededHeight = Math.pow(2, Math.ceil(Math.log2(neededRows * sdfGlyphSize)))
236+
if (neededHeight > currentHeight) {
237+
// Since resizing the canvas clears its render buffer, it needs special handling to copy the old contents over
238+
console.info(`Increasing SDF texture size ${currentHeight}->${neededHeight}`)
239+
resizeWebGLCanvasWithoutClearing(sdfTexture.image, textureWidth, neededHeight)
240+
}
241+
231242
Thenable.all(neededSDFs.map(({path, atlasIndex, sdfViewBox}) => {
232243
const maxDist = Math.max(sdfViewBox[2] - sdfViewBox[0], sdfViewBox[3] - sdfViewBox[1])
233-
return generateSDFInWorker(sdfGlyphSize, sdfGlyphSize, path, sdfViewBox, maxDist, CONFIG.sdfExponent)
234-
.then(({textureData, timing}) => {
244+
const squareIndex = Math.floor(atlasIndex / 4)
245+
const x = squareIndex % (textureWidth / sdfGlyphSize) * sdfGlyphSize
246+
const y = Math.floor(squareIndex / (textureWidth / sdfGlyphSize)) * sdfGlyphSize
247+
const channel = atlasIndex % 4
248+
return generateSDF(sdfGlyphSize, sdfGlyphSize, path, sdfViewBox, maxDist, CONFIG.sdfExponent, sdfTexture.image, x, y, channel)
249+
.then(({timing}) => {
235250
timings.sdf[atlasIndex] = timing
236-
return { atlasIndex, textureData, timing }
237-
})
238-
})).then(sdfResults => {
239-
// If we have new SDFs, copy them into the atlas texture at the specified indices
240-
if (sdfResults.length) {
241-
sdfResults.forEach(({ atlasIndex, textureData }) => {
242-
const texImg = atlas.sdfTexture.image
243-
244-
// Grow the texture by power of 2 if needed
245-
while (texImg.data.length < (atlasIndex + 1) * sdfGlyphSize * sdfGlyphSize) {
246-
const biggerArray = new Uint8Array(texImg.data.length * 2)
247-
biggerArray.set(texImg.data)
248-
texImg.data = biggerArray
249-
texImg.height *= 2
250-
}
251-
252-
// Insert the new glyph's data into the full texture image at the correct offsets
253-
// Glyphs are packed sequentially into the R,G,B,A channels of a square, advancing
254-
// to the next square every 4 glyphs.
255-
const squareIndex = Math.floor(atlasIndex / 4)
256-
const cols = texImg.width / sdfGlyphSize
257-
const baseStartIndex = Math.floor(squareIndex / cols) * texImg.width * sdfGlyphSize * 4 //full rows
258-
+ (squareIndex % cols) * sdfGlyphSize * 4 //partial row
259-
+ (atlasIndex % 4) //color channel
260-
for (let y = 0; y < sdfGlyphSize; y++) {
261-
const srcStartIndex = y * sdfGlyphSize
262-
const rowStartIndex = baseStartIndex + (y * texImg.width * 4)
263-
for (let x = 0; x < sdfGlyphSize; x++) {
264-
texImg.data[rowStartIndex + x * 4] = textureData[srcStartIndex + x]
265-
}
266-
}
267251
})
268-
atlas.sdfTexture.needsUpdate = true
269-
}
252+
})).then(() => {
270253
timings.sdfTotal = now() - sdfStart
271254
timings.total = now() - totalStart
255+
if (neededSDFs.length) {
256+
sdfTexture.needsUpdate = true
257+
}
272258

273259
// Invoke callback with the text layout arrays and updated texture
274260
callback(Object.freeze({
275261
parameters: args,
276-
sdfTexture: atlas.sdfTexture,
262+
sdfTexture,
277263
sdfGlyphSize,
278264
sdfExponent,
279265
glyphBounds,
@@ -301,6 +287,11 @@ function getTextRenderInfo(args, callback) {
301287
}))
302288
})
303289
})
290+
291+
// While the typesetting request is being handled, go ahead and make sure the atlas canvas context is
292+
// "warmed up"; the first request will be the longest due to shader program compilation so this gets
293+
// a head start on that process before SDFs actually start getting processed.
294+
Thenable.all([]).then(() => warmUpSDFCanvas(sdfTexture.image))
304295
}
305296

306297

@@ -361,53 +352,6 @@ const typesetterWorkerModule = /*#__PURE__*/defineWorkerModule({
361352
}
362353
})
363354

364-
/**
365-
* SDF generator function wrapper that fans out requests to a number of worker
366-
* threads for parallelism
367-
*/
368-
const generateSDFInWorker = /*#__PURE__*/function() {
369-
const threadCount = 4 //how many workers to spawn
370-
const idleTimeout = 2000 //workers will be terminated after being idle this many milliseconds
371-
const threads = {}
372-
let callNum = 0
373-
return function(...args) {
374-
const workerId = 'TroikaTextSDFGenerator_' + ((callNum++) % threadCount)
375-
let thread = threads[workerId]
376-
if (!thread) {
377-
thread = threads[workerId] = {
378-
workerModule: defineWorkerModule({
379-
name: workerId,
380-
workerId,
381-
dependencies: [
382-
CONFIG,
383-
createGlyphSegmentsIndex,
384-
createSDFGenerator
385-
],
386-
init(config, createGlyphSegmentsIndex, createSDFGenerator) {
387-
const {sdfExponent, sdfMargin} = config
388-
return createSDFGenerator(createGlyphSegmentsIndex, { sdfExponent, sdfMargin })
389-
},
390-
getTransferables(result) {
391-
return [result.textureData.buffer]
392-
}
393-
}),
394-
requests: 0,
395-
idleTimer: null
396-
}
397-
}
398-
399-
thread.requests++
400-
clearTimeout(thread.idleTimer)
401-
return thread.workerModule(...args)
402-
.then(result => {
403-
if (--thread.requests === 0) {
404-
thread.idleTimer = setTimeout(() => { terminateWorker(workerId) }, idleTimeout)
405-
}
406-
return result
407-
})
408-
}
409-
}()
410-
411355
const typesetInWorker = /*#__PURE__*/defineWorkerModule({
412356
name: 'Typesetter',
413357
dependencies: [
@@ -438,16 +382,9 @@ const typesetInWorker = /*#__PURE__*/defineWorkerModule({
438382
})
439383

440384
function dumpSDFTextures() {
441-
Object.keys(atlases).forEach(font => {
442-
const atlas = atlases[font]
443-
const canvas = document.createElement('canvas')
444-
const {width, height, data} = atlas.sdfTexture.image
445-
canvas.width = width
446-
canvas.height = height
447-
const imgData = new ImageData(new Uint8ClampedArray(data), width, height)
448-
const ctx = canvas.getContext('2d')
449-
ctx.putImageData(imgData, 0, 0)
450-
console.log(font, atlas, canvas.toDataURL())
385+
Object.keys(atlases).forEach(size => {
386+
const canvas = atlases[size].sdfTexture.image
387+
const {width, height} = canvas
451388
console.log("%c.", `
452389
background: url(${canvas.toDataURL()});
453390
background-size: ${width}px ${height}px;

packages/troika-three-text/src/TextDerivedMaterial.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ varying float vTroikaTextureChannel;
9191
varying vec2 vTroikaGlyphDimensions;
9292
9393
float troikaSdfValueToSignedDistance(float alpha) {
94-
// Inverse of encoding in SDFGenerator.js
94+
// Inverse of exponential encoding in webgl-sdf-generator
9595
${''/* TODO - there's some slight inaccuracy here when dealing with interpolated alpha values; those
9696
are linearly interpolated where the encoding is exponential. Look into improving this by rounding
9797
to nearest 2 whole texels, decoding those exponential values, and linearly interpolating the result.

packages/troika-three-text/src/worker/GlyphSegmentsIndex.js

-102
This file was deleted.

0 commit comments

Comments
 (0)