Repository navigation
Ground shadows: shape and placement controls (scale, offset, light direction) #1631
Description
Activity
Items 2 and 3 are implemented in #1714, as one explicit feature rather than two. Recording here how each of the four recorded objections is answered, since the issue asked that they not be re-proposed without that.
1. An offset blob slides off a slope. The offset is honoured only when
shadowGroundYis set. That setting is the game stating where its floor is, and it is the only case where sliding a flat quad across it is honest. The fallback, where the blob sits at the caster's own base, refuses the offset.2. A low sun should lengthen a shadow, which a footprint ellipse cannot express. It can, and more cheaply than this issue assumed. The basis is already oriented and anisotropic —
axX/axZandazX/azZcome from the caster's own model columns with independent lengths (mesh.js:1970-1973) — and both merely share one scalark. The stretch isS = I + (stretch - 1)·d⊗dapplied in world XZ, which leaves everything perpendicular toduntouched. No new primitive and no shader change.3. Which light wins, and the fallback with none. Nothing is inferred.
shadowLightnames one light and is read every draw;shadowDirectionX/shadowDirectionZset it by hand. No light named means no direction, so there is no dominant-light rule and no fallback to define.4. Coupling to a real light invites scrutiny the model cannot survive. The stretch is clamped to 3 and the blob fades by
1/√stretchas it pulls, so an extreme value degrades into nothing rather than a smear. The controls are named and documented as art direction, and the skill states plainly that this is not a projection and that direction-correct shadows want a shadow map, which this tier does not do.Two things found while doing it, both worth recording:
- It is per-object only. An
InstancedMeshshares one quad across instances that each carry their own rotation, so a world-space direction cannot be baked into the shared geometry. It ignores both halves rather than honouring one, with a test pinning that. Instanced support would need per-instance shadow data, which is a separate piece of work. GLTFModelwas dropping shadow settings. It forwarded onlycastGroundShadowandshadowGroundY, soshadowOpacitynever reached a loaded model, which is most of the props a scene has. Fixed in the same PR. Worth knowing because it also means item 1 (shadowScale, Add per-object ground shadow scaling #1712) would not have reached a glTF model either, which is where the motivating wide flat-bottomed prop usually comes from.
Item 1 is #1712, under review. The two compose:
shadowScalesizes the blob,shadowStretchpulls it along one axis.- It is per-object only. An
- added a commit that references this issue
on Oct 6, 2026 All three landed in 20.8.0.
- Per-object shadow scale —
shadowScale, default1, sitting next toshadowOpacityas suggested.0or less hides the blob and leaves the object drawn (#1712, thanks @snowyukitty). - Author-supplied offset —
shadowOffset, in multiples of the blob's own radius rather than world units, so one value serves casters of any size. It needsshadowGroundY, since a flat quad can only be slid across a plane the game has named (#1715). - Offset derived from the light —
shadowLightnames aLight3dto take the direction from, re-read every draw so a day/night cycle carries every shadow with it.shadowDirectionX/shadowDirectionZare the manual form. Nothing is inferred: a scene with no named light gets no direction (#1715).
The two objections recorded above as reasons not to do (3) first were addressed rather than waived:
- "an offset blob slides off the slope it is supposed to be lying on" —
shadowGroundNormalgives the ground's up normal, so the blob is rotated onto the plane rather than projected. It keeps its size, stays under its caster, and the tilt is clamped at 75 degrees. - "a low sun should lengthen a shadow, which a footprint-sized ellipse cannot express" —
shadowStretchlengthens along the light direction, clamped, and fading as it pulls.
All of it works on
InstancedMeshtoo, as one offset and one stretch for the whole set: they ride the quad the blobs share rather than the matrix that also places them, so the blobs lengthen without the scatter smearing. The two things an instanced set still cannot do are a per-instance value and a height fade, both consequences of one plane and one shared quad.Everything defaults to the previous behaviour, so none of it was breaking. There is a
#/ground-shadowsexample exercising all nine settings with an orbiting light.Closing as done.
- Per-object shadow scale —
Follow-up to #1515, which shipped the blob shadow itself. Three related gaps surfaced while building a chase-camera 3D scene; parking them together to revisit.
Today
Mesh._drawGroundShadowplaces the quad at the caster's own x/z:with an ellipse sized to the caster's footprint times a module-level
SHADOW_SPREAD(1.2). So a blob is always centred under its caster, always the same relative size, and never influenced by where the light is.1. Per-object shadow scale
shadowOpacityis per mesh; size is not — it comes from the footprint and a constant. There is no way to say "this character's shadow should read a little larger" or "this pickup's a little tighter".This is the one with the best cost/benefit. A wide, flat-bottomed prop resting on the ground hides its own blob completely from a camera looking down at it — nothing is visibly wrong, there is simply no shadow — and a slightly larger blob resolves it with no lifting and no physical claims. Suggested shape:
shadowScale(default1), sitting next toshadowOpacity.2. Author-supplied offset
shadowOffsetas a world-space x/z the game sets, giving the sun-angle look without the engine inferring anything, and without needing to decide which light wins.3. Offset derived from the light direction
The automatic version of (2): take the scene's dominant directional
Light3d, project onto the ground plane, offset the blob and stretch its major axis along that vector.Recorded with the reasons it was not done first, so it is not re-proposed without them being addressed:
Also considered and rejected
Clamping or warning when
shadowGroundYis lifted above the caster's base. It looks like a guard against a real trap — lifting the plane does not slide the blob out from under an object, it floats it up, and past a few units it projects over the top as a dark ring. But the same setting is legitimately used for a jumping or flying object whose shadow must stay on the floor and shrink with height, so a clamp would break the case the setting exists for. Documented in themelonjs-3dandmelonjs-3d-assetsskills instead (#1630).All three default to current behaviour, so none is breaking.