@woosh/meep-engine 2.169.4 → 2.169.6

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.169.4",
9
+ "version": "2.169.6",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -1 +1 @@
1
- {"version":3,"file":"TerrainLayers.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/ecs/terrain/ecs/layers/TerrainLayers.js"],"names":[],"mappings":"AA8EA;IAGQ;;;OAGG;IACH,QAFU,KAAK,YAAY,CAAC,CAEJ;IAExB;;;OAGG;IACH,YAFU,OAAO,CAEoD;IAMrE;;;OAGG;IACH,SAFU,kBAAkB,CAE8B;IAiB1D,2BAAsF;IAc1F;;;;;;MAKC;IAED;;;aAGC;IAED;;;;OAIG;IACH,gCAHW,MAAM,iBACN,MAAM,QAuChB;IAED;;;;OAIG;IACH,0DAyBC;IAED,sCAIC;IAED;;;;OAIG;IACH,WAHW,MAAM,GACL,YAAY,CAIvB;IAED;;;;OAIG;IACH,8DAFa,MAAM,CAqBlB;IAED;;;;OAIG;IACH,kCAHW,MAAM,GACJ,MAAM,CAMlB;IAED;;;OAGG;IACH,SAFY,MAAM,CAIjB;IAED;;;;OAIG;IACH,gBAHW,YAAY,GACV,MAAM,CAWlB;IAED;;;;OAIG;IACH,mBAHW,YAAY,GACX,OAAO,CAIlB;IAED;;;;;;OAMG;IACH,0CAgCC;IAED;;;OAGG;IACH,iCAFW,MAAM,QAgChB;IAED,gBAGC;IAED,cAEC;IAGD,qBA4CC;CACJ;iBApagB,6CAA6C;6BAQjC,mBAAmB;oBAP5B,qCAAqC;mCALlD,OAAO;4BAAP,OAAO"}
1
+ {"version":3,"file":"TerrainLayers.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/ecs/terrain/ecs/layers/TerrainLayers.js"],"names":[],"mappings":"AA0GA;IAGQ;;;OAGG;IACH,QAFU,KAAK,YAAY,CAAC,CAEJ;IAExB;;;OAGG;IACH,YAFU,OAAO,CAEoD;IAMrE;;;OAGG;IACH,SAFU,kBAAkB,CAE8B;IAuB1D,2BAAsF;IAc1F;;;;;;MAKC;IAED;;;aAGC;IAED;;;;OAIG;IACH,gCAHW,MAAM,iBACN,MAAM,QAuChB;IAED;;;;OAIG;IACH,0DAyBC;IAED,sCAIC;IAED;;;;OAIG;IACH,WAHW,MAAM,GACL,YAAY,CAIvB;IAED;;;;OAIG;IACH,8DAFa,MAAM,CAqBlB;IAED;;;;OAIG;IACH,kCAHW,MAAM,GACJ,MAAM,CAMlB;IAED;;;OAGG;IACH,SAFY,MAAM,CAIjB;IAED;;;;OAIG;IACH,gBAHW,YAAY,GACV,MAAM,CAWlB;IAED;;;;OAIG;IACH,mBAHW,YAAY,GACX,OAAO,CAIlB;IAED;;;;;;OAMG;IACH,0CAqCC;IAED;;;OAGG;IACH,iCAFW,MAAM,QAgChB;IAED,gBAGC;IAED,cAEC;IAGD,qBA4CC;CACJ;iBA3cgB,6CAA6C;6BAajC,mBAAmB;oBAZ5B,qCAAqC;mCALlD,OAAO;4BAAP,OAAO"}
@@ -3,13 +3,13 @@ import {
3
3
  DataTexture,
4
4
  DataTexture2DArray,
5
5
  FloatType,
6
- LinearEncoding,
7
6
  LinearFilter,
8
7
  LinearMipMapLinearFilter,
9
8
  NearestFilter,
10
9
  RepeatWrapping,
11
10
  RGBAFormat,
12
11
  RGFormat,
12
+ sRGBEncoding,
13
13
  UnsignedByteType
14
14
  } from "three";
15
15
  import { assert } from "../../../../../core/assert.js";
@@ -22,7 +22,12 @@ import { invokeObjectHash } from "../../../../../core/model/object/invokeObjectH
22
22
  import { computeStringHash } from "../../../../../core/primitives/strings/computeStringHash.js";
23
23
  import { sampler2d_scale } from "../../../../graphics/texture/sampler/resize/sampler2d_scale.js";
24
24
  import { Sampler2D } from "../../../../graphics/texture/sampler/Sampler2D.js";
25
- import { sampler2d_ensure_uint8_RGBA } from "../../../../graphics/texture/sampler/sampler2d_ensure_uint8_RGBA.js";
25
+ import {
26
+ sampler2d_linear_f16_to_srgb_u8
27
+ } from "../../../../graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.js";
28
+ import {
29
+ sampler2d_srgb_u8_to_linear_f16
30
+ } from "../../../../graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.js";
26
31
  import { TerrainLayer } from "./TerrainLayer.js";
27
32
 
28
33
  class ScaledTextureKey {
@@ -74,6 +79,29 @@ const scaled_texture_cache = new Cache({
74
79
  }
75
80
  });
76
81
 
82
+ /**
83
+ * Resample an sRGB uint8 RGBA layer sampler to the given size.
84
+ *
85
+ * Resize in linear light. Averaging sRGB-encoded bytes mixes in gamma space and
86
+ * comes out too dark, the same defect the shader used to have. Float16 is the
87
+ * intermediate so the round trip through linear costs no precision against the
88
+ * 8-bit source, at half the footprint of float32.
89
+ *
90
+ * @param {Sampler2D} sampler
91
+ * @param {number} width
92
+ * @param {number} height
93
+ * @returns {Sampler2D}
94
+ */
95
+ function rescale_layer_sampler(sampler, width, height) {
96
+ const linear = sampler2d_srgb_u8_to_linear_f16(sampler);
97
+
98
+ const scaled_linear = Sampler2D.float16(4, width, height);
99
+
100
+ sampler2d_scale(linear, scaled_linear);
101
+
102
+ return sampler2d_linear_f16_to_srgb_u8(scaled_linear);
103
+ }
104
+
77
105
  const DEFAULT_RESOLUTION = 512;
78
106
 
79
107
  export class TerrainLayers {
@@ -112,8 +140,14 @@ export class TerrainLayers {
112
140
  this.texture.minFilter = LinearMipMapLinearFilter;
113
141
 
114
142
  this.texture.generateMipmaps = true;
115
- this.texture.encoding = LinearEncoding;
116
- this.texture.internalFormat = 'RGBA8';
143
+
144
+ // Layer diffuse maps are authored in sRGB. Using an sRGB internal format makes the
145
+ // hardware decode each texel to linear BEFORE filtering, so bilinear/trilinear taps
146
+ // and the splat weight blend in TerrainShader all average in linear space.
147
+ // Consequence: the shader must NOT decode again.
148
+ this.texture.encoding = sRGBEncoding;
149
+ this.texture.internalFormat = 'SRGB8_ALPHA8';
150
+
117
151
  this.texture.needsUpdate = true;
118
152
 
119
153
  this.scalesTexture = new DataTexture(new Float32Array(64), 32, 1, RGFormat, FloatType);
@@ -321,27 +355,32 @@ export class TerrainLayers {
321
355
  if (layerSampler.width === resolution_x && layerSampler.height === resolution_y) {
322
356
  //layer's data matches texture dimensions, we can just copy the data directly
323
357
  return layer.diffuse;
324
- } else {
358
+ }
325
359
 
326
- // check cache
327
- const key = ScaledTextureKey.from(layer.textureDiffuseURL, resolution);
328
- const existing = scaled_texture_cache.get(key);
360
+ const uri = layer.textureDiffuseURL;
329
361
 
330
- if (existing !== null) {
331
- return existing;
332
- }
362
+ // The cache identifies pixels by their source URL, which only works for layers that
363
+ // were actually loaded from one. A layer whose diffuse sampler is assigned
364
+ // programmatically keeps the default empty URL, and every such layer would collide on
365
+ // ("", resolution) and silently get the first one's pixels back. Those layers rescale
366
+ // on every call instead; nothing about the URL-backed path changes.
367
+ if (uri === "") {
368
+ return rescale_layer_sampler(layerSampler, resolution_x, resolution_y);
369
+ }
333
370
 
334
- const scaledLayerSampler = Sampler2D.uint8(4, resolution_x, resolution_y);
371
+ // check cache
372
+ const key = ScaledTextureKey.from(uri, resolution);
373
+ const existing = scaled_texture_cache.get(key);
335
374
 
336
- sampler2d_scale(layerSampler, scaledLayerSampler);
375
+ if (existing !== null) {
376
+ return existing;
377
+ }
337
378
 
338
- // Ensure result is uint8 RGBA
339
- const rgba = sampler2d_ensure_uint8_RGBA(scaledLayerSampler);
379
+ const rgba = rescale_layer_sampler(layerSampler, resolution_x, resolution_y);
340
380
 
341
- scaled_texture_cache.put(key, rgba);
381
+ scaled_texture_cache.put(key, rgba);
342
382
 
343
- return rgba;
344
- }
383
+ return rgba;
345
384
  }
346
385
 
347
386
  /**
@@ -161,19 +161,6 @@ function fragment() {
161
161
 
162
162
  ${ShaderChunks.random}
163
163
 
164
- vec4 blendSplatTextures(const in vec4 tex0, const in vec4 tex1, const in vec4 tex2, const in vec4 tex3, const in vec4 splat){
165
-
166
- float b0 = splat[0];
167
-
168
- float b1 = splat[1];
169
-
170
- float b2 = splat[2];
171
-
172
- float b3 = splat[3];
173
-
174
- return (tex0*b0 + tex1*b1 + tex2*b2 + tex3*b3) / (b0 + b1 + b2 + b3);
175
- }
176
-
177
164
  void randomBlend(in vec2 uv, inout vec3 v, float level){
178
165
  vec3 noise = vec3( rand(uv.x*12.9898 + uv.y*78.233), rand(uv.x*10.9898 + uv.y*73.233) , rand(uv.x*6.9898 + uv.y*71.233));
179
166
  v = v*(1.0 - level) + noise*level;
@@ -264,8 +251,10 @@ function fragment() {
264
251
  return tile_color * tile_data;
265
252
  }
266
253
 
254
+ // Returns linear-space colour: diffuseMaps uses an sRGB internal format, so texture()
255
+ // hands back already-decoded texels and the weighted average below is done in linear space.
267
256
  vec4 computeSplatMix(vec2 uv){
268
-
257
+
269
258
  float weightSum = 0.0;
270
259
  vec4 colorSum = vec4(0.0);
271
260
 
@@ -291,11 +280,9 @@ function fragment() {
291
280
  out highp vec4 pc_fragColor;
292
281
 
293
282
  void main() {
283
+ // already linear, the sampler decoded it
294
284
  vec4 splatDiffuseColor = computeSplatMix(vUv);
295
-
296
- //decode the texture
297
- splatDiffuseColor = sRGBToLinear( splatDiffuseColor );
298
-
285
+
299
286
  //ensure it is fully opaque
300
287
  splatDiffuseColor.a = 1.0;
301
288
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Encodes a linear-light RGBA image back to 8-bit sRGB.
3
+ *
4
+ * Reverse of {@link sampler2d_srgb_u8_to_linear_f16}, and the last step of a linear-space
5
+ * resize. The conversion saturates: linear values outside [0,1] are clamped rather than
6
+ * wrapped, since the 8-bit output cannot represent them.
7
+ *
8
+ * The alpha channel is NOT transfer-encoded, it is merely scaled back to 0..255.
9
+ *
10
+ * @param {Sampler2D} input RGBA, linear light. Any numeric backing store, float16 in the resize path.
11
+ * @returns {Sampler2D} 8-bit RGBA, sRGB-encoded
12
+ * @throws {Error} if the input is not 4-channel
13
+ */
14
+ export function sampler2d_linear_f16_to_srgb_u8(input: Sampler2D): Sampler2D;
15
+ import { Sampler2D } from "./Sampler2D.js";
16
+ //# sourceMappingURL=sampler2d_linear_f16_to_srgb_u8.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampler2d_linear_f16_to_srgb_u8.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.js"],"names":[],"mappings":"AAaA;;;;;;;;;;;;GAYG;AACH,uDAJW,SAAS,GACP,SAAS,CA8BrB;0BAnDyB,gBAAgB"}
@@ -0,0 +1,54 @@
1
+ import { convert_channel_linear_to_sRGB } from "../../../../core/color/sRGB/linear_to_sRGB.js";
2
+ import { clamp } from "../../../../core/math/clamp.js";
3
+ import { Sampler2D } from "./Sampler2D.js";
4
+
5
+ /**
6
+ *
7
+ * @param {number} linear
8
+ * @returns {number} sRGB-encoded byte, 0..255
9
+ */
10
+ function encode_channel(linear) {
11
+ return Math.round(clamp(convert_channel_linear_to_sRGB(linear), 0, 1) * 255);
12
+ }
13
+
14
+ /**
15
+ * Encodes a linear-light RGBA image back to 8-bit sRGB.
16
+ *
17
+ * Reverse of {@link sampler2d_srgb_u8_to_linear_f16}, and the last step of a linear-space
18
+ * resize. The conversion saturates: linear values outside [0,1] are clamped rather than
19
+ * wrapped, since the 8-bit output cannot represent them.
20
+ *
21
+ * The alpha channel is NOT transfer-encoded, it is merely scaled back to 0..255.
22
+ *
23
+ * @param {Sampler2D} input RGBA, linear light. Any numeric backing store, float16 in the resize path.
24
+ * @returns {Sampler2D} 8-bit RGBA, sRGB-encoded
25
+ * @throws {Error} if the input is not 4-channel
26
+ */
27
+ export function sampler2d_linear_f16_to_srgb_u8(input) {
28
+ if (input.itemSize !== 4) {
29
+ throw new Error(`Input must have 4 channels exactly, instead got '${input.itemSize}'`);
30
+ }
31
+
32
+ const width = input.width;
33
+ const height = input.height;
34
+
35
+ const output = Sampler2D.uint8(4, width, height);
36
+
37
+ const source = input.data;
38
+ const destination = output.data;
39
+
40
+ const texel_count = width * height;
41
+
42
+ for (let i = 0; i < texel_count; i++) {
43
+ const address = i * 4;
44
+
45
+ destination[address] = encode_channel(source[address]);
46
+ destination[address + 1] = encode_channel(source[address + 1]);
47
+ destination[address + 2] = encode_channel(source[address + 2]);
48
+
49
+ // alpha carries no transfer function
50
+ destination[address + 3] = Math.round(clamp(source[address + 3], 0, 1) * 255);
51
+ }
52
+
53
+ return output;
54
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Decodes an 8-bit sRGB RGBA image into linear light, stored as float16.
3
+ *
4
+ * Intended as the first step of a resize: averaging sRGB-encoded values is wrong, because
5
+ * the encoding is non-linear, and the result comes out too dark. Decode first, resize in
6
+ * linear, then re-encode with {@link sampler2d_linear_f16_to_srgb_u8}.
7
+ *
8
+ * Float16 holds linear values in [0,1] with ~3 decimal digits of precision, which is finer
9
+ * than the 8-bit encoding it came from and half the footprint of float32. Note that V8 does
10
+ * not yet specialize Float16Array element access, so this is not a good fit for a per-frame
11
+ * hot loop — it is meant for load-time work whose result gets cached.
12
+ *
13
+ * The alpha channel is NOT transfer-encoded, matching how the GPU treats SRGB8_ALPHA8: only
14
+ * RGB goes through the curve, alpha is already linear and is merely normalized to [0,1].
15
+ *
16
+ * @param {Sampler2D} input 8-bit RGBA, sRGB-encoded
17
+ * @returns {Sampler2D} RGBA float16, linear light, values in [0,1]
18
+ * @throws {Error} if the input is not 4-channel, or is not 8-bit
19
+ */
20
+ export function sampler2d_srgb_u8_to_linear_f16(input: Sampler2D): Sampler2D;
21
+ import { Sampler2D } from "./Sampler2D.js";
22
+ //# sourceMappingURL=sampler2d_srgb_u8_to_linear_f16.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sampler2d_srgb_u8_to_linear_f16.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.js"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;GAkBG;AACH,uDAJW,SAAS,GACP,SAAS,CAqCrB;0BAlEyB,gBAAgB"}
@@ -0,0 +1,68 @@
1
+ import { convert_channel_sRGB_to_linear } from "../../../../core/color/sRGB/sRGB_to_linear.js";
2
+ import { Sampler2D } from "./Sampler2D.js";
3
+
4
+ /**
5
+ * Decoded linear value for every possible sRGB byte. The input domain is only 256 values
6
+ * wide, so the transfer function is evaluated once per value instead of once per channel.
7
+ * @type {Float32Array}
8
+ */
9
+ const SRGB_BYTE_TO_LINEAR = new Float32Array(256);
10
+
11
+ for (let i = 0; i < 256; i++) {
12
+ SRGB_BYTE_TO_LINEAR[i] = convert_channel_sRGB_to_linear(i / 255);
13
+ }
14
+
15
+ /**
16
+ * Decodes an 8-bit sRGB RGBA image into linear light, stored as float16.
17
+ *
18
+ * Intended as the first step of a resize: averaging sRGB-encoded values is wrong, because
19
+ * the encoding is non-linear, and the result comes out too dark. Decode first, resize in
20
+ * linear, then re-encode with {@link sampler2d_linear_f16_to_srgb_u8}.
21
+ *
22
+ * Float16 holds linear values in [0,1] with ~3 decimal digits of precision, which is finer
23
+ * than the 8-bit encoding it came from and half the footprint of float32. Note that V8 does
24
+ * not yet specialize Float16Array element access, so this is not a good fit for a per-frame
25
+ * hot loop — it is meant for load-time work whose result gets cached.
26
+ *
27
+ * The alpha channel is NOT transfer-encoded, matching how the GPU treats SRGB8_ALPHA8: only
28
+ * RGB goes through the curve, alpha is already linear and is merely normalized to [0,1].
29
+ *
30
+ * @param {Sampler2D} input 8-bit RGBA, sRGB-encoded
31
+ * @returns {Sampler2D} RGBA float16, linear light, values in [0,1]
32
+ * @throws {Error} if the input is not 4-channel, or is not 8-bit
33
+ */
34
+ export function sampler2d_srgb_u8_to_linear_f16(input) {
35
+ if (input.itemSize !== 4) {
36
+ throw new Error(`Input must have 4 channels exactly, instead got '${input.itemSize}'`);
37
+ }
38
+
39
+ const source_constructor = input.data.constructor;
40
+
41
+ if (source_constructor !== Uint8Array && source_constructor !== Uint8ClampedArray) {
42
+ // the decode is a 256-entry lookup, a wider type would index past the end of it
43
+ throw new Error(`Input must be 8-bit, instead got '${source_constructor.name}'`);
44
+ }
45
+
46
+ const width = input.width;
47
+ const height = input.height;
48
+
49
+ const output = Sampler2D.float16(4, width, height);
50
+
51
+ const source = input.data;
52
+ const destination = output.data;
53
+
54
+ const texel_count = width * height;
55
+
56
+ for (let i = 0; i < texel_count; i++) {
57
+ const address = i * 4;
58
+
59
+ destination[address] = SRGB_BYTE_TO_LINEAR[source[address]];
60
+ destination[address + 1] = SRGB_BYTE_TO_LINEAR[source[address + 1]];
61
+ destination[address + 2] = SRGB_BYTE_TO_LINEAR[source[address + 2]];
62
+
63
+ // alpha carries no transfer function
64
+ destination[address + 3] = source[address + 3] / 255;
65
+ }
66
+
67
+ return output;
68
+ }