Shader graph: alpha input on Output, Texture alpha node (#70)
game-maker / suite (push) Successful in 7m55s
game-maker / suite (push) Successful in 7m55s
Output gains a number input `alpha`; unconnected it reads the texel's alpha, like `color` reads the texel. The epilogue premultiplies with the resolved alpha. New `Texture alpha` node exposes texel.a for maths. PortDef.inline=false keeps the alpha port without an inline field: every number port is inline by default, and a typed default of 0 would have made every shaded sprite invisible. The compiler's texel fallback is typed: colour ports get rgb, number ports get texel.a. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b0c4b4ec5a
commit
3da0ef1be2
6 files changed
+125
-12
No files matched your search
@@ -21,8 +21,15 @@ import {
|
||||
SHADER_PROPERTY,
|
||||
shaderParamDefs,
|
||||
} from '../../src/core/properties/shaders';
|
||||
import { inlineInputs } from '../../src/core/graph/NodeRegistry';
|
||||
import { compileShader } from '../../src/core/shader/compile';
|
||||
import { SHADER_NODES, SHADER_OUTPUT, SHADER_TEXTURE_COLOR } from '../../src/core/shader/nodes';
|
||||
import {
|
||||
SHADER_NODES,
|
||||
SHADER_OUTPUT,
|
||||
SHADER_TEXTURE_ALPHA,
|
||||
SHADER_TEXTURE_COLOR,
|
||||
ShaderOutput,
|
||||
} from '../../src/core/shader/nodes';
|
||||
import { deserializeProject } from '../../src/core/store/serialize';
|
||||
import { paletteEntries } from '../../src/editor/graph/paletteEntries';
|
||||
import { makeProject, makeSpriteScene, place, type ProjectFixture, roundTrip } from '../helpers';
|
||||
@@ -181,7 +188,8 @@ describe('compileShader', () => {
|
||||
expect(fragment).toMatch(/vec3 n\d+_result = \(n\d+_result - vec3\(0\.5\)\);/);
|
||||
expect(fragment).toMatch(/float n\d+_value = dot\(n\d+_result, vec3\(0\.2126, 0\.7152, 0\.0722\)\);/);
|
||||
expect(fragment).toMatch(/vec3 n\d+_result = mix\(vec3\(n\d+_value\), n\d+_result, n\d+_value\);/);
|
||||
expect(fragment).toMatch(/finalColor = vec4\(clamp\(n\d+_result, 0\.0, 1\.0\) \* texel\.a, texel\.a\);/);
|
||||
expect(fragment).toContain('float alpha = clamp(texel.a, 0.0, 1.0);');
|
||||
expect(fragment).toMatch(/finalColor = vec4\(clamp\(n\d+_result, 0\.0, 1\.0\) \* alpha, alpha\);/);
|
||||
// Each local is declared before it is used.
|
||||
const declared = new Set<string>();
|
||||
for (const line of fragment.split('\n')) {
|
||||
@@ -196,7 +204,7 @@ describe('compileShader', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
s.add(SHADER_OUTPUT);
|
||||
expect(compileShader(s.asset().graph).compiled!.fragment).toContain('clamp(rgb, 0.0, 1.0) * texel.a');
|
||||
expect(compileShader(s.asset().graph).compiled!.fragment).toContain('clamp(rgb, 0.0, 1.0) * alpha');
|
||||
|
||||
const g = shaderEditor(f, 'Scaled');
|
||||
const times = g.add('ShaderMultiply', { a: 2, b: 0.75 });
|
||||
@@ -219,6 +227,41 @@ describe('compileShader', () => {
|
||||
expect(tinted).toMatch(/vec3 n1_result = \(vec3\(1\.0\) \* n0_color\);/);
|
||||
});
|
||||
|
||||
it('keeps the texture’s alpha until the Output’s alpha is fed, then clamps what feeds it', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
const alpha = s.add(SHADER_TEXTURE_ALPHA);
|
||||
const opacity = s.add(SHADER_PROPERTY, { name: 'opacity', default: 1, min: 0, max: 1 });
|
||||
const times = s.add('ShaderMultiply');
|
||||
const output = s.add(SHADER_OUTPUT);
|
||||
// Nothing to type on the alpha port: a typed-in 0 would hide every sprite by default.
|
||||
expect(inlineInputs(ShaderOutput).map((p) => p.key)).toEqual([]);
|
||||
let { fragment } = compileShader(s.asset().graph).compiled!;
|
||||
expect(fragment).toContain('float alpha = clamp(texel.a, 0.0, 1.0);');
|
||||
expect(fragment).toContain('finalColor = vec4(clamp(rgb, 0.0, 1.0) * alpha, alpha);');
|
||||
|
||||
s.link(alpha, 'value', times, 'a');
|
||||
s.link(opacity, 'value', times, 'b');
|
||||
s.link(times, 'result', output, 'alpha');
|
||||
({ fragment } = compileShader(s.asset().graph).compiled!);
|
||||
expect(fragment).toMatch(/float n0_value = texel\.a;/);
|
||||
expect(fragment).toMatch(/float n1_value = u_opacity;/);
|
||||
expect(fragment).toMatch(/float n2_result = \(n0_value \* n1_value\);/);
|
||||
expect(fragment).toContain('float alpha = clamp(n2_result, 0.0, 1.0);');
|
||||
expect(fragment).toContain('finalColor = vec4(clamp(rgb, 0.0, 1.0) * alpha, alpha);');
|
||||
|
||||
// A colour reaching the alpha port through an `any` result is the Output's problem.
|
||||
const wrong = shaderEditor(f, 'Wrong');
|
||||
const texel = wrong.add(SHADER_TEXTURE_COLOR);
|
||||
const doubled = wrong.add('ShaderMultiply', { b: 2 });
|
||||
const out = wrong.add(SHADER_OUTPUT);
|
||||
wrong.link(texel, 'color', doubled, 'a');
|
||||
wrong.link(doubled, 'result', out, 'alpha');
|
||||
const build = compileShader(wrong.asset().graph);
|
||||
expect(build.compiled).toBeNull();
|
||||
expect(build.problems).toEqual({ [out]: '"alpha" needs a number, not a colour' });
|
||||
});
|
||||
|
||||
it('names the problem on the node, and compiles nothing while there is one', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
|
||||
Reference in new issue
Block a user