From 885430e8ce37b255cb9057575ac8d4e0dce37335 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Tue, 9 Jun 2026 21:45:31 -0400 Subject: [PATCH] perf(shaders): move RoundedWithBorder border math from varyings to CPU uniforms All nine border varyings (inner/outer size, border UVs, inner/outer radii, half dimensions) were derived purely from uniforms, so they were constant across the quad yet interpolated per fragment. Compute them once on the CPU in update() (cached by the shader value-key, so they only recompute when border props or node dimensions change) and upload them as uniforms. The fragment shader now carries the same 3 varyings as the plain Rounded shader, and with a zero-width border its per-pixel cost is identical to Rounded - the common TV-app pattern of border: {w: 0} until focus no longer pays the full border pipeline on every unfocused card. borderZero is also precomputed on the CPU and uploaded as a uniform instead of being re-derived per fragment, and the vertex shader's branch is replaced with arithmetic on that flag. GLSL output is unchanged by construction; the CPU math mirrors the previous vertex shader expressions exactly and is unit tested against hand-computed cases. Co-Authored-By: Claude Fable 5 --- .../shaders/webgl/RoundedWithBorder.test.ts | 131 ++++++++ src/core/shaders/webgl/RoundedWithBorder.ts | 295 ++++++++++++------ 2 files changed, 328 insertions(+), 98 deletions(-) create mode 100644 src/core/shaders/webgl/RoundedWithBorder.test.ts diff --git a/src/core/shaders/webgl/RoundedWithBorder.test.ts b/src/core/shaders/webgl/RoundedWithBorder.test.ts new file mode 100644 index 0000000..5dfd53a --- /dev/null +++ b/src/core/shaders/webgl/RoundedWithBorder.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from 'vitest'; +import { + BORDER_VALUES_LENGTH, + calcBorderShaderValues, +} from './RoundedWithBorder.js'; +import type { Vec4 } from '../../renderers/webgl/internal/ShaderUtils.js'; + +const calc = ( + borderWidth: Vec4, + align: number, + gap: number, + radius: Vec4, + w: number, + h: number, +): Float32Array => { + const out = new Float32Array(BORDER_VALUES_LENGTH); + calcBorderShaderValues(borderWidth, align, gap, radius, w, h, out); + return out; +}; + +describe('calcBorderShaderValues', () => { + it('should compute uniform border, align outside, no gap', () => { + // border 4 on all sides, align=1 (outside), gap=0, radius 16, 200x100 node + // adjustedBorderWidth = 4 - 1 + clamp(4, -1, 1) = 4 + const v = calc([4, 4, 4, 4], 1, 0, [16, 16, 16, 16], 200, 100); + + // outerSize = (dimensions + gapSize + borderSize * align) * 0.5 + expect(v[0]).toBe(104); + expect(v[1]).toBe(54); + // innerSize = outerSize - borderSize * 0.5 + expect(v[2]).toBe(100); + expect(v[3]).toBe(50); + // symmetric border -> zero UV offsets (-0 from sign() * 0 is fine) + expect(v[4]).toBeCloseTo(0); + expect(v[5]).toBeCloseTo(0); + expect(v[6]).toBeCloseTo(0); + expect(v[7]).toBeCloseTo(0); + // outerBorderRadius = radius + max(top*align+gap, side*align+gap) = 16 + 4 + expect(v[8]).toBe(20); + expect(v[9]).toBe(20); + expect(v[10]).toBe(20); + expect(v[11]).toBe(20); + // innerBorderRadius = outer - max(top, side) = 20 - 4 + expect(v[12]).toBe(16); + expect(v[13]).toBe(16); + expect(v[14]).toBe(16); + expect(v[15]).toBe(16); + // outerSize exceeds half dimensions -> edgeOffset = extraSize + gap + expect(v[16]).toBe(8); + expect(v[17]).toBe(8); + }); + + it('should compute asymmetric border with gap and center align', () => { + // borderWidth [top, right, bottom, left] = [2, 4, 6, 8], align=0.5, gap=2 + // adjusted widths equal inputs since all > 1 + const v = calc([2, 4, 6, 8], 0.5, 2, [10, 10, 10, 10], 100, 80); + + // borderSize = (right+left, top+bottom) = (12, 8); extra = (6, 4) + // all sides have width -> gapSize = (4, 4) + // outerSize = ((100+4+6)/2, (80+4+4)/2) + expect(v[0]).toBe(55); + expect(v[1]).toBe(44); + // innerSize = outer - (6, 4) + expect(v[2]).toBe(49); + expect(v[3]).toBe(40); + // borderDiff = (right-left, bottom-top) = (-4, 4); gapDiff = (0, 0) + // outerUv = (-sign*abs*align*0.5, ...) = (1, -1) + expect(v[4]).toBe(1); + expect(v[5]).toBe(-1); + // innerUv = outerUv + sign*abs*0.5 = (-1, 1) + expect(v[6]).toBe(-1); + expect(v[7]).toBe(1); + // outer radius per corner: 10 + max(adjacent side*0.5 + 2) + expect(v[8]).toBe(16); // TL: 10 + max(2*0.5+2, 8*0.5+2) = 10 + 6 + expect(v[9]).toBe(14); // TR: 10 + max(3, 4) + expect(v[10]).toBe(15); // BR: 10 + max(5, 4) + expect(v[11]).toBe(16); // BL: 10 + max(5, 6) + // inner radius: outer - max(adjacent widths) + expect(v[12]).toBe(8); // 16 - max(2, 8) + expect(v[13]).toBe(10); // 14 - max(2, 4) + expect(v[14]).toBe(9); // 15 - max(6, 4) + expect(v[15]).toBe(8); // 16 - max(6, 8) + // edgeOffset = extraSize + gap + expect(v[16]).toBe(8); + expect(v[17]).toBe(6); + }); + + it('should collapse sub-pixel widths like the GLSL adjustment', () => { + // adjusted = 0.5 - 1 + clamp(0.5) = 0 -> behaves as zero-width sides + const v = calc([0.5, 0.5, 0.5, 0.5], 1, 2, [10, 10, 10, 10], 100, 100); + + // borderSize = (0, 0); gap only applies to sides >= 0.001 -> none + expect(v[0]).toBe(50); + expect(v[1]).toBe(50); + expect(v[2]).toBe(50); + expect(v[3]).toBe(50); + // edgeOffset: outerSize >= halfDimensions -> extra(0) + gap(2) + expect(v[16]).toBe(2); + expect(v[17]).toBe(2); + }); + + it('should align inside without growing beyond node bounds', () => { + // align=0 (inside), border 4, no gap: outerSize = halfDimensions + const v = calc([4, 4, 4, 4], 0, 0, [10, 10, 10, 10], 100, 100); + + expect(v[0]).toBe(50); + expect(v[1]).toBe(50); + expect(v[2]).toBe(46); + expect(v[3]).toBe(46); + // outer radius unchanged at align=0, gap=0 + expect(v[8]).toBe(10); + // inner radius = 10 - 4 + expect(v[12]).toBe(6); + // outerSize == halfDimensions -> step passes, but extra+gap is 0 + expect(v[16]).toBe(0); + expect(v[17]).toBe(0); + }); + + it('should clamp negative inner radius to zero', () => { + // border wider than radius: inner radius would go negative + const v = calc([12, 12, 12, 12], 1, 0, [4, 4, 4, 4], 200, 200); + + // outer = 4 + 12, inner = 16 - 12 = 4; with radius 0 corners: + expect(v[8]).toBe(16); + expect(v[12]).toBe(4); + + const sharp = calc([12, 12, 12, 12], 1, 0, [0, 0, 0, 0], 200, 200); + expect(sharp[8]).toBe(12); + expect(sharp[12]).toBe(0); + }); +}); diff --git a/src/core/shaders/webgl/RoundedWithBorder.ts b/src/core/shaders/webgl/RoundedWithBorder.ts index ac66fc9..db51cd0 100644 --- a/src/core/shaders/webgl/RoundedWithBorder.ts +++ b/src/core/shaders/webgl/RoundedWithBorder.ts @@ -7,19 +7,187 @@ import { type RoundedWithBorderProps, } from '../templates/RoundedWithBorderTemplate.js'; +/** + * Layout of the values written by {@link calcBorderShaderValues}: + * + * ``` + * [0] outerSize.x [1] outerSize.y + * [2] innerSize.x [3] innerSize.y + * [4] outerBorderUv.x [5] outerBorderUv.y + * [6] innerBorderUv.x [7] innerBorderUv.y + * [8..11] outerBorderRadius (TL, TR, BR, BL) + * [12..15] innerBorderRadius (TL, TR, BR, BL) + * [16] edgeOffset.x [17] edgeOffset.y + * ``` + */ +export const BORDER_VALUES_LENGTH = 18; + +/** + * CPU mirror of the border math that previously ran in the vertex shader. + * + * Every value here is constant across the quad (derived only from border + * props, factored radius and node dimensions), so computing it once per + * prop/dimension change and uploading uniforms is strictly cheaper than + * interpolating 18 floats of varyings per fragment. + * + * `borderWidth` is [top, right, bottom, left]; `radius` is the already + * factored corner radius array (TL, TR, BR, BL). + */ +export const calcBorderShaderValues = ( + borderWidth: Vec4, + align: number, + gap: number, + radius: Vec4, + w: number, + h: number, + out: Float32Array, +): void => { + // adjustedBorderWidth = u_borderWidth - 1.0 + clamp(u_borderWidth, -1.0, 1.0) + const bwT = borderWidth[0]; + const bwR = borderWidth[1]; + const bwB = borderWidth[2]; + const bwL = borderWidth[3]; + const top = bwT - 1 + (bwT < -1 ? -1 : bwT > 1 ? 1 : bwT); + const right = bwR - 1 + (bwR < -1 ? -1 : bwR > 1 ? 1 : bwR); + const bottom = bwB - 1 + (bwB < -1 ? -1 : bwB > 1 ? 1 : bwB); + const left = bwL - 1 + (bwL < -1 ? -1 : bwL > 1 ? 1 : bwL); + + const borderSizeX = right + left; + const borderSizeY = top + bottom; + const extraX = borderSizeX * align; + const extraY = borderSizeY * align; + + // gap* = step(0.001, border*) * u_borderGap + const gapTop = top >= 0.001 ? gap : 0; + const gapRight = right >= 0.001 ? gap : 0; + const gapBottom = bottom >= 0.001 ? gap : 0; + const gapLeft = left >= 0.001 ? gap : 0; + const gapSizeX = gapLeft + gapRight; + const gapSizeY = gapTop + gapBottom; + + const outerX = (w + gapSizeX + extraX) * 0.5; + const outerY = (h + gapSizeY + extraY) * 0.5; + out[0] = outerX; + out[1] = outerY; + out[2] = outerX - borderSizeX * 0.5; + out[3] = outerY - borderSizeY * 0.5; + + let borderDiffX = right - left; + let borderDiffY = bottom - top; + const signDiffX = borderDiffX > 0 ? 1 : borderDiffX < 0 ? -1 : 0; + const signDiffY = borderDiffY > 0 ? 1 : borderDiffY < 0 ? -1 : 0; + borderDiffX = borderDiffX < 0 ? -borderDiffX : borderDiffX; + borderDiffY = borderDiffY < 0 ? -borderDiffY : borderDiffY; + + let gapDiffX = gapRight - gapLeft; + let gapDiffY = gapBottom - gapTop; + const signGapDiffX = gapDiffX > 0 ? 1 : gapDiffX < 0 ? -1 : 0; + const signGapDiffY = gapDiffY > 0 ? 1 : gapDiffY < 0 ? -1 : 0; + gapDiffX = gapDiffX < 0 ? -gapDiffX : gapDiffX; + gapDiffY = gapDiffY < 0 ? -gapDiffY : gapDiffY; + + const outerUvX = + -signDiffX * borderDiffX * align * 0.5 - signGapDiffX * gapDiffX * 0.5; + const outerUvY = + -signDiffY * borderDiffY * align * 0.5 - signGapDiffY * gapDiffY * 0.5; + out[4] = outerUvX; + out[5] = outerUvY; + out[6] = outerUvX + signDiffX * borderDiffX * 0.5; + out[7] = outerUvY + signDiffY * borderDiffY * 0.5; + + const alignTop = top * align + gap; + const alignRight = right * align + gap; + const alignBottom = bottom * align + gap; + const alignLeft = left * align + gap; + + const oTl = radius[0] + (alignTop > alignLeft ? alignTop : alignLeft); + const oTr = radius[1] + (alignTop > alignRight ? alignTop : alignRight); + const oBr = radius[2] + (alignBottom > alignRight ? alignBottom : alignRight); + const oBl = radius[3] + (alignBottom > alignLeft ? alignBottom : alignLeft); + out[8] = oTl < 0 ? 0 : oTl; + out[9] = oTr < 0 ? 0 : oTr; + out[10] = oBr < 0 ? 0 : oBr; + out[11] = oBl < 0 ? 0 : oBl; + + const iTl = out[8]! - (top > left ? top : left); + const iTr = out[9]! - (top > right ? top : right); + const iBr = out[10]! - (bottom > right ? bottom : right); + const iBl = out[11]! - (bottom > left ? bottom : left); + out[12] = iTl < 0 ? 0 : iTl; + out[13] = iTr < 0 ? 0 : iTr; + out[14] = iBr < 0 ? 0 : iBr; + out[15] = iBl < 0 ? 0 : iBl; + + // edgeOffset = step(u_dimensions * 0.5, outerSize) * (extraSize + u_borderGap) + out[16] = outerX >= w * 0.5 ? extraX + gap : 0; + out[17] = outerY >= h * 0.5 ? extraY + gap : 0; +}; + +// Scratch buffer for calcBorderShaderValues. Safe to share: the values are +// copied into the uniform collection by the uniform setters before the next +// update() call can run. +const borderValues = new Float32Array(BORDER_VALUES_LENGTH); + +/** + * Similar to the {@link DefaultShader} but cuts out 4 rounded rectangle corners + * as defined by the specified corner {@link RoundedProps.radius} and renders a + * border as defined by {@link RoundedWithBorderProps}. + * + * All border geometry is precomputed on the CPU (cached per prop/dimension + * change via the shader value-key cache) and uploaded as uniforms, so the + * fragment shader carries the same 3 varyings as the plain Rounded shader. + * With a zero-width border the per-pixel cost is identical to Rounded. + */ export const RoundedWithBorder: WebGlShaderType = { props: RoundedWithBorderTemplate.props, update(node: CoreNode) { this.uniformRGBA('u_borderColor', this.props!['border-color']); this.uniformRGBA('u_fillColor', this.props!['border-fill']); - this.uniform4fa('u_borderWidth', this.props!['border-w'] as Vec4); - this.uniform1f('u_borderGap', this.props!['border-gap'] as number); - this.uniform1f('u_borderAlign', this.props!['border-align'] as number); + const gap = this.props!['border-gap'] as number; + this.uniform1f('u_borderGap', gap); + + const radius = calcFactoredRadiusArray( + this.props!.radius as Vec4, + node.w, + node.h, + ); + this.uniform4fa('u_radius', radius); + + const borderWidth = this.props!['border-w'] as Vec4; + + // borderZero = 1.0 - step(0.001, dot(abs(u_borderWidth), vec4(1.0))) + const sumAbs = + (borderWidth[0] < 0 ? -borderWidth[0] : borderWidth[0]) + + (borderWidth[1] < 0 ? -borderWidth[1] : borderWidth[1]) + + (borderWidth[2] < 0 ? -borderWidth[2] : borderWidth[2]) + + (borderWidth[3] < 0 ? -borderWidth[3] : borderWidth[3]); + const borderZero = sumAbs >= 0.001 ? 0 : 1; + this.uniform1f('u_borderZero', borderZero); + + // With no border, both shader stages early-out before reading any of the + // border uniforms, so skip computing and uploading them entirely. + if (borderZero === 1) { + return; + } - this.uniform4fa( - 'u_radius', - calcFactoredRadiusArray(this.props!.radius as Vec4, node.w, node.h), + const v = borderValues; + calcBorderShaderValues( + borderWidth, + this.props!['border-align'] as number, + gap, + radius, + node.w, + node.h, + v, ); + + this.uniform2f('u_outerSize', v[0]!, v[1]!); + this.uniform2f('u_innerSize', v[2]!, v[3]!); + this.uniform2f('u_outerBorderUv', v[4]!, v[5]!); + this.uniform2f('u_innerBorderUv', v[6]!, v[7]!); + this.uniform4f('u_outerBorderRadius', v[8]!, v[9]!, v[10]!, v[11]!); + this.uniform4f('u_innerBorderRadius', v[12]!, v[13]!, v[14]!, v[15]!); + this.uniform2f('u_edgeOffset', v[16]!, v[17]!); }, vertex: ` # ifdef GL_FRAGMENT_PRECISION_HIGH @@ -37,94 +205,28 @@ export const RoundedWithBorder: WebGlShaderType = { uniform float u_pixelRatio; uniform vec2 u_dimensions; - uniform vec4 u_radius; - uniform vec4 u_borderWidth; - uniform float u_borderGap; - uniform float u_borderAlign; + uniform float u_borderZero; + uniform vec2 u_edgeOffset; varying vec4 v_color; varying vec2 v_textureCoords; varying vec2 v_nodeCoords; - varying vec2 v_innerSize; - varying vec2 v_outerSize; - varying vec2 v_outerBorderUv; - varying vec2 v_innerBorderUv; - varying vec4 v_innerBorderRadius; - varying vec4 v_outerBorderRadius; - varying vec2 v_halfDimensions; - void main() { - vec2 vertexPos = a_position * u_pixelRatio; vec2 screenSpace = vec2(2.0 / u_resolution.x, -2.0 / u_resolution.y); vec2 edge = clamp(a_nodeCoords * 2.0 - vec2(1.0), -1.0, 1.0); - vec2 edgeOffset = vec2(0.0); - float borderZero = 1.0 - step(0.001, dot(abs(u_borderWidth), vec4(1.0))); - - v_innerSize = vec2(0.0); - v_outerSize = vec2(0.0); - - if(borderZero == 0.0) { - vec4 adjustedBorderWidth = u_borderWidth - 1.0 + clamp(u_borderWidth, -1.0, 1.0); - - float borderTop = adjustedBorderWidth.x; - float borderRight = adjustedBorderWidth.y; - float borderBottom = adjustedBorderWidth.z; - float borderLeft = adjustedBorderWidth.w; - - v_outerBorderUv = vec2(0.0); - v_innerBorderUv = vec2(0.0); - - vec2 borderSize = vec2(borderRight + borderLeft, borderTop + borderBottom); - vec2 extraSize = borderSize * u_borderAlign; - float gapLeft = step(0.001, borderLeft) * u_borderGap; - float gapRight = step(0.001, borderRight) * u_borderGap; - float gapTop = step(0.001, borderTop) * u_borderGap; - float gapBottom = step(0.001, borderBottom) * u_borderGap; - vec2 gapSize = vec2(gapLeft + gapRight, gapTop + gapBottom); - - v_outerSize = (u_dimensions + gapSize + extraSize) * 0.5; - v_innerSize = v_outerSize - borderSize * 0.5; - - // Use sign() to avoid branching - vec2 borderDiff = vec2(borderRight - borderLeft, borderBottom - borderTop); - vec2 signDiff = sign(borderDiff); - borderDiff = abs(borderDiff); - - vec2 gapDiff = vec2(gapRight - gapLeft, gapBottom - gapTop); - vec2 signGapDiff = sign(gapDiff); - gapDiff = abs(gapDiff); - - v_outerBorderUv = -signDiff * borderDiff * u_borderAlign * 0.5 - signGapDiff * gapDiff * 0.5; - v_innerBorderUv = v_outerBorderUv + signDiff * borderDiff * 0.5; - - v_outerBorderRadius = vec4( - max(0.0, u_radius.x + max(borderTop * u_borderAlign + u_borderGap, borderLeft * u_borderAlign + u_borderGap)), - max(0.0, u_radius.y + max(borderTop * u_borderAlign + u_borderGap, borderRight * u_borderAlign + u_borderGap)), - max(0.0, u_radius.z + max(borderBottom * u_borderAlign + u_borderGap, borderRight * u_borderAlign + u_borderGap)), - max(0.0, u_radius.w + max(borderBottom * u_borderAlign + u_borderGap, borderLeft * u_borderAlign + u_borderGap)) - ); - - v_innerBorderRadius = vec4( - max(0.0, v_outerBorderRadius.x - max(borderTop, borderLeft)), - max(0.0, v_outerBorderRadius.y - max(borderTop, borderRight)), - max(0.0, v_outerBorderRadius.z - max(borderBottom, borderRight)), - max(0.0, v_outerBorderRadius.w - max(borderBottom, borderLeft)) - ); - - vec2 edgeOffsetExtra = step(u_dimensions * 0.5, v_outerSize) * edge * (extraSize + u_borderGap); - edgeOffset = edgeOffsetExtra; - - vertexPos = (a_position + edge + edgeOffset) * u_pixelRatio; - } + + // With a border the quad is expanded by 1px plus the precomputed + // outside-growth; u_borderZero zeroes both terms when borderless. + float hasBorder = 1.0 - u_borderZero; + vec2 edgeOffset = edge * u_edgeOffset * hasBorder; + vec2 vertexPos = (a_position + edge * hasBorder + edgeOffset) * u_pixelRatio; gl_Position = vec4(vertexPos.x * screenSpace.x - 1.0, -sign(screenSpace.y) * (vertexPos.y * -abs(screenSpace.y)) + 1.0, 0.0, 1.0); v_color = a_color; v_nodeCoords = a_nodeCoords + (screenSpace + edgeOffset) / (u_dimensions); v_textureCoords = a_textureCoords + (screenSpace + edgeOffset) / (u_dimensions); - - v_halfDimensions = u_dimensions * 0.5; } `, fragment: ` @@ -134,31 +236,28 @@ export const RoundedWithBorder: WebGlShaderType = { precision mediump float; # endif - uniform vec2 u_resolution; uniform float u_pixelRatio; uniform float u_alpha; uniform vec2 u_dimensions; uniform sampler2D u_texture; uniform vec4 u_radius; - uniform vec4 u_borderWidth; uniform vec4 u_borderColor; uniform vec4 u_fillColor; uniform float u_borderGap; - uniform float u_borderAlign; + uniform float u_borderZero; + + uniform vec2 u_innerSize; + uniform vec2 u_outerSize; + uniform vec2 u_outerBorderUv; + uniform vec2 u_innerBorderUv; + uniform vec4 u_innerBorderRadius; + uniform vec4 u_outerBorderRadius; varying vec4 v_color; varying vec2 v_textureCoords; varying vec2 v_nodeCoords; - varying vec2 v_innerSize; - varying vec2 v_outerSize; - varying vec2 v_outerBorderUv; - varying vec2 v_innerBorderUv; - varying vec4 v_innerBorderRadius; - varying vec4 v_outerBorderRadius; - varying vec2 v_halfDimensions; - float roundedBox(vec2 p, vec2 s, vec4 r) { r.xy = (p.x > 0.0) ? r.yz : r.xw; r.x = (p.y > 0.0) ? r.y : r.x; @@ -169,22 +268,22 @@ export const RoundedWithBorder: WebGlShaderType = { void main() { vec4 color = texture2D(u_texture, v_textureCoords) * v_color; vec4 resultColor = vec4(0.0); - vec2 boxUv = v_nodeCoords.xy * u_dimensions - v_halfDimensions; - float borderZero = 1.0 - step(0.001, dot(abs(u_borderWidth), vec4(1.0))); + vec2 halfDimensions = u_dimensions * 0.5; + vec2 boxUv = v_nodeCoords.xy * u_dimensions - halfDimensions; float edgeWidth = 1.0 / u_pixelRatio; float nodeDist; float nodeAlpha; - if(borderZero == 1.0) { - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); + if(u_borderZero == 1.0) { + nodeDist = roundedBox(boxUv, halfDimensions - edgeWidth, u_radius); nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); gl_FragColor = (color * nodeAlpha) * u_alpha; return; } - float outerDist = roundedBox(boxUv + v_outerBorderUv, v_outerSize - edgeWidth, v_outerBorderRadius); - float innerDist = roundedBox(boxUv + v_innerBorderUv, v_innerSize - edgeWidth, v_innerBorderRadius); + float outerDist = roundedBox(boxUv + u_outerBorderUv, u_outerSize - edgeWidth, u_outerBorderRadius); + float innerDist = roundedBox(boxUv + u_innerBorderUv, u_innerSize - edgeWidth, u_innerBorderRadius); if(u_borderGap == 0.0) { float outerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, outerDist); @@ -195,7 +294,7 @@ export const RoundedWithBorder: WebGlShaderType = { return; } - nodeDist = roundedBox(boxUv, v_halfDimensions - edgeWidth, u_radius); + nodeDist = roundedBox(boxUv, halfDimensions - edgeWidth, u_radius); nodeAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, nodeDist); float innerAlpha = 1.0 - smoothstep(-0.5 * edgeWidth, 0.5 * edgeWidth, innerDist); float gapAlpha = max(0.0, innerAlpha - nodeAlpha);