@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 +1 -1
- package/src/engine/ecs/terrain/ecs/layers/TerrainLayers.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/layers/TerrainLayers.js +57 -18
- package/src/engine/graphics/geometry/instancing/InstancedMeshGroup.d.ts +7 -0
- package/src/engine/graphics/geometry/instancing/InstancedMeshGroup.d.ts.map +1 -1
- package/src/engine/graphics/geometry/instancing/InstancedMeshGroup.js +28 -4
- package/src/engine/graphics/shaders/TerrainShader.js +5 -18
- package/src/engine/graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.d.ts +16 -0
- package/src/engine/graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.d.ts.map +1 -0
- package/src/engine/graphics/texture/sampler/sampler2d_linear_f16_to_srgb_u8.js +54 -0
- package/src/engine/graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.d.ts +22 -0
- package/src/engine/graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.d.ts.map +1 -0
- package/src/engine/graphics/texture/sampler/sampler2d_srgb_u8_to_linear_f16.js +68 -0
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.
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
}
|
|
358
|
+
}
|
|
325
359
|
|
|
326
|
-
|
|
327
|
-
const key = ScaledTextureKey.from(layer.textureDiffuseURL, resolution);
|
|
328
|
-
const existing = scaled_texture_cache.get(key);
|
|
360
|
+
const uri = layer.textureDiffuseURL;
|
|
329
361
|
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
371
|
+
// check cache
|
|
372
|
+
const key = ScaledTextureKey.from(uri, resolution);
|
|
373
|
+
const existing = scaled_texture_cache.get(key);
|
|
335
374
|
|
|
336
|
-
|
|
375
|
+
if (existing !== null) {
|
|
376
|
+
return existing;
|
|
377
|
+
}
|
|
337
378
|
|
|
338
|
-
|
|
339
|
-
const rgba = sampler2d_ensure_uint8_RGBA(scaledLayerSampler);
|
|
379
|
+
const rgba = rescale_layer_sampler(layerSampler, resolution_x, resolution_y);
|
|
340
380
|
|
|
341
|
-
|
|
381
|
+
scaled_texture_cache.put(key, rgba);
|
|
342
382
|
|
|
343
|
-
|
|
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":"
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
+
}
|