melonjs 20.13.0 → 20.14.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 (120) hide show
  1. package/README.md +65 -22
  2. package/build/application/application.d.ts +6 -1
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/resize.d.ts.map +1 -1
  6. package/build/application/settings.d.ts +13 -8
  7. package/build/application/settings.d.ts.map +1 -1
  8. package/build/audio/audio.d.ts +8 -4
  9. package/build/audio/audio.d.ts.map +1 -1
  10. package/build/audio/backend/engine.d.ts.map +1 -1
  11. package/build/audio/playback.d.ts +2 -2
  12. package/build/audio/playback.d.ts.map +1 -1
  13. package/build/audio/spatial.d.ts.map +1 -1
  14. package/build/audio/types.d.ts +15 -5
  15. package/build/audio/types.d.ts.map +1 -1
  16. package/build/camera/camera2d.d.ts +10 -9
  17. package/build/camera/camera2d.d.ts.map +1 -1
  18. package/build/camera/camera3d.d.ts +11 -8
  19. package/build/camera/camera3d.d.ts.map +1 -1
  20. package/build/geometries/path2d.d.ts +9 -8
  21. package/build/geometries/path2d.d.ts.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +40993 -39738
  25. package/build/index.js.map +4 -4
  26. package/build/input/keyboard.d.ts.map +1 -1
  27. package/build/input/pointerevent.d.ts.map +1 -1
  28. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  29. package/build/level/level.d.ts.map +1 -1
  30. package/build/level/tiled/TMXLayer.d.ts +2 -1
  31. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  32. package/build/level/tiled/TMXTileMap.d.ts +13 -3
  33. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  34. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  35. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  36. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  37. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
  38. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  39. package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
  40. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  41. package/build/lighting/light3d.d.ts +55 -0
  42. package/build/lighting/light3d.d.ts.map +1 -1
  43. package/build/loader/cache.d.ts +146 -97
  44. package/build/loader/cache.d.ts.map +1 -1
  45. package/build/loader/internal-types.d.ts +69 -0
  46. package/build/loader/internal-types.d.ts.map +1 -0
  47. package/build/loader/loader.d.ts +584 -292
  48. package/build/loader/loader.d.ts.map +1 -1
  49. package/build/loader/parsers/audio.d.ts.map +1 -1
  50. package/build/loader/parsers/fontface.d.ts.map +1 -1
  51. package/build/loader/parsers/gltf.d.ts.map +1 -1
  52. package/build/loader/parsers/tmx.d.ts.map +1 -1
  53. package/build/math/color.d.ts +4 -0
  54. package/build/math/color.d.ts.map +1 -1
  55. package/build/math/noise.d.ts +16 -16
  56. package/build/math/noise.d.ts.map +1 -1
  57. package/build/physics/adapter.d.ts +64 -25
  58. package/build/physics/adapter.d.ts.map +1 -1
  59. package/build/physics/builtin/body.d.ts +9 -0
  60. package/build/physics/builtin/body.d.ts.map +1 -1
  61. package/build/physics/builtin/builtin-adapter.d.ts +25 -3
  62. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  63. package/build/physics/builtin/detector.d.ts.map +1 -1
  64. package/build/physics/builtin/raycast.d.ts.map +1 -1
  65. package/build/physics/world.d.ts +40 -10
  66. package/build/physics/world.d.ts.map +1 -1
  67. package/build/renderable/container.d.ts +1 -0
  68. package/build/renderable/container.d.ts.map +1 -1
  69. package/build/renderable/draggable.d.ts.map +1 -1
  70. package/build/renderable/frameAnimation.d.ts.map +1 -1
  71. package/build/renderable/instanced_mesh.d.ts +16 -14
  72. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  73. package/build/renderable/mesh.d.ts +62 -5
  74. package/build/renderable/mesh.d.ts.map +1 -1
  75. package/build/renderable/trigger.d.ts +5 -0
  76. package/build/renderable/trigger.d.ts.map +1 -1
  77. package/build/renderable/ui/uibaseelement.d.ts +8 -1
  78. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  79. package/build/renderable/ui/uicheckbox.d.ts.map +1 -1
  80. package/build/system/device.d.ts +11 -2
  81. package/build/system/device.d.ts.map +1 -1
  82. package/build/system/fullscreen.d.ts.map +1 -1
  83. package/build/system/pool.d.ts.map +1 -1
  84. package/build/utils/utils.d.ts.map +1 -1
  85. package/build/video/canvas/canvas_renderer.d.ts +16 -14
  86. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  87. package/build/video/canvas_factory.d.ts.map +1 -1
  88. package/build/video/effects/shadereffect.d.ts +127 -0
  89. package/build/video/effects/shadereffect.d.ts.map +1 -1
  90. package/build/video/renderer.d.ts.map +1 -1
  91. package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
  92. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  93. package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
  94. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/skills/melonjs/SKILL.md +38 -14
  97. package/skills/melonjs-20-migration/SKILL.md +50 -7
  98. package/skills/melonjs-3d/SKILL.md +138 -38
  99. package/skills/melonjs-3d-assets/SKILL.md +83 -33
  100. package/skills/melonjs-audio/SKILL.md +55 -14
  101. package/skills/melonjs-camera-and-drawing/SKILL.md +95 -8
  102. package/skills/melonjs-deployment/SKILL.md +92 -14
  103. package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
  104. package/skills/melonjs-effects-and-shaders/SKILL.md +46 -15
  105. package/skills/melonjs-events/SKILL.md +3 -2
  106. package/skills/melonjs-getting-started/SKILL.md +5 -0
  107. package/skills/melonjs-input/SKILL.md +65 -15
  108. package/skills/melonjs-lighting/SKILL.md +13 -1
  109. package/skills/melonjs-loading-assets/SKILL.md +202 -27
  110. package/skills/melonjs-math/SKILL.md +12 -5
  111. package/skills/melonjs-particles-and-trails/SKILL.md +7 -3
  112. package/skills/melonjs-performance/SKILL.md +18 -8
  113. package/skills/melonjs-physics/SKILL.md +385 -127
  114. package/skills/melonjs-plugins/SKILL.md +45 -11
  115. package/skills/melonjs-renderables/SKILL.md +24 -7
  116. package/skills/melonjs-renderer-backends/SKILL.md +38 -17
  117. package/skills/melonjs-scenes-and-state/SKILL.md +30 -5
  118. package/skills/melonjs-sprites-and-animation/SKILL.md +23 -2
  119. package/skills/melonjs-tilemaps/SKILL.md +56 -18
  120. package/skills/melonjs-ui-and-text/SKILL.md +120 -25
@@ -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, 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."
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, shadowGroundPoint, lit, emissive, specular, shininess, normalMap, alphaCutoff, depthTest, vertexColors, worldToScreen, followOffset, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -69,7 +69,8 @@ await app.init();
69
69
 
70
70
  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
- `Camera2d`, so follow, fade, shake and post effects all still work.
72
+ `Camera2d`, so follow, fade, shake and post effects all still work (follow with
73
+ a difference, see the camera traps).
73
74
 
74
75
  ### Keep the camera, do not fish it back out
75
76
 
@@ -193,8 +194,15 @@ class GameStage extends Stage {
193
194
  sense to `lookAt`'s, and one negation apart.
194
195
  - **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.
195
196
  To pin a label or marker to a point in the scene use
196
- `camera.worldToScreen(x, y, z)`, which applies the projection and returns
197
+ `camera.worldToScreen(point, out?)`: it takes a `Vector3d`, applies the
198
+ projection and returns a `Vector2d` in the camera's pixels (y down), or
197
199
  `null` behind the camera. See `melonjs-camera-and-drawing`.
200
+ - **`follow()` tracks exactly.** Under a `Camera3d` the camera sits at the
201
+ target's position plus `followOffset` (world space, set with
202
+ `camera.setFollowOffset(x, y, z)`) every frame. The 2D follow's axis,
203
+ deadzone and damping are not applied. For a damped chase camera, leave the
204
+ target unset and move the camera yourself (`math.damp`, or `slerp` below for
205
+ its facing).
198
206
 
199
207
  ## Where an up vector earns its keep
200
208
 
@@ -299,6 +307,11 @@ index** — a real world depth in a 3D scene, and never the one you wanted. Pass
299
307
  it (`world.addChild(mesh, z)`), or set `mesh.depth` afterwards. The glTF
300
308
  importer turns `autoDepth` off on the container it loads into for this reason.
301
309
 
310
+ A `Container`'s depth carries to its children when they are drawn, as it
311
+ already did for culling and the sort: a group at depth 400 draws its sprites
312
+ and meshes 400 units further away. One more reason to give containers an
313
+ explicit z.
314
+
302
315
  **`floating` does not opt out of this sort.** It skips the camera *transform*,
303
316
  not the depth *order*. A floating child is ordered by `|pos.z|` alone — its
304
317
  `pos.x/y` are screen pixels, and the camera does not move relative to it — so
@@ -327,8 +340,9 @@ Meshes render in two phases. The **opaque pass** writes depth in sort order; the
327
340
  depth. A draw lands in the second whenever its alpha is fractional.
328
341
 
329
342
  That default matters because the opaque path writes premultiplied colour with
330
- blending off, so before this a faded mesh came out **darkened toward black**
331
- rather than see-through — the background contributed nothing.
343
+ blending off, so a faded mesh that stays there (as every one did before 20.4)
344
+ comes out **darkened toward black** rather than see-through — the background
345
+ contributes nothing.
332
346
 
333
347
  **`transparent: true`** when the transparency is in the TEXTURE rather than the
334
348
  opacity — a soft-edged glow, smoke, a glTF material with `alphaMode: "BLEND"`.
@@ -376,8 +390,9 @@ const halo = new Sprite3d(0, 0, {
376
390
  });
377
391
  ```
378
392
 
379
- Honoured in the transparent pass on both GPU backends; the Canvas renderer has
380
- no depth buffer and ignores it.
393
+ Honoured in the **transparent pass only**, on both GPU backends: a mesh drawn
394
+ in the opaque pass ignores it, so pair it with `transparent: true` as above.
395
+ The Canvas renderer has no depth buffer and ignores it.
381
396
 
382
397
  Blending uses the renderable's existing `blendMode`, so a glow is one property.
383
398
  The advanced modes (`"overlay"`, `"difference"`, and the rest that need a
@@ -482,6 +497,8 @@ the built-in ones:
482
497
  uniform vec3 uFogColor; // straight (unpremultiplied) fog colour
483
498
  uniform vec4 uFogParams; // x = mode (0 off / 1 linear / 2 exp2),
484
499
  // y = near, z = 1/(far - near), w = density
500
+ uniform vec4 uFogHeight; // height falloff: xyz = falloff x world-up axis,
501
+ // in VIEW space; w = pre-baked altitude term
485
502
  ```
486
503
 
487
504
  Your vertex stage computes the distance itself — `length((uViewMatrix *
@@ -490,6 +507,12 @@ camera turns — and the blend must scale the fog colour by the fragment's own
490
507
  alpha, `mix(uFogColor * a, rgb, f)`, because `vColor` arrives premultiplied.
491
508
  Mixing toward the unscaled colour haloes every alpha-cutout edge.
492
509
 
510
+ `uFogHeight` is only needed to honour `heightFalloff`, and is fed only to a
511
+ program that also declares `uFogParams`. The engine's own shaders multiply the
512
+ distance by `uFogHeight.w * (exp(k) - 1.0) / k`, with `k = dot(uFogHeight.xyz,
513
+ viewPos)` and the factor taken as `uFogHeight.w` when `k` is near zero. Leave it
514
+ out and your mesh fogs uniformly at every altitude.
515
+
493
516
  The flip side is the reason fog costs nothing when unused: with no camera fog,
494
517
  the mesh programs are compiled without any of it, on both backends. It is not a
495
518
  branch that is skipped at runtime — the code is not there.
@@ -510,7 +533,32 @@ const cube = new Mesh(x, y, {
510
533
  `Camera3d` it opts out of the anchor offset entirely, so `anchorPoint` is inert
511
534
  there — place the origin where you want the pivot at authoring time, or nest the
512
535
  mesh under a transformed parent. The anchor is only honoured on the legacy
513
- 2D-camera path.
536
+ 2D-camera path. `Sprite3d` is the exception: it bakes its `anchorPoint` into
537
+ the quad's vertices, so a `"bottom"` anchor works under either camera.
538
+
539
+ ### Material settings
540
+
541
+ What a material file can state is also a setting, so a hand-built mesh gets the
542
+ same surface. All but `baseColorFactor` are GPU-only; the Canvas renderer
543
+ ignores them.
544
+
545
+ | setting | what it does | needs |
546
+ |---|---|---|
547
+ | `emissive: [r, g, b]` | self-illumination added on top, 0..1 (above 1 for an HDR glow) | nothing, lit or unlit |
548
+ | `specular: [r, g, b]` + `shininess` | a highlight; `shininess` 0, the default, turns it off however bright `specular` is | `lit`, a `Light3d`, normals |
549
+ | `normalMap` | tangent-space relief, a loader key or image (see Lighting below) | `lit` and a `Camera3d` |
550
+ | `alphaCutoff` | discard texels below this alpha: foliage, fences | nothing |
551
+ | `alphaMap` | per-texel opacity, multiplied in before the cutoff | a non-zero `alphaCutoff` |
552
+ | `baseColorFactor` | a LINEAR colour, encoded into `tint` (see `melonjs-3d-assets`) | nothing |
553
+
554
+ `emissive`, `specular` and `shininess` can be changed after construction too;
555
+ `setBaseColorFactor()` is the method form of the last row.
556
+
557
+ A `ShaderEffect` can shade a mesh as well: `mesh.addPostEffect(fx)` splices it
558
+ into the mesh shader as a colour hook on both GPU backends, so the mesh keeps its
559
+ placement, cutout, lighting and fog. A few builtins are unavailable there, and an
560
+ `InstancedMesh` cannot host one; both warn once. See
561
+ `melonjs-effects-and-shaders`.
514
562
 
515
563
  ### InstancedMesh, and when it is the wrong tool
516
564
 
@@ -578,9 +626,9 @@ vertices. Want smooth: share them.
578
626
  An explicit `settings.normals` always wins, and an unlit mesh gets none — there
579
627
  would be nothing to read them.
580
628
 
581
- **A lit mesh with no normals used to render fullbright**, which looks like the
582
- lighting is broken rather than absent. If an older scene suddenly picks up
583
- shading, that is why.
629
+ **A lit mesh with no normals used to render fullbright** (before 20.4), which
630
+ looks like the lighting is broken rather than absent. If an older scene suddenly
631
+ picks up shading, that is why.
584
632
 
585
633
  ## Colouring a mesh
586
634
 
@@ -591,7 +639,7 @@ There are four levels, and picking the wrong one is the usual reason a colour
591
639
  |---|---|---|
592
640
  | whole object | `mesh.setTint("#ff8800")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |
593
641
  | per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |
594
- | per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |
642
+ | per material | a multi-material OBJ + MTL | a model whose parts differ: each `Kd` is baked into the vertex colours, and only a different `map_Kd` costs a draw (one per `textureGroups` entry) |
595
643
  | per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |
596
644
 
597
645
  **`tint` is per object.** That is the trap: build a terrain as one big mesh and
@@ -629,10 +677,11 @@ On a lit mesh the colour multiplies the **lit** result, so it behaves as albedo
629
677
  rather than as an emissive override — a vertex colour will not make an unlit
630
678
  face bright.
631
679
 
632
- `alpha` hides a mesh as it hides anything else: at 0 the draw is skipped. There
633
- is no *partial* mesh transparency though — the mesh path renders opaque, so a
634
- mesh at `alpha = 0.5` draws fully opaque rather than half see-through. Fade a
635
- mesh out and it will stay solid until it vanishes.
680
+ `alpha` hides a mesh as it hides anything else: below 1/255 the draw is skipped.
681
+ A fractional `alpha` fades it, through the transparent pass described under
682
+ Transparency above, which needs a `Camera3d` on a GPU backend. Without that pass
683
+ (a 2D camera on a GPU backend, or `transparent: false`) the mesh path does not
684
+ blend, and a faded mesh darkens instead of turning see-through.
636
685
 
637
686
  ## Moving a mesh in 3D
638
687
 
@@ -649,6 +698,12 @@ rotates about that axis. Both write to `currentTransform`, a **`Matrix3d`**,
649
698
  which `Mesh` folds into its model matrix — so orienting a mesh costs a matrix
650
699
  update, not new geometry.
651
700
 
701
+ Under a `Camera3d` on a GPU backend the geometry is uploaded once and redrawn
702
+ from the GPU, so moving, turning, scaling or re-tinting needs no signal. Editing the geometry
703
+ itself does: after writing into `originalVertices`, the UVs, normals or
704
+ indices, set `mesh.needsUpdate = true`, or the change shows under a 2D camera and
705
+ not under the 3D one.
706
+
652
707
  This matters most for the case it is least obvious for: a horizontal plane.
653
708
  `Sprite3d` with `billboard: false` is a quad in the sprite plane — upright —
654
709
  and the way to lay one flat (water, a road, a shadow decal) is to rotate it a
@@ -657,13 +712,14 @@ geometry instead is tiling: a `Sprite3d` bakes its UVs 0..1 from its atlas
657
712
  frame, so a surface that needs `textureRepeat` across many world units still
658
713
  wants an explicit `vertices`/`uvs` mesh.
659
714
 
660
- ## The mesh pass is opaque
715
+ ## The opaque pass does not blend
661
716
 
662
717
  Worth stating plainly, because the symptom does not look like a blending
663
- problem: **the mesh pass disables blending**. A soft-edged texture — a glow, a
664
- sun, a halo, anything with a gradient alpha — composites as a hard-edged disc
665
- with a grey rim, which reads as a broken texture rather than as a missing
666
- feature.
718
+ problem: **the opaque mesh pass disables blending**, and a draw goes there
719
+ unless its alpha is fractional or it says `transparent: true`. A soft-edged
720
+ texture — a glow, a sun, a halo, anything with a gradient alpha — drawn at full
721
+ opacity composites as a hard-edged disc with a grey rim, which reads as a broken
722
+ texture rather than as a missing feature.
667
723
 
668
724
  `transparent: true` routes it through the transparent pass, and `blendMode:
669
725
  "additive"` on top of that makes a light source add to the sky instead of
@@ -741,11 +797,13 @@ world.addChild(new Light3d({ type: "spot", position: torch.pos, range: 400 }));
741
797
  ```
742
798
 
743
799
  `direction` and `position` take an `[x, y, z]` array **or** a `Vector3d`, and
744
- `color` takes a `Color`, a CSS string or an `[r, g, b]` array — so a value the
745
- game already holds can go straight in. Each is read, not retained: move your
746
- vector afterwards and the light stays where it was.
800
+ `color` takes a `Color`, a CSS string or an `[r, g, b]` array in **0..1** (the
801
+ glTF convention, not 0..255) — so a value the game already holds can go straight
802
+ in. The vectors are read, not retained: move your vector afterwards and the
803
+ light stays where it was. A `Color` is kept as given, so mutating it recolours
804
+ the light.
747
805
 
748
- Two things still bite, both silently:
806
+ Three things still bite, all silently:
749
807
 
750
808
  - `new Light3d(0, 0, {…})` — the constructor takes **options alone**. JavaScript
751
809
  drops the extra arguments, so `options` becomes the number `0` and every
@@ -766,6 +824,10 @@ Two things still bite, both silently:
766
824
 
767
825
  `position` follows the same convention: a lamp above the floor has a
768
826
  **smaller** y than the floor.
827
+ - **`position` and `direction` are in world coordinates, wherever the light
828
+ sits in the tree.** A `Light3d` added under a container does not move or turn
829
+ with it, and its own `pos` is never read. To carry a lamp with a car or a
830
+ torch, write the object's world position into `light.position` each frame.
769
831
 
770
832
  Use **both halves**: with a key light but no ambient, the shadow side of a mesh
771
833
  goes black. With no `Light3d` in the world at all, a `lit: true` mesh falls back
@@ -776,6 +838,10 @@ Other defaults worth knowing: `intensity` 1, `color` white, `range` 1000 (point
776
838
  and spot, a stylised quadratic falloff, not inverse-square),
777
839
  `innerConeAngle` 0 and `outerConeAngle` π/4 (spot).
778
840
 
841
+ Up to **32** directional, point and spot lights shade a mesh; ambient lights are
842
+ summed into one fill and do not count. Lights past the 32nd are dropped without
843
+ a warning.
844
+
779
845
  **`Light2d` is 2D-only** and produces visible artifacts under perspective
780
846
  projection. Do not combine it with `Camera3d`.
781
847
 
@@ -858,7 +924,10 @@ the fade clamps to full strength and the blob never fades at all; and a point
858
924
  on the wrong side of the caster reads as a large height, so the blob
859
925
  disappears outright rather than thinning to a ring the way a backwards
860
926
  `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.
927
+ than clamping or hiding: a non-finite component is rejected on the spot. A
928
+ two-component point (a `Vector2d`, a 2-element array) does not throw: its z
929
+ silently becomes 0. The point is copied, so moving the vector you passed moves
930
+ nothing; assign it again.
862
931
 
863
932
  The blob is an ellipse sized to the caster's own footprint and placed at the
864
933
  caster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows
@@ -1028,7 +1097,9 @@ level.load("diorama", {
1028
1097
  An **animated** asset instantiates as a `GLTFModel`, which is a game object as
1029
1098
  much as a scene: construct it straight from the parsed descriptor, and place it
1030
1099
  with `pos` / `depth` / `rotate` / `scale` like any other renderable — the
1031
- placement drives the whole rig and composes with the playing clip.
1100
+ placement drives the whole rig and composes with the playing clip, and still
1101
+ applies while the clip is paused, finished or was never played. `scale()`
1102
+ multiplies the `scale` option it was built with.
1032
1103
 
1033
1104
  ```js
1034
1105
  const model = new GLTFModel(loader.getGLTF("boat"), { scale: 40, lit: false });
@@ -1048,14 +1119,20 @@ animation (and the skinning that is out of scope), and OBJ/MTL.
1048
1119
 
1049
1120
  ## Requires a GPU backend
1050
1121
 
1051
- The whole 3D tier needs WebGPU or WebGL 2. The Canvas renderer has no depth
1052
- buffer, no perspective path and no `drawMesh`, so a `Camera3d` scene does not
1053
- render correctly there — usually a black canvas. `Light3d` and shader effects
1054
- are inert too. A `Mesh` under a *2D* camera still renders, CPU-projected by
1055
- painter's algorithm and unlit.
1122
+ The 3D tier is built for WebGPU or WebGL 2: target one of them. The Canvas
1123
+ renderer has no depth buffer and no perspective path, so a `Camera3d` scene
1124
+ does not render there — usually a black canvas. `Light3d` and shader effects
1125
+ are inert too.
1126
+
1127
+ A `Mesh` under a *2D* camera does render on Canvas, but only as a last resort:
1128
+ textured, CPU-projected, sorted back to front a triangle at a time (painter's
1129
+ algorithm) and unlit. Overlapping parts can show slivers of the wrong
1130
+ triangle, large textured faces are mapped without perspective correction, and
1131
+ it slows down as the triangle count grows. It keeps a 2D-camera game from going
1132
+ blank on a device with no GPU backend; do not design for it or tune on it.
1056
1133
 
1057
- `Application` does warn: constructing with a `cameraClass` whose
1058
- `defaultSortOn` is `"depth"` on a renderer with no depth buffer logs a
1134
+ `Application` does warn: `await app.init()` with a `cameraClass` whose
1135
+ `defaultSortOn` is `"depth"`, on a renderer with no depth buffer, logs a
1059
1136
  `console.warn`. But `video.AUTO` falls back silently to Canvas, so the strong
1060
1137
  gate is to ask for a GPU backend by name — `renderer: video.WEBGL` (or
1061
1138
  `video.WEBGPU`) makes `await app.init()` reject instead of misrendering:
@@ -1070,23 +1147,42 @@ await app.init();
1070
1147
  ```
1071
1148
 
1072
1149
  To branch rather than fail, read `app.renderer.supportsDepthBuffer` after
1073
- `init()`.
1150
+ `init()`: a `Camera3d` game should refuse Canvas there with a message, while a
1151
+ game whose meshes sit under a 2D camera can let it through as the fallback.
1152
+
1153
+ ```js
1154
+ const app = new Application(1024, 768, { parent: "screen", renderer: video.AUTO });
1155
+ await app.init();
1156
+ if (!app.renderer.supportsDepthBuffer) {
1157
+ // the Canvas fallback: no GPU backend on this device. Meshes under the
1158
+ // 2D camera still draw (CPU-projected, unlit); keep the scene small, or
1159
+ // tell the player the game needs WebGL 2 or WebGPU.
1160
+ }
1161
+ ```
1074
1162
 
1075
1163
  ## Symptom → cause
1076
1164
 
1077
1165
  | symptom | cause |
1078
1166
  |---|---|
1079
1167
  | scene lit from underneath | `direction` Y sign — this is Y-down, so a sun overhead is **+Y** |
1080
- | a `lit` mesh renders fullbright | it had no normals — supply them, or let the engine generate them |
1168
+ | `model.scale(2)` throws "not a function" on a `GLTFModel` (before 20.14) | the `scale` option hid the method: update |
1169
+ | a `GLTFModel` ignores `pos` / `rotate` once its clip is paused or finished (before 20.14) | it was only re-posed while a clip played: update |
1170
+ | a sprite or mesh in a container draws too near under `Camera3d`, and pops in and out at the wrong time (before 20.14) | the container's depth was used to cull and sort its children but not to draw them: update |
1171
+ | a lit mesh inside a moved or turned container is lit as if the container were at the origin (before 20.14) | lights were not taken into the container's frame: update. Under a *scaled* container, a point light's range is measured in the container's units |
1172
+ | objects at the screen edge pop in a frame late while the camera turns, or a camera cut shows one empty frame (before 20.14) | culling used the camera as it was at the last `camera.update()`: update |
1173
+ | a `lit` mesh renders fullbright | no `Light3d` in the world, or a 2D camera (which warns once). Before 20.4 a mesh with no normals did it too; they are generated now |
1081
1174
  | a gradient across one mesh is impossible | `tint` is per object — use `vertexColors` / `setVertexColor` |
1082
- | a mesh stays solid as you fade it out | meshes render opaque; only `alpha` 0 (hidden) and 1 differ |
1083
1175
  | a glow is cut along a hard straight line, worst near a round object's limb | a flat billboard being depth tested against the geometry behind it — `depthTest: false` |
1084
- | vertex colour applies under a 2D camera but not `Camera3d` | wrote the array directly without setting `needsUpdate` |
1176
+ | `depthTest: false` changes nothing | the mesh draws in the opaque pass, which ignores it — add `transparent: true` |
1177
+ | `specular` is set but no highlight appears | `shininess` is 0 (the default), or the mesh is not `lit` |
1178
+ | `setInstanceColor` does nothing | the set was built without `instanceColors: true`; the slot does not exist |
1179
+ | a vertex colour or a geometry edit applies under a 2D camera but not `Camera3d` | wrote the array directly without setting `needsUpdate` |
1085
1180
  | `Mesh: vertexColors has N entries, expected M` | one colour per *vertex*, not per triangle or per index |
1086
1181
  | nothing renders, or a backdrop covers everything | wrong depth sign — "far" is *larger* z when looking along +Z |
1087
1182
  | distant objects vanish or warp | scene exceeds the default far plane; `setClipPlanes` |
1088
1183
  | distant surfaces z-fight | `near` too small for the scene scale |
1089
1184
  | black canvas under `Camera3d` | Canvas renderer (no depth buffer) — check the `console.warn` |
1185
+ | a 2D-camera mesh shows slivers where it overlaps itself, or slows with many triangles | the Canvas fallback, a last resort: ask for `video.WEBGL` or `video.WEBGPU` |
1090
1186
  | everything flat and unlit | `lit: true` with no `Light3d` in the world (falls back to fullbright), or a mesh under a 2D camera |
1091
1187
  | a faded mesh goes dark instead of see-through | `transparent: false` on it, or a 2D camera — the transparent pass needs a `Camera3d` |
1092
1188
  | a soft-edged glow has hard edges | `alphaCutoff` discarded the soft texels; lower it |
@@ -1114,9 +1210,13 @@ To branch rather than fail, read `app.renderer.supportsDepthBuffer` after
1114
1210
  | `setBasis` throws "forward must have a non-zero length" | `forward` is all zeros, the one case that names no direction; a short one is fine, so a surface frame that shrank as it drifted still works |
1115
1211
  | roll is ignored when the target is straight up or down | no rotation about the view axis is determined there, so `lookAt` keeps the roll it had and `setBasis` falls back to the `right` axis |
1116
1212
  | an object jumps to the camera plane | `Vector3d.set(x, y)` / `camera.pos.set(x, y)` zeroed its z |
1213
+ | a follow camera ignores its deadzone and damping | `Camera3d` follows exactly, at the target plus `followOffset`; damp it yourself with no target set |
1214
+ | a `Light3d` parented to a moving object stays behind | `position` is in world coordinates and its container is not applied; copy the world position into it each frame |
1117
1215
  | lighting looks wrong under a 3D camera | `Light2d` used instead of `Light3d` |
1118
1216
 
1119
1217
  ## Related skills
1120
1218
 
1219
+ - `melonjs-3d-assets` — glTF/GLB and OBJ/MTL loading, imported materials and lights
1220
+ - `melonjs-effects-and-shaders` — a `ShaderEffect` hosted on a mesh, and custom mesh shaders
1121
1221
  - `melonjs-renderables` — the 2D z-ordering rules, anchors, custom draw
1122
1222
  - `melonjs-getting-started` — Application settings and the renderer fallback
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-3d-assets
3
- description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options including the async flag, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, skinning, Blender export, 3D asset."
3
+ description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options including the async flag, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, alphaMode, doubleSided, map_d, map_bump, normalize, animationspeed, skinning, Blender export, 3D asset."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -36,16 +36,8 @@ or it renders flat. See `melonjs-3d` for the camera.
36
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
- | `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |
39
+ | `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` children of `container`; `false` also builds the meshes unlit |
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
- ```
49
41
  | `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |
50
42
  | `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |
51
43
  | `shadowOpacity` | `0.45` | how dark the blobs are |
@@ -56,6 +48,16 @@ level.load("sceneId", { container: this.app.world, scale: 40 });
56
48
  | `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |
57
49
  | `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |
58
50
 
51
+ Those `game.` defaults are the engine's, not a recommendation. `game` is
52
+ whichever `Application` finished `init()` last, so **pass `container`
53
+ explicitly** from a `Stage`, where the right one is in your hand:
54
+
55
+ ```js
56
+ onResetEvent(app) {
57
+ level.load("sceneId", { container: app.world, scale: 40 });
58
+ }
59
+ ```
60
+
59
61
  `shadowGroundPoint` is **not** on that list, and a scene load drops it without
60
62
  a warning. One floor POINT shared by every part of a scene would stack the
61
63
  whole thing's blobs on a single spot, which is nonsense where one floor
@@ -102,9 +104,10 @@ world.addChild(new Trigger(x, y, {
102
104
 
103
105
  Every `level.load` option above travels, including the whole ground-shadow set,
104
106
  and so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is
105
- **derived** from `level.loadOptions` rather than restated, so an option added to
106
- a level format arrives here for free. It used to be hand-written and had fallen
107
- three releases behind the glTF path (#1649).
107
+ **derived** from the options each level format declares beside the code that
108
+ reads them, rather than restated, so an option added to a level format arrives
109
+ here for free. It used to be hand-written and had fallen three releases behind
110
+ the glTF path (#1649).
108
111
 
109
112
  **`async` is deliberately excluded.** A trigger with a `color` + `duration`
110
113
  sets it itself, because it needs the promise to sequence its fade and reveal; a
@@ -132,24 +135,34 @@ Do not add a flip of your own on top; two corrections cancel back to wrong.
132
135
  The importer covers a deliberate subset:
133
136
 
134
137
  - `pbrMetallicRoughness` **base colour texture and factor**
138
+ - `COLOR_0` **vertex colours**, multiplied with the factor and the texture
135
139
  - metallic/roughness **factors** converted to the engine's specular model
136
140
  - `emissiveFactor`, including `KHR_materials_emissive_strength`
137
141
  - `KHR_materials_unlit` — honoured, for baked/stylised workflows
138
142
  - `alphaMode: "MASK"` — the cutout threshold is applied
139
143
  - `doubleSided`
144
+ - the base colour texture's sampler: `wrapS` / `wrapT` (repeat by default, as
145
+ the spec says) and `magFilter`
146
+
147
+ `alphaMode: "BLEND"` is read as opaque: the loader does not set `transparent`,
148
+ because one loaded mesh can merge materials and the flag routes the whole mesh.
149
+ Set `transparent: true` on the part yourself (see `melonjs-3d`).
140
150
 
141
151
  Not imported: **normal maps**, metallic/roughness *textures*, occlusion maps,
142
152
  morph targets, `KHR_texture_transform`. Nothing errors — the material simply
143
153
  arrives without them, which reads as "flatter than the preview in my authoring
144
- tool". Bake what you need into the base colour.
154
+ tool". Bake what you need into the base colour. (OBJ/MTL does carry a normal
155
+ map, see below.)
145
156
 
146
157
  **Draco compression is the exception that does error.** Its geometry sits in
147
158
  accessors with no buffer view, which the parser rejects outright, so the whole
148
159
  asset fails to load. Export uncompressed.
149
160
 
150
- `textureFilter` on the Application settings (`"nearest"` / `"linear"` /
151
- `"auto"`) drives sampling; glTF materials that declare their own sampler carry
152
- it through. Pixel-art models want `"nearest"`.
161
+ `textureFilter` on the Application settings (`"auto"` / `"nearest"` /
162
+ `"linear"` / `"linear-mipmap"`) drives sampling; glTF materials that declare
163
+ their own sampler carry it through. Pixel-art models want `"nearest"`. A mesh
164
+ already samples mipmaps under `"linear"`, so `"linear-mipmap"` changes nothing
165
+ for one.
153
166
 
154
167
  ### `baseColorFactor` is LINEAR; a `tint` is sRGB
155
168
  **Pass the factor as a setting and the encoding is done for you.** `Mesh`
@@ -222,8 +235,10 @@ screen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.
222
235
  ## Lights
223
236
 
224
237
  Authored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`
225
- world children, plus a soft ambient fill, and each keeps its authored name.
226
- A scene carrying any of them renders through the **lit** path automatically.
238
+ children of the container the scene loads into, plus a soft white ambient fill
239
+ at intensity 0.3, and each keeps its authored name. A scene carrying any of them
240
+ renders through the **lit** path automatically (except `KHR_materials_unlit`
241
+ materials); one carrying none renders unlit.
227
242
 
228
243
  glTF stores physical units — lux for suns, candela for lamps — and a Blender
229
244
  daylight sun is 1000+, which blows out the engine's stylised shading. So every
@@ -235,7 +250,10 @@ set, scale instead of normalising:
235
250
  level.load("diorama", { scale: 50, lightIntensityScale: 0.001 });
236
251
  ```
237
252
 
238
- Pass `lights: false` to manage lighting yourself.
253
+ Pass `lights: false` to leave the authored lights out. The meshes are then built
254
+ **unlit**, so a `Light3d` of your own does not shade them either. To light a
255
+ scene your own way, keep the authored lights and retune them by name, or remove
256
+ them once the load has finished and add yours: the meshes stay lit.
239
257
 
240
258
  ## Animation
241
259
 
@@ -258,13 +276,18 @@ model.pause();
258
276
 
259
277
  `speed` is a playback multiplier (1 = authored speed), not a frame delay. Only
260
278
  an explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a
261
- clip name the asset does not define — check `getAnimationNames()` first.
279
+ clip name the asset does not define — check `getAnimationNames()` first. Called
280
+ with the clip that is already playing it returns at once and ignores its
281
+ options, so retime a running clip through `model.animationspeed`, not a second
282
+ `setCurrentAnimation(name, { speed })`.
262
283
 
263
284
  ### Placing and moving a model
264
285
 
265
286
  A `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`
266
287
  and `scale` move the **whole rig**, and compose with whatever the active clip
267
- is doing, so a walk cycle plays wherever the character stands:
288
+ is doing, so a walk cycle plays wherever the character stands. They apply just
289
+ the same while the clip is paused, finished or was never played, and `scale()`
290
+ multiplies the `scale` option the model was built with:
268
291
 
269
292
  ```js
270
293
  const boat = new me.GLTFModel(me.loader.getGLTF("boat"), { scale: 40, lit: false });
@@ -295,6 +318,11 @@ hundred thousand. Export with instancing enabled where your tool supports it.
295
318
  Sparse accessors on the instancing attributes are rejected with a clear error
296
319
  rather than silently misplacing every instance.
297
320
 
321
+ Face culling is decided once per mesh, so instances with a **negative scale**
322
+ (mirrored linked duplicates) render inside-out. Building the scene or model
323
+ warns when it meets them; mark the material `doubleSided`, which turns culling
324
+ off for the set.
325
+
298
326
  ## Ground shadows
299
327
 
300
328
  ```js
@@ -353,7 +381,7 @@ world.addChild(mesh);
353
381
  ```
354
382
 
355
383
  **The MTL loader fetches the textures it references itself** — `map_Kd`,
356
- `map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for
384
+ `map_d` and the normal map are resolved relative to the `.mtl` and preloaded for
357
385
  you, so there is no image entry in the manifest above. Passing `texture:`
358
386
  explicitly overrides that and pins *one* binding over the whole model, which on
359
387
  a multi-material model suppresses the per-material texture split; leave it out
@@ -361,19 +389,23 @@ unless you mean that.
361
389
 
362
390
  MTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),
363
391
  `Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`
364
- (per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal
365
- map — needs `lit: true`, and the tangent frame is derived per fragment, so the
366
- model needs no tangent attribute). All three normal-map spellings are read as
367
- normal maps even though the format specifies two of them as height maps, which
368
- is what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR
369
- extension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`
370
- *scalars* are read and approximated onto the specular terms, the maps would
371
- need a real PBR model.
392
+ (per-texel opacity) and `map_bump` / `map_Bump` / `bump` / `norm`
393
+ (tangent-space normal map — needs `lit: true`, and the tangent frame is derived
394
+ per fragment, so the model needs no tangent attribute). Every normal-map
395
+ spelling is read as a normal map even though the format specifies `bump` and
396
+ `map_bump` as height maps, because a normal map is what exporters actually
397
+ write there. Specular *maps* (`map_Ks`) and the PBR extension's `map_Pr` /
398
+ `map_Pm` are not supported and warn — the `Pr` / `Pm` *scalars* are read and
399
+ approximated onto the specular terms, the maps would need a real PBR model.
400
+
401
+ `map_d` cuts out only against a non-zero `alphaCutoff`, and the loader sets
402
+ none: pass one in the `Mesh` settings (`alphaCutoff: 0.5`), or nothing is cut
403
+ out.
372
404
 
373
405
  `Ka` (ambient) and `illum` (illumination model) are read onto the material and
374
406
  readable from `loader.getMTL(name)`, but take no part in shading. That is
375
407
  deliberate rather than pending: `Ka` predates scene-wide ambient light and would
376
- fight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function
408
+ fight an `"ambient"` `Light3d`, and `illum` enumerates fixed-function
377
409
  behaviours this renderer has no equivalent for. In particular an `illum` of 0 or
378
410
  1 nominally means "no specular", and the engine does **not** honour that — `Ns`
379
411
  stays the only gate, so a model that renders correctly today is not restyled by
@@ -469,11 +501,19 @@ const node = me.loader.getGLTF("palm").nodes[0];
469
501
  const trees = new me.InstancedMesh(0, groundY, {
470
502
  vertices: node.vertices, uvs: node.uvs,
471
503
  normals: node.normals, indices: node.indices,
504
+ normalize: false, // keep the authored units; see below
472
505
  texture: palette, textureFilter: "nearest", scale: 40,
473
506
  instanceCount: count,
474
507
  });
475
508
  ```
476
509
 
510
+ **Pass `normalize: false` whenever the geometry comes off a descriptor.** The
511
+ default fits the geometry into a unit cube, centred, which throws away the
512
+ authored size and the base-on-the-floor origin. Before 20.14 it was worse: the
513
+ default normalized the loader's own array in place, so every later mesh built
514
+ from that node, and a `level.load` of the same asset, got the shrunken
515
+ geometry. A normalized mesh works on a copy now.
516
+
477
517
  `nodes` is one entry **per primitive**, each in its own local space with a
478
518
  separate `world` matrix — so this only works cleanly when the asset is a single
479
519
  merged primitive exported with an identity node transform. Model it at the
@@ -515,8 +555,16 @@ need a prefix.
515
555
  | the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |
516
556
  | everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |
517
557
  | materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |
558
+ | a glTF `alphaMode: "BLEND"` material draws opaque | the loader does not set `transparent`; set it on that part yourself |
559
+ | an MTL `map_d` cuts nothing out | it needs a non-zero `alphaCutoff`, which the loader does not set |
560
+ | a mesh built from `getGLTF(name).nodes` comes out unit-sized | `normalize` defaults to `true`; pass `normalize: false` to keep the authored units |
561
+ | after one normalized mesh of a model, the scene or other meshes of that model load shrunken (before 20.14) | the default normalized the loader's cached array in place; update, or pass `normalize: false` |
562
+ | some copies in an imported scatter render inside-out | negative-scale (mirrored) instances under per-mesh face culling; make the material `doubleSided` |
518
563
  | a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |
519
564
  | animation names come back empty | the asset has no node-TRS channels (skin-only rig) |
565
+ | `setCurrentAnimation(name, { speed })` changes nothing | that clip is already playing, so the call returns at once; set `animationspeed` |
566
+ | `model.scale(2)` throws "not a function" on a `GLTFModel` (before 20.14) | the `scale` option hid the method: update |
567
+ | a `GLTFModel` ignores `pos` / `rotate` once its clip is paused or finished (before 20.14) | it was only re-posed while a clip played: update |
520
568
  | a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |
521
569
  | a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |
522
570
  | three clips where you authored one | one action per object; use a single action with a slot each |
@@ -530,12 +578,14 @@ need a prefix.
530
578
  | a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |
531
579
  | `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |
532
580
  | scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |
581
+ | scene renders in perspective but unlit | the asset carries no sun, point or spot light (or was loaded with `lights: false`), so its meshes are built unlit; author one in the asset and tune it at runtime by name |
533
582
 
534
583
  ## Related skills
535
584
 
536
585
  - `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and
537
586
  `camera.setFog` (an outdoor scene almost always wants it; it is set on the
538
587
  camera, so a loaded scene needs no per-node work)
539
- - `melonjs-lighting` — how `Light3d` behaves once imported
588
+ - `melonjs-lighting` — the 2D `Light2d` system; imported lights are `Light3d`,
589
+ covered in `melonjs-3d`
540
590
  - `melonjs-loading-assets` — the loader, asset types and base URLs
541
591
  - `melonjs-plugins` — the Spine plugin, for skeletal characters