melonjs 20.4.0 → 20.6.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/build/application/defaultApplicationSettings.d.ts +1 -0
- package/build/application/defaultApplicationSettings.d.ts.map +1 -1
- package/build/application/settings.d.ts +37 -0
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/backend/sound.d.ts.map +1 -1
- package/build/audio/procedural.d.ts +7 -2
- package/build/audio/procedural.d.ts.map +1 -1
- package/build/audio/types.d.ts +22 -0
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +28 -0
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +24 -3
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +7 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1379 -226
- package/build/index.js.map +4 -4
- package/build/level/gltf/GLTFModel.d.ts +50 -7
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/lighting/light3d.d.ts +20 -47
- package/build/lighting/light3d.d.ts.map +1 -1
- package/build/loader/loader.d.ts +106 -6
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/loadingscreen.d.ts.map +1 -1
- package/build/renderable/animation.d.ts +21 -6
- package/build/renderable/animation.d.ts.map +1 -1
- package/build/renderable/container.d.ts +27 -5
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +34 -9
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +131 -94
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts +17 -0
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite3d.d.ts +12 -2
- package/build/renderable/sprite3d.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +32 -7
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/text.d.ts +78 -3
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/text/textmetrics.d.ts +2 -0
- package/build/renderable/text/textmetrics.d.ts.map +1 -1
- package/build/renderable/text/textstyle.d.ts.map +1 -1
- package/build/state/stage.d.ts +21 -0
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/device.d.ts +1 -1
- package/build/system/device.d.ts.map +1 -1
- package/build/system/save.d.ts +44 -1
- package/build/system/save.d.ts.map +1 -1
- package/build/video/effects/glsl_realization.d.ts.map +1 -1
- package/build/video/effects/mesh_splice.d.ts +2 -0
- package/build/video/effects/mesh_splice.d.ts.map +1 -0
- package/build/video/effects/shadereffect.d.ts +12 -128
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/wgsl_realization.d.ts.map +1 -1
- package/build/video/gradient.d.ts +19 -0
- package/build/video/gradient.d.ts.map +1 -1
- package/build/video/renderer.d.ts +44 -0
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/texture/noise_texture2d.d.ts +101 -34
- package/build/video/texture/noise_texture2d.d.ts.map +1 -1
- package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/glshader.d.ts +7 -2
- package/build/video/webgl/glshader.d.ts.map +1 -1
- 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/mesh_batcher.d.ts +6 -1
- package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/effect_binding.d.ts.map +1 -1
- package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +1 -0
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/melonjs-3d/SKILL.md +152 -3
- package/skills/melonjs-3d-assets/SKILL.md +139 -0
- package/skills/melonjs-audio/SKILL.md +17 -0
- package/skills/melonjs-camera-and-drawing/SKILL.md +45 -0
- package/skills/melonjs-effects-and-shaders/SKILL.md +130 -1
- package/skills/melonjs-loading-assets/SKILL.md +131 -1
- package/skills/melonjs-performance/SKILL.md +83 -1
- package/skills/melonjs-physics/SKILL.md +78 -0
- package/skills/melonjs-renderables/SKILL.md +63 -1
- package/skills/melonjs-renderer-backends/SKILL.md +19 -0
- package/skills/melonjs-scenes-and-state/SKILL.md +93 -0
- package/skills/melonjs-ui-and-text/SKILL.md +124 -0
|
@@ -25,6 +25,34 @@ app.viewport.addPostEffect(new VignetteEffect(app.renderer));
|
|
|
25
25
|
`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys
|
|
26
26
|
whatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.
|
|
27
27
|
|
|
28
|
+
### A camera effect covers the HUD too
|
|
29
|
+
|
|
30
|
+
A camera's post-effect brackets the **entire** world draw — floating children
|
|
31
|
+
included. So the obvious way to write a full-screen pass also washes over every
|
|
32
|
+
HUD label in that world, which is rarely what you want.
|
|
33
|
+
|
|
34
|
+
To land a pass *after* the world but *before* the HUD, host the effect on a
|
|
35
|
+
screen-filling floating renderable ordered below the HUD's z instead:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const quad = new Sprite(0, 0, { image: anyImage });
|
|
39
|
+
quad.anchorPoint.set(0, 0);
|
|
40
|
+
quad.scale(viewW / anyImage.width, viewH / anyImage.height);
|
|
41
|
+
quad.floating = true; // its own uv is now screen space
|
|
42
|
+
quad.blendMode = "additive";
|
|
43
|
+
quad.addPostEffect(myEffect);
|
|
44
|
+
world.addChild(quad, HUD_Z - 10); // BELOW the labels, not above
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Two things follow from the quad filling the frame: its own `uv` is screen space
|
|
48
|
+
(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins),
|
|
49
|
+
and the incoming `color` is the quad's own texture, which a body that paints
|
|
50
|
+
from scratch can ignore entirely.
|
|
51
|
+
|
|
52
|
+
Depth here is easy to get backwards — a higher z draws **later**, i.e. on top.
|
|
53
|
+
Verify it by returning a flat colour from the body for one frame: whatever it
|
|
54
|
+
tints is what the pass covers.
|
|
55
|
+
|
|
28
56
|
## Toggle with `enabled`, do not remove
|
|
29
57
|
|
|
30
58
|
**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and
|
|
@@ -80,7 +108,71 @@ one `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` cal
|
|
|
80
108
|
feeds whichever backend is live. `setTime` is a convenience for a `uTime`
|
|
81
109
|
uniform and silently does nothing if the shader does not declare one.
|
|
82
110
|
|
|
83
|
-
|
|
111
|
+
### One effect is one program — prefer preloading, and share
|
|
112
|
+
|
|
113
|
+
A `ShaderEffect` compiles and links in its **constructor**, and the link is
|
|
114
|
+
checked with `getProgramParameter(LINK_STATUS)` straight after `linkProgram` —
|
|
115
|
+
a blocking call. So the cost is paid on whatever frame the effect is
|
|
116
|
+
constructed, and *n* identical effects cost it *n* times. `clone()` does not
|
|
117
|
+
avoid it: it constructs a new effect and links a new program.
|
|
118
|
+
|
|
119
|
+
Preload it instead. Loader type `"shader"` takes a `src` **or inline source via
|
|
120
|
+
`data`**, compiles at load time (inside the loading screen), and hands back a
|
|
121
|
+
**shared** instance:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
loader.preload([{ name: "ramp", type: "shader", data: myFragmentBody }]);
|
|
125
|
+
// …then, for every label that wants it:
|
|
126
|
+
label.addPostEffect(loader.getShader("ramp"));
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Declare a level's effects with that level's assets
|
|
130
|
+
|
|
131
|
+
An effect the loader knows about can be prepared before anything draws it; an
|
|
132
|
+
effect built inline cannot, because it does not exist until the code that
|
|
133
|
+
constructs it runs — which is the stage or level you were hoping to speed up.
|
|
134
|
+
That is the practical reason to prefer the asset form even when the source is a
|
|
135
|
+
string in your own module:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
// with the level's other assets, not in the stage that uses them.
|
|
139
|
+
// `preload()` rather than `load()` — preload is what warms the shaders it
|
|
140
|
+
// brought in, and it can be called again per level, not just at boot
|
|
141
|
+
await loader.preload([
|
|
142
|
+
{ name: "ripples", type: "shader", src: {
|
|
143
|
+
glsl: "assets/level2/ripples.glsl",
|
|
144
|
+
wgsl: "assets/level2/ripples.wgsl",
|
|
145
|
+
}},
|
|
146
|
+
// …that level's textures, maps and audio alongside it
|
|
147
|
+
], undefined, false); // `false`: stay on this stage, no loading screen
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`src` takes the dual-language pair, so one asset carries both realizations and
|
|
151
|
+
the two backends cannot drift apart. `data` takes inline source the same way.
|
|
152
|
+
|
|
153
|
+
With `prewarm` on (the default since 20.6), `loader.preload()` warms on its way
|
|
154
|
+
through — automatically, on every call — and compiles the GPU-side objects for
|
|
155
|
+
every shader it holds — on WebGPU that is the
|
|
156
|
+
WGSL module, which is otherwise built inside the draw path the first time the
|
|
157
|
+
effect is used. Effects constructed inline are not in the loader, so nothing can
|
|
158
|
+
bring them forward.
|
|
159
|
+
|
|
160
|
+
One caveat if you are moving an inline effect to a file: a template literal can
|
|
161
|
+
interpolate constants into the source (`vec2(${ASPECT}, 1.0)`), and a static
|
|
162
|
+
file cannot. Promote those to uniforms rather than duplicating the numbers
|
|
163
|
+
across the GLSL and WGSL copies — duplicated constants are exactly how the two
|
|
164
|
+
backends drift.
|
|
165
|
+
|
|
166
|
+
Sharing one instance across renderables is safe *because* the loader sets
|
|
167
|
+
`effect.shared`. Without that flag a renderable's teardown destroys its post
|
|
168
|
+
effects, which would free the program out from under everything else still
|
|
169
|
+
using it. Uniforms live on the effect, so a shared instance means one set of
|
|
170
|
+
uniform values — fine when the look is uniform, and the reason to clone (and
|
|
171
|
+
pay a link) when it is not.
|
|
172
|
+
|
|
173
|
+
Note this covers **your** effects only. The engine's own mesh/quad shader
|
|
174
|
+
variants are compiled lazily per feature combination on first draw and cannot be
|
|
175
|
+
preloaded — see `melonjs-loading-assets` for the warm-up that covers those.
|
|
84
176
|
|
|
85
177
|
### Screen-reading builtins
|
|
86
178
|
|
|
@@ -109,6 +201,43 @@ Two places where a straight port really does differ:
|
|
|
109
201
|
uniform (initialise it to `1.0`) and multiply vertical offsets by it; the
|
|
110
202
|
renderer feeds `-1` on that path and `+1` everywhere else.
|
|
111
203
|
|
|
204
|
+
## A ShaderEffect cannot shade a mesh on WebGPU
|
|
205
|
+
|
|
206
|
+
The single-effect fast path swaps the renderable's program outright instead of
|
|
207
|
+
rendering through an offscreen target. On WebGL that works for a `Mesh` too, so
|
|
208
|
+
a custom mesh shader is a `ShaderEffect` away. **On WebGPU it does not** — the
|
|
209
|
+
renderer says so in its own source, warns once, and then keeps the built-in
|
|
210
|
+
mesh shading.
|
|
211
|
+
|
|
212
|
+
Nothing throws. The mesh simply draws with its plain texture, which looks like
|
|
213
|
+
a broken shader, a bad binding, or a texture that failed to load — and since
|
|
214
|
+
`video.AUTO` prefers WebGPU where it is available, this is the default outcome
|
|
215
|
+
on most desktops. It cost several hours here: the symptom was a water surface
|
|
216
|
+
rendering flat white, and alpha, bindings, transparency and lighting were all
|
|
217
|
+
eliminated before the backend was.
|
|
218
|
+
|
|
219
|
+
2D renderables are unaffected: `Sprite`, `Text` and the rest go through the
|
|
220
|
+
quad path, which hosts effects on both backends. So a gradient fill for text,
|
|
221
|
+
or a refraction over a sprite, is fine — it is specifically the mesh path.
|
|
222
|
+
|
|
223
|
+
If you need a genuinely custom mesh shader that runs everywhere, supply a full
|
|
224
|
+
dual-language `GLShader` rather than a fragment body, and declare the fog
|
|
225
|
+
uniforms yourself (see `melonjs-3d`).
|
|
226
|
+
|
|
227
|
+
## Extra textures bind in setTexture order
|
|
228
|
+
|
|
229
|
+
`setTexture("uFoo", …)` assigns bindings **1 and 2** to the first extra
|
|
230
|
+
texture, **3 and 4** to the second, and so on. The WGSL must match:
|
|
231
|
+
|
|
232
|
+
```wgsl
|
|
233
|
+
@group(3) @binding(1) var uFoo : texture_2d<f32>;
|
|
234
|
+
@group(3) @binding(2) var uFooSampler : sampler;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Copying a two-texture effect and deleting one leaves the survivor declared at
|
|
238
|
+
3/4 while it is bound at 1/2 — the program fails to build and the effect is
|
|
239
|
+
silently dropped.
|
|
240
|
+
|
|
112
241
|
## Blend modes
|
|
113
242
|
|
|
114
243
|
Fourteen modes; thirteen of them are honoured by all three renderers:
|
|
@@ -46,11 +46,141 @@ The type strings are melonJS-specific — this is not a generic loader:
|
|
|
46
46
|
| `"binary"` | raw data, bitmap font `.fnt` descriptors |
|
|
47
47
|
| `"fontface"` | web fonts (`.ttf`, `.woff`) |
|
|
48
48
|
| `"aseprite"` | Aseprite JSON + image |
|
|
49
|
-
| `"shader"` | GLSL/WGSL shader sources |
|
|
49
|
+
| `"shader"` | GLSL/WGSL shader sources — `src` **or** inline via `data` |
|
|
50
50
|
| `"obj"` / `"mtl"` | Wavefront models and materials |
|
|
51
51
|
| `"gltf"` / `"glb"` | glTF scenes |
|
|
52
52
|
| `"js"` | scripts |
|
|
53
53
|
|
|
54
|
+
## What belongs in the manifest — and what cannot
|
|
55
|
+
|
|
56
|
+
Preloading is not only for things with a URL. Three tiers, and the middle one
|
|
57
|
+
is the one people miss.
|
|
58
|
+
|
|
59
|
+
**After preloading, switch to a stage of your own.** `preload()` puts you in
|
|
60
|
+
`state.LOADING`, and that stage is transitional: build your scene in the
|
|
61
|
+
callback and stay there, and nothing ever destroys it, so the built-in loading
|
|
62
|
+
screen's logo and progress bar remain on top of your game. See
|
|
63
|
+
`melonjs-scenes-and-state`.
|
|
64
|
+
|
|
65
|
+
**1. Assets with a source.** Everything in the type table above. Note `"shader"`
|
|
66
|
+
takes **inline source** as well as a `src`, via the `data` field:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { myRamp } from "./myRamp"; // a GLSL fragment body, in your own module
|
|
70
|
+
|
|
71
|
+
loader.preload([
|
|
72
|
+
{ name: "ramp", type: "shader", data: myRamp },
|
|
73
|
+
]);
|
|
74
|
+
const fx = loader.getShader("ramp"); // shared, already compiled
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This matters more than it looks. A `ShaderEffect` compiles its program in its
|
|
78
|
+
**constructor**, and `compileProgram` calls `getProgramParameter(LINK_STATUS)`
|
|
79
|
+
immediately after `linkProgram` — a blocking call that forces the driver to
|
|
80
|
+
finish. So building one during a scene costs that link on the frame it appears,
|
|
81
|
+
and building three identical ones costs it three times. Preloaded as an asset it
|
|
82
|
+
is compiled at load time, behind the loading screen, and the loader hands out a
|
|
83
|
+
**shared** instance (`effect.shared === true`), which is what stops one
|
|
84
|
+
renderable's teardown freeing it out from under the others. `clone()` does NOT
|
|
85
|
+
help here — it constructs a new effect, and therefore links a new program.
|
|
86
|
+
|
|
87
|
+
**2. Derived assets — build once, after the preload.** Canvases you bake,
|
|
88
|
+
`NoiseTexture2d`, `TextureAtlas`: the loader cannot carry these, but they must
|
|
89
|
+
not be rebuilt per scene either. The renderer's texture cache is keyed by the
|
|
90
|
+
**image object**, so a freshly baked canvas is a brand new GPU texture with a
|
|
91
|
+
new mip chain — rebuild one per stage and a title → play → game-over cycle
|
|
92
|
+
uploads four copies of the same pixels. Build them once after `preload`
|
|
93
|
+
resolves and share them, the way the platformer example builds its
|
|
94
|
+
`TextureAtlas` into a module the scenes read from.
|
|
95
|
+
|
|
96
|
+
**3. The engine's own shader programs — warmed for you, since 20.6.** The mesh
|
|
97
|
+
batcher compiles a program per *feature combination* (instance colours, instance
|
|
98
|
+
data, fog — fog joins the key), lazily, on the first draw that needs it.
|
|
99
|
+
`prewarm` builds them during `preload()` instead, behind the loading
|
|
100
|
+
screen:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const app = new Application(1280, 720, {
|
|
104
|
+
// on by default since 20.6 — named here only to show where it lives
|
|
105
|
+
prewarm: true,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**You do not call this yourself.** `preload()` calls `renderer.prewarm()` once
|
|
110
|
+
the assets are in and **before** `LOADER_COMPLETE` fires, so it happens while
|
|
111
|
+
the progress bar is still up. Every `preload()` warms, not just the first, so a
|
|
112
|
+
per-level preload warms whatever that level brought with it.
|
|
113
|
+
|
|
114
|
+
The one precondition: **a game that never calls `loader.preload()` is never
|
|
115
|
+
warmed**, because that is the only hook. If you load assets another way, call
|
|
116
|
+
`await app.renderer.prewarm()` yourself. On
|
|
117
|
+
WebGL that links the mesh batchers' variants, for the unlit tier and the lit
|
|
118
|
+
one that inherits from it; on WebGPU it compiles the
|
|
119
|
+
WGSL module of every shader the loader holds — which is the reason to declare
|
|
120
|
+
effects as assets rather than build them inline (see above).
|
|
121
|
+
|
|
122
|
+
Cost to a game that uses none of it is about 60ms of preload on a desktop GPU.
|
|
123
|
+
Set it to `false` only for a target where linking is slow enough that the
|
|
124
|
+
preload itself would suffer.
|
|
125
|
+
|
|
126
|
+
**What it does not do.** It makes the shaders *ready* earlier. It does not
|
|
127
|
+
remove the hitch on a scene's first frame, because most of that frame is
|
|
128
|
+
first-**submit** work — texture residency, buffer uploads, the driver
|
|
129
|
+
specializing against real bindings — which compiling a program does not touch.
|
|
130
|
+
For that, the lever is still to **draw** the scene once while the loading screen
|
|
131
|
+
is up and then throw it away; the warmed GPU state stays behind.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
await loader.preload(resources);
|
|
135
|
+
buildScene(app); // same geometry the level uses
|
|
136
|
+
await twoFrames(); // event.GAME_AFTER_DRAW
|
|
137
|
+
removeOnlyWhatYouAdded(app); // keep the programs, drop the scene
|
|
138
|
+
state.change(state.PLAY);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**It borrows the LOADING stage's world, and that has three teeth**, all learned
|
|
142
|
+
the hard way:
|
|
143
|
+
|
|
144
|
+
- **Never tear down with `app.world.reset()`.** It clears *every* child of that
|
|
145
|
+
world, the loading screen's own included, and the screen goes blank for the
|
|
146
|
+
rest of the load. Snapshot `app.world.children` first and remove only what
|
|
147
|
+
you added.
|
|
148
|
+
- **Restore anything the warm-up changes that the loading screen shows** — the
|
|
149
|
+
renderer's `backgroundColor` above all, or the warm-up flashes its own empty
|
|
150
|
+
first frame.
|
|
151
|
+
- **Under a `Camera3d` it will hide the built-in loading screen, and there is no
|
|
152
|
+
clean way around it** — see below.
|
|
153
|
+
|
|
154
|
+
### `DefaultLoadingScreen` lives in the WORLD, not on the screen
|
|
155
|
+
|
|
156
|
+
The built-in loading screen is written for a `Camera2d`. Its progress bar and
|
|
157
|
+
logo are added with `app.world.addChild(bar, 1)` / `addChild(logo, 2)` and are
|
|
158
|
+
**`floating === false`** — ordinary world-space renderables. They read as an
|
|
159
|
+
overlay only because that stage's world is otherwise empty.
|
|
160
|
+
|
|
161
|
+
So with `cameraClass: Camera3d`, anything you add to that world is in the scene
|
|
162
|
+
*with* them, and a 3D scene simply swallows them: the depth buffer puts terrain
|
|
163
|
+
in front of a bar sitting at z = 1. What does NOT rescue it, all tried:
|
|
164
|
+
|
|
165
|
+
- **Raising their `z`** — for a mesh on the `Camera3d` path `pos.z` IS world
|
|
166
|
+
depth, so the scene's own z values are geometry, not layering, and the bar
|
|
167
|
+
still loses to the depth test.
|
|
168
|
+
- **`moveToTop()`** — it early-returns for a child already at index 0, which the
|
|
169
|
+
loading screen's children always are, having been added before anything else.
|
|
170
|
+
It silently does nothing.
|
|
171
|
+
- **Flipping them to `floating = true`** for the duration — the scene still
|
|
172
|
+
covers them.
|
|
173
|
+
|
|
174
|
+
If you want a warm-up (or any 3D behind a loading screen), write your own
|
|
175
|
+
loading stage and draw its progress UI as **floating** renderables over the
|
|
176
|
+
scene. That is also the nicer result: the player watches the level assemble
|
|
177
|
+
instead of a logo. Budget it as real work, not a one-liner — and weigh it
|
|
178
|
+
against what the warm-up actually saves.
|
|
179
|
+
|
|
180
|
+
One more limit: a warm-up only covers the combinations it actually draws.
|
|
181
|
+
Renderables the real level has and the warm-up does not will still link on
|
|
182
|
+
entry.
|
|
183
|
+
|
|
54
184
|
## Two conventions that produce 404s
|
|
55
185
|
|
|
56
186
|
**Audio `src` is a directory.** The loader appends the asset name plus each
|
|
@@ -81,7 +81,16 @@ Two rules that cause subtle bugs when missed:
|
|
|
81
81
|
successfully recycled object never sees it. Pair event subscriptions with
|
|
82
82
|
`onActivateEvent` / `onDeactivateEvent` instead, or you leak handlers.
|
|
83
83
|
|
|
84
|
-
Engine classes are poolable too: `pool.pull("
|
|
84
|
+
Engine classes are poolable too: `pool.pull("Tween", target)`. The canonical
|
|
85
|
+
names are unprefixed — `Entity`, `Collectable`, `Trigger`, `Light2d`,
|
|
86
|
+
`Particle`, `Sprite`, `NineSliceSprite`, `Renderable`, `Text`, `BitmapText`,
|
|
87
|
+
`ImageLayer`, `Tween`, `ColorLayer`.
|
|
88
|
+
|
|
89
|
+
`pool.register` additionally aliases every name under an `me.` prefix, pointing
|
|
90
|
+
at the same entry, so `pool.pull("me.Tween")` resolves identically — and the
|
|
91
|
+
same alias is registered with the Tiled object factory, which is why a map
|
|
92
|
+
authored against melonJS 1.x still finds its classes. Prefer the unprefixed
|
|
93
|
+
name in new code; do not "correct" an `me.`-prefixed one, it is not broken.
|
|
85
94
|
|
|
86
95
|
## `scale()` is multiplicative
|
|
87
96
|
|
|
@@ -151,6 +160,77 @@ queries one at a time, usually not. See `melonjs-3d` for the decision table.
|
|
|
151
160
|
GPU release. Images, by contrast, only lose their loader-side reference; the
|
|
152
161
|
renderer's texture cache is not purged by unloading.
|
|
153
162
|
|
|
163
|
+
## The first frame of a scene is not like the others
|
|
164
|
+
|
|
165
|
+
A freeze the first time a level appears, and never again, is almost always
|
|
166
|
+
**shader linking**, not your scene build. Measure before assuming: procedural
|
|
167
|
+
geometry is usually trivial next to it.
|
|
168
|
+
|
|
169
|
+
Programs are created lazily. The mesh batcher keys a variant on the feature
|
|
170
|
+
combination it is asked for (lit, instanced, instance colours, instance data,
|
|
171
|
+
fog) and links it on the first draw that needs it — and `compileProgram` calls
|
|
172
|
+
`getProgramParameter(LINK_STATUS)` right after `linkProgram`, which blocks until
|
|
173
|
+
the driver finishes. That whole cost lands inside one frame.
|
|
174
|
+
|
|
175
|
+
Since 20.6 the engine's own programs are built during `preload()` instead —
|
|
176
|
+
`loader.preload()` calls `renderer.prewarm()` itself, on every call, when the
|
|
177
|
+
application's `prewarm` setting is on, which is the default. So on a current
|
|
178
|
+
version this is one cause you can rule out before looking further, **unless the
|
|
179
|
+
game does not use the preloader at all**, which is the one way to miss it — see `melonjs-loading-assets`. It does not
|
|
180
|
+
cover effects you construct inline, and it does not cover the rest of what a
|
|
181
|
+
first frame pays for, below.
|
|
182
|
+
|
|
183
|
+
How to tell them apart, all from the page:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// 1. is it the build, or the draw? time your own setup
|
|
187
|
+
const t0 = performance.now(); buildLevel(); console.log(performance.now() - t0);
|
|
188
|
+
|
|
189
|
+
// 2. how long is the bad frame? rAF deltas, not averages
|
|
190
|
+
let last = performance.now();
|
|
191
|
+
const tick = () => { const n = performance.now();
|
|
192
|
+
if (n - last > 40) console.log("long frame", (n - last).toFixed(0));
|
|
193
|
+
last = n; requestAnimationFrame(tick); };
|
|
194
|
+
|
|
195
|
+
// 3. is it programs? count them, and when
|
|
196
|
+
const gl = WebGL2RenderingContext.prototype, link = gl.linkProgram;
|
|
197
|
+
gl.linkProgram = function (p) { console.count("linkProgram"); return link.call(this, p); };
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The tell is that the second entry into the same scene is cheap: one measured
|
|
201
|
+
case went 852ms of stall (worst frame 479ms) on first entry to 277ms (worst
|
|
202
|
+
102ms) on the second.
|
|
203
|
+
|
|
204
|
+
**Careful with the conclusion, though — "the programs were cached" is the
|
|
205
|
+
tempting reading and it is wrong.** Prewarming the engine's own programs ahead
|
|
206
|
+
of that first entry moves the worst frame by about 3ms, measured. So the 575ms
|
|
207
|
+
is not program linking: it is everything else the first *submit* pays for —
|
|
208
|
+
texture residency, buffer uploads, and the driver specializing a pipeline
|
|
209
|
+
against real bindings — all of which the second entry finds already done.
|
|
210
|
+
|
|
211
|
+
That distinction decides which lever to reach for. Linking is what `prewarm`
|
|
212
|
+
covers and what shrinking your shader count helps. The rest only goes away if
|
|
213
|
+
the geometry is actually **drawn** once beforehand.
|
|
214
|
+
|
|
215
|
+
Three things follow.
|
|
216
|
+
|
|
217
|
+
- **Cut the number of programs.** Identical `ShaderEffect`s each link their own,
|
|
218
|
+
so preload one as a `"shader"` asset and share it.
|
|
219
|
+
- **Declare effects as assets, not inline**, so the loader can prepare them
|
|
220
|
+
before a scene draws — an inline effect does not exist until the stage that
|
|
221
|
+
builds it runs.
|
|
222
|
+
- **Pay what remains behind the loading screen** by drawing the scene once there
|
|
223
|
+
and discarding it (see `melonjs-loading-assets`). Linking is only part of a
|
|
224
|
+
first frame: texture residency, buffer uploads and the driver specializing
|
|
225
|
+
against real bindings all land on the first *submit*, and only an actual draw
|
|
226
|
+
front-loads those. A measured case on a fast desktop GPU had shader prewarming
|
|
227
|
+
move the worst frame by ~3ms while a warm-up draw moved it by hundreds.
|
|
228
|
+
|
|
229
|
+
Do not profile this on a software rasterizer — headless Chromium falls back to
|
|
230
|
+
SwiftShader, where the absolute numbers are wildly pessimistic and the JS
|
|
231
|
+
profile shows the time as `(program)`, outside JS entirely. Ratios transfer;
|
|
232
|
+
milliseconds do not.
|
|
233
|
+
|
|
154
234
|
## Where the costs actually are
|
|
155
235
|
|
|
156
236
|
In rough order for a typical 2D game:
|
|
@@ -175,6 +255,8 @@ In rough order for a typical 2D game:
|
|
|
175
255
|
| everything updates even when idle | `update()` returning `true` unconditionally |
|
|
176
256
|
| memory grows with long music | missing `stream: true` |
|
|
177
257
|
| slow only on some machines | Canvas fallback — no GPU tilemap path |
|
|
258
|
+
| a freeze the first time a level shows, never again | shader variants linking on first draw — warm them behind the loading screen |
|
|
259
|
+
| the same scene stutters once per entry | a derived texture rebuilt per stage: the texture cache keys on the image object, so each rebuild is a new upload |
|
|
178
260
|
|
|
179
261
|
## Related skills
|
|
180
262
|
|
|
@@ -120,6 +120,19 @@ this.body.setVelocity(vx, vy);
|
|
|
120
120
|
this.body.force.x = this.body.maxVel.x;
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
`maxVel` is what turns a force into a speed, so the two are set together. Use
|
|
124
|
+
the setters rather than writing the vectors — both take an optional THIRD
|
|
125
|
+
argument for the depth axis, left unchanged when omitted, so a 2D call keeps
|
|
126
|
+
its behaviour:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
this.body.setMaxVelocity(3, 15); // walk speed, jump speed
|
|
130
|
+
this.body.setFriction(0.4, 0); // ground drag, none vertically
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`friction` here is per-axis per-step velocity damping, not a surface
|
|
134
|
+
coefficient — see the built-in quirks below.
|
|
135
|
+
|
|
123
136
|
Under matter and planck, `syncFromPhysics()` copies the engine body's position
|
|
124
137
|
back onto `renderable.pos` after every step, so a direct write is simply erased
|
|
125
138
|
— no error. The built-in world integrates `pos` in place instead, so a write
|
|
@@ -227,6 +240,69 @@ Two prerequisites that produce empty results rather than errors:
|
|
|
227
240
|
- On the **built-in** adapter, the broadphase is cleared and rebuilt inside
|
|
228
241
|
`world.update()`, so querying before the first update returns nothing.
|
|
229
242
|
|
|
243
|
+
## 3D collision: `Box3d` and the Z pushback
|
|
244
|
+
|
|
245
|
+
The narrowphase is not 2D-only. `Box3d` is the one shape with a depth extent,
|
|
246
|
+
and a `Box3d`-vs-`Box3d` contact is resolved in three dimensions — it is the
|
|
247
|
+
only pair in the engine that can push back along **Z**.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import { Box3d } from "melonjs";
|
|
251
|
+
|
|
252
|
+
// centre offset from the body, then half-extents on each axis
|
|
253
|
+
rock.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));
|
|
254
|
+
boat.body.addShape(new Box3d(0, 0, 0, 30, 40, 60));
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Reach for this for anything moving in the XZ plane under a `Camera3d` — a
|
|
258
|
+
runner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling
|
|
259
|
+
a distance check there is the usual mistake, and it throws away the
|
|
260
|
+
penetration vector the response already carries.
|
|
261
|
+
|
|
262
|
+
**Read the Z axis off the response, not `overlapN`.** The MTV is a single
|
|
263
|
+
axis, so exactly one of `overlapN.x`, `overlapN.y` and `overlapNZ` is
|
|
264
|
+
non-zero:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
onCollision(response, other) {
|
|
268
|
+
if (response.overlapNZ !== 0) {
|
|
269
|
+
// a depth-only contact: overlapN / overlapV are BOTH zero here
|
|
270
|
+
this.pos.z -= response.overlapZ;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
That is deliberate: a collision resolved along Z leaves the 2D fields at zero,
|
|
277
|
+
so an existing 2D `onCollision` applies no push rather than a wrong one.
|
|
278
|
+
|
|
279
|
+
**Mixed pairs degrade to 2D.** `Box3d` against a `Polygon`, `Rectangle`,
|
|
280
|
+
`RoundRect` or `Ellipse` tests the box's XY footprint and treats the planar
|
|
281
|
+
shape as unbounded along Z. Only `Box3d`-vs-`Box3d` gives a depth result, so
|
|
282
|
+
give BOTH sides a `Box3d` when you want one.
|
|
283
|
+
|
|
284
|
+
This is the narrowphase and is independent of the 3D broadphase queries above
|
|
285
|
+
— `querySphere` / `raycast3d` need `world.sortOn === "depth"`; `Box3d` shapes
|
|
286
|
+
do not.
|
|
287
|
+
|
|
288
|
+
**Bounds are what the broadphase sorts by, and they come from the SHAPE.**
|
|
289
|
+
Every item is filed by its renderable's 2D `getBounds()`, so an object with no
|
|
290
|
+
extent cannot be placed and silently collides with nothing. A `Mesh` takes its
|
|
291
|
+
extent from its geometry and a `GLTFModel` from its part meshes, so both report
|
|
292
|
+
where they are without being told. A body's shapes do **not** contribute, so a
|
|
293
|
+
custom collidable built on a bare `Container` has no extent until you give it
|
|
294
|
+
one:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
// a container reports an EMPTY bounds until it has a size of its own
|
|
298
|
+
group.resize(width, height);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child
|
|
302
|
+
list on every update — reach for it only when the extent genuinely IS the
|
|
303
|
+
members' union and moves with them (a flock, a squad), not to give a fixed
|
|
304
|
+
group a size.
|
|
305
|
+
|
|
230
306
|
## Built-in world quirks
|
|
231
307
|
|
|
232
308
|
These are specific to the default world and surprise people arriving from a
|
|
@@ -308,6 +384,8 @@ use them. `adapter.capabilities` (`constraints`,
|
|
|
308
384
|
| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |
|
|
309
385
|
| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |
|
|
310
386
|
| `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== "depth"`) |
|
|
387
|
+
| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |
|
|
388
|
+
| two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |
|
|
311
389
|
| `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |
|
|
312
390
|
| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |
|
|
313
391
|
| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |
|
|
@@ -145,7 +145,36 @@ at `enabled === false` and no-ops every method rather than throwing — and
|
|
|
145
145
|
backend at runtime, an effect that works on your machine may do nothing on
|
|
146
146
|
another.
|
|
147
147
|
|
|
148
|
-
## 5.
|
|
148
|
+
## 5. Opacity cascades; `setChildsProperty` assigns
|
|
149
|
+
|
|
150
|
+
`alpha` composes down the tree. Fading a container fades everything inside
|
|
151
|
+
it, and the values multiply — a child at `0.5` inside a parent at `0.5` draws
|
|
152
|
+
at `0.25`. Each renderable keeps its own value; the composition happens at
|
|
153
|
+
draw time and is undone when the renderable is finished.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
model.alpha = 0.3; // fades the whole rig
|
|
157
|
+
container.setChildsProperty("alpha", 0.3, true); // REWRITES each child's alpha
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The two are not interchangeable. `setChildsProperty` is a bulk assignment: it
|
|
161
|
+
changes what the children *are*, so their own `alpha` now reads `0.3` and
|
|
162
|
+
stays that way after the container goes back to full. Reach for it when you
|
|
163
|
+
mean to change the children, and for `alpha` when you mean to fade a subtree.
|
|
164
|
+
|
|
165
|
+
Do not assign alpha down a hierarchy by hand. The renderer composes it as it
|
|
166
|
+
walks the tree, so setting it at more than one level multiplies: in a rig
|
|
167
|
+
nested three deep, `0.3` applied at every level lands parts on `0.3`, `0.09`
|
|
168
|
+
and `0.027` depending where they sit. The symptom does not look like an
|
|
169
|
+
opacity problem — a model whose parts hold *different* opacities, and which
|
|
170
|
+
appears to change opacity as it turns, because turning changes which parts
|
|
171
|
+
face the camera.
|
|
172
|
+
|
|
173
|
+
A useful consequence: a `GLTFModel` is a `Container`, so `model.alpha` fades
|
|
174
|
+
every part of a loaded rig — which is what you want for a ghost, a fade-in,
|
|
175
|
+
or a reflection.
|
|
176
|
+
|
|
177
|
+
## 6. `Vector3d.set(x, y)` silently zeroes `z`
|
|
149
178
|
|
|
150
179
|
```js
|
|
151
180
|
v.set(10, 20); // z becomes 0, not "unchanged"
|
|
@@ -155,6 +184,37 @@ v.set(10, 20, v.z); // keep it
|
|
|
155
184
|
`pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same
|
|
156
185
|
— so `this.pos.set(x, y)` wipes the object's depth.
|
|
157
186
|
|
|
187
|
+
## 7. A settings key the class never reads is silently ignored
|
|
188
|
+
|
|
189
|
+
A settings object is a plain literal: nothing rejects a key, so a name that
|
|
190
|
+
belongs somewhere else does nothing at all and says nothing about it.
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// WRONG — only the RENDERER reads a `blendMode` setting, never a renderable
|
|
194
|
+
const glow = new Sprite3d(x, y, { image: "sun", blendMode: "additive" });
|
|
195
|
+
|
|
196
|
+
// right: it is a property
|
|
197
|
+
glow.blendMode = "additive";
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The same shape catches `transparent` on a `GLTFModel` (its parts carry it —
|
|
201
|
+
`model.setChildsProperty("transparent", true, true)`) and any `Mesh` setting
|
|
202
|
+
handed to a class that forwards only a curated subset. If a visual option
|
|
203
|
+
appears to do nothing, check whether the class actually reads it before
|
|
204
|
+
tuning the value.
|
|
205
|
+
|
|
206
|
+
## 8. `isRenderable` does not hide anything
|
|
207
|
+
|
|
208
|
+
It gates `updateBounds`, not drawing. Setting it `false` leaves the object on
|
|
209
|
+
screen and merely stops its bounds tracking, which is worse than doing nothing.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
sprite.isRenderable = false; // WRONG — still drawn, bounds now stale
|
|
213
|
+
sprite.alpha = 0; // hides it
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use `alpha` for a blink, or remove the child for a long absence.
|
|
217
|
+
|
|
158
218
|
## Update and draw
|
|
159
219
|
|
|
160
220
|
```js
|
|
@@ -207,6 +267,8 @@ Hand-rolled equivalents miss the batching and the multi-backend support.
|
|
|
207
267
|
| custom draw offset by half the size | centred `anchorPoint` default not zeroed |
|
|
208
268
|
| spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |
|
|
209
269
|
| `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 |
|
|
270
|
+
| a settings option appears to do nothing | the class never reads that key — see section 7 |
|
|
271
|
+
| `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |
|
|
210
272
|
|
|
211
273
|
## Related skills
|
|
212
274
|
|
|
@@ -132,6 +132,14 @@ event.on(event.ONCONTEXT_RESTORED, (renderer) => {
|
|
|
132
132
|
});
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
## Test on the backend your users get, not the one your harness picks
|
|
136
|
+
|
|
137
|
+
`video.AUTO` prefers WebGPU where it exists. Headless Chromium usually has no
|
|
138
|
+
GPU adapter and falls back to WebGL, so an automated suite can pass every run
|
|
139
|
+
while the WebGPU path is broken — the two differ in real, throwing ways (see
|
|
140
|
+
below). Pin the backend explicitly when it matters: `renderer: video.WEBGPU`,
|
|
141
|
+
or the `#webgpu` / `#webgl` URI fragment on a page you can open by hand.
|
|
142
|
+
|
|
135
143
|
## Practical differences
|
|
136
144
|
|
|
137
145
|
- **Frame capture orientation differs**: row 0 of a `toFrameTexture()` capture is
|
|
@@ -147,6 +155,15 @@ event.on(event.ONCONTEXT_RESTORED, (renderer) => {
|
|
|
147
155
|
accents, ruinous for hundreds of objects. `normal`, `additive`, `multiply`,
|
|
148
156
|
`screen` and `exclusion` are ordinary fixed-function state and cost nothing
|
|
149
157
|
extra.
|
|
158
|
+
- **WebGPU rejects a zero-sized image source; WebGL shrugs at it.** Uploading a
|
|
159
|
+
0-width or 0-height canvas throws
|
|
160
|
+
`OperationError: GPUQueue.copyExternalImageToTexture: External image state is
|
|
161
|
+
not valid`, from a stack that reads `drawImage → addQuad → getBinding`. WebGL
|
|
162
|
+
uploads the same source without complaint, so this is a class of bug that
|
|
163
|
+
cannot appear on one backend and stops the game dead on the other. The usual
|
|
164
|
+
source is a `Text` whose string is empty or whitespace-only — it measures to
|
|
165
|
+
nothing, so its glyph canvas has no width. Give a placeholder label a real
|
|
166
|
+
glyph and hide it with `alpha`/position instead of blanking its text.
|
|
150
167
|
- **3D meshes do not honour blend modes.** The mesh pass draws with its own
|
|
151
168
|
depth/blend state; asking for one of the advanced modes logs a one-time
|
|
152
169
|
`blend mode "…" is not supported for 3D meshes` warning and falls back to
|
|
@@ -163,6 +180,8 @@ event.on(event.ONCONTEXT_RESTORED, (renderer) => {
|
|
|
163
180
|
| a solid box where an effect should be | carrier drawn while the effect is disabled |
|
|
164
181
|
| nothing renders at all on an old device | `video.WEBGL` pinned; WebGL 1 is rejected, not downgraded |
|
|
165
182
|
| ported shader is vertically flipped | the WebGL capture is bottom-up, the WebGPU one top-down |
|
|
183
|
+
| `copyExternalImageToTexture: External image state is not valid` | a zero-sized upload source — most often a `Text` with an empty or whitespace-only string |
|
|
184
|
+
| runs on the test machine, dies on the dev's | the harness fell back to WebGL and never exercised WebGPU |
|
|
166
185
|
| a blended mode is ignored on a mesh | meshes fall back to `"normal"`; look for the one-time warning |
|
|
167
186
|
| textures vanish after the tab is restored | cached raw GPU handles not re-acquired on context restore |
|
|
168
187
|
|