melonjs 20.4.0 → 20.5.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/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 +1061 -166
- 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/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.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/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/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 +93 -1
- package/skills/melonjs-loading-assets/SKILL.md +97 -1
- package/skills/melonjs-performance/SKILL.md +55 -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 +56 -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.5.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
|
|
@@ -6956,9 +6990,18 @@ var Renderer = class _Renderer {
|
|
|
6956
6990
|
entry2.shader = this.customShader ?? null;
|
|
6957
6991
|
const v = this.currentTransform.val;
|
|
6958
6992
|
const m = modelMatrix.val;
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6993
|
+
let cx = 0;
|
|
6994
|
+
let cy = 0;
|
|
6995
|
+
let cz = 0;
|
|
6996
|
+
if (typeof mesh?._updateLocalCenter === "function") {
|
|
6997
|
+
mesh._updateLocalCenter();
|
|
6998
|
+
cx = mesh._lcx;
|
|
6999
|
+
cy = mesh._lcy;
|
|
7000
|
+
cz = mesh._lcz;
|
|
7001
|
+
}
|
|
7002
|
+
const mx = m[0] * cx + m[4] * cy + m[8] * cz + m[12];
|
|
7003
|
+
const my = m[1] * cx + m[5] * cy + m[9] * cz + m[13];
|
|
7004
|
+
const mz = m[2] * cx + m[6] * cy + m[10] * cz + m[14];
|
|
6962
7005
|
const dx = v[0] * mx + v[4] * my + v[8] * mz + v[12];
|
|
6963
7006
|
const dy = v[1] * mx + v[5] * my + v[9] * mz + v[13];
|
|
6964
7007
|
const dz = v[2] * mx + v[6] * my + v[10] * mz + v[14];
|
|
@@ -7761,6 +7804,31 @@ var Renderer = class _Renderer {
|
|
|
7761
7804
|
);
|
|
7762
7805
|
}
|
|
7763
7806
|
break;
|
|
7807
|
+
// Box3d — a 3D box has no 2D outline of its own, so draw the XY
|
|
7808
|
+
// FOOTPRINT it already hands every 2D consumer through its own
|
|
7809
|
+
// `getBounds()` (the body bounds, the broadphase pre-gate, and
|
|
7810
|
+
// this). Without a case here it fell through to the `default`
|
|
7811
|
+
// below, so switching on the debug panel's hitbox overlay threw
|
|
7812
|
+
// "Invalid geometry for fill/stroke" on any body carrying one.
|
|
7813
|
+
case "Box3d": {
|
|
7814
|
+
const footprint = shape.getBounds();
|
|
7815
|
+
if (fill) {
|
|
7816
|
+
this.fillRect(
|
|
7817
|
+
footprint.left,
|
|
7818
|
+
footprint.top,
|
|
7819
|
+
footprint.width,
|
|
7820
|
+
footprint.height
|
|
7821
|
+
);
|
|
7822
|
+
} else {
|
|
7823
|
+
this.strokeRect(
|
|
7824
|
+
footprint.left,
|
|
7825
|
+
footprint.top,
|
|
7826
|
+
footprint.width,
|
|
7827
|
+
footprint.height
|
|
7828
|
+
);
|
|
7829
|
+
}
|
|
7830
|
+
break;
|
|
7831
|
+
}
|
|
7764
7832
|
// Rect or Bounds
|
|
7765
7833
|
case "Rectangle":
|
|
7766
7834
|
case "Bounds":
|
|
@@ -10411,14 +10479,16 @@ var Sound = class {
|
|
|
10411
10479
|
sound2._node.gain.linearRampToValueAtTime(to, end);
|
|
10412
10480
|
}
|
|
10413
10481
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
ids[i]
|
|
10420
|
-
|
|
10421
|
-
|
|
10482
|
+
const isGroup = typeof id === "undefined";
|
|
10483
|
+
if (!(len > 0) || to === from) {
|
|
10484
|
+
if (isGroup) {
|
|
10485
|
+
this._volume = to;
|
|
10486
|
+
}
|
|
10487
|
+
this.volume(to, ids[i]);
|
|
10488
|
+
this._emit("fade", ids[i]);
|
|
10489
|
+
continue;
|
|
10490
|
+
}
|
|
10491
|
+
this._startFadeInterval(sound2, from, to, len, ids[i], isGroup);
|
|
10422
10492
|
}
|
|
10423
10493
|
}
|
|
10424
10494
|
return this;
|
|
@@ -11269,13 +11339,14 @@ function tone(opts) {
|
|
|
11269
11339
|
gain = 0.1,
|
|
11270
11340
|
attack = 5e-3,
|
|
11271
11341
|
pan = 0,
|
|
11272
|
-
pitchSlide = 1
|
|
11342
|
+
pitchSlide = 1,
|
|
11343
|
+
delay = 0
|
|
11273
11344
|
} = opts;
|
|
11274
11345
|
const freqs = Array.isArray(freq) ? freq : [freq];
|
|
11275
11346
|
if (freqs.length === 0) return;
|
|
11276
11347
|
_resumeIfSuspended(ctx);
|
|
11277
11348
|
const dur = Math.max(1e-3, duration2);
|
|
11278
|
-
const t0 = ctx.currentTime;
|
|
11349
|
+
const t0 = ctx.currentTime + Math.max(0, delay);
|
|
11279
11350
|
const t1 = t0 + dur;
|
|
11280
11351
|
const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
|
|
11281
11352
|
const panner2 = _connectToOutput(ctx, env, pan, t0);
|
|
@@ -11348,11 +11419,12 @@ function noise(opts) {
|
|
|
11348
11419
|
attack = 5e-3,
|
|
11349
11420
|
pan = 0,
|
|
11350
11421
|
filter,
|
|
11351
|
-
filterSweep = 1
|
|
11422
|
+
filterSweep = 1,
|
|
11423
|
+
delay = 0
|
|
11352
11424
|
} = opts;
|
|
11353
11425
|
_resumeIfSuspended(ctx);
|
|
11354
11426
|
const dur = Math.max(1e-3, duration2);
|
|
11355
|
-
const t0 = ctx.currentTime;
|
|
11427
|
+
const t0 = ctx.currentTime + Math.max(0, delay);
|
|
11356
11428
|
const t1 = t0 + dur;
|
|
11357
11429
|
const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
|
|
11358
11430
|
const sampleCount = Math.max(1, Math.ceil(dur * ctx.sampleRate));
|
|
@@ -15216,7 +15288,7 @@ var Line = class _Line extends Polygon {
|
|
|
15216
15288
|
*/
|
|
15217
15289
|
recalc() {
|
|
15218
15290
|
const edges = this.edges;
|
|
15219
|
-
const normals = this.
|
|
15291
|
+
const normals = this.edgeNormals;
|
|
15220
15292
|
const indices = this.indices;
|
|
15221
15293
|
const points = this.points;
|
|
15222
15294
|
if (points.length !== 2) {
|
|
@@ -17271,6 +17343,9 @@ var Renderable = class _Renderable extends Rect {
|
|
|
17271
17343
|
}
|
|
17272
17344
|
/**
|
|
17273
17345
|
* get the renderable alpha channel value<br>
|
|
17346
|
+
*
|
|
17347
|
+
* This is the renderable's OWN value. What it finally draws at is this
|
|
17348
|
+
* multiplied by every ancestor's — see {@link Renderable#alpha}.
|
|
17274
17349
|
* @returns {number} current opacity value between 0 and 1
|
|
17275
17350
|
*/
|
|
17276
17351
|
getOpacity() {
|
|
@@ -17668,7 +17743,10 @@ var Renderable = class _Renderable extends Rect {
|
|
|
17668
17743
|
if (this.applyAnchorTransform !== false) {
|
|
17669
17744
|
renderer.translate(-ax, -ay);
|
|
17670
17745
|
}
|
|
17671
|
-
renderer.setTint(
|
|
17746
|
+
renderer.setTint(
|
|
17747
|
+
this.tint,
|
|
17748
|
+
this.getOpacity() * (renderer.currentTint?.alpha ?? 1)
|
|
17749
|
+
);
|
|
17672
17750
|
renderer.setDepth(this.depth);
|
|
17673
17751
|
if (this.blendMode !== renderer.getBlendMode()) {
|
|
17674
17752
|
renderer.setBlendMode(this.blendMode);
|
|
@@ -18260,9 +18338,14 @@ var GLShader = class _GLShader {
|
|
|
18260
18338
|
/**
|
|
18261
18339
|
* Set the uniform to the given value
|
|
18262
18340
|
* @param {string} name - the uniform name
|
|
18263
|
-
* @param {
|
|
18341
|
+
* @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that
|
|
18342
|
+
* uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean;
|
|
18343
|
+
* vectors and matrices take an array, a `Float32Array`, or any object
|
|
18344
|
+
* exposing `toArray()` — which is every {@link Vector2d},
|
|
18345
|
+
* {@link Vector3d}, {@link Color} and {@link Matrix3d}.
|
|
18264
18346
|
* @example
|
|
18265
18347
|
* myShader.setUniform("uProjectionMatrix", this.projectionMatrix);
|
|
18348
|
+
* myShader.setUniform("uStrength", 0.5); // a scalar is a plain number
|
|
18266
18349
|
*/
|
|
18267
18350
|
setUniform(name, value) {
|
|
18268
18351
|
if (this.destroyed) {
|
|
@@ -18547,11 +18630,78 @@ function buildGLSLProgram(fragmentBody) {
|
|
|
18547
18630
|
return {
|
|
18548
18631
|
vertex: buildEffectVertex(builtins),
|
|
18549
18632
|
fragment: fragment2,
|
|
18633
|
+
// the annotation-stripped body, so a mesh-hosted effect can be
|
|
18634
|
+
// spliced into the engine's own mesh shader (#1658) without
|
|
18635
|
+
// re-parsing. Additive: the quad `vertex`/`fragment` above are
|
|
18636
|
+
// byte-identical to what they always were.
|
|
18637
|
+
body: builtins.body,
|
|
18638
|
+
builtins,
|
|
18550
18639
|
screenTextures: builtins.screenTextures,
|
|
18551
18640
|
noiseUV: builtins.noiseUV
|
|
18552
18641
|
};
|
|
18553
18642
|
}
|
|
18554
18643
|
|
|
18644
|
+
// src/video/effects/mesh_splice.js
|
|
18645
|
+
var HOOKS = [
|
|
18646
|
+
{
|
|
18647
|
+
pattern: /vec4\s+ME_effect\s*\(\s*vec4\s+c\s*,\s*vec2\s+uv\s*\)\s*\{[^}]*\}/,
|
|
18648
|
+
replacement: "vec4 ME_effect(vec4 c, vec2 uv) { return apply(c, uv); }"
|
|
18649
|
+
},
|
|
18650
|
+
{
|
|
18651
|
+
pattern: /fn\s+ME_effect\s*\(\s*c\s*:\s*vec4f\s*,\s*uv\s*:\s*vec2f\s*\)\s*->\s*vec4f\s*\{[^}]*\}/,
|
|
18652
|
+
replacement: "fn ME_effect(c : vec4f, uv : vec2f) -> vec4f { return apply(c, uv); }"
|
|
18653
|
+
}
|
|
18654
|
+
];
|
|
18655
|
+
var MESH_UNSUPPORTED = {
|
|
18656
|
+
noiseUV: "`noise_uv` (derived from a quad's image and object size)",
|
|
18657
|
+
screenUV: "`screen_uv` (needs the screen capture the quad tier takes)",
|
|
18658
|
+
screenTexture: "`screen_texture` (needs the screen capture)"
|
|
18659
|
+
};
|
|
18660
|
+
function meshHostingBlocker(builtins, textures, language2) {
|
|
18661
|
+
if (language2 === "wgsl" && builtins?.vColor === true) {
|
|
18662
|
+
return "it reads `vColor`, which only the 2D quad tier exposes to an effect body";
|
|
18663
|
+
}
|
|
18664
|
+
if (Array.isArray(textures) && textures.length > 0) {
|
|
18665
|
+
return "it declares its own texture samplers, which a mesh-hosted effect cannot bind yet \u2014 uniforms are supported, textures are not";
|
|
18666
|
+
}
|
|
18667
|
+
if (typeof builtins !== "object" || builtins === null) {
|
|
18668
|
+
return null;
|
|
18669
|
+
}
|
|
18670
|
+
const blocked = Object.keys(MESH_UNSUPPORTED).filter((flag) => {
|
|
18671
|
+
return builtins[flag] === true;
|
|
18672
|
+
}).map((flag) => {
|
|
18673
|
+
return MESH_UNSUPPORTED[flag];
|
|
18674
|
+
});
|
|
18675
|
+
return blocked.length === 0 ? null : `it uses ${blocked.join(" and ")}, which only the 2D quad tier provides`;
|
|
18676
|
+
}
|
|
18677
|
+
function hasSpliceMarkers(source) {
|
|
18678
|
+
return typeof source === "string" && HOOKS.some((hook) => {
|
|
18679
|
+
return hook.pattern.test(source);
|
|
18680
|
+
});
|
|
18681
|
+
}
|
|
18682
|
+
function spliceEffect(source, body) {
|
|
18683
|
+
if (typeof source !== "string" || typeof body !== "string") {
|
|
18684
|
+
return null;
|
|
18685
|
+
}
|
|
18686
|
+
for (const hook of HOOKS) {
|
|
18687
|
+
if (hook.pattern.test(source)) {
|
|
18688
|
+
return source.replace(hook.pattern, () => {
|
|
18689
|
+
return `${body}
|
|
18690
|
+
${hook.replacement}`;
|
|
18691
|
+
});
|
|
18692
|
+
}
|
|
18693
|
+
}
|
|
18694
|
+
return null;
|
|
18695
|
+
}
|
|
18696
|
+
function shiftGroup3(body, shift) {
|
|
18697
|
+
return body.replace(
|
|
18698
|
+
/@group\(\s*3\s*\)\s*@binding\(\s*(\d+)\s*\)/g,
|
|
18699
|
+
(_match, binding) => {
|
|
18700
|
+
return `@group(3) @binding(${Number(binding) + shift})`;
|
|
18701
|
+
}
|
|
18702
|
+
);
|
|
18703
|
+
}
|
|
18704
|
+
|
|
18555
18705
|
// src/video/effects/wgsl/layout.js
|
|
18556
18706
|
var TYPE_LAYOUT = {
|
|
18557
18707
|
f32: { align: 4, size: 4 },
|
|
@@ -18923,6 +19073,8 @@ var WGSLEffectRealization = class {
|
|
|
18923
19073
|
if (!this.valid) {
|
|
18924
19074
|
return;
|
|
18925
19075
|
}
|
|
19076
|
+
this.body = body;
|
|
19077
|
+
this.parsed = parsed;
|
|
18926
19078
|
this.layout = parsed.layout;
|
|
18927
19079
|
this.structSize = parsed.structSize;
|
|
18928
19080
|
this.textures = parsed.textures;
|
|
@@ -18930,6 +19082,7 @@ var WGSLEffectRealization = class {
|
|
|
18930
19082
|
const module = buildWGSLModule(body, parsed);
|
|
18931
19083
|
this.code = module.code;
|
|
18932
19084
|
this.builtinBindings = module.bindings;
|
|
19085
|
+
this.meshModules = /* @__PURE__ */ new Map();
|
|
18933
19086
|
this.cpu = new ArrayBuffer(Math.max(this.structSize, 0));
|
|
18934
19087
|
this.f32 = new Float32Array(this.cpu);
|
|
18935
19088
|
this.i32 = new Int32Array(this.cpu);
|
|
@@ -19006,9 +19159,30 @@ var WGSLEffectRealization = class {
|
|
|
19006
19159
|
releaseGPU() {
|
|
19007
19160
|
this.gpu = null;
|
|
19008
19161
|
}
|
|
19162
|
+
/**
|
|
19163
|
+
* This effect realized against the MESH contract: the engine's own mesh
|
|
19164
|
+
* module with the body spliced in at its `ME_effect` hook, and the
|
|
19165
|
+
* body's group-3 bindings shifted up one so `uMesh` keeps binding 0.
|
|
19166
|
+
*
|
|
19167
|
+
* The quad module (`this.code`) is untouched — a `Sprite` still gets the
|
|
19168
|
+
* exact text it always did.
|
|
19169
|
+
* @param {string} meshSource - the host mesh shader's WGSL text
|
|
19170
|
+
* @returns {string} the spliced module
|
|
19171
|
+
* @ignore
|
|
19172
|
+
* @internal
|
|
19173
|
+
*/
|
|
19174
|
+
meshModule(meshSource) {
|
|
19175
|
+
let code = this.meshModules.get(meshSource);
|
|
19176
|
+
if (typeof code === "undefined") {
|
|
19177
|
+
code = spliceEffect(meshSource, shiftGroup3(this.body, 1));
|
|
19178
|
+
this.meshModules.set(meshSource, code);
|
|
19179
|
+
}
|
|
19180
|
+
return code;
|
|
19181
|
+
}
|
|
19009
19182
|
};
|
|
19010
19183
|
|
|
19011
19184
|
// src/video/effects/shadereffect.js
|
|
19185
|
+
var _nextEffectId = 0;
|
|
19012
19186
|
var ShaderEffect = class _ShaderEffect {
|
|
19013
19187
|
/**
|
|
19014
19188
|
* whether this effect is active (false in Canvas mode, false after
|
|
@@ -19065,10 +19239,14 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19065
19239
|
return;
|
|
19066
19240
|
}
|
|
19067
19241
|
this._extraTextures = /* @__PURE__ */ new Map();
|
|
19242
|
+
this._uniformValues = /* @__PURE__ */ new Map();
|
|
19243
|
+
this._effectId = _nextEffectId++;
|
|
19068
19244
|
if (language2 === "glsl") {
|
|
19069
19245
|
const program = buildGLSLProgram(source);
|
|
19070
19246
|
this._screenTextureUniforms = program.screenTextures;
|
|
19071
19247
|
this._hasNoiseUV = program.noiseUV;
|
|
19248
|
+
this._meshBody = program.body;
|
|
19249
|
+
this._meshBuiltins = program.builtins;
|
|
19072
19250
|
this._shader = new GLShader(
|
|
19073
19251
|
renderer.gl,
|
|
19074
19252
|
program.vertex,
|
|
@@ -19138,12 +19316,23 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19138
19316
|
/**
|
|
19139
19317
|
* Set the uniform to the given value
|
|
19140
19318
|
* @param {string} name - the uniform name
|
|
19141
|
-
* @param {
|
|
19319
|
+
* @param {number|boolean|number[]|Float32Array|object} value - the value to assign to that
|
|
19320
|
+
* uniform. Scalars (`float`, `int`, `bool`) take a number or a boolean;
|
|
19321
|
+
* vectors and matrices take an array, a `Float32Array`, or any object
|
|
19322
|
+
* exposing `toArray()` — which is every {@link Vector2d},
|
|
19323
|
+
* {@link Vector3d}, {@link Color} and {@link Matrix3d}.
|
|
19324
|
+
* @example
|
|
19325
|
+
* // a scalar the body declares as `uniform float uStrength;`
|
|
19326
|
+
* fx.setUniform("uStrength", 0.5);
|
|
19327
|
+
* // a vec3 — an array, or anything with toArray()
|
|
19328
|
+
* fx.setUniform("uTint", [1.0, 0.82, 0.55]);
|
|
19329
|
+
* fx.setUniform("uOrigin", new me.Vector2d(0.5, 0.5));
|
|
19142
19330
|
*/
|
|
19143
19331
|
setUniform(name, value) {
|
|
19144
19332
|
if (this.destroyed === true) {
|
|
19145
19333
|
return;
|
|
19146
19334
|
}
|
|
19335
|
+
this._uniformValues?.set(name, value);
|
|
19147
19336
|
if (typeof this._shader !== "undefined") {
|
|
19148
19337
|
this._shader.setUniform(name, value);
|
|
19149
19338
|
} else if (typeof this.wgslRealization !== "undefined") {
|
|
@@ -19174,7 +19363,12 @@ var ShaderEffect = class _ShaderEffect {
|
|
|
19174
19363
|
* flow.setTime(me.timer.getTime() / 1000);
|
|
19175
19364
|
*/
|
|
19176
19365
|
setTime(seconds) {
|
|
19177
|
-
if (
|
|
19366
|
+
if (this.destroyed !== true) {
|
|
19367
|
+
this._uniformValues?.set("uTime", seconds);
|
|
19368
|
+
}
|
|
19369
|
+
if (typeof this._shader !== "undefined" && this.destroyed !== true && !this._shader.suspended && // optional: the map is null on a shader with no live context
|
|
19370
|
+
// behind it, which `suspended` alone does not cover
|
|
19371
|
+
typeof this._shader.uniforms?.uTime !== "undefined") {
|
|
19178
19372
|
this._shader.setUniform("uTime", seconds);
|
|
19179
19373
|
} else if (typeof this.wgslRealization !== "undefined" && this.destroyed !== true && this.wgslRealization.hasUniform("uTime")) {
|
|
19180
19374
|
this.wgslRealization.setUniform("uTime", seconds);
|
|
@@ -19862,6 +20056,8 @@ var Camera2d = class extends Renderable {
|
|
|
19862
20056
|
* @internal
|
|
19863
20057
|
*/
|
|
19864
20058
|
_zoom;
|
|
20059
|
+
/** backing store for {@link Camera2d#roll} — see the accessor */
|
|
20060
|
+
_roll;
|
|
19865
20061
|
/**
|
|
19866
20062
|
* the world-space projection matrix for non-default cameras (offset/zoomed).
|
|
19867
20063
|
* Maps world coordinates to the camera's screen viewport.
|
|
@@ -19954,6 +20150,7 @@ var Camera2d = class extends Renderable {
|
|
|
19954
20150
|
this.screenX = 0;
|
|
19955
20151
|
this.screenY = 0;
|
|
19956
20152
|
this.zoom = 1;
|
|
20153
|
+
this._roll = 0;
|
|
19957
20154
|
this.worldProjection = new Matrix3d();
|
|
19958
20155
|
this.screenProjection = new Matrix3d();
|
|
19959
20156
|
this._worldView = new Bounds();
|
|
@@ -20029,6 +20226,48 @@ var Camera2d = class extends Renderable {
|
|
|
20029
20226
|
* // zoom out to show the full level in a 180x100 minimap
|
|
20030
20227
|
* camera.zoom = Math.min(180 / levelWidth, 100 / levelHeight);
|
|
20031
20228
|
*/
|
|
20229
|
+
/**
|
|
20230
|
+
* Roll this camera in the screen plane, in radians — the 2D spelling of
|
|
20231
|
+
* {@link Camera3d#roll}, so a helper that banks "a camera" works on either.
|
|
20232
|
+
*
|
|
20233
|
+
* Absolute and idempotent: assigning the same angle twice leaves the view
|
|
20234
|
+
* where it was. It is a thin accessor over the camera's own
|
|
20235
|
+
* `currentTransform`, which is what {@link Renderable#rotate} writes and
|
|
20236
|
+
* what `worldToLocal` / `localToWorld` already undo — so picking under a
|
|
20237
|
+
* rolled 2D camera stays correct for free.
|
|
20238
|
+
*
|
|
20239
|
+
* A 2D camera has no pitch or yaw, so a screen-plane rotation is its only
|
|
20240
|
+
* rotation; on a `Camera3d` the roll is a real view angle instead, because
|
|
20241
|
+
* a 3D view is built from its three angles and never reads
|
|
20242
|
+
* `currentTransform`.
|
|
20243
|
+
*
|
|
20244
|
+
* This OWNS the camera's `currentTransform`: the setter rebuilds it rather
|
|
20245
|
+
* than composing onto it, so assigning every frame cannot drift and
|
|
20246
|
+
* `roll = 0` lands back on an exact identity. (A delta-based version does
|
|
20247
|
+
* neither — a banking camera would accumulate float error across thousands
|
|
20248
|
+
* of frames.) The cost is that a manual `rotate()` / `scale()` on the
|
|
20249
|
+
* camera itself is discarded by the next assignment; a camera has `zoom`
|
|
20250
|
+
* and `shake` for those jobs, and neither touches this matrix.
|
|
20251
|
+
* @default 0
|
|
20252
|
+
* @example
|
|
20253
|
+
* camera.roll = Math.PI / 32; // a slight dutch angle
|
|
20254
|
+
*/
|
|
20255
|
+
get roll() {
|
|
20256
|
+
return this._roll;
|
|
20257
|
+
}
|
|
20258
|
+
set roll(value) {
|
|
20259
|
+
if (value === this._roll) {
|
|
20260
|
+
return;
|
|
20261
|
+
}
|
|
20262
|
+
this._roll = value;
|
|
20263
|
+
this.currentTransform.identity();
|
|
20264
|
+
if (value !== 0) {
|
|
20265
|
+
this.currentTransform.rotate(value);
|
|
20266
|
+
}
|
|
20267
|
+
this.invCurrentTransform.copy(this.currentTransform).invert();
|
|
20268
|
+
this.updateBounds();
|
|
20269
|
+
this.isDirty = true;
|
|
20270
|
+
}
|
|
20032
20271
|
get zoom() {
|
|
20033
20272
|
return this._zoom;
|
|
20034
20273
|
}
|
|
@@ -20086,6 +20325,7 @@ var Camera2d = class extends Renderable {
|
|
|
20086
20325
|
this.smoothFollow = true;
|
|
20087
20326
|
this.damping = 1;
|
|
20088
20327
|
this.currentTransform.identity();
|
|
20328
|
+
this._roll = 0;
|
|
20089
20329
|
this.invCurrentTransform.identity().invert();
|
|
20090
20330
|
this.worldProjection.identity();
|
|
20091
20331
|
this.screenProjection.identity();
|
|
@@ -21855,9 +22095,8 @@ var Stage = class {
|
|
|
21855
22095
|
}
|
|
21856
22096
|
}
|
|
21857
22097
|
/**
|
|
21858
|
-
*
|
|
21859
|
-
*
|
|
21860
|
-
* @internal
|
|
22098
|
+
* Update the stage. Override it to run your own per-frame logic, and call
|
|
22099
|
+
* `super.update(dt)` so the world and cameras still advance.
|
|
21861
22100
|
* @param dt - time since the last update in milliseconds.
|
|
21862
22101
|
* @returns true if the stage needs to be redrawn
|
|
21863
22102
|
*/
|
|
@@ -21876,8 +22115,9 @@ var Stage = class {
|
|
|
21876
22115
|
* Lights are rendered as part of the world tree (they're now first-class
|
|
21877
22116
|
* Renderables) and the ambient overlay pass runs inside each Camera's
|
|
21878
22117
|
* post-effect FBO bracket via {@link Stage#drawLighting}.
|
|
21879
|
-
*
|
|
21880
|
-
*
|
|
22118
|
+
*
|
|
22119
|
+
* Override it to draw under or over the world, calling `super.draw(...)`
|
|
22120
|
+
* where the world itself belongs in that order.
|
|
21881
22121
|
* @param renderer - the renderer object to draw with
|
|
21882
22122
|
* @param world - the world object to draw
|
|
21883
22123
|
*/
|
|
@@ -23080,6 +23320,9 @@ function preloadFontFace(data2, onload2, onerror) {
|
|
|
23080
23320
|
}
|
|
23081
23321
|
|
|
23082
23322
|
// src/lighting/light3d.ts
|
|
23323
|
+
function readVector(out, value) {
|
|
23324
|
+
return Array.isArray(value) ? out.set(value[0], value[1], value[2]) : out.set(value.x, value.y, value.z);
|
|
23325
|
+
}
|
|
23083
23326
|
var Light3d = class extends Renderable {
|
|
23084
23327
|
/** `"directional"`, `"ambient"`, `"point"` or `"spot"`. */
|
|
23085
23328
|
type;
|
|
@@ -23111,20 +23354,12 @@ var Light3d = class extends Renderable {
|
|
|
23111
23354
|
this.type = options.type ?? "directional";
|
|
23112
23355
|
this.direction = new Vector3d(0, 1, 0);
|
|
23113
23356
|
if (options.direction) {
|
|
23114
|
-
this.direction.
|
|
23115
|
-
options.direction[0],
|
|
23116
|
-
options.direction[1],
|
|
23117
|
-
options.direction[2]
|
|
23118
|
-
);
|
|
23357
|
+
readVector(this.direction, options.direction);
|
|
23119
23358
|
}
|
|
23120
23359
|
this.direction.normalize();
|
|
23121
23360
|
this.position = new Vector3d(0, 0, 0);
|
|
23122
23361
|
if (options.position) {
|
|
23123
|
-
this.position.
|
|
23124
|
-
options.position[0],
|
|
23125
|
-
options.position[1],
|
|
23126
|
-
options.position[2]
|
|
23127
|
-
);
|
|
23362
|
+
readVector(this.position, options.position);
|
|
23128
23363
|
}
|
|
23129
23364
|
if (options.color instanceof Color) {
|
|
23130
23365
|
this.color = options.color;
|
|
@@ -23505,6 +23740,7 @@ var Frustum = class {
|
|
|
23505
23740
|
// src/camera/camera3d.ts
|
|
23506
23741
|
var AXIS_X = new Vector3d(1, 0, 0);
|
|
23507
23742
|
var AXIS_Y = new Vector3d(0, 1, 0);
|
|
23743
|
+
var AXIS_Z = new Vector3d(0, 0, 1);
|
|
23508
23744
|
var _viewMatrix = new Matrix3d();
|
|
23509
23745
|
var _viewProjection = new Matrix3d();
|
|
23510
23746
|
var _wsPoint = new Vector3d();
|
|
@@ -23610,6 +23846,29 @@ var Camera3d = class extends Camera2d {
|
|
|
23610
23846
|
* @default 0
|
|
23611
23847
|
*/
|
|
23612
23848
|
yaw;
|
|
23849
|
+
/**
|
|
23850
|
+
* Z-axis rotation in radians (bank the horizon). Positive values
|
|
23851
|
+
* roll the camera clockwise, so the world tilts anticlockwise —
|
|
23852
|
+
* the view from a cockpit banking right.
|
|
23853
|
+
*
|
|
23854
|
+
* Completes the `pitch` / `yaw` / `roll` trio. Note this is NOT the
|
|
23855
|
+
* inherited {@link Renderable#rotation}: a 3D camera builds its view
|
|
23856
|
+
* from these three angles and never reads `currentTransform`, which
|
|
23857
|
+
* is why `camera.rotate()` on a `Camera3d` is silently inert. A
|
|
23858
|
+
* `Camera2d` is the other way round — it has no `roll` because a
|
|
23859
|
+
* screen-plane rotation IS its only rotation, and `rotate()` already
|
|
23860
|
+
* does it through `currentTransform`.
|
|
23861
|
+
* @default 0
|
|
23862
|
+
* @example
|
|
23863
|
+
* // bank with the player's steering, as a flight game would
|
|
23864
|
+
* camera.roll = (player.pos.x / PLAY_BOUND_X) * MAX_BANK;
|
|
23865
|
+
*/
|
|
23866
|
+
get roll() {
|
|
23867
|
+
return this._roll;
|
|
23868
|
+
}
|
|
23869
|
+
set roll(value) {
|
|
23870
|
+
this._roll = value;
|
|
23871
|
+
}
|
|
23613
23872
|
/**
|
|
23614
23873
|
* World-space offset from the followed target. When `target` is
|
|
23615
23874
|
* set via {@link Camera2d#follow}, the camera position resolves to
|
|
@@ -23650,6 +23909,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23650
23909
|
});
|
|
23651
23910
|
this.pitch = 0;
|
|
23652
23911
|
this.yaw = 0;
|
|
23912
|
+
this.roll = 0;
|
|
23653
23913
|
this.followOffset = new Vector3d(0, 0, 0);
|
|
23654
23914
|
this.lookAhead = new Vector3d(0, 0, 1);
|
|
23655
23915
|
this.near = this.frustum.near;
|
|
@@ -23925,6 +24185,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23925
24185
|
_basis.identity();
|
|
23926
24186
|
_basis.rotate(this.yaw, AXIS_Y);
|
|
23927
24187
|
_basis.rotate(this.pitch, AXIS_X);
|
|
24188
|
+
_basis.rotate(this.roll, AXIS_Z);
|
|
23928
24189
|
const b = _basis.val;
|
|
23929
24190
|
state4.heightAxis[0] = k * b[1];
|
|
23930
24191
|
state4.heightAxis[1] = k * b[5];
|
|
@@ -23952,6 +24213,7 @@ var Camera3d = class extends Camera2d {
|
|
|
23952
24213
|
_basis.identity();
|
|
23953
24214
|
_basis.rotate(this.yaw, AXIS_Y);
|
|
23954
24215
|
_basis.rotate(this.pitch, AXIS_X);
|
|
24216
|
+
_basis.rotate(this.roll, AXIS_Z);
|
|
23955
24217
|
const v = _basis.val;
|
|
23956
24218
|
right.set(v[0], v[1], v[2]);
|
|
23957
24219
|
up.set(v[4], v[5], v[6]);
|
|
@@ -24050,6 +24312,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24050
24312
|
* @internal
|
|
24051
24313
|
*/
|
|
24052
24314
|
_applyContainerViewTransform(container, translateX, translateY) {
|
|
24315
|
+
if (this.roll !== 0) {
|
|
24316
|
+
container.rotate(-this.roll, AXIS_Z);
|
|
24317
|
+
}
|
|
24053
24318
|
if (this.pitch !== 0) {
|
|
24054
24319
|
container.rotate(-this.pitch, AXIS_X);
|
|
24055
24320
|
}
|
|
@@ -24076,6 +24341,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24076
24341
|
if (this.pitch !== 0) {
|
|
24077
24342
|
container.rotate(this.pitch, AXIS_X);
|
|
24078
24343
|
}
|
|
24344
|
+
if (this.roll !== 0) {
|
|
24345
|
+
container.rotate(this.roll, AXIS_Z);
|
|
24346
|
+
}
|
|
24079
24347
|
}
|
|
24080
24348
|
/**
|
|
24081
24349
|
* Point the camera at a world-space target by deriving pitch and
|
|
@@ -24276,6 +24544,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24276
24544
|
*/
|
|
24277
24545
|
worldToScreen(world, out = new Vector2d()) {
|
|
24278
24546
|
_viewMatrix.identity();
|
|
24547
|
+
if (this.roll !== 0) {
|
|
24548
|
+
_viewMatrix.rotate(-this.roll, AXIS_Z);
|
|
24549
|
+
}
|
|
24279
24550
|
if (this.pitch !== 0) {
|
|
24280
24551
|
_viewMatrix.rotate(-this.pitch, AXIS_X);
|
|
24281
24552
|
}
|
|
@@ -24308,6 +24579,9 @@ var Camera3d = class extends Camera2d {
|
|
|
24308
24579
|
*/
|
|
24309
24580
|
_rebuildFrustumPlanes() {
|
|
24310
24581
|
_viewMatrix.identity();
|
|
24582
|
+
if (this.roll !== 0) {
|
|
24583
|
+
_viewMatrix.rotate(-this.roll, AXIS_Z);
|
|
24584
|
+
}
|
|
24311
24585
|
if (this.pitch !== 0) {
|
|
24312
24586
|
_viewMatrix.rotate(-this.pitch, AXIS_X);
|
|
24313
24587
|
}
|
|
@@ -24614,38 +24888,44 @@ function buildTextureGroups(groups, materials, shared, framewidth, frameheight)
|
|
|
24614
24888
|
}
|
|
24615
24889
|
return slices.length === 1 && slices[0].texture !== shared ? slices : void 0;
|
|
24616
24890
|
}
|
|
24891
|
+
function declaredSize(settings) {
|
|
24892
|
+
const width = settings.width;
|
|
24893
|
+
const height = settings.height;
|
|
24894
|
+
if (settings.normalize !== false || typeof settings.scale !== "number" || width > 0 && height > 0) {
|
|
24895
|
+
return [width, height];
|
|
24896
|
+
}
|
|
24897
|
+
const vertices = typeof settings.model === "string" ? getOBJ(settings.model)?.vertices : settings.vertices;
|
|
24898
|
+
if (!vertices || vertices.length < 3) {
|
|
24899
|
+
return [width, height];
|
|
24900
|
+
}
|
|
24901
|
+
let minX = Infinity;
|
|
24902
|
+
let maxX = -Infinity;
|
|
24903
|
+
let minY = Infinity;
|
|
24904
|
+
let maxY = -Infinity;
|
|
24905
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
24906
|
+
if (vertices[i] < minX) {
|
|
24907
|
+
minX = vertices[i];
|
|
24908
|
+
}
|
|
24909
|
+
if (vertices[i] > maxX) {
|
|
24910
|
+
maxX = vertices[i];
|
|
24911
|
+
}
|
|
24912
|
+
if (vertices[i + 1] < minY) {
|
|
24913
|
+
minY = vertices[i + 1];
|
|
24914
|
+
}
|
|
24915
|
+
if (vertices[i + 1] > maxY) {
|
|
24916
|
+
maxY = vertices[i + 1];
|
|
24917
|
+
}
|
|
24918
|
+
}
|
|
24919
|
+
return [
|
|
24920
|
+
width > 0 ? width : (maxX - minX) * settings.scale,
|
|
24921
|
+
height > 0 ? height : (maxY - minY) * settings.scale
|
|
24922
|
+
];
|
|
24923
|
+
}
|
|
24617
24924
|
var Mesh = class _Mesh extends Renderable {
|
|
24618
24925
|
/**
|
|
24619
24926
|
* @param {number} x - the x screen position of the mesh object
|
|
24620
24927
|
* @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.
|
|
24928
|
+
* @param {MeshSettings} settings - Configuration parameters for the Mesh object
|
|
24649
24929
|
* @example
|
|
24650
24930
|
* // create from OBJ + MTL (texture auto-resolved from material)
|
|
24651
24931
|
* let mesh = new me.Mesh(0, 0, {
|
|
@@ -24692,7 +24972,7 @@ var Mesh = class _Mesh extends Renderable {
|
|
|
24692
24972
|
* mesh.rotate(Math.PI / 4);
|
|
24693
24973
|
*/
|
|
24694
24974
|
constructor(x, y, settings) {
|
|
24695
|
-
super(x, y, settings
|
|
24975
|
+
super(x, y, ...declaredSize(settings));
|
|
24696
24976
|
let objGroups = null;
|
|
24697
24977
|
let sourceNormals = settings.normals;
|
|
24698
24978
|
if (typeof settings.model === "string") {
|
|
@@ -25175,6 +25455,66 @@ var Mesh = class _Mesh extends Renderable {
|
|
|
25175
25455
|
renderer?.deleteMeshGeometry?.(this);
|
|
25176
25456
|
super.onDeactivateEvent(...args);
|
|
25177
25457
|
}
|
|
25458
|
+
/**
|
|
25459
|
+
* The centre of this mesh's own geometry, in MODEL space, written into
|
|
25460
|
+
* `_lcx` / `_lcy` / `_lcz`.
|
|
25461
|
+
*
|
|
25462
|
+
* The transparent pass needs a point that represents where a mesh
|
|
25463
|
+
* actually is. Its origin does not: a mesh whose vertices run away from
|
|
25464
|
+
* the origin — a ground plane authored from one corner, a river that
|
|
25465
|
+
* trails behind the boat it is parented to — sits nowhere near it. Keyed
|
|
25466
|
+
* on that origin, such a mesh sorts by an authoring choice rather than by
|
|
25467
|
+
* its position.
|
|
25468
|
+
*
|
|
25469
|
+
* Cached against `_geometryVersion`, so the vertex walk happens once per
|
|
25470
|
+
* geometry edit rather than once per queued draw. An origin-centred mesh
|
|
25471
|
+
* — which is most of them — resolves to (0, 0, 0) and keys exactly as it
|
|
25472
|
+
* did before.
|
|
25473
|
+
* @ignore
|
|
25474
|
+
* @internal
|
|
25475
|
+
*/
|
|
25476
|
+
_updateLocalCenter() {
|
|
25477
|
+
if (this._lcVersion === this._geometryVersion) {
|
|
25478
|
+
return;
|
|
25479
|
+
}
|
|
25480
|
+
const v = this.originalVertices;
|
|
25481
|
+
const n = this.vertexCount;
|
|
25482
|
+
let minX = Infinity;
|
|
25483
|
+
let minY = Infinity;
|
|
25484
|
+
let minZ = Infinity;
|
|
25485
|
+
let maxX = -Infinity;
|
|
25486
|
+
let maxY = -Infinity;
|
|
25487
|
+
let maxZ = -Infinity;
|
|
25488
|
+
if (v !== void 0 && n > 0) {
|
|
25489
|
+
for (let i = 0; i < n; i++) {
|
|
25490
|
+
const x = v[i * 3];
|
|
25491
|
+
const y = v[i * 3 + 1];
|
|
25492
|
+
const z = v[i * 3 + 2];
|
|
25493
|
+
if (x < minX) {
|
|
25494
|
+
minX = x;
|
|
25495
|
+
}
|
|
25496
|
+
if (x > maxX) {
|
|
25497
|
+
maxX = x;
|
|
25498
|
+
}
|
|
25499
|
+
if (y < minY) {
|
|
25500
|
+
minY = y;
|
|
25501
|
+
}
|
|
25502
|
+
if (y > maxY) {
|
|
25503
|
+
maxY = y;
|
|
25504
|
+
}
|
|
25505
|
+
if (z < minZ) {
|
|
25506
|
+
minZ = z;
|
|
25507
|
+
}
|
|
25508
|
+
if (z > maxZ) {
|
|
25509
|
+
maxZ = z;
|
|
25510
|
+
}
|
|
25511
|
+
}
|
|
25512
|
+
}
|
|
25513
|
+
this._lcx = Number.isFinite(minX) ? (minX + maxX) * 0.5 : 0;
|
|
25514
|
+
this._lcy = Number.isFinite(minY) ? (minY + maxY) * 0.5 : 0;
|
|
25515
|
+
this._lcz = Number.isFinite(minZ) ? (minZ + maxZ) * 0.5 : 0;
|
|
25516
|
+
this._lcVersion = this._geometryVersion;
|
|
25517
|
+
}
|
|
25178
25518
|
/**
|
|
25179
25519
|
* The mesh's world-space 3D axis-aligned bounding box. This is the 3D analog
|
|
25180
25520
|
* of {@link Renderable#getBounds} (which returns a flat 2D box from
|
|
@@ -25587,10 +25927,7 @@ var InstancedMesh = class extends Mesh {
|
|
|
25587
25927
|
/**
|
|
25588
25928
|
* @param {number} x - the x coordinate of the group origin
|
|
25589
25929
|
* @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.
|
|
25930
|
+
* @param {InstancedMeshSettings} settings - every {@link Mesh} setting, plus the instancing ones
|
|
25594
25931
|
*/
|
|
25595
25932
|
constructor(x, y, settings) {
|
|
25596
25933
|
super(x, y, settings);
|
|
@@ -26652,9 +26989,16 @@ var Container = class _Container extends Renderable {
|
|
|
26652
26989
|
}
|
|
26653
26990
|
/**
|
|
26654
26991
|
* Automatically set the specified property of all childs to the given value
|
|
26992
|
+
*
|
|
26993
|
+
* This ASSIGNS to the children — after it, their own property carries the
|
|
26994
|
+
* value. To fade a subtree without touching what the children are, set
|
|
26995
|
+
* {@link Renderable#alpha} on the container instead: opacity cascades at
|
|
26996
|
+
* draw time and leaves every child's own value alone.
|
|
26655
26997
|
* @param {string} prop - property name
|
|
26656
|
-
* @param {
|
|
26998
|
+
* @param {*} value - property value; whatever the named property holds, so a
|
|
26999
|
+
* flag is a boolean and a colour is a string, not only an object
|
|
26657
27000
|
* @param {boolean} [recursive=false] - recursively apply the value to child containers if true
|
|
27001
|
+
* @see Renderable#alpha
|
|
26658
27002
|
*/
|
|
26659
27003
|
setChildsProperty(prop, value, recursive) {
|
|
26660
27004
|
this.forEach((child) => {
|
|
@@ -27053,6 +27397,13 @@ function linearToSrgb8(c) {
|
|
|
27053
27397
|
|
|
27054
27398
|
// src/level/gltf/GLTFModel.js
|
|
27055
27399
|
var IDENTITY16 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
27400
|
+
var _rootScratch = new Array(16);
|
|
27401
|
+
function partAbsolutePosition() {
|
|
27402
|
+
if (this._absPos === void 0) {
|
|
27403
|
+
this._absPos = vector3dPool.get();
|
|
27404
|
+
}
|
|
27405
|
+
return this._absPos.set(this.pos.x, this.pos.y, this.depth);
|
|
27406
|
+
}
|
|
27056
27407
|
var _t = [0, 0, 0];
|
|
27057
27408
|
var _r = [0, 0, 0, 1];
|
|
27058
27409
|
var _s = [1, 1, 1];
|
|
@@ -27060,7 +27411,7 @@ var _val = [0, 0, 0, 0];
|
|
|
27060
27411
|
var _localScratch = new Array(16);
|
|
27061
27412
|
var GLTFModel = class extends Container {
|
|
27062
27413
|
/**
|
|
27063
|
-
* @param {
|
|
27414
|
+
* @param {import("../../loader/loader.js").GLTFData} data - the parsed glTF descriptor, as returned by {@link loader.getGLTF}
|
|
27064
27415
|
* @param {object} [options]
|
|
27065
27416
|
* @param {number} [options.scale=1] - pixels per glTF unit (uniform scene scale)
|
|
27066
27417
|
* @param {boolean} [options.rightHanded=true] - glTF Y-up → engine Y-down via a rotation (no mirror)
|
|
@@ -27073,6 +27424,7 @@ var GLTFModel = class extends Container {
|
|
|
27073
27424
|
this.scale = options.scale ?? 1;
|
|
27074
27425
|
this._zSign = options.rightHanded !== false ? -1 : 1;
|
|
27075
27426
|
this.autoDepth = false;
|
|
27427
|
+
this.autoTransform = false;
|
|
27076
27428
|
this.applyAnchorTransform = false;
|
|
27077
27429
|
this._nodes = data2.graph.nodes;
|
|
27078
27430
|
this._roots = data2.graph.roots;
|
|
@@ -27085,6 +27437,9 @@ var GLTFModel = class extends Container {
|
|
|
27085
27437
|
const dz = b.max[2] - b.min[2];
|
|
27086
27438
|
const radius = Math.hypot(dx, dy, dz) / 2 * this.scale;
|
|
27087
27439
|
const boxSize = Math.max(radius, 1) * Math.SQRT2;
|
|
27440
|
+
this._extent = Math.max(radius, 1);
|
|
27441
|
+
this._extentX = (b.min[0] + b.max[0]) / 2 * this.scale;
|
|
27442
|
+
this._extentY = -((b.min[1] + b.max[1]) / 2) * this.scale;
|
|
27088
27443
|
const lit = options.lit === true;
|
|
27089
27444
|
const rightHanded = options.rightHanded !== false;
|
|
27090
27445
|
const castGroundShadow = typeof options.castGroundShadow === "boolean" ? options.castGroundShadow : void 0;
|
|
@@ -27142,6 +27497,7 @@ var GLTFModel = class extends Container {
|
|
|
27142
27497
|
mesh.vertexColors = prim.colors;
|
|
27143
27498
|
}
|
|
27144
27499
|
mesh.name = node.name;
|
|
27500
|
+
mesh.getAbsolutePosition = partAbsolutePosition;
|
|
27145
27501
|
(this._meshByNode[idx] ??= []).push(mesh);
|
|
27146
27502
|
this.addChild(mesh);
|
|
27147
27503
|
}
|
|
@@ -27192,7 +27548,7 @@ var GLTFModel = class extends Container {
|
|
|
27192
27548
|
* another clip when this one ends, a `function` legacy completion callback
|
|
27193
27549
|
* (return `false` to hold the final pose), or an options object.
|
|
27194
27550
|
* @param {string} name - animation clip id (see {@link GLTFModel#getAnimationNames})
|
|
27195
|
-
* @param {
|
|
27551
|
+
* @param {AnimationOptionsInput} [options] - loop / chain / completion behavior
|
|
27196
27552
|
* @param {boolean} [preserveTime=false] - keep the current playback time instead of restarting at 0
|
|
27197
27553
|
* @returns {GLTFModel} this, for chaining
|
|
27198
27554
|
* @example
|
|
@@ -27259,7 +27615,7 @@ var GLTFModel = class extends Container {
|
|
|
27259
27615
|
* (and start) it, or with no argument to resume after {@link GLTFModel#pause}.
|
|
27260
27616
|
* Always clears the paused state.
|
|
27261
27617
|
* @param {string} [name] - clip id to play; omit to just resume
|
|
27262
|
-
* @param {
|
|
27618
|
+
* @param {AnimationOptionsInput} [options] - loop / chain / completion behavior (see {@link GLTFModel#setCurrentAnimation})
|
|
27263
27619
|
* @returns {GLTFModel} this, for chaining
|
|
27264
27620
|
* @example
|
|
27265
27621
|
* model.play("walk"); // switch to + play "walk"
|
|
@@ -27362,6 +27718,52 @@ var GLTFModel = class extends Container {
|
|
|
27362
27718
|
_isLive() {
|
|
27363
27719
|
return this.pos !== void 0;
|
|
27364
27720
|
}
|
|
27721
|
+
/**
|
|
27722
|
+
* {@link Container#draw} translates the renderer by `pos` so children draw
|
|
27723
|
+
* in container-local space. This rig's children are already in world space,
|
|
27724
|
+
* so that translation is undone here rather than doubling the placement.
|
|
27725
|
+
* Paired with `autoTransform = false`, which suppresses the matching
|
|
27726
|
+
* rotation fold.
|
|
27727
|
+
* @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
|
|
27728
|
+
* @param {Camera2d} [viewport] - the camera rendering this frame
|
|
27729
|
+
*/
|
|
27730
|
+
draw(renderer, viewport) {
|
|
27731
|
+
renderer.translate(-this.pos.x, -this.pos.y);
|
|
27732
|
+
super.draw(renderer, viewport);
|
|
27733
|
+
}
|
|
27734
|
+
/**
|
|
27735
|
+
* Where this model is, as a 2D box — what culling, picking and the physics
|
|
27736
|
+
* broadphase all read.
|
|
27737
|
+
*
|
|
27738
|
+
* A `Container` has no dimensions of its own, and one left that way reports
|
|
27739
|
+
* an EMPTY bounds: a model claiming to be nowhere, which the broadphase
|
|
27740
|
+
* cannot place. A rig's size is known at load from the glTF scene AABB, so
|
|
27741
|
+
* this is a placement of a measurement taken once in the constructor rather
|
|
27742
|
+
* than a walk of the parts — `Container#updateBounds` with
|
|
27743
|
+
* `enableChildBoundsUpdate` would re-measure the whole rig on every call,
|
|
27744
|
+
* and that flag is for a group whose extent really is its members' union.
|
|
27745
|
+
*
|
|
27746
|
+
* The box is the model's bounding SPHERE squared off, so a turned model
|
|
27747
|
+
* still reports something that contains it. Generous bounds only cost
|
|
27748
|
+
* broadphase pruning; tight ones lose contacts.
|
|
27749
|
+
* @param {boolean} [absolute=true] - in world rather than local coordinates
|
|
27750
|
+
* @returns {Bounds} this model's bounding box
|
|
27751
|
+
*/
|
|
27752
|
+
updateBounds(absolute = true) {
|
|
27753
|
+
const bounds = this.getBounds();
|
|
27754
|
+
const e = this._extent;
|
|
27755
|
+
bounds.setMinMax(
|
|
27756
|
+
this._extentX - e,
|
|
27757
|
+
this._extentY - e,
|
|
27758
|
+
this._extentX + e,
|
|
27759
|
+
this._extentY + e
|
|
27760
|
+
);
|
|
27761
|
+
if (absolute === true) {
|
|
27762
|
+
const absPos = this.getAbsolutePosition();
|
|
27763
|
+
bounds.translate(absPos.x, absPos.y);
|
|
27764
|
+
}
|
|
27765
|
+
return bounds;
|
|
27766
|
+
}
|
|
27365
27767
|
/**
|
|
27366
27768
|
* @ignore
|
|
27367
27769
|
* @internal
|
|
@@ -27369,10 +27771,48 @@ var GLTFModel = class extends Container {
|
|
|
27369
27771
|
_pose() {
|
|
27370
27772
|
const clip = this.current.name ? this.anim[this.current.name] : null;
|
|
27371
27773
|
const t = this.current.time;
|
|
27774
|
+
const parent = this._rootMatrix();
|
|
27372
27775
|
for (const root of this._roots) {
|
|
27373
|
-
this._visit(root,
|
|
27776
|
+
this._visit(root, parent, clip, t);
|
|
27374
27777
|
}
|
|
27375
27778
|
}
|
|
27779
|
+
/**
|
|
27780
|
+
* The model's own placement, as the parent transform every root node hangs
|
|
27781
|
+
* from — so `pos`, `depth` and `currentTransform` move, turn and scale the
|
|
27782
|
+
* whole rig the way they do for any other renderable.
|
|
27783
|
+
*
|
|
27784
|
+
* It has to be handed to the DFS in **glTF space**, because that is where
|
|
27785
|
+
* the node matrices live and `_applyWorldToMesh` only bridges to engine
|
|
27786
|
+
* space afterwards. The bridge is `D = diag(1, -1, zSign)` and a uniform
|
|
27787
|
+
* `scale`, so the inverse trip is a division by `scale` for the translation
|
|
27788
|
+
* and a conjugation `D · R · D` for the rotation/scale — `D` is its own
|
|
27789
|
+
* inverse, which is what makes the conjugation this cheap.
|
|
27790
|
+
* @returns {number[]} 16-element column-major matrix
|
|
27791
|
+
* @ignore
|
|
27792
|
+
* @internal
|
|
27793
|
+
*/
|
|
27794
|
+
_rootMatrix() {
|
|
27795
|
+
const { x, y, z } = this.pos;
|
|
27796
|
+
const t = this.currentTransform.val;
|
|
27797
|
+
const s = this.scale;
|
|
27798
|
+
if (x === 0 && y === 0 && z === 0 && this.currentTransform.isIdentity()) {
|
|
27799
|
+
return IDENTITY16;
|
|
27800
|
+
}
|
|
27801
|
+
const out = _rootScratch;
|
|
27802
|
+
const zs = this._zSign;
|
|
27803
|
+
for (let col = 0; col < 3; col++) {
|
|
27804
|
+
const dc = col === 0 ? 1 : col === 1 ? -1 : zs;
|
|
27805
|
+
out[col * 4] = t[col * 4] * dc;
|
|
27806
|
+
out[col * 4 + 1] = -t[col * 4 + 1] * dc;
|
|
27807
|
+
out[col * 4 + 2] = zs * t[col * 4 + 2] * dc;
|
|
27808
|
+
out[col * 4 + 3] = 0;
|
|
27809
|
+
}
|
|
27810
|
+
out[12] = x / s;
|
|
27811
|
+
out[13] = -y / s;
|
|
27812
|
+
out[14] = zs * z / s;
|
|
27813
|
+
out[15] = 1;
|
|
27814
|
+
return out;
|
|
27815
|
+
}
|
|
27376
27816
|
/**
|
|
27377
27817
|
* DFS one node: compose its local matrix, multiply by the parent world,
|
|
27378
27818
|
* apply to its meshes, recurse into children.
|
|
@@ -30203,13 +30643,47 @@ function createShapeObject(settings) {
|
|
|
30203
30643
|
}
|
|
30204
30644
|
|
|
30205
30645
|
// src/renderable/text/textstyle.js
|
|
30646
|
+
var VERTICAL_TOLERANCE = 0.1;
|
|
30647
|
+
function resolveFill(context, style) {
|
|
30648
|
+
const gradient = style.fillGradient;
|
|
30649
|
+
if (gradient === void 0) {
|
|
30650
|
+
return style.fillStyle.toRGBA();
|
|
30651
|
+
}
|
|
30652
|
+
const lines = style._text?.length ?? 1;
|
|
30653
|
+
if (style.gradientPerLine !== true || lines <= 1 || gradient.type !== "linear") {
|
|
30654
|
+
return gradient.toCanvasGradient(context);
|
|
30655
|
+
}
|
|
30656
|
+
const [x0, y0, x1, y1] = gradient.coords;
|
|
30657
|
+
const span = y1 - y0;
|
|
30658
|
+
if (span === 0 || Math.abs(x1 - x0) > Math.abs(span) * VERTICAL_TOLERANCE) {
|
|
30659
|
+
return gradient.toCanvasGradient(context);
|
|
30660
|
+
}
|
|
30661
|
+
const lineHeight = style.metrics.lineHeight();
|
|
30662
|
+
const blockHeight = lines * lineHeight;
|
|
30663
|
+
if (blockHeight <= 0) {
|
|
30664
|
+
return gradient.toCanvasGradient(context);
|
|
30665
|
+
}
|
|
30666
|
+
const repeated = context.createLinearGradient(x0, 0, x1, blockHeight);
|
|
30667
|
+
for (let line = 0; line < lines; line++) {
|
|
30668
|
+
for (const stop2 of gradient.colorStops) {
|
|
30669
|
+
const y = line * lineHeight + y0 + stop2.offset * span;
|
|
30670
|
+
repeated.addColorStop(
|
|
30671
|
+
Math.min(1, Math.max(0, y / blockHeight)),
|
|
30672
|
+
stop2.color
|
|
30673
|
+
);
|
|
30674
|
+
}
|
|
30675
|
+
}
|
|
30676
|
+
return repeated;
|
|
30677
|
+
}
|
|
30206
30678
|
function setContextStyle(context, style) {
|
|
30679
|
+
const fill = resolveFill(context, style);
|
|
30207
30680
|
context.font = style.font;
|
|
30208
|
-
context.fillStyle =
|
|
30681
|
+
context.fillStyle = fill;
|
|
30209
30682
|
context.strokeStyle = style.strokeStyle.toRGBA();
|
|
30210
30683
|
context.lineWidth = style.lineWidth;
|
|
30211
30684
|
context.textAlign = style.textAlign;
|
|
30212
30685
|
context.textBaseline = style.textBaseline;
|
|
30686
|
+
return fill;
|
|
30213
30687
|
}
|
|
30214
30688
|
|
|
30215
30689
|
// src/renderable/text/text.js
|
|
@@ -30237,7 +30711,8 @@ var Text = class extends Renderable {
|
|
|
30237
30711
|
* @param {object} settings - the text configuration
|
|
30238
30712
|
* @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
30713
|
* @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
|
|
30714
|
+
* @param {Color|Gradient|string} [settings.fillStyle="#000000"] - a CSS color value used to fill the glyphs, or a {@link Gradient} to ramp them
|
|
30715
|
+
* @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
30716
|
* @param {Color|string} [settings.strokeStyle="#000000"] - a CSS color value used for the glyph outline (drawn when `lineWidth` > 0)
|
|
30242
30717
|
* @param {number} [settings.lineWidth=0] - outline width in pixels (0 = no stroke)
|
|
30243
30718
|
* @param {string} [settings.textAlign="left"] - horizontal text alignment ("left", "center", "right")
|
|
@@ -30245,6 +30720,8 @@ var Text = class extends Renderable {
|
|
|
30245
30720
|
* @param {number} [settings.lineHeight=1.0] - line spacing height
|
|
30246
30721
|
* @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
30722
|
* @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixels of a line before it wraps
|
|
30723
|
+
* @param {boolean} [settings.bold=false] - render the face bold, as {@link Text#bold} does
|
|
30724
|
+
* @param {boolean} [settings.italic=false] - render the face italic, as {@link Text#italic} does
|
|
30248
30725
|
* @param {(string|string[])} [settings.text=""] - a string, or an array of strings
|
|
30249
30726
|
* @example
|
|
30250
30727
|
* // a styled, word-wrapped, multi-line label using a generic system font
|
|
@@ -30263,6 +30740,23 @@ var Text = class extends Renderable {
|
|
|
30263
30740
|
* label.setOpacity(0.8); // per-object transparency
|
|
30264
30741
|
* app.world.addChild(label);
|
|
30265
30742
|
* @example
|
|
30743
|
+
* // a gradient fill: `fillStyle` takes a Gradient as well as a colour.
|
|
30744
|
+
* // Its coordinates are the label's OWN bake, so (0, 0) is the top-left of
|
|
30745
|
+
* // the render box and the ramp below runs down exactly one line.
|
|
30746
|
+
* const ramp = app.renderer.createLinearGradient(0, 0, 0, 32);
|
|
30747
|
+
* ramp.addColorStop(0, "#fffdf0");
|
|
30748
|
+
* ramp.addColorStop(1, "#ffa71d");
|
|
30749
|
+
*
|
|
30750
|
+
* app.world.addChild(new Text(8, 8, {
|
|
30751
|
+
* font: "sans-serif",
|
|
30752
|
+
* size: 32,
|
|
30753
|
+
* fillStyle: ramp, // ramps the glyphs...
|
|
30754
|
+
* strokeStyle: "#000000", // ...while the outline keeps its own colour
|
|
30755
|
+
* lineWidth: 1,
|
|
30756
|
+
* text: "GAME\nOVER", // every line restarts the ramp by default
|
|
30757
|
+
* // gradientPerLine: false, // ...or span ONE ramp across both lines
|
|
30758
|
+
* }));
|
|
30759
|
+
* @example
|
|
30266
30760
|
* // a web font (loaded via the fontface loader) is used by its family name
|
|
30267
30761
|
* loader.preload(
|
|
30268
30762
|
* [{ name: "kenpixel", type: "fontface", src: "data/font/kenvector.woff2" }],
|
|
@@ -30294,11 +30788,16 @@ var Text = class extends Renderable {
|
|
|
30294
30788
|
if (typeof this.fillStyle === "undefined") {
|
|
30295
30789
|
this.fillStyle = colorPool.get(0, 0, 0);
|
|
30296
30790
|
}
|
|
30791
|
+
this.fillGradient = void 0;
|
|
30792
|
+
this.gradientPerLine = true;
|
|
30297
30793
|
if (typeof this.strokeStyle === "undefined") {
|
|
30298
30794
|
this.strokeStyle = colorPool.get(0, 0, 0);
|
|
30299
30795
|
}
|
|
30796
|
+
this.fillGradient = void 0;
|
|
30300
30797
|
if (typeof settings.fillStyle !== "undefined") {
|
|
30301
|
-
if (settings.fillStyle instanceof
|
|
30798
|
+
if (settings.fillStyle instanceof Gradient) {
|
|
30799
|
+
this.fillGradient = settings.fillStyle;
|
|
30800
|
+
} else if (settings.fillStyle instanceof Color) {
|
|
30302
30801
|
this.fillStyle.copy(settings.fillStyle);
|
|
30303
30802
|
} else {
|
|
30304
30803
|
this.fillStyle.parseCSS(settings.fillStyle);
|
|
@@ -30311,6 +30810,9 @@ var Text = class extends Renderable {
|
|
|
30311
30810
|
this.strokeStyle.parseCSS(settings.strokeStyle);
|
|
30312
30811
|
}
|
|
30313
30812
|
}
|
|
30813
|
+
if (typeof settings.gradientPerLine === "boolean") {
|
|
30814
|
+
this.gradientPerLine = settings.gradientPerLine;
|
|
30815
|
+
}
|
|
30314
30816
|
this.lineWidth = settings.lineWidth || 0;
|
|
30315
30817
|
this.textAlign = settings.textAlign || "left";
|
|
30316
30818
|
this.textBaseline = settings.textBaseline || "top";
|
|
@@ -30425,9 +30927,10 @@ var Text = class extends Renderable {
|
|
|
30425
30927
|
true
|
|
30426
30928
|
);
|
|
30427
30929
|
const width = Math.ceil(this.metrics.width / 32) * 32;
|
|
30428
|
-
const height = Math.ceil(
|
|
30930
|
+
const height = Math.ceil(
|
|
30931
|
+
(this.metrics.height + this.metrics.inkPadTop + this.metrics.inkPadBottom) / 32
|
|
30932
|
+
) * 32;
|
|
30429
30933
|
const renderer = this.parentApp?.renderer ?? game.renderer;
|
|
30430
|
-
this.canvasTexture.invalidate(renderer);
|
|
30431
30934
|
if (this.canvasTexture.width < width || this.canvasTexture.height < height) {
|
|
30432
30935
|
this.canvasTexture.resize(width, height);
|
|
30433
30936
|
}
|
|
@@ -30438,6 +30941,7 @@ var Text = class extends Renderable {
|
|
|
30438
30941
|
this.pos.x - this.metrics.x,
|
|
30439
30942
|
this.pos.y - this.metrics.y
|
|
30440
30943
|
);
|
|
30944
|
+
this.canvasTexture.invalidate(renderer);
|
|
30441
30945
|
this.isDirty = true;
|
|
30442
30946
|
return this;
|
|
30443
30947
|
}
|
|
@@ -30464,9 +30968,18 @@ var Text = class extends Renderable {
|
|
|
30464
30968
|
}
|
|
30465
30969
|
/**
|
|
30466
30970
|
* the ratio of visible characters (0.0 to 1.0).
|
|
30467
|
-
* Setting this automatically updates {@link visibleCharacters}.
|
|
30971
|
+
* Setting this automatically updates {@link Text#visibleCharacters}.
|
|
30972
|
+
*
|
|
30973
|
+
* This is the one to tween: it is independent of how many characters the
|
|
30974
|
+
* label holds, so a reveal takes the same time whatever the string is.
|
|
30468
30975
|
* @public
|
|
30469
30976
|
* @type {number}
|
|
30977
|
+
* @default 1.0
|
|
30978
|
+
* @see Text#visibleCharacters
|
|
30979
|
+
* @example
|
|
30980
|
+
* // reveal over two seconds, regardless of length
|
|
30981
|
+
* label.visibleRatio = 0;
|
|
30982
|
+
* new Tween(label).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
|
|
30470
30983
|
*/
|
|
30471
30984
|
get visibleRatio() {
|
|
30472
30985
|
if (this._visibleCharacters === -1) {
|
|
@@ -30535,6 +31048,14 @@ var Text = class extends Renderable {
|
|
|
30535
31048
|
* measure the given text size in pixels
|
|
30536
31049
|
* @param {string} [text] - the text to be measured
|
|
30537
31050
|
* @returns {TextMetrics} a TextMetrics object defining the dimensions of the given piece of text
|
|
31051
|
+
* @example
|
|
31052
|
+
* // size a panel around a label
|
|
31053
|
+
* const size = label.measureText();
|
|
31054
|
+
* panel.resize(size.width + 16, size.height + 16);
|
|
31055
|
+
*
|
|
31056
|
+
* // or measure a string the label does not currently hold, to reserve
|
|
31057
|
+
* // room for the widest state a counter will reach
|
|
31058
|
+
* const widest = label.measureText("00:00").width;
|
|
30538
31059
|
*/
|
|
30539
31060
|
measureText(text = this._text) {
|
|
30540
31061
|
return this.metrics.measureText(text, this.canvasTexture.context);
|
|
@@ -30544,8 +31065,8 @@ var Text = class extends Renderable {
|
|
|
30544
31065
|
* @param {CanvasRenderer|WebGLRenderer} renderer - Reference to the destination renderer instance
|
|
30545
31066
|
*/
|
|
30546
31067
|
draw(renderer) {
|
|
31068
|
+
this.metrics.updateOrigin();
|
|
30547
31069
|
if (this.isDirty) {
|
|
30548
|
-
this.canvasTexture.invalidate(renderer);
|
|
30549
31070
|
this.canvasTexture.clear();
|
|
30550
31071
|
this._drawFont(
|
|
30551
31072
|
this.canvasTexture.context,
|
|
@@ -30553,9 +31074,10 @@ var Text = class extends Renderable {
|
|
|
30553
31074
|
this.pos.x - this.metrics.x,
|
|
30554
31075
|
this.pos.y - this.metrics.y
|
|
30555
31076
|
);
|
|
31077
|
+
this.canvasTexture.invalidate(renderer);
|
|
30556
31078
|
}
|
|
30557
31079
|
let x = this.metrics.x;
|
|
30558
|
-
let y = this.metrics.y;
|
|
31080
|
+
let y = this.metrics.y - this.metrics.inkPadTop;
|
|
30559
31081
|
if (renderer.settings.subPixel === false) {
|
|
30560
31082
|
x = ~~x;
|
|
30561
31083
|
y = ~~y;
|
|
@@ -30567,25 +31089,54 @@ var Text = class extends Renderable {
|
|
|
30567
31089
|
* @internal
|
|
30568
31090
|
*/
|
|
30569
31091
|
_drawFont(context, text, x, y) {
|
|
30570
|
-
|
|
30571
|
-
|
|
30572
|
-
|
|
30573
|
-
|
|
30574
|
-
|
|
30575
|
-
|
|
30576
|
-
|
|
31092
|
+
context.save();
|
|
31093
|
+
context.translate(0, this.metrics.inkPadTop);
|
|
31094
|
+
const fill = setContextStyle(context, this);
|
|
31095
|
+
const fills = this.fillStyle.alpha > 0;
|
|
31096
|
+
const strokes = this.lineWidth > 0 && this.strokeStyle.alpha > 0;
|
|
31097
|
+
const masked = fills && this.fillGradient !== void 0;
|
|
31098
|
+
const eachLine = (paint) => {
|
|
31099
|
+
let remaining = this.visibleCharacters;
|
|
31100
|
+
let lineY = y;
|
|
31101
|
+
for (let i = 0; i < text.length; i++) {
|
|
31102
|
+
let string = text[i].trimEnd();
|
|
31103
|
+
if (remaining !== -1) {
|
|
31104
|
+
if (remaining <= 0) {
|
|
31105
|
+
break;
|
|
31106
|
+
}
|
|
31107
|
+
string = string.substring(0, remaining);
|
|
31108
|
+
remaining -= string.length;
|
|
30577
31109
|
}
|
|
30578
|
-
string
|
|
30579
|
-
|
|
30580
|
-
}
|
|
30581
|
-
if (this.fillStyle.alpha > 0) {
|
|
30582
|
-
context.fillText(string, x, y);
|
|
31110
|
+
paint(string, x, lineY);
|
|
31111
|
+
lineY += this.metrics.lineHeight();
|
|
30583
31112
|
}
|
|
30584
|
-
|
|
30585
|
-
|
|
31113
|
+
};
|
|
31114
|
+
if (fills) {
|
|
31115
|
+
if (masked === true) {
|
|
31116
|
+
context.fillStyle = "#000000";
|
|
30586
31117
|
}
|
|
30587
|
-
|
|
31118
|
+
eachLine((line, lineX, lineY) => {
|
|
31119
|
+
context.fillText(line, lineX, lineY);
|
|
31120
|
+
});
|
|
30588
31121
|
}
|
|
31122
|
+
if (masked === true) {
|
|
31123
|
+
const canvas = context.canvas;
|
|
31124
|
+
context.globalCompositeOperation = "source-in";
|
|
31125
|
+
context.fillStyle = fill;
|
|
31126
|
+
context.fillRect(
|
|
31127
|
+
-1,
|
|
31128
|
+
-this.metrics.inkPadTop - 1,
|
|
31129
|
+
canvas.width + 2,
|
|
31130
|
+
canvas.height + 2
|
|
31131
|
+
);
|
|
31132
|
+
context.globalCompositeOperation = "source-over";
|
|
31133
|
+
}
|
|
31134
|
+
if (strokes) {
|
|
31135
|
+
eachLine((line, lineX, lineY) => {
|
|
31136
|
+
context.strokeText(line, lineX, lineY);
|
|
31137
|
+
});
|
|
31138
|
+
}
|
|
31139
|
+
context.restore();
|
|
30589
31140
|
return this.metrics;
|
|
30590
31141
|
}
|
|
30591
31142
|
/**
|
|
@@ -30597,6 +31148,7 @@ var Text = class extends Renderable {
|
|
|
30597
31148
|
const renderer = this.parentApp?.renderer ?? game.renderer;
|
|
30598
31149
|
this.canvasTexture.destroy(renderer);
|
|
30599
31150
|
this.canvasTexture = void 0;
|
|
31151
|
+
this.fillGradient = void 0;
|
|
30600
31152
|
colorPool.release(this.fillStyle);
|
|
30601
31153
|
colorPool.release(this.strokeStyle);
|
|
30602
31154
|
this.fillStyle = this.strokeStyle = void 0;
|
|
@@ -30607,6 +31159,8 @@ var Text = class extends Renderable {
|
|
|
30607
31159
|
};
|
|
30608
31160
|
|
|
30609
31161
|
// src/renderable/text/textmetrics.js
|
|
31162
|
+
var INK_SLACK = 1;
|
|
31163
|
+
var INK_GUESS = 0.25;
|
|
30610
31164
|
var TextMetrics = class extends Bounds {
|
|
30611
31165
|
/**
|
|
30612
31166
|
* @param {Text|BitmapText} ancestor - the parent object that contains this TextMetrics object
|
|
@@ -30690,17 +31244,55 @@ var TextMetrics = class extends Bounds {
|
|
|
30690
31244
|
this.glyphYOffset = fontData.glyphMinTop * scaleY;
|
|
30691
31245
|
this.glyphMaxBottom = fontData.glyphMaxBottom * scaleY;
|
|
30692
31246
|
}
|
|
31247
|
+
this.inkPadTop = 0;
|
|
31248
|
+
this.inkPadBottom = 0;
|
|
31249
|
+
if (!isBitmapText && strings.length > 0 && typeof context !== "undefined") {
|
|
31250
|
+
const style = this.ancestor;
|
|
31251
|
+
const stroke = style.lineWidth > 0 && style.strokeStyle.alpha > 0 ? style.lineWidth / 2 : 0;
|
|
31252
|
+
const first = context.measureText(strings[0].trimEnd());
|
|
31253
|
+
const last = strings.length === 1 ? first : context.measureText(strings[strings.length - 1].trimEnd());
|
|
31254
|
+
const above = first.actualBoundingBoxAscent;
|
|
31255
|
+
const below = last.actualBoundingBoxDescent;
|
|
31256
|
+
const measured = Number.isFinite(above) && Number.isFinite(below);
|
|
31257
|
+
const top = measured ? above : style.fontSize * INK_GUESS;
|
|
31258
|
+
const bottom = measured ? below : style.fontSize * INK_GUESS;
|
|
31259
|
+
this.inkPadTop = Math.max(0, Math.ceil(top + stroke) + INK_SLACK);
|
|
31260
|
+
this.inkPadBottom = Math.max(
|
|
31261
|
+
0,
|
|
31262
|
+
Math.ceil(this.inkPadTop + bottom + stroke - this.lineHeight()) + INK_SLACK
|
|
31263
|
+
);
|
|
31264
|
+
}
|
|
30693
31265
|
this.width = Math.ceil(this.width);
|
|
30694
31266
|
this.height = Math.ceil(this.height);
|
|
31267
|
+
this.updateOrigin();
|
|
31268
|
+
if (typeof context !== "undefined") {
|
|
31269
|
+
context.restore();
|
|
31270
|
+
}
|
|
31271
|
+
return this;
|
|
31272
|
+
}
|
|
31273
|
+
/**
|
|
31274
|
+
* Recompute the box's position from the ancestor's CURRENT `pos`.
|
|
31275
|
+
*
|
|
31276
|
+
* Split out of `measureText` because it depends on `pos` while everything
|
|
31277
|
+
* else there depends only on the string and the font. A label that moves
|
|
31278
|
+
* needs this refreshed — and nothing else — so the draw path can call it
|
|
31279
|
+
* per frame without re-measuring a single glyph.
|
|
31280
|
+
*
|
|
31281
|
+
* The `Math.floor` is what puts the canvas on a whole pixel. The remainder
|
|
31282
|
+
* it drops is not lost: `Text` bakes its glyphs at `pos - metrics`, so the
|
|
31283
|
+
* sub-pixel part lands in the rasterization, where the font rasterizer can
|
|
31284
|
+
* antialias it, instead of resampling the whole texture.
|
|
31285
|
+
* @returns {TextMetrics} this instance for chaining
|
|
31286
|
+
* @ignore
|
|
31287
|
+
* @internal
|
|
31288
|
+
*/
|
|
31289
|
+
updateOrigin() {
|
|
30695
31290
|
this.x = Math.floor(
|
|
30696
31291
|
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
31292
|
);
|
|
30698
31293
|
this.y = Math.floor(
|
|
30699
31294
|
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
31295
|
);
|
|
30701
|
-
if (typeof context !== "undefined") {
|
|
30702
|
-
context.restore();
|
|
30703
|
-
}
|
|
30704
31296
|
return this;
|
|
30705
31297
|
}
|
|
30706
31298
|
/**
|
|
@@ -30748,9 +31340,8 @@ var BitmapText = class extends Renderable {
|
|
|
30748
31340
|
* @param {object} settings - the text configuration
|
|
30749
31341
|
* @param {string|Image} settings.font - a font name to identify the corresponding source image
|
|
30750
31342
|
* @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
|
|
31343
|
+
* @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.)
|
|
31344
|
+
* @param {Color|string} [settings.fillStyle] - a CSS color value used to tint the glyphs, see {@link Renderable#tint}
|
|
30754
31345
|
* @param {string} [settings.textAlign="left"] - horizontal text alignment
|
|
30755
31346
|
* @param {string} [settings.textBaseline="top"] - the text baseline
|
|
30756
31347
|
* @param {number} [settings.lineHeight=1.0] - line spacing height
|
|
@@ -30816,7 +31407,7 @@ var BitmapText = class extends Renderable {
|
|
|
30816
31407
|
/**
|
|
30817
31408
|
* change the font settings
|
|
30818
31409
|
* @param {string} textAlign - ("left", "center", "right")
|
|
30819
|
-
* @param {number} [scale]
|
|
31410
|
+
* @param {number} [scale] - a scaling ratio, applied through {@link BitmapText#resize} when given
|
|
30820
31411
|
* @returns {BitmapText} this object for chaining
|
|
30821
31412
|
*/
|
|
30822
31413
|
set(textAlign, scale2) {
|
|
@@ -30941,10 +31532,27 @@ var BitmapText = class extends Renderable {
|
|
|
30941
31532
|
return bounds;
|
|
30942
31533
|
}
|
|
30943
31534
|
/**
|
|
30944
|
-
* defines the color used to tint the bitmap text
|
|
31535
|
+
* defines the color used to tint the bitmap text.
|
|
31536
|
+
*
|
|
31537
|
+
* This is {@link Renderable#tint} under another name, so the same rule
|
|
31538
|
+
* applies: white — `(255, 255, 255)` — is the absence of a tint, and any
|
|
31539
|
+
* other colour tints away from there. A page image authored in white
|
|
31540
|
+
* therefore keeps every colour available to it.
|
|
30945
31541
|
* @public
|
|
30946
31542
|
* @type {Color}
|
|
30947
31543
|
* @see Renderable#tint
|
|
31544
|
+
* @example
|
|
31545
|
+
* // tint at construction...
|
|
31546
|
+
* const score = new BitmapText(8, 8, {
|
|
31547
|
+
* font: "arial",
|
|
31548
|
+
* text: "1000",
|
|
31549
|
+
* fillStyle: "#ffd700", // gold
|
|
31550
|
+
* });
|
|
31551
|
+
* app.world.addChild(score);
|
|
31552
|
+
*
|
|
31553
|
+
* // ...or at any point after it, from a CSS string or a Color
|
|
31554
|
+
* score.fillStyle = "#ff4040"; // flash red on damage
|
|
31555
|
+
* score.fillStyle = new Color(255, 255, 255); // back to untinted
|
|
30948
31556
|
*/
|
|
30949
31557
|
get fillStyle() {
|
|
30950
31558
|
return this.tint;
|
|
@@ -30958,8 +31566,14 @@ var BitmapText = class extends Renderable {
|
|
|
30958
31566
|
}
|
|
30959
31567
|
/**
|
|
30960
31568
|
* change the font display size
|
|
30961
|
-
* @param {number} scale - ratio
|
|
31569
|
+
* @param {number} scale - a ratio against the font's authored size, NOT a
|
|
31570
|
+
* pixel size: `1` is the page image at its native scale, `2` is double
|
|
30962
31571
|
* @returns {BitmapText} this object for chaining
|
|
31572
|
+
* @example
|
|
31573
|
+
* // a bitmap font is pixel art — whole-number ratios stay crisp, and
|
|
31574
|
+
* // fractional ones resample the page image
|
|
31575
|
+
* title.resize(3); // three times its authored size
|
|
31576
|
+
* title.set("center", 2); // align and rescale in one call
|
|
30963
31577
|
*/
|
|
30964
31578
|
resize(scale2) {
|
|
30965
31579
|
this.fontScale.set(scale2, scale2);
|
|
@@ -30972,6 +31586,10 @@ var BitmapText = class extends Renderable {
|
|
|
30972
31586
|
* measure the given text size in pixels
|
|
30973
31587
|
* @param {string} [text]
|
|
30974
31588
|
* @returns {TextMetrics} a TextMetrics object with two properties: `width` and `height`, defining the output dimensions
|
|
31589
|
+
* @example
|
|
31590
|
+
* // size a panel around a label, at the label's CURRENT scale
|
|
31591
|
+
* const size = label.measureText();
|
|
31592
|
+
* panel.resize(size.width + 16, size.height + 16);
|
|
30975
31593
|
*/
|
|
30976
31594
|
measureText(text = this._text) {
|
|
30977
31595
|
return this.metrics.measureText(text);
|
|
@@ -40544,7 +41162,7 @@ function writeLight3dBlock(out, packed) {
|
|
|
40544
41162
|
}
|
|
40545
41163
|
|
|
40546
41164
|
// 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";
|
|
41165
|
+
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
41166
|
|
|
40549
41167
|
// src/video/webgl/shaders/mesh-lit.vert
|
|
40550
41168
|
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 +41362,7 @@ function vertexDataExceedsUint16(indices) {
|
|
|
40744
41362
|
}
|
|
40745
41363
|
|
|
40746
41364
|
// 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";
|
|
41365
|
+
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
41366
|
|
|
40749
41367
|
// src/video/webgl/shaders/mesh.vert
|
|
40750
41368
|
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 +41932,99 @@ var MeshBatcher = class extends MaterialBatcher {
|
|
|
41314
41932
|
fragmentDefines
|
|
41315
41933
|
);
|
|
41316
41934
|
}
|
|
41935
|
+
/**
|
|
41936
|
+
* The program that hosts a `ShaderEffect` on a mesh (#1658).
|
|
41937
|
+
*
|
|
41938
|
+
* A ShaderEffect is realized against the QUAD contract — its vertex stage
|
|
41939
|
+
* projects straight from `uProjectionMatrix` and declares neither
|
|
41940
|
+
* `uModelMatrix` nor `uViewMatrix` — so it cannot be bound to a mesh as-is:
|
|
41941
|
+
* the geometry arrives in model space and would draw unplaced and without
|
|
41942
|
+
* the camera (the very case `validateShaderLocations` warns about). Instead
|
|
41943
|
+
* the body is spliced into THIS batcher's own mesh shader, so the mesh keeps
|
|
41944
|
+
* its placement, alpha cutout, lighting and fog and the effect becomes a
|
|
41945
|
+
* colour hook over the result.
|
|
41946
|
+
*
|
|
41947
|
+
* Keyed through the same variant map as every other permutation, so it is
|
|
41948
|
+
* released on context loss and destroy along with the rest — and keyed on
|
|
41949
|
+
* fog too, since the spliced source inherits the fog `#define`.
|
|
41950
|
+
* @param {object} effect - the hosted ShaderEffect
|
|
41951
|
+
* @returns {GLShader} the spliced program
|
|
41952
|
+
* @ignore
|
|
41953
|
+
* @internal
|
|
41954
|
+
*/
|
|
41955
|
+
effectShaderFor(effect) {
|
|
41956
|
+
const fog = this._fogDefine();
|
|
41957
|
+
const sources = this._shaderSources();
|
|
41958
|
+
const fragment2 = spliceEffect(sources.fragment, effect._meshBody);
|
|
41959
|
+
if (fragment2 === null) {
|
|
41960
|
+
return null;
|
|
41961
|
+
}
|
|
41962
|
+
const compat = /^\s*#version\s+300\s+es/m.test(sources.fragment) ? "#define texture2D texture\n" : "";
|
|
41963
|
+
try {
|
|
41964
|
+
return this.shaderVariant(
|
|
41965
|
+
`effect|${effect._effectId}|${fog !== "" ? 1 : 0}`,
|
|
41966
|
+
{ vertex: sources.vertex, fragment: fragment2 },
|
|
41967
|
+
fog,
|
|
41968
|
+
fog + compat
|
|
41969
|
+
);
|
|
41970
|
+
} catch (error) {
|
|
41971
|
+
if (this._meshEffectCompileWarned !== true) {
|
|
41972
|
+
this._meshEffectCompileWarned = true;
|
|
41973
|
+
console.warn(
|
|
41974
|
+
`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.`
|
|
41975
|
+
);
|
|
41976
|
+
}
|
|
41977
|
+
return null;
|
|
41978
|
+
}
|
|
41979
|
+
}
|
|
41980
|
+
/**
|
|
41981
|
+
* Bring a spliced program up to date with the effect's uniforms.
|
|
41982
|
+
*
|
|
41983
|
+
* `ShaderEffect.setUniform` writes into the effect's OWN quad program;
|
|
41984
|
+
* this one is a different program, so every value the effect has ever been
|
|
41985
|
+
* given is replayed onto it. Unknown names are skipped rather than passed
|
|
41986
|
+
* on: `GLShader.setUniform` throws on one, and a body is free to declare
|
|
41987
|
+
* uniforms the spliced mesh source optimised away.
|
|
41988
|
+
* @param {object} effect - the hosted ShaderEffect
|
|
41989
|
+
* @param {GLShader} shader - the spliced program
|
|
41990
|
+
* @ignore
|
|
41991
|
+
* @internal
|
|
41992
|
+
*/
|
|
41993
|
+
replayEffectUniforms(effect, shader) {
|
|
41994
|
+
for (const [name, value] of effect._uniformValues) {
|
|
41995
|
+
if (shader.uniforms[name] !== void 0) {
|
|
41996
|
+
shader.setUniform(name, value);
|
|
41997
|
+
}
|
|
41998
|
+
}
|
|
41999
|
+
}
|
|
42000
|
+
/**
|
|
42001
|
+
* Whether this batcher can host `effect`, warning once when it cannot.
|
|
42002
|
+
* @param {object} effect - the candidate ShaderEffect
|
|
42003
|
+
* @returns {boolean} true when the effect can be spliced in
|
|
42004
|
+
* @ignore
|
|
42005
|
+
* @internal
|
|
42006
|
+
*/
|
|
42007
|
+
canHostEffect(effect) {
|
|
42008
|
+
if (typeof effect._meshBody !== "string") {
|
|
42009
|
+
return false;
|
|
42010
|
+
}
|
|
42011
|
+
if (!hasSpliceMarkers(this._shaderSources().fragment)) {
|
|
42012
|
+
return false;
|
|
42013
|
+
}
|
|
42014
|
+
const blocker = meshHostingBlocker(effect._meshBuiltins, [
|
|
42015
|
+
...effect._extraTextures?.keys() ?? []
|
|
42016
|
+
]);
|
|
42017
|
+
if (blocker === null) {
|
|
42018
|
+
return true;
|
|
42019
|
+
}
|
|
42020
|
+
if (this._meshEffectBlockedWarned !== true) {
|
|
42021
|
+
this._meshEffectBlockedWarned = true;
|
|
42022
|
+
console.warn(
|
|
42023
|
+
`melonJS: this ShaderEffect cannot be hosted on a Mesh \u2014 ${blocker}. The mesh draws with the built-in shading.`
|
|
42024
|
+
);
|
|
42025
|
+
}
|
|
42026
|
+
return false;
|
|
42027
|
+
}
|
|
41317
42028
|
/**
|
|
41318
42029
|
* `"#define FOG\n"` while the camera drawing has fog, `""` otherwise.
|
|
41319
42030
|
*
|
|
@@ -44841,8 +45552,14 @@ var WebGLRenderer = class _WebGLRenderer extends Renderer {
|
|
|
44841
45552
|
return;
|
|
44842
45553
|
}
|
|
44843
45554
|
this.setBatcher(mesh.lit === true ? "litMesh" : "mesh");
|
|
44844
|
-
const
|
|
45555
|
+
const effectHosted = this.customShader != null && this.currentBatcher.canHostEffect?.(this.customShader) === true;
|
|
45556
|
+
const spliced = effectHosted ? this.currentBatcher.effectShaderFor(this.customShader) : null;
|
|
45557
|
+
let hostedShader = spliced !== null && spliced !== void 0;
|
|
44845
45558
|
if (hostedShader) {
|
|
45559
|
+
this.currentBatcher.useShader(spliced);
|
|
45560
|
+
this.currentBatcher.replayEffectUniforms(this.customShader, spliced);
|
|
45561
|
+
} else if (this.customShader != null && this.customShader.program != null) {
|
|
45562
|
+
hostedShader = true;
|
|
44846
45563
|
this.currentBatcher.useShader(this.customShader);
|
|
44847
45564
|
} else if (this.customShader != null && this.customShader.isWebGL !== true && this._meshShaderWarned !== true) {
|
|
44848
45565
|
this._meshShaderWarned = true;
|
|
@@ -48408,7 +49125,7 @@ function _computeShapeAbsPos(renderable, shape, outPos) {
|
|
|
48408
49125
|
var _shapeAbs = new Vector2d(0, 0);
|
|
48409
49126
|
function _raycastPolygon(fromX, fromY, dx, dy, shape, shapeAbsX, shapeAbsY) {
|
|
48410
49127
|
const points = shape.points;
|
|
48411
|
-
const normals = shape.
|
|
49128
|
+
const normals = shape.edgeNormals;
|
|
48412
49129
|
const len = points.length;
|
|
48413
49130
|
let bestT = Infinity;
|
|
48414
49131
|
let bestNormalIdx = -1;
|
|
@@ -48649,10 +49366,10 @@ function vornoiRegion(line, point) {
|
|
|
48649
49366
|
}
|
|
48650
49367
|
function testPolygonPolygon(a, polyA, b, polyB, response) {
|
|
48651
49368
|
const aPoints = polyA.points;
|
|
48652
|
-
const aNormals = polyA.
|
|
49369
|
+
const aNormals = polyA.edgeNormals;
|
|
48653
49370
|
const aLen = aNormals.length;
|
|
48654
49371
|
const bPoints = polyB.points;
|
|
48655
|
-
const bNormals = polyB.
|
|
49372
|
+
const bNormals = polyB.edgeNormals;
|
|
48656
49373
|
const bLen = bNormals.length;
|
|
48657
49374
|
const posA = T_VECTORS[--T_VECTORS_IDX].copy(a.pos).add(a.ancestor.getAbsolutePosition()).add(polyA.pos);
|
|
48658
49375
|
const posB = T_VECTORS[--T_VECTORS_IDX].copy(b.pos).add(b.ancestor.getAbsolutePosition()).add(polyB.pos);
|
|
@@ -48792,7 +49509,7 @@ function testPolygonEllipse(a, polyA, b, ellipseB, response) {
|
|
|
48792
49509
|
}
|
|
48793
49510
|
}
|
|
48794
49511
|
} else {
|
|
48795
|
-
normal.copy(polyA.
|
|
49512
|
+
normal.copy(polyA.edgeNormals[i]);
|
|
48796
49513
|
dist = point.dot(normal);
|
|
48797
49514
|
const distAbs = Math.abs(dist);
|
|
48798
49515
|
if ((len === 1 || dist > 0) && distAbs > radius) {
|
|
@@ -51772,7 +52489,7 @@ var Trigger = class extends Renderable {
|
|
|
51772
52489
|
};
|
|
51773
52490
|
|
|
51774
52491
|
// src/version.ts
|
|
51775
|
-
var version = "20.
|
|
52492
|
+
var version = "20.5.0";
|
|
51776
52493
|
|
|
51777
52494
|
// src/system/bootstrap.ts
|
|
51778
52495
|
var initialized = false;
|
|
@@ -52222,7 +52939,7 @@ var LIT_INSTANCED = {
|
|
|
52222
52939
|
};
|
|
52223
52940
|
|
|
52224
52941
|
// 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";
|
|
52942
|
+
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
52943
|
|
|
52227
52944
|
// src/video/webgpu/buffer/instance_buffer.js
|
|
52228
52945
|
var WebGPUInstanceBuffer = class {
|
|
@@ -52416,7 +53133,7 @@ var WebGPURetainedGeometry = class {
|
|
|
52416
53133
|
};
|
|
52417
53134
|
|
|
52418
53135
|
// 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";
|
|
53136
|
+
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
53137
|
|
|
52421
53138
|
// src/video/webgpu/shaders/mesh-shadow-instanced.wgsl
|
|
52422
53139
|
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 +53313,13 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52596
53313
|
}
|
|
52597
53314
|
this.vertexLayoutKey = vertexLayoutKey;
|
|
52598
53315
|
this.customShader = null;
|
|
53316
|
+
this.hostedEffect = null;
|
|
52599
53317
|
this.indexData = new Uint32Array(this.vertexData.maxVertex * 6);
|
|
52600
53318
|
this.indexCount = 0;
|
|
52601
53319
|
this.currentMaterial = null;
|
|
52602
53320
|
this.uniformBinding = null;
|
|
52603
53321
|
this.uniformBindGroups = /* @__PURE__ */ new Map();
|
|
53322
|
+
this.effectBindGroups = /* @__PURE__ */ new Map();
|
|
52604
53323
|
this.meshState = { cullMode: "back", frontFace: "ccw" };
|
|
52605
53324
|
if (this.retained !== void 0) {
|
|
52606
53325
|
this.releaseAllRetained();
|
|
@@ -52745,9 +53464,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52745
53464
|
const frame = renderer.currentFrameBinding;
|
|
52746
53465
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
52747
53466
|
this.bindLights(pass);
|
|
52748
|
-
pass.setBindGroup(
|
|
52749
|
-
|
|
52750
|
-
|
|
53467
|
+
pass.setBindGroup(
|
|
53468
|
+
3,
|
|
53469
|
+
this.uniformBinding.bindGroup,
|
|
53470
|
+
this.uniformBinding.offsets
|
|
53471
|
+
);
|
|
52751
53472
|
pass.setVertexBuffer(0, geometry.vertexBuffer);
|
|
52752
53473
|
pass.setVertexBuffer(1, instances.buffer);
|
|
52753
53474
|
pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
|
|
@@ -52856,9 +53577,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52856
53577
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
52857
53578
|
pass.setBindGroup(1, this.currentMaterial);
|
|
52858
53579
|
this.bindLights(pass);
|
|
52859
|
-
pass.setBindGroup(
|
|
52860
|
-
|
|
52861
|
-
|
|
53580
|
+
pass.setBindGroup(
|
|
53581
|
+
3,
|
|
53582
|
+
this.uniformBinding.bindGroup,
|
|
53583
|
+
this.uniformBinding.offsets
|
|
53584
|
+
);
|
|
52862
53585
|
pass.setVertexBuffer(0, quadGeometry.vertexBuffer);
|
|
52863
53586
|
pass.setVertexBuffer(1, instances.buffer);
|
|
52864
53587
|
pass.setIndexBuffer(quadGeometry.indexBuffer, quadGeometry.indexFormat);
|
|
@@ -52946,6 +53669,108 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52946
53669
|
uniformSignature() {
|
|
52947
53670
|
return `mesh:u${MESH_UNIFORM_SIZE}`;
|
|
52948
53671
|
}
|
|
53672
|
+
/**
|
|
53673
|
+
* The group-3 layout when a `ShaderEffect` is hosted on the mesh (#1658).
|
|
53674
|
+
*
|
|
53675
|
+
* `uMesh` keeps binding 0 — the spliced module is the engine's own mesh
|
|
53676
|
+
* shader and still declares it there — and the effect's own uniform block
|
|
53677
|
+
* takes binding 1, its texture/sampler pairs following at their declared
|
|
53678
|
+
* bindings shifted by the same one. That shift is why the body is rewritten
|
|
53679
|
+
* rather than pasted: a quad effect declares its block at
|
|
53680
|
+
* `@group(3) @binding(0)`, which is exactly where `uMesh` lives.
|
|
53681
|
+
* @param {object} effect - the hosted effect
|
|
53682
|
+
* @returns {GPUBindGroupLayout} the combined layout
|
|
53683
|
+
* @ignore
|
|
53684
|
+
* @internal
|
|
53685
|
+
*/
|
|
53686
|
+
hostedEffectLayout(effect) {
|
|
53687
|
+
const realization = effect.wgslRealization;
|
|
53688
|
+
const entries = [
|
|
53689
|
+
{
|
|
53690
|
+
binding: 0,
|
|
53691
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
53692
|
+
buffer: {
|
|
53693
|
+
type: "uniform",
|
|
53694
|
+
hasDynamicOffset: true,
|
|
53695
|
+
minBindingSize: MESH_UNIFORM_SIZE
|
|
53696
|
+
}
|
|
53697
|
+
}
|
|
53698
|
+
];
|
|
53699
|
+
let signature = `${this.uniformSignature()}|fx${realization.structSize}`;
|
|
53700
|
+
if (realization.structSize > 0) {
|
|
53701
|
+
entries.push({
|
|
53702
|
+
binding: 1,
|
|
53703
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
53704
|
+
buffer: {
|
|
53705
|
+
type: "uniform",
|
|
53706
|
+
hasDynamicOffset: true,
|
|
53707
|
+
minBindingSize: realization.structSize
|
|
53708
|
+
}
|
|
53709
|
+
});
|
|
53710
|
+
}
|
|
53711
|
+
return this.renderer.pipelineCache.getEffectLayout(signature, entries);
|
|
53712
|
+
}
|
|
53713
|
+
/**
|
|
53714
|
+
* Build the group-3 binding for a draw that hosts an effect: `uMesh`
|
|
53715
|
+
* plus a fresh snapshot of the effect's uniform mirror, plus its
|
|
53716
|
+
* textures.
|
|
53717
|
+
* @param {GPUDevice} device - the device
|
|
53718
|
+
* @param {object} renderer - the owning renderer
|
|
53719
|
+
* @param {object} effect - the hosted effect
|
|
53720
|
+
* @param {object} meshRegion - the arena region already holding `uMesh`
|
|
53721
|
+
* @returns {{bindGroup: GPUBindGroup, offsets: number[]}} the binding
|
|
53722
|
+
* @ignore
|
|
53723
|
+
* @internal
|
|
53724
|
+
*/
|
|
53725
|
+
bindHostedEffect(device, renderer, effect, meshRegion) {
|
|
53726
|
+
const realization = effect.wgslRealization;
|
|
53727
|
+
const entries = [
|
|
53728
|
+
{
|
|
53729
|
+
binding: 0,
|
|
53730
|
+
resource: { buffer: meshRegion.buffer, size: MESH_UNIFORM_SIZE }
|
|
53731
|
+
}
|
|
53732
|
+
];
|
|
53733
|
+
const offsets = [meshRegion.offset];
|
|
53734
|
+
if (realization.structSize > 0) {
|
|
53735
|
+
const region = renderer.effectUniformArena.alloc(
|
|
53736
|
+
realization.structSize,
|
|
53737
|
+
device.limits.minUniformBufferOffsetAlignment
|
|
53738
|
+
);
|
|
53739
|
+
device.queue.writeBuffer(
|
|
53740
|
+
region.buffer,
|
|
53741
|
+
region.offset,
|
|
53742
|
+
realization.cpu,
|
|
53743
|
+
0,
|
|
53744
|
+
realization.structSize
|
|
53745
|
+
);
|
|
53746
|
+
entries.push({
|
|
53747
|
+
binding: 1,
|
|
53748
|
+
resource: { buffer: region.buffer, size: realization.structSize }
|
|
53749
|
+
});
|
|
53750
|
+
offsets.push(region.offset);
|
|
53751
|
+
}
|
|
53752
|
+
const effectBuffer = offsets.length > 1 ? entries[1].resource.buffer : null;
|
|
53753
|
+
let byEffectPage = this.effectBindGroups.get(meshRegion.buffer);
|
|
53754
|
+
if (byEffectPage === void 0) {
|
|
53755
|
+
byEffectPage = /* @__PURE__ */ new Map();
|
|
53756
|
+
this.effectBindGroups.set(meshRegion.buffer, byEffectPage);
|
|
53757
|
+
}
|
|
53758
|
+
let byEffect = byEffectPage.get(effectBuffer);
|
|
53759
|
+
if (byEffect === void 0) {
|
|
53760
|
+
byEffect = /* @__PURE__ */ new Map();
|
|
53761
|
+
byEffectPage.set(effectBuffer, byEffect);
|
|
53762
|
+
}
|
|
53763
|
+
let bindGroup = byEffect.get(effect._effectId);
|
|
53764
|
+
if (bindGroup === void 0) {
|
|
53765
|
+
bindGroup = device.createBindGroup({
|
|
53766
|
+
label: "melonJS mesh uniforms + hosted effect",
|
|
53767
|
+
layout: this.hostedEffectLayout(effect),
|
|
53768
|
+
entries
|
|
53769
|
+
});
|
|
53770
|
+
byEffect.set(effect._effectId, bindGroup);
|
|
53771
|
+
}
|
|
53772
|
+
return { bindGroup, offsets };
|
|
53773
|
+
}
|
|
52949
53774
|
/**
|
|
52950
53775
|
* The shader family for the next recorded draw: the built-in family,
|
|
52951
53776
|
* or the hosted custom module when `renderer.drawMesh` routed a
|
|
@@ -52960,6 +53785,27 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52960
53785
|
* @internal
|
|
52961
53786
|
*/
|
|
52962
53787
|
activeShaderKey() {
|
|
53788
|
+
const effect = this.hostedEffect;
|
|
53789
|
+
if (effect != null) {
|
|
53790
|
+
const cache4 = this.renderer.pipelineCache;
|
|
53791
|
+
if (this._effectKeys?.epoch !== cache4.epoch) {
|
|
53792
|
+
this._effectKeys = { epoch: cache4.epoch, keys: /* @__PURE__ */ new Map() };
|
|
53793
|
+
}
|
|
53794
|
+
const host = `${this.vertexLayoutKey}|fx${effect._effectId}`;
|
|
53795
|
+
let key2 = this._effectKeys.keys.get(host);
|
|
53796
|
+
if (typeof key2 === "undefined") {
|
|
53797
|
+
key2 = cache4.registerShader(
|
|
53798
|
+
effect.wgslRealization.meshModule(this.shaderSource()),
|
|
53799
|
+
{
|
|
53800
|
+
bindGroupLayouts: this.bindGroupLayoutList(cache4),
|
|
53801
|
+
vertexLayoutKey: this.vertexLayoutKey,
|
|
53802
|
+
label: `melonJS ${this.vertexLayoutKey} + hosted effect`
|
|
53803
|
+
}
|
|
53804
|
+
);
|
|
53805
|
+
this._effectKeys.keys.set(host, key2);
|
|
53806
|
+
}
|
|
53807
|
+
return key2;
|
|
53808
|
+
}
|
|
52963
53809
|
const custom = this.customShader;
|
|
52964
53810
|
if (custom === null) {
|
|
52965
53811
|
return this.shaderKey;
|
|
@@ -52984,9 +53830,27 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
52984
53830
|
cache3.frameLayout,
|
|
52985
53831
|
cache3.meshMaterialLayout,
|
|
52986
53832
|
cache3.emptyLayout,
|
|
52987
|
-
this.
|
|
53833
|
+
this.perDrawLayout()
|
|
52988
53834
|
];
|
|
52989
53835
|
}
|
|
53836
|
+
/**
|
|
53837
|
+
* The group-3 layout for the draw being recorded: `uMesh` alone, or
|
|
53838
|
+
* `uMesh` plus a hosted effect's own block.
|
|
53839
|
+
*
|
|
53840
|
+
* A single accessor on purpose. Every subclass builds its own positional
|
|
53841
|
+
* layout list — the lit tier swaps group 2 for its light block — and when
|
|
53842
|
+
* this choice was inlined into the base list only, the lit subclass kept
|
|
53843
|
+
* handing over the plain layout while the module declared the effect's
|
|
53844
|
+
* binding. The pipeline then failed to validate with "Binding doesn't
|
|
53845
|
+
* exist", every frame drawing that mesh was dropped, and the scene
|
|
53846
|
+
* flickered. Subclasses override the list; they must not re-decide this.
|
|
53847
|
+
* @returns {GPUBindGroupLayout} the group-3 layout
|
|
53848
|
+
* @ignore
|
|
53849
|
+
* @internal
|
|
53850
|
+
*/
|
|
53851
|
+
perDrawLayout() {
|
|
53852
|
+
return this.hostedEffect != null ? this.hostedEffectLayout(this.hostedEffect) : this.meshLayout;
|
|
53853
|
+
}
|
|
52990
53854
|
/**
|
|
52991
53855
|
* Resolve the group-1 material for a mesh: its texture through the
|
|
52992
53856
|
* texture store, honouring the mesh's own `textureRepeat` wrap override
|
|
@@ -53103,6 +53967,16 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53103
53967
|
0,
|
|
53104
53968
|
MESH_UNIFORM_SIZE
|
|
53105
53969
|
);
|
|
53970
|
+
const effect = this.hostedEffect;
|
|
53971
|
+
if (effect !== null && effect !== void 0) {
|
|
53972
|
+
this.uniformBinding = this.bindHostedEffect(
|
|
53973
|
+
device,
|
|
53974
|
+
renderer,
|
|
53975
|
+
effect,
|
|
53976
|
+
region
|
|
53977
|
+
);
|
|
53978
|
+
return;
|
|
53979
|
+
}
|
|
53106
53980
|
let bindGroup = this.uniformBindGroups.get(region.buffer);
|
|
53107
53981
|
if (typeof bindGroup === "undefined") {
|
|
53108
53982
|
bindGroup = device.createBindGroup({
|
|
@@ -53117,7 +53991,7 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53117
53991
|
});
|
|
53118
53992
|
this.uniformBindGroups.set(region.buffer, bindGroup);
|
|
53119
53993
|
}
|
|
53120
|
-
this.uniformBinding = { bindGroup,
|
|
53994
|
+
this.uniformBinding = { bindGroup, offsets: [region.offset] };
|
|
53121
53995
|
}
|
|
53122
53996
|
/**
|
|
53123
53997
|
* Write one vertex into the staging buffer. The base (unlit) layout is
|
|
@@ -53273,9 +54147,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53273
54147
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
53274
54148
|
pass.setBindGroup(1, this.currentMaterial);
|
|
53275
54149
|
this.bindLights(pass);
|
|
53276
|
-
pass.setBindGroup(
|
|
53277
|
-
|
|
53278
|
-
|
|
54150
|
+
pass.setBindGroup(
|
|
54151
|
+
3,
|
|
54152
|
+
this.uniformBinding.bindGroup,
|
|
54153
|
+
this.uniformBinding.offsets
|
|
54154
|
+
);
|
|
53279
54155
|
pass.setVertexBuffer(
|
|
53280
54156
|
0,
|
|
53281
54157
|
vertexRegion.buffer,
|
|
@@ -53386,9 +54262,11 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53386
54262
|
const frame = renderer.currentFrameBinding;
|
|
53387
54263
|
pass.setBindGroup(0, frame.bindGroup, [frame.dynamicOffset]);
|
|
53388
54264
|
this.bindLights(pass);
|
|
53389
|
-
pass.setBindGroup(
|
|
53390
|
-
|
|
53391
|
-
|
|
54265
|
+
pass.setBindGroup(
|
|
54266
|
+
3,
|
|
54267
|
+
this.uniformBinding.bindGroup,
|
|
54268
|
+
this.uniformBinding.offsets
|
|
54269
|
+
);
|
|
53392
54270
|
pass.setVertexBuffer(0, geometry.vertexBuffer);
|
|
53393
54271
|
pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
|
|
53394
54272
|
const slices = mesh.textureGroups;
|
|
@@ -53438,8 +54316,10 @@ var WebGPUMeshBatcher = class extends WebGPUBatcher {
|
|
|
53438
54316
|
this.indexCount = 0;
|
|
53439
54317
|
this.currentMaterial = null;
|
|
53440
54318
|
this.customShader = null;
|
|
54319
|
+
this.hostedEffect = null;
|
|
53441
54320
|
this.uniformBinding = null;
|
|
53442
54321
|
this.uniformBindGroups.clear();
|
|
54322
|
+
this.effectBindGroups.clear();
|
|
53443
54323
|
this.releaseAllRetained();
|
|
53444
54324
|
}
|
|
53445
54325
|
/**
|
|
@@ -53547,7 +54427,9 @@ var WebGPULitMeshBatcher = class extends WebGPUMeshBatcher {
|
|
|
53547
54427
|
cache3.frameLayout,
|
|
53548
54428
|
cache3.meshMaterialLayout,
|
|
53549
54429
|
this.ensureLightsLayout(),
|
|
53550
|
-
this.meshLayout
|
|
54430
|
+
// NOT `this.meshLayout`: a hosted effect extends group 3, and the
|
|
54431
|
+
// base decides that for every tier (see `perDrawLayout`)
|
|
54432
|
+
this.perDrawLayout()
|
|
53551
54433
|
];
|
|
53552
54434
|
}
|
|
53553
54435
|
/**
|
|
@@ -57841,23 +58723,46 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
|
|
|
57841
58723
|
this.pendingDepthClear = true;
|
|
57842
58724
|
}
|
|
57843
58725
|
const customShader = this.customShader != null && this.customShader.isWebGPU === true ? this.customShader : null;
|
|
57844
|
-
|
|
58726
|
+
const hostedEffect = customShader === null && this.customShader != null && typeof this.customShader.wgslRealization !== "undefined" && meshHostingBlocker(
|
|
58727
|
+
this.customShader.wgslRealization.builtins,
|
|
58728
|
+
this.customShader.wgslRealization.textures,
|
|
58729
|
+
"wgsl"
|
|
58730
|
+
) === null && this.customShader.wgslRealization.meshModule(
|
|
58731
|
+
this.batchers.get(
|
|
58732
|
+
mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
|
|
58733
|
+
)?.shaderSource?.() ?? ""
|
|
58734
|
+
) !== null ? this.customShader : null;
|
|
58735
|
+
if (this.customShader != null && customShader === null && hostedEffect === null && this.meshEffectWarned !== true) {
|
|
57845
58736
|
this.meshEffectWarned = true;
|
|
58737
|
+
const realization = this.customShader.wgslRealization;
|
|
58738
|
+
const blocker = typeof realization !== "undefined" ? meshHostingBlocker(
|
|
58739
|
+
realization.builtins,
|
|
58740
|
+
realization.textures,
|
|
58741
|
+
"wgsl"
|
|
58742
|
+
) : null;
|
|
57846
58743
|
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"
|
|
58744
|
+
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
58745
|
);
|
|
57849
58746
|
}
|
|
57850
58747
|
const batcher = this.setBatcher(
|
|
57851
58748
|
mesh.lit === true && this.batchers.has("litMesh") ? "litMesh" : "mesh"
|
|
57852
58749
|
);
|
|
57853
58750
|
batcher.customShader = customShader;
|
|
58751
|
+
const instanced = mesh.instanceLayout !== void 0 && retained && this.supportsInstancing === true;
|
|
58752
|
+
if (instanced && hostedEffect !== null && this.instancedEffectWarned !== true) {
|
|
58753
|
+
this.instancedEffectWarned = true;
|
|
58754
|
+
console.warn(
|
|
58755
|
+
"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."
|
|
58756
|
+
);
|
|
58757
|
+
}
|
|
58758
|
+
batcher.hostedEffect = instanced ? null : hostedEffect;
|
|
57854
58759
|
const culling = mesh.cullBackFaces === true;
|
|
57855
58760
|
const state4 = batcher.meshState;
|
|
57856
58761
|
state4.cullMode = culling ? "back" : "none";
|
|
57857
58762
|
state4.frontFace = culling && retained && mesh.rightHanded !== true ? "cw" : "ccw";
|
|
57858
58763
|
try {
|
|
57859
58764
|
const tint = this.currentTint.toUint32(this.getGlobalAlpha());
|
|
57860
|
-
if (
|
|
58765
|
+
if (instanced) {
|
|
57861
58766
|
batcher.drawInstancedMesh(mesh, modelMatrix, tint);
|
|
57862
58767
|
} else if (retained) {
|
|
57863
58768
|
batcher.drawRetainedMesh(mesh, modelMatrix, tint);
|
|
@@ -57867,6 +58772,7 @@ var WebGPURenderer = class _WebGPURenderer extends Renderer {
|
|
|
57867
58772
|
}
|
|
57868
58773
|
} finally {
|
|
57869
58774
|
batcher.customShader = null;
|
|
58775
|
+
batcher.hostedEffect = null;
|
|
57870
58776
|
}
|
|
57871
58777
|
}
|
|
57872
58778
|
/**
|
|
@@ -60914,7 +61820,7 @@ var Sprite3d = class extends Mesh {
|
|
|
60914
61820
|
* @param {number} x - world x position
|
|
60915
61821
|
* @param {number} y - world y position
|
|
60916
61822
|
* @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`.
|
|
61823
|
+
* @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
61824
|
* @param {number} [settings.width=settings.framewidth] - quad width in world units (pixels)
|
|
60919
61825
|
* @param {number} [settings.height=settings.width] - quad height in world units
|
|
60920
61826
|
* @param {number} [settings.framewidth] - width of a single frame within a spritesheet (enables frame animation)
|
|
@@ -60928,6 +61834,11 @@ var Sprite3d = class extends Mesh {
|
|
|
60928
61834
|
* @param {boolean} [settings.flipY=false] - mirror the sprite vertically (see {@link Sprite3d#flipY})
|
|
60929
61835
|
* @param {boolean} [settings.lit=false] - shade through the lit mesh batcher (see {@link Mesh})
|
|
60930
61836
|
* @param {number[]|Float32Array} [settings.emissive] - emissive color (see {@link Mesh})
|
|
61837
|
+
* @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.
|
|
61838
|
+
* @param {number} [settings.shadowOpacity=0.45] - opacity of the shadow directly beneath the sprite, before any height fade
|
|
61839
|
+
* @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}.
|
|
61840
|
+
* @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.
|
|
61841
|
+
* @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
61842
|
* @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
61843
|
*/
|
|
60933
61844
|
constructor(x, y, settings) {
|
|
@@ -61010,6 +61921,7 @@ var Sprite3d = class extends Mesh {
|
|
|
61010
61921
|
// flatten it to an explicit false, silently opting every sprite out
|
|
61011
61922
|
// of a scene-wide default
|
|
61012
61923
|
castGroundShadow: settings.castGroundShadow,
|
|
61924
|
+
fog: settings.fog,
|
|
61013
61925
|
// raw for the same reason as above: `undefined` means "decide from
|
|
61014
61926
|
// the draw's alpha", and coercing it would pin every sprite opaque
|
|
61015
61927
|
transparent: settings.transparent,
|
|
@@ -64247,25 +65159,8 @@ var lerp3 = (a, b, t) => {
|
|
|
64247
65159
|
};
|
|
64248
65160
|
var NoiseTexture2d = class extends Texture2d {
|
|
64249
65161
|
/**
|
|
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`)
|
|
65162
|
+
* @param {NoiseTexture2dSettings} [settings] - bake settings, plus any
|
|
65163
|
+
* {@link NoiseSettings} for the field itself
|
|
64269
65164
|
*/
|
|
64270
65165
|
constructor(settings = {}) {
|
|
64271
65166
|
super();
|