obiot · GitHub

Context

melonJS has no 3D shadow system. For 2.5D scenes (paper-thin billboard characters), characters read much better with a simple blob shadow on the ground. Today that's hand-rolled (a flat dark quad / Sprite3d with billboard: false, or a tinted Mesh) — see the "Shadows" caveat in 2.5D Games.

Why a fake shadow is the right answer here, not a shortcut

Worth stating explicitly, since "blob shadow" sounds like a placeholder for real shadows:

A shadow map would be actively wrong for this art style. A paper-thin billboard casts a paper-thin shadow — a line — which reads as broken rather than realistic. What the player needs from a shadow here is contact: where the character is standing, and how far off the ground they are mid-jump. A blob conveys exactly that.

It is also the wrong cost. Real shadows mean a depth pass per light, a shadow map texture, light-space matrices, and PCF sampling in the lit shaders on both backends — to end up looking worse for this case. Out of scope, and pointing the wrong way.

Proposal — the sprite draws its own shadow

The technique is the blob; the design decision is ownership.

The obvious shape — a helper that spawns a paired shadow renderable and syncs it to its target each frame — is the part that would be a hack: two scene-graph nodes for one conceptual object, a depth-sort relationship to get right by hand, teardown that can leak, and a shadow that desyncs the moment someone moves the sprite without going through the helper.

Instead, Sprite3d emits a second, ground-projected quad as part of its own draw. It already emits one quad; a second costs no new render pass, no new node, and nothing to keep in sync.

const hero = new Sprite3d(x, y, {
    image: "hero",
    castGroundShadow: true,      // or a settings object for size / opacity / fade
});

This removes two of the fiddly problems outright:

  • No z-fighting decision — the sprite places its own shadow relative to itself, rather than a separate object guessing at the floor plane.
  • No sort-order problem — both quads come out of one draw, so the shadow cannot land in front of the character.

Open sub-decisions:

  • Size/opacity falling off with height above the ground (a jumping character's shadow shrinking and fading is most of the readability win).
  • How the ground plane is established: a fixed Y, or a per-sprite groundY.
  • A soft-edged radial texture (generated once, shared) versus an alpha-falloff in the shader.

Acceptance

  • One setting renders a soft blob under a character and follows it, with no second renderable to manage.
  • Works under Camera3d; the shadow sits on the floor, under the sprite, with no z-fighting and no sort inversion.
  • A sprite with the setting off is byte-identical to today.
  • Example/wiki snippet.

Scope note: this is a fake shadow helper, not real shadow mapping (out of scope, and deliberately so — see above).

Read the original on github.com ↗