Buckets:
ktongue/docker_container / simsite /frontend /node_modules /troika-three-text /src /TextDerivedMaterial.js
| 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 | |
| } | |
Xet Storage Details
- Size:
- 10.4 kB
- Xet hash:
- b0fcdc93eb1ffc5b78b4d4da034950fd8d78caaa166eec6439310b41e20a5c19
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.