melonjs 20.8.0 → 20.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +1 -0
  2. package/build/application/application.d.ts +66 -8
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/defaultApplicationSettings.d.ts +1 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/resize.d.ts +25 -0
  7. package/build/application/resize.d.ts.map +1 -1
  8. package/build/application/scaleMethods.d.ts +13 -3
  9. package/build/application/scaleMethods.d.ts.map +1 -1
  10. package/build/application/settings.d.ts +56 -5
  11. package/build/application/settings.d.ts.map +1 -1
  12. package/build/audio/audio.d.ts +29 -5
  13. package/build/audio/audio.d.ts.map +1 -1
  14. package/build/audio/autopause.d.ts +36 -0
  15. package/build/audio/autopause.d.ts.map +1 -0
  16. package/build/audio/backend/engine.d.ts +10 -0
  17. package/build/audio/backend/engine.d.ts.map +1 -1
  18. package/build/audio/backend/sound.d.ts.map +1 -1
  19. package/build/audio/backend/spatial.d.ts.map +1 -1
  20. package/build/audio/context.d.ts +35 -0
  21. package/build/audio/context.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts.map +1 -1
  23. package/build/audio/procedural.d.ts.map +1 -1
  24. package/build/audio/spatial.d.ts +87 -0
  25. package/build/audio/spatial.d.ts.map +1 -0
  26. package/build/audio/state.d.ts +1 -0
  27. package/build/audio/state.d.ts.map +1 -1
  28. package/build/audio/types.d.ts +46 -6
  29. package/build/audio/types.d.ts.map +1 -1
  30. package/build/camera/camera2d.d.ts.map +1 -1
  31. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  32. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  33. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  34. package/build/geometries/box3d.d.ts +1 -1
  35. package/build/geometries/box3d.d.ts.map +1 -1
  36. package/build/geometries/ellipse.d.ts +1 -1
  37. package/build/geometries/ellipse.d.ts.map +1 -1
  38. package/build/geometries/line.d.ts +1 -1
  39. package/build/geometries/line.d.ts.map +1 -1
  40. package/build/geometries/point.d.ts +1 -1
  41. package/build/geometries/point.d.ts.map +1 -1
  42. package/build/geometries/polygon.d.ts +1 -1
  43. package/build/geometries/polygon.d.ts.map +1 -1
  44. package/build/geometries/rectangle.d.ts +1 -1
  45. package/build/geometries/rectangle.d.ts.map +1 -1
  46. package/build/geometries/roundrect.d.ts +1 -1
  47. package/build/geometries/roundrect.d.ts.map +1 -1
  48. package/build/geometries/sphere.d.ts +1 -1
  49. package/build/geometries/sphere.d.ts.map +1 -1
  50. package/build/index.d.ts +8 -2
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +36985 -34678
  53. package/build/index.js.map +4 -4
  54. package/build/input/gamepad.d.ts +129 -3
  55. package/build/input/gamepad.d.ts.map +1 -1
  56. package/build/level/gltf/GLTFModel.d.ts +5 -4
  57. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  58. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  59. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  60. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  61. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  62. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -4
  63. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  64. package/build/loader/cache.d.ts +220 -0
  65. package/build/loader/cache.d.ts.map +1 -1
  66. package/build/loader/loader.d.ts +20 -349
  67. package/build/loader/loader.d.ts.map +1 -1
  68. package/build/loader/parsers/gltf.d.ts +115 -0
  69. package/build/loader/parsers/gltf.d.ts.map +1 -1
  70. package/build/math/color.d.ts +1 -1
  71. package/build/math/color.d.ts.map +1 -1
  72. package/build/math/matrix2d.d.ts +1 -1
  73. package/build/math/matrix2d.d.ts.map +1 -1
  74. package/build/math/matrix3d.d.ts +1 -1
  75. package/build/math/matrix3d.d.ts.map +1 -1
  76. package/build/math/observableVector2d.d.ts +14 -2
  77. package/build/math/observableVector2d.d.ts.map +1 -1
  78. package/build/math/observableVector3d.d.ts +15 -1
  79. package/build/math/observableVector3d.d.ts.map +1 -1
  80. package/build/math/vector2d.d.ts +31 -1
  81. package/build/math/vector2d.d.ts.map +1 -1
  82. package/build/math/vector3d.d.ts +38 -1
  83. package/build/math/vector3d.d.ts.map +1 -1
  84. package/build/math/vertex.d.ts.map +1 -1
  85. package/build/particles/emitter.d.ts.map +1 -1
  86. package/build/particles/particle.d.ts +7 -1
  87. package/build/particles/particle.d.ts.map +1 -1
  88. package/build/particles/settings.d.ts +35 -0
  89. package/build/particles/settings.d.ts.map +1 -1
  90. package/build/physics/bounds.d.ts +1 -1
  91. package/build/physics/bounds.d.ts.map +1 -1
  92. package/build/physics/broadphase/octree.d.ts +24 -0
  93. package/build/physics/broadphase/octree.d.ts.map +1 -1
  94. package/build/physics/broadphase/quadtree.d.ts +31 -0
  95. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  96. package/build/physics/builtin/body.d.ts +14 -0
  97. package/build/physics/builtin/body.d.ts.map +1 -1
  98. package/build/pool.d.ts +4 -0
  99. package/build/pool.d.ts.map +1 -1
  100. package/build/renderable/colorlayer.d.ts +13 -0
  101. package/build/renderable/colorlayer.d.ts.map +1 -1
  102. package/build/renderable/container.d.ts.map +1 -1
  103. package/build/renderable/groundshadow.d.ts.map +1 -1
  104. package/build/renderable/instanced_mesh.d.ts +56 -0
  105. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  106. package/build/renderable/mesh.d.ts +97 -11
  107. package/build/renderable/mesh.d.ts.map +1 -1
  108. package/build/renderable/renderable.d.ts +41 -3
  109. package/build/renderable/renderable.d.ts.map +1 -1
  110. package/build/renderable/sprite.d.ts +2 -1
  111. package/build/renderable/sprite.d.ts.map +1 -1
  112. package/build/renderable/sprite3d.d.ts +6 -2
  113. package/build/renderable/sprite3d.d.ts.map +1 -1
  114. package/build/renderable/text/bitmaptext.d.ts +63 -2
  115. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  116. package/build/renderable/text/bitmaptextdata.d.ts +1 -1
  117. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  118. package/build/renderable/text/glypheffect.d.ts +44 -0
  119. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  120. package/build/renderable/text/text.d.ts +17 -0
  121. package/build/renderable/text/text.d.ts.map +1 -1
  122. package/build/renderable/trail.d.ts.map +1 -1
  123. package/build/renderable/trail3d.d.ts +174 -0
  124. package/build/renderable/trail3d.d.ts.map +1 -0
  125. package/build/renderable/trigger.d.ts +2 -1
  126. package/build/renderable/trigger.d.ts.map +1 -1
  127. package/build/renderable/ui/panelpaint.d.ts +5 -0
  128. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  129. package/build/renderable/ui/progressbar.d.ts +2 -4
  130. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  131. package/build/renderable/ui/uipanel.d.ts +245 -0
  132. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  133. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  134. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  135. package/build/state/state.d.ts.map +1 -1
  136. package/build/system/bootstrap.d.ts.map +1 -1
  137. package/build/system/legacy_pool.d.ts +22 -2
  138. package/build/system/legacy_pool.d.ts.map +1 -1
  139. package/build/system/pool.d.ts +5 -8
  140. package/build/system/pool.d.ts.map +1 -1
  141. package/build/tweens/tween.d.ts +1 -1
  142. package/build/tweens/tween.d.ts.map +1 -1
  143. package/build/video/effects/tintPulse.d.ts +0 -5
  144. package/build/video/effects/tintPulse.d.ts.map +1 -1
  145. package/build/video/effects/wave.d.ts +0 -5
  146. package/build/video/effects/wave.d.ts.map +1 -1
  147. package/build/video/gpu/batcher.d.ts +8 -2
  148. package/build/video/gpu/batcher.d.ts.map +1 -1
  149. package/build/video/gradient.d.ts +1 -0
  150. package/build/video/gradient.d.ts.map +1 -1
  151. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  152. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  153. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  154. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  155. package/build/video/rendertarget/webgpurendertarget.d.ts +6 -12
  156. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  157. package/build/video/texture/atlas.d.ts +1 -1
  158. package/build/video/texture/atlas.d.ts.map +1 -1
  159. package/build/video/texture/texture2d.d.ts +6 -0
  160. package/build/video/texture/texture2d.d.ts.map +1 -1
  161. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -11
  162. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  163. package/package.json +2 -2
  164. package/skills/AGENTS.md +1 -1
  165. package/skills/melonjs/SKILL.md +2 -0
  166. package/skills/melonjs-3d/SKILL.md +192 -18
  167. package/skills/melonjs-3d-assets/SKILL.md +36 -2
  168. package/skills/melonjs-audio/SKILL.md +229 -8
  169. package/skills/melonjs-camera-and-drawing/SKILL.md +73 -3
  170. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  171. package/skills/melonjs-effects-and-shaders/SKILL.md +4 -4
  172. package/skills/melonjs-events/SKILL.md +8 -6
  173. package/skills/melonjs-input/SKILL.md +100 -4
  174. package/skills/melonjs-math/SKILL.md +313 -0
  175. package/skills/melonjs-particles-and-trails/SKILL.md +88 -14
  176. package/skills/melonjs-performance/SKILL.md +50 -19
  177. package/skills/melonjs-physics/SKILL.md +2 -1
  178. package/skills/melonjs-renderables/SKILL.md +6 -1
  179. package/skills/melonjs-scenes-and-state/SKILL.md +14 -3
  180. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  181. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  182. package/skills/melonjs-ui-and-text/SKILL.md +382 -4
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-3d
3
- description: "Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal."
3
+ description: "Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -71,6 +71,86 @@ That flips the world's sort to `"depth"`, switches the broadphase to a 3D index,
71
71
  and renders sprites and meshes with perspective projection. `Camera3d` extends
72
72
  `Camera2d`, so follow, fade, shake and post effects all still work.
73
73
 
74
+ ### Keep the camera, do not fish it back out
75
+
76
+ `app.viewport` is the **active stage's** default camera, so it is typed
77
+ `Camera2d`: a stage overrides the application's `cameraClass`, and the
78
+ built-in loading screen does exactly that. Reading your `Camera3d` back out of
79
+ it therefore needs a cast, and a cast that is wrong while the loader is up:
80
+
81
+ ```js
82
+ const camera = app.viewport as Camera3d; // works, until it does not
83
+ ```
84
+
85
+ Own it instead. Pass the camera in and keep the typed reference, and nothing
86
+ downstream ever narrows:
87
+
88
+ ```js
89
+ class GameStage extends Stage {
90
+ readonly camera: Camera3d;
91
+
92
+ constructor(app) {
93
+ const camera = new Camera3d(0, 0, app.renderer.width, app.renderer.height);
94
+ super({ cameras: [camera] });
95
+ this.camera = camera;
96
+ }
97
+ }
98
+ ```
99
+
100
+ A freshly built camera is named `"default"`, and a stage registers its
101
+ `cameras` by name, so this IS the stage's default camera: `app.viewport`
102
+ returns the same instance.
103
+
104
+ ### The two patterns differ in LIFETIME, and that decides which you want
105
+
106
+ This is the part that bites. `cameraClass` builds a **fresh camera for each
107
+ stage**, which the engine does deliberately so "Camera3d state shouldn't
108
+ bleed across stages". An owned camera passed through `super({ cameras })` is
109
+ built once and lives as long as the stage object, which for a stage you
110
+ register with `state.set()` means as long as the application.
111
+
112
+ So anything you attach to the camera now outlives the visit:
113
+
114
+ ```js
115
+ // in onResetEvent — runs EVERY time the stage is entered
116
+ app.viewport.addPostEffect(bloom);
117
+ ```
118
+
119
+ With `cameraClass` that lands on a fresh camera each time and is harmless.
120
+ With an owned camera it stacks: a second bloom on the second visit, a third
121
+ on the third. The symptom is text and edges appearing to double or smear,
122
+ because the scene is being post-processed once per accumulated effect.
123
+
124
+ Note that `removePostEffect` DESTROYS the effect rather than detaching it, so
125
+ "just remove it on the way out" is not a free undo.
126
+
127
+ **Choose on lifetime, not on typing:**
128
+
129
+ | your effects / camera state are | use |
130
+ |---|---|
131
+ | set up per stage visit, in `onResetEvent` | `cameraClass` — fresh camera per stage |
132
+ | set up once, in the stage constructor | an owned camera in `super({ cameras })` |
133
+
134
+ If you want the typed reference but your setup is per-visit, keep
135
+ `cameraClass` and narrow once on reset:
136
+
137
+ ```js
138
+ override onResetEvent(app) {
139
+ const viewport = app.viewport;
140
+ if (!(viewport instanceof Camera3d)) {
141
+ throw new Error("this stage needs a Camera3d");
142
+ }
143
+ this.camera = viewport; // typed, checked, and still fresh per visit
144
+ }
145
+ ```
146
+
147
+ That is what the Earth Defender example does, and why: it attaches post
148
+ effects on every reset and never removes them.
149
+
150
+ Narrowing beats asserting wherever you read the shared slot: the language
151
+ verifies `instanceof`, and fails loudly if the application was never given a
152
+ `cameraClass`, where a cast fails silently at the first 3D call.
153
+
74
154
  Per-stage opt-in also works, which is useful when the loading screen is 2D:
75
155
 
76
156
  ```js
@@ -91,8 +171,9 @@ class GameStage extends Stage {
91
171
  Objects beyond the far plane clip or project with bad w-divides; a `near` that
92
172
  is too small wastes depth precision and distant props z-fight. Use
93
173
  `camera.setClipPlanes(near, far)`.
94
- - **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use `camera.depth` —
95
- the documented z accessor — or assign `pos.x`/`pos.y` individually.
174
+ - **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use
175
+ `camera.setPosition(x, y, z)`, which leaves the depth untouched when `z` is
176
+ omitted; `camera.depth` is the z accessor if you only want that.
96
177
  - **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot
97
178
  express "up is this direction" and it leaves `roll` alone. Pass an up to
98
179
  bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should
@@ -159,6 +240,52 @@ the same picture.** If a game already transforms its world into view space by
159
240
  hand, there is nothing to gain by switching; the value of `setBasis` is not
160
241
  having to write that transform in the first place.
161
242
 
243
+ ## Turning a direction: `slerp`, not `lerp`
244
+
245
+ `lerp` walks the straight line between the two tips, which passes INSIDE the
246
+ sphere. The result points the right way at both ends, lingers near them, rushes
247
+ the middle, and comes out shorter than either input as it crosses the midpoint.
248
+ On a camera swing that reads as a lurch; on a body walking over a globe it dips
249
+ into the surface.
250
+
251
+ `Vector3d#slerp` turns along the great circle instead, at a constant angular
252
+ rate, and carries the length across linearly, so two unit vectors give back a
253
+ unit vector and nothing needs renormalizing.
254
+
255
+ ```js
256
+ // a turret, a camera, a ship: a facing chasing another facing
257
+ facing.slerp(toTarget, 0.25);
258
+
259
+ // frame-rate independent, the same shape as math.damp
260
+ facing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));
261
+ ```
262
+
263
+ `ObservableVector3d` has it too and fires its callback ONCE, so
264
+ `renderable.pos.slerp(...)` is one notification rather than three.
265
+
266
+ **`Vector2d` has the same method**, and it is not a cut-down copy: in 2D the
267
+ whole turn is one signed angle, so there is no sine to divide by and no
268
+ degenerate near-parallel case at all. What it buys a 2D game is the wrap: a
269
+ heading damped as a NUMBER from 170 degrees to -170 degrees sweeps the long 340
270
+ instead of the short 20, and an arc between directions cannot do that.
271
+
272
+ Two degenerate pairs are handled inside rather than left to the caller, and
273
+ both turn up in ordinary game code:
274
+
275
+ - **the same direction**, where every path between them is the same path.
276
+ - **opposite directions**, where infinitely many great circles pass through
277
+ both and none is the shorter way round. The textbook formula divides by a
278
+ vanishing sine here and hands back `NaN`, which propagates into a position or
279
+ a matrix and the object disappears. `slerp` picks a stable perpendicular and
280
+ turns through half a circle instead.
281
+
282
+ That second case is why a hand-rolled version is a liability rather than a
283
+ shortcut: anything orbiting a planet reaches the antipode of where it started,
284
+ every run.
285
+
286
+ **`math.damp` for a scalar, `slerp` for a direction.** Same job, different
287
+ space; what `damp` cannot do is keep a vector on the sphere.
288
+
162
289
  ## Depth sorting
163
290
 
164
291
  Under `Camera3d` the world sorts on `"depth"` **every frame** (recursively), so
@@ -273,6 +400,12 @@ const glow = new Mesh(0, 0, {
273
400
  Ground shadows ride the same pass — a blob is a decal, and decals are its first
274
401
  client rather than a feature of their own.
275
402
 
403
+ **`Trail3d`** is the transparent pass's other obvious client: a camera-facing
404
+ ribbon for an exhaust streak, a speed trail or a tyre mark. It is a `Mesh`, so
405
+ it sorts and fogs with everything else here, and like the rest of this tier it
406
+ needs a `Camera3d`. `Trail` is the 2D one and stays flat under a perspective
407
+ camera. See `melonjs-particles-and-trails`.
408
+
276
409
  ## Distance fog
277
410
 
278
411
  Off until you ask for it, and one call on the camera:
@@ -456,7 +589,7 @@ There are four levels, and picking the wrong one is the usual reason a colour
456
589
 
457
590
  | level | how | use for |
458
591
  |---|---|---|
459
- | whole object | `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |
592
+ | whole object | `mesh.setTint("#ff8800")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |
460
593
  | per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |
461
594
  | per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |
462
595
  | per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |
@@ -671,6 +804,7 @@ is read **every draw**, so all of them can be animated.
671
804
  |---|---|---|
672
805
  | `castGroundShadow` | yes | yes |
673
806
  | `shadowGroundY` | yes, the caster's own floor | yes, **one plane** through the SET's origin |
807
+ | `shadowGroundPoint` | yes on a `Mesh` or `Sprite3d`; on a `GLTFModel` part only by setting it after the load, since it is neither a model nor a `level.load` option | **no**, warns and ignores |
674
808
  | `shadowGroundNormal` | yes | yes, one plane for the set |
675
809
  | `shadowOpacity` | yes | yes |
676
810
  | `shadowScale` | yes | yes |
@@ -681,13 +815,22 @@ is read **every draw**, so all of them can be animated.
681
815
  | height fade as the caster rises | yes | **no** |
682
816
  | a different value per instance | n/a | **no**, and there is no plan for one |
683
817
 
684
- The two "no"s are the same limitation seen twice: a set gets ONE shadow plane
685
- and ONE shared quad, so the instance's own height is dropped and nothing can
686
- vary between instances. Everything else is one value for the whole set, which
687
- is what a set means. A scatter over curved ground is therefore one set per flat
688
- facet, and a flying scatter wanting a height fade has to be per-object meshes.
689
-
690
- Nothing here is per-object-only. If a claim that a setting "does not work on an
818
+ The last two "no"s are the same limitation seen twice: a set gets ONE shadow
819
+ plane and ONE shared quad, so the instance's own height is dropped and nothing
820
+ can vary between instances. Everything else is one value for the whole set,
821
+ which is what a set means. A scatter over curved ground is therefore one set
822
+ per flat facet, and a flying scatter wanting a height fade has to be
823
+ per-object meshes.
824
+
825
+ `shadowGroundPoint` (20.9) is the one genuinely per-object setting, and the
826
+ reason is structural rather than missing work: the instanced tier builds its
827
+ plane THROUGH the set's own origin so the translation's x and z drop out of
828
+ the intercept, which is then `groundY + lift / nY`. An arbitrary point does
829
+ not drop out, and that same `nY` divisor is why the 75 degree tilt ceiling
830
+ cannot be lifted there either. `InstancedMesh` warns and ignores rather than
831
+ storing it, so a game cannot think it took.
832
+
833
+ Apart from that one, if a claim that a setting "does not work on an
691
834
  `InstancedMesh`" is in your head, it is out of date: `shadowOffset` and
692
835
  `shadowStretch` both reached the instanced tier in 20.8.
693
836
 
@@ -698,8 +841,27 @@ skip geometry with no vertical extent — a ground plane. Per object,
698
841
  given, safeguard included; `shadowGroundY` names the floor the blob lands on,
699
842
  `shadowOpacity` how dark it is and `shadowScale` how big it is.
700
843
 
844
+ On ground that is not flat, name the floor with `shadowGroundPoint` instead:
845
+ a point and `shadowGroundNormal` are a plane, and the blob leaves the caster's
846
+ vertical column to land on it. `shadowGroundY` cannot express that, because
847
+ the blob is always directly below the caster in world Y, which is only "below"
848
+ on level ground. It is also what lifts the tilt ceiling, since that ceiling
849
+ measures tilt from world up and so assumes a camera looking down the Y axis.
850
+
851
+ The height fade follows whichever floor you named. With a point **and** a
852
+ normal it is measured along that normal; with a point alone it is the
853
+ straight-line distance to the point. So on a sphere the blob fades with
854
+ ALTITUDE rather than with the vertical drop, which is the whole reason the
855
+ point exists. Two consequences worth knowing, because they fail in opposite
856
+ directions: a `shadowGroundNormal` facing inward makes the height negative, so
857
+ the fade clamps to full strength and the blob never fades at all; and a point
858
+ on the wrong side of the caster reads as a large height, so the blob
859
+ disappears outright rather than thinning to a ring the way a backwards
860
+ `shadowGroundY` does. It is also the one shadow setting that THROWS rather
861
+ than clamping or hiding: a non-finite component is rejected on the spot.
862
+
701
863
  The blob is an ellipse sized to the caster's own footprint and placed at the
702
- caster's x/z. So a tall or narrow object (a character, a tree, a pickup) shows
864
+ caster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows
703
865
  its shadow clearly, while a wide, flat-bottomed one resting on the floor covers
704
866
  its own completely from a camera looking down at it. That is the shadow
705
867
  behaving correctly, not a bug.
@@ -731,7 +893,8 @@ mesh.shadowStretch = 2; // and lengthen it along the same line
731
893
  `shadowOffset` is measured in multiples of the blob's **own radius**, not in
732
894
  world units, so one value serves a boulder and a pebble and every part of a
733
895
  glTF model. About `stretch - 1` is what keeps the shadow attached to its
734
- caster. It is honoured **only when `shadowGroundY` is set**, because the
896
+ caster. It is honoured **only when the floor has been named**, by
897
+ `shadowGroundY` or by `shadowGroundPoint`, because the
735
898
  blob is a flat quad on one named plane: sliding it across a plane the game has
736
899
  not named puts it where there may be no floor. `shadowStretch` is clamped to 3
737
900
  and the blob fades as it pulls, so an extreme value degrades to nothing rather
@@ -761,12 +924,18 @@ prop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0]; // a 20 deg slo
761
924
 
762
925
  World up is `(0, -1, 0)`, because render space is Y-down, and that is the
763
926
  default. The blob is **rotated** onto the plane rather than projected, so it
764
- keeps its size; it still sits directly under the caster, since the normal
765
- turns it rather than moving it. The tilt is clamped at 75 degrees, past which
766
- an edge-on blob has nothing left to show. It needs `shadowGroundY`, for the
767
- same reason the offset does: tilting the bounds fallback has no anchor to turn
927
+ keeps its size; it sits directly under the caster, since the normal turns it
928
+ rather than moving it, unless `shadowGroundPoint` says where instead. It needs
929
+ the floor named, by `shadowGroundY` or `shadowGroundPoint`, for the same
930
+ reason the offset does: tilting the bounds fallback has no anchor to turn
768
931
  about.
769
932
 
933
+ The tilt is clamped at 75 degrees alongside `shadowGroundY`, past which an
934
+ edge-on blob has nothing left to show. That ceiling measures tilt from WORLD
935
+ up, which stands in for "tilted away from the viewer" only while the camera
936
+ looks down the Y axis, so it is lifted alongside `shadowGroundPoint`, where a
937
+ point and a normal state the plane outright.
938
+
770
939
  On an `InstancedMesh` one plane serves the **whole set**: it passes through
771
940
  the set's origin at `shadowGroundY`, faces the normal, and every blob is
772
941
  projected onto it whatever height its own instance sits at, exactly as the
@@ -845,7 +1014,8 @@ Loaded through the same level director as everything else:
845
1014
  await loader.preload([{ name: "diorama", type: "glb", src: "data/diorama.glb" }]);
846
1015
  level.load("diorama", { scale, castGroundShadow, shadowGroundY, onLoaded });
847
1016
 
848
- // every ground-shadow setting travels, not just those two: a loaded scene can
1017
+ // every ground-shadow setting travels except shadowGroundPoint, which is
1018
+ // per object and dropped by a scene load: a loaded scene can
849
1019
  // be told where its sun is, and `shadowLight` is re-read every draw
850
1020
  level.load("diorama", {
851
1021
  scale, castGroundShadow: true, shadowGroundY: 0,
@@ -931,6 +1101,10 @@ To branch rather than fail, read `app.renderer.supportsDepthBuffer` after
931
1101
  | one marker must stay readable in fog | `fog: false` on that mesh |
932
1102
  | an object casts no visible shadow | wide and flat-bottomed — its own blob is underneath it; raise `shadowScale` (or slide it out with `shadowOffset`), not `shadowGroundY`, which haloes it instead of revealing it |
933
1103
  | a dark ring around the top of an object | `shadowGroundY` lifted too far, floating the blob up into the caster |
1104
+ | a tilted blob never fades as its caster rises | `shadowGroundNormal` faces inward, so the height came out negative and clamped to full strength |
1105
+ | a blob vanishes completely after setting `shadowGroundPoint` | the point is on the wrong side of the caster; the fade reads that as height and hides it |
1106
+ | `shadowGroundPoint` throws | it is the one shadow setting that rejects a bad value instead of clamping: all three components must be finite |
1107
+ | a scatter ignores `shadowGroundPoint` | `InstancedMesh` shares one plane across the set and warns; use one set per facet, or per-object meshes |
934
1108
  | a mesh sits at the wrong depth after being added | `autoDepth` overwrote `pos.z` with the child index — pass `addChild(mesh, z)` |
935
1109
  | a mesh sits half its size off | `anchorPoint` — only on the 2D-camera path; a `Camera3d` mesh pivots on its model origin |
936
1110
  | a billboard tips over when the camera looks down | `"spherical"`, or a mistyped mode string falling through to it — use `true` / `"cylindrical"` |
@@ -33,21 +33,35 @@ or it renders flat. See `melonjs-3d` for the camera.
33
33
  |---|---|---|
34
34
  | `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |
35
35
  | `container` | `game.world` | where the nodes are added |
36
- | `onLoaded` | `app.onLevelLoaded` | called with the **level id**, not the scene |
36
+ | `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |
37
37
  | `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |
38
38
  | `rightHanded` | `true` | see below |
39
39
  | `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |
40
40
  | `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |
41
+
42
+ Those `game.` defaults are the engine's, not a recommendation. `game` is
43
+ whichever `Application` finished `init()` last, so **pass `container`
44
+ explicitly** from a `Stage`, where the right one is in your hand:
45
+
46
+ ```js
47
+ level.load("sceneId", { container: this.app.world, scale: 40 });
48
+ ```
41
49
  | `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |
42
50
  | `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |
43
51
  | `shadowOpacity` | `0.45` | how dark the blobs are |
44
52
  | `shadowScale` | `1` | blob footprint multiplier; `0` or less hides them |
45
- | `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY` |
53
+ | `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY`, which is the only way a scene load can name its floor |
46
54
  | `shadowStretch` | `1` | lengthen them along it, clamped to 3 |
47
55
  | `shadowDirectionX` / `shadowDirectionZ` | `0` | the ground direction they are cast along |
48
56
  | `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |
49
57
  | `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |
50
58
 
59
+ `shadowGroundPoint` is **not** on that list, and a scene load drops it without
60
+ a warning. One floor POINT shared by every part of a scene would stack the
61
+ whole thing's blobs on a single spot, which is nonsense where one floor
62
+ HEIGHT is perfectly sensible. Ground that is not flat is a per-object answer:
63
+ set it on the parts you want once the load has finished.
64
+
51
65
  `onLoaded` receives the level id — it is a "done" signal, not a handle on the
52
66
  scene. You need it, or `async`: with the game loop running, `level.load` stops
53
67
  the loop and defers the actual load to a timer, so by default it returns
@@ -138,6 +152,26 @@ asset fails to load. Export uncompressed.
138
152
  it through. Pixel-art models want `"nearest"`.
139
153
 
140
154
  ### `baseColorFactor` is LINEAR; a `tint` is sRGB
155
+ **Pass the factor as a setting and the encoding is done for you.** `Mesh`
156
+ takes `baseColorFactor` — `[r, g, b]` in 0..1, linear, which is the form the
157
+ loader hands back — so a mesh built from a node passes the node's own field
158
+ straight through:
159
+
160
+ ```js
161
+ const node = loader.getGLTF("palm").nodes[0];
162
+ const mesh = new Mesh(x, y, {
163
+ vertices: node.vertices, normals: node.normals,
164
+ uvs: node.uvs, indices: node.indices, normalize: false,
165
+ baseColorFactor: node.baseColorFactor, // encoded, not scaled
166
+ });
167
+ ```
168
+
169
+ The setting's type admits `undefined`, because the loader declares the field
170
+ optional and that line has to compile under `exactOptionalPropertyTypes`.
171
+ `Sprite3d` forwards it too, and `mesh.setBaseColorFactor(f)` applies one after
172
+ construction. For an ordinary sRGB colour use `setTint` instead — the rest of
173
+ this section is why the two are different.
174
+
141
175
 
142
176
  The loader handles this for you. It only bites when you build meshes yourself
143
177
  from `loader.getGLTF(name).nodes` — which is what you do for per-node control,