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
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* melonJS Game Engine - 20.
|
|
2
|
+
* melonJS Game Engine - 20.6.0
|
|
3
3
|
* http://www.melonjs.org
|
|
4
4
|
* melonjs is licensed under the MIT License.
|
|
5
5
|
* http://www.opensource.org/licenses/mit-license
|
|
@@ -1843,17 +1843,33 @@ var Polygon = class {
|
|
|
1843
1843
|
*/
|
|
1844
1844
|
edges;
|
|
1845
1845
|
/**
|
|
1846
|
-
* a list of indices for all vertices composing this polygon
|
|
1846
|
+
* a list of indices for all vertices composing this polygon, filled in by
|
|
1847
|
+
* {@link Polygon#getIndices} and invalidated by {@link Polygon#recalc}.
|
|
1848
|
+
*
|
|
1849
|
+
* A subclass may repurpose this name for geometry of its own: a
|
|
1850
|
+
* {@link Mesh} holds a GPU index buffer here, a typed array rather than a
|
|
1851
|
+
* plain one. `recalc` therefore only clears the slot when it owns it —
|
|
1852
|
+
* anything else reading it should check what it got.
|
|
1847
1853
|
*/
|
|
1848
1854
|
indices;
|
|
1849
1855
|
/**
|
|
1850
|
-
* The normals here are the direction of the normal for the `n`th
|
|
1851
|
-
* to the position of the `n`th point. If you want to draw
|
|
1852
|
-
* translate to the position of the starting
|
|
1856
|
+
* The normals here are the direction of the normal for the `n`th EDGE of the
|
|
1857
|
+
* polygon, relative to the position of the `n`th point. If you want to draw
|
|
1858
|
+
* an edge normal, you must first translate to the position of the starting
|
|
1859
|
+
* point.
|
|
1860
|
+
*
|
|
1861
|
+
* Named for the edges rather than just `normals` because a subclass may
|
|
1862
|
+
* carry normals of its own with a different meaning and a different type:
|
|
1863
|
+
* a {@link Mesh} holds per-VERTEX normals as a `Float32Array`, and when
|
|
1864
|
+
* that shadowed this one, {@link Polygon#recalc} walked it expecting
|
|
1865
|
+
* `Vector2d`s. It allocates only into empty slots, and a typed array's
|
|
1866
|
+
* slots are `0` rather than `undefined` — so it took the reuse branch and
|
|
1867
|
+
* threw `Cannot create property 'x' on number '0'`, breaking `recalc`,
|
|
1868
|
+
* `resize` and the `width`/`height` setters on every mesh.
|
|
1853
1869
|
* @ignore
|
|
1854
1870
|
* @internal
|
|
1855
1871
|
*/
|
|
1856
|
-
|
|
1872
|
+
edgeNormals;
|
|
1857
1873
|
/**
|
|
1858
1874
|
* The bounding rectangle for this shape
|
|
1859
1875
|
* @ignore
|
|
@@ -1874,7 +1890,7 @@ var Polygon = class {
|
|
|
1874
1890
|
this.points = [];
|
|
1875
1891
|
this.edges = [];
|
|
1876
1892
|
this.indices = [];
|
|
1877
|
-
this.
|
|
1893
|
+
this.edgeNormals = [];
|
|
1878
1894
|
this._bounds = boundsPool.get();
|
|
1879
1895
|
this.setVertices(vertices);
|
|
1880
1896
|
}
|
|
@@ -2016,7 +2032,7 @@ var Polygon = class {
|
|
|
2016
2032
|
*/
|
|
2017
2033
|
recalc() {
|
|
2018
2034
|
const edges = this.edges;
|
|
2019
|
-
const normals = this.
|
|
2035
|
+
const normals = this.edgeNormals;
|
|
2020
2036
|
const indices = this.indices;
|
|
2021
2037
|
const points = this.points;
|
|
2022
2038
|
const len = points.length;
|
|
@@ -2047,7 +2063,9 @@ var Polygon = class {
|
|
|
2047
2063
|
}
|
|
2048
2064
|
edges.length = len;
|
|
2049
2065
|
normals.length = len;
|
|
2050
|
-
indices
|
|
2066
|
+
if (Array.isArray(indices)) {
|
|
2067
|
+
indices.length = 0;
|
|
2068
|
+
}
|
|
2051
2069
|
return this;
|
|
2052
2070
|
}
|
|
2053
2071
|
/**
|
|
@@ -3097,6 +3115,7 @@ var save = {
|
|
|
3097
3115
|
if (hasLocalStorage) {
|
|
3098
3116
|
localStorage.setItem("me.save", JSON.stringify(Object.keys(data)));
|
|
3099
3117
|
}
|
|
3118
|
+
return save;
|
|
3100
3119
|
},
|
|
3101
3120
|
/**
|
|
3102
3121
|
* Remove a key from localStorage
|
|
@@ -4255,11 +4274,22 @@ var Gradient = class {
|
|
|
4255
4274
|
return this;
|
|
4256
4275
|
}
|
|
4257
4276
|
/**
|
|
4258
|
-
* Get or create a native CanvasGradient for use with a 2D context.
|
|
4277
|
+
* Get or create a native `CanvasGradient` for use with a 2D context.
|
|
4278
|
+
*
|
|
4279
|
+
* This is the one to reach for when baking a gradient into your own
|
|
4280
|
+
* canvas — it hands back a value for `ctx.fillStyle` and touches no
|
|
4281
|
+
* shared state, so two callers cannot clobber each other.
|
|
4282
|
+
*
|
|
4283
|
+
* The result is cached and reused until a stop changes.
|
|
4259
4284
|
* @param {CanvasRenderingContext2D} context - the 2D context to create the gradient on
|
|
4260
|
-
* @returns {CanvasGradient}
|
|
4261
|
-
* @
|
|
4262
|
-
*
|
|
4285
|
+
* @returns {CanvasGradient} a native gradient bound to that context
|
|
4286
|
+
* @example
|
|
4287
|
+
* // bake a gradient into a standalone canvas
|
|
4288
|
+
* const c = document.createElement("canvas");
|
|
4289
|
+
* c.width = 64; c.height = 64;
|
|
4290
|
+
* const ctx = c.getContext("2d");
|
|
4291
|
+
* ctx.fillStyle = myGradient.toCanvasGradient(ctx);
|
|
4292
|
+
* ctx.fillRect(0, 0, 64, 64);
|
|
4263
4293
|
*/
|
|
4264
4294
|
toCanvasGradient(context) {
|
|
4265
4295
|
if (this._canvasGradient && !this._dirty) {
|
|
@@ -4297,6 +4327,10 @@ var Gradient = class {
|
|
|
4297
4327
|
* which is visually equivalent (linear stop interpolation × linear
|
|
4298
4328
|
* texture filtering). The returned `width`/`height` describe the region
|
|
4299
4329
|
* of the canvas the caller must use as the drawImage SOURCE rect.
|
|
4330
|
+
* Internal: the bake target is SHARED across every gradient and valid only
|
|
4331
|
+
* until the next call, which is a renderer implementation detail rather
|
|
4332
|
+
* than something a caller should reason about. The public way to rasterise
|
|
4333
|
+
* a gradient is {@link Gradient#toCanvasGradient}.
|
|
4300
4334
|
* @param {CanvasRenderer|WebGLRenderer} renderer - the active renderer (used to invalidate the GPU texture)
|
|
4301
4335
|
* @param {number} x - draw rect x
|
|
4302
4336
|
* @param {number} y - draw rect y
|
|
@@ -6857,6 +6891,52 @@ var Renderer = class _Renderer {
|
|
|
6857
6891
|
*/
|
|
6858
6892
|
flush() {
|
|
6859
6893
|
}
|
|
6894
|
+
/**
|
|
6895
|
+
* Compile the backend's built-in shader programs up front, so the first
|
|
6896
|
+
* frame that draws them does not stall while the driver builds them.
|
|
6897
|
+
*
|
|
6898
|
+
* A GPU backend does not finish a shader when you hand it the source. It
|
|
6899
|
+
* finishes it the first time something is actually drawn with it, which
|
|
6900
|
+
* puts the whole cost on the frame a scene first appears — the moment a
|
|
6901
|
+
* level or stage comes up and its geometry arrives a beat late. Calling
|
|
6902
|
+
* this while a loading screen is still on screen moves that cost to where
|
|
6903
|
+
* nobody is waiting on a frame.
|
|
6904
|
+
*
|
|
6905
|
+
* Resolves immediately on a backend with nothing to compile (Canvas).
|
|
6906
|
+
* What "ready" means is backend-specific and worth knowing: WebGL's link
|
|
6907
|
+
* blocks, so a resolved promise there means the programs are genuinely
|
|
6908
|
+
* built; WebGPU's `createShaderModule` returns before its compilation
|
|
6909
|
+
* finishes, so a resolved promise there means the modules have been
|
|
6910
|
+
* requested and validation is under way.
|
|
6911
|
+
*
|
|
6912
|
+
* **You normally do not call this.** `loader.preload()` calls it for you,
|
|
6913
|
+
* once the assets are in and before `LOADER_COMPLETE` fires — so it runs
|
|
6914
|
+
* while the loading screen is still up — whenever the application's
|
|
6915
|
+
* `prewarm` setting is on, which it is by default. Every `preload()` warms,
|
|
6916
|
+
* not just the first, so a per-level preload warms whatever that level
|
|
6917
|
+
* brought with it.
|
|
6918
|
+
*
|
|
6919
|
+
* Two consequences worth knowing:
|
|
6920
|
+
*
|
|
6921
|
+
* - **A game that never calls `loader.preload()` is never warmed.** There
|
|
6922
|
+
* is no other hook. Call this yourself if you load assets some other way
|
|
6923
|
+
* and still want the warm-up.
|
|
6924
|
+
* - It is deliberately NOT called from `Application.init()`. That runs
|
|
6925
|
+
* before anything is on screen, so warming there would only lengthen a
|
|
6926
|
+
* blank page instead of hiding behind a progress bar.
|
|
6927
|
+
* @returns {Promise<void>} settles when the built-in programs are ready
|
|
6928
|
+
* @example
|
|
6929
|
+
* // the usual case: nothing to write. `preload()` warms on the way through
|
|
6930
|
+
* await loader.preload(resources);
|
|
6931
|
+
* @example
|
|
6932
|
+
* // only needed if you do not use the preloader, or want a second warm-up
|
|
6933
|
+
* // at a point of your own choosing
|
|
6934
|
+
* await app.renderer.prewarm();
|
|
6935
|
+
* state.change(state.PLAY);
|
|
6936
|
+
*/
|
|
6937
|
+
prewarm() {
|
|
6938
|
+
return Promise.resolve();
|
|
6939
|
+
}
|
|
6860
6940
|
/**
|
|
6861
6941
|
* Mark the start of a screen-space (`floating`) draw, during which the
|
|
6862
6942
|
* camera's screen projection is installed and world-space geometry cannot
|
|
@@ -6956,9 +7036,18 @@ var Renderer = class _Renderer {
|
|
|
6956
7036
|
entry2.shader = this.customShader ?? null;
|
|
6957
7037
|
const v = this.currentTransform.val;
|
|
6958
7038
|
const m = modelMatrix.val;
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
7039
|
+
let cx = 0;
|
|
7040
|
+
let cy = 0;
|
|
7041
|
+
let cz = 0;
|
|
7042
|
+
if (typeof mesh?._updateLocalCenter === "function") {
|
|
7043
|
+
mesh._updateLocalCenter();
|
|
7044
|
+
cx = mesh._lcx;
|
|
7045
|
+
cy = mesh._lcy;
|
|
7046
|
+
cz = mesh._lcz;
|
|
7047
|
+
}
|
|
7048
|
+
const mx = m[0] * cx + m[4] * cy + m[8] * cz + m[12];
|
|
7049
|
+
const my = m[1] * cx + m[5] * cy + m[9] * cz + m[13];
|
|
7050
|
+
const mz = m[2] * cx + m[6] * cy + m[10] * cz + m[14];
|
|
6962
7051
|
const dx = v[0] * mx + v[4] * my + v[8] * mz + v[12];
|
|
6963
7052
|
const dy = v[1] * mx + v[5] * my + v[9] * mz + v[13];
|
|
6964
7053
|
const dz = v[2] * mx + v[6] * my + v[10] * mz + v[14];
|
|
@@ -7761,6 +7850,31 @@ var Renderer = class _Renderer {
|
|
|
7761
7850
|
);
|
|
7762
7851
|
}
|
|
7763
7852
|
break;
|
|
7853
|
+
// Box3d — a 3D box has no 2D outline of its own, so draw the XY
|
|
7854
|
+
// FOOTPRINT it already hands every 2D consumer through its own
|
|
7855
|
+
// `getBounds()` (the body bounds, the broadphase pre-gate, and
|
|
7856
|
+
// this). Without a case here it fell through to the `default`
|
|
7857
|
+
// below, so switching on the debug panel's hitbox overlay threw
|
|
7858
|
+
// "Invalid geometry for fill/stroke" on any body carrying one.
|
|
7859
|
+
case "Box3d": {
|
|
7860
|
+
const footprint = shape.getBounds();
|
|
7861
|
+
if (fill) {
|
|
7862
|
+
this.fillRect(
|
|
7863
|
+
footprint.left,
|
|
7864
|
+
footprint.top,
|
|
7865
|
+
footprint.width,
|
|
7866
|
+
footprint.height
|
|
7867
|
+
);
|
|
7868
|
+
} else {
|
|
7869
|
+
this.strokeRect(
|
|
7870
|
+
footprint.left,
|
|
7871
|
+
footprint.top,
|
|
7872
|
+
footprint.width,
|
|
7873
|
+
footprint.height
|
|
7874
|
+
);
|
|
7875
|
+
}
|
|
7876
|
+
break;
|
|
7877
|
+
}
|
|
7764
7878
|
// Rect or Bounds
|
|
7765
7879
|
case "Rectangle":
|
|
7766
7880
|
case "Bounds":
|
|
@@ -10411,14 +10525,16 @@ var Sound = class {
|
|
|
10411
10525
|
sound2._node.gain.linearRampToValueAtTime(to, end);
|
|
10412
10526
|
}
|
|
10413
10527
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
ids[i]
|
|
10420
|
-
|
|
10421
|
-
|
|
10528
|
+
const isGroup = typeof id === "undefined";
|
|
10529
|
+
if (!(len > 0) || to === from) {
|
|
10530
|
+
if (isGroup) {
|
|
10531
|
+
this._volume = to;
|
|
10532
|
+
}
|
|
10533
|
+
this.volume(to, ids[i]);
|
|
10534
|
+
this._emit("fade", ids[i]);
|
|
10535
|
+
continue;
|
|
10536
|
+
}
|
|
10537
|
+
this._startFadeInterval(sound2, from, to, len, ids[i], isGroup);
|
|
10422
10538
|
}
|
|
10423
10539
|
}
|
|
10424
10540
|
return this;
|
|
@@ -11269,13 +11385,14 @@ function tone(opts) {
|
|
|
11269
11385
|
gain = 0.1,
|
|
11270
11386
|
attack = 5e-3,
|
|
11271
11387
|
pan = 0,
|
|
11272
|
-
pitchSlide = 1
|
|
11388
|
+
pitchSlide = 1,
|
|
11389
|
+
delay = 0
|
|
11273
11390
|
} = opts;
|
|
11274
11391
|
const freqs = Array.isArray(freq) ? freq : [freq];
|
|
11275
11392
|
if (freqs.length === 0) return;
|
|
11276
11393
|
_resumeIfSuspended(ctx);
|
|
11277
11394
|
const dur = Math.max(1e-3, duration2);
|
|
11278
|
-
const t0 = ctx.currentTime;
|
|
11395
|
+
const t0 = ctx.currentTime + Math.max(0, delay);
|
|
11279
11396
|
const t1 = t0 + dur;
|
|
11280
11397
|
const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
|
|
11281
11398
|
const panner2 = _connectToOutput(ctx, env, pan, t0);
|
|
@@ -11348,11 +11465,12 @@ function noise(opts) {
|
|
|
11348
11465
|
attack = 5e-3,
|
|
11349
11466
|
pan = 0,
|
|
11350
11467
|
filter,
|
|
11351
|
-
filterSweep = 1
|
|
11468
|
+
filterSweep = 1,
|
|
11469
|
+
delay = 0
|
|
11352
11470
|
} = opts;
|
|
11353
11471
|
_resumeIfSuspended(ctx);
|
|
11354
11472
|
const dur = Math.max(1e-3, duration2);
|
|
11355
|
-
const t0 = ctx.currentTime;
|
|
11473
|
+
const t0 = ctx.currentTime + Math.max(0, delay);
|
|
11356
11474
|
const t1 = t0 + dur;
|
|
11357
11475
|
const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
|
|
11358
11476
|
const sampleCount = Math.max(1, Math.ceil(dur * ctx.sampleRate));
|
|
@@ -13357,7 +13475,7 @@ var input_exports = {};
|
|
|
13357
13475
|
__export(input_exports, {
|
|
13358
13476
|
GAMEPAD: () => GAMEPAD,
|
|
13359
13477
|
KEY: () => KEY,
|
|
13360
|
-
_app: () =>
|
|
13478
|
+
_app: () => _app3,
|
|
13361
13479
|
bindGamepad: () => bindGamepad,
|
|
13362
13480
|
bindKey: () => bindKey,
|
|
13363
13481
|
bindPointer: () => bindPointer,
|
|
@@ -15216,7 +15334,7 @@ var Line = class _Line extends Polygon {
|
|
|
15216
15334
|
*/
|
|
15217
15335
|
recalc() {
|
|
15218
15336
|
const edges = this.edges;
|
|
15219
|
-
const normals = this.
|
|
15337
|
+
const normals = this.edgeNormals;
|
|
15220
15338
|
const indices = this.indices;
|
|
15221
15339
|
const points = this.points;
|
|
15222
15340
|
if (points.length !== 2) {
|
|
@@ -17271,6 +17389,9 @@ var Renderable = class _Renderable extends Rect {
|
|
|
17271
17389
|
}
|
|
17272
17390
|
/**
|
|
17273
17391
|
* get the renderable alpha channel value<br>
|
|
17392
|
+
*
|
|
17393
|
+
* This is the renderable's OWN value. What it finally draws at is this
|
|
17394
|
+
* multiplied by every ancestor's — see {@link Renderable#alpha}.
|
|
17274
17395
|
* @returns {number} current opacity value between 0 and 1
|
|
17275
17396
|
*/
|
|
17276
17397
|
getOpacity() {
|
|
@@ -17668,7 +17789,10 @@ var Renderable = class _Renderable extends Rect {
|
|
|
17668
17789
|
if (this.applyAnchorTransform !== false) {
|
|
17669
17790
|
renderer.translate(-ax, -ay);
|
|
17670
17791
|
}
|
|
17671
|
-
renderer.setTint(
|
|
17792
|
+
renderer.setTint(
|
|
17793
|
+
this.tint,
|
|
17794
|
+
this.getOpacity() * (renderer.currentTint?.alpha ?? 1)
|
|
17795
|
+
);
|
|
17672
17796
|
renderer.setDepth(this.depth);
|
|
17673
17797
|
if (this.blendMode !== renderer.getBlendMode()) {
|
|
17674
17798
|
renderer.setBlendMode(this.blendMode);
|
|
@@ -18260,9 +18384,14 @@ var GLShader = class _GLShader {
|
|
|
18260
18384
|
/**
|
|
18261
18385
|
* Set the uniform to the given value
|
|
18262
18386
|
* @param {string} name - the uniform name
|
|
18263
|
-
* @param {
|
|
18387
|
+
* @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that
|
|
18388
|
+
* uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean;
|
|
18389
|
+
* vectors and matrices take an array, a `Float32Array`, or any object
|
|
18390
|
+
* exposing `toArray()` — which is every {@link Vector2d},
|
|
18391
|
+
* {@link Vector3d}, {@link Color} and {@link Matrix3d}.
|
|
18264
18392
|
* @example
|
|
18265
18393
|
* myShader.setUniform("uProjectionMatrix", this.projectionMatrix);
|
|
18394
|
+
* myShader.setUniform("uStrength", 0.5); // a scalar is a plain number
|
|
18266
18395
|
*/
|
|
18267
18396
|
setUniform(name, value) {
|
|
18268
18397
|
if (this.destroyed) {
|
|
@@ -18547,11 +18676,78 @@ function buildGLSLProgram(fragmentBody) {
|
|
|
18547
18676
|
return {
|
|
18548
18677
|
vertex: buildEffectVertex(builtins),
|
|
18549
18678
|
fragment: fragment2,
|
|
18679
|
+
// the annotation-stripped body, so a mesh-hosted effect can be
|
|
18680
|
+
// spliced into the engine's own mesh shader (#1658) without
|
|
18681
|
+
// re-parsing. Additive: the quad `vertex`/`fragment` above are
|
|
18682
|
+
// byte-identical to what they always were.
|
|
18683
|
+
body: builtins.body,
|
|
18684
|
+
builtins,
|
|
18550
18685
|
screenTextures: builtins.screenTextures,
|
|
18551
18686
|
noiseUV: builtins.noiseUV
|
|
18552
18687
|
};
|
|
18553
18688
|
}
|
|
18554
18689
|
|
|
18690
|
+
// src/video/effects/mesh_splice.js
|
|
18691
|
+
var HOOKS = [
|
|
18692
|
+
{
|
|
18693
|
+
pattern: /vec4\s+ME_effect\s*\(\s*vec4\s+c\s*,\s*vec2\s+uv\s*\)\s*\{[^}]*\}/,
|
|
18694
|
+
replacement: "vec4 ME_effect(vec4 c, vec2 uv) { return apply(c, uv); }"
|
|
18695
|
+
},
|
|
18696
|
+
{
|
|
18697
|
+
pattern: /fn\s+ME_effect\s*\(\s*c\s*:\s*vec4f\s*,\s*uv\s*:\s*vec2f\s*\)\s*->\s*vec4f\s*\{[^}]*\}/,
|
|
18698
|
+
replacement: "fn ME_effect(c : vec4f, uv : vec2f) -> vec4f { return apply(c, uv); }"
|
|
18699
|
+
}
|
|
18700
|
+
];
|
|
18701
|
+
var MESH_UNSUPPORTED = {
|
|
18702
|
+
noiseUV: "`noise_uv` (derived from a quad's image and object size)",
|
|
18703
|
+
screenUV: "`screen_uv` (needs the screen capture the quad tier takes)",
|
|
18704
|
+
screenTexture: "`screen_texture` (needs the screen capture)"
|
|
18705
|
+
};
|
|
18706
|
+
function meshHostingBlocker(builtins, textures, language2) {
|
|
18707
|
+
if (language2 === "wgsl" && builtins?.vColor === true) {
|
|
18708
|
+
return "it reads `vColor`, which only the 2D quad tier exposes to an effect body";
|
|
18709
|
+
}
|
|
18710
|
+
if (Array.isArray(textures) && textures.length > 0) {
|
|
18711
|
+
return "it declares its own texture samplers, which a mesh-hosted effect cannot bind yet \u2014 uniforms are supported, textures are not";
|
|
18712
|
+
}
|
|
18713
|
+
if (typeof builtins !== "object" || builtins === null) {
|
|
18714
|
+
return null;
|
|
18715
|
+
}
|
|
18716
|
+
const blocked = Object.keys(MESH_UNSUPPORTED).filter((flag) => {
|
|
18717
|
+
return builtins[flag] === true;
|
|
18718
|
+
}).map((flag) => {
|
|
18719
|
+
return MESH_UNSUPPORTED[flag];
|
|
18720
|
+
});
|
|
18721
|
+
return blocked.length === 0 ? null : `it uses ${blocked.join(" and ")}, which only the 2D quad tier provides`;
|
|
18722
|
+
}
|
|
18723
|
+
function hasSpliceMarkers(source) {
|
|
18724
|
+
return typeof source === "string" && HOOKS.some((hook) => {
|
|
18725
|
+
return hook.pattern.test(source);
|
|
18726
|
+
});
|
|
18727
|
+
}
|
|
18728
|
+
function spliceEffect(source, body) {
|
|
18729
|
+
if (typeof source !== "string" || typeof body !== "string") {
|
|
18730
|
+
return null;
|
|
18731
|
+
}
|
|
18732
|
+
for (const hook of HOOKS) {
|
|
18733
|
+
if (hook.pattern.test(source)) {
|
|
18734
|
+
return source.replace(hook.pattern, () => {
|
|
18735
|
+
return `${body}
|
|
18736
|
+
${hook.replacement}`;
|
|
18737
|
+
});
|
|
18738
|
+
}
|
|
18739
|
+
}
|
|
18740
|
+
return null;
|
|
18741
|
+
}
|
|
18742
|
+
function shiftGroup3(body, shift) {
|
|
18743
|
+
return body.replace(
|
|
18744
|
+
/@group\(\s*3\s*\)\s*@binding\(\s*(\d+)\s*\)/g,
|
|
18745
|
+
(_match, binding) => {
|
|
18746
|
+
return `@group(3) @binding(${Number(binding) + shift})`;
|
|
18747
|
+
}
|
|
18748
|
+
);
|
|
18749
|
+
}
|
|
18750
|
+
|
|
18555
18751
|
// src/video/effects/wgsl/layout.js
|
|
18556
18752
|
var TYPE_LAYOUT = {
|
|
18557
18753
|
f32: { align: 4, size: 4 },
|
|
@@ -18923,6 +19119,8 @@ var WGSLEffectRealization = class {
|
|
|
18923
19119
|
if (!this.valid) {
|
|
18924
19120
|
return;
|
|
18925
19121
|
}
|
|
19122
|
+
this.body = body;
|
|
19123
|
+
this.parsed = parsed;
|
|
18926
19124
|
this.layout = parsed.layout;
|
|
18927
19125
|
this.structSize = parsed.structSize;
|
|
18928
19126
|
this.textures = parsed.textures;
|
|
@@ -18930,6 +19128,7 @@ var WGSLEffectRealization = class {
|
|
|
18930
19128
|
const module = buildWGSLModule(body, parsed);
|
|
18931
19129
|
this.code = module.code;
|
|
18932
19130
|
this.builtinBindings = module.bindings;
|
|
19131
|
+
this.meshModules = /* @__PURE__ */ new Map();
|
|
18933
19132
|
this.cpu = new ArrayBuffer(Math.max(this.structSize, 0));
|
|
18934
19133
|
this.f32 = new Float32Array(this.cpu);
|
|
18935
19134
|
this.i32 = new Int32Array(this.cpu);
|
|
@@ -19006,9 +19205,30 @@ var WGSLEffectRealization = class {
|
|
|
19006
19205
|
releaseGPU() {
|
|
19007
19206
|
this.gpu = null;
|
|
19008
19207
|
}
|
|
19208
|
+
/**
|
|
19209
|
+
* This effect realized against the MESH contract: the engine's own mesh
|
|
19210
|
+
* module with the body spliced in at its `ME_effect` hook, and the
|
|
19211
|
+
* body's group-3 bindings shifted up one so `uMesh` keeps binding 0.
|
|
19212
|
+
*
|
|
19213
|
+
* The quad module (`this.code`) is untouched — a `Sprite` still gets the
|
|
19214
|
+
* exact text it always did.
|
|
19215
|
+
* @param {string} meshSource - the host mesh shader's WGSL text
|
|
19216
|
+
* @returns {string} the spliced module
|
|
19217
|
+
* @ignore
|
|
19218
|
+
* @internal
|
|
19219
|
+
*/
|
|
19220
|
+
meshModule(meshSource) {
|
|
19221
|
+
let code = this.meshModules.get(meshSource);
|
|
19222
|
+
if (typeof code === "undefined") {
|
|
19223
|
+
code = spliceEffect(meshSource, shiftGroup3(this.body, 1));
|
|
19224
|
+
this.meshModules.set(meshSource, code);
|
|
19225
|
+
}
|
|
19226
|
+
return code;
|
|
19227
|
+
}
|
|
19009
19228
|
};
|
|
19010
19229
|
|
|
19011
19230
|
// src/video/effects/shadereffect.js
|
|
19231
|
+
var _nextEffectId = 0;
|
|
19012
19232
|
var ShaderEffect = class _ShaderEffect {
|
|
19013
19233
|
/**
|
|
19014
19234
|
* whether this effect is active (false in Canvas mode, false after
|
|
@@ -19065,10 +19285,14 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19065
19285
|
return;
|
|
19066
19286
|
}
|
|
19067
19287
|
this._extraTextures = /* @__PURE__ */ new Map();
|
|
19288
|
+
this._uniformValues = /* @__PURE__ */ new Map();
|
|
19289
|
+
this._effectId = _nextEffectId++;
|
|
19068
19290
|
if (language2 === "glsl") {
|
|
19069
19291
|
const program = buildGLSLProgram(source);
|
|
19070
19292
|
this._screenTextureUniforms = program.screenTextures;
|
|
19071
19293
|
this._hasNoiseUV = program.noiseUV;
|
|
19294
|
+
this._meshBody = program.body;
|
|
19295
|
+
this._meshBuiltins = program.builtins;
|
|
19072
19296
|
this._shader = new GLShader(
|
|
19073
19297
|
renderer.gl,
|
|
19074
19298
|
program.vertex,
|
|
@@ -19138,12 +19362,23 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19138
19362
|
/**
|
|
19139
19363
|
* Set the uniform to the given value
|
|
19140
19364
|
* @param {string} name - the uniform name
|
|
19141
|
-
* @param {
|
|
19365
|
+
* @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that
|
|
19366
|
+
* uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean;
|
|
19367
|
+
* vectors and matrices take an array, a `Float32Array`, or any object
|
|
19368
|
+
* exposing `toArray()` — which is every {@link Vector2d},
|
|
19369
|
+
* {@link Vector3d}, {@link Color} and {@link Matrix3d}.
|
|
19370
|
+
* @example
|
|
19371
|
+
* // a scalar the body declares as `uniform float uStrength;`
|
|
19372
|
+
* fx.setUniform("uStrength", 0.5);
|
|
19373
|
+
* // a vec3 — an array, or anything with toArray()
|
|
19374
|
+
* fx.setUniform("uTint", [1.0, 0.82, 0.55]);
|
|
19375
|
+
* fx.setUniform("uOrigin", new me.Vector2d(0.5, 0.5));
|
|
19142
19376
|
*/
|
|
19143
19377
|
setUniform(name, value) {
|
|
19144
19378
|
if (this.destroyed === true) {
|
|
19145
19379
|
return;
|
|
19146
19380
|
}
|
|
19381
|
+
this._uniformValues?.set(name, value);
|
|
19147
19382
|
if (typeof this._shader !== "undefined") {
|
|
19148
19383
|
this._shader.setUniform(name, value);
|
|
19149
19384
|
} else if (typeof this.wgslRealization !== "undefined") {
|
|
@@ -19174,7 +19409,12 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19174
19409
|
* flow.setTime(me.timer.getTime() / 1000);
|
|
19175
19410
|
*/
|
|
19176
19411
|
setTime(seconds) {
|
|
19177
|
-
if (
|
|
19412
|
+
if (this.destroyed !== true) {
|
|
19413
|
+
this._uniformValues?.set("uTime", seconds);
|
|
19414
|
+
}
|
|
19415
|
+
if (typeof this._shader !== "undefined" && this.destroyed !== true && !this._shader.suspended && // optional: the map is null on a shader with no live context
|
|
19416
|
+
// behind it, which `suspended` alone does not cover
|
|
19417
|
+
typeof this._shader.uniforms?.uTime !== "undefined") {
|
|
19178
19418
|
this._shader.setUniform("uTime", seconds);
|
|
19179
19419
|
} else if (typeof this.wgslRealization !== "undefined" && this.destroyed !== true && this.wgslRealization.hasUniform("uTime")) {
|
|
19180
19420
|
this.wgslRealization.setUniform("uTime", seconds);
|
|
@@ -19862,6 +20102,8 @@ var Camera2d = class extends Renderable {
|
|
|
19862
20102
|
* @internal
|
|
19863
20103
|
*/
|
|
19864
20104
|
_zoom;
|
|
20105
|
+
/** backing store for {@link Camera2d#roll} — see the accessor */
|
|
20106
|
+
_roll;
|
|
19865
20107
|
/**
|
|
19866
20108
|
* the world-space projection matrix for non-default cameras (offset/zoomed).
|
|
19867
20109
|
* Maps world coordinates to the camera's screen viewport.
|
|
@@ -19954,6 +20196,7 @@ var Camera2d = class extends Renderable {
|
|
|
19954
20196
|
this.screenX = 0;
|
|
19955
20197
|
this.screenY = 0;
|
|
19956
20198
|
this.zoom = 1;
|
|
20199
|
+
this._roll = 0;
|
|
19957
20200
|
this.worldProjection = new Matrix3d();
|
|
19958
20201
|
this.screenProjection = new Matrix3d();
|
|
19959
20202
|
this._worldView = new Bounds();
|
|
@@ -20029,6 +20272,48 @@ var Camera2d = class extends Renderable {
|
|
|
20029
20272
|
* // zoom out to show the full level in a 180x100 minimap
|
|
20030
20273
|
* camera.zoom = Math.min(180 / levelWidth, 100 / levelHeight);
|
|
20031
20274
|
*/
|
|
20275
|
+
/**
|
|
20276
|
+
* Roll this camera in the screen plane, in radians — the 2D spelling of
|
|
20277
|
+
* {@link Camera3d#roll}, so a helper that banks "a camera" works on either.
|
|
20278
|
+
*
|
|
20279
|
+
* Absolute and idempotent: assigning the same angle twice leaves the view
|
|
20280
|
+
* where it was. It is a thin accessor over the camera's own
|
|
20281
|
+
* `currentTransform`, which is what {@link Renderable#rotate} writes and
|
|
20282
|
+
* what `worldToLocal` / `localToWorld` already undo — so picking under a
|
|
20283
|
+
* rolled 2D camera stays correct for free.
|
|
20284
|
+
*
|
|
20285
|
+
* A 2D camera has no pitch or yaw, so a screen-plane rotation is its only
|
|
20286
|
+
* rotation; on a `Camera3d` the roll is a real view angle instead, because
|
|
20287
|
+
* a 3D view is built from its three angles and never reads
|
|
20288
|
+
* `currentTransform`.
|
|
20289
|
+
*
|
|
20290
|
+
* This OWNS the camera's `currentTransform`: the setter rebuilds it rather
|
|
20291
|
+
* than composing onto it, so assigning every frame cannot drift and
|
|
20292
|
+
* `roll = 0` lands back on an exact identity. (A delta-based version does
|
|
20293
|
+
* neither — a banking camera would accumulate float error across thousands
|
|
20294
|
+
* of frames.) The cost is that a manual `rotate()` / `scale()` on the
|
|
20295
|
+
* camera itself is discarded by the next assignment; a camera has `zoom`
|
|
20296
|
+
* and `shake` for those jobs, and neither touches this matrix.
|
|
20297
|
+
* @default 0
|
|
20298
|
+
* @example
|
|
20299
|
+
* camera.roll = Math.PI / 32; // a slight dutch angle
|
|
20300
|
+
*/
|
|
20301
|
+
get roll() {
|
|
20302
|
+
return this._roll;
|
|
20303
|
+
}
|
|
20304
|
+
set roll(value) {
|
|
20305
|
+
if (value === this._roll) {
|
|
20306
|
+
return;
|
|
20307
|
+
}
|
|
20308
|
+
this._roll = value;
|
|
20309
|
+
this.currentTransform.identity();
|
|
20310
|
+
if (value !== 0) {
|
|
20311
|
+
this.currentTransform.rotate(value);
|
|
20312
|
+
}
|
|
20313
|
+
this.invCurrentTransform.copy(this.currentTransform).invert();
|
|
20314
|
+
this.updateBounds();
|
|
20315
|
+
this.isDirty = true;
|
|
20316
|
+
}
|
|
20032
20317
|
get zoom() {
|
|
20033
20318
|
return this._zoom;
|
|
20034
20319
|
}
|
|
@@ -20086,6 +20371,7 @@ var Camera2d = class extends Renderable {
|
|
|
20086
20371
|
this.smoothFollow = true;
|
|
20087
20372
|
this.damping = 1;
|
|
20088
20373
|
this.currentTransform.identity();
|
|
20374
|
+
this._roll = 0;
|
|
20089
20375
|
this.invCurrentTransform.identity().invert();
|
|
20090
20376
|
this.worldProjection.identity();
|
|
20091
20377
|
this.screenProjection.identity();
|
|
@@ -20558,7 +20844,16 @@ var Camera2d = class extends Renderable {
|
|
|
20558
20844
|
container.draw(r, this);
|
|
20559
20845
|
}
|
|
20560
20846
|
renderer.flushTransparent();
|
|
20847
|
+
const hasCameraEffects = this.cameraEffects.length > 0;
|
|
20848
|
+
if (hasCameraEffects) {
|
|
20849
|
+
renderer.setProjection(this.screenProjection);
|
|
20850
|
+
}
|
|
20561
20851
|
this.drawFX(renderer);
|
|
20852
|
+
if (hasCameraEffects) {
|
|
20853
|
+
renderer.setProjection(
|
|
20854
|
+
isNonDefault ? this.worldProjection : this.projectionMatrix
|
|
20855
|
+
);
|
|
20856
|
+
}
|
|
20562
20857
|
container.postDraw(r);
|
|
20563
20858
|
this.postDraw(r);
|
|
20564
20859
|
if (stage) {
|
|
@@ -21855,9 +22150,8 @@ var Stage = class {
|
|
|
21855
22150
|
}
|
|
21856
22151
|
}
|
|
21857
22152
|
/**
|
|
21858
|
-
*
|
|
21859
|
-
*
|
|
21860
|
-
* @internal
|
|
22153
|
+
* Update the stage. Override it to run your own per-frame logic, and call
|
|
22154
|
+
* `super.update(dt)` so the world and cameras still advance.
|
|
21861
22155
|
* @param dt - time since the last update in milliseconds.
|
|
21862
22156
|
* @returns true if the stage needs to be redrawn
|
|
21863
22157
|
*/
|
|
@@ -21876,8 +22170,9 @@ var Stage = class {
|
|
|
21876
22170
|
* Lights are rendered as part of the world tree (they're now first-class
|
|
21877
22171
|
* Renderables) and the ambient overlay pass runs inside each Camera's
|
|
21878
22172
|
* post-effect FBO bracket via {@link Stage#drawLighting}.
|
|
21879
|
-
*
|
|
21880
|
-
*
|
|
22173
|
+
*
|
|
22174
|
+
* Override it to draw under or over the world, calling `super.draw(...)`
|
|
22175
|
+
* where the world itself belongs in that order.
|
|
21881
22176
|
* @param renderer - the renderer object to draw with
|
|
21882
22177
|
* @param world - the world object to draw
|
|
21883
22178
|
*/
|
|
@@ -22051,6 +22346,13 @@ var DefaultLoadingScreen = class extends Stage {
|
|
|
22051
22346
|
* @internal
|
|
22052
22347
|
*/
|
|
22053
22348
|
#cleanedUp = false;
|
|
22349
|
+
/**
|
|
22350
|
+
* whether the preloader has reported done. Latched on LOADER_COMPLETE
|
|
22351
|
+
* and read by the logo's own load callback — NOT a teardown signal.
|
|
22352
|
+
* @ignore
|
|
22353
|
+
* @internal
|
|
22354
|
+
*/
|
|
22355
|
+
#loadingComplete = false;
|
|
22054
22356
|
/**
|
|
22055
22357
|
* Pin the loading screen to a Camera2d regardless of the
|
|
22056
22358
|
* application's `cameraClass` setting. The loader must render
|
|
@@ -22070,13 +22372,14 @@ var DefaultLoadingScreen = class extends Stage {
|
|
|
22070
22372
|
const barHeight = 8;
|
|
22071
22373
|
this.#app = app;
|
|
22072
22374
|
this.#cleanedUp = false;
|
|
22375
|
+
this.#loadingComplete = false;
|
|
22073
22376
|
app.world.backgroundColor.parseCSS("#202020");
|
|
22074
22377
|
const { width, height } = app.renderer;
|
|
22075
22378
|
this.progressBar = new ProgressBar(0, height / 2, width, barHeight);
|
|
22076
22379
|
app.world.addChild(this.progressBar, 1);
|
|
22077
|
-
once(LOADER_COMPLETE, this.#
|
|
22078
|
-
load2({ name: "melonjs_logo", type: "image", src: melonjs_logo_default }
|
|
22079
|
-
if (this.#cleanedUp) {
|
|
22380
|
+
once(LOADER_COMPLETE, this.#onLoaderComplete, this);
|
|
22381
|
+
load2({ name: "melonjs_logo", type: "image", src: melonjs_logo_default }).then(() => {
|
|
22382
|
+
if (this.#loadingComplete || this.#cleanedUp) {
|
|
22080
22383
|
return;
|
|
22081
22384
|
}
|
|
22082
22385
|
this.logoSprite = new Sprite(width / 2, height / 2, {
|
|
@@ -22085,8 +22388,18 @@ var DefaultLoadingScreen = class extends Stage {
|
|
|
22085
22388
|
frameheight: 256
|
|
22086
22389
|
});
|
|
22087
22390
|
app.world.addChild(this.logoSprite, 2);
|
|
22391
|
+
}).catch(() => {
|
|
22088
22392
|
});
|
|
22089
22393
|
}
|
|
22394
|
+
/**
|
|
22395
|
+
* The preloader is done. Latched so a late-arriving logo is not added
|
|
22396
|
+
* to a world that is about to be replaced; nothing is torn down here.
|
|
22397
|
+
* @ignore
|
|
22398
|
+
* @internal
|
|
22399
|
+
*/
|
|
22400
|
+
#onLoaderComplete() {
|
|
22401
|
+
this.#loadingComplete = true;
|
|
22402
|
+
}
|
|
22090
22403
|
/**
|
|
22091
22404
|
* Remove loading screen children and unload the logo
|
|
22092
22405
|
* @ignore
|
|
@@ -22114,8 +22427,8 @@ var DefaultLoadingScreen = class extends Stage {
|
|
|
22114
22427
|
* @internal
|
|
22115
22428
|
*/
|
|
22116
22429
|
onDestroyEvent() {
|
|
22117
|
-
if (!this.#
|
|
22118
|
-
off(LOADER_COMPLETE, this.#
|
|
22430
|
+
if (!this.#loadingComplete) {
|
|
22431
|
+
off(LOADER_COMPLETE, this.#onLoaderComplete, this);
|
|
22119
22432
|
}
|
|
22120
22433
|
this.#cleanup();
|
|
22121
22434
|
}
|
|
@@ -23080,6 +23393,9 @@ function preloadFontFace(data2, onload2, onerror) {
|
|
|
23080
23393
|
}
|
|
23081
23394
|
|
|
23082
23395
|
// src/lighting/light3d.ts
|
|
23396
|
+
function readVector(out, value) {
|
|
23397
|
+
return Array.isArray(value) ? out.set(value[0], value[1], value[2]) : out.set(value.x, value.y, value.z);
|
|
23398
|
+
}
|
|
23083
23399
|
var Light3d = class extends Renderable {
|
|
23084
23400
|
/** `"directional"`, `"ambient"`, `"point"` or `"spot"`. */
|
|
23085
23401
|
type;
|
|
@@ -23111,20 +23427,12 @@ var Light3d = class extends Renderable {
|
|
|
23111
23427
|
this.type = options.type ?? "directional";
|
|
23112
23428
|
this.direction = new Vector3d(0, 1, 0);
|
|
23113
23429
|
if (options.direction) {
|
|
23114
|
-
this.direction.
|
|
23115
|
-
options.direction[0],
|
|
23116
|
-
options.direction[1],
|
|
23117
|
-
options.direction[2]
|
|
23118
|
-
);
|
|
23430
|
+
readVector(this.direction, options.direction);
|
|
23119
23431
|
}
|
|
23120
23432
|
this.direction.normalize();
|
|
23121
23433
|
this.position = new Vector3d(0, 0, 0);
|
|
23122
23434
|
if (options.position) {
|
|
23123
|
-
this.position.
|
|
23124
|
-
options.position[0],
|
|
23125
|
-
options.position[1],
|
|
23126
|
-
options.position[2]
|
|
23127
|
-
);
|
|
23435
|
+
readVector(this.position, options.position);
|
|
23128
23436
|
}
|
|
23129
23437
|
if (options.color instanceof Color) {
|
|
23130
23438
|
this.color = options.color;
|
|
@@ -23505,6 +23813,7 @@ var Frustum = class {
|
|
|
23505
23813
|
// src/camera/camera3d.ts
|
|
23506
23814
|
var AXIS_X = new Vector3d(1, 0, 0);
|
|
23507
23815
|
var AXIS_Y = new Vector3d(0, 1, 0);
|
|
23816
|
+
var AXIS_Z = new Vector3d(0, 0, 1);
|
|
23508
23817
|
var _viewMatrix = new Matrix3d();
|
|
23509
23818
|
var _viewProjection = new Matrix3d();
|
|
23510
23819
|
var _wsPoint = new Vector3d();
|
|
@@ -23610,6 +23919,29 @@ var Camera3d = class extends Camera2d {
|
|
|
23610
23919
|
* @default 0
|
|
23611
23920
|
*/
|
|
23612
23921
|
yaw;
|
|
23922
|
+
/**
|
|
23923
|
+
* Z-axis rotation in radians (bank the horizon). Positive values
|
|
23924
|
+
* roll the camera clockwise, so the world tilts anticlockwise —
|
|
23925
|
+
* the view from a cockpit banking right.
|
|
23926
|
+
*
|
|
23927
|
+
* Completes the `pitch` / `yaw` / `roll` trio. Note this is NOT the
|
|
23928
|
+
* inherited {@link Renderable#rotation}: a 3D camera builds its view
|
|
23929
|
+
* from these three angles and never reads `currentTransform`, which
|
|
23930
|
+
* is why `camera.rotate()` on a `Camera3d` is silently inert. A
|
|
23931
|
+
* `Camera2d` is the other way round — it has no `roll` because a
|
|
23932
|
+
* screen-plane rotation IS its only rotation, and `rotate()` already
|
|
23933
|
+
* does it through `currentTransform`.
|
|
23934
|
+
* @default 0
|
|
23935
|
+
* @example
|
|
23936
|
+
* // bank with the player's steering, as a flight game would
|
|
23937
|
+
* camera.roll = (player.pos.x / PLAY_BOUND_X) * MAX_BANK;
|
|
23938
|
+
*/
|
|
23939
|
+
get roll() {
|
|
23940
|
+
return this._roll;
|
|
23941
|
+
}
|
|
23942
|
+
set roll(value) {
|
|
23943
|
+
this._roll = value;
|
|
23944
|
+
}
|
|
23613
23945
|
/**
|
|
23614
23946
|
* World-space offset from the followed target. When `target` is
|
|
23615
23947
|
* set via {@link Camera2d#follow}, the camera position resolves to
|
|
@@ -23650,6 +23982,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23650
23982
|
});
|
|
23651
23983
|
this.pitch = 0;
|
|
23652
23984
|
this.yaw = 0;
|
|
23985
|
+
this.roll = 0;
|
|
23653
23986
|
this.followOffset = new Vector3d(0, 0, 0);
|
|
23654
23987
|
this.lookAhead = new Vector3d(0, 0, 1);
|
|
23655
23988
|
this.near = this.frustum.near;
|
|
@@ -23925,6 +24258,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23925
24258
|
_basis.identity();
|
|
23926
24259
|
_basis.rotate(this.yaw, AXIS_Y);
|
|
23927
24260
|
_basis.rotate(this.pitch, AXIS_X);
|
|
24261
|
+
_basis.rotate(this.roll, AXIS_Z);
|
|
23928
24262
|
const b = _basis.val;
|
|
23929
24263
|
state4.heightAxis[0] = k * b[1];
|
|
23930
24264
|
state4.heightAxis[1] = k * b[5];
|
|
@@ -23952,6 +24286,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23952
24286
|
_basis.identity();
|
|
23953
24287
|
_basis.rotate(this.yaw, AXIS_Y);
|
|
23954
24288
|
_basis.rotate(this.pitch, AXIS_X);
|
|
24289
|
+
_basis.rotate(this.roll, AXIS_Z);
|
|
23955
24290
|
const v = _basis.val;
|
|
23956
24291
|
right.set(v[0], v[1], v[2]);
|
|
23957
24292
|
up.set(v[4], v[5], v[6]);
|
|
@@ -24050,6 +24385,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24050
24385
|
* @internal
|
|
24051
24386
|
*/
|
|
24052
24387
|
_applyContainerViewTransform(container, translateX, translateY) {
|
|
24388
|
+
if (this.roll !== 0) {
|
|
24389
|
+
container.rotate(-this.roll, AXIS_Z);
|
|
24390
|
+
}
|
|
24053
24391
|
if (this.pitch !== 0) {
|
|
24054
24392
|
container.rotate(-this.pitch, AXIS_X);
|
|
24055
24393
|
}
|
|
@@ -24076,6 +24414,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24076
24414
|
if (this.pitch !== 0) {
|
|
24077
24415
|
container.rotate(this.pitch, AXIS_X);
|
|
24078
24416
|
}
|
|
24417
|
+
if (this.roll !== 0) {
|
|
24418
|
+
container.rotate(this.roll, AXIS_Z);
|
|
24419
|
+
}
|
|
24079
24420
|
}
|
|
24080
24421
|
/**
|
|
24081
24422
|
* Point the camera at a world-space target by deriving pitch and
|
|
@@ -24276,6 +24617,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24276
24617
|
*/
|
|
24277
24618
|
worldToScreen(world, out = new Vector2d()) {
|
|
24278
24619
|
_viewMatrix.identity();
|
|
24620
|
+
if (this.roll !== 0) {
|
|
24621
|
+
_viewMatrix.rotate(-this.roll, AXIS_Z);
|
|
24622
|
+
}
|
|
24279
24623
|
if (this.pitch !== 0) {
|
|
24280
24624
|
_viewMatrix.rotate(-this.pitch, AXIS_X);
|
|
24281
24625
|
}
|
|
@@ -24308,6 +24652,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24308
24652
|
*/
|
|
24309
24653
|
_rebuildFrustumPlanes() {
|
|
24310
24654
|
_viewMatrix.identity();
|
|
24655
|
+
if (this.roll !== 0) {
|
|
24656
|
+
_viewMatrix.rotate(-this.roll, AXIS_Z);
|
|
24657
|
+
}
|
|
24311
24658
|
if (this.pitch !== 0) {
|
|
24312
24659
|
_viewMatrix.rotate(-this.pitch, AXIS_X);
|
|
24313
24660
|
}
|
|
@@ -24614,38 +24961,44 @@ function buildTextureGroups(groups, materials, shared, framewidth, frameheight)
|
|
|
24614
24961
|
}
|
|
24615
24962
|
return slices.length === 1 && slices[0].texture !== shared ? slices : void 0;
|
|
24616
24963
|
}
|
|
24964
|
+
function declaredSize(settings) {
|
|
24965
|
+
const width = settings.width;
|
|
24966
|
+
const height = settings.height;
|
|
24967
|
+
if (settings.normalize !== false || typeof settings.scale !== "number" || width > 0 && height > 0) {
|
|
24968
|
+
return [width, height];
|
|
24969
|
+
}
|
|
24970
|
+
const vertices = typeof settings.model === "string" ? getOBJ(settings.model)?.vertices : settings.vertices;
|
|
24971
|
+
if (!vertices || vertices.length < 3) {
|
|
24972
|
+
return [width, height];
|
|
24973
|
+
}
|
|
24974
|
+
let minX = Infinity;
|
|
24975
|
+
let maxX = -Infinity;
|
|
24976
|
+
let minY = Infinity;
|
|
24977
|
+
let maxY = -Infinity;
|
|
24978
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
24979
|
+
if (vertices[i] < minX) {
|
|
24980
|
+
minX = vertices[i];
|
|
24981
|
+
}
|
|
24982
|
+
if (vertices[i] > maxX) {
|
|
24983
|
+
maxX = vertices[i];
|
|
24984
|
+
}
|
|
24985
|
+
if (vertices[i + 1] < minY) {
|
|
24986
|
+
minY = vertices[i + 1];
|
|
24987
|
+
}
|
|
24988
|
+
if (vertices[i + 1] > maxY) {
|
|
24989
|
+
maxY = vertices[i + 1];
|
|
24990
|
+
}
|
|
24991
|
+
}
|
|
24992
|
+
return [
|
|
24993
|
+
width > 0 ? width : (maxX - minX) * settings.scale,
|
|
24994
|
+
height > 0 ? height : (maxY - minY) * settings.scale
|
|
24995
|
+
];
|
|
24996
|
+
}
|
|
24617
24997
|
var Mesh = class _Mesh extends Renderable {
|
|
24618
24998
|
/**
|
|
24619
24999
|
* @param {number} x - the x screen position of the mesh object
|
|
24620
25000
|
* @param {number} y - the y screen position of the mesh object
|
|
24621
|
-
* @param {
|
|
24622
|
-
* @param {string} [settings.model] - name of a preloaded OBJ model (via loader.preload with type "obj"). Vertex normals come with it — authored `vn` when the file has them, generated from face geometry when it does not — so an OBJ model can be `lit`.
|
|
24623
|
-
* @param {Float32Array|number[]} [settings.vertices] - vertex positions as x,y,z triplets (alternative to settings.model)
|
|
24624
|
-
* @param {Float32Array|number[]} [settings.uvs] - texture coordinates as u,v pairs (alternative to settings.model)
|
|
24625
|
-
* @param {Uint16Array|number[]} [settings.indices] - triangle vertex indices (alternative to settings.model)
|
|
24626
|
-
* @param {HTMLImageElement|TextureAtlas|string} [settings.texture] - the texture to apply (image name, HTMLImageElement, or TextureAtlas). If omitted and settings.material is provided, the texture is resolved from the MTL material's map_Kd. Passing this pins ONE binding over the whole model, which on a multi-material model suppresses the per-material texture split — see {@link Mesh#textureGroups}.
|
|
24627
|
-
* @param {string} [settings.material] - name of a preloaded MTL material (via loader.preload with type "mtl"). When provided, the diffuse texture (map_Kd), tint color (Kd), and opacity (d) are automatically applied. On a multi-material model each material's own `map_Kd` is bound for its own slice of the geometry (#1573) and each `Kd` is baked per-vertex, so one `Mesh` renders the whole model.
|
|
24628
|
-
* @param {number} settings.width - display width in pixels. With normalization on (the default) the model is scaled to fit this size; with `normalize: false` this is the uniform pixels-per-unit scale applied to the raw geometry.
|
|
24629
|
-
* @param {number} [settings.height] - display height in pixels (normalized models only; ignored when `normalize: false`)
|
|
24630
|
-
* @param {boolean} [settings.cullBackFaces=true] - enable backface culling
|
|
24631
|
-
* @param {boolean} [settings.normalize=true] - fit the source geometry into a `[-0.5, 0.5]` unit cube before scaling, so `width`/`height` behave like a Sprite. Set `false` to keep the geometry's real-world coordinates — required when several meshes share one coordinate space (e.g. nodes of an imported glTF scene) so their relative scale and layout are preserved.
|
|
24632
|
-
* @param {number} [settings.scale] - world-space scale (pixels per source unit) for the Camera3d path; defaults to `width`. Set this when `width`/`height` describe the renderable's world bounds (frustum culling) rather than the geometry scale — see {@link Mesh#meshScale}.
|
|
24633
|
-
* @param {boolean} [settings.rightHanded=false] - treat the source as right-handed (Y-up, e.g. glTF) under the `Camera3d` world path. The default Y-up→Y-down bridge negates Y only (a reflection, which mirrors the scene left/right); `true` negates Y **and** Z (a rotation) so chirality is preserved and the result matches the authoring tool. See {@link Mesh#rightHanded}.
|
|
24634
|
-
* @param {string} [settings.textureRepeat] - texture wrap mode (`"repeat"` / `"repeat-x"` / `"repeat-y"` / `"no-repeat"`) this mesh samples its texture with (per-mesh — it does not modify the shared texture, so other meshes/sprites using the same image are unaffected). Use `"repeat"` when the geometry's UVs fall outside the `[0, 1]` range and rely on the texture tiling (e.g. glTF assets, whose default sampler wrap is REPEAT) — otherwise the texture clamps to its edge texels and looks flat. Ignored for the white-pixel fallback. Note: REPEAT on a non-power-of-two texture requires WebGL 2.
|
|
24635
|
-
* @param {string} [settings.textureFilter] - texture magnification filter (`"nearest"` for crisp pixel-art upscaling, `"linear"` for smooth) applied to the resolved texture. Omit to keep the renderer's global `antiAlias` default. On the mesh path, linear filtering also samples a generated mip chain with trilinear minification and 4× anisotropy (distant geometry stops shimmering) — `"nearest"` opts out, keeping crisp pixel-art models on hard level-0 sampling. GPU backends only (ignored by the Canvas renderer).
|
|
24636
|
-
* @param {number} [settings.alphaCutoff=0] - alpha cutout threshold. Fragments whose final alpha is below this value are discarded (hard-edged cutout — foliage, fences, decals — with no blending or sorting). `0` disables the cutout. Set automatically by the glTF loader from a material's `alphaMode: "MASK"`. GPU mesh path only (WebGL and WebGPU; the Canvas renderer ignores it).
|
|
24637
|
-
* @param {number[]|Float32Array} [settings.emissive] - emissive (self-illumination) color `[r, g, b]` (0..1, may exceed 1 for HDR glow) added on top of the lit/unlit color so the surface glows regardless of scene lights (neon, lava, screens). Omit / all-zero for no emission. Set automatically by the glTF loader (`emissiveFactor`) and OBJ loader (MTL `Ke`). GPU mesh path only (WebGL and WebGPU; the Canvas renderer ignores it).
|
|
24638
|
-
* @param {boolean} [settings.lit=false] - shade this mesh with the scene's {@link Light3d} lights (the lit mesh pipeline) instead of rendering fullbright. Set automatically by the glTF importer when the scene carries a directional, point or spot light. With `lit` on and no lights present the batcher uploads a white ambient, so the result is indistinguishable from unlit.
|
|
24639
|
-
* @param {Uint32Array|Color[]|number[]} [settings.vertexColors] - per-vertex colour, one entry per vertex, multiplied into {@link Mesh#tint}. Either packed RGBA8 (`Uint32Array`, the form the batchers read — no conversion) or one {@link Color} per vertex. Omit for plain white. Lets a single mesh carry a gradient — fading a terrain toward the sky with distance, darkening a crease — which a per-object `tint` cannot express. An explicit value wins over the colours a multi-material OBJ bakes from its MTL.
|
|
24640
|
-
* @param {number[]|Float32Array} [settings.normals] - per-vertex normals for the lit path. An explicit value wins over the ones an OBJ or glTF source supplies; omit it and they are taken from the model, or generated from the geometry when the mesh is `lit`. Generated normals average per vertex where faces share vertices (smooth shading) and equal the face normal where they do not (flat shading) — the geometry decides, not a flag.
|
|
24641
|
-
* @param {number[]|Float32Array} [settings.specular] - specular color `[r, g, b]` (0..1) for the lit path. Set by the OBJ loader from MTL `Ks`, and derived from glTF metallic/roughness.
|
|
24642
|
-
* @param {number} [settings.shininess=0] - specular exponent for the lit path (MTL `Ns`). `0` for a fully diffuse surface.
|
|
24643
|
-
* @param {string|TextureAtlas|HTMLImageElement} [settings.alphaMap] - per-texel opacity map, sampled in addition to the diffuse texture (MTL `map_d`).
|
|
24644
|
-
* @param {boolean} [settings.castGroundShadow] - give this mesh a blob ground shadow, overriding the application's `castGroundShadow` setting in both directions. Omit to inherit. Needs a GPU backend and a `Camera3d`.
|
|
24645
|
-
* @param {boolean} [settings.transparent] - draw in the transparent pass (blended, back-to-front, no depth write). Omit and a mesh goes transparent whenever its draw alpha is fractional; `true` for soft-alpha textures; `false` to stay opaque however faded
|
|
24646
|
-
* @param {boolean} [settings.fog] - set `false` to exempt this mesh from the camera's distance fog ({@link Camera3d#setFog}); omit to fog whenever the camera does
|
|
24647
|
-
* @param {number} [settings.shadowGroundY] - world Y of the floor the shadow lands on. Omit and the blob sits at the object's own base at full strength; set it and the blob shrinks and fades as the object rises. Render space is Y-down, so the floor is a **greater** Y than the object above it.
|
|
24648
|
-
* @param {number} [settings.shadowOpacity=0.45] - opacity of the shadow directly beneath the object, before any height fade.
|
|
25001
|
+
* @param {MeshSettings} settings - Configuration parameters for the Mesh object
|
|
24649
25002
|
* @example
|
|
24650
25003
|
* // create from OBJ + MTL (texture auto-resolved from material)
|
|
24651
25004
|
* let mesh = new me.Mesh(0, 0, {
|
|
@@ -24692,7 +25045,7 @@ var Mesh = class _Mesh extends Renderable {
|
|
|
24692
25045
|
* mesh.rotate(Math.PI / 4);
|
|
24693
25046
|
*/
|
|
24694
25047
|
constructor(x, y, settings) {
|
|
24695
|
-
super(x, y, settings
|
|
25048
|
+
super(x, y, ...declaredSize(settings));
|
|
24696
25049
|
let objGroups = null;
|
|
24697
25050
|
let sourceNormals = settings.normals;
|
|
24698
25051
|
if (typeof settings.model === "string") {
|
|
@@ -25175,6 +25528,66 @@ var Mesh = class _Mesh extends Renderable {
|
|
|
25175
25528
|
renderer?.deleteMeshGeometry?.(this);
|
|
25176
25529
|
super.onDeactivateEvent(...args);
|
|
25177
25530
|
}
|
|
25531
|
+
/**
|
|
25532
|
+
* The centre of this mesh's own geometry, in MODEL space, written into
|
|
25533
|
+
* `_lcx` / `_lcy` / `_lcz`.
|
|
25534
|
+
*
|
|
25535
|
+
* The transparent pass needs a point that represents where a mesh
|
|
25536
|
+
* actually is. Its origin does not: a mesh whose vertices run away from
|
|
25537
|
+
* the origin — a ground plane authored from one corner, a river that
|
|
25538
|
+
* trails behind the boat it is parented to — sits nowhere near it. Keyed
|
|
25539
|
+
* on that origin, such a mesh sorts by an authoring choice rather than by
|
|
25540
|
+
* its position.
|
|
25541
|
+
*
|
|
25542
|
+
* Cached against `_geometryVersion`, so the vertex walk happens once per
|
|
25543
|
+
* geometry edit rather than once per queued draw. An origin-centred mesh
|
|
25544
|
+
* — which is most of them — resolves to (0, 0, 0) and keys exactly as it
|
|
25545
|
+
* did before.
|
|
25546
|
+
* @ignore
|
|
25547
|
+
* @internal
|
|
25548
|
+
*/
|
|
25549
|
+
_updateLocalCenter() {
|
|
25550
|
+
if (this._lcVersion === this._geometryVersion) {
|
|
25551
|
+
return;
|
|
25552
|
+
}
|
|
25553
|
+
const v = this.originalVertices;
|
|
25554
|
+
const n = this.vertexCount;
|
|
25555
|
+
let minX = Infinity;
|
|
25556
|
+
let minY = Infinity;
|
|
25557
|
+
let minZ = Infinity;
|
|
25558
|
+
let maxX = -Infinity;
|
|
25559
|
+
let maxY = -Infinity;
|
|
25560
|
+
let maxZ = -Infinity;
|
|
25561
|
+
if (v !== void 0 && n > 0) {
|
|
25562
|
+
for (let i = 0; i < n; i++) {
|
|
25563
|
+
const x = v[i * 3];
|
|
25564
|
+
const y = v[i * 3 + 1];
|
|
25565
|
+
const z = v[i * 3 + 2];
|
|
25566
|
+
if (x < minX) {
|
|
25567
|
+
minX = x;
|
|
25568
|
+
}
|
|
25569
|
+
if (x > maxX) {
|
|
25570
|
+
maxX = x;
|
|
25571
|
+
}
|
|
25572
|
+
if (y < minY) {
|
|
25573
|
+
minY = y;
|
|
25574
|
+
}
|
|
25575
|
+
if (y > maxY) {
|
|
25576
|
+
maxY = y;
|
|
25577
|
+
}
|
|
25578
|
+
if (z < minZ) {
|
|
25579
|
+
minZ = z;
|
|
25580
|
+
}
|
|
25581
|
+
if (z > maxZ) {
|
|
25582
|
+
maxZ = z;
|
|
25583
|
+
}
|
|
25584
|
+
}
|
|
25585
|
+
}
|
|
25586
|
+
this._lcx = Number.isFinite(minX) ? (minX + maxX) * 0.5 : 0;
|
|
25587
|
+
this._lcy = Number.isFinite(minY) ? (minY + maxY) * 0.5 : 0;
|
|
25588
|
+
this._lcz = Number.isFinite(minZ) ? (minZ + maxZ) * 0.5 : 0;
|
|
25589
|
+
this._lcVersion = this._geometryVersion;
|
|
25590
|
+
}
|
|
25178
25591
|
/**
|
|
25179
25592
|
* The mesh's world-space 3D axis-aligned bounding box. This is the 3D analog
|
|
25180
25593
|
* of {@link Renderable#getBounds} (which returns a flat 2D box from
|
|
@@ -25587,10 +26000,7 @@ var InstancedMesh = class extends Mesh {
|
|
|
25587
26000
|
/**
|
|
25588
26001
|
* @param {number} x - the x coordinate of the group origin
|
|
25589
26002
|
* @param {number} y - the y coordinate of the group origin
|
|
25590
|
-
* @param {
|
|
25591
|
-
* @param {number} [settings.instanceCount=0] - number of instances to pre-allocate. Instances start at the group origin (identity transform) until placed; `addInstance` grows past this.
|
|
25592
|
-
* @param {boolean} [settings.instanceColors=false] - give each instance its own colour (16 bytes per instance), multiplied into the mesh tint
|
|
25593
|
-
* @param {boolean} [settings.instanceData=false] - give each instance an opaque `vec4` (16 bytes per instance). Read as emissive by the built-in lit shading, or as anything at all by a custom mesh shader.
|
|
26003
|
+
* @param {InstancedMeshSettings} settings - every {@link Mesh} setting, plus the instancing ones
|
|
25594
26004
|
*/
|
|
25595
26005
|
constructor(x, y, settings) {
|
|
25596
26006
|
super(x, y, settings);
|
|
@@ -26652,9 +27062,16 @@ var Container = class _Container extends Renderable {
|
|
|
26652
27062
|
}
|
|
26653
27063
|
/**
|
|
26654
27064
|
* Automatically set the specified property of all childs to the given value
|
|
27065
|
+
*
|
|
27066
|
+
* This ASSIGNS to the children — after it, their own property carries the
|
|
27067
|
+
* value. To fade a subtree without touching what the children are, set
|
|
27068
|
+
* {@link Renderable#alpha} on the container instead: opacity cascades at
|
|
27069
|
+
* draw time and leaves every child's own value alone.
|
|
26655
27070
|
* @param {string} prop - property name
|
|
26656
|
-
* @param {
|
|
27071
|
+
* @param {*} value - property value; whatever the named property holds, so a
|
|
27072
|
+
* flag is a boolean and a colour is a string, not only an object
|
|
26657
27073
|
* @param {boolean} [recursive=false] - recursively apply the value to child containers if true
|
|
27074
|
+
* @see Renderable#alpha
|
|
26658
27075
|
*/
|
|
26659
27076
|
setChildsProperty(prop, value, recursive) {
|
|
26660
27077
|
this.forEach((child) => {
|
|
@@ -27053,6 +27470,13 @@ function linearToSrgb8(c) {
|
|
|
27053
27470
|
|
|
27054
27471
|
// src/level/gltf/GLTFModel.js
|
|
27055
27472
|
var IDENTITY16 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
27473
|
+
var _rootScratch = new Array(16);
|
|
27474
|
+
function partAbsolutePosition() {
|
|
27475
|
+
if (this._absPos === void 0) {
|
|
27476
|
+
this._absPos = vector3dPool.get();
|
|
27477
|
+
}
|
|
27478
|
+
return this._absPos.set(this.pos.x, this.pos.y, this.depth);
|
|
27479
|
+
}
|
|
27056
27480
|
var _t = [0, 0, 0];
|
|
27057
27481
|
var _r = [0, 0, 0, 1];
|
|
27058
27482
|
var _s = [1, 1, 1];
|
|
@@ -27060,7 +27484,7 @@ var _val = [0, 0, 0, 0];
|
|
|
27060
27484
|
var _localScratch = new Array(16);
|
|
27061
27485
|
var GLTFModel = class extends Container {
|
|
27062
27486
|
/**
|
|
27063
|
-
* @param {
|
|
27487
|
+
* @param {import("../../loader/loader.js").GLTFData} data - the parsed glTF descriptor, as returned by {@link loader.getGLTF}
|
|
27064
27488
|
* @param {object} [options]
|
|
27065
27489
|
* @param {number} [options.scale=1] - pixels per glTF unit (uniform scene scale)
|
|
27066
27490
|
* @param {boolean} [options.rightHanded=true] - glTF Y-up → engine Y-down via a rotation (no mirror)
|
|
@@ -27073,6 +27497,7 @@ var GLTFModel = class extends Container {
|
|
|
27073
27497
|
this.scale = options.scale ?? 1;
|
|
27074
27498
|
this._zSign = options.rightHanded !== false ? -1 : 1;
|
|
27075
27499
|
this.autoDepth = false;
|
|
27500
|
+
this.autoTransform = false;
|
|
27076
27501
|
this.applyAnchorTransform = false;
|
|
27077
27502
|
this._nodes = data2.graph.nodes;
|
|
27078
27503
|
this._roots = data2.graph.roots;
|
|
@@ -27085,6 +27510,9 @@ var GLTFModel = class extends Container {
|
|
|
27085
27510
|
const dz = b.max[2] - b.min[2];
|
|
27086
27511
|
const radius = Math.hypot(dx, dy, dz) / 2 * this.scale;
|
|
27087
27512
|
const boxSize = Math.max(radius, 1) * Math.SQRT2;
|
|
27513
|
+
this._extent = Math.max(radius, 1);
|
|
27514
|
+
this._extentX = (b.min[0] + b.max[0]) / 2 * this.scale;
|
|
27515
|
+
this._extentY = -((b.min[1] + b.max[1]) / 2) * this.scale;
|
|
27088
27516
|
const lit = options.lit === true;
|
|
27089
27517
|
const rightHanded = options.rightHanded !== false;
|
|
27090
27518
|
const castGroundShadow = typeof options.castGroundShadow === "boolean" ? options.castGroundShadow : void 0;
|
|
@@ -27142,6 +27570,7 @@ var GLTFModel = class extends Container {
|
|
|
27142
27570
|
mesh.vertexColors = prim.colors;
|
|
27143
27571
|
}
|
|
27144
27572
|
mesh.name = node.name;
|
|
27573
|
+
mesh.getAbsolutePosition = partAbsolutePosition;
|
|
27145
27574
|
(this._meshByNode[idx] ??= []).push(mesh);
|
|
27146
27575
|
this.addChild(mesh);
|
|
27147
27576
|
}
|
|
@@ -27192,7 +27621,7 @@ var GLTFModel = class extends Container {
|
|
|
27192
27621
|
* another clip when this one ends, a `function` legacy completion callback
|
|
27193
27622
|
* (return `false` to hold the final pose), or an options object.
|
|
27194
27623
|
* @param {string} name - animation clip id (see {@link GLTFModel#getAnimationNames})
|
|
27195
|
-
* @param {
|
|
27624
|
+
* @param {AnimationOptionsInput} [options] - loop / chain / completion behavior
|
|
27196
27625
|
* @param {boolean} [preserveTime=false] - keep the current playback time instead of restarting at 0
|
|
27197
27626
|
* @returns {GLTFModel} this, for chaining
|
|
27198
27627
|
* @example
|
|
@@ -27259,7 +27688,7 @@ var GLTFModel = class extends Container {
|
|
|
27259
27688
|
* (and start) it, or with no argument to resume after {@link GLTFModel#pause}.
|
|
27260
27689
|
* Always clears the paused state.
|
|
27261
27690
|
* @param {string} [name] - clip id to play; omit to just resume
|
|
27262
|
-
* @param {
|
|
27691
|
+
* @param {AnimationOptionsInput} [options] - loop / chain / completion behavior (see {@link GLTFModel#setCurrentAnimation})
|
|
27263
27692
|
* @returns {GLTFModel} this, for chaining
|
|
27264
27693
|
* @example
|
|
27265
27694
|
* model.play("walk"); // switch to + play "walk"
|
|
@@ -27362,6 +27791,52 @@ var GLTFModel = class extends Container {
|
|
|
27362
27791
|
_isLive() {
|
|
27363
27792
|
return this.pos !== void 0;
|
|
27364
27793
|
}
|
|
27794
|
+
/**
|
|
27795
|
+
* {@link Container#draw} translates the renderer by `pos` so children draw
|
|
27796
|
+
* in container-local space. This rig's children are already in world space,
|
|
27797
|
+
* so that translation is undone here rather than doubling the placement.
|
|
27798
|
+
* Paired with `autoTransform = false`, which suppresses the matching
|
|
27799
|
+
* rotation fold.
|
|
27800
|
+
* @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
|
|
27801
|
+
* @param {Camera2d} [viewport] - the camera rendering this frame
|
|
27802
|
+
*/
|
|
27803
|
+
draw(renderer, viewport) {
|
|
27804
|
+
renderer.translate(-this.pos.x, -this.pos.y);
|
|
27805
|
+
super.draw(renderer, viewport);
|
|
27806
|
+
}
|
|
27807
|
+
/**
|
|
27808
|
+
* Where this model is, as a 2D box — what culling, picking and the physics
|
|
27809
|
+
* broadphase all read.
|
|
27810
|
+
*
|
|
27811
|
+
* A `Container` has no dimensions of its own, and one left that way reports
|
|
27812
|
+
* an EMPTY bounds: a model claiming to be nowhere, which the broadphase
|
|
27813
|
+
* cannot place. A rig's size is known at load from the glTF scene AABB, so
|
|
27814
|
+
* this is a placement of a measurement taken once in the constructor rather
|
|
27815
|
+
* than a walk of the parts — `Container#updateBounds` with
|
|
27816
|
+
* `enableChildBoundsUpdate` would re-measure the whole rig on every call,
|
|
27817
|
+
* and that flag is for a group whose extent really is its members' union.
|
|
27818
|
+
*
|
|
27819
|
+
* The box is the model's bounding SPHERE squared off, so a turned model
|
|
27820
|
+
* still reports something that contains it. Generous bounds only cost
|
|
27821
|
+
* broadphase pruning; tight ones lose contacts.
|
|
27822
|
+
* @param {boolean} [absolute=true] - in world rather than local coordinates
|
|
27823
|
+
* @returns {Bounds} this model's bounding box
|
|
27824
|
+
*/
|
|
27825
|
+
updateBounds(absolute = true) {
|
|
27826
|
+
const bounds = this.getBounds();
|
|
27827
|
+
const e = this._extent;
|
|
27828
|
+
bounds.setMinMax(
|
|
27829
|
+
this._extentX - e,
|
|
27830
|
+
this._extentY - e,
|
|
27831
|
+
this._extentX + e,
|
|
27832
|
+
this._extentY + e
|
|
27833
|
+
);
|
|
27834
|
+
if (absolute === true) {
|
|
27835
|
+
const absPos = this.getAbsolutePosition();
|
|
27836
|
+
bounds.translate(absPos.x, absPos.y);
|
|
27837
|
+
}
|
|
27838
|
+
return bounds;
|
|
27839
|
+
}
|
|
27365
27840
|
/**
|
|
27366
27841
|
* @ignore
|
|
27367
27842
|
* @internal
|
|
@@ -27369,9 +27844,47 @@ var GLTFModel = class extends Container {
|
|
|
27369
27844
|
_pose() {
|
|
27370
27845
|
const clip = this.current.name ? this.anim[this.current.name] : null;
|
|
27371
27846
|
const t = this.current.time;
|
|
27847
|
+
const parent = this._rootMatrix();
|
|
27372
27848
|
for (const root of this._roots) {
|
|
27373
|
-
this._visit(root,
|
|
27849
|
+
this._visit(root, parent, clip, t);
|
|
27850
|
+
}
|
|
27851
|
+
}
|
|
27852
|
+
/**
|
|
27853
|
+
* The model's own placement, as the parent transform every root node hangs
|
|
27854
|
+
* from — so `pos`, `depth` and `currentTransform` move, turn and scale the
|
|
27855
|
+
* whole rig the way they do for any other renderable.
|
|
27856
|
+
*
|
|
27857
|
+
* It has to be handed to the DFS in **glTF space**, because that is where
|
|
27858
|
+
* the node matrices live and `_applyWorldToMesh` only bridges to engine
|
|
27859
|
+
* space afterwards. The bridge is `D = diag(1, -1, zSign)` and a uniform
|
|
27860
|
+
* `scale`, so the inverse trip is a division by `scale` for the translation
|
|
27861
|
+
* and a conjugation `D · R · D` for the rotation/scale — `D` is its own
|
|
27862
|
+
* inverse, which is what makes the conjugation this cheap.
|
|
27863
|
+
* @returns {number[]} 16-element column-major matrix
|
|
27864
|
+
* @ignore
|
|
27865
|
+
* @internal
|
|
27866
|
+
*/
|
|
27867
|
+
_rootMatrix() {
|
|
27868
|
+
const { x, y, z } = this.pos;
|
|
27869
|
+
const t = this.currentTransform.val;
|
|
27870
|
+
const s = this.scale;
|
|
27871
|
+
if (x === 0 && y === 0 && z === 0 && this.currentTransform.isIdentity()) {
|
|
27872
|
+
return IDENTITY16;
|
|
27873
|
+
}
|
|
27874
|
+
const out = _rootScratch;
|
|
27875
|
+
const zs = this._zSign;
|
|
27876
|
+
for (let col = 0; col < 3; col++) {
|
|
27877
|
+
const dc = col === 0 ? 1 : col === 1 ? -1 : zs;
|
|
27878
|
+
out[col * 4] = t[col * 4] * dc;
|
|
27879
|
+
out[col * 4 + 1] = -t[col * 4 + 1] * dc;
|
|
27880
|
+
out[col * 4 + 2] = zs * t[col * 4 + 2] * dc;
|
|
27881
|
+
out[col * 4 + 3] = 0;
|
|
27374
27882
|
}
|
|
27883
|
+
out[12] = x / s;
|
|
27884
|
+
out[13] = -y / s;
|
|
27885
|
+
out[14] = zs * z / s;
|
|
27886
|
+
out[15] = 1;
|
|
27887
|
+
return out;
|
|
27375
27888
|
}
|
|
27376
27889
|
/**
|
|
27377
27890
|
* DFS one node: compose its local matrix, multiply by the parent world,
|
|
@@ -30203,13 +30716,47 @@ function createShapeObject(settings) {
|
|
|
30203
30716
|
}
|
|
30204
30717
|
|
|
30205
30718
|
// src/renderable/text/textstyle.js
|
|
30719
|
+
var VERTICAL_TOLERANCE = 0.1;
|
|
30720
|
+
function resolveFill(context, style) {
|
|
30721
|
+
const gradient = style.fillGradient;
|
|
30722
|
+
if (gradient === void 0) {
|
|
30723
|
+
return style.fillStyle.toRGBA();
|
|
30724
|
+
}
|
|
30725
|
+
const lines = style._text?.length ?? 1;
|
|
30726
|
+
if (style.gradientPerLine !== true || lines <= 1 || gradient.type !== "linear") {
|
|
30727
|
+
return gradient.toCanvasGradient(context);
|
|
30728
|
+
}
|
|
30729
|
+
const [x0, y0, x1, y1] = gradient.coords;
|
|
30730
|
+
const span = y1 - y0;
|
|
30731
|
+
if (span === 0 || Math.abs(x1 - x0) > Math.abs(span) * VERTICAL_TOLERANCE) {
|
|
30732
|
+
return gradient.toCanvasGradient(context);
|
|
30733
|
+
}
|
|
30734
|
+
const lineHeight = style.metrics.lineHeight();
|
|
30735
|
+
const blockHeight = lines * lineHeight;
|
|
30736
|
+
if (blockHeight <= 0) {
|
|
30737
|
+
return gradient.toCanvasGradient(context);
|
|
30738
|
+
}
|
|
30739
|
+
const repeated = context.createLinearGradient(x0, 0, x1, blockHeight);
|
|
30740
|
+
for (let line = 0; line < lines; line++) {
|
|
30741
|
+
for (const stop2 of gradient.colorStops) {
|
|
30742
|
+
const y = line * lineHeight + y0 + stop2.offset * span;
|
|
30743
|
+
repeated.addColorStop(
|
|
30744
|
+
Math.min(1, Math.max(0, y / blockHeight)),
|
|
30745
|
+
stop2.color
|
|
30746
|
+
);
|
|
30747
|
+
}
|
|
30748
|
+
}
|
|
30749
|
+
return repeated;
|
|
30750
|
+
}
|
|
30206
30751
|
function setContextStyle(context, style) {
|
|
30752
|
+
const fill = resolveFill(context, style);
|
|
30207
30753
|
context.font = style.font;
|
|
30208
|
-
context.fillStyle =
|
|
30754
|
+
context.fillStyle = fill;
|
|
30209
30755
|
context.strokeStyle = style.strokeStyle.toRGBA();
|
|
30210
30756
|
context.lineWidth = style.lineWidth;
|
|
30211
30757
|
context.textAlign = style.textAlign;
|
|
30212
30758
|
context.textBaseline = style.textBaseline;
|
|
30759
|
+
return fill;
|
|
30213
30760
|
}
|
|
30214
30761
|
|
|
30215
30762
|
// src/renderable/text/text.js
|
|
@@ -30237,7 +30784,8 @@ var Text = class extends Renderable {
|
|
|
30237
30784
|
* @param {object} settings - the text configuration
|
|
30238
30785
|
* @param {string} settings.font - a CSS font family: a specific name (`"Arial"`), a generic keyword (`"sans-serif"`, `"monospace"`, …), or a web font loaded via the `fontface` loader (referenced by its family name)
|
|
30239
30786
|
* @param {number|string} settings.size - the font size: a number in pixels, or a CSS size string with a unit (`"24px"` / `"1.5em"` / `"18pt"`)
|
|
30240
|
-
* @param {Color|string} [settings.fillStyle="#000000"] - a CSS color value used to fill the glyphs
|
|
30787
|
+
* @param {Color|Gradient|string} [settings.fillStyle="#000000"] - a CSS color value used to fill the glyphs, or a {@link Gradient} to ramp them
|
|
30788
|
+
* @param {boolean} [settings.gradientPerLine=true] - restart a gradient fill on every line; `false` spans one ramp across the whole block, as a plain canvas does
|
|
30241
30789
|
* @param {Color|string} [settings.strokeStyle="#000000"] - a CSS color value used for the glyph outline (drawn when `lineWidth` > 0)
|
|
30242
30790
|
* @param {number} [settings.lineWidth=0] - outline width in pixels (0 = no stroke)
|
|
30243
30791
|
* @param {string} [settings.textAlign="left"] - horizontal text alignment ("left", "center", "right")
|
|
@@ -30245,6 +30793,8 @@ var Text = class extends Renderable {
|
|
|
30245
30793
|
* @param {number} [settings.lineHeight=1.0] - line spacing height
|
|
30246
30794
|
* @param {string|Vector2d|{x:number,y:number}} [settings.anchorPoint={x:0.0, y:0.0}] - anchor point to draw the text at. Also accepts the named presets `"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`.
|
|
30247
30795
|
* @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixels of a line before it wraps
|
|
30796
|
+
* @param {boolean} [settings.bold=false] - render the face bold, as {@link Text#bold} does
|
|
30797
|
+
* @param {boolean} [settings.italic=false] - render the face italic, as {@link Text#italic} does
|
|
30248
30798
|
* @param {(string|string[])} [settings.text=""] - a string, or an array of strings
|
|
30249
30799
|
* @example
|
|
30250
30800
|
* // a styled, word-wrapped, multi-line label using a generic system font
|
|
@@ -30263,6 +30813,23 @@ var Text = class extends Renderable {
|
|
|
30263
30813
|
* label.setOpacity(0.8); // per-object transparency
|
|
30264
30814
|
* app.world.addChild(label);
|
|
30265
30815
|
* @example
|
|
30816
|
+
* // a gradient fill: `fillStyle` takes a Gradient as well as a colour.
|
|
30817
|
+
* // Its coordinates are the label's OWN bake, so (0, 0) is the top-left of
|
|
30818
|
+
* // the render box and the ramp below runs down exactly one line.
|
|
30819
|
+
* const ramp = app.renderer.createLinearGradient(0, 0, 0, 32);
|
|
30820
|
+
* ramp.addColorStop(0, "#fffdf0");
|
|
30821
|
+
* ramp.addColorStop(1, "#ffa71d");
|
|
30822
|
+
*
|
|
30823
|
+
* app.world.addChild(new Text(8, 8, {
|
|
30824
|
+
* font: "sans-serif",
|
|
30825
|
+
* size: 32,
|
|
30826
|
+
* fillStyle: ramp, // ramps the glyphs...
|
|
30827
|
+
* strokeStyle: "#000000", // ...while the outline keeps its own colour
|
|
30828
|
+
* lineWidth: 1,
|
|
30829
|
+
* text: "GAME\nOVER", // every line restarts the ramp by default
|
|
30830
|
+
* // gradientPerLine: false, // ...or span ONE ramp across both lines
|
|
30831
|
+
* }));
|
|
30832
|
+
* @example
|
|
30266
30833
|
* // a web font (loaded via the fontface loader) is used by its family name
|
|
30267
30834
|
* loader.preload(
|
|
30268
30835
|
* [{ name: "kenpixel", type: "fontface", src: "data/font/kenvector.woff2" }],
|
|
@@ -30294,11 +30861,16 @@ var Text = class extends Renderable {
|
|
|
30294
30861
|
if (typeof this.fillStyle === "undefined") {
|
|
30295
30862
|
this.fillStyle = colorPool.get(0, 0, 0);
|
|
30296
30863
|
}
|
|
30864
|
+
this.fillGradient = void 0;
|
|
30865
|
+
this.gradientPerLine = true;
|
|
30297
30866
|
if (typeof this.strokeStyle === "undefined") {
|
|
30298
30867
|
this.strokeStyle = colorPool.get(0, 0, 0);
|
|
30299
30868
|
}
|
|
30869
|
+
this.fillGradient = void 0;
|
|
30300
30870
|
if (typeof settings.fillStyle !== "undefined") {
|
|
30301
|
-
if (settings.fillStyle instanceof
|
|
30871
|
+
if (settings.fillStyle instanceof Gradient) {
|
|
30872
|
+
this.fillGradient = settings.fillStyle;
|
|
30873
|
+
} else if (settings.fillStyle instanceof Color) {
|
|
30302
30874
|
this.fillStyle.copy(settings.fillStyle);
|
|
30303
30875
|
} else {
|
|
30304
30876
|
this.fillStyle.parseCSS(settings.fillStyle);
|
|
@@ -30311,6 +30883,9 @@ var Text = class extends Renderable {
|
|
|
30311
30883
|
this.strokeStyle.parseCSS(settings.strokeStyle);
|
|
30312
30884
|
}
|
|
30313
30885
|
}
|
|
30886
|
+
if (typeof settings.gradientPerLine === "boolean") {
|
|
30887
|
+
this.gradientPerLine = settings.gradientPerLine;
|
|
30888
|
+
}
|
|
30314
30889
|
this.lineWidth = settings.lineWidth || 0;
|
|
30315
30890
|
this.textAlign = settings.textAlign || "left";
|
|
30316
30891
|
this.textBaseline = settings.textBaseline || "top";
|
|
@@ -30425,9 +31000,10 @@ var Text = class extends Renderable {
|
|
|
30425
31000
|
true
|
|
30426
31001
|
);
|
|
30427
31002
|
const width = Math.ceil(this.metrics.width / 32) * 32;
|
|
30428
|
-
const height = Math.ceil(
|
|
31003
|
+
const height = Math.ceil(
|
|
31004
|
+
(this.metrics.height + this.metrics.inkPadTop + this.metrics.inkPadBottom) / 32
|
|
31005
|
+
) * 32;
|
|
30429
31006
|
const renderer = this.parentApp?.renderer ?? game.renderer;
|
|
30430
|
-
this.canvasTexture.invalidate(renderer);
|
|
30431
31007
|
if (this.canvasTexture.width < width || this.canvasTexture.height < height) {
|
|
30432
31008
|
this.canvasTexture.resize(width, height);
|
|
30433
31009
|
}
|
|
@@ -30438,6 +31014,7 @@ var Text = class extends Renderable {
|
|
|
30438
31014
|
this.pos.x - this.metrics.x,
|
|
30439
31015
|
this.pos.y - this.metrics.y
|
|
30440
31016
|
);
|
|
31017
|
+
this.canvasTexture.invalidate(renderer);
|
|
30441
31018
|
this.isDirty = true;
|
|
30442
31019
|
return this;
|
|
30443
31020
|
}
|
|
@@ -30464,9 +31041,18 @@ var Text = class extends Renderable {
|
|
|
30464
31041
|
}
|
|
30465
31042
|
/**
|
|
30466
31043
|
* the ratio of visible characters (0.0 to 1.0).
|
|
30467
|
-
* Setting this automatically updates {@link visibleCharacters}.
|
|
31044
|
+
* Setting this automatically updates {@link Text#visibleCharacters}.
|
|
31045
|
+
*
|
|
31046
|
+
* This is the one to tween: it is independent of how many characters the
|
|
31047
|
+
* label holds, so a reveal takes the same time whatever the string is.
|
|
30468
31048
|
* @public
|
|
30469
31049
|
* @type {number}
|
|
31050
|
+
* @default 1.0
|
|
31051
|
+
* @see Text#visibleCharacters
|
|
31052
|
+
* @example
|
|
31053
|
+
* // reveal over two seconds, regardless of length
|
|
31054
|
+
* label.visibleRatio = 0;
|
|
31055
|
+
* new Tween(label).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
|
|
30470
31056
|
*/
|
|
30471
31057
|
get visibleRatio() {
|
|
30472
31058
|
if (this._visibleCharacters === -1) {
|
|
@@ -30535,6 +31121,14 @@ var Text = class extends Renderable {
|
|
|
30535
31121
|
* measure the given text size in pixels
|
|
30536
31122
|
* @param {string} [text] - the text to be measured
|
|
30537
31123
|
* @returns {TextMetrics} a TextMetrics object defining the dimensions of the given piece of text
|
|
31124
|
+
* @example
|
|
31125
|
+
* // size a panel around a label
|
|
31126
|
+
* const size = label.measureText();
|
|
31127
|
+
* panel.resize(size.width + 16, size.height + 16);
|
|
31128
|
+
*
|
|
31129
|
+
* // or measure a string the label does not currently hold, to reserve
|
|
31130
|
+
* // room for the widest state a counter will reach
|
|
31131
|
+
* const widest = label.measureText("00:00").width;
|
|
30538
31132
|
*/
|
|
30539
31133
|
measureText(text = this._text) {
|
|
30540
31134
|
return this.metrics.measureText(text, this.canvasTexture.context);
|
|
@@ -30544,8 +31138,8 @@ var Text = class extends Renderable {
|
|
|
30544
31138
|
* @param {CanvasRenderer|WebGLRenderer} renderer - Reference to the destination renderer instance
|
|
30545
31139
|
*/
|
|
30546
31140
|
draw(renderer) {
|
|
31141
|
+
this.metrics.updateOrigin();
|
|
30547
31142
|
if (this.isDirty) {
|
|
30548
|
-
this.canvasTexture.invalidate(renderer);
|
|
30549
31143
|
this.canvasTexture.clear();
|
|
30550
31144
|
this._drawFont(
|
|
30551
31145
|
this.canvasTexture.context,
|
|
@@ -30553,9 +31147,10 @@ var Text = class extends Renderable {
|
|
|
30553
31147
|
this.pos.x - this.metrics.x,
|
|
30554
31148
|
this.pos.y - this.metrics.y
|
|
30555
31149
|
);
|
|
31150
|
+
this.canvasTexture.invalidate(renderer);
|
|
30556
31151
|
}
|
|
30557
31152
|
let x = this.metrics.x;
|
|
30558
|
-
let y = this.metrics.y;
|
|
31153
|
+
let y = this.metrics.y - this.metrics.inkPadTop;
|
|
30559
31154
|
if (renderer.settings.subPixel === false) {
|
|
30560
31155
|
x = ~~x;
|
|
30561
31156
|
y = ~~y;
|
|
@@ -30567,25 +31162,54 @@ var Text = class extends Renderable {
|
|
|
30567
31162
|
* @internal
|
|
30568
31163
|
*/
|
|
30569
31164
|
_drawFont(context, text, x, y) {
|
|
30570
|
-
|
|
30571
|
-
|
|
30572
|
-
|
|
30573
|
-
|
|
30574
|
-
|
|
30575
|
-
|
|
30576
|
-
|
|
31165
|
+
context.save();
|
|
31166
|
+
context.translate(0, this.metrics.inkPadTop);
|
|
31167
|
+
const fill = setContextStyle(context, this);
|
|
31168
|
+
const fills = this.fillStyle.alpha > 0;
|
|
31169
|
+
const strokes = this.lineWidth > 0 && this.strokeStyle.alpha > 0;
|
|
31170
|
+
const masked = fills && this.fillGradient !== void 0;
|
|
31171
|
+
const eachLine = (paint) => {
|
|
31172
|
+
let remaining = this.visibleCharacters;
|
|
31173
|
+
let lineY = y;
|
|
31174
|
+
for (let i = 0; i < text.length; i++) {
|
|
31175
|
+
let string = text[i].trimEnd();
|
|
31176
|
+
if (remaining !== -1) {
|
|
31177
|
+
if (remaining <= 0) {
|
|
31178
|
+
break;
|
|
31179
|
+
}
|
|
31180
|
+
string = string.substring(0, remaining);
|
|
31181
|
+
remaining -= string.length;
|
|
30577
31182
|
}
|
|
30578
|
-
string
|
|
30579
|
-
|
|
31183
|
+
paint(string, x, lineY);
|
|
31184
|
+
lineY += this.metrics.lineHeight();
|
|
30580
31185
|
}
|
|
30581
|
-
|
|
30582
|
-
|
|
30583
|
-
|
|
30584
|
-
|
|
30585
|
-
context.strokeText(string, x, y);
|
|
31186
|
+
};
|
|
31187
|
+
if (fills) {
|
|
31188
|
+
if (masked === true) {
|
|
31189
|
+
context.fillStyle = "#000000";
|
|
30586
31190
|
}
|
|
30587
|
-
|
|
31191
|
+
eachLine((line, lineX, lineY) => {
|
|
31192
|
+
context.fillText(line, lineX, lineY);
|
|
31193
|
+
});
|
|
31194
|
+
}
|
|
31195
|
+
if (masked === true) {
|
|
31196
|
+
const canvas = context.canvas;
|
|
31197
|
+
context.globalCompositeOperation = "source-in";
|
|
31198
|
+
context.fillStyle = fill;
|
|
31199
|
+
context.fillRect(
|
|
31200
|
+
-1,
|
|
31201
|
+
-this.metrics.inkPadTop - 1,
|
|
31202
|
+
canvas.width + 2,
|
|
31203
|
+
canvas.height + 2
|
|
31204
|
+
);
|
|
31205
|
+
context.globalCompositeOperation = "source-over";
|
|
31206
|
+
}
|
|
31207
|
+
if (strokes) {
|
|
31208
|
+
eachLine((line, lineX, lineY) => {
|
|
31209
|
+
context.strokeText(line, lineX, lineY);
|
|
31210
|
+
});
|
|
30588
31211
|
}
|
|
31212
|
+
context.restore();
|
|
30589
31213
|
return this.metrics;
|
|
30590
31214
|
}
|
|
30591
31215
|
/**
|
|
@@ -30597,6 +31221,7 @@ var Text = class extends Renderable {
|
|
|
30597
31221
|
const renderer = this.parentApp?.renderer ?? game.renderer;
|
|
30598
31222
|
this.canvasTexture.destroy(renderer);
|
|
30599
31223
|
this.canvasTexture = void 0;
|
|
31224
|
+
this.fillGradient = void 0;
|
|
30600
31225
|
colorPool.release(this.fillStyle);
|
|
30601
31226
|
colorPool.release(this.strokeStyle);
|
|
30602
31227
|
this.fillStyle = this.strokeStyle = void 0;
|
|
@@ -30607,6 +31232,8 @@ var Text = class extends Renderable {
|
|
|
30607
31232
|
};
|
|
30608
31233
|
|
|
30609
31234
|
// src/renderable/text/textmetrics.js
|
|
31235
|
+
var INK_SLACK = 1;
|
|
31236
|
+
var INK_GUESS = 0.25;
|
|
30610
31237
|
var TextMetrics = class extends Bounds {
|
|
30611
31238
|
/**
|
|
30612
31239
|
* @param {Text|BitmapText} ancestor - the parent object that contains this TextMetrics object
|
|
@@ -30690,17 +31317,55 @@ var TextMetrics = class extends Bounds {
|
|
|
30690
31317
|
this.glyphYOffset = fontData.glyphMinTop * scaleY;
|
|
30691
31318
|
this.glyphMaxBottom = fontData.glyphMaxBottom * scaleY;
|
|
30692
31319
|
}
|
|
31320
|
+
this.inkPadTop = 0;
|
|
31321
|
+
this.inkPadBottom = 0;
|
|
31322
|
+
if (!isBitmapText && strings.length > 0 && typeof context !== "undefined") {
|
|
31323
|
+
const style = this.ancestor;
|
|
31324
|
+
const stroke = style.lineWidth > 0 && style.strokeStyle.alpha > 0 ? style.lineWidth / 2 : 0;
|
|
31325
|
+
const first = context.measureText(strings[0].trimEnd());
|
|
31326
|
+
const last = strings.length === 1 ? first : context.measureText(strings[strings.length - 1].trimEnd());
|
|
31327
|
+
const above = first.actualBoundingBoxAscent;
|
|
31328
|
+
const below = last.actualBoundingBoxDescent;
|
|
31329
|
+
const measured = Number.isFinite(above) && Number.isFinite(below);
|
|
31330
|
+
const top = measured ? above : style.fontSize * INK_GUESS;
|
|
31331
|
+
const bottom = measured ? below : style.fontSize * INK_GUESS;
|
|
31332
|
+
this.inkPadTop = Math.max(0, Math.ceil(top + stroke) + INK_SLACK);
|
|
31333
|
+
this.inkPadBottom = Math.max(
|
|
31334
|
+
0,
|
|
31335
|
+
Math.ceil(this.inkPadTop + bottom + stroke - this.lineHeight()) + INK_SLACK
|
|
31336
|
+
);
|
|
31337
|
+
}
|
|
30693
31338
|
this.width = Math.ceil(this.width);
|
|
30694
31339
|
this.height = Math.ceil(this.height);
|
|
31340
|
+
this.updateOrigin();
|
|
31341
|
+
if (typeof context !== "undefined") {
|
|
31342
|
+
context.restore();
|
|
31343
|
+
}
|
|
31344
|
+
return this;
|
|
31345
|
+
}
|
|
31346
|
+
/**
|
|
31347
|
+
* Recompute the box's position from the ancestor's CURRENT `pos`.
|
|
31348
|
+
*
|
|
31349
|
+
* Split out of `measureText` because it depends on `pos` while everything
|
|
31350
|
+
* else there depends only on the string and the font. A label that moves
|
|
31351
|
+
* needs this refreshed — and nothing else — so the draw path can call it
|
|
31352
|
+
* per frame without re-measuring a single glyph.
|
|
31353
|
+
*
|
|
31354
|
+
* The `Math.floor` is what puts the canvas on a whole pixel. The remainder
|
|
31355
|
+
* it drops is not lost: `Text` bakes its glyphs at `pos - metrics`, so the
|
|
31356
|
+
* sub-pixel part lands in the rasterization, where the font rasterizer can
|
|
31357
|
+
* antialias it, instead of resampling the whole texture.
|
|
31358
|
+
* @returns {TextMetrics} this instance for chaining
|
|
31359
|
+
* @ignore
|
|
31360
|
+
* @internal
|
|
31361
|
+
*/
|
|
31362
|
+
updateOrigin() {
|
|
30695
31363
|
this.x = Math.floor(
|
|
30696
31364
|
this.ancestor.textAlign === "right" ? this.ancestor.pos.x - this.width : this.ancestor.textAlign === "center" ? this.ancestor.pos.x - this.width / 2 : this.ancestor.pos.x
|
|
30697
31365
|
);
|
|
30698
31366
|
this.y = Math.floor(
|
|
30699
31367
|
this.ancestor.textBaseline.search(/^(top|hanging)$/) === 0 ? this.ancestor.pos.y : this.ancestor.textBaseline === "middle" ? this.ancestor.pos.y - this.lineHeight() / 2 : this.ancestor.pos.y - this.lineHeight()
|
|
30700
31368
|
);
|
|
30701
|
-
if (typeof context !== "undefined") {
|
|
30702
|
-
context.restore();
|
|
30703
|
-
}
|
|
30704
31369
|
return this;
|
|
30705
31370
|
}
|
|
30706
31371
|
/**
|
|
@@ -30748,9 +31413,8 @@ var BitmapText = class extends Renderable {
|
|
|
30748
31413
|
* @param {object} settings - the text configuration
|
|
30749
31414
|
* @param {string|Image} settings.font - a font name to identify the corresponding source image
|
|
30750
31415
|
* @param {string} [settings.fontData=settings.font] - the bitmap font data corresponding name, or the bitmap font data itself (AngelCode BMFont, `.fnt` text or `.xml`)
|
|
30751
|
-
* @param {number} [settings.size] - size a
|
|
30752
|
-
* @param {Color|string} [settings.fillStyle] - a CSS color value used to tint the
|
|
30753
|
-
* @param {number} [settings.lineWidth=1] - line width, in pixels, when drawing stroke
|
|
31416
|
+
* @param {number} [settings.size=1.0] - a scaling RATIO applied to the font's authored size, not a pixel size: `2` draws it at double. ({@link Text} takes pixels here; this one does not.)
|
|
31417
|
+
* @param {Color|string} [settings.fillStyle] - a CSS color value used to tint the glyphs, see {@link Renderable#tint}
|
|
30754
31418
|
* @param {string} [settings.textAlign="left"] - horizontal text alignment
|
|
30755
31419
|
* @param {string} [settings.textBaseline="top"] - the text baseline
|
|
30756
31420
|
* @param {number} [settings.lineHeight=1.0] - line spacing height
|
|
@@ -30816,7 +31480,7 @@ var BitmapText = class extends Renderable {
|
|
|
30816
31480
|
/**
|
|
30817
31481
|
* change the font settings
|
|
30818
31482
|
* @param {string} textAlign - ("left", "center", "right")
|
|
30819
|
-
* @param {number} [scale]
|
|
31483
|
+
* @param {number} [scale] - a scaling ratio, applied through {@link BitmapText#resize} when given
|
|
30820
31484
|
* @returns {BitmapText} this object for chaining
|
|
30821
31485
|
*/
|
|
30822
31486
|
set(textAlign, scale2) {
|
|
@@ -30941,10 +31605,27 @@ var BitmapText = class extends Renderable {
|
|
|
30941
31605
|
return bounds;
|
|
30942
31606
|
}
|
|
30943
31607
|
/**
|
|
30944
|
-
* defines the color used to tint the bitmap text
|
|
31608
|
+
* defines the color used to tint the bitmap text.
|
|
31609
|
+
*
|
|
31610
|
+
* This is {@link Renderable#tint} under another name, so the same rule
|
|
31611
|
+
* applies: white — `(255, 255, 255)` — is the absence of a tint, and any
|
|
31612
|
+
* other colour tints away from there. A page image authored in white
|
|
31613
|
+
* therefore keeps every colour available to it.
|
|
30945
31614
|
* @public
|
|
30946
31615
|
* @type {Color}
|
|
30947
31616
|
* @see Renderable#tint
|
|
31617
|
+
* @example
|
|
31618
|
+
* // tint at construction...
|
|
31619
|
+
* const score = new BitmapText(8, 8, {
|
|
31620
|
+
* font: "arial",
|
|
31621
|
+
* text: "1000",
|
|
31622
|
+
* fillStyle: "#ffd700", // gold
|
|
31623
|
+
* });
|
|
31624
|
+
* app.world.addChild(score);
|
|
31625
|
+
*
|
|
31626
|
+
* // ...or at any point after it, from a CSS string or a Color
|
|
31627
|
+
* score.fillStyle = "#ff4040"; // flash red on damage
|
|
31628
|
+
* score.fillStyle = new Color(255, 255, 255); // back to untinted
|
|
30948
31629
|
*/
|
|
30949
31630
|
get fillStyle() {
|
|
30950
31631
|
return this.tint;
|
|
@@ -30958,8 +31639,14 @@ var BitmapText = class extends Renderable {
|
|
|
30958
31639
|
}
|
|
30959
31640
|
/**
|
|
30960
31641
|
* change the font display size
|
|
30961
|
-
* @param {number} scale - ratio
|
|
31642
|
+
* @param {number} scale - a ratio against the font's authored size, NOT a
|
|
31643
|
+
* pixel size: `1` is the page image at its native scale, `2` is double
|
|
30962
31644
|
* @returns {BitmapText} this object for chaining
|
|
31645
|
+
* @example
|
|
31646
|
+
* // a bitmap font is pixel art — whole-number ratios stay crisp, and
|
|
31647
|
+
* // fractional ones resample the page image
|
|
31648
|
+
* title.resize(3); // three times its authored size
|
|
31649
|
+
* title.set("center", 2); // align and rescale in one call
|
|
30963
31650
|
*/
|
|
30964
31651
|
resize(scale2) {
|
|
30965
31652
|
this.fontScale.set(scale2, scale2);
|
|
@@ -30972,6 +31659,10 @@ var BitmapText = class extends Renderable {
|
|
|
30972
31659
|
* measure the given text size in pixels
|
|
30973
31660
|
* @param {string} [text]
|
|
30974
31661
|
* @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
|
|
31662
|
+
* @example
|
|
31663
|
+
* // size a panel around a label, at the label's CURRENT scale
|
|
31664
|
+
* const size = label.measureText();
|
|
31665
|
+
* panel.resize(size.width + 16, size.height + 16);
|
|
30975
31666
|
*/
|
|
30976
31667
|
measureText(text = this._text) {
|
|
30977
31668
|
return this.metrics.measureText(text);
|
|
@@ -34510,6 +35201,21 @@ function initParsers() {
|
|
|
34510
35201
|
setParser("shader", preloadShader);
|
|
34511
35202
|
parserInitialized = true;
|
|
34512
35203
|
}
|
|
35204
|
+
var _app2;
|
|
35205
|
+
on(GAME_INIT, (app) => {
|
|
35206
|
+
_app2 = app;
|
|
35207
|
+
});
|
|
35208
|
+
function runShaderPrewarm() {
|
|
35209
|
+
if (_app2 === void 0 || _app2.settings?.prewarm !== true) {
|
|
35210
|
+
return Promise.resolve();
|
|
35211
|
+
}
|
|
35212
|
+
try {
|
|
35213
|
+
return Promise.resolve(_app2.renderer?.prewarm?.()).catch(() => {
|
|
35214
|
+
});
|
|
35215
|
+
} catch {
|
|
35216
|
+
return Promise.resolve();
|
|
35217
|
+
}
|
|
35218
|
+
}
|
|
34513
35219
|
function completeLoading(onloadcb) {
|
|
34514
35220
|
const callback = onloadcb || onload;
|
|
34515
35221
|
if (typeof callback === "function") {
|
|
@@ -34553,7 +35259,7 @@ function preload(assets, onloadcb, switchToLoadState = true) {
|
|
|
34553
35259
|
if (switchToLoadState === true) {
|
|
34554
35260
|
state_default.change(state_default.LOADING);
|
|
34555
35261
|
}
|
|
34556
|
-
return Promise.all(promises).then(() => {
|
|
35262
|
+
return Promise.all(promises).then(runShaderPrewarm).then(() => {
|
|
34557
35263
|
completeLoading(onload);
|
|
34558
35264
|
});
|
|
34559
35265
|
}
|
|
@@ -38399,6 +39105,18 @@ var WebGLBatcher = class extends Batcher {
|
|
|
38399
39105
|
vertex.clear();
|
|
38400
39106
|
}
|
|
38401
39107
|
}
|
|
39108
|
+
/**
|
|
39109
|
+
* Build any shader variant this batcher compiles lazily.
|
|
39110
|
+
*
|
|
39111
|
+
* A batcher whose only program is the one built in its constructor has
|
|
39112
|
+
* nothing to do here — which is every batcher but the mesh one.
|
|
39113
|
+
* @returns {Promise<void>} settles once this batcher is warm
|
|
39114
|
+
* @ignore
|
|
39115
|
+
* @internal
|
|
39116
|
+
*/
|
|
39117
|
+
prewarm() {
|
|
39118
|
+
return Promise.resolve();
|
|
39119
|
+
}
|
|
38402
39120
|
};
|
|
38403
39121
|
|
|
38404
39122
|
// src/video/gpu/primitives.ts
|
|
@@ -40544,7 +41262,7 @@ function writeLight3dBlock(out, packed) {
|
|
|
40544
41262
|
}
|
|
40545
41263
|
|
|
40546
41264
|
// src/video/webgl/shaders/mesh-lit.frag
|
|
40547
|
-
var mesh_lit_default = "#version 300 es\n// Lit mesh fragment shader: half-Lambert diffuse from directional lights\n// plus an ambient floor.\n//\n// GLSL ES 3.00 because the light data arrives in a uniform block, which does\n// not exist in ES 1.00. That is what lifted the light cap: uniform arrays are\n// charged against MAX_FRAGMENT_UNIFORM_VECTORS, a small driver-reported\n// budget, while a block is charged against MAX_UNIFORM_BLOCK_SIZE.\n//\n// `__MAX_LIGHTS__` is replaced with the MAX_LIGHTS constant\n// (src/video/webgl/lighting/constants.ts) by LitMeshBatcher._shaderSources at\n// load time, so the array size can't drift from the uniform packer. (Used\n// directly rather than via a #define so it survives the GLSL preprocessor\n// regardless of how it handles macros.)\n//\n// The block's layout is std140 and is written by\n// src/video/webgl/lighting/std140.ts. The two must agree byte for byte \u2014 a\n// mismatch does not fail to link, it silently shifts every light.\n//\n// No `precision` line here on purpose: `setPrecision` injects float and int\n// precision after `#version`, using whatever the device reports and the\n// application asked for, so shader files don't hardcode it.\n\nuniform sampler2D uSampler;\nuniform float uAlphaCutoff; // alpha cutout threshold (0 = disabled)\nuniform vec3 uEmissive; // self-illumination color (0 = none)\nuniform vec3 uSpecular; // specular color (0 = purely diffuse)\nuniform float uShininess; // specular exponent (0 = no highlight)\nuniform vec3 uEyePosition; // camera position, world space\nuniform sampler2D uAlphaMap; // per-texel opacity (MTL map_d)\nuniform float uHasAlphaMap; // 0 = uAlphaMap is filler, ignore it\n\n// One light, type inferred from sentinels (see std140.ts):\n// posRange.w < 0 -> directional (dirCone.xyz = surface->light, normalized);\n// otherwise positional at posRange.xyz with quadratic falloff over\n// posRange.w, plus a spot cone when dirCone.w > -1 (dirCone.xyz = the cone\n// axis / travel direction, w = cos(outer), colorInner.w = cos(inner)).\n// colorInner.rgb is premultiplied by intensity.\nstruct Light3dData {\n vec4 posRange;\n vec4 dirCone;\n vec4 colorInner;\n};\n\nlayout(std140) uniform Light3dBlock {\n // `count` is a float: mixing int and float members would stop the staging\n // buffer being a single Float32Array view, and the cast below is free. The\n // vec3 that follows aligns to 16 bytes, so the three floats after `count`\n // are padding either way.\n float uLightCount;\n vec3 uAmbient;\n Light3dData uLights[__MAX_LIGHTS__];\n};\n\nin vec4 vColor;\nin vec2 vRegion;\nin vec3 vNormal;\nin vec3 vWorldPos;\n#ifdef INSTANCE_DATA\n// per-instance custom slot \u2014 read as emissive by this built-in shading\nin vec4 vInstanceData;\n#endif\n\nout vec4 fragColor;\n\n// Distance fog is a COMPILED VARIANT, not a runtime branch: the batcher\n// injects `#define FOG` only while a camera has fog enabled. A software\n// rasterizer predicates both sides of a branch, so an `exp()` guarded at\n// runtime still costs every fragment of every scene \u2014 including the scenes\n// that never asked for fog.\n#ifdef FOG\nuniform vec3 uFogColor; // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams; // x = mode (0 off / 1 linear / 2 exp2),\n // y = near, z = 1/(far - near), w = density\nin float vFogDepth; // radial view-space distance, interpolated\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of the block in\n// mesh.frag \u2014 keep the two in step; they are duplicated rather than shared\n// because the production build loads each shader as raw text (no preprocessor\n// include), and this shader is ES 3.00 while its twin is ES 1.00.\n//\n// `rgb` is already multiplied by `a`, so the fog colour has to be scaled by\n// the fragment's own coverage. A plain mix toward uFogColor would paint\n// full-strength fog onto near-transparent fragments \u2014 grey halos around every\n// alpha-cutout leaf.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nvec3 applyFog(vec3 rgb, float a) {\n float mode = uFogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n float f; // fraction of the scene colour that survives\n if (mode < 1.5) {\n f = 1.0 - clamp((vFogDepth - uFogParams.y) * uFogParams.z, 0.0, 1.0);\n } else {\n float dd = vFogDepth * uFogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uFogColor * a, rgb, f);\n}\n#endif\n\n\n\nvoid main(void) {\n vec4 base = texture(uSampler, vRegion);\n\n // per-texel opacity (MTL map_d) multiplies in BEFORE the cutout, so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Red channel: the format stores a\n // greyscale map, and every channel carries the same value.\n // Sampled unconditionally and WEIGHTED rather than branched: when no map\n // is bound the second sampler is filler (the diffuse texture), so the\n // value is thrown away \u2014 and both backends run the identical expression\n // instead of one branching and the other not.\n base.a *= mix(1.0, texture(uAlphaMap, vRegion).r, uHasAlphaMap);\n\n // hard alpha cutout (glTF alphaMode MASK) \u2014 discard before any shading\n // so cut-away texels cost nothing and never write depth.\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply. The cutout describes the shape of the surface, and that shape\n // does not change when the object fades: testing the drawn alpha instead\n // makes a cutout mesh vanish completely the moment its opacity crosses its\n // own threshold \u2014 and `Sprite3d` defaults that threshold to 0.5, so half a\n // fade-out was a hard pop.\n if (base.a < uAlphaCutoff) {\n discard;\n }\n base *= vColor;\n\n // A mesh marked `lit` with no usable normals \u2014 the 2D-camera path,\n // which leaves world normals unwritten, or geometry that supplied none \u2014\n // would normalize a zero vector to NaN and render BLACK. Degrade to\n // unlit instead: wrong, but recognisably the model rather than a hole.\n float nLength = length(vNormal);\n if (nLength < 1e-6) {\n // the emissive term is built exactly as the lit path below builds\n // it, per-instance slot included \u2014 degrading to unlit must not also\n // drop an instance's glow\n vec3 unlitEmissive = uEmissive;\n#ifdef INSTANCE_DATA\n unlitEmissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n fragColor = vec4(applyFog(base.rgb + unlitEmissive, base.a), base.a);\n#else\n fragColor = vec4(base.rgb + unlitEmissive, base.a);\n#endif\n return;\n }\n vec3 N = vNormal / nLength;\n vec3 lit = uAmbient;\n // Blinn-Phong specular, accumulated alongside the diffuse term. Gated on\n // the exponent rather than the colour: `Ns` of 0 is the format's \"no\n // highlight\", and exporters happily write a non-black `Ks` next to it.\n // The half-vector form (rather than reflect()) is the cheaper of the two\n // and is what the fixed-function pipeline this format predates used.\n vec3 specular = vec3(0.0);\n bool hasSpecular = uShininess > 0.0;\n vec3 V = hasSpecular ? normalize(uEyePosition - vWorldPos) : vec3(0.0);\n // ES 3.00 allows a non-constant loop bound, so this runs exactly as many\n // iterations as there are live lights \u2014 unused capacity costs nothing,\n // which is what makes a larger cap safe.\n // clamped to the array size, not just taken on trust: if the block ever\n // read as something other than what the writer put there, an unbounded\n // trip count would also index `uLights` out of range\n int count = min(int(uLightCount), __MAX_LIGHTS__);\n for (int i = 0; i < count; i++) {\n vec4 pr = uLights[i].posRange;\n vec4 dc = uLights[i].dirCone;\n vec3 L;\n float atten = 1.0;\n if (pr.w < 0.0) {\n // directional: dirCone.xyz is already the surface->light vector\n L = dc.xyz;\n } else {\n // positional: quadratic falloff over range (the Light2d model \u2014\n // stylized, not physical inverse-square, which is unusable in\n // pixel-unit worlds with unit intensities)\n vec3 toLight = pr.xyz - vWorldPos;\n float dist = max(length(toLight), 1e-4);\n L = toLight / dist;\n float linearAtt = max(0.0, 1.0 - dist / pr.w);\n atten = linearAtt * linearAtt;\n if (dc.w > -1.0) {\n // spot cone: -L is the light->surface direction; fade from\n // the inner cone cosine to the outer\n float cd = dot(-L, dc.xyz);\n atten *= smoothstep(dc.w, uLights[i].colorInner.w, cd);\n }\n }\n // Half-Lambert (\"wrap\") diffuse: dot * 0.5 + 0.5. Softens the\n // terminator and lifts the shadowed side, for a gentler, more\n // diffuse look than hard Lambert (which reads as harsh noon).\n float ndl = dot(N, L) * 0.5 + 0.5;\n lit += uLights[i].colorInner.rgb * (ndl * ndl * atten);\n if (hasSpecular) {\n // masked by the UNWRAPPED Lambert term: half-Lambert lifts the\n // shadowed side, and a highlight on a surface facing away from\n // the light reads as a rendering error\n float facing = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float spec = pow(max(dot(N, H), 0.0), uShininess);\n specular += uLights[i].colorInner.rgb * (spec * facing * atten);\n }\n }\n\n // emissive self-illuminates: added AFTER lighting so it glows at full\n // strength regardless of the scene lights (neon, lava, glowing eyes).\n vec3 emissive = uEmissive;\n#ifdef INSTANCE_DATA\n emissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n fragColor = vec4(\n applyFog(base.rgb * lit + specular * uSpecular + emissive, base.a),\n base.a\n );\n#else\n fragColor = vec4(base.rgb * lit + specular * uSpecular + emissive, base.a);\n#endif\n}\n";
|
|
41265
|
+
var mesh_lit_default = "#version 300 es\n// Lit mesh fragment shader: half-Lambert diffuse from directional lights\n// plus an ambient floor.\n//\n// GLSL ES 3.00 because the light data arrives in a uniform block, which does\n// not exist in ES 1.00. That is what lifted the light cap: uniform arrays are\n// charged against MAX_FRAGMENT_UNIFORM_VECTORS, a small driver-reported\n// budget, while a block is charged against MAX_UNIFORM_BLOCK_SIZE.\n//\n// `__MAX_LIGHTS__` is replaced with the MAX_LIGHTS constant\n// (src/video/webgl/lighting/constants.ts) by LitMeshBatcher._shaderSources at\n// load time, so the array size can't drift from the uniform packer. (Used\n// directly rather than via a #define so it survives the GLSL preprocessor\n// regardless of how it handles macros.)\n//\n// The block's layout is std140 and is written by\n// src/video/webgl/lighting/std140.ts. The two must agree byte for byte \u2014 a\n// mismatch does not fail to link, it silently shifts every light.\n//\n// No `precision` line here on purpose: `setPrecision` injects float and int\n// precision after `#version`, using whatever the device reports and the\n// application asked for, so shader files don't hardcode it.\n\nuniform sampler2D uSampler;\nuniform float uAlphaCutoff; // alpha cutout threshold (0 = disabled)\nuniform vec3 uEmissive; // self-illumination color (0 = none)\nuniform vec3 uSpecular; // specular color (0 = purely diffuse)\nuniform float uShininess; // specular exponent (0 = no highlight)\nuniform vec3 uEyePosition; // camera position, world space\nuniform sampler2D uAlphaMap; // per-texel opacity (MTL map_d)\nuniform float uHasAlphaMap; // 0 = uAlphaMap is filler, ignore it\n\n// One light, type inferred from sentinels (see std140.ts):\n// posRange.w < 0 -> directional (dirCone.xyz = surface->light, normalized);\n// otherwise positional at posRange.xyz with quadratic falloff over\n// posRange.w, plus a spot cone when dirCone.w > -1 (dirCone.xyz = the cone\n// axis / travel direction, w = cos(outer), colorInner.w = cos(inner)).\n// colorInner.rgb is premultiplied by intensity.\nstruct Light3dData {\n vec4 posRange;\n vec4 dirCone;\n vec4 colorInner;\n};\n\nlayout(std140) uniform Light3dBlock {\n // `count` is a float: mixing int and float members would stop the staging\n // buffer being a single Float32Array view, and the cast below is free. The\n // vec3 that follows aligns to 16 bytes, so the three floats after `count`\n // are padding either way.\n float uLightCount;\n vec3 uAmbient;\n Light3dData uLights[__MAX_LIGHTS__];\n};\n\nin vec4 vColor;\nin vec2 vRegion;\nin vec3 vNormal;\nin vec3 vWorldPos;\n#ifdef INSTANCE_DATA\n// per-instance custom slot \u2014 read as emissive by this built-in shading\nin vec4 vInstanceData;\n#endif\n\nout vec4 fragColor;\n\n// Distance fog is a COMPILED VARIANT, not a runtime branch: the batcher\n// injects `#define FOG` only while a camera has fog enabled. A software\n// rasterizer predicates both sides of a branch, so an `exp()` guarded at\n// runtime still costs every fragment of every scene \u2014 including the scenes\n// that never asked for fog.\n#ifdef FOG\nuniform vec3 uFogColor; // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams; // x = mode (0 off / 1 linear / 2 exp2),\n // y = near, z = 1/(far - near), w = density\nin float vFogDepth; // radial view-space distance, interpolated\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of the block in\n// mesh.frag \u2014 keep the two in step; they are duplicated rather than shared\n// because the production build loads each shader as raw text (no preprocessor\n// include), and this shader is ES 3.00 while its twin is ES 1.00.\n//\n// `rgb` is already multiplied by `a`, so the fog colour has to be scaled by\n// the fragment's own coverage. A plain mix toward uFogColor would paint\n// full-strength fog onto near-transparent fragments \u2014 grey halos around every\n// alpha-cutout leaf.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nvec3 applyFog(vec3 rgb, float a) {\n float mode = uFogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n float f; // fraction of the scene colour that survives\n if (mode < 1.5) {\n f = 1.0 - clamp((vFogDepth - uFogParams.y) * uFogParams.z, 0.0, 1.0);\n } else {\n float dd = vFogDepth * uFogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uFogColor * a, rgb, f);\n}\n#endif\n\n\n\n// The mesh-hosted ShaderEffect hook (#1658). Identity by default \u2014 every\n// compiler inlines it away, so an un-effected mesh pays nothing. When an\n// effect is hosted, `spliceEffect` replaces this definition with one that\n// calls the effect's `apply()`, and pastes the effect body above it.\n//\n// Deliberately a FUNCTION and not a marker comment: the shader pipeline\n// strips comments, so a comment cannot be relied on to reach the splicer.\nvec4 ME_effect(vec4 c, vec2 uv) { return c; }\n\nvoid main(void) {\n vec4 base = texture(uSampler, vRegion);\n\n // per-texel opacity (MTL map_d) multiplies in BEFORE the cutout, so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Red channel: the format stores a\n // greyscale map, and every channel carries the same value.\n // Sampled unconditionally and WEIGHTED rather than branched: when no map\n // is bound the second sampler is filler (the diffuse texture), so the\n // value is thrown away \u2014 and both backends run the identical expression\n // instead of one branching and the other not.\n base.a *= mix(1.0, texture(uAlphaMap, vRegion).r, uHasAlphaMap);\n\n // hard alpha cutout (glTF alphaMode MASK) \u2014 discard before any shading\n // so cut-away texels cost nothing and never write depth.\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply. The cutout describes the shape of the surface, and that shape\n // does not change when the object fades: testing the drawn alpha instead\n // makes a cutout mesh vanish completely the moment its opacity crosses its\n // own threshold \u2014 and `Sprite3d` defaults that threshold to 0.5, so half a\n // fade-out was a hard pop.\n if (base.a < uAlphaCutoff) {\n discard;\n }\n base *= vColor;\n base = ME_effect(base, vRegion);\n\n // A mesh marked `lit` with no usable normals \u2014 the 2D-camera path,\n // which leaves world normals unwritten, or geometry that supplied none \u2014\n // would normalize a zero vector to NaN and render BLACK. Degrade to\n // unlit instead: wrong, but recognisably the model rather than a hole.\n float nLength = length(vNormal);\n if (nLength < 1e-6) {\n // the emissive term is built exactly as the lit path below builds\n // it, per-instance slot included \u2014 degrading to unlit must not also\n // drop an instance's glow\n vec3 unlitEmissive = uEmissive;\n#ifdef INSTANCE_DATA\n unlitEmissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n fragColor = vec4(applyFog(base.rgb + unlitEmissive, base.a), base.a);\n#else\n fragColor = vec4(base.rgb + unlitEmissive, base.a);\n#endif\n return;\n }\n vec3 N = vNormal / nLength;\n vec3 lit = uAmbient;\n // Blinn-Phong specular, accumulated alongside the diffuse term. Gated on\n // the exponent rather than the colour: `Ns` of 0 is the format's \"no\n // highlight\", and exporters happily write a non-black `Ks` next to it.\n // The half-vector form (rather than reflect()) is the cheaper of the two\n // and is what the fixed-function pipeline this format predates used.\n vec3 specular = vec3(0.0);\n bool hasSpecular = uShininess > 0.0;\n vec3 V = hasSpecular ? normalize(uEyePosition - vWorldPos) : vec3(0.0);\n // ES 3.00 allows a non-constant loop bound, so this runs exactly as many\n // iterations as there are live lights \u2014 unused capacity costs nothing,\n // which is what makes a larger cap safe.\n // clamped to the array size, not just taken on trust: if the block ever\n // read as something other than what the writer put there, an unbounded\n // trip count would also index `uLights` out of range\n int count = min(int(uLightCount), __MAX_LIGHTS__);\n for (int i = 0; i < count; i++) {\n vec4 pr = uLights[i].posRange;\n vec4 dc = uLights[i].dirCone;\n vec3 L;\n float atten = 1.0;\n if (pr.w < 0.0) {\n // directional: dirCone.xyz is already the surface->light vector\n L = dc.xyz;\n } else {\n // positional: quadratic falloff over range (the Light2d model \u2014\n // stylized, not physical inverse-square, which is unusable in\n // pixel-unit worlds with unit intensities)\n vec3 toLight = pr.xyz - vWorldPos;\n float dist = max(length(toLight), 1e-4);\n L = toLight / dist;\n float linearAtt = max(0.0, 1.0 - dist / pr.w);\n atten = linearAtt * linearAtt;\n if (dc.w > -1.0) {\n // spot cone: -L is the light->surface direction; fade from\n // the inner cone cosine to the outer\n float cd = dot(-L, dc.xyz);\n atten *= smoothstep(dc.w, uLights[i].colorInner.w, cd);\n }\n }\n // Half-Lambert (\"wrap\") diffuse: dot * 0.5 + 0.5. Softens the\n // terminator and lifts the shadowed side, for a gentler, more\n // diffuse look than hard Lambert (which reads as harsh noon).\n float ndl = dot(N, L) * 0.5 + 0.5;\n lit += uLights[i].colorInner.rgb * (ndl * ndl * atten);\n if (hasSpecular) {\n // masked by the UNWRAPPED Lambert term: half-Lambert lifts the\n // shadowed side, and a highlight on a surface facing away from\n // the light reads as a rendering error\n float facing = max(dot(N, L), 0.0);\n vec3 H = normalize(L + V);\n float spec = pow(max(dot(N, H), 0.0), uShininess);\n specular += uLights[i].colorInner.rgb * (spec * facing * atten);\n }\n }\n\n // emissive self-illuminates: added AFTER lighting so it glows at full\n // strength regardless of the scene lights (neon, lava, glowing eyes).\n vec3 emissive = uEmissive;\n#ifdef INSTANCE_DATA\n emissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n fragColor = vec4(\n applyFog(base.rgb * lit + specular * uSpecular + emissive, base.a),\n base.a\n );\n#else\n fragColor = vec4(base.rgb * lit + specular * uSpecular + emissive, base.a);\n#endif\n}\n";
|
|
40548
41266
|
|
|
40549
41267
|
// src/video/webgl/shaders/mesh-lit.vert
|
|
40550
41268
|
var mesh_lit_default2 = "#version 300 es\n// Lit mesh vertex shader (Camera3d + Light3d) \u2014 retained-mode.\n//\n// GLSL ES 3.00 because the fragment stage reads light data from a uniform\n// block, which does not exist in ES 1.00. Both stages of a program must share\n// a dialect, so the vertex shader follows.\n//\n// Same placement as mesh.vert (clip = projection \xD7 view \xD7 model \xD7 vertex,\n// vertices in MODEL space) plus a normal carried to the fragment shader in\n// world space for diffuse shading.\nin vec3 aVertex;\nin vec2 aRegion;\nin vec4 aColor;\n// Model-space normal, rotated into world space below.\nin vec3 aNormal;\n\nuniform mat4 uProjectionMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uModelMatrix;\nuniform vec4 uTint;\n\nout vec2 vRegion;\nout vec4 vColor;\nout vec3 vNormal;\n// world-space fragment position \u2014 positional lights (point / spot) fall\n// off with distance, so the fragment stage needs where the surface IS\nout vec3 vWorldPos;\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n#ifdef FOG\nuniform vec4 uFogHeight; // xyz = falloff x world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked\n\n// How much the height falloff scales the fog along this view ray.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the whole ray costs one `exp` and\n// no marching. The result multiplies the distance, which leaves both fog\n// curves exactly as they are.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `uModelMatrix * position` \u2014 and that is the mesh's parent\n// space rather than the world, because `Container.draw` folds every ancestor\n// into the view matrix. Reading a height straight off it put the fog floor at\n// the wrong altitude for anything under a scaled container. Dotting the\n// view-space position against the world-up axis expressed in view space gives\n// the height above the camera whatever those ancestors did, since they are\n// inside the very matrix that produced this position. `uFogHeight.xyz`\n// carries the falloff too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite\n// sign to every published form of this. The axis is signed that way round,\n// and there is a test that catches it being flipped.\n//\n// A falloff of 0 gives exactly 1: the axis is all zero, so `kdy` is 0, the\n// series limit is taken, and the pre-baked term is exp(0) \u2014 uniform fog is\n// not a special case, it is this with the dial at zero.\nfloat fogHeightFactor(vec3 viewPos) {\n float kdy = dot(uFogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, and a horizontal view ray \u2014 looking\n // straight across a valley \u2014 is exactly that case. Take the limit rather\n // than guarding, or the fog steps as the ray approaches horizontal.\n float t = abs(kdy) < 1e-4 ? 1.0 : (exp(kdy) - 1.0) / kdy;\n return uFogHeight.w * t;\n}\n#endif\n\nvoid main(void) {\n vec4 worldPos = uModelMatrix * vec4(aVertex, 1.0);\n gl_Position = uProjectionMatrix * uViewMatrix * worldPos;\n vWorldPos = worldPos.xyz;\n#ifdef FOG\n // Radial view-space distance for distance fog. Radial rather than view-space\n // z, so fog holds steady as the camera turns instead of sliding across the\n // scene. The clip position above keeps its own product: re-associating it\n // could shift vertices by an ulp, and a scene without fog must be unchanged.\n vec3 viewPos = (uViewMatrix * worldPos).xyz;\n vFogDepth = length(viewPos) * fogHeightFactor(viewPos);\n#endif\n vec4 tinted = aColor * uTint;\n vColor = vec4(tinted.rgb * tinted.a, tinted.a);\n vRegion = aRegion;\n // Rotate the normal into world space with the model matrix's upper 3\xD73.\n // Lighting is evaluated in world space, so the view transform is\n // deliberately excluded. Uniform scale cancels when the fragment shader\n // renormalizes; non-uniform scale is approximated \u2014 unchanged behaviour,\n // an exact result would need the inverse-transpose.\n vNormal = mat3(uModelMatrix) * aNormal;\n}\n";
|
|
@@ -40744,7 +41462,7 @@ function vertexDataExceedsUint16(indices) {
|
|
|
40744
41462
|
}
|
|
40745
41463
|
|
|
40746
41464
|
// src/video/webgl/shaders/mesh.frag
|
|
40747
|
-
var mesh_default = "uniform sampler2D uSampler;\nuniform float uAlphaCutoff; // alpha cutout threshold (0 = disabled)\nuniform vec3 uEmissive; // self-illumination color added on top (0 = none)\nuniform sampler2D uAlphaMap; // per-texel opacity (MTL map_d)\nuniform float uHasAlphaMap; // 0 = uAlphaMap is filler, ignore it\nvarying vec4 vColor;\nvarying vec2 vRegion;\n// Distance fog is a COMPILED VARIANT, not a runtime branch: the batcher\n// injects `#define FOG` only while a camera has fog enabled. A software\n// rasterizer predicates both sides of a branch, so an `exp()` guarded at\n// runtime still costs every fragment of every scene \u2014 including the scenes\n// that never asked for fog.\n#ifdef FOG\nuniform vec3 uFogColor; // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams; // x = mode (0 off / 1 linear / 2 exp2),\n // y = near, z = 1/(far - near), w = density\nvarying float vFogDepth; // radial view-space distance, interpolated\n\n// Fold distance fog into a PREMULTIPLIED colour.\n//\n// `rgb` here is already multiplied by `a` (the vertex stage premultiplies\n// vColor), so the fog colour has to be scaled by the fragment's own coverage.\n// A plain mix toward uFogColor would paint full-strength fog onto\n// near-transparent fragments \u2014 grey halos around every alpha-cutout leaf, and\n// unattenuated fog added on the blended shadow pass.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nvec3 applyFog(vec3 rgb, float a) {\n float mode = uFogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n float f; // fraction of the scene colour that survives\n if (mode < 1.5) {\n // linear: 1 at `near`, reaching 0 at `far`\n f = 1.0 - clamp((vFogDepth - uFogParams.y) * uFogParams.z, 0.0, 1.0);\n } else {\n // exponential squared: survival = exp(-(density * d)^2)\n float dd = vFogDepth * uFogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uFogColor * a, rgb, f);\n}\n#endif\n\n\n#ifdef INSTANCE_DATA\n// per-instance custom slot. The built-in shading reads its rgb as emissive,\n// so a forest can glow per tree without a uniform per instance; a CUSTOM mesh\n// shader is free to read the same slot as anything else entirely.\nvarying vec4 vInstanceData;\n#endif\n\nvoid main(void) {\n vec4 texel = texture2D(uSampler, vRegion);\n // per-texel opacity (MTL map_d) multiplies in BEFORE the cutout, so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Red channel: the format stores a\n // greyscale map, and every channel carries the same value.\n // Sampled unconditionally and WEIGHTED rather than branched: when no map\n // is bound the second sampler is filler (the diffuse texture), so the\n // value is thrown away \u2014 and both backends run the identical expression\n // instead of one branching and the other not.\n texel.a *= mix(1.0, texture2D(uAlphaMap, vRegion).r, uHasAlphaMap);\n // hard alpha cutout (glTF alphaMode MASK): drop fully-transparent texels\n // so foliage / fences / decals read crisp without blending or sorting.\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply. The cutout describes the shape of the surface, and that shape\n // does not change when the object fades: testing the drawn alpha instead\n // makes a cutout mesh vanish completely the moment its opacity crosses its\n // own threshold \u2014 and `Sprite3d` defaults that threshold to 0.5, so half a\n // fade-out was a hard pop.\n if (texel.a < uAlphaCutoff) {\n discard;\n }\n vec4 color = texel * vColor;\n // emissive adds a self-lit color on top (neon, lava, screens); the unlit\n // path has no lighting, so it's simply added to the base color.\n vec3 emissive = uEmissive;\n#ifdef INSTANCE_DATA\n emissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n gl_FragColor = vec4(applyFog(color.rgb + emissive, color.a), color.a);\n#else\n gl_FragColor = vec4(color.rgb + emissive, color.a);\n#endif\n}\n";
|
|
41465
|
+
var mesh_default = "uniform sampler2D uSampler;\nuniform float uAlphaCutoff; // alpha cutout threshold (0 = disabled)\nuniform vec3 uEmissive; // self-illumination color added on top (0 = none)\nuniform sampler2D uAlphaMap; // per-texel opacity (MTL map_d)\nuniform float uHasAlphaMap; // 0 = uAlphaMap is filler, ignore it\nvarying vec4 vColor;\nvarying vec2 vRegion;\n// Distance fog is a COMPILED VARIANT, not a runtime branch: the batcher\n// injects `#define FOG` only while a camera has fog enabled. A software\n// rasterizer predicates both sides of a branch, so an `exp()` guarded at\n// runtime still costs every fragment of every scene \u2014 including the scenes\n// that never asked for fog.\n#ifdef FOG\nuniform vec3 uFogColor; // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams; // x = mode (0 off / 1 linear / 2 exp2),\n // y = near, z = 1/(far - near), w = density\nvarying float vFogDepth; // radial view-space distance, interpolated\n\n// Fold distance fog into a PREMULTIPLIED colour.\n//\n// `rgb` here is already multiplied by `a` (the vertex stage premultiplies\n// vColor), so the fog colour has to be scaled by the fragment's own coverage.\n// A plain mix toward uFogColor would paint full-strength fog onto\n// near-transparent fragments \u2014 grey halos around every alpha-cutout leaf, and\n// unattenuated fog added on the blended shadow pass.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nvec3 applyFog(vec3 rgb, float a) {\n float mode = uFogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n float f; // fraction of the scene colour that survives\n if (mode < 1.5) {\n // linear: 1 at `near`, reaching 0 at `far`\n f = 1.0 - clamp((vFogDepth - uFogParams.y) * uFogParams.z, 0.0, 1.0);\n } else {\n // exponential squared: survival = exp(-(density * d)^2)\n float dd = vFogDepth * uFogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uFogColor * a, rgb, f);\n}\n#endif\n\n\n#ifdef INSTANCE_DATA\n// per-instance custom slot. The built-in shading reads its rgb as emissive,\n// so a forest can glow per tree without a uniform per instance; a CUSTOM mesh\n// shader is free to read the same slot as anything else entirely.\nvarying vec4 vInstanceData;\n#endif\n\n// The mesh-hosted ShaderEffect hook (#1658). Identity by default \u2014 every\n// compiler inlines it away, so an un-effected mesh pays nothing. When an\n// effect is hosted, `spliceEffect` replaces this definition with one that\n// calls the effect's `apply()`, and pastes the effect body above it.\n//\n// Deliberately a FUNCTION and not a marker comment: the shader pipeline\n// strips comments, so a comment cannot be relied on to reach the splicer.\nvec4 ME_effect(vec4 c, vec2 uv) { return c; }\n\nvoid main(void) {\n vec4 texel = texture2D(uSampler, vRegion);\n // per-texel opacity (MTL map_d) multiplies in BEFORE the cutout, so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Red channel: the format stores a\n // greyscale map, and every channel carries the same value.\n // Sampled unconditionally and WEIGHTED rather than branched: when no map\n // is bound the second sampler is filler (the diffuse texture), so the\n // value is thrown away \u2014 and both backends run the identical expression\n // instead of one branching and the other not.\n texel.a *= mix(1.0, texture2D(uAlphaMap, vRegion).r, uHasAlphaMap);\n // hard alpha cutout (glTF alphaMode MASK): drop fully-transparent texels\n // so foliage / fences / decals read crisp without blending or sorting.\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply. The cutout describes the shape of the surface, and that shape\n // does not change when the object fades: testing the drawn alpha instead\n // makes a cutout mesh vanish completely the moment its opacity crosses its\n // own threshold \u2014 and `Sprite3d` defaults that threshold to 0.5, so half a\n // fade-out was a hard pop.\n if (texel.a < uAlphaCutoff) {\n discard;\n }\n vec4 color = texel * vColor;\n color = ME_effect(color, vRegion);\n // emissive adds a self-lit color on top (neon, lava, screens); the unlit\n // path has no lighting, so it's simply added to the base color.\n vec3 emissive = uEmissive;\n#ifdef INSTANCE_DATA\n emissive += vInstanceData.rgb;\n#endif\n#ifdef FOG\n gl_FragColor = vec4(applyFog(color.rgb + emissive, color.a), color.a);\n#else\n gl_FragColor = vec4(color.rgb + emissive, color.a);\n#endif\n}\n";
|
|
40748
41466
|
|
|
40749
41467
|
// src/video/webgl/shaders/mesh.vert
|
|
40750
41468
|
var mesh_default2 = "// Mesh vertex shader (retained-mode).\n//\n// Vertices arrive in MODEL space and are placed by uniforms rather than by\n// CPU-side transformation, so moving/rotating/scaling a mesh costs a uniform\n// update instead of re-uploading its geometry:\n//\n// clip = projection \xD7 view \xD7 model \xD7 vertex\n//\n// `uModelMatrix` carries the mesh's own transform (including the Y/Z axis\n// bridge and mesh scale); `uViewMatrix` carries the camera view plus any\n// ancestor container transform. Both are always written before a draw \u2014 the\n// accumulation path (2D camera, vertices pre-projected on the CPU) pushes\n// identity explicitly. Note GLSL uniforms default to ZERO, not identity, so a\n// path that reached this shader without setting them would collapse every\n// vertex onto the origin rather than drawing untransformed.\nattribute vec3 aVertex;\nattribute vec2 aRegion;\nattribute vec4 aColor;\n\nuniform mat4 uProjectionMatrix;\nuniform mat4 uViewMatrix;\nuniform mat4 uModelMatrix;\n// Per-draw tint \xD7 global alpha. Kept out of the vertex data so that changing\n// a mesh's tint never invalidates its retained geometry.\nuniform vec4 uTint;\n\nvarying vec2 vRegion;\nvarying vec4 vColor;\n#ifdef FOG\nvarying float vFogDepth;\n#endif\n\n#ifdef FOG\nuniform vec4 uFogHeight; // xyz = falloff x world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked\n\n// How much the height falloff scales the fog along this view ray.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the whole ray costs one `exp` and\n// no marching. The result multiplies the distance, which leaves both fog\n// curves exactly as they are.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `uModelMatrix * position` \u2014 and that is the mesh's parent\n// space rather than the world, because `Container.draw` folds every ancestor\n// into the view matrix. Reading a height straight off it put the fog floor at\n// the wrong altitude for anything under a scaled container. Dotting the\n// view-space position against the world-up axis expressed in view space gives\n// the height above the camera whatever those ancestors did, since they are\n// inside the very matrix that produced this position. `uFogHeight.xyz`\n// carries the falloff too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite\n// sign to every published form of this. The axis is signed that way round,\n// and there is a test that catches it being flipped.\n//\n// A falloff of 0 gives exactly 1: the axis is all zero, so `kdy` is 0, the\n// series limit is taken, and the pre-baked term is exp(0) \u2014 uniform fog is\n// not a special case, it is this with the dial at zero.\nfloat fogHeightFactor(vec3 viewPos) {\n float kdy = dot(uFogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, and a horizontal view ray \u2014 looking\n // straight across a valley \u2014 is exactly that case. Take the limit rather\n // than guarding, or the fog steps as the ray approaches horizontal.\n float t = abs(kdy) < 1e-4 ? 1.0 : (exp(kdy) - 1.0) / kdy;\n return uFogHeight.w * t;\n}\n#endif\n\nvoid main(void) {\n gl_Position =\n uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertex, 1.0);\n#ifdef FOG\n // Radial view-space distance for distance fog. Radial rather than view-space\n // z, so fog holds steady as the camera turns instead of sliding across the\n // scene. The clip position above keeps its own product: re-associating it\n // could shift vertices by an ulp, and a scene without fog must be unchanged.\n vec4 viewPos = uViewMatrix * uModelMatrix * vec4(aVertex, 1.0);\n vFogDepth = length(viewPos.xyz) * fogHeightFactor(viewPos.xyz);\n#endif\n // tint first, then premultiply \u2014 matches the fragment shader's expectation\n vec4 tinted = aColor * uTint;\n vColor = vec4(tinted.rgb * tinted.a, tinted.a);\n vRegion = aRegion;\n}\n";
|
|
@@ -41314,6 +42032,99 @@ var MeshBatcher = class extends MaterialBatcher {
|
|
|
41314
42032
|
fragmentDefines
|
|
41315
42033
|
);
|
|
41316
42034
|
}
|
|
42035
|
+
/**
|
|
42036
|
+
* The program that hosts a `ShaderEffect` on a mesh (#1658).
|
|
42037
|
+
*
|
|
42038
|
+
* A ShaderEffect is realized against the QUAD contract — its vertex stage
|
|
42039
|
+
* projects straight from `uProjectionMatrix` and declares neither
|
|
42040
|
+
* `uModelMatrix` nor `uViewMatrix` — so it cannot be bound to a mesh as-is:
|
|
42041
|
+
* the geometry arrives in model space and would draw unplaced and without
|
|
42042
|
+
* the camera (the very case `validateShaderLocations` warns about). Instead
|
|
42043
|
+
* the body is spliced into THIS batcher's own mesh shader, so the mesh keeps
|
|
42044
|
+
* its placement, alpha cutout, lighting and fog and the effect becomes a
|
|
42045
|
+
* colour hook over the result.
|
|
42046
|
+
*
|
|
42047
|
+
* Keyed through the same variant map as every other permutation, so it is
|
|
42048
|
+
* released on context loss and destroy along with the rest — and keyed on
|
|
42049
|
+
* fog too, since the spliced source inherits the fog `#define`.
|
|
42050
|
+
* @param {object} effect - the hosted ShaderEffect
|
|
42051
|
+
* @returns {GLShader} the spliced program
|
|
42052
|
+
* @ignore
|
|
42053
|
+
* @internal
|
|
42054
|
+
*/
|
|
42055
|
+
effectShaderFor(effect) {
|
|
42056
|
+
const fog = this._fogDefine();
|
|
42057
|
+
const sources = this._shaderSources();
|
|
42058
|
+
const fragment2 = spliceEffect(sources.fragment, effect._meshBody);
|
|
42059
|
+
if (fragment2 === null) {
|
|
42060
|
+
return null;
|
|
42061
|
+
}
|
|
42062
|
+
const compat = /^\s*#version\s+300\s+es/m.test(sources.fragment) ? "#define texture2D texture\n" : "";
|
|
42063
|
+
try {
|
|
42064
|
+
return this.shaderVariant(
|
|
42065
|
+
`effect|${effect._effectId}|${fog !== "" ? 1 : 0}`,
|
|
42066
|
+
{ vertex: sources.vertex, fragment: fragment2 },
|
|
42067
|
+
fog,
|
|
42068
|
+
fog + compat
|
|
42069
|
+
);
|
|
42070
|
+
} catch (error) {
|
|
42071
|
+
if (this._meshEffectCompileWarned !== true) {
|
|
42072
|
+
this._meshEffectCompileWarned = true;
|
|
42073
|
+
console.warn(
|
|
42074
|
+
`melonJS: this ShaderEffect failed to compile against the mesh shader \u2014 ${error instanceof Error ? error.message : String(error)}. The mesh draws with the built-in shading.`
|
|
42075
|
+
);
|
|
42076
|
+
}
|
|
42077
|
+
return null;
|
|
42078
|
+
}
|
|
42079
|
+
}
|
|
42080
|
+
/**
|
|
42081
|
+
* Bring a spliced program up to date with the effect's uniforms.
|
|
42082
|
+
*
|
|
42083
|
+
* `ShaderEffect.setUniform` writes into the effect's OWN quad program;
|
|
42084
|
+
* this one is a different program, so every value the effect has ever been
|
|
42085
|
+
* given is replayed onto it. Unknown names are skipped rather than passed
|
|
42086
|
+
* on: `GLShader.setUniform` throws on one, and a body is free to declare
|
|
42087
|
+
* uniforms the spliced mesh source optimised away.
|
|
42088
|
+
* @param {object} effect - the hosted ShaderEffect
|
|
42089
|
+
* @param {GLShader} shader - the spliced program
|
|
42090
|
+
* @ignore
|
|
42091
|
+
* @internal
|
|
42092
|
+
*/
|
|
42093
|
+
replayEffectUniforms(effect, shader) {
|
|
42094
|
+
for (const [name, value] of effect._uniformValues) {
|
|
42095
|
+
if (shader.uniforms[name] !== void 0) {
|
|
42096
|
+
shader.setUniform(name, value);
|
|
42097
|
+
}
|
|
42098
|
+
}
|
|
42099
|
+
}
|
|
42100
|
+
/**
|
|
42101
|
+
* Whether this batcher can host `effect`, warning once when it cannot.
|
|
42102
|
+
* @param {object} effect - the candidate ShaderEffect
|
|
42103
|
+
* @returns {boolean} true when the effect can be spliced in
|
|
42104
|
+
* @ignore
|
|
42105
|
+
* @internal
|
|
42106
|
+
*/
|
|
42107
|
+
canHostEffect(effect) {
|
|
42108
|
+
if (typeof effect._meshBody !== "string") {
|
|
42109
|
+
return false;
|
|
42110
|
+
}
|
|
42111
|
+
if (!hasSpliceMarkers(this._shaderSources().fragment)) {
|
|
42112
|
+
return false;
|
|
42113
|
+
}
|
|
42114
|
+
const blocker = meshHostingBlocker(effect._meshBuiltins, [
|
|
42115
|
+
...effect._extraTextures?.keys() ?? []
|
|
42116
|
+
]);
|
|
42117
|
+
if (blocker === null) {
|
|
42118
|
+
return true;
|
|
42119
|
+
}
|
|
42120
|
+
if (this._meshEffectBlockedWarned !== true) {
|
|
42121
|
+
this._meshEffectBlockedWarned = true;
|
|
42122
|
+
console.warn(
|
|
42123
|
+
`melonJS: this ShaderEffect cannot be hosted on a Mesh \u2014 ${blocker}. The mesh draws with the built-in shading.`
|
|
42124
|
+
);
|
|
42125
|
+
}
|
|
42126
|
+
return false;
|
|
42127
|
+
}
|
|
41317
42128
|
/**
|
|
41318
42129
|
* `"#define FOG\n"` while the camera drawing has fog, `""` otherwise.
|
|
41319
42130
|
*
|
|
@@ -41359,6 +42170,71 @@ var MeshBatcher = class extends MaterialBatcher {
|
|
|
41359
42170
|
}
|
|
41360
42171
|
return shader;
|
|
41361
42172
|
}
|
|
42173
|
+
/**
|
|
42174
|
+
* Build every shader variant this batcher can reach, so none of them is
|
|
42175
|
+
* compiled on the frame a scene first draws.
|
|
42176
|
+
*
|
|
42177
|
+
* The set is enumerable because each variant is a source permutation, not
|
|
42178
|
+
* a runtime branch: fog on or off, and the three instance-record slots.
|
|
42179
|
+
* That is 1 fogged mesh + 8 instanced + 2 ground-shadow programs; the
|
|
42180
|
+
* plain mesh program is built in the constructor and is already here.
|
|
42181
|
+
*
|
|
42182
|
+
* Yields between programs. Linking is synchronous in WebGL and a dozen
|
|
42183
|
+
* links in one task is a dropped frame — which on a loading screen is a
|
|
42184
|
+
* stuttering progress bar, trading one visible hitch for another.
|
|
42185
|
+
* @returns {Promise<void>} settles once every variant is linked
|
|
42186
|
+
* @ignore
|
|
42187
|
+
* @internal
|
|
42188
|
+
*/
|
|
42189
|
+
async prewarm() {
|
|
42190
|
+
const yieldToFrame = () => {
|
|
42191
|
+
if (globalThis.document?.hidden === true) {
|
|
42192
|
+
return Promise.resolve();
|
|
42193
|
+
}
|
|
42194
|
+
return new Promise((resolve) => {
|
|
42195
|
+
setTimeout(resolve, 0);
|
|
42196
|
+
});
|
|
42197
|
+
};
|
|
42198
|
+
const alive = () => {
|
|
42199
|
+
return this.gl !== null && this.gl?.isContextLost?.() !== true;
|
|
42200
|
+
};
|
|
42201
|
+
const sources = this._shaderSources();
|
|
42202
|
+
const instanced = this._instancedShaderSources();
|
|
42203
|
+
for (const fogDefine of ["", "#define FOG\n"]) {
|
|
42204
|
+
if (fogDefine !== "") {
|
|
42205
|
+
this.shaderVariant("mesh|fog", sources, fogDefine, fogDefine);
|
|
42206
|
+
await yieldToFrame();
|
|
42207
|
+
if (!alive()) {
|
|
42208
|
+
return;
|
|
42209
|
+
}
|
|
42210
|
+
}
|
|
42211
|
+
for (const hasColor of [false, true]) {
|
|
42212
|
+
for (const hasData of [false, true]) {
|
|
42213
|
+
const key = (hasColor ? 1 : 0) | (hasData ? 2 : 0) | (fogDefine !== "" ? 4 : 0);
|
|
42214
|
+
this.shaderVariant(
|
|
42215
|
+
`instanced|${key}`,
|
|
42216
|
+
instanced,
|
|
42217
|
+
(hasColor ? "#define INSTANCE_COLORS\n" : "") + (hasData ? "#define INSTANCE_DATA\n" : "") + fogDefine,
|
|
42218
|
+
(hasData ? "#define INSTANCE_DATA\n" : "") + fogDefine
|
|
42219
|
+
);
|
|
42220
|
+
await yieldToFrame();
|
|
42221
|
+
if (!alive()) {
|
|
42222
|
+
return;
|
|
42223
|
+
}
|
|
42224
|
+
}
|
|
42225
|
+
}
|
|
42226
|
+
this.shaderVariant(
|
|
42227
|
+
fogDefine === "" ? "shadow" : "shadow|fog",
|
|
42228
|
+
{ vertex: mesh_shadow_instanced_default, fragment: mesh_default },
|
|
42229
|
+
fogDefine,
|
|
42230
|
+
fogDefine
|
|
42231
|
+
);
|
|
42232
|
+
await yieldToFrame();
|
|
42233
|
+
if (!alive()) {
|
|
42234
|
+
return;
|
|
42235
|
+
}
|
|
42236
|
+
}
|
|
42237
|
+
}
|
|
41362
42238
|
/**
|
|
41363
42239
|
* Whether the bound program is one this batcher would pick for a plain
|
|
41364
42240
|
* mesh — its own, or the fog variant of its own.
|
|
@@ -43632,6 +44508,7 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
|
|
|
43632
44508
|
this.onContextRestoredInvalidate = (renderer) => {
|
|
43633
44509
|
if (renderer === this) {
|
|
43634
44510
|
this.currentProgram = void 0;
|
|
44511
|
+
this._prewarmed = void 0;
|
|
43635
44512
|
}
|
|
43636
44513
|
};
|
|
43637
44514
|
this.onCanvasResize = (width, height) => {
|
|
@@ -43715,6 +44592,29 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
|
|
|
43715
44592
|
batcher.releaseRetained?.(mesh);
|
|
43716
44593
|
});
|
|
43717
44594
|
}
|
|
44595
|
+
/**
|
|
44596
|
+
* Compile every shader variant the built-in batchers can reach.
|
|
44597
|
+
*
|
|
44598
|
+
* A WebGL batcher builds its own program in its constructor, so the base
|
|
44599
|
+
* programs are already linked by the time `init()` resolves. What is not
|
|
44600
|
+
* is the mesh batcher's permutations — fog, and the three instance-record
|
|
44601
|
+
* slots — which are built on first use and so land on the frame a 3D
|
|
44602
|
+
* scene first draws.
|
|
44603
|
+
* @returns {Promise<void>} settles once every variant is linked
|
|
44604
|
+
* @see {@link Renderer#prewarm}
|
|
44605
|
+
*/
|
|
44606
|
+
prewarm() {
|
|
44607
|
+
if (this._prewarmed === void 0) {
|
|
44608
|
+
this._prewarmed = Promise.all(
|
|
44609
|
+
[...this.batchers.values()].map((batcher) => {
|
|
44610
|
+
return batcher.prewarm();
|
|
44611
|
+
})
|
|
44612
|
+
).then(() => {
|
|
44613
|
+
this.setBatcher("quad");
|
|
44614
|
+
});
|
|
44615
|
+
}
|
|
44616
|
+
return this._prewarmed;
|
|
44617
|
+
}
|
|
43718
44618
|
destroy() {
|
|
43719
44619
|
off(GAME_RESET, this.onGameReset);
|
|
43720
44620
|
off(ONCONTEXT_RESTORED, this.onContextRestoredInvalidate);
|
|
@@ -44841,8 +45741,14 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
|
|
|
44841
45741
|
return;
|
|
44842
45742
|
}
|
|
44843
45743
|
this.setBatcher(mesh.lit === true ? "litMesh" : "mesh");
|
|
44844
|
-
const
|
|
45744
|
+
const effectHosted = this.customShader != null && this.currentBatcher.canHostEffect?.(this.customShader) === true;
|
|
45745
|
+
const spliced = effectHosted ? this.currentBatcher.effectShaderFor(this.customShader) : null;
|
|
45746
|
+
let hostedShader = spliced !== null && spliced !== void 0;
|
|
44845
45747
|
if (hostedShader) {
|
|
45748
|
+
this.currentBatcher.useShader(spliced);
|
|
45749
|
+
this.currentBatcher.replayEffectUniforms(this.customShader, spliced);
|
|
45750
|
+
} else if (this.customShader != null && this.customShader.program != null) {
|
|
45751
|
+
hostedShader = true;
|
|
44846
45752
|
this.currentBatcher.useShader(this.customShader);
|
|
44847
45753
|
} else if (this.customShader != null && this.customShader.isWebGL !== true && this._meshShaderWarned !== true) {
|
|
44848
45754
|
this._meshShaderWarned = true;
|
|
@@ -46598,8 +47504,8 @@ var Pointer = class extends Bounds {
|
|
|
46598
47504
|
this.isPrimary = typeof event.isPrimary !== "undefined" ? event.isPrimary : true;
|
|
46599
47505
|
this.button = event.button || 0;
|
|
46600
47506
|
this.type = event.type;
|
|
46601
|
-
if (typeof
|
|
46602
|
-
|
|
47507
|
+
if (typeof _app3?.viewport !== "undefined") {
|
|
47508
|
+
_app3.viewport.localToWorld(this.gameScreenX, this.gameScreenY, tmpVec);
|
|
46603
47509
|
}
|
|
46604
47510
|
this.gameWorldX = tmpVec.x;
|
|
46605
47511
|
this.gameWorldY = tmpVec.y;
|
|
@@ -46620,9 +47526,9 @@ var pointer_default = Pointer;
|
|
|
46620
47526
|
var T_POINTERS = [];
|
|
46621
47527
|
var eventHandlers = /* @__PURE__ */ new Map();
|
|
46622
47528
|
var currentPointer;
|
|
46623
|
-
var
|
|
47529
|
+
var _app3;
|
|
46624
47530
|
on(GAME_INIT, (app) => {
|
|
46625
|
-
|
|
47531
|
+
_app3 = app;
|
|
46626
47532
|
});
|
|
46627
47533
|
var pointerInitialized = false;
|
|
46628
47534
|
var lastTimeStamp = 0;
|
|
@@ -46686,7 +47592,7 @@ function registerEventListener(eventList, callback) {
|
|
|
46686
47592
|
}
|
|
46687
47593
|
function enablePointerEvent() {
|
|
46688
47594
|
if (!pointerInitialized) {
|
|
46689
|
-
if (!
|
|
47595
|
+
if (!_app3) {
|
|
46690
47596
|
throw new Error("Pointer events require an initialized Application");
|
|
46691
47597
|
}
|
|
46692
47598
|
currentPointer = new Rect(0, 0, 1, 1);
|
|
@@ -46694,7 +47600,7 @@ function enablePointerEvent() {
|
|
|
46694
47600
|
T_POINTERS.push(new pointer_default());
|
|
46695
47601
|
}
|
|
46696
47602
|
if (pointerEventTarget === null || pointerEventTarget === void 0) {
|
|
46697
|
-
pointerEventTarget =
|
|
47603
|
+
pointerEventTarget = _app3.renderer.getCanvas();
|
|
46698
47604
|
}
|
|
46699
47605
|
if (pointerEvent) {
|
|
46700
47606
|
activeEventList = pointerEventList;
|
|
@@ -46748,7 +47654,7 @@ function enablePointerEvent() {
|
|
|
46748
47654
|
globalThis.document.addEventListener(
|
|
46749
47655
|
POINTERLOCKCHANGE,
|
|
46750
47656
|
() => {
|
|
46751
|
-
locked = globalThis.document.pointerLockElement ===
|
|
47657
|
+
locked = globalThis.document.pointerLockElement === _app3.getParentElement();
|
|
46752
47658
|
emit(POINTERLOCKCHANGE, locked);
|
|
46753
47659
|
},
|
|
46754
47660
|
true
|
|
@@ -46798,7 +47704,7 @@ function dispatchEvent(normalizedEvents2) {
|
|
|
46798
47704
|
}
|
|
46799
47705
|
lastTimeStamp = pointer2.event.timeStamp;
|
|
46800
47706
|
}
|
|
46801
|
-
const worldOffset =
|
|
47707
|
+
const worldOffset = _app3.world.pos;
|
|
46802
47708
|
const absoluteWorldX = pointer2.gameWorldX + worldOffset.x;
|
|
46803
47709
|
const absoluteWorldY = pointer2.gameWorldY + worldOffset.y;
|
|
46804
47710
|
currentPointer.pos.set(pointer2.gameWorldX, pointer2.gameWorldY);
|
|
@@ -46808,16 +47714,16 @@ function dispatchEvent(normalizedEvents2) {
|
|
|
46808
47714
|
pointer2.gameY = pointer2.gameLocalY = pointer2.gameScreenY;
|
|
46809
47715
|
emit(POINTERMOVE, pointer2);
|
|
46810
47716
|
}
|
|
46811
|
-
let candidates =
|
|
47717
|
+
let candidates = _app3.world.broadphase.retrieve(
|
|
46812
47718
|
currentPointer,
|
|
46813
|
-
(a, b) =>
|
|
47719
|
+
(a, b) => _app3.world._sortReverseZ(a, b),
|
|
46814
47720
|
void 0
|
|
46815
47721
|
);
|
|
46816
47722
|
if (worldOffset.x !== 0 || worldOffset.y !== 0) {
|
|
46817
47723
|
currentPointer.pos.set(absoluteWorldX, absoluteWorldY);
|
|
46818
|
-
const absoluteCandidates =
|
|
47724
|
+
const absoluteCandidates = _app3.world.broadphase.retrieve(
|
|
46819
47725
|
currentPointer,
|
|
46820
|
-
(a, b) =>
|
|
47726
|
+
(a, b) => _app3.world._sortReverseZ(a, b),
|
|
46821
47727
|
void 0
|
|
46822
47728
|
);
|
|
46823
47729
|
for (const candidate of candidates) {
|
|
@@ -46826,10 +47732,10 @@ function dispatchEvent(normalizedEvents2) {
|
|
|
46826
47732
|
}
|
|
46827
47733
|
}
|
|
46828
47734
|
candidates = absoluteCandidates.sort(
|
|
46829
|
-
(a, b) =>
|
|
47735
|
+
(a, b) => _app3.world._sortReverseZ(a, b)
|
|
46830
47736
|
);
|
|
46831
47737
|
}
|
|
46832
|
-
candidates = candidates.concat([
|
|
47738
|
+
candidates = candidates.concat([_app3.viewport]);
|
|
46833
47739
|
for (let c = candidates.length, candidate; c--, candidate = candidates[c]; ) {
|
|
46834
47740
|
if (eventHandlers.has(candidate) && candidate.isKinematic !== true) {
|
|
46835
47741
|
const handlers = eventHandlers.get(candidate);
|
|
@@ -46992,11 +47898,11 @@ function hasRegisteredEvents() {
|
|
|
46992
47898
|
}
|
|
46993
47899
|
function globalToLocal(x, y, v) {
|
|
46994
47900
|
v = v || vector2dPool.get();
|
|
46995
|
-
const rect = getElementBounds(
|
|
47901
|
+
const rect = getElementBounds(_app3.renderer.getCanvas());
|
|
46996
47902
|
const pixelRatio = globalThis.devicePixelRatio || 1;
|
|
46997
47903
|
x -= rect.left + (globalThis.pageXOffset || 0);
|
|
46998
47904
|
y -= rect.top + (globalThis.pageYOffset || 0);
|
|
46999
|
-
const scale2 =
|
|
47905
|
+
const scale2 = _app3.renderer.scaleRatio;
|
|
47000
47906
|
if (scale2.x !== 1 || scale2.y !== 1) {
|
|
47001
47907
|
x /= scale2.x;
|
|
47002
47908
|
y /= scale2.y;
|
|
@@ -47088,7 +47994,7 @@ function releaseAllPointerEvents(region) {
|
|
|
47088
47994
|
}
|
|
47089
47995
|
function requestPointerLock() {
|
|
47090
47996
|
if (hasPointerLockSupport) {
|
|
47091
|
-
const element =
|
|
47997
|
+
const element = _app3.getParentElement();
|
|
47092
47998
|
void element.requestPointerLock();
|
|
47093
47999
|
return true;
|
|
47094
48000
|
}
|
|
@@ -48408,7 +49314,7 @@ function _computeShapeAbsPos(renderable, shape, outPos) {
|
|
|
48408
49314
|
var _shapeAbs = new Vector2d(0, 0);
|
|
48409
49315
|
function _raycastPolygon(fromX, fromY, dx, dy, shape, shapeAbsX, shapeAbsY) {
|
|
48410
49316
|
const points = shape.points;
|
|
48411
|
-
const normals = shape.
|
|
49317
|
+
const normals = shape.edgeNormals;
|
|
48412
49318
|
const len = points.length;
|
|
48413
49319
|
let bestT = Infinity;
|
|
48414
49320
|
let bestNormalIdx = -1;
|
|
@@ -48649,10 +49555,10 @@ function vornoiRegion(line, point) {
|
|
|
48649
49555
|
}
|
|
48650
49556
|
function testPolygonPolygon(a, polyA, b, polyB, response) {
|
|
48651
49557
|
const aPoints = polyA.points;
|
|
48652
|
-
const aNormals = polyA.
|
|
49558
|
+
const aNormals = polyA.edgeNormals;
|
|
48653
49559
|
const aLen = aNormals.length;
|
|
48654
49560
|
const bPoints = polyB.points;
|
|
48655
|
-
const bNormals = polyB.
|
|
49561
|
+
const bNormals = polyB.edgeNormals;
|
|
48656
49562
|
const bLen = bNormals.length;
|
|
48657
49563
|
const posA = T_VECTORS[--T_VECTORS_IDX].copy(a.pos).add(a.ancestor.getAbsolutePosition()).add(polyA.pos);
|
|
48658
49564
|
const posB = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(polyB.pos);
|
|
@@ -48792,7 +49698,7 @@ function testPolygonEllipse(a, polyA, b, ellipseB, response) {
|
|
|
48792
49698
|
}
|
|
48793
49699
|
}
|
|
48794
49700
|
} else {
|
|
48795
|
-
normal.copy(polyA.
|
|
49701
|
+
normal.copy(polyA.edgeNormals[i]);
|
|
48796
49702
|
dist = point.dot(normal);
|
|
48797
49703
|
const distAbs = Math.abs(dist);
|
|
48798
49704
|
if ((len === 1 || dist > 0) && distAbs > radius) {
|
|
@@ -51772,7 +52678,7 @@ var Trigger = class extends Renderable {
|
|
|
51772
52678
|
};
|
|
51773
52679
|
|
|
51774
52680
|
// src/version.ts
|
|
51775
|
-
var version = "20.
|
|
52681
|
+
var version = "20.6.0";
|
|
51776
52682
|
|
|
51777
52683
|
// src/system/bootstrap.ts
|
|
51778
52684
|
var initialized = false;
|
|
@@ -52222,7 +53128,7 @@ var LIT_INSTANCED = {
|
|
|
52222
53128
|
};
|
|
52223
53129
|
|
|
52224
53130
|
// src/video/webgpu/shaders/mesh-lit.wgsl
|
|
52225
|
-
var mesh_lit_default3 = "// melonJS WebGPU lit mesh shader \u2014 the WGSL port of mesh-lit.vert /\n// mesh-lit.frag: half-Lambert diffuse from directional Light3d lights plus\n// an ambient floor, over the mesh tier's uniform-driven placement.\n//\n// Vertex layout (48-byte stride, shared with the WebGL backend): the\n// 36-byte unlit layout plus\n// @location(3) aNormal float32x3 offset 36\n// aNormal is MODEL-space on the retained path (rotated below by the model\n// matrix's upper 3\xD73) and world-space on the accumulated path \u2014 which\n// supplies an identity model matrix, so one shader serves both.\n//\n// The std140 Light3dBlock binds at group 2 with a dynamic offset \u2014 one\n// snapshot per light change per frame, per the queue-write ordering law.\n// Its layout is written by src/video/webgl/lighting/std140.ts and the two\n// must agree byte for byte \u2014 a mismatch does not fail validation, it\n// silently shifts every light.\n\nstruct FrameUniforms {\n projection : mat4x4<f32>,\n // unused by this shader; part of the shared frame-globals block\n lineWidth : f32,\n};\n\nstruct MeshUniforms {\n model : mat4x4<f32>,\n view : mat4x4<f32>,\n tint : vec4f,\n // x = alpha cutout threshold (0 = disabled), y = 1 when an opacity map\n // is bound (0 = the second sampler is filler); z, w reserved\n params : vec4f,\n // self-illumination added AFTER lighting (r, g, b; w reserved)\n emissive : vec4f,\n // specular color (rgb) and exponent (w); w = 0 means no highlight\n specular : vec4f,\n // the camera's world position (xyz; w reserved), for the specular\n // half-vector \u2014 nothing else in this shader needs it\n eye : vec4f,\n // straight (unpremultiplied) fog colour (rgb; w reserved)\n fogColor : vec4f,\n // x = mode (0 off / 1 linear / 2 exp2), y = near, z = 1/(far - near),\n // w = density\n fogParams : vec4f,\n // xyz = height falloff x the world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked on the CPU\n fogHeight : vec4f,\n};\n\n// One light, type inferred from sentinels (see std140.ts):\n// posRange.w < 0 \u2192 directional (dirCone.xyz = surface\u2192light, normalized);\n// otherwise positional at posRange.xyz with quadratic falloff over\n// posRange.w, plus a spot cone when dirCone.w > -1 (dirCone.xyz = the\n// cone axis / travel direction, w = cos(outer), colorInner.w =\n// cos(inner)). colorInner.rgb is premultiplied by intensity.\nstruct Light3dData {\n posRange : vec4f,\n dirCone : vec4f,\n colorInner : vec4f,\n};\n\n// header = count + pad (vec4), ambient rgb + pad (vec4), then 32 lights\n// of 3 vec4 each \u2014 1568 bytes total (BLOCK3D_BYTES)\nstruct Light3dBlock {\n countPad : vec4f,\n ambient : vec4f,\n lights : array<Light3dData, 32>,\n};\n\n@group(0) @binding(0) var<uniform> uFrame : FrameUniforms;\n@group(1) @binding(0) var uTexture : texture_2d<f32>;\n@group(1) @binding(1) var uSampler : sampler;\n// per-texel opacity (MTL map_d). When no map is bound this pair is filler \u2014\n// the diffuse texture again \u2014 and `params.y` is 0, so its sample is weighted\n// away rather than branched around.\n@group(1) @binding(2) var uAlphaMap : texture_2d<f32>;\n@group(1) @binding(3) var uAlphaSampler : sampler;\n@group(2) @binding(0) var<uniform> uLights : Light3dBlock;\n@group(3) @binding(0) var<uniform> uMesh : MeshUniforms;\n\nstruct VSOut {\n @builtin(position) position : vec4f,\n @location(0) vRegion : vec2f,\n @location(1) vColor : vec4f,\n @location(2) vNormal : vec3f,\n // world-space fragment position \u2014 positional lights (point / spot)\n // fall off with distance, so the fragment stage needs where the\n // surface IS\n @location(3) vWorldPos : vec3f,\n // radial view-space distance for distance fog\n @location(4) vFogDepth : f32,\n};\n\n// Distance fog is behind a PIPELINE-OVERRIDABLE CONSTANT rather than a plain\n// runtime test. `enable_fog` is fixed when the pipeline is created, so the\n// implementation can fold the branch and drop the dead side \u2014 the same result\n// the WebGL backend gets from `#define FOG`, without WGSL needing a\n// preprocessor or the engine deriving a second module. A scene that never\n// enables fog pays for none of the work below.\noverride enable_fog : bool = false;\n\n// How much the height falloff scales the fog along this view ray. Twin of\n// `fogHeightFactor` in the GLSL mesh shaders \u2014 keep them in step.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the ray costs one `exp` and no\n// marching. The result multiplies the distance, leaving both curves untouched.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `model * position` \u2014 and that is the mesh's parent space\n// rather than the world, because `Container.draw` folds every ancestor into\n// the view matrix. Reading a height straight off it put the fog floor at the\n// wrong altitude for anything under a scaled container. Dotting the view-space\n// position against the world-up axis expressed in view space gives the height\n// above the camera whatever those ancestors did, since they are inside the\n// very matrix that produced this position. `fogHeight.xyz` carries the falloff\n// too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite sign\n// to every published form of this.\n//\n// A falloff of 0 gives exactly 1, so uniform fog is this with the dial at zero\n// rather than a special case.\nfn fog_height_factor(viewPos : vec3f) -> f32 {\n let kdy = dot(uMesh.fogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, which is a horizontal view ray \u2014 take\n // the limit rather than guarding, or the fog steps as the ray flattens\n var t = 1.0;\n if (abs(kdy) >= 1e-4) {\n t = (exp(kdy) - 1.0) / kdy;\n }\n return uMesh.fogHeight.w * t;\n}\n\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of `apply_fog` in\n// mesh.wgsl and `applyFog` in the GLSL mesh shaders \u2014 keep them in step. WGSL\n// has no preprocessor and the build loads shaders as raw text, so the block is\n// duplicated rather than shared.\n//\n// `rgb` is already multiplied by `a`, so the fog colour must be scaled by the\n// fragment's own coverage \u2014 a plain mix would paint fog onto near-transparent\n// fragments and halo every alpha-cutout leaf. Mode 0 returns the input\n// untouched, so a scene with no fog is bit-identical.\nfn apply_fog(rgb : vec3f, a : f32, fogDepth : f32) -> vec3f {\n if (!enable_fog) {\n return rgb;\n }\n let mode = uMesh.fogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n var f : f32;\n if (mode < 1.5) {\n f = 1.0 - clamp((fogDepth - uMesh.fogParams.y) * uMesh.fogParams.z, 0.0, 1.0);\n } else {\n let dd = fogDepth * uMesh.fogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uMesh.fogColor.rgb * a, rgb, f);\n}\n\n@vertex\nfn vertex_main(\n @location(0) aVertex : vec3f,\n @location(1) aRegion : vec2f,\n @location(2) aColor : vec4f,\n @location(3) aNormal : vec3f,\n) -> VSOut {\n var out : VSOut;\n let worldPos = uMesh.model * vec4f(aVertex, 1.0);\n let clip = uFrame.projection * uMesh.view * worldPos;\n // GL-convention clip z in [-w, w] remapped to WebGPU's [0, w]\n out.position = vec4f(clip.xy, (clip.z + clip.w) * 0.5, clip.w);\n let tinted = aColor * uMesh.tint;\n out.vColor = vec4f(tinted.rgb * tinted.a, tinted.a);\n out.vRegion = aRegion;\n out.vWorldPos = worldPos.xyz;\n if (enable_fog) {\n let viewPos = (uMesh.view * worldPos).xyz;\n out.vFogDepth = length(viewPos) * fog_height_factor(viewPos);\n } else {\n out.vFogDepth = 0.0;\n }\n // Rotate the normal into world space with the model matrix's upper\n // 3\xD73. Lighting is evaluated in world space, so the view transform is\n // deliberately excluded. Uniform scale cancels when the fragment\n // renormalizes; non-uniform scale is approximated \u2014 an exact result\n // would need the inverse-transpose.\n let m = uMesh.model;\n out.vNormal = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * aNormal;\n return out;\n}\n\n@fragment\nfn fragment_main(in : VSOut) -> @location(0) vec4f {\n // sampled unconditionally, before the discard (uniform control flow)\n var base = textureSample(uTexture, uSampler, in.vRegion);\n // Per-texel opacity (MTL map_d), applied BEFORE the cutout so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Sampled unconditionally and\n // WEIGHTED: with no map bound the second pair is filler, and this keeps\n // the sample in uniform control flow (and identical to the GLSL twin).\n base.a = base.a * mix(1.0, textureSample(uAlphaMap, uAlphaSampler, in.vRegion).r, uMesh.params.y);\n // hard alpha cutout (glTF alphaMode MASK) \u2014 discard before any shading\n // so cut-away texels cost nothing and never write depth\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply \u2014 see mesh.frag: the cutout is the surface's shape, and a\n // fading object must keep it rather than pop out at its own threshold.\n if (base.a < uMesh.params.x) {\n discard;\n }\n base = base * in.vColor;\n\n // see mesh-lit.frag: a `lit` mesh with no usable normals must degrade to\n // unlit rather than normalize a zero vector to NaN and render black\n let nLength = length(in.vNormal);\n if (nLength < 1e-6) {\n return vec4f(\n apply_fog(base.rgb + uMesh.emissive.rgb, base.a, in.vFogDepth),\n base.a\n );\n }\n let n = in.vNormal / nLength;\n var lit = uLights.ambient.rgb;\n // Blinn-Phong specular, accumulated alongside the diffuse term. Gated on\n // the exponent rather than the colour: `Ns` of 0 is the MTL format's \"no\n // highlight\", and exporters happily write a non-black `Ks` next to it.\n var specular = vec3f(0.0);\n let shininess = uMesh.specular.w;\n let hasSpecular = shininess > 0.0;\n var viewDir = vec3f(0.0);\n if (hasSpecular) {\n viewDir = normalize(uMesh.eye.xyz - in.vWorldPos);\n }\n // clamped to the array size, not just taken on trust: if the block\n // ever read as something other than what the writer put there, an\n // unbounded trip count would also index out of range\n let count = min(i32(uLights.countPad.x), 32);\n for (var i = 0; i < count; i = i + 1) {\n let pr = uLights.lights[i].posRange;\n let dc = uLights.lights[i].dirCone;\n var lightDir : vec3f;\n var atten = 1.0;\n if (pr.w < 0.0) {\n // directional: dirCone.xyz is already the surface\u2192light vector\n lightDir = dc.xyz;\n } else {\n // positional: quadratic falloff over range (the Light2d model \u2014\n // stylized, not physical inverse-square, which is unusable in\n // pixel-unit worlds with unit intensities)\n let toLight = pr.xyz - in.vWorldPos;\n let dist = max(length(toLight), 1e-4);\n lightDir = toLight / dist;\n let linearAtt = max(0.0, 1.0 - dist / pr.w);\n atten = linearAtt * linearAtt;\n if (dc.w > -1.0) {\n // spot cone: -lightDir is the light\u2192surface direction; fade\n // from the inner cone cosine to the outer\n let cd = dot(-lightDir, dc.xyz);\n atten = atten * smoothstep(dc.w, uLights.lights[i].colorInner.w, cd);\n }\n }\n // Half-Lambert (\"wrap\") diffuse: dot * 0.5 + 0.5, squared. Softens\n // the terminator and lifts the shadowed side \u2014 gentler than hard\n // Lambert, which reads as harsh noon.\n let ndl = dot(n, lightDir) * 0.5 + 0.5;\n lit = lit + uLights.lights[i].colorInner.rgb * (ndl * ndl * atten);\n if (hasSpecular) {\n // masked by the UNWRAPPED Lambert term: half-Lambert lifts the\n // shadowed side, and a highlight on a surface facing away from\n // the light reads as a rendering error\n let facing = max(dot(n, lightDir), 0.0);\n let h = normalize(lightDir + viewDir);\n let spec = pow(max(dot(n, h), 0.0), shininess);\n specular = specular\n + uLights.lights[i].colorInner.rgb * (spec * facing * atten);\n }\n }\n\n // emissive self-illuminates: added AFTER lighting so it glows at full\n // strength regardless of the scene lights (neon, lava, glowing eyes)\n return vec4f(\n apply_fog(\n base.rgb * lit + specular * uMesh.specular.rgb + uMesh.emissive.rgb,\n base.a,\n in.vFogDepth\n ),\n base.a\n );\n}\n";
|
|
53131
|
+
var mesh_lit_default3 = "// melonJS WebGPU lit mesh shader \u2014 the WGSL port of mesh-lit.vert /\n// mesh-lit.frag: half-Lambert diffuse from directional Light3d lights plus\n// an ambient floor, over the mesh tier's uniform-driven placement.\n//\n// Vertex layout (48-byte stride, shared with the WebGL backend): the\n// 36-byte unlit layout plus\n// @location(3) aNormal float32x3 offset 36\n// aNormal is MODEL-space on the retained path (rotated below by the model\n// matrix's upper 3\xD73) and world-space on the accumulated path \u2014 which\n// supplies an identity model matrix, so one shader serves both.\n//\n// The std140 Light3dBlock binds at group 2 with a dynamic offset \u2014 one\n// snapshot per light change per frame, per the queue-write ordering law.\n// Its layout is written by src/video/webgl/lighting/std140.ts and the two\n// must agree byte for byte \u2014 a mismatch does not fail validation, it\n// silently shifts every light.\n\nstruct FrameUniforms {\n projection : mat4x4<f32>,\n // unused by this shader; part of the shared frame-globals block\n lineWidth : f32,\n};\n\nstruct MeshUniforms {\n model : mat4x4<f32>,\n view : mat4x4<f32>,\n tint : vec4f,\n // x = alpha cutout threshold (0 = disabled), y = 1 when an opacity map\n // is bound (0 = the second sampler is filler); z, w reserved\n params : vec4f,\n // self-illumination added AFTER lighting (r, g, b; w reserved)\n emissive : vec4f,\n // specular color (rgb) and exponent (w); w = 0 means no highlight\n specular : vec4f,\n // the camera's world position (xyz; w reserved), for the specular\n // half-vector \u2014 nothing else in this shader needs it\n eye : vec4f,\n // straight (unpremultiplied) fog colour (rgb; w reserved)\n fogColor : vec4f,\n // x = mode (0 off / 1 linear / 2 exp2), y = near, z = 1/(far - near),\n // w = density\n fogParams : vec4f,\n // xyz = height falloff x the world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked on the CPU\n fogHeight : vec4f,\n};\n\n// One light, type inferred from sentinels (see std140.ts):\n// posRange.w < 0 \u2192 directional (dirCone.xyz = surface\u2192light, normalized);\n// otherwise positional at posRange.xyz with quadratic falloff over\n// posRange.w, plus a spot cone when dirCone.w > -1 (dirCone.xyz = the\n// cone axis / travel direction, w = cos(outer), colorInner.w =\n// cos(inner)). colorInner.rgb is premultiplied by intensity.\nstruct Light3dData {\n posRange : vec4f,\n dirCone : vec4f,\n colorInner : vec4f,\n};\n\n// header = count + pad (vec4), ambient rgb + pad (vec4), then 32 lights\n// of 3 vec4 each \u2014 1568 bytes total (BLOCK3D_BYTES)\nstruct Light3dBlock {\n countPad : vec4f,\n ambient : vec4f,\n lights : array<Light3dData, 32>,\n};\n\n@group(0) @binding(0) var<uniform> uFrame : FrameUniforms;\n@group(1) @binding(0) var uTexture : texture_2d<f32>;\n@group(1) @binding(1) var uSampler : sampler;\n// per-texel opacity (MTL map_d). When no map is bound this pair is filler \u2014\n// the diffuse texture again \u2014 and `params.y` is 0, so its sample is weighted\n// away rather than branched around.\n@group(1) @binding(2) var uAlphaMap : texture_2d<f32>;\n@group(1) @binding(3) var uAlphaSampler : sampler;\n@group(2) @binding(0) var<uniform> uLights : Light3dBlock;\n@group(3) @binding(0) var<uniform> uMesh : MeshUniforms;\n\nstruct VSOut {\n @builtin(position) position : vec4f,\n @location(0) vRegion : vec2f,\n @location(1) vColor : vec4f,\n @location(2) vNormal : vec3f,\n // world-space fragment position \u2014 positional lights (point / spot)\n // fall off with distance, so the fragment stage needs where the\n // surface IS\n @location(3) vWorldPos : vec3f,\n // radial view-space distance for distance fog\n @location(4) vFogDepth : f32,\n};\n\n// Distance fog is behind a PIPELINE-OVERRIDABLE CONSTANT rather than a plain\n// runtime test. `enable_fog` is fixed when the pipeline is created, so the\n// implementation can fold the branch and drop the dead side \u2014 the same result\n// the WebGL backend gets from `#define FOG`, without WGSL needing a\n// preprocessor or the engine deriving a second module. A scene that never\n// enables fog pays for none of the work below.\noverride enable_fog : bool = false;\n\n// How much the height falloff scales the fog along this view ray. Twin of\n// `fogHeightFactor` in the GLSL mesh shaders \u2014 keep them in step.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the ray costs one `exp` and no\n// marching. The result multiplies the distance, leaving both curves untouched.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `model * position` \u2014 and that is the mesh's parent space\n// rather than the world, because `Container.draw` folds every ancestor into\n// the view matrix. Reading a height straight off it put the fog floor at the\n// wrong altitude for anything under a scaled container. Dotting the view-space\n// position against the world-up axis expressed in view space gives the height\n// above the camera whatever those ancestors did, since they are inside the\n// very matrix that produced this position. `fogHeight.xyz` carries the falloff\n// too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite sign\n// to every published form of this.\n//\n// A falloff of 0 gives exactly 1, so uniform fog is this with the dial at zero\n// rather than a special case.\nfn fog_height_factor(viewPos : vec3f) -> f32 {\n let kdy = dot(uMesh.fogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, which is a horizontal view ray \u2014 take\n // the limit rather than guarding, or the fog steps as the ray flattens\n var t = 1.0;\n if (abs(kdy) >= 1e-4) {\n t = (exp(kdy) - 1.0) / kdy;\n }\n return uMesh.fogHeight.w * t;\n}\n\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of `apply_fog` in\n// mesh.wgsl and `applyFog` in the GLSL mesh shaders \u2014 keep them in step. WGSL\n// has no preprocessor and the build loads shaders as raw text, so the block is\n// duplicated rather than shared.\n//\n// `rgb` is already multiplied by `a`, so the fog colour must be scaled by the\n// fragment's own coverage \u2014 a plain mix would paint fog onto near-transparent\n// fragments and halo every alpha-cutout leaf. Mode 0 returns the input\n// untouched, so a scene with no fog is bit-identical.\nfn apply_fog(rgb : vec3f, a : f32, fogDepth : f32) -> vec3f {\n if (!enable_fog) {\n return rgb;\n }\n let mode = uMesh.fogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n var f : f32;\n if (mode < 1.5) {\n f = 1.0 - clamp((fogDepth - uMesh.fogParams.y) * uMesh.fogParams.z, 0.0, 1.0);\n } else {\n let dd = fogDepth * uMesh.fogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uMesh.fogColor.rgb * a, rgb, f);\n}\n\n// The mesh-hosted ShaderEffect hook (#1658). Identity by default \u2014 every\n// compiler inlines it away, so an un-effected mesh pays nothing. When an\n// effect is hosted, `spliceEffect` replaces this definition with one that\n// calls the effect's `apply()`, and pastes the effect body above it.\n//\n// Deliberately a FUNCTION and not a marker comment: the shader pipeline\n// strips comments, so a comment cannot be relied on to reach the splicer.\n//\n// Declared ABOVE `@vertex` on purpose: `buildInstancedMeshWGSL` assembles the\n// instanced variant as `source.slice(0, vertexAt)` + a generated vertex stage\n// + `source.slice(fragmentAt)`, so anything BETWEEN the two stages is dropped.\n// Defined there, the instanced module kept the call and lost the definition,\n// and every frame drawing an instanced lit mesh failed to compile.\nfn ME_effect(c : vec4f, uv : vec2f) -> vec4f { return c; }\n\n@vertex\nfn vertex_main(\n @location(0) aVertex : vec3f,\n @location(1) aRegion : vec2f,\n @location(2) aColor : vec4f,\n @location(3) aNormal : vec3f,\n) -> VSOut {\n var out : VSOut;\n let worldPos = uMesh.model * vec4f(aVertex, 1.0);\n let clip = uFrame.projection * uMesh.view * worldPos;\n // GL-convention clip z in [-w, w] remapped to WebGPU's [0, w]\n out.position = vec4f(clip.xy, (clip.z + clip.w) * 0.5, clip.w);\n let tinted = aColor * uMesh.tint;\n out.vColor = vec4f(tinted.rgb * tinted.a, tinted.a);\n out.vRegion = aRegion;\n out.vWorldPos = worldPos.xyz;\n if (enable_fog) {\n let viewPos = (uMesh.view * worldPos).xyz;\n out.vFogDepth = length(viewPos) * fog_height_factor(viewPos);\n } else {\n out.vFogDepth = 0.0;\n }\n // Rotate the normal into world space with the model matrix's upper\n // 3\xD73. Lighting is evaluated in world space, so the view transform is\n // deliberately excluded. Uniform scale cancels when the fragment\n // renormalizes; non-uniform scale is approximated \u2014 an exact result\n // would need the inverse-transpose.\n let m = uMesh.model;\n out.vNormal = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * aNormal;\n return out;\n}\n\n\n@fragment\nfn fragment_main(in : VSOut) -> @location(0) vec4f {\n // sampled unconditionally, before the discard (uniform control flow)\n var base = textureSample(uTexture, uSampler, in.vRegion);\n // Per-texel opacity (MTL map_d), applied BEFORE the cutout so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Sampled unconditionally and\n // WEIGHTED: with no map bound the second pair is filler, and this keeps\n // the sample in uniform control flow (and identical to the GLSL twin).\n base.a = base.a * mix(1.0, textureSample(uAlphaMap, uAlphaSampler, in.vRegion).r, uMesh.params.y);\n // hard alpha cutout (glTF alphaMode MASK) \u2014 discard before any shading\n // so cut-away texels cost nothing and never write depth\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply \u2014 see mesh.frag: the cutout is the surface's shape, and a\n // fading object must keep it rather than pop out at its own threshold.\n if (base.a < uMesh.params.x) {\n discard;\n }\n base = base * in.vColor;\n base = ME_effect(base, in.vRegion);\n\n // see mesh-lit.frag: a `lit` mesh with no usable normals must degrade to\n // unlit rather than normalize a zero vector to NaN and render black\n let nLength = length(in.vNormal);\n if (nLength < 1e-6) {\n return vec4f(\n apply_fog(base.rgb + uMesh.emissive.rgb, base.a, in.vFogDepth),\n base.a\n );\n }\n let n = in.vNormal / nLength;\n var lit = uLights.ambient.rgb;\n // Blinn-Phong specular, accumulated alongside the diffuse term. Gated on\n // the exponent rather than the colour: `Ns` of 0 is the MTL format's \"no\n // highlight\", and exporters happily write a non-black `Ks` next to it.\n var specular = vec3f(0.0);\n let shininess = uMesh.specular.w;\n let hasSpecular = shininess > 0.0;\n var viewDir = vec3f(0.0);\n if (hasSpecular) {\n viewDir = normalize(uMesh.eye.xyz - in.vWorldPos);\n }\n // clamped to the array size, not just taken on trust: if the block\n // ever read as something other than what the writer put there, an\n // unbounded trip count would also index out of range\n let count = min(i32(uLights.countPad.x), 32);\n for (var i = 0; i < count; i = i + 1) {\n let pr = uLights.lights[i].posRange;\n let dc = uLights.lights[i].dirCone;\n var lightDir : vec3f;\n var atten = 1.0;\n if (pr.w < 0.0) {\n // directional: dirCone.xyz is already the surface\u2192light vector\n lightDir = dc.xyz;\n } else {\n // positional: quadratic falloff over range (the Light2d model \u2014\n // stylized, not physical inverse-square, which is unusable in\n // pixel-unit worlds with unit intensities)\n let toLight = pr.xyz - in.vWorldPos;\n let dist = max(length(toLight), 1e-4);\n lightDir = toLight / dist;\n let linearAtt = max(0.0, 1.0 - dist / pr.w);\n atten = linearAtt * linearAtt;\n if (dc.w > -1.0) {\n // spot cone: -lightDir is the light\u2192surface direction; fade\n // from the inner cone cosine to the outer\n let cd = dot(-lightDir, dc.xyz);\n atten = atten * smoothstep(dc.w, uLights.lights[i].colorInner.w, cd);\n }\n }\n // Half-Lambert (\"wrap\") diffuse: dot * 0.5 + 0.5, squared. Softens\n // the terminator and lifts the shadowed side \u2014 gentler than hard\n // Lambert, which reads as harsh noon.\n let ndl = dot(n, lightDir) * 0.5 + 0.5;\n lit = lit + uLights.lights[i].colorInner.rgb * (ndl * ndl * atten);\n if (hasSpecular) {\n // masked by the UNWRAPPED Lambert term: half-Lambert lifts the\n // shadowed side, and a highlight on a surface facing away from\n // the light reads as a rendering error\n let facing = max(dot(n, lightDir), 0.0);\n let h = normalize(lightDir + viewDir);\n let spec = pow(max(dot(n, h), 0.0), shininess);\n specular = specular\n + uLights.lights[i].colorInner.rgb * (spec * facing * atten);\n }\n }\n\n // emissive self-illuminates: added AFTER lighting so it glows at full\n // strength regardless of the scene lights (neon, lava, glowing eyes)\n return vec4f(\n apply_fog(\n base.rgb * lit + specular * uMesh.specular.rgb + uMesh.emissive.rgb,\n base.a,\n in.vFogDepth\n ),\n base.a\n );\n}\n";
|
|
52226
53132
|
|
|
52227
53133
|
// src/video/webgpu/buffer/instance_buffer.js
|
|
52228
53134
|
var WebGPUInstanceBuffer = class {
|
|
@@ -52416,7 +53322,7 @@ var WebGPURetainedGeometry = class {
|
|
|
52416
53322
|
};
|
|
52417
53323
|
|
|
52418
53324
|
// src/video/webgpu/shaders/mesh.wgsl
|
|
52419
|
-
var mesh_default3 = "// melonJS WebGPU mesh shader \u2014 the WGSL port of mesh.vert / mesh.frag.\n//\n// Vertex layout (36-byte stride, shared with the WebGL backend):\n// @location(0) aVertex float32x3 offset 0\n// @location(1) aRegion float32x2 offset 12\n// @location(2) aColor float32x4 offset 20\n//\n// aColor is four straight floats (R, G, B, A in [0, 1]) \u2014 NOT the quad\n// tier's packed unorm8x4 \u2014 so no BGRA swizzle applies. The float layout is\n// shared with the GL backend, where it exists to dodge NaN-pattern\n// canonicalization on Metal-backed drivers.\n//\n// Placement is uniform-driven so retained geometry uploads once and reuses:\n// clip = projection \xD7 view \xD7 model \xD7 vertex\n// One MeshUniforms snapshot binds per draw (group 3, dynamic offset \u2014 the\n// queue-write ordering law: a shared region would be retroactively\n// clobbered for draws already recorded). The accumulated path (2D camera,\n// CPU-projected vertices) supplies an identity model matrix.\n\nstruct FrameUniforms {\n projection : mat4x4<f32>,\n // unused by this shader; part of the shared frame-globals block\n lineWidth : f32,\n};\n\nstruct MeshUniforms {\n // the mesh's own placement (axis bridge + mesh scale included)\n model : mat4x4<f32>,\n // the camera view plus any ancestor container transform\n view : mat4x4<f32>,\n // per-draw tint \xD7 global alpha (r, g, b, a) \u2014 kept out of the vertex\n // data so re-tinting never invalidates retained geometry\n tint : vec4f,\n // x = alpha cutout threshold (0 = disabled), y = 1 when an opacity map\n // is bound (0 = the second sampler is filler); z, w reserved\n params : vec4f,\n // self-illumination added on top (r, g, b; w reserved)\n emissive : vec4f,\n // specular color (rgb) and exponent (w); w = 0 means no highlight.\n // Unused by the unlit tier, present so both tiers share one block size.\n specular : vec4f,\n // the camera's world position (xyz; w reserved)\n eye : vec4f,\n // straight (unpremultiplied) fog colour (rgb; w reserved)\n fogColor : vec4f,\n // x = mode (0 off / 1 linear / 2 exp2), y = near, z = 1/(far - near),\n // w = density\n fogParams : vec4f,\n // xyz = height falloff x the world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked on the CPU\n fogHeight : vec4f,\n};\n\n@group(0) @binding(0) var<uniform> uFrame : FrameUniforms;\n@group(1) @binding(0) var uTexture : texture_2d<f32>;\n@group(1) @binding(1) var uSampler : sampler;\n// per-texel opacity (MTL map_d). When no map is bound this pair is filler \u2014\n// the diffuse texture again \u2014 and `params.y` is 0, so its sample is weighted\n// away rather than branched around.\n@group(1) @binding(2) var uAlphaMap : texture_2d<f32>;\n@group(1) @binding(3) var uAlphaSampler : sampler;\n@group(3) @binding(0) var<uniform> uMesh : MeshUniforms;\n\nstruct VSOut {\n @builtin(position) position : vec4f,\n @location(0) vRegion : vec2f,\n @location(1) vColor : vec4f,\n @location(2) vFogDepth : f32,\n};\n\n// Distance fog is behind a PIPELINE-OVERRIDABLE CONSTANT rather than a plain\n// runtime test. `enable_fog` is fixed when the pipeline is created, so the\n// implementation can fold the branch and drop the dead side \u2014 the same result\n// the WebGL backend gets from `#define FOG`, without WGSL needing a\n// preprocessor or the engine deriving a second module. A scene that never\n// enables fog pays for none of the work below.\noverride enable_fog : bool = false;\n\n// How much the height falloff scales the fog along this view ray. Twin of\n// `fogHeightFactor` in the GLSL mesh shaders \u2014 keep them in step.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the ray costs one `exp` and no\n// marching. The result multiplies the distance, leaving both curves untouched.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `model * position` \u2014 and that is the mesh's parent space\n// rather than the world, because `Container.draw` folds every ancestor into\n// the view matrix. Reading a height straight off it put the fog floor at the\n// wrong altitude for anything under a scaled container. Dotting the view-space\n// position against the world-up axis expressed in view space gives the height\n// above the camera whatever those ancestors did, since they are inside the\n// very matrix that produced this position. `fogHeight.xyz` carries the falloff\n// too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite sign\n// to every published form of this.\n//\n// A falloff of 0 gives exactly 1, so uniform fog is this with the dial at zero\n// rather than a special case.\nfn fog_height_factor(viewPos : vec3f) -> f32 {\n let kdy = dot(uMesh.fogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, which is a horizontal view ray \u2014 take\n // the limit rather than guarding, or the fog steps as the ray flattens\n var t = 1.0;\n if (abs(kdy) >= 1e-4) {\n t = (exp(kdy) - 1.0) / kdy;\n }\n return uMesh.fogHeight.w * t;\n}\n\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of `applyFog` in the\n// GLSL mesh shaders \u2014 keep the two in step. They are duplicated rather than\n// shared because WGSL has no preprocessor and the production build loads each\n// shader as raw text.\n//\n// `rgb` is already multiplied by `a`, so the fog colour must be scaled by the\n// fragment's own coverage: a plain mix toward the fog colour would paint\n// full-strength fog onto near-transparent fragments \u2014 grey halos around every\n// alpha-cutout leaf.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nfn apply_fog(rgb : vec3f, a : f32, fogDepth : f32) -> vec3f {\n if (!enable_fog) {\n return rgb;\n }\n let mode = uMesh.fogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n var f : f32;\n if (mode < 1.5) {\n // linear: 1 at `near`, reaching 0 at `far`\n f = 1.0 - clamp((fogDepth - uMesh.fogParams.y) * uMesh.fogParams.z, 0.0, 1.0);\n } else {\n // exponential squared: survival = exp(-(density * d)^2)\n let dd = fogDepth * uMesh.fogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uMesh.fogColor.rgb * a, rgb, f);\n}\n\n@vertex\nfn vertex_main(\n @location(0) aVertex : vec3f,\n @location(1) aRegion : vec2f,\n @location(2) aColor : vec4f,\n) -> VSOut {\n var out : VSOut;\n let clip =\n uFrame.projection * uMesh.view * uMesh.model * vec4f(aVertex, 1.0);\n // GL-convention clip z in [-w, w] remapped to WebGPU's [0, w]\n out.position = vec4f(clip.xy, (clip.z + clip.w) * 0.5, clip.w);\n // tint first, then premultiply \u2014 matches the fragment's expectation\n let tinted = aColor * uMesh.tint;\n out.vColor = vec4f(tinted.rgb * tinted.a, tinted.a);\n // Radial view-space distance for distance fog. Radial rather than view-space\n // z, so fog holds steady as the camera turns. The clip position above keeps\n // its own product: re-associating it could shift vertices by an ulp, and\n // fog-off output must stay bit-identical.\n // the slot is always declared \u2014 an override cannot remove an inter-stage\n // variable \u2014 but the work behind it folds away with the constant\n if (enable_fog) {\n let viewPos = uMesh.view * uMesh.model * vec4f(aVertex, 1.0);\n out.vFogDepth = length(viewPos.xyz)\n * fog_height_factor(viewPos.xyz);\n } else {\n out.vFogDepth = 0.0;\n }\n out.vRegion = aRegion;\n return out;\n}\n\n@fragment\nfn fragment_main(in : VSOut) -> @location(0) vec4f {\n // sampled unconditionally, before the discard (uniform control flow)\n var color = textureSample(uTexture, uSampler, in.vRegion);\n // Per-texel opacity (MTL map_d), applied BEFORE the cutout so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Sampled unconditionally and\n // WEIGHTED: with no map bound the second pair is filler, and this keeps\n // the sample in uniform control flow (and identical to the GLSL twin).\n color.a = color.a * mix(1.0, textureSample(uAlphaMap, uAlphaSampler, in.vRegion).r, uMesh.params.y);\n // hard alpha cutout (glTF alphaMode MASK): drop cut texels so foliage /\n // fences / decals read crisp without blending or sorting\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply \u2014 see mesh.frag: the cutout is the surface's shape, and a\n // fading object must keep it rather than pop out at its own threshold.\n if (color.a < uMesh.params.x) {\n discard;\n }\n color = color * in.vColor;\n // emissive adds a self-lit color on top (neon, lava, screens); the\n // unlit path has no lighting, so it is simply added to the base color\n return vec4f(\n apply_fog(color.rgb + uMesh.emissive.rgb, color.a, in.vFogDepth),\n color.a\n );\n}\n";
|
|
53325
|
+
var mesh_default3 = "// melonJS WebGPU mesh shader \u2014 the WGSL port of mesh.vert / mesh.frag.\n//\n// Vertex layout (36-byte stride, shared with the WebGL backend):\n// @location(0) aVertex float32x3 offset 0\n// @location(1) aRegion float32x2 offset 12\n// @location(2) aColor float32x4 offset 20\n//\n// aColor is four straight floats (R, G, B, A in [0, 1]) \u2014 NOT the quad\n// tier's packed unorm8x4 \u2014 so no BGRA swizzle applies. The float layout is\n// shared with the GL backend, where it exists to dodge NaN-pattern\n// canonicalization on Metal-backed drivers.\n//\n// Placement is uniform-driven so retained geometry uploads once and reuses:\n// clip = projection \xD7 view \xD7 model \xD7 vertex\n// One MeshUniforms snapshot binds per draw (group 3, dynamic offset \u2014 the\n// queue-write ordering law: a shared region would be retroactively\n// clobbered for draws already recorded). The accumulated path (2D camera,\n// CPU-projected vertices) supplies an identity model matrix.\n\nstruct FrameUniforms {\n projection : mat4x4<f32>,\n // unused by this shader; part of the shared frame-globals block\n lineWidth : f32,\n};\n\nstruct MeshUniforms {\n // the mesh's own placement (axis bridge + mesh scale included)\n model : mat4x4<f32>,\n // the camera view plus any ancestor container transform\n view : mat4x4<f32>,\n // per-draw tint \xD7 global alpha (r, g, b, a) \u2014 kept out of the vertex\n // data so re-tinting never invalidates retained geometry\n tint : vec4f,\n // x = alpha cutout threshold (0 = disabled), y = 1 when an opacity map\n // is bound (0 = the second sampler is filler); z, w reserved\n params : vec4f,\n // self-illumination added on top (r, g, b; w reserved)\n emissive : vec4f,\n // specular color (rgb) and exponent (w); w = 0 means no highlight.\n // Unused by the unlit tier, present so both tiers share one block size.\n specular : vec4f,\n // the camera's world position (xyz; w reserved)\n eye : vec4f,\n // straight (unpremultiplied) fog colour (rgb; w reserved)\n fogColor : vec4f,\n // x = mode (0 off / 1 linear / 2 exp2), y = near, z = 1/(far - near),\n // w = density\n fogParams : vec4f,\n // xyz = height falloff x the world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked on the CPU\n fogHeight : vec4f,\n};\n\n@group(0) @binding(0) var<uniform> uFrame : FrameUniforms;\n@group(1) @binding(0) var uTexture : texture_2d<f32>;\n@group(1) @binding(1) var uSampler : sampler;\n// per-texel opacity (MTL map_d). When no map is bound this pair is filler \u2014\n// the diffuse texture again \u2014 and `params.y` is 0, so its sample is weighted\n// away rather than branched around.\n@group(1) @binding(2) var uAlphaMap : texture_2d<f32>;\n@group(1) @binding(3) var uAlphaSampler : sampler;\n@group(3) @binding(0) var<uniform> uMesh : MeshUniforms;\n\nstruct VSOut {\n @builtin(position) position : vec4f,\n @location(0) vRegion : vec2f,\n @location(1) vColor : vec4f,\n @location(2) vFogDepth : f32,\n};\n\n// Distance fog is behind a PIPELINE-OVERRIDABLE CONSTANT rather than a plain\n// runtime test. `enable_fog` is fixed when the pipeline is created, so the\n// implementation can fold the branch and drop the dead side \u2014 the same result\n// the WebGL backend gets from `#define FOG`, without WGSL needing a\n// preprocessor or the engine deriving a second module. A scene that never\n// enables fog pays for none of the work below.\noverride enable_fog : bool = false;\n\n// How much the height falloff scales the fog along this view ray. Twin of\n// `fogHeightFactor` in the GLSL mesh shaders \u2014 keep them in step.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the ray costs one `exp` and no\n// marching. The result multiplies the distance, leaving both curves untouched.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `model * position` \u2014 and that is the mesh's parent space\n// rather than the world, because `Container.draw` folds every ancestor into\n// the view matrix. Reading a height straight off it put the fog floor at the\n// wrong altitude for anything under a scaled container. Dotting the view-space\n// position against the world-up axis expressed in view space gives the height\n// above the camera whatever those ancestors did, since they are inside the\n// very matrix that produced this position. `fogHeight.xyz` carries the falloff\n// too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite sign\n// to every published form of this.\n//\n// A falloff of 0 gives exactly 1, so uniform fog is this with the dial at zero\n// rather than a special case.\nfn fog_height_factor(viewPos : vec3f) -> f32 {\n let kdy = dot(uMesh.fogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, which is a horizontal view ray \u2014 take\n // the limit rather than guarding, or the fog steps as the ray flattens\n var t = 1.0;\n if (abs(kdy) >= 1e-4) {\n t = (exp(kdy) - 1.0) / kdy;\n }\n return uMesh.fogHeight.w * t;\n}\n\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of `applyFog` in the\n// GLSL mesh shaders \u2014 keep the two in step. They are duplicated rather than\n// shared because WGSL has no preprocessor and the production build loads each\n// shader as raw text.\n//\n// `rgb` is already multiplied by `a`, so the fog colour must be scaled by the\n// fragment's own coverage: a plain mix toward the fog colour would paint\n// full-strength fog onto near-transparent fragments \u2014 grey halos around every\n// alpha-cutout leaf.\n//\n// Mode 0 returns the input untouched, so a scene with no fog is bit-identical\n// to one built before fog existed.\nfn apply_fog(rgb : vec3f, a : f32, fogDepth : f32) -> vec3f {\n if (!enable_fog) {\n return rgb;\n }\n let mode = uMesh.fogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n var f : f32;\n if (mode < 1.5) {\n // linear: 1 at `near`, reaching 0 at `far`\n f = 1.0 - clamp((fogDepth - uMesh.fogParams.y) * uMesh.fogParams.z, 0.0, 1.0);\n } else {\n // exponential squared: survival = exp(-(density * d)^2)\n let dd = fogDepth * uMesh.fogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uMesh.fogColor.rgb * a, rgb, f);\n}\n\n// The mesh-hosted ShaderEffect hook (#1658). Identity by default \u2014 every\n// compiler inlines it away, so an un-effected mesh pays nothing. When an\n// effect is hosted, `spliceEffect` replaces this definition with one that\n// calls the effect's `apply()`, and pastes the effect body above it.\n//\n// Deliberately a FUNCTION and not a marker comment: the shader pipeline\n// strips comments, so a comment cannot be relied on to reach the splicer.\n//\n// Declared ABOVE `@vertex` on purpose: `buildInstancedMeshWGSL` assembles the\n// instanced variant as `source.slice(0, vertexAt)` + a generated vertex stage\n// + `source.slice(fragmentAt)`, so anything BETWEEN the two stages is dropped.\n// Defined there, the instanced module kept the call and lost the definition,\n// and every frame drawing an instanced lit mesh failed to compile.\nfn ME_effect(c : vec4f, uv : vec2f) -> vec4f { return c; }\n\n@vertex\nfn vertex_main(\n @location(0) aVertex : vec3f,\n @location(1) aRegion : vec2f,\n @location(2) aColor : vec4f,\n) -> VSOut {\n var out : VSOut;\n let clip =\n uFrame.projection * uMesh.view * uMesh.model * vec4f(aVertex, 1.0);\n // GL-convention clip z in [-w, w] remapped to WebGPU's [0, w]\n out.position = vec4f(clip.xy, (clip.z + clip.w) * 0.5, clip.w);\n // tint first, then premultiply \u2014 matches the fragment's expectation\n let tinted = aColor * uMesh.tint;\n out.vColor = vec4f(tinted.rgb * tinted.a, tinted.a);\n // Radial view-space distance for distance fog. Radial rather than view-space\n // z, so fog holds steady as the camera turns. The clip position above keeps\n // its own product: re-associating it could shift vertices by an ulp, and\n // fog-off output must stay bit-identical.\n // the slot is always declared \u2014 an override cannot remove an inter-stage\n // variable \u2014 but the work behind it folds away with the constant\n if (enable_fog) {\n let viewPos = uMesh.view * uMesh.model * vec4f(aVertex, 1.0);\n out.vFogDepth = length(viewPos.xyz)\n * fog_height_factor(viewPos.xyz);\n } else {\n out.vFogDepth = 0.0;\n }\n out.vRegion = aRegion;\n return out;\n}\n\n\n@fragment\nfn fragment_main(in : VSOut) -> @location(0) vec4f {\n // sampled unconditionally, before the discard (uniform control flow)\n var color = textureSample(uTexture, uSampler, in.vRegion);\n // Per-texel opacity (MTL map_d), applied BEFORE the cutout so one\n // material can cut out to the shape of a leaf rather than at a single\n // threshold across the whole surface. Sampled unconditionally and\n // WEIGHTED: with no map bound the second pair is filler, and this keeps\n // the sample in uniform control flow (and identical to the GLSL twin).\n color.a = color.a * mix(1.0, textureSample(uAlphaMap, uAlphaSampler, in.vRegion).r, uMesh.params.y);\n // hard alpha cutout (glTF alphaMode MASK): drop cut texels so foliage /\n // fences / decals read crisp without blending or sorting\n // Thresholded on the MATERIAL's own alpha, deliberately BEFORE the tint\n // multiply \u2014 see mesh.frag: the cutout is the surface's shape, and a\n // fading object must keep it rather than pop out at its own threshold.\n if (color.a < uMesh.params.x) {\n discard;\n }\n color = color * in.vColor;\n color = ME_effect(color, in.vRegion);\n // emissive adds a self-lit color on top (neon, lava, screens); the\n // unlit path has no lighting, so it is simply added to the base color\n return vec4f(\n apply_fog(color.rgb + uMesh.emissive.rgb, color.a, in.vFogDepth),\n color.a\n );\n}\n";
|
|
52420
53326
|
|
|
52421
53327
|
// src/video/webgpu/shaders/mesh-shadow-instanced.wgsl
|
|
52422
53328
|
var mesh_shadow_instanced_default2 = "// Instanced ground-shadow module (#1515) \u2014 the WGSL twin of\n// `webgl/shaders/mesh-shadow-instanced.vert`.\n//\n// One flat blob per instance, drawn from the SAME instance buffer the mesh\n// itself uses, so a forest of a hundred thousand trees costs one extra draw\n// rather than a hundred thousand.\n//\n// Hand-written and STANDALONE, rather than derived from the mesh module by\n// `buildInstancedMeshWGSL`. Deriving it would inherit three things it must\n// not have: the `hasColor`/`hasData` variant matrix (this reads neither slot,\n// so one module serves every record layout), the per-instance tint multiply\n// and emissive substitution (which would give a forest coloured, glowing\n// shadows), and the VSOut member-written guard, which on the lit tier\n// requires a body that writes `vWorldPos` and `vNormal` \u2014 neither of which a\n// flat unlit blob has any use for.\n//\n// The instance's rotation and vertical scale are DISCARDED: a blob lies flat\n// however its tree is turned, and only the horizontal footprint sets its\n// width. Translation and horizontal scale come straight out of the record's\n// three rows with no matrix built. Flattening onto the ground is\n// `uMesh.model`'s job \u2014 the caller passes the group matrix with its Y basis\n// column zeroed and its translation Y set to the ground height.\n\nstruct FrameUniforms {\n projection : mat4x4<f32>,\n // unused by this shader; part of the shared frame-globals block. Its size\n // has to match the block the layout declares EXACTLY \u2014 a struct that reads\n // past `minBindingSize` is a pipeline-creation error, not a silent\n // over-read, and takes the whole frame's command buffer down with it.\n lineWidth : f32,\n};\n\nstruct MeshUniforms {\n model : mat4x4<f32>,\n view : mat4x4<f32>,\n tint : vec4f,\n params : vec4f,\n emissive : vec4f,\n specular : vec4f,\n eye : vec4f,\n // straight (unpremultiplied) fog colour (rgb; w reserved)\n fogColor : vec4f,\n // x = mode (0 off / 1 linear / 2 exp2), y = near, z = 1/(far - near),\n // w = density\n fogParams : vec4f,\n // xyz = height falloff x the world-up axis, in VIEW space;\n // w = exp(k * (cameraY - fogHeight)), pre-baked on the CPU\n fogHeight : vec4f,\n};\n\n@group(0) @binding(0) var<uniform> uFrame : FrameUniforms;\n@group(1) @binding(0) var uTexture : texture_2d<f32>;\n@group(1) @binding(1) var uSampler : sampler;\n@group(1) @binding(2) var uAlphaMap : texture_2d<f32>;\n@group(1) @binding(3) var uAlphaSampler : sampler;\n@group(3) @binding(0) var<uniform> uMesh : MeshUniforms;\n\n// Distance fog is behind a PIPELINE-OVERRIDABLE CONSTANT rather than a plain\n// runtime test. `enable_fog` is fixed when the pipeline is created, so the\n// implementation can fold the branch and drop the dead side \u2014 the same result\n// the WebGL backend gets from `#define FOG`, without WGSL needing a\n// preprocessor or the engine deriving a second module. A scene that never\n// enables fog pays for none of the work below.\noverride enable_fog : bool = false;\n\n// How much the height falloff scales the fog along this view ray. Twin of\n// `fogHeightFactor` in the GLSL mesh shaders \u2014 keep them in step.\n//\n// Density falls off exponentially with altitude, and an exponential integrates\n// analytically along a straight segment, so the ray costs one `exp` and no\n// marching. The result multiplies the distance, leaving both curves untouched.\n//\n// Takes the VIEW-space position, not a height. The only position this stage\n// has is pre-view \u2014 `model * position` \u2014 and that is the mesh's parent space\n// rather than the world, because `Container.draw` folds every ancestor into\n// the view matrix. Reading a height straight off it put the fog floor at the\n// wrong altitude for anything under a scaled container. Dotting the view-space\n// position against the world-up axis expressed in view space gives the height\n// above the camera whatever those ancestors did, since they are inside the\n// very matrix that produced this position. `fogHeight.xyz` carries the falloff\n// too, so the exponent is one dot product.\n//\n// Render space is Y-DOWN: density rises as height INCREASES, the opposite sign\n// to every published form of this.\n//\n// A falloff of 0 gives exactly 1, so uniform fog is this with the dial at zero\n// rather than a special case.\nfn fog_height_factor(viewPos : vec3f) -> f32 {\n let kdy = dot(uMesh.fogHeight.xyz, viewPos);\n // (exp(x) - 1) / x is 0/0 at x = 0, which is a horizontal view ray \u2014 take\n // the limit rather than guarding, or the fog steps as the ray flattens\n var t = 1.0;\n if (abs(kdy) >= 1e-4) {\n t = (exp(kdy) - 1.0) / kdy;\n }\n return uMesh.fogHeight.w * t;\n}\n\n\nstruct VSOut {\n @builtin(position) position : vec4f,\n @location(0) vRegion : vec2f,\n @location(1) vColor : vec4f,\n // radial view-space distance for distance fog\n @location(2) vFogDepth : f32,\n};\n\n@vertex\nfn vertex_main(\n @location(0) aVertex : vec3f,\n @location(1) aRegion : vec2f,\n @location(2) aColor : vec4f,\n @location(3) aInstanceRow0 : vec4f,\n @location(4) aInstanceRow1 : vec4f,\n @location(5) aInstanceRow2 : vec4f,\n) -> VSOut {\n var out : VSOut;\n\n let instancePos = vec3f(aInstanceRow0.w, aInstanceRow1.w, aInstanceRow2.w);\n // column 0 of the instance basis is the X axis times its scale; a scatter\n // is rotated about the vertical axis, so its length is the horizontal\n // footprint whatever the rotation\n let footprint = length(\n vec3f(aInstanceRow0.x, aInstanceRow1.x, aInstanceRow2.x));\n\n // the quad is a unit square in the ground plane (XZ); its own Y is\n // irrelevant, because uMesh.model flattens it onto the floor\n let local = instancePos + vec3f(aVertex.x, 0.0, aVertex.z) * footprint;\n\n let clip = uFrame.projection * uMesh.view * uMesh.model * vec4f(local, 1.0);\n // WebGPU clip space is [0, w] in Z where GL is [-w, w]\n out.position = vec4f(clip.xy, (clip.z + clip.w) * 0.5, clip.w);\n\n let tinted = aColor * uMesh.tint;\n out.vColor = vec4f(tinted.rgb * tinted.a, tinted.a);\n // a blob fades with distance like the ground it lies on\n if (enable_fog) {\n let viewPos = uMesh.view * uMesh.model * vec4f(local, 1.0);\n out.vFogDepth = length(viewPos.xyz)\n * fog_height_factor(viewPos.xyz);\n } else {\n out.vFogDepth = 0.0;\n }\n out.vRegion = aRegion;\n return out;\n}\n\n// Fold distance fog into a PREMULTIPLIED colour. Twin of `apply_fog` in\n// mesh.wgsl \u2014 kept in step by hand (WGSL has no preprocessor, and the build\n// loads shaders as raw text). Present here because on WebGL this vertex\n// shader pairs with mesh.frag, which fogs; without this the two backends would\n// disagree on whether distant shadows fade.\nfn apply_fog(rgb : vec3f, a : f32, fogDepth : f32) -> vec3f {\n if (!enable_fog) {\n return rgb;\n }\n let mode = uMesh.fogParams.x;\n if (mode < 0.5) {\n return rgb;\n }\n var f : f32;\n if (mode < 1.5) {\n f = 1.0 - clamp((fogDepth - uMesh.fogParams.y) * uMesh.fogParams.z, 0.0, 1.0);\n } else {\n let dd = fogDepth * uMesh.fogParams.w;\n f = exp(-dd * dd);\n }\n return mix(uMesh.fogColor.rgb * a, rgb, f);\n}\n\n@fragment\nfn fragment_main(in : VSOut) -> @location(0) vec4f {\n let texel = textureSample(uTexture, uSampler, in.vRegion);\n let color = texel * in.vColor;\n return vec4f(apply_fog(color.rgb, color.a, in.vFogDepth), color.a);\n}\n";
|
|
@@ -52596,11 +53502,13 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52596
53502
|
}
|
|
52597
53503
|
this.vertexLayoutKey = vertexLayoutKey;
|
|
52598
53504
|
this.customShader = null;
|
|
53505
|
+
this.hostedEffect = null;
|
|
52599
53506
|
this.indexData = new Uint32Array(this.vertexData.maxVertex * 6);
|
|
52600
53507
|
this.indexCount = 0;
|
|
52601
53508
|
this.currentMaterial = null;
|
|
52602
53509
|
this.uniformBinding = null;
|
|
52603
53510
|
this.uniformBindGroups = /* @__PURE__ */ new Map();
|
|
53511
|
+
this.effectBindGroups = /* @__PURE__ */ new Map();
|
|
52604
53512
|
this.meshState = { cullMode: "back", frontFace: "ccw" };
|
|
52605
53513
|
if (this.retained !== void 0) {
|
|
52606
53514
|
this.releaseAllRetained();
|
|
@@ -52745,9 +53653,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52745
53653
|
const frame = renderer.currentFrameBinding;
|
|
52746
53654
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
52747
53655
|
this.bindLights(pass);
|
|
52748
|
-
pass.setBindGroup(
|
|
52749
|
-
|
|
52750
|
-
|
|
53656
|
+
pass.setBindGroup(
|
|
53657
|
+
3,
|
|
53658
|
+
this.uniformBinding.bindGroup,
|
|
53659
|
+
this.uniformBinding.offsets
|
|
53660
|
+
);
|
|
52751
53661
|
pass.setVertexBuffer(0, geometry.vertexBuffer);
|
|
52752
53662
|
pass.setVertexBuffer(1, instances.buffer);
|
|
52753
53663
|
pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
|
|
@@ -52856,9 +53766,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52856
53766
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
52857
53767
|
pass.setBindGroup(1, this.currentMaterial);
|
|
52858
53768
|
this.bindLights(pass);
|
|
52859
|
-
pass.setBindGroup(
|
|
52860
|
-
|
|
52861
|
-
|
|
53769
|
+
pass.setBindGroup(
|
|
53770
|
+
3,
|
|
53771
|
+
this.uniformBinding.bindGroup,
|
|
53772
|
+
this.uniformBinding.offsets
|
|
53773
|
+
);
|
|
52862
53774
|
pass.setVertexBuffer(0, quadGeometry.vertexBuffer);
|
|
52863
53775
|
pass.setVertexBuffer(1, instances.buffer);
|
|
52864
53776
|
pass.setIndexBuffer(quadGeometry.indexBuffer, quadGeometry.indexFormat);
|
|
@@ -52946,6 +53858,108 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52946
53858
|
uniformSignature() {
|
|
52947
53859
|
return `mesh:u${MESH_UNIFORM_SIZE}`;
|
|
52948
53860
|
}
|
|
53861
|
+
/**
|
|
53862
|
+
* The group-3 layout when a `ShaderEffect` is hosted on the mesh (#1658).
|
|
53863
|
+
*
|
|
53864
|
+
* `uMesh` keeps binding 0 — the spliced module is the engine's own mesh
|
|
53865
|
+
* shader and still declares it there — and the effect's own uniform block
|
|
53866
|
+
* takes binding 1, its texture/sampler pairs following at their declared
|
|
53867
|
+
* bindings shifted by the same one. That shift is why the body is rewritten
|
|
53868
|
+
* rather than pasted: a quad effect declares its block at
|
|
53869
|
+
* `@group(3) @binding(0)`, which is exactly where `uMesh` lives.
|
|
53870
|
+
* @param {object} effect - the hosted effect
|
|
53871
|
+
* @returns {GPUBindGroupLayout} the combined layout
|
|
53872
|
+
* @ignore
|
|
53873
|
+
* @internal
|
|
53874
|
+
*/
|
|
53875
|
+
hostedEffectLayout(effect) {
|
|
53876
|
+
const realization = effect.wgslRealization;
|
|
53877
|
+
const entries = [
|
|
53878
|
+
{
|
|
53879
|
+
binding: 0,
|
|
53880
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
53881
|
+
buffer: {
|
|
53882
|
+
type: "uniform",
|
|
53883
|
+
hasDynamicOffset: true,
|
|
53884
|
+
minBindingSize: MESH_UNIFORM_SIZE
|
|
53885
|
+
}
|
|
53886
|
+
}
|
|
53887
|
+
];
|
|
53888
|
+
let signature = `${this.uniformSignature()}|fx${realization.structSize}`;
|
|
53889
|
+
if (realization.structSize > 0) {
|
|
53890
|
+
entries.push({
|
|
53891
|
+
binding: 1,
|
|
53892
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
53893
|
+
buffer: {
|
|
53894
|
+
type: "uniform",
|
|
53895
|
+
hasDynamicOffset: true,
|
|
53896
|
+
minBindingSize: realization.structSize
|
|
53897
|
+
}
|
|
53898
|
+
});
|
|
53899
|
+
}
|
|
53900
|
+
return this.renderer.pipelineCache.getEffectLayout(signature, entries);
|
|
53901
|
+
}
|
|
53902
|
+
/**
|
|
53903
|
+
* Build the group-3 binding for a draw that hosts an effect: `uMesh`
|
|
53904
|
+
* plus a fresh snapshot of the effect's uniform mirror, plus its
|
|
53905
|
+
* textures.
|
|
53906
|
+
* @param {GPUDevice} device - the device
|
|
53907
|
+
* @param {object} renderer - the owning renderer
|
|
53908
|
+
* @param {object} effect - the hosted effect
|
|
53909
|
+
* @param {object} meshRegion - the arena region already holding `uMesh`
|
|
53910
|
+
* @returns {{bindGroup: GPUBindGroup, offsets: number[]}} the binding
|
|
53911
|
+
* @ignore
|
|
53912
|
+
* @internal
|
|
53913
|
+
*/
|
|
53914
|
+
bindHostedEffect(device, renderer, effect, meshRegion) {
|
|
53915
|
+
const realization = effect.wgslRealization;
|
|
53916
|
+
const entries = [
|
|
53917
|
+
{
|
|
53918
|
+
binding: 0,
|
|
53919
|
+
resource: { buffer: meshRegion.buffer, size: MESH_UNIFORM_SIZE }
|
|
53920
|
+
}
|
|
53921
|
+
];
|
|
53922
|
+
const offsets = [meshRegion.offset];
|
|
53923
|
+
if (realization.structSize > 0) {
|
|
53924
|
+
const region = renderer.effectUniformArena.alloc(
|
|
53925
|
+
realization.structSize,
|
|
53926
|
+
device.limits.minUniformBufferOffsetAlignment
|
|
53927
|
+
);
|
|
53928
|
+
device.queue.writeBuffer(
|
|
53929
|
+
region.buffer,
|
|
53930
|
+
region.offset,
|
|
53931
|
+
realization.cpu,
|
|
53932
|
+
0,
|
|
53933
|
+
realization.structSize
|
|
53934
|
+
);
|
|
53935
|
+
entries.push({
|
|
53936
|
+
binding: 1,
|
|
53937
|
+
resource: { buffer: region.buffer, size: realization.structSize }
|
|
53938
|
+
});
|
|
53939
|
+
offsets.push(region.offset);
|
|
53940
|
+
}
|
|
53941
|
+
const effectBuffer = offsets.length > 1 ? entries[1].resource.buffer : null;
|
|
53942
|
+
let byEffectPage = this.effectBindGroups.get(meshRegion.buffer);
|
|
53943
|
+
if (byEffectPage === void 0) {
|
|
53944
|
+
byEffectPage = /* @__PURE__ */ new Map();
|
|
53945
|
+
this.effectBindGroups.set(meshRegion.buffer, byEffectPage);
|
|
53946
|
+
}
|
|
53947
|
+
let byEffect = byEffectPage.get(effectBuffer);
|
|
53948
|
+
if (byEffect === void 0) {
|
|
53949
|
+
byEffect = /* @__PURE__ */ new Map();
|
|
53950
|
+
byEffectPage.set(effectBuffer, byEffect);
|
|
53951
|
+
}
|
|
53952
|
+
let bindGroup = byEffect.get(effect._effectId);
|
|
53953
|
+
if (bindGroup === void 0) {
|
|
53954
|
+
bindGroup = device.createBindGroup({
|
|
53955
|
+
label: "melonJS mesh uniforms + hosted effect",
|
|
53956
|
+
layout: this.hostedEffectLayout(effect),
|
|
53957
|
+
entries
|
|
53958
|
+
});
|
|
53959
|
+
byEffect.set(effect._effectId, bindGroup);
|
|
53960
|
+
}
|
|
53961
|
+
return { bindGroup, offsets };
|
|
53962
|
+
}
|
|
52949
53963
|
/**
|
|
52950
53964
|
* The shader family for the next recorded draw: the built-in family,
|
|
52951
53965
|
* or the hosted custom module when `renderer.drawMesh` routed a
|
|
@@ -52960,6 +53974,27 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52960
53974
|
* @internal
|
|
52961
53975
|
*/
|
|
52962
53976
|
activeShaderKey() {
|
|
53977
|
+
const effect = this.hostedEffect;
|
|
53978
|
+
if (effect != null) {
|
|
53979
|
+
const cache4 = this.renderer.pipelineCache;
|
|
53980
|
+
if (this._effectKeys?.epoch !== cache4.epoch) {
|
|
53981
|
+
this._effectKeys = { epoch: cache4.epoch, keys: /* @__PURE__ */ new Map() };
|
|
53982
|
+
}
|
|
53983
|
+
const host = `${this.vertexLayoutKey}|fx${effect._effectId}`;
|
|
53984
|
+
let key2 = this._effectKeys.keys.get(host);
|
|
53985
|
+
if (typeof key2 === "undefined") {
|
|
53986
|
+
key2 = cache4.registerShader(
|
|
53987
|
+
effect.wgslRealization.meshModule(this.shaderSource()),
|
|
53988
|
+
{
|
|
53989
|
+
bindGroupLayouts: this.bindGroupLayoutList(cache4),
|
|
53990
|
+
vertexLayoutKey: this.vertexLayoutKey,
|
|
53991
|
+
label: `melonJS ${this.vertexLayoutKey} + hosted effect`
|
|
53992
|
+
}
|
|
53993
|
+
);
|
|
53994
|
+
this._effectKeys.keys.set(host, key2);
|
|
53995
|
+
}
|
|
53996
|
+
return key2;
|
|
53997
|
+
}
|
|
52963
53998
|
const custom = this.customShader;
|
|
52964
53999
|
if (custom === null) {
|
|
52965
54000
|
return this.shaderKey;
|
|
@@ -52984,9 +54019,27 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52984
54019
|
cache3.frameLayout,
|
|
52985
54020
|
cache3.meshMaterialLayout,
|
|
52986
54021
|
cache3.emptyLayout,
|
|
52987
|
-
this.
|
|
54022
|
+
this.perDrawLayout()
|
|
52988
54023
|
];
|
|
52989
54024
|
}
|
|
54025
|
+
/**
|
|
54026
|
+
* The group-3 layout for the draw being recorded: `uMesh` alone, or
|
|
54027
|
+
* `uMesh` plus a hosted effect's own block.
|
|
54028
|
+
*
|
|
54029
|
+
* A single accessor on purpose. Every subclass builds its own positional
|
|
54030
|
+
* layout list — the lit tier swaps group 2 for its light block — and when
|
|
54031
|
+
* this choice was inlined into the base list only, the lit subclass kept
|
|
54032
|
+
* handing over the plain layout while the module declared the effect's
|
|
54033
|
+
* binding. The pipeline then failed to validate with "Binding doesn't
|
|
54034
|
+
* exist", every frame drawing that mesh was dropped, and the scene
|
|
54035
|
+
* flickered. Subclasses override the list; they must not re-decide this.
|
|
54036
|
+
* @returns {GPUBindGroupLayout} the group-3 layout
|
|
54037
|
+
* @ignore
|
|
54038
|
+
* @internal
|
|
54039
|
+
*/
|
|
54040
|
+
perDrawLayout() {
|
|
54041
|
+
return this.hostedEffect != null ? this.hostedEffectLayout(this.hostedEffect) : this.meshLayout;
|
|
54042
|
+
}
|
|
52990
54043
|
/**
|
|
52991
54044
|
* Resolve the group-1 material for a mesh: its texture through the
|
|
52992
54045
|
* texture store, honouring the mesh's own `textureRepeat` wrap override
|
|
@@ -53103,6 +54156,16 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53103
54156
|
0,
|
|
53104
54157
|
MESH_UNIFORM_SIZE
|
|
53105
54158
|
);
|
|
54159
|
+
const effect = this.hostedEffect;
|
|
54160
|
+
if (effect !== null && effect !== void 0) {
|
|
54161
|
+
this.uniformBinding = this.bindHostedEffect(
|
|
54162
|
+
device,
|
|
54163
|
+
renderer,
|
|
54164
|
+
effect,
|
|
54165
|
+
region
|
|
54166
|
+
);
|
|
54167
|
+
return;
|
|
54168
|
+
}
|
|
53106
54169
|
let bindGroup = this.uniformBindGroups.get(region.buffer);
|
|
53107
54170
|
if (typeof bindGroup === "undefined") {
|
|
53108
54171
|
bindGroup = device.createBindGroup({
|
|
@@ -53117,7 +54180,7 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53117
54180
|
});
|
|
53118
54181
|
this.uniformBindGroups.set(region.buffer, bindGroup);
|
|
53119
54182
|
}
|
|
53120
|
-
this.uniformBinding = { bindGroup,
|
|
54183
|
+
this.uniformBinding = { bindGroup, offsets: [region.offset] };
|
|
53121
54184
|
}
|
|
53122
54185
|
/**
|
|
53123
54186
|
* Write one vertex into the staging buffer. The base (unlit) layout is
|
|
@@ -53273,9 +54336,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53273
54336
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
53274
54337
|
pass.setBindGroup(1, this.currentMaterial);
|
|
53275
54338
|
this.bindLights(pass);
|
|
53276
|
-
pass.setBindGroup(
|
|
53277
|
-
|
|
53278
|
-
|
|
54339
|
+
pass.setBindGroup(
|
|
54340
|
+
3,
|
|
54341
|
+
this.uniformBinding.bindGroup,
|
|
54342
|
+
this.uniformBinding.offsets
|
|
54343
|
+
);
|
|
53279
54344
|
pass.setVertexBuffer(
|
|
53280
54345
|
0,
|
|
53281
54346
|
vertexRegion.buffer,
|
|
@@ -53386,9 +54451,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53386
54451
|
const frame = renderer.currentFrameBinding;
|
|
53387
54452
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
53388
54453
|
this.bindLights(pass);
|
|
53389
|
-
pass.setBindGroup(
|
|
53390
|
-
|
|
53391
|
-
|
|
54454
|
+
pass.setBindGroup(
|
|
54455
|
+
3,
|
|
54456
|
+
this.uniformBinding.bindGroup,
|
|
54457
|
+
this.uniformBinding.offsets
|
|
54458
|
+
);
|
|
53392
54459
|
pass.setVertexBuffer(0, geometry.vertexBuffer);
|
|
53393
54460
|
pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
|
|
53394
54461
|
const slices = mesh.textureGroups;
|
|
@@ -53438,8 +54505,10 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53438
54505
|
this.indexCount = 0;
|
|
53439
54506
|
this.currentMaterial = null;
|
|
53440
54507
|
this.customShader = null;
|
|
54508
|
+
this.hostedEffect = null;
|
|
53441
54509
|
this.uniformBinding = null;
|
|
53442
54510
|
this.uniformBindGroups.clear();
|
|
54511
|
+
this.effectBindGroups.clear();
|
|
53443
54512
|
this.releaseAllRetained();
|
|
53444
54513
|
}
|
|
53445
54514
|
/**
|
|
@@ -53547,7 +54616,9 @@ var WebGPULitMeshBatcher = class extends WebGPUMeshBatcher {
|
|
|
53547
54616
|
cache3.frameLayout,
|
|
53548
54617
|
cache3.meshMaterialLayout,
|
|
53549
54618
|
this.ensureLightsLayout(),
|
|
53550
|
-
this.meshLayout
|
|
54619
|
+
// NOT `this.meshLayout`: a hosted effect extends group 3, and the
|
|
54620
|
+
// base decides that for every tier (see `perDrawLayout`)
|
|
54621
|
+
this.perDrawLayout()
|
|
53551
54622
|
];
|
|
53552
54623
|
}
|
|
53553
54624
|
/**
|
|
@@ -54334,46 +55405,75 @@ var WebGPUPipelineCache = class _WebGPUPipelineCache {
|
|
|
54334
55405
|
primitive.cullMode = meshState.cullMode;
|
|
54335
55406
|
primitive.frontFace = meshState.frontFace;
|
|
54336
55407
|
}
|
|
54337
|
-
pipeline = this.device.createRenderPipeline(
|
|
54338
|
-
|
|
54339
|
-
|
|
54340
|
-
|
|
54341
|
-
|
|
54342
|
-
|
|
54343
|
-
|
|
54344
|
-
...meshState ? { constants: { enable_fog: fogFlag } } : {}
|
|
54345
|
-
},
|
|
54346
|
-
fragment: {
|
|
54347
|
-
module: this.modules[shaderKey],
|
|
54348
|
-
entryPoint: "fragment_main",
|
|
54349
|
-
...meshState ? { constants: { enable_fog: fogFlag } } : {},
|
|
54350
|
-
targets: [
|
|
54351
|
-
{
|
|
54352
|
-
format: this.format,
|
|
54353
|
-
blend: shaderKey === "clear" ? void 0 : gpuBlendState(blend, pma),
|
|
54354
|
-
writeMask: stencil.colorWriteMask
|
|
54355
|
-
}
|
|
54356
|
-
]
|
|
54357
|
-
},
|
|
54358
|
-
primitive,
|
|
54359
|
-
depthStencil: {
|
|
54360
|
-
format: DEPTH_STENCIL_FORMAT,
|
|
54361
|
-
// `!== false`, not `!!`: mesh draws that never set the field (and
|
|
54362
|
-
// every bare `{cullMode, frontFace}` literal) must keep writing
|
|
54363
|
-
// depth exactly as they did before the axis existed
|
|
54364
|
-
depthWriteEnabled: meshState ? meshState.depthWrite !== false : false,
|
|
54365
|
-
depthCompare: meshState ? "less-equal" : "always",
|
|
54366
|
-
stencilFront: stencil.stencil,
|
|
54367
|
-
stencilBack: stencil.stencil,
|
|
54368
|
-
stencilReadMask: stencil.readMask,
|
|
54369
|
-
stencilWriteMask: stencil.writeMask
|
|
54370
|
-
},
|
|
54371
|
-
multisample: { count: this.sampleCount }
|
|
54372
|
-
});
|
|
55408
|
+
pipeline = this.device.createRenderPipeline(
|
|
55409
|
+
this._descriptor(key, shaderKey, blend, pma, stencil, primitive, {
|
|
55410
|
+
vertexLayout,
|
|
55411
|
+
meshState,
|
|
55412
|
+
fogFlag
|
|
55413
|
+
})
|
|
55414
|
+
);
|
|
54373
55415
|
this.pipelines.set(key, pipeline);
|
|
54374
55416
|
}
|
|
54375
55417
|
return pipeline;
|
|
54376
55418
|
}
|
|
55419
|
+
/**
|
|
55420
|
+
* Assemble a pipeline descriptor.
|
|
55421
|
+
*
|
|
55422
|
+
* Split out of `get()` so the descriptor can be read on its own — it is
|
|
55423
|
+
* long, and every field in it is load-bearing for cache correctness: a
|
|
55424
|
+
* pipeline built from a descriptor that differs from the one a draw would
|
|
55425
|
+
* have produced is worse than no cache entry at all, because the draw
|
|
55426
|
+
* silently builds a second one under a key nothing will hit again.
|
|
55427
|
+
* @param {string} key - the cache key, used as the label
|
|
55428
|
+
* @param {string} shaderKey - the module family
|
|
55429
|
+
* @param {string} blend - normalized blend mode
|
|
55430
|
+
* @param {boolean} pma - premultiplied alpha
|
|
55431
|
+
* @param {object} stencil - the resolved stencil state
|
|
55432
|
+
* @param {object} primitive - the resolved primitive state
|
|
55433
|
+
* @param {object} opts - `{vertexLayout, meshState, fogFlag}`
|
|
55434
|
+
* @returns {GPURenderPipelineDescriptor} the descriptor
|
|
55435
|
+
* @ignore
|
|
55436
|
+
* @internal
|
|
55437
|
+
*/
|
|
55438
|
+
_descriptor(key, shaderKey, blend, pma, stencil, primitive, opts) {
|
|
55439
|
+
const { vertexLayout, meshState, fogFlag } = opts;
|
|
55440
|
+
return {
|
|
55441
|
+
label: `melonJS ${key}`,
|
|
55442
|
+
layout: this.pipelineLayouts[shaderKey],
|
|
55443
|
+
vertex: {
|
|
55444
|
+
module: this.modules[shaderKey],
|
|
55445
|
+
entryPoint: "vertex_main",
|
|
55446
|
+
buffers: vertexLayout ?? [],
|
|
55447
|
+
...meshState ? { constants: { enable_fog: fogFlag } } : {}
|
|
55448
|
+
},
|
|
55449
|
+
fragment: {
|
|
55450
|
+
module: this.modules[shaderKey],
|
|
55451
|
+
entryPoint: "fragment_main",
|
|
55452
|
+
...meshState ? { constants: { enable_fog: fogFlag } } : {},
|
|
55453
|
+
targets: [
|
|
55454
|
+
{
|
|
55455
|
+
format: this.format,
|
|
55456
|
+
blend: shaderKey === "clear" ? void 0 : gpuBlendState(blend, pma),
|
|
55457
|
+
writeMask: stencil.colorWriteMask
|
|
55458
|
+
}
|
|
55459
|
+
]
|
|
55460
|
+
},
|
|
55461
|
+
primitive,
|
|
55462
|
+
depthStencil: {
|
|
55463
|
+
format: DEPTH_STENCIL_FORMAT,
|
|
55464
|
+
// `!== false`, not `!!`: mesh draws that never set the field (and
|
|
55465
|
+
// every bare `{cullMode, frontFace}` literal) must keep writing
|
|
55466
|
+
// depth exactly as they did before the axis existed
|
|
55467
|
+
depthWriteEnabled: meshState ? meshState.depthWrite !== false : false,
|
|
55468
|
+
depthCompare: meshState ? "less-equal" : "always",
|
|
55469
|
+
stencilFront: stencil.stencil,
|
|
55470
|
+
stencilBack: stencil.stencil,
|
|
55471
|
+
stencilReadMask: stencil.readMask,
|
|
55472
|
+
stencilWriteMask: stencil.writeMask
|
|
55473
|
+
},
|
|
55474
|
+
multisample: { count: this.sampleCount }
|
|
55475
|
+
};
|
|
55476
|
+
}
|
|
54377
55477
|
/**
|
|
54378
55478
|
* drop every cached pipeline (device loss — the whole cache object is
|
|
54379
55479
|
* rebuilt by the renderer's restore path, this exists for symmetry)
|
|
@@ -57568,6 +58668,45 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
|
|
|
57568
58668
|
}
|
|
57569
58669
|
return this.renderPass;
|
|
57570
58670
|
}
|
|
58671
|
+
/**
|
|
58672
|
+
* Compile the WGSL module of every shader asset the loader has, ahead of
|
|
58673
|
+
* the frame that first draws it.
|
|
58674
|
+
*
|
|
58675
|
+
* WebGPU splits what WebGL fuses into one program. The **modules** for
|
|
58676
|
+
* the engine's own families — quad, blit, primitive, clear, and the mesh
|
|
58677
|
+
* tiers — are already created when the pipeline cache and the batchers
|
|
58678
|
+
* are constructed, so there is nothing lazy there to warm. A **pipeline**
|
|
58679
|
+
* is lazy, but it is keyed by draw state (blend, cull, depth-write) as
|
|
58680
|
+
* well as by shader, so the set a scene will ask for cannot be known from
|
|
58681
|
+
* here — and creating one costs almost nothing anyway, because WebGPU
|
|
58682
|
+
* records the intent and leaves the work to first use.
|
|
58683
|
+
*
|
|
58684
|
+
* What IS both lazy and knowable is a `ShaderEffect`'s module:
|
|
58685
|
+
* `prepareEffectBinding` builds it, and it is only ever called from
|
|
58686
|
+
* inside the draw path. Every effect the loader compiled from a
|
|
58687
|
+
* `{type: "shader"}` asset is already in `shaderList`, so those are
|
|
58688
|
+
* exactly the ones that can be brought forward — which is what makes
|
|
58689
|
+
* declaring a level's effects alongside that level's assets worth doing.
|
|
58690
|
+
*
|
|
58691
|
+
* Effects constructed inline when a stage builds are not here yet and
|
|
58692
|
+
* cannot be warmed; that is a property of when they come into existence,
|
|
58693
|
+
* not a limitation of this method.
|
|
58694
|
+
* @returns {Promise<void>} settles once those modules are built
|
|
58695
|
+
* @see {@link Renderer#prewarm}
|
|
58696
|
+
*/
|
|
58697
|
+
prewarm() {
|
|
58698
|
+
return Promise.resolve().then(() => {
|
|
58699
|
+
for (const effect of Object.values(shaderList)) {
|
|
58700
|
+
const realization = effect?.wgslRealization;
|
|
58701
|
+
if (realization === void 0 || realization.valid !== true) {
|
|
58702
|
+
continue;
|
|
58703
|
+
}
|
|
58704
|
+
if (realization.gpu === null || realization.gpu.epoch !== this.pipelineCache.epoch) {
|
|
58705
|
+
buildEffectGPU(this, effect, realization);
|
|
58706
|
+
}
|
|
58707
|
+
}
|
|
58708
|
+
});
|
|
58709
|
+
}
|
|
57571
58710
|
/**
|
|
57572
58711
|
* end the open pass and submit the frame's command buffer
|
|
57573
58712
|
* @override
|
|
@@ -57841,23 +58980,46 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
|
|
|
57841
58980
|
this.pendingDepthClear = true;
|
|
57842
58981
|
}
|
|
57843
58982
|
const customShader = this.customShader != null && this.customShader.isWebGPU === true ? this.customShader : null;
|
|
57844
|
-
|
|
58983
|
+
const hostedEffect = customShader === null && this.customShader != null && typeof this.customShader.wgslRealization !== "undefined" && meshHostingBlocker(
|
|
58984
|
+
this.customShader.wgslRealization.builtins,
|
|
58985
|
+
this.customShader.wgslRealization.textures,
|
|
58986
|
+
"wgsl"
|
|
58987
|
+
) === null && this.customShader.wgslRealization.meshModule(
|
|
58988
|
+
this.batchers.get(
|
|
58989
|
+
mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
|
|
58990
|
+
)?.shaderSource?.() ?? ""
|
|
58991
|
+
) !== null ? this.customShader : null;
|
|
58992
|
+
if (this.customShader != null && customShader === null && hostedEffect === null && this.meshEffectWarned !== true) {
|
|
57845
58993
|
this.meshEffectWarned = true;
|
|
58994
|
+
const realization = this.customShader.wgslRealization;
|
|
58995
|
+
const blocker = typeof realization !== "undefined" ? meshHostingBlocker(
|
|
58996
|
+
realization.builtins,
|
|
58997
|
+
realization.textures,
|
|
58998
|
+
"wgsl"
|
|
58999
|
+
) : null;
|
|
57846
59000
|
console.warn(
|
|
57847
|
-
"melonJS: this custom shader cannot be hosted on a Mesh by the WebGPU renderer (it carries no `wgsl` module) \u2014 the mesh draws with the built-in shading"
|
|
59001
|
+
blocker !== null ? `melonJS: this ShaderEffect cannot be hosted on a Mesh \u2014 ${blocker}. The mesh draws with the built-in shading.` : "melonJS: this custom shader cannot be hosted on a Mesh by the WebGPU renderer (it carries no `wgsl` module) \u2014 the mesh draws with the built-in shading"
|
|
57848
59002
|
);
|
|
57849
59003
|
}
|
|
57850
59004
|
const batcher = this.setBatcher(
|
|
57851
59005
|
mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
|
|
57852
59006
|
);
|
|
57853
59007
|
batcher.customShader = customShader;
|
|
59008
|
+
const instanced = mesh.instanceLayout !== void 0 && retained && this.supportsInstancing === true;
|
|
59009
|
+
if (instanced && hostedEffect !== null && this.instancedEffectWarned !== true) {
|
|
59010
|
+
this.instancedEffectWarned = true;
|
|
59011
|
+
console.warn(
|
|
59012
|
+
"melonJS: a ShaderEffect cannot be hosted on an InstancedMesh \u2014 the instanced shading is a separate shader family. The mesh draws with the built-in shading."
|
|
59013
|
+
);
|
|
59014
|
+
}
|
|
59015
|
+
batcher.hostedEffect = instanced ? null : hostedEffect;
|
|
57854
59016
|
const culling = mesh.cullBackFaces === true;
|
|
57855
59017
|
const state4 = batcher.meshState;
|
|
57856
59018
|
state4.cullMode = culling ? "back" : "none";
|
|
57857
59019
|
state4.frontFace = culling && retained && mesh.rightHanded !== true ? "cw" : "ccw";
|
|
57858
59020
|
try {
|
|
57859
59021
|
const tint = this.currentTint.toUint32(this.getGlobalAlpha());
|
|
57860
|
-
if (
|
|
59022
|
+
if (instanced) {
|
|
57861
59023
|
batcher.drawInstancedMesh(mesh, modelMatrix, tint);
|
|
57862
59024
|
} else if (retained) {
|
|
57863
59025
|
batcher.drawRetainedMesh(mesh, modelMatrix, tint);
|
|
@@ -57867,6 +59029,7 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
|
|
|
57867
59029
|
}
|
|
57868
59030
|
} finally {
|
|
57869
59031
|
batcher.customShader = null;
|
|
59032
|
+
batcher.hostedEffect = null;
|
|
57870
59033
|
}
|
|
57871
59034
|
}
|
|
57872
59035
|
/**
|
|
@@ -59349,6 +60512,7 @@ var defaultApplicationSettings = {
|
|
|
59349
60512
|
powerPreference: "default",
|
|
59350
60513
|
transparent: false,
|
|
59351
60514
|
antiAlias: false,
|
|
60515
|
+
prewarm: true,
|
|
59352
60516
|
textureFilter: "auto",
|
|
59353
60517
|
maxTextures: "auto",
|
|
59354
60518
|
castGroundShadow: true,
|
|
@@ -60914,7 +62078,7 @@ var Sprite3d = class extends Mesh {
|
|
|
60914
62078
|
* @param {number} x - world x position
|
|
60915
62079
|
* @param {number} y - world y position
|
|
60916
62080
|
* @param {object} settings - configuration
|
|
60917
|
-
* @param {HTMLImageElement|Texture2d|string} [settings.image] - the sprite texture (image name, image, or a {@link Texture2d} asset such as a {@link TextureAtlas}). Alias: `settings.texture`.
|
|
62081
|
+
* @param {HTMLImageElement|HTMLCanvasElement|Texture2d|string} [settings.image] - the sprite texture (image name, image, or a {@link Texture2d} asset such as a {@link TextureAtlas}). Alias: `settings.texture`.
|
|
60918
62082
|
* @param {number} [settings.width=settings.framewidth] - quad width in world units (pixels)
|
|
60919
62083
|
* @param {number} [settings.height=settings.width] - quad height in world units
|
|
60920
62084
|
* @param {number} [settings.framewidth] - width of a single frame within a spritesheet (enables frame animation)
|
|
@@ -60928,6 +62092,11 @@ var Sprite3d = class extends Mesh {
|
|
|
60928
62092
|
* @param {boolean} [settings.flipY=false] - mirror the sprite vertically (see {@link Sprite3d#flipY})
|
|
60929
62093
|
* @param {boolean} [settings.lit=false] - shade through the lit mesh batcher (see {@link Mesh})
|
|
60930
62094
|
* @param {number[]|Float32Array} [settings.emissive] - emissive color (see {@link Mesh})
|
|
62095
|
+
* @param {number} [settings.shadowGroundY] - world Y of the floor the blob shadow lands on. Omit and it falls back to the sprite's own base — which for a billboard moves with the camera, so a scene that knows where its floor is should say so.
|
|
62096
|
+
* @param {number} [settings.shadowOpacity=0.45] - opacity of the shadow directly beneath the sprite, before any height fade
|
|
62097
|
+
* @param {boolean} [settings.castGroundShadow] - give this sprite a blob ground shadow, overriding the application's `castGroundShadow` setting in both directions. Omit to inherit. Needs a GPU backend and a {@link Camera3d}.
|
|
62098
|
+
* @param {boolean} [settings.fog] - set `false` to exempt this sprite from the camera's distance fog ({@link Camera3d#setFog}); omit to fog whenever the camera does. A sun or a moon wants this — everything else at that distance dissolves into the haze, and so would it.
|
|
62099
|
+
* @param {boolean} [settings.transparent] - draw in the transparent pass (blended, back-to-front, no depth write) instead of the opaque one. Omit and the sprite goes transparent whenever its draw alpha is fractional; `true` for a soft-alpha sprite such as an additive glow; `false` to stay opaque however faded.
|
|
60931
62100
|
* @param {number} [settings.alphaCutoff=0.5] - alpha cutout threshold (see {@link Mesh}). The mesh pass is opaque (no alpha blending), so this defaults to `0.5` to discard a sprite's transparent background (clean cutout silhouette, correct depth, no sorting). Set `0` for a fully-opaque quad, or tune the threshold.
|
|
60932
62101
|
*/
|
|
60933
62102
|
constructor(x, y, settings) {
|
|
@@ -61010,6 +62179,7 @@ var Sprite3d = class extends Mesh {
|
|
|
61010
62179
|
// flatten it to an explicit false, silently opting every sprite out
|
|
61011
62180
|
// of a scene-wide default
|
|
61012
62181
|
castGroundShadow: settings.castGroundShadow,
|
|
62182
|
+
fog: settings.fog,
|
|
61013
62183
|
// raw for the same reason as above: `undefined` means "decide from
|
|
61014
62184
|
// the draw's alpha", and coercing it would pin every sprite opaque
|
|
61015
62185
|
transparent: settings.transparent,
|
|
@@ -64247,25 +65417,8 @@ var lerp3 = (a, b, t) => {
|
|
|
64247
65417
|
};
|
|
64248
65418
|
var NoiseTexture2d = class extends Texture2d {
|
|
64249
65419
|
/**
|
|
64250
|
-
* @param {
|
|
64251
|
-
*
|
|
64252
|
-
* `lacunarity`, `fractalType`, domain-warp settings) is forwarded when no
|
|
64253
|
-
* `noise` instance is given.
|
|
64254
|
-
* @param {number} [settings.width=256] - baked texture width in pixels
|
|
64255
|
-
* @param {number} [settings.height=256] - baked texture height in pixels
|
|
64256
|
-
* @param {Noise} [settings.noise] - an existing {@link Noise} to bake; when
|
|
64257
|
-
* omitted one is built from the forwarded settings
|
|
64258
|
-
* @param {boolean} [settings.seamless=false] - tile cleanly in both axes
|
|
64259
|
-
* @param {number} [settings.seamlessBlendSkirt=0.1] - edge blend band width
|
|
64260
|
-
* as a fraction (0..1) of the smaller dimension, when `seamless`
|
|
64261
|
-
* @param {boolean} [settings.invert=false] - invert the noise value (`1 - v`)
|
|
64262
|
-
* @param {boolean} [settings.asNormalMap=false] - encode as a normal map
|
|
64263
|
-
* @param {number} [settings.bumpStrength=1] - normal steepness when `asNormalMap`
|
|
64264
|
-
* @param {Gradient} [settings.colorRamp] - map the noise value to a color
|
|
64265
|
-
* @param {boolean} [settings.animated=false] - sample in 3D (`getNoise3d`)
|
|
64266
|
-
* using an internal `time` as the third axis, advanced by {@link NoiseTexture2d#update}
|
|
64267
|
-
* @param {number} [settings.speed=1] - animation speed in noise z-units per
|
|
64268
|
-
* second (only used while `animated`)
|
|
65420
|
+
* @param {NoiseTexture2dSettings} [settings] - bake settings, plus any
|
|
65421
|
+
* {@link NoiseSettings} for the field itself
|
|
64269
65422
|
*/
|
|
64270
65423
|
constructor(settings = {}) {
|
|
64271
65424
|
super();
|