Attaching a single ShaderEffect to a Mesh takes the fast path that swaps the renderable's program outright. On WebGL that shades the mesh. On WebGPU it does not — the renderer keeps the built-in mesh shading and warns once:
melonJS: this custom shader cannot be hosted on a Mesh by the WebGPU renderer
(it carries no `wgsl` module) — the mesh draws with the built-in shading
The message is wrong, which is most of the cost
ShaderEffect is dual-language: it takes { glsl, wgsl } bodies, and a correctly written effect has a WGSL half. The guard that rejects it tests customShader.isWebGPU, which glshader.js:115 sets from typeof wgsl === "string" — a property of GLShader, describing a complete WGSL module. A ShaderEffect compiles its body into a WGSLEffectRealization instead and never carries that flag, so a perfectly valid dual-language effect is told it "carries no wgsl module".
The real reason is the one in the code comment beside the guard: effect realizations are bound to the frozen quad vertex layout, which is not the mesh vertex contract.
Chasing the stated reason costs hours. The visible symptom is a mesh rendering as its flat texture, which reads as a bad binding, a failed texture load or an alpha problem — and the message sends you to look for a missing WGSL body that is already there.
Why it is worth fixing beyond the message
video.AUTO prefers WebGPU where it exists, so identical game code shades correctly on one machine and silently falls back on another. Nothing throws. The mesh simply draws in a way the author did not ask for.
What works today
A GLShader carrying a complete WGSL module is a mesh-contract program and is routed to the batcher — see Mesh.shader:
loader.preload([{ name: "toon", type: "shader", src: {
vertex: "shaders/toon.vert",
fragment: "shaders/toon.frag",
wgsl: "shaders/toon.wgsl", // a complete module, not a fragment body
}}], () => {
myMesh.addPostEffect(loader.getShader("toon"));
});
Note the asymmetry that makes the convenience worth restoring: a ShaderEffect body is a dozen lines, while the GLShader route means authoring the whole mesh vertex stage, the uniform blocks and the fog handling by hand. There is also no route for per-effect uniforms on a mesh-hosted shader — MeshUniforms is fixed — so an animated effect has no clean way to receive a time value.
2D renderables are unaffected: Sprite, Text and the rest go through the quad path and host effects on both backends.
Proposal
- Fix the warning first — it should name the vertex-layout mismatch and point at
Mesh.shader. Cheap, and it removes the misdirection.
- Realize an effect body against the mesh contract, so
addPostEffect on a Mesh behaves the same on both backends. The generated module would be the mesh module with the body's apply() spliced into the fragment stage; the effect's uniform block needs relocating, since it currently occupies @group(3) @binding(0) — exactly where uMesh lives.
- Give a mesh-hosted effect a way to receive its own uniforms, or the parity is nominal:
setUniform/setTime are most of what makes effects usable.
Acceptance
- a
ShaderEffect on a Mesh shades identically on WebGL and WebGPU
setUniform and setTime reach a mesh-hosted effect
lit meshes still receive their light data through the custom program
- the existing
GLShader-with-WGSL route keeps working unchanged
- a body that genuinely cannot be realized fails in a way the author cannot miss
Attaching a single
ShaderEffectto aMeshtakes the fast path that swaps the renderable's program outright. On WebGL that shades the mesh. On WebGPU it does not — the renderer keeps the built-in mesh shading and warns once:The message is wrong, which is most of the cost
ShaderEffectis dual-language: it takes{ glsl, wgsl }bodies, and a correctly written effect has a WGSL half. The guard that rejects it testscustomShader.isWebGPU, whichglshader.js:115sets fromtypeof wgsl === "string"— a property ofGLShader, describing a complete WGSL module. AShaderEffectcompiles its body into aWGSLEffectRealizationinstead and never carries that flag, so a perfectly valid dual-language effect is told it "carries nowgslmodule".The real reason is the one in the code comment beside the guard: effect realizations are bound to the frozen quad vertex layout, which is not the mesh vertex contract.
Chasing the stated reason costs hours. The visible symptom is a mesh rendering as its flat texture, which reads as a bad binding, a failed texture load or an alpha problem — and the message sends you to look for a missing WGSL body that is already there.
Why it is worth fixing beyond the message
video.AUTOprefers WebGPU where it exists, so identical game code shades correctly on one machine and silently falls back on another. Nothing throws. The mesh simply draws in a way the author did not ask for.What works today
A
GLShadercarrying a complete WGSL module is a mesh-contract program and is routed to the batcher — seeMesh.shader:Note the asymmetry that makes the convenience worth restoring: a
ShaderEffectbody is a dozen lines, while theGLShaderroute means authoring the whole mesh vertex stage, the uniform blocks and the fog handling by hand. There is also no route for per-effect uniforms on a mesh-hosted shader —MeshUniformsis fixed — so an animated effect has no clean way to receive a time value.2D renderables are unaffected:
Sprite,Textand the rest go through the quad path and host effects on both backends.Proposal
Mesh.shader. Cheap, and it removes the misdirection.addPostEffecton aMeshbehaves the same on both backends. The generated module would be the mesh module with the body'sapply()spliced into the fragment stage; the effect's uniform block needs relocating, since it currently occupies@group(3) @binding(0)— exactly whereuMeshlives.setUniform/setTimeare most of what makes effects usable.Acceptance
ShaderEffecton aMeshshades identically on WebGL and WebGPUsetUniformandsetTimereach a mesh-hosted effectlitmeshes still receive their light data through the custom programGLShader-with-WGSL route keeps working unchanged