download
raw
10.4 kB
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.