melonjs 20.7.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 (332) hide show
  1. package/README.md +7 -3
  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 +6 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/header.d.ts.map +1 -1
  7. package/build/application/resize.d.ts +25 -0
  8. package/build/application/resize.d.ts.map +1 -1
  9. package/build/application/scaleMethods.d.ts +13 -3
  10. package/build/application/scaleMethods.d.ts.map +1 -1
  11. package/build/application/settings.d.ts +144 -5
  12. package/build/application/settings.d.ts.map +1 -1
  13. package/build/audio/audio.d.ts +29 -5
  14. package/build/audio/audio.d.ts.map +1 -1
  15. package/build/audio/autopause.d.ts +36 -0
  16. package/build/audio/autopause.d.ts.map +1 -0
  17. package/build/audio/backend/engine.d.ts +10 -1
  18. package/build/audio/backend/engine.d.ts.map +1 -1
  19. package/build/audio/backend/sound.d.ts +0 -1
  20. package/build/audio/backend/sound.d.ts.map +1 -1
  21. package/build/audio/backend/spatial.d.ts.map +1 -1
  22. package/build/audio/context.d.ts +35 -0
  23. package/build/audio/context.d.ts.map +1 -0
  24. package/build/audio/playback.d.ts.map +1 -1
  25. package/build/audio/procedural.d.ts.map +1 -1
  26. package/build/audio/spatial.d.ts +87 -0
  27. package/build/audio/spatial.d.ts.map +1 -0
  28. package/build/audio/state.d.ts +1 -0
  29. package/build/audio/state.d.ts.map +1 -1
  30. package/build/audio/types.d.ts +46 -6
  31. package/build/audio/types.d.ts.map +1 -1
  32. package/build/camera/camera2d.d.ts.map +1 -1
  33. package/build/camera/camera3d.d.ts +95 -8
  34. package/build/camera/camera3d.d.ts.map +1 -1
  35. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  36. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  37. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  38. package/build/geometries/box3d.d.ts +25 -7
  39. package/build/geometries/box3d.d.ts.map +1 -1
  40. package/build/geometries/ellipse.d.ts +15 -1
  41. package/build/geometries/ellipse.d.ts.map +1 -1
  42. package/build/geometries/line.d.ts +1 -1
  43. package/build/geometries/line.d.ts.map +1 -1
  44. package/build/geometries/point.d.ts +1 -1
  45. package/build/geometries/point.d.ts.map +1 -1
  46. package/build/geometries/polygon.d.ts +15 -1
  47. package/build/geometries/polygon.d.ts.map +1 -1
  48. package/build/geometries/rectangle.d.ts +1 -1
  49. package/build/geometries/rectangle.d.ts.map +1 -1
  50. package/build/geometries/roundrect.d.ts +1 -1
  51. package/build/geometries/roundrect.d.ts.map +1 -1
  52. package/build/geometries/sphere.d.ts +36 -7
  53. package/build/geometries/sphere.d.ts.map +1 -1
  54. package/build/index.d.ts +16 -3
  55. package/build/index.d.ts.map +1 -1
  56. package/build/index.js +38172 -33633
  57. package/build/index.js.map +4 -4
  58. package/build/input/gamepad.d.ts +129 -3
  59. package/build/input/gamepad.d.ts.map +1 -1
  60. package/build/input/pointerevent.d.ts +8 -0
  61. package/build/input/pointerevent.d.ts.map +1 -1
  62. package/build/level/gltf/GLTFModel.d.ts +20 -33
  63. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  64. package/build/level/gltf/GLTFScene.d.ts +40 -1
  65. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  66. package/build/level/level.d.ts +35 -0
  67. package/build/level/level.d.ts.map +1 -1
  68. package/build/level/tiled/TMXLayer.d.ts +5 -2
  69. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  70. package/build/level/tiled/TMXObject.d.ts +0 -1
  71. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  72. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  73. package/build/level/tiled/TMXTile.d.ts +2 -1
  74. package/build/level/tiled/TMXTile.d.ts.map +1 -1
  75. package/build/level/tiled/TMXTileMap.d.ts +7 -4
  76. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  77. package/build/level/tiled/TMXTileset.d.ts +1 -1
  78. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  79. package/build/level/tiled/TMXTilesetGroup.d.ts +5 -0
  80. package/build/level/tiled/TMXTilesetGroup.d.ts.map +1 -1
  81. package/build/level/tiled/factories/shape.d.ts +0 -1
  82. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts +6 -5
  83. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  84. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts +5 -0
  85. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  86. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +5 -7
  87. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  88. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts +5 -0
  89. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  90. package/build/level/tiled/renderer/TMXRenderer.d.ts +19 -4
  91. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  92. package/build/level/tiled/renderer/autodetect.d.ts +0 -4
  93. package/build/level/tiled/renderer/autodetect.d.ts.map +1 -1
  94. package/build/loader/cache.d.ts +220 -0
  95. package/build/loader/cache.d.ts.map +1 -1
  96. package/build/loader/loader.d.ts +20 -340
  97. package/build/loader/loader.d.ts.map +1 -1
  98. package/build/loader/loadingscreen.d.ts +1 -2
  99. package/build/loader/loadingscreen.d.ts.map +1 -1
  100. package/build/loader/parsers/gltf.d.ts +115 -0
  101. package/build/loader/parsers/gltf.d.ts.map +1 -1
  102. package/build/loader/parsers/shader.d.ts +0 -2
  103. package/build/math/color.d.ts +28 -5
  104. package/build/math/color.d.ts.map +1 -1
  105. package/build/math/matrix2d.d.ts +1 -1
  106. package/build/math/matrix2d.d.ts.map +1 -1
  107. package/build/math/matrix3d.d.ts +1 -1
  108. package/build/math/matrix3d.d.ts.map +1 -1
  109. package/build/math/observableVector2d.d.ts +14 -2
  110. package/build/math/observableVector2d.d.ts.map +1 -1
  111. package/build/math/observableVector3d.d.ts +15 -1
  112. package/build/math/observableVector3d.d.ts.map +1 -1
  113. package/build/math/vector2d.d.ts +31 -1
  114. package/build/math/vector2d.d.ts.map +1 -1
  115. package/build/math/vector3d.d.ts +38 -1
  116. package/build/math/vector3d.d.ts.map +1 -1
  117. package/build/math/vertex.d.ts.map +1 -1
  118. package/build/particles/emitter.d.ts.map +1 -1
  119. package/build/particles/particle.d.ts +29 -2
  120. package/build/particles/particle.d.ts.map +1 -1
  121. package/build/particles/settings.d.ts +104 -0
  122. package/build/particles/settings.d.ts.map +1 -1
  123. package/build/physics/adapter.d.ts +9 -1
  124. package/build/physics/adapter.d.ts.map +1 -1
  125. package/build/physics/bounds.d.ts +1 -1
  126. package/build/physics/bounds.d.ts.map +1 -1
  127. package/build/physics/broadphase/octree.d.ts +24 -0
  128. package/build/physics/broadphase/octree.d.ts.map +1 -1
  129. package/build/physics/broadphase/quadtree.d.ts +31 -0
  130. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  131. package/build/physics/builtin/body.d.ts +17 -2
  132. package/build/physics/builtin/body.d.ts.map +1 -1
  133. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  134. package/build/physics/builtin/detector.d.ts +0 -1
  135. package/build/physics/builtin/detector.d.ts.map +1 -1
  136. package/build/physics/builtin/sat.d.ts +0 -7
  137. package/build/physics/builtin/sat3d.d.ts +46 -0
  138. package/build/physics/builtin/sat3d.d.ts.map +1 -1
  139. package/build/physics/response.d.ts +45 -62
  140. package/build/physics/response.d.ts.map +1 -1
  141. package/build/pool.d.ts +4 -0
  142. package/build/pool.d.ts.map +1 -1
  143. package/build/renderable/collectable.d.ts +6 -1
  144. package/build/renderable/collectable.d.ts.map +1 -1
  145. package/build/renderable/colorlayer.d.ts +17 -4
  146. package/build/renderable/colorlayer.d.ts.map +1 -1
  147. package/build/renderable/container.d.ts +23 -17
  148. package/build/renderable/container.d.ts.map +1 -1
  149. package/build/renderable/entity/entity.d.ts +12 -16
  150. package/build/renderable/entity/entity.d.ts.map +1 -1
  151. package/build/renderable/frameAnimation.d.ts +1 -1
  152. package/build/renderable/groundshadow.d.ts +1 -0
  153. package/build/renderable/groundshadow.d.ts.map +1 -1
  154. package/build/renderable/imagelayer.d.ts +12 -5
  155. package/build/renderable/imagelayer.d.ts.map +1 -1
  156. package/build/renderable/instanced_mesh.d.ts +56 -7
  157. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  158. package/build/renderable/mesh.d.ts +369 -4
  159. package/build/renderable/mesh.d.ts.map +1 -1
  160. package/build/renderable/nineslicesprite.d.ts +3 -13
  161. package/build/renderable/nineslicesprite.d.ts.map +1 -1
  162. package/build/renderable/renderable.d.ts +129 -28
  163. package/build/renderable/renderable.d.ts.map +1 -1
  164. package/build/renderable/sprite.d.ts +7 -20
  165. package/build/renderable/sprite.d.ts.map +1 -1
  166. package/build/renderable/sprite3d.d.ts +31 -16
  167. package/build/renderable/sprite3d.d.ts.map +1 -1
  168. package/build/renderable/text/bitmaptext.d.ts +76 -5
  169. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  170. package/build/renderable/text/bitmaptextdata.d.ts +1 -5
  171. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  172. package/build/renderable/text/glypheffect.d.ts +44 -0
  173. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  174. package/build/renderable/text/text.d.ts +22 -3
  175. package/build/renderable/text/text.d.ts.map +1 -1
  176. package/build/renderable/text/textmetrics.d.ts +2 -0
  177. package/build/renderable/text/textmetrics.d.ts.map +1 -1
  178. package/build/renderable/trail.d.ts +0 -1
  179. package/build/renderable/trail.d.ts.map +1 -1
  180. package/build/renderable/trail3d.d.ts +174 -0
  181. package/build/renderable/trail3d.d.ts.map +1 -0
  182. package/build/renderable/trigger.d.ts +28 -5
  183. package/build/renderable/trigger.d.ts.map +1 -1
  184. package/build/renderable/ui/panelpaint.d.ts +5 -0
  185. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  186. package/build/renderable/ui/progressbar.d.ts +192 -0
  187. package/build/renderable/ui/progressbar.d.ts.map +1 -0
  188. package/build/renderable/ui/uibaseelement.d.ts +48 -3
  189. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  190. package/build/renderable/ui/uipanel.d.ts +245 -0
  191. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  192. package/build/renderable/ui/uispriteelement.d.ts +19 -3
  193. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  194. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  195. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  196. package/build/state/state.d.ts.map +1 -1
  197. package/build/system/bootstrap.d.ts.map +1 -1
  198. package/build/system/legacy_pool.d.ts +24 -4
  199. package/build/system/legacy_pool.d.ts.map +1 -1
  200. package/build/system/pool.d.ts +15 -8
  201. package/build/system/pool.d.ts.map +1 -1
  202. package/build/tweens/tween.d.ts +50 -3
  203. package/build/tweens/tween.d.ts.map +1 -1
  204. package/build/video/canvas/canvas_renderer.d.ts +34 -23
  205. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  206. package/build/video/effects/blendEffect.d.ts +0 -1
  207. package/build/video/effects/bloom.d.ts +72 -0
  208. package/build/video/effects/bloom.d.ts.map +1 -0
  209. package/build/video/effects/blur.d.ts +3 -2
  210. package/build/video/effects/blur.d.ts.map +1 -1
  211. package/build/video/effects/chromaticAberration.d.ts +3 -2
  212. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  213. package/build/video/effects/colorMatrix.d.ts +3 -2
  214. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  215. package/build/video/effects/desaturate.d.ts +10 -2
  216. package/build/video/effects/desaturate.d.ts.map +1 -1
  217. package/build/video/effects/dissolve.d.ts +3 -2
  218. package/build/video/effects/dissolve.d.ts.map +1 -1
  219. package/build/video/effects/dropShadow.d.ts +3 -2
  220. package/build/video/effects/dropShadow.d.ts.map +1 -1
  221. package/build/video/effects/flash.d.ts +3 -2
  222. package/build/video/effects/flash.d.ts.map +1 -1
  223. package/build/video/effects/glow.d.ts +3 -2
  224. package/build/video/effects/glow.d.ts.map +1 -1
  225. package/build/video/effects/hologram.d.ts +3 -2
  226. package/build/video/effects/hologram.d.ts.map +1 -1
  227. package/build/video/effects/invert.d.ts +10 -2
  228. package/build/video/effects/invert.d.ts.map +1 -1
  229. package/build/video/effects/outline.d.ts +3 -2
  230. package/build/video/effects/outline.d.ts.map +1 -1
  231. package/build/video/effects/pixelate.d.ts +3 -2
  232. package/build/video/effects/pixelate.d.ts.map +1 -1
  233. package/build/video/effects/radialGradient.d.ts +8 -3
  234. package/build/video/effects/radialGradient.d.ts.map +1 -1
  235. package/build/video/effects/scanline.d.ts +3 -2
  236. package/build/video/effects/scanline.d.ts.map +1 -1
  237. package/build/video/effects/sepia.d.ts +10 -2
  238. package/build/video/effects/sepia.d.ts.map +1 -1
  239. package/build/video/effects/shadereffect.d.ts +10 -4
  240. package/build/video/effects/shadereffect.d.ts.map +1 -1
  241. package/build/video/effects/shine.d.ts +3 -2
  242. package/build/video/effects/shine.d.ts.map +1 -1
  243. package/build/video/effects/tintPulse.d.ts +3 -7
  244. package/build/video/effects/tintPulse.d.ts.map +1 -1
  245. package/build/video/effects/tonemap.d.ts +121 -0
  246. package/build/video/effects/tonemap.d.ts.map +1 -0
  247. package/build/video/effects/vignette.d.ts +3 -2
  248. package/build/video/effects/vignette.d.ts.map +1 -1
  249. package/build/video/effects/wave.d.ts +3 -7
  250. package/build/video/effects/wave.d.ts.map +1 -1
  251. package/build/video/gpu/batcher.d.ts +8 -2
  252. package/build/video/gpu/batcher.d.ts.map +1 -1
  253. package/build/video/gpu/vertexformat.d.ts +26 -0
  254. package/build/video/gpu/vertexformat.d.ts.map +1 -1
  255. package/build/video/gradient.d.ts +1 -1
  256. package/build/video/gradient.d.ts.map +1 -1
  257. package/build/video/renderer.d.ts +111 -7
  258. package/build/video/renderer.d.ts.map +1 -1
  259. package/build/video/renderstate.d.ts +2 -0
  260. package/build/video/renderstate.d.ts.map +1 -1
  261. package/build/video/rendertarget/canvasrendertarget.d.ts +3 -4
  262. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  263. package/build/video/rendertarget/render_target_pool.d.ts +0 -1
  264. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  265. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  266. package/build/video/rendertarget/webglrendertarget.d.ts +0 -1
  267. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  268. package/build/video/rendertarget/webgpurendertarget.d.ts +24 -14
  269. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  270. package/build/video/texture/atlas.d.ts +2 -1
  271. package/build/video/texture/atlas.d.ts.map +1 -1
  272. package/build/video/texture/cache.d.ts +1 -3
  273. package/build/video/texture/frametexture.d.ts +0 -1
  274. package/build/video/texture/frametexture.d.ts.map +1 -1
  275. package/build/video/texture/noise_texture2d.d.ts +1 -0
  276. package/build/video/texture/noise_texture2d.d.ts.map +1 -1
  277. package/build/video/texture/texture2d.d.ts +6 -0
  278. package/build/video/texture/texture2d.d.ts.map +1 -1
  279. package/build/video/utils/autodetect.d.ts +0 -2
  280. package/build/video/webgl/batchers/batcher.d.ts +0 -9
  281. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  282. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -0
  283. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  284. package/build/video/webgl/batchers/material_batcher.d.ts +4 -2
  285. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  286. package/build/video/webgl/batchers/mesh_batcher.d.ts +0 -3
  287. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  288. package/build/video/webgl/batchers/quad_batcher.d.ts +4 -0
  289. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  290. package/build/video/webgl/buffer/index.d.ts +0 -1
  291. package/build/video/webgl/buffer/retained_geometry.d.ts +0 -1
  292. package/build/video/webgl/glshader.d.ts +5 -3
  293. package/build/video/webgl/glshader.d.ts.map +1 -1
  294. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts +0 -4
  295. package/build/video/webgl/texture/store.d.ts +0 -1
  296. package/build/video/webgl/webgl_renderer.d.ts +63 -24
  297. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  298. package/build/video/webgpu/batchers/mesh_batcher.d.ts +0 -3
  299. package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
  300. package/build/video/webgpu/batchers/quad_batcher.d.ts +0 -2
  301. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  302. package/build/video/webgpu/batchers/webgpu_batcher.d.ts +0 -2
  303. package/build/video/webgpu/batchers/webgpu_batcher.d.ts.map +1 -1
  304. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  305. package/build/video/webgpu/renderers/tmxlayer/orthogonal.d.ts +0 -2
  306. package/build/video/webgpu/texture/frametexture.d.ts +0 -2
  307. package/build/video/webgpu/texture/frametexture.d.ts.map +1 -1
  308. package/build/video/webgpu/texture/store.d.ts +0 -1
  309. package/build/video/webgpu/webgpu_renderer.d.ts +7 -75
  310. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  311. package/package.json +2 -2
  312. package/skills/AGENTS.md +1 -1
  313. package/skills/melonjs/SKILL.md +2 -0
  314. package/skills/melonjs-3d/SKILL.md +454 -22
  315. package/skills/melonjs-3d-assets/SKILL.md +111 -10
  316. package/skills/melonjs-audio/SKILL.md +229 -8
  317. package/skills/melonjs-camera-and-drawing/SKILL.md +127 -1
  318. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  319. package/skills/melonjs-effects-and-shaders/SKILL.md +341 -29
  320. package/skills/melonjs-events/SKILL.md +8 -6
  321. package/skills/melonjs-input/SKILL.md +147 -7
  322. package/skills/melonjs-math/SKILL.md +313 -0
  323. package/skills/melonjs-particles-and-trails/SKILL.md +189 -13
  324. package/skills/melonjs-performance/SKILL.md +131 -23
  325. package/skills/melonjs-physics/SKILL.md +103 -28
  326. package/skills/melonjs-renderables/SKILL.md +8 -2
  327. package/skills/melonjs-scenes-and-state/SKILL.md +134 -3
  328. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  329. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  330. package/skills/melonjs-ui-and-text/SKILL.md +546 -7
  331. package/build/level/gltf/srgb.d.ts +0 -20
  332. package/build/level/gltf/srgb.d.ts.map +0 -1
@@ -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."
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
 
@@ -20,11 +20,15 @@ This is the single most important thing on this page.
20
20
  | vertical | **Y-down** — higher `y` is *lower* on screen | Y-up |
21
21
  | depth | **+Z forward** — higher `z` is *farther* away | −Z forward |
22
22
 
23
- Rotations are extrinsic XYZ, and `Camera3d` exposes all three: `camera.pitch`
23
+ `Camera3d` exposes three rotation angles: `camera.pitch`
24
24
  (X, look up/down), `camera.yaw` (Y, look left/right) and `camera.roll` (Z, bank
25
25
  the horizon). The view is `R(yaw) ∘ R(pitch) ∘ R(roll)` inverted, and the frustum
26
26
  planes come off that same matrix — so culling follows a banked view.
27
27
 
28
+ The basis those angles produce is **readable and writable**: `getBasis(right,
29
+ up, forward)` reads it, `setBasis(right, up, forward)` poses the camera from
30
+ one.
31
+
28
32
  `camera.rotate()` is NOT the way to bank a 3D camera. It writes the inherited
29
33
  `currentTransform`, which a 3D view never reads, so the call is silently inert.
30
34
  Set `roll`. (On a `Camera2d` it is the other way round: roll IS that transform's
@@ -67,6 +71,86 @@ That flips the world's sort to `"depth"`, switches the broadphase to a 3D index,
67
71
  and renders sprites and meshes with perspective projection. `Camera3d` extends
68
72
  `Camera2d`, so follow, fade, shake and post effects all still work.
69
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
+
70
154
  Per-stage opt-in also works, which is useful when the loading screen is 2D:
71
155
 
72
156
  ```js
@@ -87,13 +171,121 @@ class GameStage extends Stage {
87
171
  Objects beyond the far plane clip or project with bad w-divides; a `near` that
88
172
  is too small wastes depth precision and distant props z-fight. Use
89
173
  `camera.setClipPlanes(near, far)`.
90
- - **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use `camera.depth` —
91
- 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.
177
+ - **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot
178
+ express "up is this direction" and it leaves `roll` alone. Pass an up to
179
+ bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should
180
+ appear UP ON SCREEN. This engine renders Y-down, so a level horizon is
181
+ `(0, -1, 0)`. There is deliberately no default, because giving one would
182
+ silently level every camera that had banked its own horizon.
183
+ - **When the game already has all three axes, hand them over**:
184
+ `camera.setBasis(right, up, forward)`, the writable counterpart of
185
+ `getBasis`. The case that needs it is a view over a curved surface, where up
186
+ is the surface normal and differs at every point, so no pitch and yaw pair
187
+ expresses it and integrating towards one drifts and gimbals at the poles.
188
+ `setBasis` absorbs a triple that has drifted off orthonormal rather than
189
+ leaving the camera holding something that is not a rotation, and it does not
190
+ care what the axes are scaled to: a direction is a direction, so anything
191
+ from a denormal to 1e300 resolves the same way. Note its `up` is the camera's
192
+ local +Y column, which under Y-down points DOWN the screen: the opposite
193
+ sense to `lookAt`'s, and one negation apart.
92
194
  - **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.
93
195
  To pin a label or marker to a point in the scene use
94
196
  `camera.worldToScreen(x, y, z)`, which applies the projection and returns
95
197
  `null` behind the camera. See `melonjs-camera-and-drawing`.
96
198
 
199
+ ## Where an up vector earns its keep
200
+
201
+ Three cases. In the first two, `lookAt(target)` on its own is already right and
202
+ an up would be noise. The third is the one it exists for.
203
+
204
+ ```js
205
+ // 1. ordinary third-person: world up never changes, so say nothing
206
+ camera.setPosition(x, y, z).lookAt(player);
207
+
208
+ // 2. a deliberate dutch angle: this is a ROLL, not an up vector
209
+ camera.lookAt(player);
210
+ camera.roll = Math.PI / 32;
211
+
212
+ // 3. a view over a curved surface: up is the surface normal, and it is
213
+ // different at every point on the globe. No pitch and yaw pair expresses
214
+ // it, and integrating towards it drifts and gimbals over the poles.
215
+ const up = player.pos.clone().sub(planetCentre).normalize(); // surface normal
216
+ camera.setPosition(eye.x, eye.y, eye.z).lookAt(player, up.negate());
217
+ ```
218
+
219
+ Note the `negate()` in case 3. The surface normal points away from the planet,
220
+ which is the direction the player's head points, and `up` wants what should
221
+ appear UP ON SCREEN. This engine renders Y-down, so `(0, -1, 0)` is a level
222
+ horizon and a world-space "up" direction has to be handed over in that sense.
223
+ Get it backwards and the view is upside down, which is the single most likely
224
+ mistake with this argument.
225
+
226
+ If the game already computes all three axes, skip the solve entirely:
227
+
228
+ ```js
229
+ // a surface frame the game is already maintaining
230
+ camera.setBasis(right, up, forward);
231
+ ```
232
+
233
+ `setBasis` takes the camera's OWN columns, where `up` is local +Y and so points
234
+ down the screen: the opposite sense to `lookAt`'s, one negation apart. It also
235
+ absorbs a frame that has drifted off orthonormal, which one integrated over
236
+ thousands of steps will have.
237
+
238
+ **A camera posed this way and a world rotated into a fixed camera's frame are
239
+ the same picture.** If a game already transforms its world into view space by
240
+ hand, there is nothing to gain by switching; the value of `setBasis` is not
241
+ having to write that transform in the first place.
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
+
97
289
  ## Depth sorting
98
290
 
99
291
  Under `Camera3d` the world sorts on `"depth"` **every frame** (recursively), so
@@ -153,6 +345,40 @@ geometry with an opaque one would drag the opaque half into the transparent pass
153
345
 
154
346
  **`transparent: false`** pins a mesh to the opaque pass however it is faded.
155
347
 
348
+ ### `depthTest: false` for something with no surface to be occluded on
349
+
350
+ `transparent` decides the PASS, and the transparent pass turns depth *writing*
351
+ off on its own so overlapping blended draws blend instead of fighting. Whether
352
+ geometry in front of a mesh **hides** it is a separate question, and
353
+ `depthTest` is where you answer it. The default, `true`, is what you want for
354
+ anything genuinely in the scene — a muzzle flash behind a wall should still be
355
+ behind the wall, and this is not a synonym for "additive".
356
+
357
+ Set it `false` for a mesh standing in for a screen-space effect: an additive
358
+ glow carrying a world position only so it can sort and move with the thing it
359
+ belongs to. A world-space caption billboarded over a prop wants it for the
360
+ same reason, since it has no surface for the prop's own foliage to be in front
361
+ of. Left depth tested, a flat billboard is sliced along a hard straight
362
+ line the moment any geometry is nearer at some pixel. That shows up worst
363
+ around something round — a glow on a rock near a planet's limb gets cut,
364
+ and no offset escapes it, because the near surface bulges further toward the
365
+ camera than any offset you would dare apply.
366
+
367
+ ```js
368
+ const halo = new Sprite3d(0, 0, {
369
+ image: glowTexture,
370
+ width: rock.radius * 3, height: rock.radius * 3,
371
+ billboard: "spherical",
372
+ blendMode: "additive",
373
+ transparent: true, // the blended pass
374
+ alphaCutoff: 0, // do not cut the soft falloff
375
+ depthTest: false, // and do not let the world slice it
376
+ });
377
+ ```
378
+
379
+ Honoured in the transparent pass on both GPU backends; the Canvas renderer has
380
+ no depth buffer and ignores it.
381
+
156
382
  Blending uses the renderable's existing `blendMode`, so a glow is one property.
157
383
  The advanced modes (`"overlay"`, `"difference"`, and the rest that need a
158
384
  compositing pass) fall back to `"normal"` here, on both backends:
@@ -174,6 +400,12 @@ const glow = new Mesh(0, 0, {
174
400
  Ground shadows ride the same pass — a blob is a decal, and decals are its first
175
401
  client rather than a feature of their own.
176
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
+
177
409
  ## Distance fog
178
410
 
179
411
  Off until you ask for it, and one call on the camera:
@@ -233,10 +465,14 @@ mesh shaders, so a screen-space overlay stays clean with no work.
233
465
  **A custom mesh shader is not fogged unless it asks to be.** Fog is compiled
234
466
  into the engine's own mesh programs — `#define FOG` on WebGL, an `enable_fog`
235
467
  overridable constant on WebGPU — and a shader you supply is yours: the engine
236
- binds it as written and never substitutes a fogged variant. So a mesh carrying a
237
- `ShaderEffect` keeps full contrast while the scene around it recedes. It is safe
238
- — nothing throws, and the camera's fog is simply not applied — but it is usually
239
- surprising.
468
+ binds it as written and never substitutes a fogged variant. So a mesh given its
469
+ own `GLShader` keeps full contrast while the scene around it recedes. It is
470
+ safe — nothing throws, and the camera's fog is simply not applied — but it is
471
+ usually surprising.
472
+
473
+ A hosted `ShaderEffect` is the other case and needs nothing: its body is
474
+ spliced into the engine's own mesh program as a colour hook, and fog is applied
475
+ to the hook's result, so such a mesh fogs like any other.
240
476
 
241
477
  To opt in, declare the same uniforms and the engine will feed them, because the
242
478
  fog values are pushed to any mesh program that declares them rather than only to
@@ -353,7 +589,7 @@ There are four levels, and picking the wrong one is the usual reason a colour
353
589
 
354
590
  | level | how | use for |
355
591
  |---|---|---|
356
- | 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 |
357
593
  | per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |
358
594
  | per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |
359
595
  | per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |
@@ -475,6 +711,22 @@ Billboarding needs a `Camera3d` drawing the frame; under a 2D camera the quad
475
711
  renders fixed-orientation. **Any other string falls through to the spherical
476
712
  branch**, so a guessed value like `"upright"` silently gives you spherical.
477
713
 
714
+ A billboard takes its **orientation** from the camera, so `rotate()` has no
715
+ effect on one: a card already turned to face the camera has no free rotation
716
+ left to give. Everything that is not an orientation still applies, `scale()`
717
+ and `meshScale` included, so animating the size of a shockwave or a pickup is
718
+ the ordinary call and the cull bounds grow with it:
719
+
720
+ ```js
721
+ const wave = new Sprite3d(x, y, { image: "ring", width: 64, height: 64,
722
+ billboard: "spherical", transparent: true });
723
+ wave.scale(1 + 3 * t); // grows; `scale` accumulates, so drive it
724
+ // from the authored size, not per frame
725
+ ```
726
+
727
+ For an in-plane rotation, a bolt streak or an exhaust, use `billboard: false`
728
+ and orient the quad yourself.
729
+
478
730
  ## Lighting
479
731
 
480
732
  `Light3d` takes **one argument — the options object** (it has no `x, y, z`
@@ -543,23 +795,167 @@ the model needs no tangent attribute, and an unmapped mesh is unaffected. This
543
795
  is the 3D counterpart of the `Sprite` normal maps in the lighting skill; they
544
796
  are separate systems and a `Mesh` uses this one.
545
797
 
798
+ ### What the ground-shadow tier can and cannot do
799
+
800
+ Check this table before claiming a setting is unsupported. Every one of these
801
+ is read **every draw**, so all of them can be animated.
802
+
803
+ | setting | `Mesh` / `Sprite3d` / `GLTFModel` part | `InstancedMesh` |
804
+ |---|---|---|
805
+ | `castGroundShadow` | yes | yes |
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 |
808
+ | `shadowGroundNormal` | yes | yes, one plane for the set |
809
+ | `shadowOpacity` | yes | yes |
810
+ | `shadowScale` | yes | yes |
811
+ | `shadowOffset` | yes | yes, one throw for the set |
812
+ | `shadowStretch` | yes | yes, one stretch for the set |
813
+ | `shadowDirectionX` / `Z` | yes | yes |
814
+ | `shadowLight` | yes | yes |
815
+ | height fade as the caster rises | yes | **no** |
816
+ | a different value per instance | n/a | **no**, and there is no plan for one |
817
+
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
834
+ `InstancedMesh`" is in your head, it is out of date: `shadowOffset` and
835
+ `shadowStretch` both reached the instanced tier in 20.8.
836
+
546
837
  Ground shadows are **on by default** (the `castGroundShadow` application
547
838
  setting), and need a GPU backend and a `Camera3d`. As a blanket default they
548
839
  skip geometry with no vertical extent — a ground plane. Per object,
549
840
  `castGroundShadow: true`/`false` overrides the app setting and is obeyed as
550
- given, safeguard included; `shadowGroundY` names the floor the blob lands on.
841
+ given, safeguard included; `shadowGroundY` names the floor the blob lands on,
842
+ `shadowOpacity` how dark it is and `shadowScale` how big it is.
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.
551
862
 
552
863
  The blob is an ellipse sized to the caster's own footprint and placed at the
553
- caster's x/z — it is **never offset by light direction**. So a tall or narrow
554
- object (a character, a tree, a pickup) shows its shadow clearly, while a wide,
555
- flat-bottomed one resting on the floor covers its own completely from a camera
556
- looking down at it. That is the shadow behaving correctly, not a bug.
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
865
+ its shadow clearly, while a wide, flat-bottomed one resting on the floor covers
866
+ its own completely from a camera looking down at it. That is the shadow
867
+ behaving correctly, not a bug.
557
868
 
558
869
  **Do not chase it by raising `shadowGroundY`.** Lifting the plane does not slide
559
870
  the blob out from under the object, it floats the blob *up* — and past a few
560
- units it projects over the top of the caster as a dark halo ringing it. If an
561
- object needs a visible shadow, give it a smaller footprint relative to its
562
- height, or accept that a boulder bedded in the ground has none.
871
+ units it projects over the top of the caster as a dark halo ringing it.
872
+
873
+ The simplest fix is a bigger blob, so its edge shows past the object:
874
+
875
+ ```js
876
+ mesh.shadowScale = 1.5; // 1 is the default; 0 hides the shadow
877
+ ```
878
+
879
+ `shadowScale` works everywhere, `InstancedMesh` included. Changing it on a
880
+ set rewrites the shared quad's four corners in place rather than rebuilding
881
+ it, as does a `shadowLight` that moves, so an animated sun costs no
882
+ allocation.
883
+
884
+ Or, for the look of a sun that is not overhead, use the shape controls:
885
+
886
+ ```js
887
+ mesh.shadowOffset = 1.5; // slide it out, in blob radii
888
+ mesh.shadowDirectionX = 1; // the ground direction it is cast along
889
+ mesh.shadowDirectionZ = 0;
890
+ mesh.shadowStretch = 2; // and lengthen it along the same line
891
+ ```
892
+
893
+ `shadowOffset` is measured in multiples of the blob's **own radius**, not in
894
+ world units, so one value serves a boulder and a pebble and every part of a
895
+ glTF model. About `stretch - 1` is what keeps the shadow attached to its
896
+ caster. It is honoured **only when the floor has been named**, by
897
+ `shadowGroundY` or by `shadowGroundPoint`, because the
898
+ blob is a flat quad on one named plane: sliding it across a plane the game has
899
+ not named puts it where there may be no floor. `shadowStretch` is clamped to 3
900
+ and the blob fades as it pulls, so an extreme value degrades to nothing rather
901
+ than to a smear.
902
+
903
+ An `InstancedMesh` honours both, as **one** offset and **one** stretch for the
904
+ whole set, which is all a shared set can carry. Both ride the quad the blobs
905
+ share rather than the matrix that also places them: a stretch in the matrix
906
+ would scale the instance positions too and smear the scatter instead of
907
+ lengthening its blobs. So a scatter can sit next to loose props under the same
908
+ sun without looking lit from somewhere else.
909
+
910
+ `shadowLight` takes the direction from a `Light3d` you name, re-read every
911
+ draw, so a moving sun carries the shadows with it. Nothing is inferred: the
912
+ engine never picks a dominant light, because a scene with several has no
913
+ non-arbitrary answer and one with none has no answer at all.
914
+
915
+ ### A floor that is not level
916
+
917
+ `shadowGroundY` says where the floor is; `shadowGroundNormal` says which way
918
+ it faces:
919
+
920
+ ```js
921
+ prop.shadowGroundY = floorY;
922
+ prop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0]; // a 20 deg slope
923
+ ```
924
+
925
+ World up is `(0, -1, 0)`, because render space is Y-down, and that is the
926
+ default. The blob is **rotated** onto the plane rather than projected, so it
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
931
+ about.
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
+
939
+ On an `InstancedMesh` one plane serves the **whole set**: it passes through
940
+ the set's origin at `shadowGroundY`, faces the normal, and every blob is
941
+ projected onto it whatever height its own instance sits at, exactly as the
942
+ level case puts every blob at one height. A scatter over curved ground is
943
+ therefore one set per flat facet, each with the facet's own plane, not one
944
+ set with an averaged normal.
945
+
946
+ All three compose on a set: the stretch turns the shared quad, the offset
947
+ slides the whole set along the light, and both are carried onto the tilted
948
+ plane rather than applied flat, so a bank's planting lies along its slope
949
+ instead of hovering across it.
950
+
951
+ Give blobs to the plants that stand apart. Dense low ground cover is better
952
+ left without: a thousand blobs each as wide as its own fern sum to a darkened
953
+ hillside, not to a thousand shadows, and ground cover is the ground.
954
+
955
+ These are **art direction, not a projection**. A stretched ellipse is not a
956
+ silhouette and has no contact with terrain, so on ground that is not the plane
957
+ you named it will not lie on it. Direction-correct shadows want a shadow map,
958
+ which this tier does not do.
563
959
 
564
960
  ### Get the sign right: the floor is a GREATER y
565
961
 
@@ -587,10 +983,28 @@ caster's own y and check which one is larger.
587
983
 
588
984
  ## Collision in 3D
589
985
 
590
- Use `Box3d` bodies, not a hand-rolled distance check. `Box3d`-vs-`Box3d` is
591
- the engine's 3D narrowphase and the only contact that pushes back along Z; the
592
- response carries `overlapNZ` / `overlapZ` for the depth axis. See the physics
593
- skill for the contract and the mixed-pair caveat.
986
+ Use a body with a depth shape, not a hand-rolled distance check. Two shapes
987
+ carry depth — `Box3d` and `Sphere` — and any pair of them is resolved in three
988
+ dimensions; the response carries `overlapNZ` / `overlapZ` for the depth axis.
989
+
990
+ Pick by whether the thing turns. `Box3d` is world-axis-aligned and cannot be
991
+ oriented, so it fits walls, floors and crates and misfits anything that
992
+ tumbles. `Sphere` has no orientation at all, which makes it the shape for
993
+ spinning debris, projectiles, and anything spread over a curved surface: on a
994
+ globe each object's own "up" points a different way, so one axis-aligned box
995
+ cannot fit any of them.
996
+
997
+ A hand-rolled check is the usual mistake here and it costs twice — it throws
998
+ away the penetration vector the response already carries, and it is invisible
999
+ to the debug plugin's hitbox overlay, which draws from `body.shapes`. If the
1000
+ red boxes never appear, the reason is usually that there is no body to draw.
1001
+
1002
+ Against a planar shape, either 3D shape degrades to its XY footprint with the
1003
+ planar side read as unbounded along Z, so a sphere or box body still collides
1004
+ with an ordinary collision layer, just without a depth result. And mind the
1005
+ normal: a box pair separates along one world axis, a sphere along the line
1006
+ between two centres, so a sphere contact fills the 2D and depth halves of the
1007
+ response at the same time. See the physics skill for the full contract.
594
1008
 
595
1009
  ## glTF / GLB scenes
596
1010
 
@@ -599,6 +1013,14 @@ Loaded through the same level director as everything else:
599
1013
  ```js
600
1014
  await loader.preload([{ name: "diorama", type: "glb", src: "data/diorama.glb" }]);
601
1015
  level.load("diorama", { scale, castGroundShadow, shadowGroundY, onLoaded });
1016
+
1017
+ // every ground-shadow setting travels except shadowGroundPoint, which is
1018
+ // per object and dropped by a scene load: a loaded scene can
1019
+ // be told where its sun is, and `shadowLight` is re-read every draw
1020
+ level.load("diorama", {
1021
+ scale, castGroundShadow: true, shadowGroundY: 0,
1022
+ shadowLight: sun, shadowOffset: 0.6, shadowStretch: 1.8, async: true,
1023
+ });
602
1024
  ```
603
1025
 
604
1026
  `.obj` / `.mtl` are also supported loader types.
@@ -658,6 +1080,7 @@ To branch rather than fail, read `app.renderer.supportsDepthBuffer` after
658
1080
  | a `lit` mesh renders fullbright | it had no normals — supply them, or let the engine generate them |
659
1081
  | a gradient across one mesh is impossible | `tint` is per object — use `vertexColors` / `setVertexColor` |
660
1082
  | a mesh stays solid as you fade it out | meshes render opaque; only `alpha` 0 (hidden) and 1 differ |
1083
+ | 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` |
661
1084
  | vertex colour applies under a 2D camera but not `Camera3d` | wrote the array directly without setting `needsUpdate` |
662
1085
  | `Mesh: vertexColors has N entries, expected M` | one colour per *vertex*, not per triangle or per index |
663
1086
  | nothing renders, or a backdrop covers everything | wrong depth sign — "far" is *larger* z when looking along +Z |
@@ -676,11 +1099,20 @@ To branch rather than fail, read `app.renderer.supportsDepthBuffer` after
676
1099
  | fog does not match the sky after a background fade | an explicit `color` was passed; omit it to track `renderer.backgroundColor` |
677
1100
  | geometry clips before it has finished fading | fog `far` beyond the clip far — omit the distances and they default to the clip planes |
678
1101
  | one marker must stay readable in fog | `fog: false` on that mesh |
679
- | an object casts no visible shadow | wide and flat-bottomed — its own blob is underneath it; raising `shadowGroundY` haloes it instead of revealing it |
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 |
680
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 |
681
1108
  | a mesh sits at the wrong depth after being added | `autoDepth` overwrote `pos.z` with the child index — pass `addChild(mesh, z)` |
682
1109
  | a mesh sits half its size off | `anchorPoint` — only on the 2D-camera path; a `Camera3d` mesh pivots on its model origin |
683
1110
  | a billboard tips over when the camera looks down | `"spherical"`, or a mistyped mode string falling through to it — use `true` / `"cylindrical"` |
1111
+ | `rotate()` does nothing on a billboard | the camera owns its orientation; `billboard: false` and orient the quad yourself |
1112
+ | the camera will not tilt its horizon to an arbitrary up | `lookAt(target)` solves pitch and yaw only; pass the up as the next argument, or hand all three axes to `setBasis` |
1113
+ | the view is upside down after `lookAt(target, up)` | `up` is the direction that should appear UP ON SCREEN, and this is Y-down, so a level horizon is `(0, -1, 0)` |
1114
+ | `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
+ | 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 |
684
1116
  | an object jumps to the camera plane | `Vector3d.set(x, y)` / `camera.pos.set(x, y)` zeroed its z |
685
1117
  | lighting looks wrong under a 3D camera | `Light2d` used instead of `Light3d` |
686
1118