Give a shader room to draw around the picture it wears (#69)
A shader ran only inside the rectangle of what wears it: the quad has the size of the sprite, and outside of it no fragment runs. An outline or a glow built with Sample texture was cut off at the border. The issue still spoke of Filter.padding, but since #77 the shader is no filter any more, the limit is the geometry. - The Output node has a number Padding next to Edge: so many pixels of the picture per side the wearer draws beyond it. 0 is the default and draws as before, 256 is the most. - The quad grows and its UVs grow with it, so the picture keeps its place and size. In the margin the UVs run on below 0 and above 1. - Transparent is a fourth Edge mode: outside of the picture there is nothing. It is cut in the fragment (texelAt), so it needs no copy of the picture with an address mode of its own. - Edge holds everywhere alike, also for Texture color, Texture alpha and unconnected inputs. - Padding above 0 with Edge on Clamp puts the hint "Padding with Clamp smears the border" on the Output. The shader still compiles. A chosen Clamp and the default cannot be told apart. - All four wearers draw the margin. A sprite counts in pixels of its frame, a text in its units, a particle source grows every particle about its centre. - A nine-slice sprite gets a ring around its nine fields, as wide as so many border pixels. The fields themselves stay. - Without a picture a pixel is one unit of the wearer's own size, or of the particle size. - Particles now take the Edge mode of their shader. It had no effect on them before. - An object wearing a padded shader is never culled, or its margin would vanish at the edge of the screen. - The preview in the shader tab shows the margin and leaves room for it. - Picking, the selection frame, the size and the collider stay with the rectangle without the margin. Tests: - Gate on melektaus-atx right before this commit: 216 files, 3336 tests (3319 + 17), exit 0. - e2e 28,30,31,37,41 on melektaus-atx: 178 checks, all pass. e2e 1-37,47,48,58 ran before the last change (no culling with padding): 1281 checks, all pass. - 17 new unit tests: the compiler (padding, Transparent, the hint) and the geometry (quad, nine-slice ring). - Seven new e2e checks: four at the sprite in module 28, one each for text, particles and nine-slice. - 17 unit counter-probes all fail their tests. Three browser counter-probes fail their checks (5, 3 and 1 of them). - Three screenshots looked at: the sprite with Clamp and margin in the scene, the preview in the shader tab, the text. - The e2e sum of 1687 is counted (1680 + 7), the full suite and the smokes did not run. - Not seen: the hint at the node in a picture, particles and nine-slice in a picture, the game view while playing, sprites without a picture, vector and pixel art pictures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46c620ae3b
commit
e933fad446
20 files changed
+644
-55
No files matched your search
+109
-4
@@ -35,10 +35,13 @@ import {
|
||||
} from '../../src/core/properties/shaders';
|
||||
import { inlineInputs } from '../../src/core/graph/NodeRegistry';
|
||||
import { graphFingerprint } from '../../src/core/graph/Graph';
|
||||
import { compileShader } from '../../src/core/shader/compile';
|
||||
import { compileShader, PADDING_CLAMP_HINT } from '../../src/core/shader/compile';
|
||||
import {
|
||||
DEFAULT_EDGE,
|
||||
EDGE_PARAM,
|
||||
MAX_PADDING,
|
||||
PADDING_PARAM,
|
||||
paddingOf,
|
||||
SHADER_NODES,
|
||||
SHADER_NUMBER,
|
||||
SHADER_OUTPUT,
|
||||
@@ -220,7 +223,8 @@ describe('compileShader', () => {
|
||||
for (const name of ['brightness', 'contrast', 'saturation'])
|
||||
expect(fragment).toContain(`uniform float u_${name};`);
|
||||
// Drawn in the sprite's own pass: the tint goes in before the graph, premultiplied like the texel.
|
||||
expect(fragment).toContain('vec4 texel = texture(uTexture, vTextureCoord) * vColor;');
|
||||
expect(fragment).toContain('vec4 texelAt(vec2 uv) { return texture(uTexture, uv); }');
|
||||
expect(fragment).toContain('vec4 texel = texelAt(vTextureCoord) * vColor;');
|
||||
expect(fragment).toContain('vec3 straight(vec4 c) { return c.a > 0.0 ? c.rgb / c.a : c.rgb; }');
|
||||
expect(fragment).toContain('vec3 rgb = straight(texel);');
|
||||
expect(compiled).toMatchObject({ time: false, edge: 'mirror' });
|
||||
@@ -470,10 +474,10 @@ describe('compileShader', () => {
|
||||
expect(fragment).toMatch(/float n\d+_u = vTextureCoord\.x;/);
|
||||
expect(fragment).toMatch(/float n\d+_result = \(1\.0 - n\d+_u\);/);
|
||||
expect(fragment).toMatch(
|
||||
/vec3 n\d+_color = straight\(texture\(uTexture, vec2\(n\d+_result, vTextureCoord\.y\)\) \* vColor\);/,
|
||||
/vec3 n\d+_color = straight\(texelAt\(vec2\(n\d+_result, vTextureCoord\.y\)\) \* vColor\);/,
|
||||
);
|
||||
expect(fragment).toMatch(
|
||||
/float n\d+_alpha = \(texture\(uTexture, vec2\(n\d+_result, vTextureCoord\.y\)\) \* vColor\)\.a;/,
|
||||
/float n\d+_alpha = \(texelAt\(vec2\(n\d+_result, vTextureCoord\.y\)\) \* vColor\)\.a;/,
|
||||
);
|
||||
expect(fragment).toMatch(/float n\d+_width = float\(textureSize\(uTexture, 0\)\.x\);/);
|
||||
expect(fragment).toMatch(/float n\d+_value = uTime;/);
|
||||
@@ -510,6 +514,107 @@ describe('compileShader', () => {
|
||||
expect(compileShader(s.asset().graph).compiled!.edge).toBe('clamp');
|
||||
});
|
||||
|
||||
describe('padding and the Transparent edge (#69)', () => {
|
||||
const plain = (params: Record<string, unknown> = {}) => {
|
||||
const s = shaderEditor(makeProject(), 'Glow');
|
||||
const out = s.add(SHADER_OUTPUT, params);
|
||||
return { s, out, build: () => compileShader(s.asset().graph) };
|
||||
};
|
||||
|
||||
it('has no padding on a new Output, and hands on what is set there', () => {
|
||||
const fresh = plain();
|
||||
const params = fresh.s.asset().graph.nodes.find((n) => n.id === fresh.out)!.params;
|
||||
expect(params[PADDING_PARAM]).toBe(0);
|
||||
expect(fresh.build().compiled!.padding).toBe(0);
|
||||
expect(plain({ padding: 3 }).build().compiled!.padding).toBe(3);
|
||||
// A graph from before the setting existed has no value and draws as it did.
|
||||
delete params[PADDING_PARAM];
|
||||
expect(fresh.build().compiled!.padding).toBe(0);
|
||||
});
|
||||
|
||||
it('reads a padding nobody could mean as none, and stops at the limit', () => {
|
||||
expect(paddingOf({ padding: -4 })).toBe(0);
|
||||
expect(paddingOf({ padding: 'wide' })).toBe(0);
|
||||
expect(paddingOf({ padding: Number.POSITIVE_INFINITY })).toBe(0);
|
||||
expect(paddingOf({ padding: MAX_PADDING + 1 })).toBe(MAX_PADDING);
|
||||
expect(paddingOf({ padding: 1.5 })).toBe(1.5);
|
||||
});
|
||||
|
||||
it('shows the Edge in the margin: with padding the mode counts without any shifted Sample', () => {
|
||||
expect(plain({ edge: 'repeat' }).build().compiled!.edge).toBe(SHEET_EDGE);
|
||||
expect(plain({ edge: 'repeat', padding: 2 }).build().compiled!.edge).toBe('repeat');
|
||||
expect(plain({ edge: 'clamp', padding: 2 }).build().compiled!.edge).toBe('clamp');
|
||||
});
|
||||
|
||||
it('cuts Transparent in the fragment, on the plain sheet', () => {
|
||||
const { compiled } = plain({ edge: 'transparent', padding: 2 }).build();
|
||||
// No copy of the picture with a sampler of its own: the fragment empties the outside itself.
|
||||
expect(compiled!.edge).toBe(SHEET_EDGE);
|
||||
expect(compiled!.fragment).toContain(
|
||||
'vec4 texelAt(vec2 uv) { vec2 inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); return texture(uTexture, uv) * (inside.x * inside.y); }',
|
||||
);
|
||||
// The sprite's own pixel goes through the same cut, so the margin itself is empty.
|
||||
expect(compiled!.fragment).toContain('vec4 texel = texelAt(vTextureCoord) * vColor;');
|
||||
for (const edge of ['clamp', 'mirror', 'repeat'])
|
||||
expect(plain({ edge, padding: 2 }).build().compiled!.fragment).toContain(
|
||||
'vec4 texelAt(vec2 uv) { return texture(uTexture, uv); }',
|
||||
);
|
||||
});
|
||||
|
||||
it('reads a shifted Sample through the same cut', () => {
|
||||
const { s, out, build } = plain({ edge: 'transparent' });
|
||||
const uv = s.add(SHADER_UV);
|
||||
const shift = s.add('ShaderAdd', { b: 0.5 });
|
||||
const sample = s.add(SHADER_SAMPLE);
|
||||
s.link(uv, 'u', shift, 'a');
|
||||
s.link(shift, 'result', sample, 'u');
|
||||
s.link(sample, 'color', out, 'color');
|
||||
const { compiled } = build();
|
||||
expect(compiled!.edge).toBe(SHEET_EDGE);
|
||||
expect(compiled!.fragment).toMatch(
|
||||
/straight\(texelAt\(vec2\(n\d+_result, vTextureCoord\.y\)\) \* vColor\)/,
|
||||
);
|
||||
expect(compiled!.fragment).not.toMatch(/straight\(texture\(/);
|
||||
});
|
||||
|
||||
it('says at the Output that padding with Clamp smears, and still compiles', () => {
|
||||
const smeared = plain({ edge: 'clamp', padding: 2 });
|
||||
const build = smeared.build();
|
||||
expect(build.hints).toEqual({ [smeared.out]: PADDING_CLAMP_HINT });
|
||||
expect(build.problems).toEqual({});
|
||||
expect(build.reason).toBe('');
|
||||
expect(build.compiled).not.toBeNull();
|
||||
expect(plain({ edge: 'clamp' }).build().hints).toEqual({});
|
||||
for (const edge of ['transparent', 'mirror', 'repeat'])
|
||||
expect(plain({ edge, padding: 2 }).build().hints).toEqual({});
|
||||
});
|
||||
|
||||
it('offers Transparent and Padding on the Output', () => {
|
||||
const [edge, padding] = ShaderOutput.params;
|
||||
expect(edge).toMatchObject({ key: EDGE_PARAM, kind: 'enum' });
|
||||
expect((edge as { options: { value: string }[] }).options.map((o) => o.value)).toEqual([
|
||||
'clamp',
|
||||
'mirror',
|
||||
'repeat',
|
||||
'transparent',
|
||||
]);
|
||||
expect(padding).toMatchObject({
|
||||
key: PADDING_PARAM,
|
||||
kind: 'number',
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: MAX_PADDING,
|
||||
});
|
||||
});
|
||||
|
||||
it('compiles anew when the padding changes, so every wearer hears of it', () => {
|
||||
const { s, out } = plain({ padding: 1 });
|
||||
const before = graphFingerprint(s.asset().graph);
|
||||
s.asset().graph.nodes.find((n) => n.id === out)!.params[PADDING_PARAM] = 2;
|
||||
expect(graphFingerprint(s.asset().graph)).not.toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
it('writes Fract, Step and Floor as the GLSL calls, per channel for a colour', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f, 'Lines');
|
||||
|
||||
Reference in new issue
Block a user