import { createDerivedMaterial, voidMainRegExp } from 'troika-three-utils' import { Color, Vector2, Vector4, Matrix3 } from 'three' // language=GLSL const VERTEX_DEFS = ` uniform vec2 uTroikaSDFTextureSize; uniform float uTroikaSDFGlyphSize; uniform vec4 uTroikaTotalBounds; uniform vec4 uTroikaClipRect; uniform mat3 uTroikaOrient; uniform bool uTroikaUseGlyphColors; uniform float uTroikaEdgeOffset; uniform float uTroikaBlurRadius; uniform vec2 uTroikaPositionOffset; uniform float uTroikaCurveRadius; attribute vec4 aTroikaGlyphBounds; attribute float aTroikaGlyphIndex; attribute vec3 aTroikaGlyphColor; varying vec2 vTroikaGlyphUV; varying vec4 vTroikaTextureUVBounds; varying float vTroikaTextureChannel; varying vec3 vTroikaGlyphColor; varying vec2 vTroikaGlyphDimensions; ` // language=GLSL prefix="void main() {" suffix="}" const VERTEX_TRANSFORM = ` vec4 bounds = aTroikaGlyphBounds; bounds.xz += uTroikaPositionOffset.x; bounds.yw -= uTroikaPositionOffset.y; vec4 outlineBounds = vec4( bounds.xy - uTroikaEdgeOffset - uTroikaBlurRadius, bounds.zw + uTroikaEdgeOffset + uTroikaBlurRadius ); vec4 clippedBounds = vec4( clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw), clamp(outlineBounds.zw, uTroikaClipRect.xy, uTroikaClipRect.zw) ); vec2 clippedXY = (mix(clippedBounds.xy, clippedBounds.zw, position.xy) - bounds.xy) / (bounds.zw - bounds.xy); position.xy = mix(bounds.xy, bounds.zw, clippedXY); uv = (position.xy - uTroikaTotalBounds.xy) / (uTroikaTotalBounds.zw - uTroikaTotalBounds.xy); float rad = uTroikaCurveRadius; if (rad != 0.0) { float angle = position.x / rad; position.xz = vec2(sin(angle) * rad, rad - cos(angle) * rad); normal.xz = vec2(sin(angle), cos(angle)); } position = uTroikaOrient * position; normal = uTroikaOrient * normal; vTroikaGlyphUV = clippedXY.xy; vTroikaGlyphDimensions = vec2(bounds[2] - bounds[0], bounds[3] - bounds[1]); ${''/* NOTE: it seems important to calculate the glyph's bounding texture UVs here in the vertex shader, rather than in the fragment shader, as the latter gives strange artifacts on some glyphs (those in the leftmost texture column) on some systems. The exact reason isn't understood but doing this here, then mix()-ing in the fragment shader, seems to work. */} 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) ); vTroikaTextureUVBounds = vec4(txStartUV, vec2(txStartUV) + txUvPerSquare); vTroikaTextureChannel = mod(aTroikaGlyphIndex, 4.0); ` // language=GLSL const FRAGMENT_DEFS = ` uniform sampler2D uTroikaSDFTexture; uniform vec2 uTroikaSDFTextureSize; uniform float uTroikaSDFGlyphSize; uniform float uTroikaSDFExponent; uniform float uTroikaEdgeOffset; uniform float uTroikaFillOpacity; uniform float uTroikaBlurRadius; uniform vec3 uTroikaStrokeColor; uniform float uTroikaStrokeWidth; uniform float uTroikaStrokeOpacity; uniform bool uTroikaSDFDebug; varying vec2 vTroikaGlyphUV; varying vec4 vTroikaTextureUVBounds; varying float vTroikaTextureChannel; varying vec2 vTroikaGlyphDimensions; float troikaSdfValueToSignedDistance(float alpha) { // Inverse of exponential encoding in webgl-sdf-generator ${''/* TODO - there's some slight inaccuracy here when dealing with interpolated alpha values; those are linearly interpolated where the encoding is exponential. Look into improving this by rounding to nearest 2 whole texels, decoding those exponential values, and linearly interpolating the result. */} float maxDimension = max(vTroikaGlyphDimensions.x, vTroikaGlyphDimensions.y); float absDist = (1.0 - pow(2.0 * (alpha > 0.5 ? 1.0 - alpha : alpha), 1.0 / uTroikaSDFExponent)) * maxDimension; float signedDist = absDist * (alpha > 0.5 ? -1.0 : 1.0); return signedDist; } float troikaGlyphUvToSdfValue(vec2 glyphUV) { vec2 textureUV = mix(vTroikaTextureUVBounds.xy, vTroikaTextureUVBounds.zw, glyphUV); vec4 rgba = texture2D(uTroikaSDFTexture, textureUV); float ch = floor(vTroikaTextureChannel + 0.5); //NOTE: can't use round() in WebGL1 return ch == 0.0 ? rgba.r : ch == 1.0 ? rgba.g : ch == 2.0 ? rgba.b : rgba.a; } float troikaGlyphUvToDistance(vec2 uv) { return troikaSdfValueToSignedDistance(troikaGlyphUvToSdfValue(uv)); } float troikaGetAADist() { ${''/* When the standard derivatives extension is available, we choose an antialiasing alpha threshold based on the potential change in the SDF's alpha from this fragment to its neighbor. This strategy maximizes readability and edge crispness at all sizes and screen resolutions. */} #if defined(GL_OES_standard_derivatives) || __VERSION__ >= 300 return length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.5; #else return vTroikaGlyphDimensions.x / 64.0; #endif } float troikaGetFragDistValue() { vec2 clampedGlyphUV = clamp(vTroikaGlyphUV, 0.5 / uTroikaSDFGlyphSize, 1.0 - 0.5 / uTroikaSDFGlyphSize); float distance = troikaGlyphUvToDistance(clampedGlyphUV); // Extrapolate distance when outside bounds: distance += clampedGlyphUV == vTroikaGlyphUV ? 0.0 : length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions); ${''/* // TODO more refined extrapolated distance by adjusting for angle of gradient at edge... // This has potential but currently gives very jagged extensions, maybe due to precision issues? float uvStep = 1.0 / uTroikaSDFGlyphSize; vec2 neighbor1UV = clampedGlyphUV + ( vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * sign(0.5 - vTroikaGlyphUV.y)) : vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * sign(0.5 - vTroikaGlyphUV.x), 0.0) : vec2(0.0) ); vec2 neighbor2UV = clampedGlyphUV + ( vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * -sign(0.5 - vTroikaGlyphUV.y)) : vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * -sign(0.5 - vTroikaGlyphUV.x), 0.0) : vec2(0.0) ); float neighbor1Distance = troikaGlyphUvToDistance(neighbor1UV); float neighbor2Distance = troikaGlyphUvToDistance(neighbor2UV); float distToUnclamped = length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions); float distToNeighbor = length((clampedGlyphUV - neighbor1UV) * vTroikaGlyphDimensions); float gradientAngle1 = min(asin(abs(neighbor1Distance - distance) / distToNeighbor), PI / 2.0); float gradientAngle2 = min(asin(abs(neighbor2Distance - distance) / distToNeighbor), PI / 2.0); distance += (cos(gradientAngle1) + cos(gradientAngle2)) / 2.0 * distToUnclamped; */} return distance; } float troikaGetEdgeAlpha(float distance, float distanceOffset, float aaDist) { #if defined(IS_DEPTH_MATERIAL) || defined(IS_DISTANCE_MATERIAL) float alpha = step(-distanceOffset, -distance); #else float alpha = smoothstep( distanceOffset + aaDist, distanceOffset - aaDist, distance ); #endif return alpha; } ` // language=GLSL prefix="void main() {" suffix="}" const FRAGMENT_TRANSFORM = ` float aaDist = troikaGetAADist(); float fragDistance = troikaGetFragDistValue(); float edgeAlpha = uTroikaSDFDebug ? troikaGlyphUvToSdfValue(vTroikaGlyphUV) : troikaGetEdgeAlpha(fragDistance, uTroikaEdgeOffset, max(aaDist, uTroikaBlurRadius)); #if !defined(IS_DEPTH_MATERIAL) && !defined(IS_DISTANCE_MATERIAL) vec4 fillRGBA = gl_FragColor; fillRGBA.a *= uTroikaFillOpacity; vec4 strokeRGBA = uTroikaStrokeWidth == 0.0 ? fillRGBA : vec4(uTroikaStrokeColor, uTroikaStrokeOpacity); if (fillRGBA.a == 0.0) fillRGBA.rgb = strokeRGBA.rgb; gl_FragColor = mix(fillRGBA, strokeRGBA, smoothstep( -uTroikaStrokeWidth - aaDist, -uTroikaStrokeWidth + aaDist, fragDistance )); gl_FragColor.a *= edgeAlpha; #endif if (edgeAlpha == 0.0) { discard; } ` /** * Create a material for rendering text, derived from a baseMaterial */ export function createTextDerivedMaterial(baseMaterial) { const textMaterial = createDerivedMaterial(baseMaterial, { chained: true, extensions: { derivatives: true }, uniforms: { uTroikaSDFTexture: {value: null}, uTroikaSDFTextureSize: {value: new Vector2()}, uTroikaSDFGlyphSize: {value: 0}, uTroikaSDFExponent: {value: 0}, uTroikaTotalBounds: {value: new Vector4(0,0,0,0)}, uTroikaClipRect: {value: new Vector4(0,0,0,0)}, uTroikaEdgeOffset: {value: 0}, uTroikaFillOpacity: {value: 1}, uTroikaPositionOffset: {value: new Vector2()}, uTroikaCurveRadius: {value: 0}, uTroikaBlurRadius: {value: 0}, uTroikaStrokeWidth: {value: 0}, uTroikaStrokeColor: {value: new Color()}, uTroikaStrokeOpacity: {value: 1}, uTroikaOrient: {value: new Matrix3()}, uTroikaUseGlyphColors: {value: true}, uTroikaSDFDebug: {value: false} }, vertexDefs: VERTEX_DEFS, vertexTransform: VERTEX_TRANSFORM, fragmentDefs: FRAGMENT_DEFS, fragmentColorTransform: FRAGMENT_TRANSFORM, customRewriter({vertexShader, fragmentShader}) { let uDiffuseRE = /\buniform\s+vec3\s+diffuse\b/ if (uDiffuseRE.test(fragmentShader)) { // Replace all instances of `diffuse` with our varying fragmentShader = fragmentShader .replace(uDiffuseRE, 'varying vec3 vTroikaGlyphColor') .replace(/\bdiffuse\b/g, 'vTroikaGlyphColor') // Make sure the vertex shader declares the uniform so we can grab it as a fallback if (!uDiffuseRE.test(vertexShader)) { vertexShader = vertexShader.replace( voidMainRegExp, 'uniform vec3 diffuse;\n$&\nvTroikaGlyphColor = uTroikaUseGlyphColors ? aTroikaGlyphColor / 255.0 : diffuse;\n' ) } } return { vertexShader, fragmentShader } } }) // Force transparency - TODO is this reasonable? textMaterial.transparent = true // Force single draw call when double-sided textMaterial.forceSinglePass = true Object.defineProperties(textMaterial, { isTroikaTextMaterial: {value: true}, // WebGLShadowMap reverses the side of the shadow material by default, which fails // for planes, so here we force the `shadowSide` to always match the main side. shadowSide: { get() { return this.side }, set() { //no-op } } }) return textMaterial }