/** * A shader's `Blur` node (#300): the picture softened around a spot in one pass, the loop written * by the compiler into the prelude. */ import { describe, expect, it } from 'vitest'; import { AddNodeCommand, ConnectCommand } from '../../src/core/commands/graphCommands'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { findAssetOf } from '../../src/core/model/Project'; import { SHADER_PROPERTY } from '../../src/core/properties/shaders'; import { compileShader, perCornerProblem } from '../../src/core/shader/compile'; import { BLUR_FN, PIXEL_SCALE_UNIFORM, SHADER_BLUR, SHADER_OUTPUT, SHADER_VERTEX_OUTPUT, SHEET_EDGE, } from '../../src/core/shader/nodes'; import { makeSpriteScene } from '../helpers'; function shader(outParams = {}) { const f = makeSpriteScene(); const create = new CreateAssetCommand('shader', 'Soften'); f.stack.execute(create); const id = create.asset.id; const asset = () => findAssetOf(f.project, id, 'shader')!; const add = (type: string, params = {}) => { const command = new AddNodeCommand(id, type, 0, 0, params); f.stack.execute(command); return command.node.id; }; const link = (from: string, fromPort: string, to: string, toPort: string) => f.stack.execute(new ConnectCommand(id, { node: from, port: fromPort }, { node: to, port: toPort })); const out = add(SHADER_OUTPUT, outParams); return { add, link, out, build: () => compileShader(asset().graph) }; } /** A Blur feeding both outputs of the Output node. */ function blurred(params = {}, outParams = {}) { const s = shader(outParams); const blur = s.add(SHADER_BLUR, params); s.link(blur, 'color', s.out, 'color'); s.link(blur, 'alpha', s.out, 'alpha'); return { ...s, blur }; } const calls = (fragment: string) => fragment.split('void main()')[1]!.match(/blur\(/g)?.length ?? 0; describe('Blur (#300)', () => { it('samples once per pixel, premultiplied, and both outputs read that one sum', () => { const fragment = blurred().build().compiled!.fragment; expect(fragment).toContain(BLUR_FN); expect(fragment).toContain( 'vec4 n0_prepared = blur(uTexture, vec2(vTextureCoord.x, vTextureCoord.y), 2.0, vec2(1.0, 1.0), 0.0) * vColor;', ); expect(fragment).toContain('vec3 n1_color = straight(n0_prepared);'); expect(fragment).toContain('float n2_alpha = n0_prepared.a;'); expect(calls(fragment)).toBe(1); }); it('turns Kind and Direction into the arguments of the one helper', () => { const call = (params: object) => blurred(params) .build() .compiled!.fragment.match(/= blur\(.*;/)![0]; expect(call({ kind: 'gaussian' })).toContain('vec2(1.0, 1.0), 1.0)'); expect(call({ direction: 'horizontal' })).toContain('vec2(1.0, 0.0), 0.0)'); expect(call({ direction: 'vertical' })).toContain('vec2(0.0, 1.0), 0.0)'); expect(call({ kind: 'nonsense', direction: 'nonsense' })).toContain('vec2(1.0, 1.0), 0.0)'); }); it('holds the radius at 0 to 8 in the program, and takes it from a Property as a uniform', () => { expect(BLUR_FN).toContain('float r = clamp(radius, 0.0, 8.0);'); const s = blurred({ radius: 3.5 }); expect(s.build().compiled!.fragment).toContain( 'blur(uTexture, vec2(vTextureCoord.x, vTextureCoord.y), 3.5,', ); const soft = s.add(SHADER_PROPERTY, { name: 'soft', default: 2, min: 0, max: 8 }); s.link(soft, 'value', s.blur, 'radius'); const compiled = s.build().compiled!; expect(compiled.uniforms).toEqual([{ name: 'soft', kind: 'number', default: 2 }]); expect(compiled.fragment).toMatch( /= blur\(uTexture, vec2\(vTextureCoord\.x, vTextureCoord\.y\), n\d+_value,/, ); }); it('reads at the UV it is fed', () => { const s = blurred(); const uv = s.add('ShaderUV'); const flip = s.add('ShaderSubtract', { a: 1 }); s.link(uv, 'u', flip, 'b'); s.link(flip, 'result', s.blur, 'u'); expect(s.build().compiled!.fragment).toMatch( /= blur\(uTexture, vec2\(n\d+_result, vTextureCoord\.y\), 2\.0,/, ); }); it('leaves the helper out of a shader that does not reach it', () => { const s = shader(); s.add(SHADER_BLUR); const fragment = s.build().compiled!.fragment; expect(fragment).not.toContain('blurWeight'); expect(fragment).not.toContain(PIXEL_SCALE_UNIFORM); expect(calls(fragment)).toBe(0); }); it('steps a pixel as the wearer counts one, so a field’s blur does not follow the zoom', () => { expect(BLUR_FN).toContain(`vec2 texel = 1.0 / (vec2(textureSize(picture, 0)) * ${PIXEL_SCALE_UNIFORM});`); expect(blurred().build().compiled!.fragment).toContain(`uniform vec2 ${PIXEL_SCALE_UNIFORM};`); }); it('reads past the border, so the picture comes with the Output’s Edge', () => { expect(blurred().build().compiled!.edge).toBe('clamp'); expect(blurred({}, { edge: 'repeat' }).build().compiled!.edge).toBe('repeat'); expect(blurred({}, { edge: 'transparent' }).build().compiled!.edge).toBe(SHEET_EDGE); }); it('reads the picture, so it cannot feed the Vertex output', () => { const s = shader(); const corner = s.add(SHADER_VERTEX_OUTPUT); const blur = s.add(SHADER_BLUR); s.link(blur, 'alpha', corner, 'x'); const build = s.build(); expect(build.compiled).toBeNull(); expect(build.problems[corner]).toBe(perCornerProblem('Blur')); }); });