@driftengine/core 4.8.6 → 4.8.7
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/CHANGELOG.json +71 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +35 -0
- package/dist/render/areaLights.js +35 -0
- package/dist/render/backend/api.d.ts +33 -3
- package/dist/render/backend/webgl2/renderer.d.ts +50 -14
- package/dist/render/backend/webgl2/renderer.js +141 -30
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/renderer.d.ts +35 -1
- package/dist/render/backend/webgpu/renderer.js +155 -16
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/clusteredLights.d.ts +23 -5
- package/dist/render/clusteredLights.js +35 -8
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +5 -0
- package/dist/render/instances.d.ts +19 -5
- package/dist/render/instances.js +9 -3
- package/dist/render/lightBudget.d.ts +12 -1
- package/dist/render/lightBudget.js +23 -3
- package/dist/render/lightmap.d.ts +23 -9
- package/dist/render/lightmap.js +40 -18
- package/dist/render/particlePool.d.ts +21 -0
- package/dist/render/particlePool.js +7 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/renderQuality.d.ts +8 -7
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/screenSpaceReflection.d.ts +27 -1
- package/dist/render/screenSpaceReflection.js +9 -0
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +46 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +3 -1
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +16 -2
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +57 -22
- package/dist/render/shaders/flat/models.d.ts +5 -2
- package/dist/render/shaders/flat/models.js +100 -16
- package/dist/render/shaders/flat/preamble.js +10 -3
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
- package/dist/render/shaders/generated/flat.wgsl.js +54 -38
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/ssrTrace.js +20 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.js +1 -0
- package/dist/render/surfaceModel.d.ts +29 -8
- package/dist/render/surfaceModel.js +33 -9
- package/dist/render/surfaceTexture.d.ts +25 -1
- package/dist/render/surfaceTexture.js +27 -5
- package/package.json +6 -6
- package/src/index.ts +6 -2
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +58 -0
- package/src/render/backend/api.ts +49 -2
- package/src/render/backend/webgl2/renderer.ts +192 -38
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +194 -19
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
- package/src/render/clusteredLights.ts +41 -8
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +4 -0
- package/src/render/instances.ts +26 -6
- package/src/render/lightBudget.ts +40 -2
- package/src/render/lightmap.ts +64 -23
- package/src/render/particlePool.ts +23 -0
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/renderQuality.ts +8 -7
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/screenSpaceReflection.ts +33 -1
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +46 -0
- package/src/render/shaders/flat/glassTint.ts +9 -2
- package/src/render/shaders/flat/index.ts +21 -2
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +57 -22
- package/src/render/shaders/flat/models.ts +100 -16
- package/src/render/shaders/flat/preamble.ts +10 -3
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/flat.wgsl.ts +54 -38
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/ssrTrace.ts +20 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +1 -0
- package/src/render/surfaceModel.ts +47 -10
- package/src/render/surfaceTexture.ts +69 -9
|
@@ -3,6 +3,8 @@ import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.
|
|
|
3
3
|
import { toHalfFloats } from '../../halfFloat.ts';
|
|
4
4
|
import { isLightmapTexels } from '../../lightmap.ts';
|
|
5
5
|
import type { LightmapTexels } from '../../lightmap.ts';
|
|
6
|
+
import { isSceneCaptureTexels } from '../../sceneCapture.ts';
|
|
7
|
+
import type { SceneCaptureTexels } from '../../sceneCapture.ts';
|
|
6
8
|
import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.ts';
|
|
7
9
|
import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.ts';
|
|
8
10
|
import type {
|
|
@@ -195,35 +197,54 @@ export class GpuSurfaceTexture {
|
|
|
195
197
|
constructor(
|
|
196
198
|
private readonly device: GPUDevice,
|
|
197
199
|
private readonly pipelines: PipelineCache,
|
|
198
|
-
source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels,
|
|
200
|
+
source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels,
|
|
199
201
|
options: SurfaceTextureOptions = {},
|
|
200
202
|
compressed: readonly CompressedTextureFormat[] = [],
|
|
201
203
|
) {
|
|
202
|
-
/* A
|
|
204
|
+
/* A scene capture's target: one empty layer the world is drawn into. See `sceneCapture.ts`. */
|
|
205
|
+
const capture = isSceneCaptureTexels(source) ? source : null;
|
|
206
|
+
/* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
|
|
203
207
|
const baked = isLightmapTexels(source) ? source : null;
|
|
204
208
|
const listed = (
|
|
205
|
-
baked !== null ? [] : Array.isArray(source) ? source : [source]
|
|
209
|
+
baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]
|
|
206
210
|
) as readonly SurfaceSource[];
|
|
207
211
|
const blocks = compressedLayers(listed);
|
|
208
212
|
const srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
209
213
|
/* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
|
|
210
|
-
place it is correct — `surfaceTexture.ts` makes the argument in full.
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
place it is correct — `surfaceTexture.ts` makes the argument in full. A capture takes the
|
|
215
|
+
world's own format, which its pass resolves into. */
|
|
216
|
+
this.format =
|
|
217
|
+
capture !== null
|
|
218
|
+
? pipelines.format
|
|
219
|
+
: srgb && baked === null
|
|
220
|
+
? 'rgba8unorm-srgb'
|
|
221
|
+
: 'rgba8unorm';
|
|
222
|
+
if (capture !== null) {
|
|
223
|
+
this.layers = 1;
|
|
224
|
+
this.blockFormat = null;
|
|
225
|
+
this.mipmapped = false;
|
|
226
|
+
this.width = capture.width;
|
|
227
|
+
this.height = capture.height;
|
|
228
|
+
this.levels = 1;
|
|
229
|
+
this.texture = this.allocate();
|
|
230
|
+
} else if (baked !== null) {
|
|
213
231
|
/* No chain: a page is read at its own resolution, and a level of it would blend regions. */
|
|
214
|
-
this.layers = baked.layers;
|
|
232
|
+
this.layers = baked.layers.length;
|
|
215
233
|
this.blockFormat = null;
|
|
216
234
|
this.mipmapped = false;
|
|
217
235
|
this.width = baked.width;
|
|
218
236
|
this.height = baked.height;
|
|
219
237
|
this.levels = 1;
|
|
220
238
|
this.texture = this.allocate();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
/* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
|
|
240
|
+
for (let layer = 0; layer < this.layers; layer++) {
|
|
241
|
+
device.queue.writeTexture(
|
|
242
|
+
{ texture: this.texture, origin: [0, 0, layer] },
|
|
243
|
+
baked.layers[layer] as Uint8Array<ArrayBuffer>,
|
|
244
|
+
{ bytesPerRow: baked.width * 4, rowsPerImage: baked.height },
|
|
245
|
+
{ width: baked.width, height: baked.height, depthOrArrayLayers: 1 },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
227
248
|
} else if (blocks !== null) {
|
|
228
249
|
/* Refused before anything is allocated; the stored chain, or level 0 alone where no chain
|
|
229
250
|
was asked for. See `compressedUpload.ts`. */
|
|
@@ -282,7 +303,9 @@ export class GpuSurfaceTexture {
|
|
|
282
303
|
: 1,
|
|
283
304
|
});
|
|
284
305
|
|
|
285
|
-
if (blocks === null && baked === null
|
|
306
|
+
if (blocks === null && baked === null && capture === null) {
|
|
307
|
+
this.upload(listed as readonly TexImageSource[]);
|
|
308
|
+
}
|
|
286
309
|
|
|
287
310
|
/* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
|
|
288
311
|
and read by `textureLoad` alone, so the filter never runs. */
|
|
@@ -315,6 +338,13 @@ export class GpuSurfaceTexture {
|
|
|
315
338
|
return this.current;
|
|
316
339
|
}
|
|
317
340
|
|
|
341
|
+
/** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
|
|
342
|
+
layerView(): GPUTextureView | null {
|
|
343
|
+
return (
|
|
344
|
+
this.texture?.createView({ dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1 }) ?? null
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
318
348
|
/**
|
|
319
349
|
* Replace the pixels, keeping the handle and its sampler.
|
|
320
350
|
*
|
|
@@ -92,7 +92,7 @@ export const MAX_CLUSTERED_LIGHTS = 320;
|
|
|
92
92
|
* it cannot. That is what lets a light index the shadow uniforms by a number read from a texture,
|
|
93
93
|
* and it is the same fact the octahedral array texture was built on.
|
|
94
94
|
*/
|
|
95
|
-
export const LIGHT_TEXELS =
|
|
95
|
+
export const LIGHT_TEXELS = 6;
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Where each field of a light's record sits, as an offset in `uint`s from `lightBase`.
|
|
@@ -123,7 +123,11 @@ export const LIGHT_TEXELS = 5;
|
|
|
123
123
|
* **Reordered 2026-10-02 so a plain light's fields are the first eight**, which is what lets the
|
|
124
124
|
* shader stop after two texels. Nine values are needed by every light and eight fit in two
|
|
125
125
|
* texels, so the emitter size and the weight share slot 7 as two halves; see `packSizeAndWeight`.
|
|
126
|
-
*
|
|
126
|
+
*
|
|
127
|
+
* **A sixth texel arrived in 4.8.7 for a light's own falloff exponent**, the fifth's last slot
|
|
128
|
+
* having gone to the channels: 6 KB more of a table near 300, read only for a light whose record
|
|
129
|
+
* carries a fixture, which a light with an exponent does. Slots 21 and 22 are a rectangle's barn
|
|
130
|
+
* doors and spare on a lamp; slot 23 is spare.
|
|
127
131
|
*/
|
|
128
132
|
export const LIGHT_RECORD = {
|
|
129
133
|
positionX: 0,
|
|
@@ -154,6 +158,11 @@ export const LIGHT_RECORD = {
|
|
|
154
158
|
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
155
159
|
*/
|
|
156
160
|
channels: 19,
|
|
161
|
+
/** The light's own falloff exponent, or 0 for the frame's falloff. `PointLightSource.falloffExponent`. */
|
|
162
|
+
falloffExponent: 20,
|
|
163
|
+
/** A rectangle's barn doors: the cosine of their angle from its normal, then their length in metres. */
|
|
164
|
+
barnDoorCos: 21,
|
|
165
|
+
barnDoorLength: 22,
|
|
157
166
|
} as const;
|
|
158
167
|
|
|
159
168
|
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
@@ -171,11 +180,20 @@ export function lightChannelsOf(value: number | undefined): number {
|
|
|
171
180
|
return value;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
/**
|
|
184
|
+
* A light's own falloff exponent as the shader reads it: a positive finite number, or 0 for the
|
|
185
|
+
* frame's falloff where none was named or what was named is not one. The record and the uniform
|
|
186
|
+
* path both write this, so a light named badly falls off by the frame's rule in both.
|
|
187
|
+
*/
|
|
188
|
+
export function lightFalloffExponentOf(value: number | undefined): number {
|
|
189
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
175
193
|
export const PLAIN_LIGHT_TEXELS = 2;
|
|
176
194
|
|
|
177
195
|
/**
|
|
178
|
-
* The bit of `sizeAndWeight` that says the record's last
|
|
196
|
+
* The bit of `sizeAndWeight` that says the record's last four texels hold something: the sign of
|
|
179
197
|
* the emitter size's half, which a size never needs.
|
|
180
198
|
*/
|
|
181
199
|
export const LIGHT_FIXTURE_FLAG = 0x8000;
|
|
@@ -426,6 +444,8 @@ export interface ClusterLightSet {
|
|
|
426
444
|
readonly iesProfiles?: Float32Array;
|
|
427
445
|
/** One per light: its lighting channels. See `PointLightSet.lightChannels`. */
|
|
428
446
|
readonly channels?: Float32Array;
|
|
447
|
+
/** One per light: its own falloff exponent, or 0. See `PointLightSource.falloffExponent`. */
|
|
448
|
+
readonly falloffExponents?: Float32Array;
|
|
429
449
|
/**
|
|
430
450
|
* The rectangles shaded through the table, after the `count` point lights: those of `areas` from
|
|
431
451
|
* `areaFrom` on, `areaCount` of them. The ones before `areaFrom` are the fixed arm's, which is
|
|
@@ -450,6 +470,7 @@ export interface ClusterLightArrays {
|
|
|
450
470
|
readonly lightIesAxes?: Float32Array;
|
|
451
471
|
readonly lightCookies?: Float32Array;
|
|
452
472
|
readonly lightChannels?: Float32Array;
|
|
473
|
+
readonly lightFalloffExponents?: Float32Array;
|
|
453
474
|
readonly areaLights?: AreaLightBuffer | null;
|
|
454
475
|
}
|
|
455
476
|
|
|
@@ -510,6 +531,7 @@ export function fillClusterLightSet(
|
|
|
510
531
|
out.iesAxes = env.lightIesAxes;
|
|
511
532
|
out.cookies = env.lightCookies;
|
|
512
533
|
out.channels = env.lightChannels;
|
|
534
|
+
out.falloffExponents = env.lightFalloffExponents;
|
|
513
535
|
return out;
|
|
514
536
|
}
|
|
515
537
|
|
|
@@ -559,8 +581,8 @@ function distanceSqToBounds(x: number, y: number, z: number, b: Float32Array): n
|
|
|
559
581
|
}
|
|
560
582
|
|
|
561
583
|
/**
|
|
562
|
-
* Whether a light's record needs its last
|
|
563
|
-
* or a
|
|
584
|
+
* Whether a light's record needs its last four texels: it holds a shadow slot, a cone, a profile,
|
|
585
|
+
* a cookie, channels of its own or a falloff exponent of its own. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
|
|
564
586
|
*
|
|
565
587
|
* **`shadowSlots` is zero where the lit shader has no point shadows**, and both binners are handed
|
|
566
588
|
* it that way: a slot is only read under `POINT_SHADOWS`, so a light holding one in a build without
|
|
@@ -580,7 +602,8 @@ export function lightHasFixture(
|
|
|
580
602
|
(cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
|
|
581
603
|
(lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
582
604
|
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
583
|
-
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS
|
|
605
|
+
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS ||
|
|
606
|
+
lightFalloffExponentOf(lights.falloffExponents?.[light]) > 0
|
|
584
607
|
);
|
|
585
608
|
}
|
|
586
609
|
|
|
@@ -665,6 +688,12 @@ export function writeLightRecord(
|
|
|
665
688
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
|
|
666
689
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
|
|
667
690
|
out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
|
|
691
|
+
out[at + LIGHT_RECORD.falloffExponent] = floatBits(
|
|
692
|
+
lightFalloffExponentOf(lights.falloffExponents?.[light]),
|
|
693
|
+
);
|
|
694
|
+
out[at + LIGHT_RECORD.barnDoorCos] = 0;
|
|
695
|
+
out[at + LIGHT_RECORD.barnDoorLength] = 0;
|
|
696
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
668
697
|
}
|
|
669
698
|
|
|
670
699
|
/**
|
|
@@ -677,9 +706,9 @@ function areaSlot(lights: ClusterLightSet, light: number): number {
|
|
|
677
706
|
|
|
678
707
|
/**
|
|
679
708
|
* A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
|
|
680
|
-
* reach, negated — and reads to its
|
|
709
|
+
* reach, negated — and reads to its sixth texel: sidedness where a lamp keeps its shadow slot,
|
|
681
710
|
* the right axis where a spot keeps its direction, the half extents where its cone is, the up
|
|
682
|
-
* axis where the azimuth is, and the
|
|
711
|
+
* axis where the azimuth is, the channels, and the barn doors in the sixth. Never shadowed: a rectangle's occlusion is
|
|
683
712
|
* the fixed arm's, which is why the first `maxAreaLights` stay there.
|
|
684
713
|
*/
|
|
685
714
|
function writeAreaRecord(
|
|
@@ -715,6 +744,10 @@ function writeAreaRecord(
|
|
|
715
744
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
|
|
716
745
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
|
|
717
746
|
out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
|
|
747
|
+
out[at + LIGHT_RECORD.falloffExponent] = 0;
|
|
748
|
+
out[at + LIGHT_RECORD.barnDoorCos] = floatBits(areas?.barnDoors?.[a * 2] ?? 0);
|
|
749
|
+
out[at + LIGHT_RECORD.barnDoorLength] = floatBits(areas?.barnDoors?.[a * 2 + 1] ?? 0);
|
|
750
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
718
751
|
}
|
|
719
752
|
|
|
720
753
|
/** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
|
|
@@ -32,6 +32,8 @@ export interface FieldLight extends LightReach {
|
|
|
32
32
|
readonly b: number;
|
|
33
33
|
/** The emitter's physical radius, which widens its penumbra. */
|
|
34
34
|
readonly sourceRadius: number;
|
|
35
|
+
/** Its own falloff exponent, overriding the frame's above zero. See `pointLightShape`. */
|
|
36
|
+
readonly falloffExponent?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -115,7 +117,7 @@ export function sampleFieldLight(
|
|
|
115
117
|
const dz = source.z - pz;
|
|
116
118
|
const dist = Math.hypot(dx, dy, dz);
|
|
117
119
|
if (dist >= source.radius) continue;
|
|
118
|
-
const shape = pointLightShape(dist, source.radius, falloff);
|
|
120
|
+
const shape = pointLightShape(dist, source.radius, falloff, source.falloffExponent);
|
|
119
121
|
if (shape <= 0) continue;
|
|
120
122
|
const seen =
|
|
121
123
|
distance === null
|
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
|
|
11
11
|
* the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
|
|
12
|
-
* floored at ten centimetres so a surface touching a light is not infinitely bright.
|
|
12
|
+
* floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
|
|
13
|
+
* own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
|
|
14
|
+
* (`PointLightSource.falloffExponent`).
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export type PointLightFalloff = 'smooth' | 'inverseSquare';
|
|
16
18
|
|
|
17
19
|
/** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
|
|
18
|
-
export function pointLightShape(
|
|
20
|
+
export function pointLightShape(
|
|
21
|
+
dist: number,
|
|
22
|
+
radius: number,
|
|
23
|
+
falloff: PointLightFalloff,
|
|
24
|
+
exponent = 0,
|
|
25
|
+
): number {
|
|
26
|
+
if (exponent > 0) {
|
|
27
|
+
const reach = dist / Math.max(radius, 1e-4);
|
|
28
|
+
return Math.min(1, Math.max(0, 1 - reach * reach)) ** exponent;
|
|
29
|
+
}
|
|
19
30
|
if (falloff === 'inverseSquare') {
|
|
20
31
|
const ratio = dist / Math.max(radius, 1e-4);
|
|
21
32
|
const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
|
|
@@ -23,15 +23,20 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import type { PointLightSet } from '../lightBudget.ts';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
POINT_LIGHT_COS_INNER,
|
|
28
|
+
POINT_LIGHT_COS_OUTER,
|
|
29
|
+
lightFalloffExponentOf,
|
|
30
|
+
} from '../clusteredLights.ts';
|
|
27
31
|
|
|
28
32
|
/** Lights the probe bake shades a hit with. */
|
|
29
33
|
export const MAX_BOUNCE_LIGHTS = 32;
|
|
30
34
|
/**
|
|
31
35
|
* Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
|
|
32
|
-
* of its inner cone; the way it points and the cosine of its outer cone
|
|
36
|
+
* of its inner cone; the way it points and the cosine of its outer cone; and its own falloff
|
|
37
|
+
* exponent, 0 for the frame's, in a fourth row of its own (`PointLightSource.falloffExponent`).
|
|
33
38
|
*/
|
|
34
|
-
export const BOUNCE_LIGHT_FLOATS =
|
|
39
|
+
export const BOUNCE_LIGHT_FLOATS = 16;
|
|
35
40
|
|
|
36
41
|
export function createBounceLights(): Float32Array {
|
|
37
42
|
return new Float32Array(MAX_BOUNCE_LIGHTS * BOUNCE_LIGHT_FLOATS);
|
|
@@ -62,6 +67,10 @@ export function resolveBounceLights(lights: PointLightSet | null, out: Float32Ar
|
|
|
62
67
|
out[at + 9] = lights.lightDirections?.[i * 3 + 1] ?? 0;
|
|
63
68
|
out[at + 10] = lights.lightDirections?.[i * 3 + 2] ?? 0;
|
|
64
69
|
out[at + 11] = lights.lightConeCos?.[i * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
70
|
+
out[at + 12] = lightFalloffExponentOf(lights.lightFalloffExponents?.[i]);
|
|
71
|
+
out[at + 13] = 0;
|
|
72
|
+
out[at + 14] = 0;
|
|
73
|
+
out[at + 15] = 0;
|
|
65
74
|
count++;
|
|
66
75
|
}
|
|
67
76
|
return count;
|
|
@@ -76,6 +76,10 @@ export function cullInstances(
|
|
|
76
76
|
if (regions !== undefined && out.lightmapRegions !== undefined) {
|
|
77
77
|
for (let k = 0; k < 4; k++) out.lightmapRegions[kept * 4 + k] = regions[i * 4 + k] as number;
|
|
78
78
|
}
|
|
79
|
+
const cells = data.uvRegions;
|
|
80
|
+
if (cells !== undefined && out.uvRegions !== undefined) {
|
|
81
|
+
for (let k = 0; k < 4; k++) out.uvRegions[kept * 4 + k] = cells[i * 4 + k] as number;
|
|
82
|
+
}
|
|
79
83
|
kept++;
|
|
80
84
|
}
|
|
81
85
|
out.count = kept;
|
package/src/render/instances.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **What it gives up** is a matrix per instance where a scatter spends five floats: 80 bytes
|
|
14
14
|
* against 44, and no wind. **What would make it wrong** is a caller wanting per-instance
|
|
15
|
-
* anything beyond an opacity — a morph weight,
|
|
16
|
-
* and the sixteen WebGL2 guarantees are already spent. The opacity rides the float the
|
|
17
|
-
* padded with, which is why
|
|
15
|
+
* anything beyond an opacity and a texture cell — a morph weight, say — which wants another
|
|
16
|
+
* attribute, and the sixteen WebGL2 guarantees are already spent. The opacity rides the float the
|
|
17
|
+
* stride was padded with and the cell the matrix's bottom row, which is why neither costs a byte.
|
|
18
18
|
*/
|
|
19
19
|
export interface MeshInstances {
|
|
20
20
|
/**
|
|
@@ -51,6 +51,20 @@ export interface MeshInstances {
|
|
|
51
51
|
* either mismatch is said once on the console.
|
|
52
52
|
*/
|
|
53
53
|
readonly lightmapRegions?: Float32Array;
|
|
54
|
+
/**
|
|
55
|
+
* Four floats each, the instance's cell of its texture: `[scaleU, scaleV, offsetU, offsetV]`,
|
|
56
|
+
* applied to the mesh's coordinates before the material's own scale and offset, in every pass
|
|
57
|
+
* that reads them — so one draw carries particles in different cells of a flipbook, or props
|
|
58
|
+
* wearing different tiles of one atlas. **Absent, or past `count`, is the whole texture**,
|
|
59
|
+
* `[1, 1, 0, 0]`, which is what every instance read before 4.8.7.
|
|
60
|
+
*
|
|
61
|
+
* **It rides the matrix's bottom row**, which an instance's placement — a turn, a scale and a
|
|
62
|
+
* move — leaves at `[0, 0, 0, 1]`, and which every stage drawing an instance rebuilds as that.
|
|
63
|
+
* So a placement must be affine, as every placement of a solid object is; a projective one is
|
|
64
|
+
* drawn as its affine part. A culled batch (`cullInstances`) carries the cells only into a
|
|
65
|
+
* target that has the array.
|
|
66
|
+
*/
|
|
67
|
+
readonly uvRegions?: Float32Array;
|
|
54
68
|
/** How many instances the arrays hold. Fixed at creation; the buffers are sized from it. */
|
|
55
69
|
readonly capacity: number;
|
|
56
70
|
/** How many are live. The rest of the buffer is neither uploaded nor drawn. */
|
|
@@ -75,8 +89,8 @@ export function createMeshInstances(capacity: number): MeshInstances {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
|
-
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix,
|
|
79
|
-
* tint, and the opacity.
|
|
92
|
+
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, the bottom row
|
|
93
|
+
* of which carries the texture cell, three of tint, and the opacity.
|
|
80
94
|
*
|
|
81
95
|
* **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
|
|
82
96
|
* of four on both backends, which 76 already is — the twentieth float was padding for the
|
|
@@ -101,12 +115,18 @@ export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
|
|
|
101
115
|
* Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
|
|
102
116
|
*/
|
|
103
117
|
export function packInstances(instances: MeshInstances, out: Float32Array): void {
|
|
104
|
-
const { models, tints, alphas, lightmapRegions } = instances;
|
|
118
|
+
const { models, tints, alphas, lightmapRegions, uvRegions } = instances;
|
|
105
119
|
const count = Math.min(instances.count, instances.capacity);
|
|
106
120
|
for (let i = 0; i < count; i += 1) {
|
|
107
121
|
const at = i * INSTANCE_FLOATS;
|
|
108
122
|
const m = i * 16;
|
|
109
123
|
for (let c = 0; c < 16; c += 1) out[at + c] = models[m + c] as number;
|
|
124
|
+
/* The texture cell in the bottom row an affine placement leaves free. See `uvRegions`. */
|
|
125
|
+
const cell = i * 4;
|
|
126
|
+
out[at + 3] = uvRegions?.[cell] ?? 1;
|
|
127
|
+
out[at + 7] = uvRegions?.[cell + 1] ?? 1;
|
|
128
|
+
out[at + 11] = uvRegions?.[cell + 2] ?? 0;
|
|
129
|
+
out[at + 15] = uvRegions?.[cell + 3] ?? 0;
|
|
110
130
|
if (lightmapRegions !== undefined) {
|
|
111
131
|
/* The region in the tint's and the opacity's place. See `lightmapRegions`. */
|
|
112
132
|
for (let c = 0; c < 4; c += 1) out[at + 16 + c] = lightmapRegions[i * 4 + c] as number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Fixed point-light budget shared by CPU selection and the flat shader. */
|
|
2
|
+
import { MAX_AREA_LIGHTS, packAreaSizesAndDoors } from './areaLights.ts';
|
|
2
3
|
import type { AreaLightBuffer } from './areaLights.ts';
|
|
3
4
|
import type { ResolvedAreaShadows } from './areaShadowSet.ts';
|
|
4
5
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
lampSourceRadius,
|
|
8
9
|
lampWeight,
|
|
9
10
|
lightChannelsOf,
|
|
11
|
+
lightFalloffExponentOf,
|
|
10
12
|
} from './clusteredLights.ts';
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -401,11 +403,20 @@ export interface PointLightSet {
|
|
|
401
403
|
* character's own key and rim lights, which the floor around the character must not take.
|
|
402
404
|
*/
|
|
403
405
|
readonly lightChannels?: Float32Array;
|
|
406
|
+
/**
|
|
407
|
+
* One per light: its own falloff exponent, `(1 - (d/R)^2)^n` with no distance term, or 0 for the
|
|
408
|
+
* frame's falloff. **Absent, or a short array, is 0 for every light**, so a scene that names none
|
|
409
|
+
* falls off as it did. See `PointLightSource.falloffExponent`.
|
|
410
|
+
*/
|
|
411
|
+
readonly lightFalloffExponents?: Float32Array;
|
|
404
412
|
}
|
|
405
413
|
|
|
406
414
|
/** Weights are optional per consumer; a short array means every light is fully present. */
|
|
407
415
|
const FULLY_PRESENT: Float32Array = new Float32Array(MAX_POINT_LIGHTS).fill(1);
|
|
408
416
|
|
|
417
|
+
/** The fixed arm's rectangles as `uAreaLightSize` holds them; refilled per bind, allocated once. */
|
|
418
|
+
const AREA_SIZES_AND_DOORS = new Float32Array(MAX_AREA_LIGHTS * 4);
|
|
419
|
+
|
|
409
420
|
/** No size at all, which is the behaviour every scene had before a light could have one. */
|
|
410
421
|
const POINT_SOURCES: Float32Array = new Float32Array(MAX_POINT_LIGHTS);
|
|
411
422
|
/**
|
|
@@ -504,7 +515,8 @@ export function bindPointLights(
|
|
|
504
515
|
* every lamp switched off.
|
|
505
516
|
*/
|
|
506
517
|
gl.uniform3fv(uniforms['uLightDir[0]'] ?? null, r.directions);
|
|
507
|
-
|
|
518
|
+
/* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
|
|
519
|
+
gl.uniform4fv(uniforms['uLightCone[0]'] ?? null, r.conesAndFalloffs);
|
|
508
520
|
gl.uniform1fv(uniforms['uLightIesProfile[0]'] ?? null, r.iesProfiles);
|
|
509
521
|
/* The azimuth and, in w, the channels. See `axesAndChannels`. */
|
|
510
522
|
gl.uniform4fv(uniforms['uLightIesAxis[0]'] ?? null, r.axesAndChannels);
|
|
@@ -535,7 +547,9 @@ export function bindAreaLights(
|
|
|
535
547
|
gl.uniform3fv(uniforms['uAreaLightColor[0]'] ?? null, lights.colors);
|
|
536
548
|
gl.uniform3fv(uniforms['uAreaLightRight[0]'] ?? null, lights.right);
|
|
537
549
|
gl.uniform3fv(uniforms['uAreaLightUp[0]'] ?? null, lights.up);
|
|
538
|
-
|
|
550
|
+
/* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
|
|
551
|
+
packAreaSizesAndDoors(lights, AREA_SIZES_AND_DOORS);
|
|
552
|
+
gl.uniform4fv(uniforms['uAreaLightSize[0]'] ?? null, AREA_SIZES_AND_DOORS);
|
|
539
553
|
gl.uniform1fv(uniforms['uAreaLightTwoSided[0]'] ?? null, lights.twoSided);
|
|
540
554
|
}
|
|
541
555
|
|
|
@@ -589,6 +603,12 @@ export interface ResolvedPointLights {
|
|
|
589
603
|
* array of their own, which would cost a row a light of a budget that has none to give.
|
|
590
604
|
*/
|
|
591
605
|
readonly axesAndChannels: Float32Array;
|
|
606
|
+
/**
|
|
607
|
+
* Owned, four a light: the cone's two cosines, then the light's own falloff exponent, as
|
|
608
|
+
* `uLightCone` carries them — a `vec2` array costs a whole row a light on WebGL2 anyway, so the
|
|
609
|
+
* exponent rides the half it left rather than a row of its own.
|
|
610
|
+
*/
|
|
611
|
+
readonly conesAndFalloffs: Float32Array;
|
|
592
612
|
/**
|
|
593
613
|
* Owned, `MAX_POINT_LIGHTS` long: the emitter sizes and weights as the froxel record carries
|
|
594
614
|
* them, at half precision. `sourceRadii` and `weights` point here once a light set resolves, so
|
|
@@ -614,6 +634,7 @@ const glLights: ResolvedPointLights = {
|
|
|
614
634
|
iesAxes: NO_IES_AXES,
|
|
615
635
|
cookies: NO_COOKIES,
|
|
616
636
|
axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
637
|
+
conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
617
638
|
heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
|
|
618
639
|
heldWeights: new Float32Array(MAX_POINT_LIGHTS),
|
|
619
640
|
};
|
|
@@ -649,6 +670,7 @@ export function resolvePointLights(
|
|
|
649
670
|
out.iesAxes = NO_IES_AXES;
|
|
650
671
|
out.cookies = NO_COOKIES;
|
|
651
672
|
packAxesAndChannels(NO_IES_AXES, undefined, out.axesAndChannels);
|
|
673
|
+
packConesAndFalloffs(OPEN_CONES, undefined, out.conesAndFalloffs);
|
|
652
674
|
return out;
|
|
653
675
|
}
|
|
654
676
|
/*
|
|
@@ -742,9 +764,25 @@ export function resolvePointLights(
|
|
|
742
764
|
out.iesAxes = iesAxes;
|
|
743
765
|
out.cookies = cookies;
|
|
744
766
|
packAxesAndChannels(iesAxes, lights.lightChannels, out.axesAndChannels);
|
|
767
|
+
packConesAndFalloffs(coneCos, lights.lightFalloffExponents, out.conesAndFalloffs);
|
|
745
768
|
return out;
|
|
746
769
|
}
|
|
747
770
|
|
|
771
|
+
/** The cone and the falloff exponent, four a light, as `uLightCone` holds them. */
|
|
772
|
+
function packConesAndFalloffs(
|
|
773
|
+
cones: Float32Array,
|
|
774
|
+
exponents: Float32Array | undefined,
|
|
775
|
+
out: Float32Array,
|
|
776
|
+
): void {
|
|
777
|
+
const named = (exponents?.length ?? 0) >= MAX_POINT_LIGHTS ? exponents : undefined;
|
|
778
|
+
for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
|
|
779
|
+
out[light * 4] = cones[light * 2] ?? POINT_LIGHT_COS_INNER;
|
|
780
|
+
out[light * 4 + 1] = cones[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
781
|
+
out[light * 4 + 2] = lightFalloffExponentOf(named?.[light]);
|
|
782
|
+
out[light * 4 + 3] = 0;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
748
786
|
/** The azimuth and the channels, four a light, as `uLightIesAxis` holds them. */
|
|
749
787
|
function packAxesAndChannels(
|
|
750
788
|
axes: Float32Array,
|
package/src/render/lightmap.ts
CHANGED
|
@@ -30,17 +30,19 @@
|
|
|
30
30
|
* regions no tint. **What it does not do**: the GPU-driven pipeline and splats read no page.
|
|
31
31
|
*/
|
|
32
32
|
import type { MeshData } from '@driftengine/drft';
|
|
33
|
-
import {
|
|
33
|
+
import { packRgb9e5 } from './rgb9e5.ts';
|
|
34
34
|
|
|
35
35
|
/** One page as a consumer decoded it. */
|
|
36
36
|
export interface LightmapPage {
|
|
37
37
|
readonly width: number;
|
|
38
38
|
readonly height: number;
|
|
39
39
|
/**
|
|
40
|
-
* Linear irradiance,
|
|
41
|
-
*
|
|
40
|
+
* Linear irradiance, row-major from the top, in the same units every other light here is in — a
|
|
41
|
+
* bake from another engine scaled by that engine's exposure, as its lights are. Either three
|
|
42
|
+
* floats a texel, or one word a texel already packed as rgb9e5 (`rgb9e5.ts`), which is how such a
|
|
43
|
+
* bake usually arrives and which uploads with no conversion at all.
|
|
42
44
|
*/
|
|
43
|
-
readonly irradiance: Float32Array;
|
|
45
|
+
readonly irradiance: Float32Array | Uint32Array;
|
|
44
46
|
/**
|
|
45
47
|
* The direction the light arrives from, four bytes a texel: a first-order spherical harmonic in
|
|
46
48
|
* this engine's world axes, each value stored as `v * 0.5 + 0.5` — xyz the axis, w the constant.
|
|
@@ -60,13 +62,25 @@ export type LightmapRegion = readonly [number, number, number, number];
|
|
|
60
62
|
/** The whole page. */
|
|
61
63
|
export const WHOLE_PAGE: LightmapRegion = [1, 1, 0, 0];
|
|
62
64
|
|
|
63
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* A page as both backends upload it: two layers of four bytes a texel, row-major from the top. The
|
|
67
|
+
* first is the irradiance's rgb9e5 word, byte by byte from its lowest; the second is the direction.
|
|
68
|
+
*
|
|
69
|
+
* **Eight bytes a texel, and the shader filters it.** Half floats were sixteen: a stage of 49 pages
|
|
70
|
+
* and 21.8 million texels held 348 MB, where this holds 174. The price is that the irradiance cannot
|
|
71
|
+
* be filtered as it is stored — a bilinear blend of two words with different exponents is not the
|
|
72
|
+
* blend of the colours they hold — so the lit stage fetches a texel's four neighbours, decodes each
|
|
73
|
+
* and blends the colours (`modelBaked` in `shaders/flat/models.ts`): four fetches a layer where one
|
|
74
|
+
* filtered sample was, on lightmapped surfaces alone. **What would make it wrong** is every device
|
|
75
|
+
* a page reaches sampling `rgb9e5ufloat`, which WebGL2 does as `RGB9_E5` too; it would then be a
|
|
76
|
+
* texture of its own, filtered by the device, and a binding the lit stage has no room for.
|
|
77
|
+
*/
|
|
64
78
|
export interface LightmapTexels {
|
|
65
79
|
readonly kind: 'lightmap-texels';
|
|
66
80
|
readonly width: number;
|
|
67
81
|
readonly height: number;
|
|
68
|
-
|
|
69
|
-
readonly
|
|
82
|
+
/** Each layer's bytes, in layer order: the irradiance, then the direction. */
|
|
83
|
+
readonly layers: readonly Uint8Array[];
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
/** Whether a surface texture's source is a page's texels rather than an image or blocks. */
|
|
@@ -79,9 +93,9 @@ export function isLightmapTexels(source: unknown): source is LightmapTexels {
|
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/**
|
|
82
|
-
* A page as both backends upload it
|
|
83
|
-
*
|
|
84
|
-
*
|
|
96
|
+
* A page as both backends upload it (see `LightmapTexels`). Refuses a page whose arrays are not its
|
|
97
|
+
* size, naming the one that is short. Packed irradiance is read where it lies, with no copy; float
|
|
98
|
+
* irradiance is packed once, here.
|
|
85
99
|
*/
|
|
86
100
|
export function lightmapTexels(page: LightmapPage): LightmapTexels {
|
|
87
101
|
const { width, height } = page;
|
|
@@ -89,10 +103,14 @@ export function lightmapTexels(page: LightmapPage): LightmapTexels {
|
|
|
89
103
|
throw new Error(`createLightmap: a ${width} by ${height} page has nothing in it`);
|
|
90
104
|
}
|
|
91
105
|
const texels = width * height;
|
|
92
|
-
|
|
106
|
+
const irradiance = page.irradiance;
|
|
107
|
+
const packed = irradiance instanceof Uint32Array;
|
|
108
|
+
const wanted = packed ? texels : texels * 3;
|
|
109
|
+
if (irradiance.length !== wanted) {
|
|
93
110
|
throw new Error(
|
|
94
|
-
`createLightmap: ${
|
|
95
|
-
|
|
111
|
+
`createLightmap: ${irradiance.length} irradiance ${packed ? 'words' : 'floats'} for a ` +
|
|
112
|
+
`${width} by ${height} page, which wants ${wanted}: ` +
|
|
113
|
+
(packed ? 'one rgb9e5 word a texel' : 'three a texel'),
|
|
96
114
|
);
|
|
97
115
|
}
|
|
98
116
|
if (page.direction.length !== texels * 4) {
|
|
@@ -101,24 +119,47 @@ export function lightmapTexels(page: LightmapPage): LightmapTexels {
|
|
|
101
119
|
`page, which wants ${texels * 4}: four a texel`,
|
|
102
120
|
);
|
|
103
121
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
let words: Uint32Array;
|
|
123
|
+
if (packed) {
|
|
124
|
+
words = irradiance;
|
|
125
|
+
} else {
|
|
126
|
+
words = new Uint32Array(texels);
|
|
127
|
+
for (let t = 0; t < texels; t++) {
|
|
128
|
+
words[t] = packRgb9e5(
|
|
129
|
+
irradiance[t * 3] as number,
|
|
130
|
+
irradiance[t * 3 + 1] as number,
|
|
131
|
+
irradiance[t * 3 + 2] as number,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
return {
|
|
114
136
|
kind: 'lightmap-texels',
|
|
115
137
|
width,
|
|
116
138
|
height,
|
|
117
|
-
layers:
|
|
118
|
-
texels: toHalfFloats(floats),
|
|
139
|
+
layers: [littleEndianBytes(words), page.direction],
|
|
119
140
|
};
|
|
120
141
|
}
|
|
121
142
|
|
|
143
|
+
/** Whether this platform lays a word out lowest byte first, which the shader's decode assumes. */
|
|
144
|
+
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A page's words as bytes, lowest first: a view of the same memory on every little-endian platform,
|
|
148
|
+
* which is every one a browser runs on, and a reordered copy anywhere else.
|
|
149
|
+
*/
|
|
150
|
+
function littleEndianBytes(words: Uint32Array): Uint8Array {
|
|
151
|
+
const bytes = new Uint8Array(words.buffer, words.byteOffset, words.length * 4);
|
|
152
|
+
if (LITTLE_ENDIAN) return bytes;
|
|
153
|
+
const swapped = new Uint8Array(bytes.length);
|
|
154
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
155
|
+
swapped[i] = bytes[i + 3] as number;
|
|
156
|
+
swapped[i + 1] = bytes[i + 2] as number;
|
|
157
|
+
swapped[i + 2] = bytes[i + 1] as number;
|
|
158
|
+
swapped[i + 3] = bytes[i] as number;
|
|
159
|
+
}
|
|
160
|
+
return swapped;
|
|
161
|
+
}
|
|
162
|
+
|
|
122
163
|
/**
|
|
123
164
|
* A mesh with its second coordinates moved onto the grain and relief streams they ride, or the
|
|
124
165
|
* mesh itself where it has none. Called by both backends' `createMesh`, before anything records
|