melonjs 20.12.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.
- package/README.md +66 -22
- package/build/application/application.d.ts +6 -1
- package/build/application/application.d.ts.map +1 -1
- package/build/application/header.d.ts.map +1 -1
- package/build/application/resize.d.ts.map +1 -1
- package/build/application/settings.d.ts +13 -8
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +8 -4
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/backend/engine.d.ts.map +1 -1
- package/build/audio/playback.d.ts +2 -2
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/spatial.d.ts.map +1 -1
- package/build/audio/types.d.ts +15 -5
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +12 -9
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +11 -8
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/camera/effects/fade_effect.d.ts.map +1 -1
- package/build/geometries/ellipse.d.ts +3 -0
- package/build/geometries/ellipse.d.ts.map +1 -1
- package/build/geometries/line.d.ts.map +1 -1
- package/build/geometries/path2d.d.ts +9 -8
- package/build/geometries/path2d.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +2 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/index.d.ts +14 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +50215 -45051
- package/build/index.js.map +4 -4
- package/build/input/gamepad.d.ts.map +1 -1
- package/build/input/keyboard.d.ts.map +1 -1
- package/build/input/pointerevent.d.ts.map +1 -1
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/level/level.d.ts.map +1 -1
- package/build/level/tiled/TMXLayer.d.ts +50 -15
- package/build/level/tiled/TMXLayer.d.ts.map +1 -1
- package/build/level/tiled/TMXTileMap.d.ts +14 -4
- package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
- package/build/level/tiled/TMXTileset.d.ts.map +1 -1
- package/build/level/tiled/TMXUtils.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
- package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
- package/build/lighting/light3d.d.ts +55 -0
- package/build/lighting/light3d.d.ts.map +1 -1
- package/build/loader/cache.d.ts +146 -97
- package/build/loader/cache.d.ts.map +1 -1
- package/build/loader/internal-types.d.ts +69 -0
- package/build/loader/internal-types.d.ts.map +1 -0
- package/build/loader/loader.d.ts +589 -253
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/audio.d.ts.map +1 -1
- package/build/loader/parsers/fontface.d.ts.map +1 -1
- package/build/loader/parsers/gltf.d.ts.map +1 -1
- package/build/loader/parsers/tmx.d.ts.map +1 -1
- package/build/math/color.d.ts +4 -0
- package/build/math/color.d.ts.map +1 -1
- package/build/math/math.d.ts +6 -1
- package/build/math/math.d.ts.map +1 -1
- package/build/math/noise.d.ts +16 -16
- package/build/math/noise.d.ts.map +1 -1
- package/build/math/observableVector2d.d.ts +6 -6
- package/build/math/observableVector2d.d.ts.map +1 -1
- package/build/math/observableVector3d.d.ts +8 -8
- package/build/math/observableVector3d.d.ts.map +1 -1
- package/build/particles/emitter.d.ts +15 -3
- package/build/particles/emitter.d.ts.map +1 -1
- package/build/particles/particle.d.ts +6 -0
- package/build/particles/particle.d.ts.map +1 -1
- package/build/particles/settings.d.ts +22 -5
- package/build/particles/settings.d.ts.map +1 -1
- package/build/physics/adapter.d.ts +64 -25
- package/build/physics/adapter.d.ts.map +1 -1
- package/build/physics/builtin/body.d.ts +23 -4
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/builtin-adapter.d.ts +25 -3
- package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
- package/build/physics/builtin/detector.d.ts.map +1 -1
- package/build/physics/builtin/raycast.d.ts.map +1 -1
- package/build/physics/world.d.ts +40 -10
- package/build/physics/world.d.ts.map +1 -1
- package/build/renderable/container.d.ts +41 -6
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/draggable.d.ts.map +1 -1
- package/build/renderable/frameAnimation.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +16 -14
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +62 -5
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +4 -2
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +12 -3
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/text.d.ts +147 -4
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/text/textmetrics.d.ts +10 -2
- package/build/renderable/text/textmetrics.d.ts.map +1 -1
- package/build/renderable/text/textstyle.d.ts.map +1 -1
- package/build/renderable/trigger.d.ts +5 -0
- package/build/renderable/trigger.d.ts.map +1 -1
- package/build/renderable/ui/panelpaint.d.ts +5 -1
- package/build/renderable/ui/panelpaint.d.ts.map +1 -1
- package/build/renderable/ui/progressbar.d.ts +267 -95
- package/build/renderable/ui/progressbar.d.ts.map +1 -1
- package/build/renderable/ui/uibaseelement.d.ts +443 -50
- package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
- package/build/renderable/ui/uicheckbox.d.ts +383 -0
- package/build/renderable/ui/uicheckbox.d.ts.map +1 -0
- package/build/renderable/ui/uifocus.d.ts +6 -0
- package/build/renderable/ui/uifocus.d.ts.map +1 -0
- package/build/renderable/ui/uilabel.d.ts +31 -0
- package/build/renderable/ui/uilabel.d.ts.map +1 -0
- package/build/renderable/ui/uipanel.d.ts +589 -116
- package/build/renderable/ui/uipanel.d.ts.map +1 -1
- package/build/renderable/ui/uiskin.d.ts +25 -0
- package/build/renderable/ui/uiskin.d.ts.map +1 -0
- package/build/renderable/ui/uislider.d.ts +518 -0
- package/build/renderable/ui/uislider.d.ts.map +1 -0
- package/build/renderable/ui/uispriteelement.d.ts +161 -85
- package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
- package/build/renderable/ui/uitextbutton.d.ts +369 -63
- package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/device.d.ts +11 -2
- package/build/system/device.d.ts.map +1 -1
- package/build/system/fullscreen.d.ts.map +1 -1
- package/build/system/pool.d.ts.map +1 -1
- package/build/system/save.d.ts +2 -1
- package/build/system/save.d.ts.map +1 -1
- package/build/tweens/tween.d.ts +5 -2
- package/build/tweens/tween.d.ts.map +1 -1
- package/build/utils/fetchdata.d.ts.map +1 -1
- package/build/utils/utils.d.ts.map +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +16 -19
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/canvas_factory.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts +127 -0
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/renderer.d.ts +20 -4
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/renderstate.d.ts +26 -3
- package/build/video/renderstate.d.ts.map +1 -1
- package/build/video/utils/tessellation.d.ts +11 -0
- package/build/video/utils/tessellation.d.ts.map +1 -1
- package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/webgl_renderer.d.ts +3 -6
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +3 -6
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/melonjs/SKILL.md +41 -17
- package/skills/melonjs-20-migration/SKILL.md +50 -7
- package/skills/melonjs-3d/SKILL.md +138 -38
- package/skills/melonjs-3d-assets/SKILL.md +83 -33
- package/skills/melonjs-audio/SKILL.md +62 -19
- package/skills/melonjs-camera-and-drawing/SKILL.md +135 -8
- package/skills/melonjs-deployment/SKILL.md +92 -14
- package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
- package/skills/melonjs-effects-and-shaders/SKILL.md +47 -15
- package/skills/melonjs-events/SKILL.md +6 -3
- package/skills/melonjs-getting-started/SKILL.md +6 -2
- package/skills/melonjs-input/SKILL.md +102 -16
- package/skills/melonjs-lighting/SKILL.md +13 -1
- package/skills/melonjs-loading-assets/SKILL.md +255 -39
- package/skills/melonjs-math/SKILL.md +26 -8
- package/skills/melonjs-particles-and-trails/SKILL.md +77 -9
- package/skills/melonjs-performance/SKILL.md +18 -8
- package/skills/melonjs-physics/SKILL.md +435 -128
- package/skills/melonjs-plugins/SKILL.md +50 -11
- package/skills/melonjs-renderables/SKILL.md +31 -7
- package/skills/melonjs-renderer-backends/SKILL.md +38 -17
- package/skills/melonjs-scenes-and-state/SKILL.md +45 -5
- package/skills/melonjs-sprites-and-animation/SKILL.md +37 -2
- package/skills/melonjs-tilemaps/SKILL.md +149 -17
- package/skills/melonjs-ui-and-text/SKILL.md +674 -42
|
@@ -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`
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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 (`"
|
|
151
|
-
`"
|
|
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
|
-
|
|
226
|
-
A scene carrying any of them
|
|
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
|
|
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
|
|
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`
|
|
365
|
-
map — needs `lit: true`, and the tangent frame is derived
|
|
366
|
-
model needs no tangent attribute).
|
|
367
|
-
normal
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 `
|
|
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` —
|
|
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
|
|
@@ -19,10 +19,13 @@ await loader.preload([
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
1. **`audio.init(format)` must run before any audio asset is preloaded**, and
|
|
22
|
-
lists the formats you actually ship. Preloading first
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
lists the formats you actually ship. Preloading first fails the preload:
|
|
23
|
+
`await loader.preload(...)` rejects with the loader's `Failed loading
|
|
24
|
+
resource <src>`, which hides the real reason, `target audio extension(s)
|
|
25
|
+
should be set through me.audio.init() before calling the preloader.`
|
|
26
|
+
(thrown as it is by `audio.load()` called directly). It returns `false`
|
|
27
|
+
when no audio backend is available, so it is worth checking. The format
|
|
28
|
+
list defaults to `"mp3"`.
|
|
26
29
|
2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus
|
|
27
30
|
each configured extension and uses the first that loads — so
|
|
28
31
|
`data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename
|
|
@@ -50,13 +53,19 @@ audio.playTrack("theme"); // background music
|
|
|
50
53
|
audio.stopTrack();
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)`
|
|
56
|
+
Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` (or
|
|
57
|
+
`playTrack(name, { volume, sprite })`, to loop one region of the clip) always
|
|
54
58
|
loops, and it records the name as the current track — which is what
|
|
55
59
|
`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via
|
|
56
60
|
`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,
|
|
57
61
|
`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there
|
|
58
62
|
is only ever one current *track name*.
|
|
59
63
|
|
|
64
|
+
`playTrack` on a track that is paused restarts it. `pauseTrack` and
|
|
65
|
+
`resumeTrack` act on the track alone, so effects played from the same file (an
|
|
66
|
+
audio sprite holding both) keep going, and `resumeTrack` on a track that is
|
|
67
|
+
playing does nothing.
|
|
68
|
+
|
|
60
69
|
**`playTrack` stops the track it replaces** (20.9.0 and later; before that it
|
|
61
70
|
only assigned the slot, and the previous bed kept looping underneath where
|
|
62
71
|
`stopTrack()` could no longer reach it). So a scene just states its own music
|
|
@@ -155,6 +164,11 @@ audio.unmuteAll(); // (audio.enable() is an alias)
|
|
|
155
164
|
audio.muted(); // global mute state
|
|
156
165
|
```
|
|
157
166
|
|
|
167
|
+
`setVolume` is a master level over everything; the `volume` given to a `play`
|
|
168
|
+
or `playTrack` is that sound's own level, 1 when left out, and what is heard is
|
|
169
|
+
the product of the two. So a music slider fades the track's own level
|
|
170
|
+
(`audio.fade(name, v, v, 1)`) and a master slider calls `setVolume`.
|
|
171
|
+
|
|
158
172
|
`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`
|
|
159
173
|
also clears the current-track pointer if it named that clip.
|
|
160
174
|
|
|
@@ -163,8 +177,10 @@ also clears the current-track pointer if it named that clip.
|
|
|
163
177
|
When the window loses focus the engine mutes all audio, and unmutes it on
|
|
164
178
|
focus. That covers sound effects, procedural `tone` / `noise` and the mixer
|
|
165
179
|
effects, not just the music track that `state.pause()` pauses. Set
|
|
166
|
-
`pauseOnBlur
|
|
167
|
-
in the background.
|
|
180
|
+
`app.pauseOnBlur = false` (with `stopOnBlur` left at its `false` default) if a
|
|
181
|
+
clip has to keep playing in the background. It is a property of the
|
|
182
|
+
application, not a constructor setting: `new Application(w, h, { pauseOnBlur:
|
|
183
|
+
false })` does nothing.
|
|
168
184
|
|
|
169
185
|
## Spatial audio
|
|
170
186
|
|
|
@@ -197,10 +213,15 @@ audio.play("portal", { at: { x: 1200, y: 480 } });
|
|
|
197
213
|
|
|
198
214
|
`follow` and `at` are mutually exclusive: passing both throws, rather than
|
|
199
215
|
quietly picking one. `follow` re-reads the target's **absolute** position each
|
|
200
|
-
frame, so a renderable nested in a moving container is placed correctly.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
frame, so a renderable nested in a moving container is placed correctly. That
|
|
217
|
+
position is where its `anchorPoint` sits: the center of a `Renderable` or a
|
|
218
|
+
`Sprite`, but the **top-left corner** of an `Entity` or an object loaded from
|
|
219
|
+
Tiled, whose anchor is `(0, 0)`, so a large one (a boss) sounds from up and to
|
|
220
|
+
the left of where it looks to be. `stopWithTarget` is what you want for a sound
|
|
221
|
+
that belongs to an entity: without it a looping clip on a destroyed renderable
|
|
222
|
+
keeps playing from wherever it last was. It also counts a pooled renderable
|
|
223
|
+
going back to its pool, which can lend it out again before the next frame.
|
|
224
|
+
`audio.unfollow(id)` detaches a sound and leaves it playing where it is.
|
|
204
225
|
|
|
205
226
|
### Move the listener, don't do the subtraction yourself
|
|
206
227
|
|
|
@@ -221,7 +242,12 @@ player's position.
|
|
|
221
242
|
`setListener` places the ear at the target's absolute position after the world
|
|
222
243
|
updates. A plain renderable contributes position only, so a side-scroller keeps
|
|
223
244
|
the default "facing into the screen" and gets left-right panning plus distance
|
|
224
|
-
falloff from the one call.
|
|
245
|
+
falloff from the one call. A `Camera2d` (`audio.setListener(app.viewport)` in a
|
|
246
|
+
2D game) is heard from the **center of its view**, its `worldView`, zoom and
|
|
247
|
+
offset included, not from its `pos`, the view's top-left corner (before 20.12
|
|
248
|
+
it was the corner, and everything panned right). Following stops on its own
|
|
249
|
+
when the target is destroyed, which a stage that owns its camera does on every
|
|
250
|
+
state change.
|
|
225
251
|
|
|
226
252
|
A `Camera3d` target also contributes its **orientation**, taken from its own
|
|
227
253
|
basis, so turning the camera swings the stereo image with it. That is the whole
|
|
@@ -380,6 +406,14 @@ a gate screen — a splash, a "press any key", or a health/photosensitivity noti
|
|
|
380
406
|
unconditionally. Note the unlock listens for `keydown` too, so a keyboard-only
|
|
381
407
|
player is covered.
|
|
382
408
|
|
|
409
|
+
Where there is no DOM (nx.js on the Nintendo Switch, see `melonjs-deployment`)
|
|
410
|
+
there is no page to gesture at and no autoplay policy: audio unlocks as soon as
|
|
411
|
+
the engine has its context, and music can start on load. nx.js's Web Audio has
|
|
412
|
+
buffer sources, gain and stereo panning, but not the `PannerNode`,
|
|
413
|
+
`OscillatorNode` and `BiquadFilterNode` behind 3D `position()` / `panner()` and
|
|
414
|
+
`tone()` / `noise()`, nor the reverb, echo and compressor nodes: those throw
|
|
415
|
+
"Method not implemented" there.
|
|
416
|
+
|
|
383
417
|
### The context can stop again, long after it was unlocked
|
|
384
418
|
|
|
385
419
|
Unlocking is only the first half. A context that has been running can be
|
|
@@ -463,10 +497,12 @@ the level starts. The loader calls the same function, so the same rules apply.
|
|
|
463
497
|
Re-preloading a manifest that already contains a loaded clip is a no-op —
|
|
464
498
|
`audio.unload(name)` first if you genuinely want to reload it.
|
|
465
499
|
|
|
466
|
-
**Credentials
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
500
|
+
**Credentials work as for every other asset type.** Buffered audio on the
|
|
501
|
+
game's own origin carries its cookies and HTTP auth with no option; a clip on
|
|
502
|
+
another origin behind a login needs `loader.setOptions({ withCredentials: true })`,
|
|
503
|
+
not an audio-specific option, and that server must allow credentials. Buffered
|
|
504
|
+
audio fetches through the shared loader path, so it behaves exactly as an image
|
|
505
|
+
or a JSON file does. A streamed clip (`stream: true`, or
|
|
470
506
|
`preload: false`) does not: it plays
|
|
471
507
|
through an `<audio>` element, which needs a `crossorigin` attribute rather than
|
|
472
508
|
fetch credentials. Preload a clip buffered if it sits behind a login.
|
|
@@ -488,8 +524,9 @@ either way — blank screen, empty console.
|
|
|
488
524
|
| symptom | cause |
|
|
489
525
|
|---|---|
|
|
490
526
|
| audio assets 404 | `src` given as a full filename instead of a directory prefix |
|
|
491
|
-
| `target audio extension(s) should be set...`
|
|
527
|
+
| `preload()` rejects with `Failed loading resource data/sfx/` (or `audio.load()` throws `target audio extension(s) should be set...`) | `audio.init()` not called before preloading |
|
|
492
528
|
| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
|
|
529
|
+
| "Method not implemented" from `position()`, `panner()`, `tone()`, `noise()` or an effect, on nx.js | nx.js's Web Audio lacks the node it needs (panner, oscillator, filter, convolver...): use `stereo()` and recorded sounds there |
|
|
493
530
|
| nothing plays until the user clicks | browser autoplay policy — expected, design around it |
|
|
494
531
|
| `playing()` is false right after `play()` | playback starts asynchronously |
|
|
495
532
|
| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |
|
|
@@ -498,18 +535,24 @@ either way — blank screen, empty console.
|
|
|
498
535
|
| `stereo` / `position` do nothing | the clip was loaded with `stream: true` or `preload: false` — spatial is WebAudio-only |
|
|
499
536
|
| a `preload: false` clip never plays, and `audio.state()` stays `"unloaded"` | melonJS before 20.12.0 never fetched such a clip; preload it, or update |
|
|
500
537
|
| one format works, another does not | that codec is not in the `audio.init()` list |
|
|
501
|
-
| audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |
|
|
538
|
+
| audio on another origin behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses. Audio on the game's own origin needs nothing (before 20.13 it needed it too) |
|
|
502
539
|
| a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |
|
|
503
540
|
| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |
|
|
504
541
|
| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |
|
|
505
542
|
| a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |
|
|
506
543
|
| a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |
|
|
507
544
|
| a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |
|
|
508
|
-
| a
|
|
545
|
+
| with `audio.setListener(app.viewport)`, a sound in the middle of the screen is heard off to the right (before 20.12) | the ear was at the camera's top-left corner: update, it is at the center of the view |
|
|
546
|
+
| a followed `Entity` (or Tiled object) sounds offset up and to the left | `follow` uses the anchor point, which is the top-left corner under their `(0, 0)` anchor |
|
|
547
|
+
| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true`; with it, before 20.14 a pooled entity going back to its pool did not count either |
|
|
548
|
+
| new sounds are quieter than the volume slider says, or the music stays silent after the volume went to 0 and back (before 20.14) | the global volume was applied twice: update |
|
|
549
|
+
| "Retry" from a pause menu restarts the level with no music (before 20.14) | `playTrack` on a paused track stopped it: update |
|
|
550
|
+
| a clip's load-time `on: { end }` stops firing after `audio.stop(name)` (before 20.14) | `stop(name)` removed it: update |
|
|
509
551
|
| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |
|
|
510
552
|
| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |
|
|
511
553
|
| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see "A loop with no gap") |
|
|
512
554
|
| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |
|
|
555
|
+
| the game still pauses and goes quiet on blur although `pauseOnBlur: false` was passed to `new Application()` | it is not a setting: assign `app.pauseOnBlur = false` |
|
|
513
556
|
|
|
514
557
|
## Related skills
|
|
515
558
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: melonjs-camera-and-drawing
|
|
3
|
-
description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, shake, fadeIn, fadeOut, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, Rect, Ellipse, Polygon, Line, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
|
|
3
|
+
description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, zoom, roll, shake, fadeIn, fadeOut, CameraEffect, addCameraEffect, MaskEffect, PixelateTransitionEffect, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, setGlobalAlpha, fillArc, Rect, Ellipse, Polygon, Line, Path2D, SVG path, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
|
|
4
4
|
license: MIT
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -109,6 +109,13 @@ The fade pair is named from the *colour's* point of view, not the scene's:
|
|
|
109
109
|
(screen starts at `color` and clears). Both default to a 1000 ms duration and
|
|
110
110
|
take an optional completion callback.
|
|
111
111
|
|
|
112
|
+
`shake()` while a shake is still running is ignored, unless its fifth
|
|
113
|
+
argument, `force`, is `true`, which replaces the running one:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
app.viewport.shake(8, 300, app.viewport.AXIS.BOTH, undefined, true);
|
|
117
|
+
```
|
|
118
|
+
|
|
112
119
|
**`fadeIn` does not HOLD the colour.** The effect completes, the camera's own
|
|
113
120
|
update destroys it, and the screen snaps back — which is why the engine's
|
|
114
121
|
idiom does the work in the completion callback and then calls `fadeOut`. To
|
|
@@ -136,6 +143,48 @@ non-identity, and drops it again after the frame. It is **not** shader-free —
|
|
|
136
143
|
it is a `ColorMatrixEffect`, i.e. a `ShaderEffect`, so like every post effect it
|
|
137
144
|
is inert on the Canvas renderer.
|
|
138
145
|
|
|
146
|
+
### The camera effects, and your own
|
|
147
|
+
|
|
148
|
+
Each is a `CameraEffect`, added with `camera.addCameraEffect(effect)` (which
|
|
149
|
+
returns it), found with `getCameraEffect(Class)` and taken off with
|
|
150
|
+
`removeCameraEffect(effect)`, which destroys it:
|
|
151
|
+
|
|
152
|
+
| effect | options | what it does |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `ShakeEffect` | `intensity`, `duration`, `axis`, `onComplete` | jitters `offset`; what `shake()` adds. Several can run at once this way |
|
|
155
|
+
| `FadeEffect` | `color`, `duration` (1000), `direction: "in" \| "out"`, `onComplete` | what `fadeIn()` / `fadeOut()` add |
|
|
156
|
+
| `MaskEffect` | `shape`, `color`, `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | an iris or a wipe through a unit-sized `Ellipse` or `Polygon` centred on the origin |
|
|
157
|
+
| `PixelateTransitionEffect` | `renderer`, `size` (32), `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | the view breaks into blocks up to `size` screen pixels, or comes back from them. A shader: on Canvas it keeps its timing and draws nothing |
|
|
158
|
+
|
|
159
|
+
`state.transition("fade" | "mask" | "pixelate", …)` drives the last three
|
|
160
|
+
for a state change (see `melonjs-scenes-and-state`). A `Trigger` that loads a
|
|
161
|
+
level transitions only with both a `color` and a `duration`, and takes only
|
|
162
|
+
`transition: "fade"` (the default) or `"mask"` with a `shape`: anything else,
|
|
163
|
+
`"pixelate"` included, is a fade.
|
|
164
|
+
|
|
165
|
+
For an effect of your own, extend `CameraEffect`: `update(dt)` runs every
|
|
166
|
+
frame (`dt` in ms), `draw(renderer, width, height)` draws over the camera's
|
|
167
|
+
view, and `destroy()` frees what it holds. Set `isComplete = true` and the
|
|
168
|
+
camera's update destroys it that frame, unless it is `isPersistent`.
|
|
169
|
+
|
|
170
|
+
## Zoom and roll
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
app.viewport.zoom = 2; // above 1 zooms in, below 1 out
|
|
174
|
+
app.viewport.roll = Math.PI / 32; // a slight dutch angle, in radians
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A zoomed camera follows, keeps to its bounds and centres on `focusOn` in world
|
|
178
|
+
units, and `localToWorld` / `worldToLocal` count the zoom. A zoom of 0 or less
|
|
179
|
+
is taken as 1.
|
|
180
|
+
|
|
181
|
+
`roll` is absolute (assigning the same angle twice changes nothing), and the
|
|
182
|
+
pointer conversions undo it, so picking under a rolled camera needs nothing.
|
|
183
|
+
It OWNS the camera's `currentTransform`: each assignment rebuilds it, so a
|
|
184
|
+
`rotate()` or `scale()` called on the camera itself is lost at the next one.
|
|
185
|
+
`reset()` puts it back to 0. On a `Camera3d` the same `roll` banks the view
|
|
186
|
+
(see below).
|
|
187
|
+
|
|
139
188
|
## Coordinate conversion
|
|
140
189
|
|
|
141
190
|
```js
|
|
@@ -143,6 +192,12 @@ app.viewport.worldToLocal(x, y); // world → screen
|
|
|
143
192
|
app.viewport.localToWorld(x, y); // screen → world
|
|
144
193
|
```
|
|
145
194
|
|
|
195
|
+
They agree with where the camera draws: a level smaller than the view is
|
|
196
|
+
centred by offsetting the world container (`world.pos`), which the default
|
|
197
|
+
camera draws and a minimap, corner or zoomed camera leaves out, and each
|
|
198
|
+
camera's conversions follow suit (20.13 and later; before, a minimap's were off
|
|
199
|
+
by `world.pos`).
|
|
200
|
+
|
|
146
201
|
Both return a `Vector2d`; with no third argument that vector comes from the
|
|
147
202
|
shared pool, so in a per-frame path pass your own `v` as the third argument, or
|
|
148
203
|
release it with `getPool("vector2d").release(v)`.
|
|
@@ -205,6 +260,14 @@ and an overridden `postDraw`:
|
|
|
205
260
|
this.cameras.set("minimap", new MinimapCamera());
|
|
206
261
|
```
|
|
207
262
|
|
|
263
|
+
A stage's cameras draw in the order they were added, each over the ones before
|
|
264
|
+
it, so a full-screen second camera is a layer: a `Camera3d` drawing a 3D
|
|
265
|
+
background first, the game's own camera over it, each kept to its part of the
|
|
266
|
+
world with `visibilityLayer` / `cullMask` (below). A camera's post effects
|
|
267
|
+
apply to what that camera draws: the first camera's pass starts the frame, and
|
|
268
|
+
a later camera's is blended over what is already there (20.13 and later;
|
|
269
|
+
before, it wiped out the cameras before it).
|
|
270
|
+
|
|
208
271
|
View state is per camera, not per scene: a `Camera3d` carries its own distance
|
|
209
272
|
fog (`setFog`), so a minimap or split-screen view fogs independently of the main
|
|
210
273
|
one — and a `Camera2d` never fogs. See `melonjs-3d`.
|
|
@@ -228,6 +291,9 @@ minimap.cullMask = ~PARTICLES; // the minimap draws every layer but 2
|
|
|
228
291
|
set it on an emitter and none of its particles are drawn. The world itself
|
|
229
292
|
obeys it too, and so does what it contains: a child is drawn only if it and
|
|
230
293
|
every container above it share a bit with the camera.
|
|
294
|
+
- Particles are on their emitter's layers, and follow a change to them on the
|
|
295
|
+
emitter's next update, so a camera that draws only the emitter's layer draws
|
|
296
|
+
its particles too.
|
|
231
297
|
- A renderable on layer `0` is drawn by no camera; a `cullMask` of `0` draws
|
|
232
298
|
nothing.
|
|
233
299
|
- It decides drawing only: what a camera leaves out still updates, collides and
|
|
@@ -256,9 +322,56 @@ draw(renderer) {
|
|
|
256
322
|
}
|
|
257
323
|
```
|
|
258
324
|
|
|
325
|
+
**The colour's alpha and the global alpha are two things**, as on a 2D
|
|
326
|
+
context. A fill or a stroke is drawn at the alpha of `setColor` times
|
|
327
|
+
`setGlobalAlpha` (which is where the renderable's opacity comes in), an image at
|
|
328
|
+
the global alpha (times the tint) alone, and `getGlobalAlpha()` never moves with
|
|
329
|
+
`setColor`. So to fade a translucent shape, lower the global alpha and keep the
|
|
330
|
+
colour; folding the colour's alpha into `setGlobalAlpha` as well counts it twice.
|
|
331
|
+
|
|
332
|
+
`lineWidth`, `lineJoin`, the colour and the global alpha are all part of what
|
|
333
|
+
`save()` / `restore()` keep, and every renderable's `draw()` runs between the
|
|
334
|
+
two, so set them freely there: the next renderable gets them back.
|
|
335
|
+
|
|
336
|
+
`fillArc` / `strokeArc` read their angles like `CanvasRenderingContext2D.arc`:
|
|
337
|
+
an end before the start wraps round (`end + 2π`), and a span of 2π or more in
|
|
338
|
+
the arc's own direction is the whole circle. A filled part-circle differs by
|
|
339
|
+
backend, though: Canvas closes it with a straight chord, the GPU renderers fill
|
|
340
|
+
a pie slice from the centre.
|
|
341
|
+
|
|
259
342
|
Geometry available: `Rect`, `RoundRect`, `Ellipse`, `Polygon`, `Line`, `Point`
|
|
260
|
-
(and a `Bounds`)
|
|
261
|
-
|
|
343
|
+
(and a `Bounds`), plus the 3D `Box3d` and `Sphere`, drawn as their footprint
|
|
344
|
+
and their circle on the screen plane. `renderer.fill(shape)` and
|
|
345
|
+
`renderer.stroke(shape)` take any of them and throw `Invalid geometry for
|
|
346
|
+
fill/stroke` for anything else.
|
|
347
|
+
|
|
348
|
+
### Paths and SVG strings
|
|
349
|
+
|
|
350
|
+
For any other outline, build a path on the renderer's own `renderer.path2D`,
|
|
351
|
+
then call `fill()` / `stroke()` with no argument:
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
renderer.path2D.parseSVGPath("M 50 0 L 100 100 L 0 100 Z");
|
|
355
|
+
renderer.setColor("#4CAF50");
|
|
356
|
+
renderer.fill();
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`renderer.path2D` also takes the methods of the browser's `Path2D`
|
|
360
|
+
(`beginPath`, `moveTo`, `lineTo`, `arc`, `arcTo`, `ellipse`, `rect`,
|
|
361
|
+
`roundRect`, `quadraticCurveTo`, `bezierCurveTo`, `closePath`).
|
|
362
|
+
|
|
363
|
+
- **Every `moveTo`, every `M`, starts a sub-path, and filled, each one after
|
|
364
|
+
the first is a HOLE**: a donut, the counter of an "O". Wind a hole the
|
|
365
|
+
opposite way to the outline, as Canvas fills by the non-zero rule, where a
|
|
366
|
+
hole wound the same way fills in. Draw separate shapes with separate fills.
|
|
367
|
+
- **`parseSVGPath` reads absolute commands only** (`M L H V Q C A Z`). A
|
|
368
|
+
lowercase, relative command is read as its absolute twin, so a path
|
|
369
|
+
exported with relative commands comes out scrambled, and `H` / `V` take an
|
|
370
|
+
OFFSET from the pen, not the coordinate SVG means. Export absolute
|
|
371
|
+
coordinates.
|
|
372
|
+
- **The engine's `Path2D` class is not exported.** `new Path2D()` in a game is
|
|
373
|
+
the browser's own, which `fill()` / `stroke()` refuse as invalid geometry:
|
|
374
|
+
use `renderer.path2D`.
|
|
262
375
|
|
|
263
376
|
`Gradient` gives linear and radial fills, built with
|
|
264
377
|
`renderer.createLinearGradient(x0, y0, x1, y1)` /
|
|
@@ -319,10 +432,11 @@ drawable, and the engine's own bakes (the particle emitter's default texture)
|
|
|
319
432
|
hand `.canvas` on for exactly that reason.
|
|
320
433
|
|
|
321
434
|
**Create it once and keep it.** On a GPU backend, once drawn, its texture stays
|
|
322
|
-
on the GPU
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
435
|
+
on the GPU until `rt.destroy(renderer)` frees it: a stage change does not, and
|
|
436
|
+
`destroy()` without the renderer frees the canvas but not that copy. A target
|
|
437
|
+
made for every level, every restart or every spawned object and never
|
|
438
|
+
destroyed leaks one texture each time, so make it once (at module level, say)
|
|
439
|
+
and clear it for reuse. If you paint into it again after it has
|
|
326
440
|
been drawn, call `rt.invalidate(renderer)` before the next draw so the GPU copy
|
|
327
441
|
is refreshed.
|
|
328
442
|
|
|
@@ -383,6 +497,7 @@ const data = await rt.toImageData(0, 0, w, h);
|
|
|
383
497
|
|
|
384
498
|
| symptom | cause |
|
|
385
499
|
|---|---|
|
|
500
|
+
| a second camera's post effect wipes out what the first camera drew | before 20.13 a later camera's post-effect pass replaced the whole frame; update, or put the effects on a container instead of the camera |
|
|
386
501
|
| `follow()` set but the view never moves | viewport bounds still at renderer size |
|
|
387
502
|
| `invalid target for me.Camera2d.follow` | passed `player.pos` — pass the renderable, a `Vector2d` or a `Vector3d` |
|
|
388
503
|
| camera drag jumps around | using world (`gameWorldX`) instead of screen (`gameScreenX`) coordinates |
|
|
@@ -390,12 +505,24 @@ const data = await rt.toImageData(0, 0, w, h);
|
|
|
390
505
|
| custom drawing appears at the origin | `draw()` not reading `this.pos` |
|
|
391
506
|
| custom drawing offset by half its size | centred `anchorPoint` not zeroed |
|
|
392
507
|
| `setLineWidth` is not a function | it is the `lineWidth` property |
|
|
508
|
+
| after a translucent `setColor`, images, later colours and the next renderables come out faded (WebGL / WebGPU) | before 20.13 the colour's alpha leaked into the global alpha; update |
|
|
509
|
+
| a translucent colour comes out fainter than asked | its alpha folded into `setGlobalAlpha` as well, so it counts twice |
|
|
510
|
+
| a `lineWidth` set in one `draw()` thickens everything drawn after it (WebGL / WebGPU) | before 20.13 `restore()` did not put `lineWidth` back; update, or put it back yourself |
|
|
511
|
+
| `fillArc` with the end angle before the start fills the wrong part of the circle (WebGL / WebGPU) | before 20.13 the GPU renderers did not wrap the angles like Canvas; update |
|
|
512
|
+
| a minimap's `worldToLocal` / `localToWorld` are off by the level's centring offset | before 20.13 they applied `world.pos`, which a non-default camera does not draw; update |
|
|
393
513
|
| clipping in the wrong place inside a container | container clips in local coordinates, after its own translate |
|
|
394
514
|
| `container.clipping = true` does nothing | container has no explicit size, so `width`/`height` are `Infinity` |
|
|
395
515
|
| baked target draws nothing / throws | passed the `CanvasRenderTarget`, not its `.canvas` |
|
|
396
516
|
| `getImageData()` throws on one machine but not another | that one is on the WebGPU renderer, which cannot read back synchronously — `await toImageData()` instead |
|
|
397
517
|
| frame rate drops with many static draws | bake into a `CanvasRenderTarget` instead |
|
|
398
|
-
| GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it |
|
|
518
|
+
| GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it, or `destroy(renderer)` the old one |
|
|
519
|
+
| a second `shake()` does nothing | one is still running: pass `force` (the fifth argument) to replace it |
|
|
520
|
+
| a `Trigger` with `transition: "pixelate"` fades instead | a trigger takes only `"fade"` or `"mask"`; add a `PixelateTransitionEffect` yourself |
|
|
521
|
+
| a `Trigger` loads its level with no transition | it needs both `color` and `duration` |
|
|
522
|
+
| a `rotate()` or `scale()` on the camera is lost | `roll` owns the camera's `currentTransform` and rebuilds it; use `roll` and `zoom` |
|
|
523
|
+
| an SVG path from an editor comes out scrambled | `parseSVGPath` reads absolute commands only, and `H` / `V` as offsets |
|
|
524
|
+
| a hole in a path fills in, on Canvas only | it is wound the same way as the outline; wind it the opposite way |
|
|
525
|
+
| `fill(new Path2D(...))` throws `Invalid geometry for fill/stroke` | that is the browser's `Path2D`; build on `renderer.path2D` and call `fill()` |
|
|
399
526
|
| a repainted `CanvasRenderTarget` still shows the old pixels | call `rt.invalidate(renderer)` after painting |
|
|
400
527
|
| a 3D camera cannot be given an arbitrary up | `Camera3d#lookAt(target, up)` or `setBasis(right, up, forward)`; see `melonjs-3d` |
|
|
401
528
|
| particles or effects clutter a minimap | every camera draws everything: put them on their own `visibilityLayer` and leave it out of the minimap's `cullMask` |
|