@woosh/meep-engine 2.169.3 → 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.3",
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
  /**
@@ -192,7 +192,14 @@ export class InstancedMeshGroup {
192
192
  }
193
193
  export type CacheKey = {
194
194
  color: Material;
195
+ /**
196
+ * material for the depth shadow pass, used by directional and spot lights
197
+ */
195
198
  depth: Material;
199
+ /**
200
+ * material for the distance shadow pass, used by point lights
201
+ */
202
+ distance: Material;
196
203
  };
197
204
  import { BufferGeometry } from 'three';
198
205
  import { Mesh } from 'three';
@@ -1 +1 @@
1
- {"version":3,"file":"InstancedMeshGroup.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/geometry/instancing/InstancedMeshGroup.js"],"names":[],"mappings":"AAyCA;IAqiBI;;;;;OAKG;IACH,sBAJW,oBAAoB,uBAElB,kBAAkB,CAS9B;IA5iBG;;;;OAIG;IACH,wBAA2B;IAE3B;;;;OAIG;IACH,gCAAmC;IAEnC;;;OAGG;IACH,cAAc;IAEd;;;OAGG;IACH,UAFU,MAAM,CAEC;IAEjB;;;OAGG;IACH,YAFU,MAAM,CAEK;IACrB;;;OAGG;IACH,cAFU,MAAM,CAEM;IAEtB;;;OAGG;IACH,cAFU,MAAM,CAEO;IAEvB;;;OAGG;IACH,gBAFU,MAAM,CAEQ;IAExB;;;;OAIG;IACH,6BAAgC;IAEhC;;;;OAIG;IACH,kCAAqC;IAGrC;;;;;OAKG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,8BAAiC;IAEjC;;;;OAIG;IACH,wBAAuC;IAEvC;;;;OAIG;IACH,mBAAsB;IAEtB,eAAiB;IACjB,kBAAoB;IAEpB;;;;OAIG;IACH,oBAAqC;IAErC,kFAAqC;IAErC,mBAAqB;IAGzB,qBA8BC;IAED;;;OAGG;IACH,2BAUC;IAED;;;OAGG;IACH,+BAcC;IAED;;;OAGG;IACH,4BAEC;IAED,gBAIC;IAED;;;OAGG;IACH,sBAFW,oBAAoB,QAU9B;IA4CD;;;OAGG;IACH,4BAFW,MAAM,QAAQ,GAAC,MAAM,cAAc,QAS7C;IAED;;;OAGG;IACH,kBAFW,MAAM,QAWhB;IAED;;;OAGG;IACH,qBAFW,MAAM,QA6BhB;IAED;;;;OAIG;IACH,sBAHW,MAAM,aACN,MAAM,EAAE,GAAC,UAAU,MAAM,CAAC,GAAC,YAAY,QAIjD;IAED;;;;OAIG;IACH,kBAHW,MAAM,SACN,MAAM,EAAE,GAAC,UAAU,MAAM,CAAC,GAAC,YAAY,QAIjD;IAED;;;;;;;OAOG;IACH,6BANW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,QAWhB;IAED,+BAMC;IAED;;;;OAIG;IACH,qCAEC;IAED;;;OAGG;IACH,0BAKC;IAED;;;OAGG;IACH,YAFY,MAAM,MAAI,CAIrB;IAED;;;;OAIG;IACH,yBAUC;IAED;;;;OAIG;IACH,4DAH2C,IAAI,uBAiB9C;IAED;;;OAGG;IACH,6BAoDC;IAED,cAiEC;;CAgBJ;;;;;+BAllBM,OAAO;qBAAP,OAAO"}
1
+ {"version":3,"file":"InstancedMeshGroup.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/geometry/instancing/InstancedMeshGroup.js"],"names":[],"mappings":"AA2CA;IA2jBI;;;;;OAKG;IACH,sBAJW,oBAAoB,uBAElB,kBAAkB,CAS9B;IAlkBG;;;;OAIG;IACH,wBAA2B;IAE3B;;;;OAIG;IACH,gCAAmC;IAEnC;;;OAGG;IACH,cAAc;IAEd;;;OAGG;IACH,UAFU,MAAM,CAEC;IAEjB;;;OAGG;IACH,YAFU,MAAM,CAEK;IACrB;;;OAGG;IACH,cAFU,MAAM,CAEM;IAEtB;;;OAGG;IACH,cAFU,MAAM,CAEO;IAEvB;;;OAGG;IACH,gBAFU,MAAM,CAEQ;IAExB;;;;OAIG;IACH,6BAAgC;IAEhC;;;;OAIG;IACH,kCAAqC;IAGrC;;;;;OAKG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,8BAAiC;IAEjC;;;;OAIG;IACH,wBAAuC;IAEvC;;;;OAIG;IACH,mBAAsB;IAEtB,eAAiB;IACjB,kBAAoB;IAEpB;;;;OAIG;IACH,oBAAqC;IAErC,kFAAqC;IAErC,mBAAqB;IAGzB,qBA+BC;IAED;;;OAGG;IACH,2BAUC;IAED;;;OAGG;IACH,+BAcC;IAED;;;OAGG;IACH,4BAEC;IAED,gBAIC;IAED;;;OAGG;IACH,sBAFW,oBAAoB,QAU9B;IAgED;;;OAGG;IACH,4BAFW,MAAM,QAAQ,GAAC,MAAM,cAAc,QAU7C;IAED;;;OAGG;IACH,kBAFW,MAAM,QAWhB;IAED;;;OAGG;IACH,qBAFW,MAAM,QA6BhB;IAED;;;;OAIG;IACH,sBAHW,MAAM,aACN,MAAM,EAAE,GAAC,UAAU,MAAM,CAAC,GAAC,YAAY,QAIjD;IAED;;;;OAIG;IACH,kBAHW,MAAM,SACN,MAAM,EAAE,GAAC,UAAU,MAAM,CAAC,GAAC,YAAY,QAIjD;IAED;;;;;;;OAOG;IACH,6BANW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,QAWhB;IAED,+BAMC;IAED;;;;OAIG;IACH,qCAEC;IAED;;;OAGG;IACH,0BAKC;IAED;;;OAGG;IACH,YAFY,MAAM,MAAI,CAIrB;IAED;;;;OAIG;IACH,yBAUC;IAED;;;;OAIG;IACH,4DAH2C,IAAI,uBAiB9C;IAED;;;OAGG;IACH,6BAoDC;IAED,cAiEC;;CAgBJ;;;;;;;;;;;;+BAzmBM,OAAO;qBAAP,OAAO"}
@@ -7,6 +7,7 @@ import {
7
7
  LineSegments,
8
8
  Mesh,
9
9
  MeshDepthMaterial,
10
+ MeshDistanceMaterial,
10
11
  RGBADepthPacking
11
12
  } from 'three';
12
13
  import { Cache } from "../../../../core/cache/Cache.js";
@@ -26,7 +27,8 @@ import { rewriteMaterial } from "./rewriteMaterial.js";
26
27
  /**
27
28
  * @typedef {Object} CacheKey
28
29
  * @property {Material} color
29
- * @property {Material} depth
30
+ * @property {Material} depth material for the depth shadow pass, used by directional and spot lights
31
+ * @property {Material} distance material for the distance shadow pass, used by point lights
30
32
  */
31
33
 
32
34
  /**
@@ -180,6 +182,7 @@ export class InstancedMeshGroup {
180
182
  new_object.receiveShadow = this.mesh.receiveShadow;
181
183
 
182
184
  new_object.customDepthMaterial = this.mesh.customDepthMaterial;
185
+ new_object.customDistanceMaterial = this.mesh.customDistanceMaterial;
183
186
  }
184
187
 
185
188
  this.mesh = new_object;
@@ -269,24 +272,44 @@ export class InstancedMeshGroup {
269
272
 
270
273
  material.onBeforeCompile = composeCompile(sourceMaterial.onBeforeCompile, rewriteMaterial);
271
274
 
272
- //we need a custom depth material to ensure shadows will be drawn correctly
275
+ // We need custom shadow materials to ensure shadows will be drawn correctly.
276
+ //
277
+ // Instancing here is not three's own: the per-instance transform lives in an
278
+ // `instanceMatrix` attribute on our InstancedBufferGeometry and is applied by the
279
+ // `rewriteMaterial` shader patch, NOT by three's `USE_INSTANCING` (which keys off
280
+ // `Object3D.isInstancedMesh`, and this mesh is a plain Mesh). So every material
281
+ // this geometry is ever drawn with has to carry the patch, or that draw renders
282
+ // every instance at the local origin.
283
+ //
284
+ // There are two shadow passes and they take different materials: depth for
285
+ // directional and spot lights, distance for point lights (which compare world-space
286
+ // distance to the light rather than projected depth). Both are honoured by
287
+ // ThreeLightBinding, which prefers `customDepthMaterial`/`customDistanceMaterial`
288
+ // over its own unpatched fallbacks.
273
289
  const depthMaterial = new MeshDepthMaterial({
274
290
  depthPacking: RGBADepthPacking,
275
291
  });
276
292
 
277
- //if the source material uses alpha testing - enable it on the depth material
293
+ const distanceMaterial = new MeshDistanceMaterial();
294
+
295
+ //if the source material uses alpha testing - enable it on the shadow materials
278
296
  if (material.alphaTest !== 0) {
279
297
  depthMaterial.alphaTest = material.alphaTest;
280
298
  depthMaterial.map = material.map;
299
+
300
+ distanceMaterial.alphaTest = material.alphaTest;
301
+ distanceMaterial.map = material.map;
281
302
  }
282
303
 
283
304
  depthMaterial.onBeforeCompile = rewriteMaterial;
305
+ distanceMaterial.onBeforeCompile = rewriteMaterial;
284
306
 
285
307
  const cachedMaterial = StaticMaterialCache.Global.acquire(material);
286
308
 
287
309
  return {
288
310
  color: cachedMaterial,
289
- depth: StaticMaterialCache.Global.acquire(depthMaterial)
311
+ depth: StaticMaterialCache.Global.acquire(depthMaterial),
312
+ distance: StaticMaterialCache.Global.acquire(distanceMaterial)
290
313
  };
291
314
 
292
315
  }
@@ -302,6 +325,7 @@ export class InstancedMeshGroup {
302
325
 
303
326
  this.mesh.material = m.color;
304
327
  this.mesh.customDepthMaterial = m.depth;
328
+ this.mesh.customDistanceMaterial = m.distance;
305
329
  }
306
330
 
307
331
  /**
@@ -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
+ }