Skip to content

Commit

Permalink
feat(troika-three-text): text outline and better antialiasing at smal…
Browse files Browse the repository at this point in the history
…l sizes

Added outlineWidth and outlineColor props; adding an outline in a
contrasting color can improve readability against backgrounds with low
or varying contrast.

This implementation involved reworking the SDF encoding to fill the
entire texture, and an exponential encoding scale to maintain precision
near the glyph paths. The antialiasing is also now based on decoded
distance values rather than UV, improving the antialiasing crispness at
small sizes.
  • Loading branch information
lojjic committed Oct 19, 2020
1 parent f3340ec commit 3836809
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 103 deletions.
2 changes: 2 additions & 0 deletions packages/troika-3d-text/src/facade/Text3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const TEXT_MESH_PROPS = [
'material',
'color',
'colorRanges',
'outlineWidth',
'outlineColor',
'depthOffset',
'clipRect',
'orientation',
Expand Down
9 changes: 6 additions & 3 deletions packages/troika-examples/text/TextExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class TextExample extends React.Component {
anchorX: 'center',
anchorY: 'middle',
color: 0xffffff,
outlineWidth: 0,
fog: false,
animTextColor: true,
animTilt: true,
Expand Down Expand Up @@ -148,8 +149,8 @@ class TextExample extends React.Component {
{type: 'ambient', color: 0x666666},
{
type: 'point',
z: 2,
y: 1,
z: 3,
y: 1.5,
x: 0,
castShadow: state.shadows,
shadow: {
Expand Down Expand Up @@ -187,6 +188,7 @@ class TextExample extends React.Component {
anchorY: state.anchorY,
selectable: state.selectable,
debugSDF: state.debugSDF,
outlineWidth: state.outlineWidth,
material: material,
color: 0xffffff,
scaleX: state.textScale || 1,
Expand Down Expand Up @@ -277,8 +279,9 @@ class TextExample extends React.Component {
{type: 'number', path: "maxWidth", min: 1, max: 5, step: 0.01},
{type: 'number', path: "lineHeight", min: 1, max: 2, step: 0.01},
{type: 'number', path: "letterSpacing", min: -0.1, max: 0.5, step: 0.01},
{type: 'boolean', path: "debugSDF", label: "Show SDF"},
{type: 'number', path: "outlineWidth", min: 0, max: 0.05, step: 0.0001},
{type: 'number', path: "sdfGlyphSize", label: 'SDF size (2^n):', min: 3, max: 8},
{type: 'boolean', path: "debugSDF", label: "Show SDF"},
]
}
] }
Expand Down
19 changes: 18 additions & 1 deletion packages/troika-three-text/src/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ const Text = /*#__PURE__*/(() => {
*/
this.colorRanges = null

/**
* @member {number} outlineWidth
* NOTE: BETA FEATURE, NOT STABLE
* The width, in local units, of an outline drawn around each text glyph using the
* `outlineColor`. Defaults to `0`.
*/
this.outlineWidth = 0

/**
* @member {string|number|THREE.Color} outlineColor
* NOTE: BETA FEATURE, NOT STABLE
* The color of the text outline, if `outlineWidth` is greater than zero. Defaults to black.
*/
this.outlineColor = 0

/**
* @member {number} depthOffset
* This is a shortcut for setting the material's `polygonOffset` and related properties,
Expand Down Expand Up @@ -405,9 +420,11 @@ const Text = /*#__PURE__*/(() => {
uniforms.uTroikaSDFTexture.value = sdfTexture
uniforms.uTroikaSDFTextureSize.value.set(sdfTexture.image.width, sdfTexture.image.height)
uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize
uniforms.uTroikaSDFMinDistancePct.value = textInfo.sdfMinDistancePercent
uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent
uniforms.uTroikaTotalBounds.value.fromArray(blockBounds)
uniforms.uTroikaUseGlyphColors.value = !!textInfo.glyphColors
uniforms.uTroikaOutlineWidth.value = this.outlineWidth || 0
uniforms.uTroikaOutlineColor.value.set(this.outlineColor || 0)

let clipRect = this.clipRect
if (clipRect && Array.isArray(clipRect) && clipRect.length === 4) {
Expand Down
45 changes: 18 additions & 27 deletions packages/troika-three-text/src/TextBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import fontParser from './worker/FontParser_Typr.js'
const CONFIG = {
defaultFontURL: 'https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxM.woff', //Roboto Regular
sdfGlyphSize: 64,
sdfMargin: 1 / 16,
sdfExponent: 9,
textureWidth: 2048
}
const tempColor = /*#__PURE__*/new Color()
Expand All @@ -30,6 +32,13 @@ let hasRequested = false
* Larger sizes can improve the quality of glyph rendering by increasing the sharpness
* of corners and preventing loss of very thin lines, at the expense of memory. Defaults
* to 64 which is generally a good balance of size and quality.
* @param {Number} config.sdfExponent - The exponent used when encoding the SDF values. A higher exponent
* shifts the encoded 8-bit values to achieve higher precision/accuracy at texels nearer
* the glyph's path, with lower precision further away. Defaults to 9.
* @param {Number} config.sdfMargin - How much space to reserve in the SDF as margin outside the glyph's
* path, as a percentage of the SDF width. A larger margin increases the quality of
* extruded glyph outlines, but decreases the precision available for the glyph itself.
* Defaults to 1/16th of the glyph size.
* @param {Number} config.textureWidth - The width of the SDF texture; must be a power of 2. Defaults to
* 2048 which is a safe maximum texture dimension according to the stats at
* https://webglstats.com/webgl/parameter/MAX_TEXTURE_SIZE and should allow for a
Expand All @@ -45,18 +54,6 @@ function configureTextBuilder(config) {
}
}


/**
* The radial distance from glyph edges over which the SDF alpha will be calculated; if the alpha
* at distance:0 is 0.5, then the alpha at this distance will be zero. This is defined as a percentage
* of each glyph's maximum dimension in font space units so that it maps to the same minimum number of
* SDF texels regardless of the glyph's size. A larger value provides greater alpha gradient resolution
* and improves readability/antialiasing quality at small display sizes, but also decreases the number
* of texels available for encoding path details.
*/
const SDF_DISTANCE_PERCENT = 1 / 8


/**
* Repository for all font SDF atlas textures
*
Expand All @@ -72,8 +69,8 @@ const atlases = Object.create(null)
* @typedef {object} TroikaTextRenderInfo - Format of the result from `getTextRenderInfo`.
* @property {object} parameters - The normalized input arguments to the render call.
* @property {DataTexture} sdfTexture - The SDF atlas texture.
* @property {number} sdfGlyphSize - The size of each glyph's SDF.
* @property {number} sdfMinDistancePercent - See `SDF_DISTANCE_PERCENT`
* @property {number} sdfGlyphSize - The size of each glyph's SDF; see `configureTextBuilder`.
* @property {number} sdfExponent - The exponent used in encoding the SDF's values; see `configureTextBuilder`.
* @property {Float32Array} glyphBounds - List of [minX, minY, maxX, maxY] quad bounds for each glyph.
* @property {Float32Array} glyphAtlasIndices - List holding each glyph's index in the SDF atlas.
* @property {Uint8Array} [glyphColors] - List holding each glyph's [r, g, b] color, if `colorRanges` was supplied.
Expand Down Expand Up @@ -109,6 +106,7 @@ const atlases = Object.create(null)
* @param {getTextRenderInfo~callback} callback
*/
function getTextRenderInfo(args, callback) {
hasRequested = true
args = assign({}, args)

// Apply default font here to avoid a 'null' atlas, and convert relative
Expand Down Expand Up @@ -138,7 +136,7 @@ function getTextRenderInfo(args, callback) {
Object.freeze(args)

// Init the atlas for this font if needed
const {textureWidth} = CONFIG
const {textureWidth, sdfExponent} = CONFIG
const {sdfGlyphSize} = args
let atlasKey = `${args.font}@${sdfGlyphSize}`
let atlas = atlases[atlasKey]
Expand Down Expand Up @@ -195,7 +193,7 @@ function getTextRenderInfo(args, callback) {
parameters: args,
sdfTexture: atlas.sdfTexture,
sdfGlyphSize,
sdfMinDistancePercent: SDF_DISTANCE_PERCENT,
sdfExponent,
glyphBounds: result.glyphBounds,
glyphAtlasIndices: result.glyphAtlasIndices,
glyphColors: result.glyphColors,
Expand Down Expand Up @@ -270,22 +268,15 @@ const fontProcessorWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'FontProcessor',
dependencies: [
CONFIG,
SDF_DISTANCE_PERCENT,
fontParser,
createGlyphSegmentsQuadtree,
createSDFGenerator,
createFontProcessor
],
init(config, sdfDistancePercent, fontParser, createGlyphSegmentsQuadtree, createSDFGenerator, createFontProcessor) {
const sdfGenerator = createSDFGenerator(
createGlyphSegmentsQuadtree,
{
sdfDistancePercent
}
)
return createFontProcessor(fontParser, sdfGenerator, {
defaultFontUrl: config.defaultFontURL
})
init(config, fontParser, createGlyphSegmentsQuadtree, createSDFGenerator, createFontProcessor) {
const {sdfExponent, sdfMargin, defaultFontURL} = config
const sdfGenerator = createSDFGenerator(createGlyphSegmentsQuadtree, { sdfExponent, sdfMargin })
return createFontProcessor(fontParser, sdfGenerator, { defaultFontURL })
}
})

Expand Down
Loading

0 comments on commit 3836809

Please sign in to comment.