From 2637feb715cacfa98b452a19ed29e23aeebd9daa Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Mon, 14 Sep 2026 19:35:00 +0200 Subject: [PATCH] A square root is one node everywhere else, so the shader graph gets its own (#92) `Sqrt` already exists in the behavior graph (`data/Sqrt.ts`) and in particles (`ParticleSqrt`); the shader was the only graph without it. `Pow` with exponent 0.5 does the same job, so this is convenience, not a missing capability -- a radial falloff no longer needs a node whose purpose has to be read off its parameter. Like `Pow` it cannot go through `unary()`: GLSL leaves `sqrt` undefined below 0, so the value is held there with `max(value, 0.0)`. That form takes a float second argument for a `vec3` too, so unlike `pow` the exponent broadcast has no counterpart here. Co-Authored-By: Claude Opus 5 --- src/core/shader/nodes.ts | 16 ++++++++++++++++ tests/core/shaders.test.ts | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/core/shader/nodes.ts b/src/core/shader/nodes.ts index e8447ffd..c5c2d6b3 100644 --- a/src/core/shader/nodes.ts +++ b/src/core/shader/nodes.ts @@ -383,6 +383,21 @@ export const ShaderPow: ShaderNodeDef = { }, }; +/** GLSL leaves `sqrt` undefined below 0, so the value is held at 0 there. */ +export const ShaderSqrt: ShaderNodeDef = { + type: 'ShaderSqrt', + label: 'Sqrt', + category: 'shader', + description: + 'The square root: 0.25 becomes 0.5, 4 becomes 2. Below 0 the value counts as 0. Per channel for colours.', + inputs: [anyIn('value', 0)], + outputs: [anyOut('result')], + params: [], + emit: ({ value }) => ({ + result: { code: `sqrt(max(${value!.code}, 0.0))`, type: value!.type }, + }), +}; + /** Every node a shader graph offers. Registered by `graph/nodes`, offered by no behavior palette. */ export const SHADER_NODES: readonly ShaderNodeDef[] = [ ShaderTextureColor, @@ -407,6 +422,7 @@ export const SHADER_NODES: readonly ShaderNodeDef[] = [ ShaderAbs, ShaderStep, ShaderPow, + ShaderSqrt, ]; /** A GLSL float literal: always with a decimal point, so `1` does not become an int. */ diff --git a/tests/core/shaders.test.ts b/tests/core/shaders.test.ts index 14033ebf..2453c6e7 100644 --- a/tests/core/shaders.test.ts +++ b/tests/core/shaders.test.ts @@ -487,6 +487,25 @@ describe('compileShader', () => { expect(fragment).toMatch(/vec3 n\d+_result = pow\(max\(n\d+_color, 0\.0\), vec3\(2\.2\)\);/); }); + it('writes Sqrt as the GLSL call with the value held at 0, per channel for a colour', () => { + const f = makeProject(); + const s = shaderEditor(f, 'Root'); + const uv = s.add(SHADER_UV); + const root = s.add('ShaderSqrt'); + const tint = s.add('ShaderColor', { value: 0xff8000 }); + const rolled = s.add('ShaderSqrt'); + const output = s.add(SHADER_OUTPUT); + s.link(uv, 'u', root, 'value'); + s.link(root, 'result', output, 'alpha'); + s.link(tint, 'color', rolled, 'value'); + s.link(rolled, 'result', output, 'color'); + const build = compileShader(s.asset().graph); + expect(build.problems).toEqual({}); + const { fragment } = build.compiled!; + expect(fragment).toMatch(/float n\d+_result = sqrt\(max\(n\d+_u, 0\.0\)\);/); + expect(fragment).toMatch(/vec3 n\d+_result = sqrt\(max\(n\d+_color, 0\.0\)\);/); + }); + it('writes Abs as the GLSL call, per channel for a colour', () => { const f = makeProject(); const s = shaderEditor(f, 'Fold');