Skip to content

WebGPU: a ShaderEffect on a Mesh is silently ignored, and the warning names the wrong reason #1658

Description

@obiot

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

  1. Fix the warning first — it should name the vertex-layout mismatch and point at Mesh.shader. Cheap, and it removes the misdirection.
  2. 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.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions