diff --git a/README.md b/README.md index 7a5101ff..2ca1bb66 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2008 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 2009 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/src/core/shader/compile.ts b/src/core/shader/compile.ts index 7a9e0c19..6fa651f5 100644 --- a/src/core/shader/compile.ts +++ b/src/core/shader/compile.ts @@ -64,7 +64,10 @@ const glslOf = (u: ShaderUniform) => (u.kind === 'color' ? 'vec3' : 'float'); * `vTextureCoord` and the same uniforms the fragment does. */ export interface ShaderVertex { - /** Declarations ahead of `main`: the uniforms, at the fragment's precision, and the helpers. */ + /** + * Declarations ahead of `main`: the uniforms, at the fragment's precision, and the helpers. The + * picture's size, `CORNER_SIZE_UNIFORM`, is the wearer's to declare. + */ head: string; /** Statements inside `main`, ending in `vec2 offset`: the corner's move in pixels of the picture. */ body: string; @@ -384,8 +387,10 @@ function walker( prepared = `${prefix}${count++}_prepared`; lines.push(`vec4 ${prepared} = ${def.prepare(inputs, node.params)};`); } + const emitted = + perPixel && def.emitPerCorner ? def.emitPerCorner(inputs) : def.emit(inputs, node.params, prepared); const outputs: Record = {}; - for (const [key, expr] of Object.entries(def.emit(inputs, node.params, prepared))) { + for (const [key, expr] of Object.entries(emitted)) { const local = `${prefix}${count++}_${key}`; lines.push(`${expr.type} ${local} = ${expr.code};`); outputs[key] = { code: local, type: expr.type }; diff --git a/src/core/shader/nodes.ts b/src/core/shader/nodes.ts index ddb871b6..f96d25ca 100644 --- a/src/core/shader/nodes.ts +++ b/src/core/shader/nodes.ts @@ -33,6 +33,8 @@ export interface ShaderNodeDef extends NodeDef { unfed?: Record; /** Reads the picture or is a colour: there is none per corner, so it cannot feed the Vertex output (#79). */ perPixel?: boolean; + /** What the node is at a corner where that differs from `emit`, the picture's size for one (#409). */ + emitPerCorner?: (inputs: Record) => Record; /** A vec4 all outputs read, worked out once into a local `emit` gets as `prepared` (#300). */ prepare?: (inputs: Record, params: PropertyBag) => string; } @@ -87,6 +89,8 @@ const sampleOf = (picture: Expr, uv: string) => `${TEXEL_AT}(${picture.code}, ${ export const PIXEL_SCALE_UNIFORM = 'uPixelScale'; /** `picture`'s size in the pixels a node counts. */ const countedSize = (picture: Expr) => `(vec2(textureSize(${picture.code}, 0)) * ${PIXEL_SCALE_UNIFORM})`; +/** The picture a Vertex output counts its pixels in, in the vertex stage (#409); zero where no corner moves. */ +export const CORNER_SIZE_UNIFORM = 'uPictureSize'; /** The input a node reads a picture from; unconnected, the wearer's own. */ const pictureIn: PortDef = { key: 'picture', label: 'picture', type: 'picture', inline: false }; @@ -504,16 +508,20 @@ export const ShaderTextureSize: ShaderNodeDef = { label: 'Texture size', category: 'shader', description: - "The sprite's picture in pixels, for effects that need to know where its pixels are. Fed a Pass's picture, that one's, Padding included. In a field shader a pixel is a unit of the frame, whatever the zoom.", + "The sprite's picture in pixels, for effects that need to know where its pixels are. Fed a Pass's picture, that one's, Padding included. In a field shader a pixel is a unit of the frame, whatever the zoom. It can feed the Vertex output too, e.g. to scale the picture about its middle.", inputs: [pictureIn], outputs: [numberOut('width'), numberOut('height')], params: [], - perPixel: true, unfed: { picture: ORIGINAL }, emit: ({ picture }) => ({ width: { code: `${countedSize(picture!)}.x`, type: 'float' }, height: { code: `${countedSize(picture!)}.y`, type: 'float' }, }), + // A corner only ever sees the wearer's own picture: a Pass has none per corner. + emitPerCorner: () => ({ + width: { code: `${CORNER_SIZE_UNIFORM}.x`, type: 'float' }, + height: { code: `${CORNER_SIZE_UNIFORM}.y`, type: 'float' }, + }), }; export const ShaderTime: ShaderNodeDef = { diff --git a/src/viewport/shaders.ts b/src/viewport/shaders.ts index 3a203077..8b011c13 100644 --- a/src/viewport/shaders.ts +++ b/src/viewport/shaders.ts @@ -12,7 +12,7 @@ import { import { colorOf, type PropertyBag } from '../core/model/Properties'; import { shaderOf } from '../core/properties/shaders'; import type { CompiledShader, ShaderVertex } from '../core/shader/compile'; -import { PIXEL_SCALE_UNIFORM, TIME_UNIFORM, uniformName } from '../core/shader/nodes'; +import { CORNER_SIZE_UNIFORM, PIXEL_SCALE_UNIFORM, TIME_UNIFORM, uniformName } from '../core/shader/nodes'; import { type QuadPadding, quadPadding } from './quadPadding'; import { PassPictures } from './shaderPasses'; @@ -56,7 +56,8 @@ void main() /** * The same stage with a graph's Vertex output in it (#79): the corner's UV is known before the * graph runs, and what it says in pixels of the picture moves the corner in the quad's own 0..1. - * `uPerPixel` is one such pixel there – zero on a wearer whose corners are not a grid. + * `uPerPixel` is one such pixel there – zero on a wearer whose corners are not a grid – and the + * picture's size is what a Texture size reads at the corner (#409). */ function cornerVertex(vertex: ShaderVertex): string { return ` @@ -70,6 +71,7 @@ uniform vec4 uWorldColorAlpha; uniform mat3 uTransformMatrix; uniform vec4 uColor; uniform vec2 uPerPixel; +uniform vec2 ${CORNER_SIZE_UNIFORM}; ${vertex.head} void main() { @@ -263,10 +265,14 @@ class GraphShader extends CompiledGraphShader { /** What `Mesh.shader` asks of a shader; kept in step with the mesh's own by `wear`. */ texture: Texture = Texture.WHITE; private readonly perPixel: Float32Array | null; + private readonly size: Float32Array | null; constructor(assetId: string, compiled: CompiledShader) { const corner = compiled.vertex - ? new UniformGroup({ uPerPixel: { value: new Float32Array(2), type: 'vec2' } }) + ? new UniformGroup({ + uPerPixel: { value: new Float32Array(2), type: 'vec2' }, + [CORNER_SIZE_UNIFORM]: { value: new Float32Array(2), type: 'vec2' }, + }) : null; super( assetId, @@ -279,6 +285,7 @@ class GraphShader extends CompiledGraphShader { }, ); this.perPixel = corner ? corner.uniforms.uPerPixel : null; + this.size = corner ? corner.uniforms[CORNER_SIZE_UNIFORM] : null; } protected override vertexFor(compiled: CompiledShader): string { @@ -287,10 +294,12 @@ class GraphShader extends CompiledGraphShader { /** The picture the Vertex output's pixels are counted in; null or a fixed one holds every corner in place. */ count(picture: WornPicture | null): void { - if (!this.perPixel) return; + if (!this.perPixel || !this.size) return; const counted = picture && !picture.fixed ? picture : null; this.perPixel[0] = counted && counted.w > 0 ? 1 / counted.w : 0; this.perPixel[1] = counted && counted.h > 0 ? 1 / counted.h : 0; + this.size[0] = counted?.w ?? 0; + this.size[1] = counted?.h ?? 0; } /** Pixi binds a mesh's texture only for its own shader, so a custom one takes it here. */ diff --git a/tests/core/shader-passes.test.ts b/tests/core/shader-passes.test.ts index 309734fe..fd5a56c5 100644 --- a/tests/core/shader-passes.test.ts +++ b/tests/core/shader-passes.test.ts @@ -185,6 +185,14 @@ describe('Pass (#301)', () => { s.link(pass, 'picture', alpha, 'picture'); s.link(alpha, 'value', corner, 'x'); expect(s.build().problems[corner]).toBe(perCornerProblem('Pass')); + // Not even its size, though a Texture size reads the wearer's own there (#409). + const sized = shader(); + const sizedPass = sized.add(SHADER_PASS); + const size = sized.add(SHADER_TEXTURE_SIZE); + const sizedCorner = sized.add(SHADER_VERTEX_OUTPUT); + sized.link(sizedPass, 'picture', size, 'picture'); + sized.link(size, 'width', sizedCorner, 'x'); + expect(sized.build().problems[sizedCorner]).toBe(perCornerProblem('Pass')); }); it('sizes its picture as the wearer’s with the margin, at its resolution, within limits', () => { diff --git a/tests/core/shader-vertex.test.ts b/tests/core/shader-vertex.test.ts index cd5f6717..09d1b20f 100644 --- a/tests/core/shader-vertex.test.ts +++ b/tests/core/shader-vertex.test.ts @@ -109,7 +109,6 @@ describe('Vertex output (#79)', () => { it.each([ [SHADER_TEXTURE_ALPHA, 'value', 'Texture alpha'], [SHADER_SAMPLE, 'alpha', 'Sample texture'], - [SHADER_TEXTURE_SIZE, 'width', 'Texture size'], ])('says at the node that %s is not there per corner', (type, port, label) => { const s = shader(); s.link(s.add(type), port, s.corner, 'x'); @@ -134,6 +133,29 @@ describe('Vertex output (#79)', () => { expect(unfed.build().problems).toEqual({ [unfed.corner]: perCornerProblem('the picture') }); }); + it('reads the picture’s size at a corner, the one its pixels are counted in (#409)', () => { + // A scale of 1.2 about the middle: x = (u - 0.5) * width * 0.2. + const s = shader(); + const uv = s.add(SHADER_UV); + const size = s.add(SHADER_TEXTURE_SIZE); + const centred = s.add('ShaderSubtract', { b: 0.5 }); + const wide = s.add('ShaderMultiply'); + const grown = s.add('ShaderMultiply', { b: 0.2 }); + s.link(uv, 'u', centred, 'a'); + s.link(centred, 'result', wide, 'a'); + s.link(size, 'width', wide, 'b'); + s.link(wide, 'result', grown, 'a'); + s.link(grown, 'result', s.corner, 'x'); + s.link(size, 'height', s.out, 'alpha'); + const build = s.build(); + expect(build.problems).toEqual({}); + const { vertex, fragment } = build.compiled!; + expect(vertex!.body).toMatch(/float c\d+_width = uPictureSize\.x;/); + expect(vertex!.body).not.toContain('textureSize'); + // The Output still reads the texture itself. + expect(fragment).toMatch(/float n\d+_height = \(vec2\(textureSize\(uTexture, 0\)\) \* uPixelScale\)\.y;/); + }); + it('still lets the same nodes feed the Output', () => { const s = shader(); s.link(s.add(SHADER_TEXTURE_ALPHA), 'value', s.out, 'alpha'); diff --git a/tools/e2e/28-shader.mjs b/tools/e2e/28-shader.mjs index 652994b4..3984b020 100644 --- a/tools/e2e/28-shader.mjs +++ b/tools/e2e/28-shader.mjs @@ -893,6 +893,26 @@ export async function shader(t) { colourDistance(bare.left, LEFT) < 12, { moved, bare }, ); + // The picture's size reaches the corner (#409): half its width is the 16 from before. + await cornered( + [node('size', 'ShaderTextureSize', {}, 0, 260), node('half', 'ShaderMultiply', { b: 0.5 }, 260, 260)], + [edge('size', 'width', 'half', 'a'), edge('half', 'result', 'vout', 'x')], + 8, + ); + const sized = { + left: await pixel(-0.3, -0.3), + right: await pixel(0.3, -0.3), + past: await pixel(0.8, -0.3), + problem: await json("__gm.shader.nodeProblem('vout')"), + }; + check( + 'half of a Texture size at the Vertex output moves the picture as far as 16 does', + colourDistance(sized.left, LEFT) > 60 && + colourDistance(sized.right, LEFT) < 12 && + colourDistance(sized.past, RIGHT) < 12 && + !sized.problem, + { sized }, + ); const lowerHalf = async (subdivisions) => { await cornered( [