reze-engine 0.56.11 → 0.56.13
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/dist/camera.d.ts +13 -1
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +25 -2
- package/dist/engine.d.ts +75 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +257 -53
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +20 -0
- package/dist/ibl.d.ts +28 -0
- package/dist/ibl.d.ts.map +1 -1
- package/dist/ibl.js +21 -0
- package/dist/shaders/lights.d.ts +16 -6
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -6
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +7 -0
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +41 -0
- package/package.json +1 -1
- package/src/camera.ts +26 -2
- package/src/engine.ts +281 -60
- package/src/graph/registry.ts +21 -0
- package/src/ibl.ts +41 -0
- package/src/shaders/lights.ts +16 -6
- package/src/shaders/materials/common.ts +7 -0
- package/src/shaders/materials/nodes.ts +41 -0
package/dist/engine.js
CHANGED
|
@@ -26,7 +26,7 @@ import { SHADOW_CASCADES, buildShadowVP } from "./shadow-cascades";
|
|
|
26
26
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection";
|
|
27
27
|
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror";
|
|
28
28
|
import { packHalf } from "./hdr";
|
|
29
|
-
import { evalIrradianceSH, projectIrradianceSH } from "./ibl";
|
|
29
|
+
import { evalIrradianceSH, projectIrradianceSH, gradientIrradianceSH } from "./ibl";
|
|
30
30
|
import { LYRIC_ATLAS_MAX_H, LYRIC_ATLAS_MAX_W, LYRICS_FLOATS, packLyrics } from "./shaders/lyrics-api";
|
|
31
31
|
import { sceneTargets as sceneTargetsFor, sceneColorFormats, setMrtIds, mrtIdsEnabled, SCENE_ID_FORMAT, } from "./shaders/passes/scene-contract";
|
|
32
32
|
import { LIGHT_HEADER, LIGHT_STRIDE, LIGHTS_FLOATS, MAX_LIGHTS, buildLightEmitShader, hasLightEmit, } from "./shaders/lights";
|
|
@@ -1011,6 +1011,9 @@ export class Engine {
|
|
|
1011
1011
|
this.worldEquirectTexture = null;
|
|
1012
1012
|
this.worldEquirectView = null;
|
|
1013
1013
|
this.worldStrength = 1;
|
|
1014
|
+
/** A three-colour sky's irradiance SH, when the world is a gradient rather
|
|
1015
|
+
* than a picture. An HDRI outranks it — see writeWorld. */
|
|
1016
|
+
this.worldGradientSH = null;
|
|
1014
1017
|
/** The installed HDRI's folded irradiance SH (27 floats), or null. */
|
|
1015
1018
|
this.worldSH = null;
|
|
1016
1019
|
// The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
|
|
@@ -1175,6 +1178,37 @@ export class Engine {
|
|
|
1175
1178
|
/** A pose pushed in from outside — see setCameraPose. Reapplied every frame,
|
|
1176
1179
|
* so it outranks the orbit AND a loaded track for as long as it is set. */
|
|
1177
1180
|
this.cameraPoseOverride = null;
|
|
1181
|
+
/**
|
|
1182
|
+
* The per-frame bind groups, the camera's and the mirror's.
|
|
1183
|
+
*
|
|
1184
|
+
* A METHOD rather than two literals at init, because one of their resources
|
|
1185
|
+
* changes after it: the world's sky texture is swapped whenever an HDRI is
|
|
1186
|
+
* installed or cleared, and a bind group holds the view it was built with.
|
|
1187
|
+
* The mirror's is identical but for the camera — a reflection is the same
|
|
1188
|
+
* scene lit the same way, seen from a reflected eye.
|
|
1189
|
+
*/
|
|
1190
|
+
/**
|
|
1191
|
+
* Skies this engine has replaced, kept alive rather than destroyed.
|
|
1192
|
+
*
|
|
1193
|
+
* NOT DESTROYED AT ALL, after three attempts that each deferred it further:
|
|
1194
|
+
* one frame, then three, then "once both bind groups have rebuilt". Every one
|
|
1195
|
+
* still produced "destroyed texture used in a submit" somewhere — a command
|
|
1196
|
+
* buffer encoded before the swap, a rebuild that bailed on half-built
|
|
1197
|
+
* resources, an engine torn down by a hot reload whose last frame is still in
|
|
1198
|
+
* flight. The engine cannot see all of those, and each attempt to enumerate
|
|
1199
|
+
* them found another.
|
|
1200
|
+
*
|
|
1201
|
+
* So the trade is taken explicitly: a replaced sky is a megabyte or two held
|
|
1202
|
+
* until the page goes away, against a validation error every frame. A scene
|
|
1203
|
+
* swaps its world a handful of times in a session, and the GPU frees all of
|
|
1204
|
+
* it when the device does.
|
|
1205
|
+
*/
|
|
1206
|
+
this.retiredSkies = [];
|
|
1207
|
+
/** A world was installed or cleared and the bind groups that name its texture
|
|
1208
|
+
* may not have been rebuilt yet — both rebuilds bail when their own
|
|
1209
|
+
* resources are half-built, which is exactly the moment a host swaps a sky.
|
|
1210
|
+
* Retried at the top of a frame until one of them takes. */
|
|
1211
|
+
this.worldBindingsDirty = false;
|
|
1178
1212
|
/** Per cascade: does its map currently hold nothing but the cleared far plane?
|
|
1179
1213
|
* Set by the cascade loop, which skips a cascade that is unwanted and already
|
|
1180
1214
|
* cleared rather than re-clearing it every frame. */
|
|
@@ -1215,6 +1249,9 @@ export class Engine {
|
|
|
1215
1249
|
this.cpuRenderMs = 0;
|
|
1216
1250
|
this.frameAnimMsRaw = 0;
|
|
1217
1251
|
this.framePhysicsMsRaw = 0;
|
|
1252
|
+
/** How far the loaded scene reaches from the origin, in world units — the
|
|
1253
|
+
* largest any model has ever needed. */
|
|
1254
|
+
this.sceneExtent = 0;
|
|
1218
1255
|
/** Every shadow caster in one sphere: (x, y, z, radius). radius 0 = nothing
|
|
1219
1256
|
* casts, -1 = do not use (a rigid caster has no sphere). See updateCasterSphere. */
|
|
1220
1257
|
/** A card's own texture, by model key — see setPlaneFrame. */
|
|
@@ -2406,11 +2443,14 @@ export class Engine {
|
|
|
2406
2443
|
errors: [...this.gpuErrors].map(([message, count]) => ({ message, count })),
|
|
2407
2444
|
};
|
|
2408
2445
|
}
|
|
2446
|
+
/** Returns whether it actually rebuilt: a caller retrying after a resource
|
|
2447
|
+
* swap has to know, because a bail leaves the previous group — and the
|
|
2448
|
+
* previous group names the texture the swap replaced. */
|
|
2409
2449
|
rebuildCompositeBindGroup() {
|
|
2410
2450
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView || !this.depthReadView)
|
|
2411
|
-
return;
|
|
2451
|
+
return false;
|
|
2412
2452
|
if (!this.castBuffer)
|
|
2413
|
-
return;
|
|
2453
|
+
return false;
|
|
2414
2454
|
// BEFORE the entries below, not after: they ask fieldPairUsed which effects
|
|
2415
2455
|
// draw, and that answer includes whether an effect has its field bind group
|
|
2416
2456
|
// yet. Building the composite first and the field groups second would bind
|
|
@@ -2445,6 +2485,7 @@ export class Engine {
|
|
|
2445
2485
|
{ binding: 21, resource: this.fieldLayerView(this.fieldFgViews[1], 1) },
|
|
2446
2486
|
],
|
|
2447
2487
|
});
|
|
2488
|
+
return true;
|
|
2448
2489
|
}
|
|
2449
2490
|
/**
|
|
2450
2491
|
* Does any installed effect draw into field pair `layer`?
|
|
@@ -2887,7 +2928,14 @@ export class Engine {
|
|
|
2887
2928
|
* coefficients, so what lights her is what you see.
|
|
2888
2929
|
*/
|
|
2889
2930
|
setWorldEquirect(source, options) {
|
|
2890
|
-
|
|
2931
|
+
// RETIRED, NOT DESTROYED. A frame already encoded against the old sky can
|
|
2932
|
+
// still be in flight — the bind groups that name it are rebuilt below, but
|
|
2933
|
+
// the command buffer holding the previous ones is submitted at the end of
|
|
2934
|
+
// the frame this call landed in the middle of. Destroying here is what
|
|
2935
|
+
// produces "Destroyed texture used in a submit"; the next frame's start is
|
|
2936
|
+
// the first moment nothing can be pointing at it.
|
|
2937
|
+
if (this.worldEquirectTexture)
|
|
2938
|
+
this.retiredSkies.push(this.worldEquirectTexture);
|
|
2891
2939
|
this.worldEquirectTexture = null;
|
|
2892
2940
|
this.worldEquirectView = null;
|
|
2893
2941
|
this.worldStrength = Math.max(options?.strength ?? 1, 0);
|
|
@@ -2897,13 +2945,49 @@ export class Engine {
|
|
|
2897
2945
|
// Scene-linear radiance in rgba16float. The composite treats it as light
|
|
2898
2946
|
// rather than wallpaper (mode 3) — a sun in it rolls off like a sun,
|
|
2899
2947
|
// through the same exposure and view transform as the scene.
|
|
2948
|
+
// MIPPED, and the chain is the prefilter a reflection reads: level n is a
|
|
2949
|
+
// box average of the sky over twice the solid angle of n-1, which is what
|
|
2950
|
+
// lets rzWorldSpecular answer a rough surface without a blur pass per
|
|
2951
|
+
// level. Built on the CPU because the source is already float data in
|
|
2952
|
+
// memory and the alternative is a render pipeline per mip.
|
|
2953
|
+
const levels = Math.floor(Math.log2(Math.max(source.width, source.height))) + 1;
|
|
2900
2954
|
const tex = this.device.createTexture({
|
|
2901
2955
|
label: "world equirect (HDR)",
|
|
2902
2956
|
size: [source.width, source.height],
|
|
2903
2957
|
format: "rgba16float",
|
|
2958
|
+
mipLevelCount: levels,
|
|
2904
2959
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
2905
2960
|
});
|
|
2906
|
-
|
|
2961
|
+
let data = source.data;
|
|
2962
|
+
let w = source.width;
|
|
2963
|
+
let h = source.height;
|
|
2964
|
+
for (let level = 0; level < levels; level++) {
|
|
2965
|
+
if (level > 0) {
|
|
2966
|
+
const nw = Math.max(1, w >> 1);
|
|
2967
|
+
const nh = Math.max(1, h >> 1);
|
|
2968
|
+
const down = new Float32Array(nw * nh * 4);
|
|
2969
|
+
for (let y = 0; y < nh; y++) {
|
|
2970
|
+
for (let x = 0; x < nw; x++) {
|
|
2971
|
+
const x0 = Math.min(x * 2, w - 1);
|
|
2972
|
+
const x1 = Math.min(x * 2 + 1, w - 1);
|
|
2973
|
+
const y0 = Math.min(y * 2, h - 1);
|
|
2974
|
+
const y1 = Math.min(y * 2 + 1, h - 1);
|
|
2975
|
+
for (let c = 0; c < 4; c++) {
|
|
2976
|
+
down[(y * nw + x) * 4 + c] =
|
|
2977
|
+
(data[(y0 * w + x0) * 4 + c] +
|
|
2978
|
+
data[(y0 * w + x1) * 4 + c] +
|
|
2979
|
+
data[(y1 * w + x0) * 4 + c] +
|
|
2980
|
+
data[(y1 * w + x1) * 4 + c]) *
|
|
2981
|
+
0.25;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
data = down;
|
|
2986
|
+
w = nw;
|
|
2987
|
+
h = nh;
|
|
2988
|
+
}
|
|
2989
|
+
this.device.queue.writeTexture({ texture: tex, mipLevel: level }, packHalf(data), { bytesPerRow: w * 8, rowsPerImage: h }, [w, h]);
|
|
2990
|
+
}
|
|
2907
2991
|
this.worldEquirectTexture = tex;
|
|
2908
2992
|
this.worldEquirectView = tex.createView();
|
|
2909
2993
|
// The sky lights the scene, not only backs it. The sun keeps the toon
|
|
@@ -2917,6 +3001,12 @@ export class Engine {
|
|
|
2917
3001
|
}
|
|
2918
3002
|
if (this.worldSH || hadSH)
|
|
2919
3003
|
this.writeWorld();
|
|
3004
|
+
// The MATERIAL groups too, not only the composite's: a surface that
|
|
3005
|
+
// reflects the sky samples this very texture, so a world swapped after
|
|
3006
|
+
// init would otherwise keep reflecting the one it replaced. Marked as well,
|
|
3007
|
+
// because either rebuild may bail on resources that are not up yet.
|
|
3008
|
+
this.worldBindingsDirty = true;
|
|
3009
|
+
this.rebuildPerFrameBindGroups();
|
|
2920
3010
|
this.rebuildCompositeBindGroup();
|
|
2921
3011
|
if (this.device && this.compositeUniformBuffer)
|
|
2922
3012
|
this.writeCompositeViewUniforms();
|
|
@@ -3890,6 +3980,13 @@ export class Engine {
|
|
|
3890
3980
|
* uniform, so nothing recompiles.
|
|
3891
3981
|
*/
|
|
3892
3982
|
allocateLightSlots() {
|
|
3983
|
+
// BEFORE INIT IS A REAL CALLER. A host sets a scene's lamps from an effect
|
|
3984
|
+
// that runs as soon as its state exists, which on a hot reload is before
|
|
3985
|
+
// the engine has a device — and every public setter here has to survive
|
|
3986
|
+
// that, the way setWorldEquirect does. The buffers are written from the
|
|
3987
|
+
// engine's own state at init, so nothing is lost by returning.
|
|
3988
|
+
if (!this.device)
|
|
3989
|
+
return;
|
|
3893
3990
|
let next = this.docLightCount;
|
|
3894
3991
|
for (const e of this.effects) {
|
|
3895
3992
|
if (!e.lights)
|
|
@@ -5423,8 +5520,11 @@ export class Engine {
|
|
|
5423
5520
|
// The positional lights. Always bound, empty or not, so every material
|
|
5424
5521
|
// pipeline shares one layout whether or not the scene has any.
|
|
5425
5522
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
5426
|
-
// The far cascade's shadow map
|
|
5523
|
+
// The far cascade's shadow map, then the world's sky (8) and the BRDF
|
|
5524
|
+
// LUT (9). The sky is always bound — the 1x1 fallback when the scene
|
|
5525
|
+
// has none — so every material pipeline keeps sharing one layout.
|
|
5427
5526
|
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
5527
|
+
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5428
5528
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
5429
5529
|
],
|
|
5430
5530
|
});
|
|
@@ -5658,6 +5758,18 @@ export class Engine {
|
|
|
5658
5758
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
5659
5759
|
});
|
|
5660
5760
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightsData);
|
|
5761
|
+
// BEFORE the per-frame groups, which bind it. The composite needs this 1x1
|
|
5762
|
+
// stand-in too, and it used to be created with the composite's own
|
|
5763
|
+
// resources further down — far enough down that a material group built up
|
|
5764
|
+
// here had nothing to put on its sky binding, and every draw went out with
|
|
5765
|
+
// no bind group at index 0.
|
|
5766
|
+
this.fallbackEquirectTexture = this.device.createTexture({
|
|
5767
|
+
label: "equirect fallback",
|
|
5768
|
+
size: [1, 1],
|
|
5769
|
+
format: "rgba8unorm",
|
|
5770
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
5771
|
+
});
|
|
5772
|
+
this.fallbackEquirectView = this.fallbackEquirectTexture.createView();
|
|
5661
5773
|
// Zero-filled = zero lines, which every accessor answers gracefully.
|
|
5662
5774
|
this.lyricsBuffer = this.device.createBuffer({
|
|
5663
5775
|
label: "lyrics",
|
|
@@ -5674,39 +5786,7 @@ export class Engine {
|
|
|
5674
5786
|
});
|
|
5675
5787
|
this.lyricsTextureView = this.lyricsTexture.createView();
|
|
5676
5788
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
5677
|
-
this.
|
|
5678
|
-
label: "main per-frame bind group",
|
|
5679
|
-
layout: this.mainPerFrameBindGroupLayout,
|
|
5680
|
-
entries: [
|
|
5681
|
-
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
5682
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
5683
|
-
{ binding: 2, resource: this.materialSampler },
|
|
5684
|
-
{ binding: 3, resource: this.shadowMapDepthViews[0] },
|
|
5685
|
-
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
5686
|
-
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
5687
|
-
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
5688
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
5689
|
-
{ binding: 9, resource: this.brdfLutView },
|
|
5690
|
-
],
|
|
5691
|
-
});
|
|
5692
|
-
// The mirror's, identical but for the camera — same lights, same shadow
|
|
5693
|
-
// maps, because a reflection is the same scene lit the same way, seen from
|
|
5694
|
-
// a reflected eye.
|
|
5695
|
-
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
5696
|
-
label: "mirror per-frame bind group",
|
|
5697
|
-
layout: this.mainPerFrameBindGroupLayout,
|
|
5698
|
-
entries: [
|
|
5699
|
-
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
5700
|
-
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
5701
|
-
{ binding: 2, resource: this.materialSampler },
|
|
5702
|
-
{ binding: 3, resource: this.shadowMapDepthViews[0] },
|
|
5703
|
-
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
5704
|
-
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
5705
|
-
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
5706
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
5707
|
-
{ binding: 9, resource: this.brdfLutView },
|
|
5708
|
-
],
|
|
5709
|
-
});
|
|
5789
|
+
this.rebuildPerFrameBindGroups();
|
|
5710
5790
|
this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
5711
5791
|
label: "ground shadow layout",
|
|
5712
5792
|
entries: [
|
|
@@ -6091,13 +6171,6 @@ export class Engine {
|
|
|
6091
6171
|
{ binding: 21, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
6092
6172
|
],
|
|
6093
6173
|
});
|
|
6094
|
-
this.fallbackEquirectTexture = this.device.createTexture({
|
|
6095
|
-
label: "equirect fallback",
|
|
6096
|
-
size: [1, 1],
|
|
6097
|
-
format: "rgba8unorm",
|
|
6098
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
6099
|
-
});
|
|
6100
|
-
this.fallbackEquirectView = this.fallbackEquirectTexture.createView();
|
|
6101
6174
|
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
6102
6175
|
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
6103
6176
|
});
|
|
@@ -7756,6 +7829,67 @@ export class Engine {
|
|
|
7756
7829
|
this.writeWorld();
|
|
7757
7830
|
this.writeSun(0);
|
|
7758
7831
|
}
|
|
7832
|
+
/** Returns whether it actually rebuilt — see rebuildCompositeBindGroup. */
|
|
7833
|
+
rebuildPerFrameBindGroups() {
|
|
7834
|
+
// EVERY resource, not just the layout. This runs twice: once at init, where
|
|
7835
|
+
// the order is known, and again whenever a world is installed — which the
|
|
7836
|
+
// host may do before init has finished, and a bind group built around an
|
|
7837
|
+
// undefined buffer is a validation error at creation rather than a missing
|
|
7838
|
+
// picture later.
|
|
7839
|
+
if (!this.device ||
|
|
7840
|
+
!this.mainPerFrameBindGroupLayout ||
|
|
7841
|
+
!this.shadowMapDepthViews?.length ||
|
|
7842
|
+
!this.cameraUniformBuffer ||
|
|
7843
|
+
!this.mirrorCameraBuffer ||
|
|
7844
|
+
!this.lightUniformBuffer ||
|
|
7845
|
+
!this.lightsBuffer ||
|
|
7846
|
+
!this.shadowLightVPBuffer ||
|
|
7847
|
+
!this.materialSampler ||
|
|
7848
|
+
!this.shadowComparisonSampler ||
|
|
7849
|
+
!this.brdfLutView ||
|
|
7850
|
+
!(this.worldEquirectView ?? this.fallbackEquirectView)) {
|
|
7851
|
+
return false;
|
|
7852
|
+
}
|
|
7853
|
+
// BOTH ENTRY LISTS ARE WRITTEN OUT, rather than shared through a helper.
|
|
7854
|
+
// tests/bindings.test.mjs reads this file and checks statically that every
|
|
7855
|
+
// bind group covers exactly its layout — it cannot see through a function,
|
|
7856
|
+
// and a missing binding is a validation error at draw time rather than a
|
|
7857
|
+
// compile error here. The duplication is the price of that check.
|
|
7858
|
+
const env = this.worldEquirectView ?? this.fallbackEquirectView;
|
|
7859
|
+
this.perFrameBindGroup = this.device.createBindGroup({
|
|
7860
|
+
label: "main per-frame bind group",
|
|
7861
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
7862
|
+
entries: [
|
|
7863
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
7864
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
7865
|
+
{ binding: 2, resource: this.materialSampler },
|
|
7866
|
+
{ binding: 3, resource: this.shadowMapDepthViews[0] },
|
|
7867
|
+
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
7868
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
7869
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
7870
|
+
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
7871
|
+
{ binding: 8, resource: env },
|
|
7872
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
7873
|
+
],
|
|
7874
|
+
});
|
|
7875
|
+
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
7876
|
+
label: "mirror per-frame bind group",
|
|
7877
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
7878
|
+
entries: [
|
|
7879
|
+
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
7880
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
7881
|
+
{ binding: 2, resource: this.materialSampler },
|
|
7882
|
+
{ binding: 3, resource: this.shadowMapDepthViews[0] },
|
|
7883
|
+
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
7884
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
7885
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
7886
|
+
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
7887
|
+
{ binding: 8, resource: env },
|
|
7888
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
7889
|
+
],
|
|
7890
|
+
});
|
|
7891
|
+
return true;
|
|
7892
|
+
}
|
|
7759
7893
|
/**
|
|
7760
7894
|
* Write world ambient. For a uniform-radiance world, hemispherical irradiance
|
|
7761
7895
|
* is E = π·L and a Lambertian BRDF reflects (albedo/π)·E = albedo·L, so the
|
|
@@ -7766,17 +7900,22 @@ export class Engine {
|
|
|
7766
7900
|
this.lightData[0] = this.world.color.x * s;
|
|
7767
7901
|
this.lightData[1] = this.world.color.y * s;
|
|
7768
7902
|
this.lightData[2] = this.world.color.z * s;
|
|
7769
|
-
|
|
7903
|
+
// The spare in the ambient vec4, which rzWorldSpecular multiplies its sky
|
|
7904
|
+
// sample by: the SH carries strength already, the texture cannot.
|
|
7905
|
+
this.lightData[3] = this.world.strength;
|
|
7770
7906
|
// The sky's irradiance, when an HDRI world is installed — the world
|
|
7771
7907
|
// STRENGTH dial keeps its meaning by scaling it, and the world COLOUR is
|
|
7772
7908
|
// simply unread while the flag is up (Blender's own semantics: the image
|
|
7773
7909
|
// replaces the colour, strength applies to either).
|
|
7910
|
+
// An HDRI outranks a gradient outranks the flat colour, which is the order
|
|
7911
|
+
// of how much each one knows about the sky.
|
|
7912
|
+
const sh = this.worldSH ?? this.worldGradientSH;
|
|
7774
7913
|
for (let i = 0; i < 9; i++) {
|
|
7775
7914
|
const b = 36 + i * 4;
|
|
7776
|
-
if (
|
|
7777
|
-
this.lightData[b] =
|
|
7778
|
-
this.lightData[b + 1] =
|
|
7779
|
-
this.lightData[b + 2] =
|
|
7915
|
+
if (sh) {
|
|
7916
|
+
this.lightData[b] = sh[i * 3] * s;
|
|
7917
|
+
this.lightData[b + 1] = sh[i * 3 + 1] * s;
|
|
7918
|
+
this.lightData[b + 2] = sh[i * 3 + 2] * s;
|
|
7780
7919
|
}
|
|
7781
7920
|
else {
|
|
7782
7921
|
this.lightData[b] = 0;
|
|
@@ -7785,7 +7924,11 @@ export class Engine {
|
|
|
7785
7924
|
}
|
|
7786
7925
|
this.lightData[b + 3] = 0;
|
|
7787
7926
|
}
|
|
7788
|
-
|
|
7927
|
+
// WHICH KIND of sky, not merely whether there is one: 2 is a picture a
|
|
7928
|
+
// reflection can be sampled from, 1 is a gradient it can only be fitted to,
|
|
7929
|
+
// 0 is a flat colour. rzWorldSpecular reads this to decide whether to touch
|
|
7930
|
+
// the equirect texture at all.
|
|
7931
|
+
this.lightData[39] = this.worldSH ? 2 : this.worldGradientSH ? 1 : 0;
|
|
7789
7932
|
this.updateLightBuffer();
|
|
7790
7933
|
}
|
|
7791
7934
|
/** Write sun lamp into light slot `index` (0..3). Layout mirrors the WGSL struct. */
|
|
@@ -7812,6 +7955,9 @@ export class Engine {
|
|
|
7812
7955
|
this.world.color = options.color;
|
|
7813
7956
|
if (options.strength !== undefined)
|
|
7814
7957
|
this.world.strength = options.strength;
|
|
7958
|
+
if (options.gradient !== undefined) {
|
|
7959
|
+
this.worldGradientSH = options.gradient ? gradientIrradianceSH(options.gradient) : null;
|
|
7960
|
+
}
|
|
7815
7961
|
this.writeWorld();
|
|
7816
7962
|
}
|
|
7817
7963
|
/** Update the sun lamp (Blender: Light > Sun). Direction change marks shadow VP dirty. */
|
|
@@ -7866,6 +8012,7 @@ export class Engine {
|
|
|
7866
8012
|
this.hasGround = true;
|
|
7867
8013
|
this.groundDrawCall = {
|
|
7868
8014
|
type: "ground",
|
|
8015
|
+
baseType: "ground",
|
|
7869
8016
|
count: 6,
|
|
7870
8017
|
firstIndex: 0,
|
|
7871
8018
|
bindGroup: this.groundShadowBindGroup,
|
|
@@ -7953,7 +8100,9 @@ export class Engine {
|
|
|
7953
8100
|
this.lightsData[b + 14] = 0;
|
|
7954
8101
|
this.lightsData[b + 15] = 0;
|
|
7955
8102
|
}
|
|
7956
|
-
|
|
8103
|
+
// The CPU copy is written either way; only the upload needs a device. A
|
|
8104
|
+
// scene whose lamps arrive before init keeps them, and init uploads them.
|
|
8105
|
+
if (list.length && this.device && this.lightsBuffer) {
|
|
7957
8106
|
this.device.queue.writeBuffer(this.lightsBuffer, LIGHT_HEADER * 4, this.lightsData.buffer, LIGHT_HEADER * 4, list.length * LIGHT_STRIDE * 4);
|
|
7958
8107
|
}
|
|
7959
8108
|
// The effects' bases move when the document's count does; the header —
|
|
@@ -9835,6 +9984,35 @@ export class Engine {
|
|
|
9835
9984
|
this.cullReadback?.shadow.destroy();
|
|
9836
9985
|
this.cullReadback = null;
|
|
9837
9986
|
}
|
|
9987
|
+
/**
|
|
9988
|
+
* Tell the camera how big the scene is, so its far plane can cover it.
|
|
9989
|
+
*
|
|
9990
|
+
* MEASURED ON LOAD, from the model's own vertices, and only ever raised. A
|
|
9991
|
+
* stage is the case that matters: framed from ten units away, a sky dome a
|
|
9992
|
+
* thousand units out sits well beyond a far plane derived from the orbit, and
|
|
9993
|
+
* the part of it past that plane is simply not drawn — which looks like a
|
|
9994
|
+
* polygon of background colour that follows the camera, not like clipping.
|
|
9995
|
+
*
|
|
9996
|
+
* Never lowered when a model leaves: the cost of a far plane that is too
|
|
9997
|
+
* generous is depth precision, and the cost of one that is too near is
|
|
9998
|
+
* geometry that vanishes. One of those is a bug report.
|
|
9999
|
+
*/
|
|
10000
|
+
noteSceneExtent(model) {
|
|
10001
|
+
const { positions } = model.getGeometry();
|
|
10002
|
+
let most = 0;
|
|
10003
|
+
// Every 16th vertex: an extent is a bound, not a measurement, and a stage
|
|
10004
|
+
// has a quarter of a million of them.
|
|
10005
|
+
for (let i = 0; i + 2 < positions.length; i += 48) {
|
|
10006
|
+
const d = positions[i] * positions[i] + positions[i + 1] * positions[i + 1] + positions[i + 2] * positions[i + 2];
|
|
10007
|
+
if (d > most)
|
|
10008
|
+
most = d;
|
|
10009
|
+
}
|
|
10010
|
+
const extent = Math.sqrt(most);
|
|
10011
|
+
if (extent <= this.sceneExtent)
|
|
10012
|
+
return;
|
|
10013
|
+
this.sceneExtent = extent;
|
|
10014
|
+
this.camera?.setSceneExtent(extent);
|
|
10015
|
+
}
|
|
9838
10016
|
/**
|
|
9839
10017
|
* One record per model: the rigid transform its per-material boxes live under,
|
|
9840
10018
|
* or the world sphere that bounds it in any pose. Written every frame, because
|
|
@@ -10500,6 +10678,7 @@ export class Engine {
|
|
|
10500
10678
|
const vertices = model.getVertices();
|
|
10501
10679
|
const skinning = model.getSkinning();
|
|
10502
10680
|
const skeleton = model.getSkeleton();
|
|
10681
|
+
this.noteSceneExtent(model);
|
|
10503
10682
|
const boneCount = skeleton.bones.length;
|
|
10504
10683
|
const matrixSize = boneCount * 16 * 4;
|
|
10505
10684
|
const vertexBuffer = this.device.createBuffer({
|
|
@@ -11176,6 +11355,7 @@ export class Engine {
|
|
|
11176
11355
|
const type = inst.isPlane ? "opaque" : isTransparent ? "transparent" : "opaque";
|
|
11177
11356
|
inst.drawCalls.push({
|
|
11178
11357
|
type,
|
|
11358
|
+
baseType: type,
|
|
11179
11359
|
count: indexCount,
|
|
11180
11360
|
firstIndex: currentIndexOffset,
|
|
11181
11361
|
bindGroup,
|
|
@@ -12353,6 +12533,17 @@ export class Engine {
|
|
|
12353
12533
|
this.renderWithDelta(deltaSeconds);
|
|
12354
12534
|
}
|
|
12355
12535
|
renderWithDelta(deltaTime) {
|
|
12536
|
+
// The bind groups first, then the textures they used to name. A retired
|
|
12537
|
+
// texture is freed only once a frame has been rebuilt without it — freeing
|
|
12538
|
+
// it while a group still points at it is what "destroyed texture used in a
|
|
12539
|
+
// submit" means, and it repeats every frame because the stale group stays.
|
|
12540
|
+
if (this.worldBindingsDirty) {
|
|
12541
|
+
// Both have to take: either one bailing leaves a group naming the sky
|
|
12542
|
+
// that was replaced, which is a frame drawn against the wrong one.
|
|
12543
|
+
const perFrame = this.rebuildPerFrameBindGroups();
|
|
12544
|
+
const composite = this.rebuildCompositeBindGroup();
|
|
12545
|
+
this.worldBindingsDirty = !(perFrame && composite);
|
|
12546
|
+
}
|
|
12356
12547
|
// The scene clock, and the only clock a trail may sample on: renderFrame()
|
|
12357
12548
|
// drives offline export with an exact per-frame delta, so a path recorded
|
|
12358
12549
|
// against this is reproducible where one recorded against wall time is not.
|
|
@@ -13274,6 +13465,19 @@ export class Engine {
|
|
|
13274
13465
|
const groupId = install ? wantId : null;
|
|
13275
13466
|
if (groupId)
|
|
13276
13467
|
inst.materialToGroup.set(dc.materialName, groupId);
|
|
13468
|
+
// A HASHED GROUP DRAWS IN THE OPAQUE PHASE. Alpha-to-coverage is the
|
|
13469
|
+
// transparency technique built for that phase — it is why a card already
|
|
13470
|
+
// takes this route — and it is what a cutout wants: crisp edges, depth
|
|
13471
|
+
// written, correct occlusion between layers.
|
|
13472
|
+
//
|
|
13473
|
+
// Foliage is the case that needs it. A leaf card's antialiased edges put
|
|
13474
|
+
// its translucentFrac over the 2% bar, so the whole shrub lands in the
|
|
13475
|
+
// alpha-blend bucket and is drawn in author order: soft haloed leaves
|
|
13476
|
+
// that do not occlude each other, and haze wherever cards overlap. The
|
|
13477
|
+
// game it came from alpha-tests exactly these materials.
|
|
13478
|
+
const type = install?.alphaMode === "hashed" ? "opaque" : dc.baseType;
|
|
13479
|
+
if (dc.type !== type)
|
|
13480
|
+
dc.type = type;
|
|
13277
13481
|
if (dc.groupId === groupId)
|
|
13278
13482
|
continue;
|
|
13279
13483
|
dc.groupId = groupId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAYD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAoBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AAyCD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/graph/registry.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzD,KAAK,SAAS,GAAG;IACf,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC9B,2FAA2F;IAC3F,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAA;IAC/C,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAYD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,CAoBhE;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAIpE;AAMD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAM1D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKpE;AAyCD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA8pBlD,CAAA"}
|
package/dist/graph/registry.js
CHANGED
|
@@ -601,6 +601,26 @@ export const NODE_REGISTRY = {
|
|
|
601
601
|
outputs: { value: "float" },
|
|
602
602
|
emit: (a) => `layer_weight_facing(${a.blend}, n, v)`,
|
|
603
603
|
},
|
|
604
|
+
/**
|
|
605
|
+
* The world's radiance from a direction — Blender's Environment Texture, read
|
|
606
|
+
* off the scene's own sky rather than off an image a material carries.
|
|
607
|
+
*
|
|
608
|
+
* ITS DEFAULT VECTOR IS THE REFLECTION, which is the whole point: wire
|
|
609
|
+
* nothing and it answers what this surface reflects. The roughness input
|
|
610
|
+
* picks how blurred the answer is; at 0 it is a mirror.
|
|
611
|
+
*
|
|
612
|
+
* A scene with no HDRI has no sky to reflect, and this returns the flat world
|
|
613
|
+
* colour — the same value the ambient already carries, so such a scene looks
|
|
614
|
+
* exactly as it did.
|
|
615
|
+
*/
|
|
616
|
+
environment: {
|
|
617
|
+
inputs: {
|
|
618
|
+
vector: { type: "vector", contextDefault: "reflect(-v, n)" },
|
|
619
|
+
roughness: F(0),
|
|
620
|
+
},
|
|
621
|
+
outputs: { color: "color" },
|
|
622
|
+
emit: (a) => `rzWorldSpecular(${a.vector}, ${a.roughness})`,
|
|
623
|
+
},
|
|
604
624
|
// ── Lighting capture ──
|
|
605
625
|
/**
|
|
606
626
|
* Shader → RGB on a white diffuse closure, reduced to a scalar.
|
package/dist/ibl.d.ts
CHANGED
|
@@ -16,4 +16,32 @@ export declare function evalIrradianceSH(sh: Float32Array, n: {
|
|
|
16
16
|
y: number;
|
|
17
17
|
z: number;
|
|
18
18
|
}): [number, number, number];
|
|
19
|
+
/**
|
|
20
|
+
* A three-colour sky as irradiance SH — the same nine coefficients an HDRI
|
|
21
|
+
* gives, so nothing downstream has to know which kind of world it is looking at.
|
|
22
|
+
*
|
|
23
|
+
* The gradient is Unity's trilight and every other engine's "ambient gradient":
|
|
24
|
+
* one colour straight up, one at the horizon, one straight down, interpolated
|
|
25
|
+
* by the direction's height. It is what lights a game stage that has no baked
|
|
26
|
+
* lighting, and it is not a decoration — the fill on a wall facing the sky and
|
|
27
|
+
* the fill under a roof differ by the whole span between sky and ground.
|
|
28
|
+
*
|
|
29
|
+
* Built by SAMPLING IT INTO A SMALL EQUIRECT and projecting that, rather than by
|
|
30
|
+
* integrating the basis functions in closed form. The analytic route is a page
|
|
31
|
+
* of coefficients that has to be rederived if the interpolation ever changes;
|
|
32
|
+
* this is six lines that reuse the projector the HDRI path is already trusted
|
|
33
|
+
* to get right, and at 64x32 the fit is exact to well under a percent because
|
|
34
|
+
* the function has no detail finer than the harmonics can hold anyway.
|
|
35
|
+
*/
|
|
36
|
+
type Rgb = {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
z: number;
|
|
40
|
+
};
|
|
41
|
+
export declare function gradientIrradianceSH(g: {
|
|
42
|
+
sky: Rgb;
|
|
43
|
+
equator: Rgb;
|
|
44
|
+
ground: Rgb;
|
|
45
|
+
}): Float32Array;
|
|
46
|
+
export {};
|
|
19
47
|
//# sourceMappingURL=ibl.d.ts.map
|
package/dist/ibl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ibl.d.ts","sourceRoot":"","sources":["../src/ibl.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,SAAI,GAAG,YAAY,CA0E3E;AAED;qEACqE;AACrE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAQnH"}
|
|
1
|
+
{"version":3,"file":"ibl.d.ts","sourceRoot":"","sources":["../src/ibl.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,SAAI,GAAG,YAAY,CA0E3E;AAED;qEACqE;AACrE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAQnH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,KAAK,GAAG,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAAG,YAAY,CAoB7F"}
|
package/dist/ibl.js
CHANGED
|
@@ -111,3 +111,24 @@ export function evalIrradianceSH(sh, n) {
|
|
|
111
111
|
}
|
|
112
112
|
return out;
|
|
113
113
|
}
|
|
114
|
+
export function gradientIrradianceSH(g) {
|
|
115
|
+
const w = 64;
|
|
116
|
+
const h = 32;
|
|
117
|
+
const data = new Float32Array(w * h * 4);
|
|
118
|
+
for (let y = 0; y < h; y++) {
|
|
119
|
+
// The same parameterisation projectIrradianceSH reads: v runs top to bottom
|
|
120
|
+
// and the direction's height is cos(pi v).
|
|
121
|
+
const ny = Math.cos(Math.PI * ((y + 0.5) / h));
|
|
122
|
+
const t = Math.abs(ny);
|
|
123
|
+
const a = ny >= 0 ? g.sky : g.ground;
|
|
124
|
+
const c = [g.equator.x + (a.x - g.equator.x) * t, g.equator.y + (a.y - g.equator.y) * t, g.equator.z + (a.z - g.equator.z) * t];
|
|
125
|
+
for (let x = 0; x < w; x++) {
|
|
126
|
+
const i = (y * w + x) * 4;
|
|
127
|
+
data[i] = c[0];
|
|
128
|
+
data[i + 1] = c[1];
|
|
129
|
+
data[i + 2] = c[2];
|
|
130
|
+
data[i + 3] = 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return projectIrradianceSH({ width: w, height: h, data }, 1);
|
|
134
|
+
}
|
package/dist/shaders/lights.d.ts
CHANGED
|
@@ -4,13 +4,23 @@ export declare const LIGHT_HEADER = 4;
|
|
|
4
4
|
/** Floats per light — see the layout above. */
|
|
5
5
|
export declare const LIGHT_STRIDE = 16;
|
|
6
6
|
/**
|
|
7
|
-
* The cap. The loop below runs per fragment over the lights a scene
|
|
8
|
-
* this bounds the worst case rather than the ordinary one
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The cap. The loop below runs per fragment over the lights a scene HAS, so
|
|
8
|
+
* this bounds the worst case rather than the ordinary one, and the buffer is
|
|
9
|
+
* storage rather than uniform — 128 records is 8 KiB, which costs nothing to
|
|
10
|
+
* declare and is paid for only by the scenes that fill it.
|
|
11
|
+
*
|
|
12
|
+
* Sized against the work, not against a round number: a Unity stage arrives
|
|
13
|
+
* with a lamp per fixture. One rip of a resort brought thirty-three, a lit
|
|
14
|
+
* interior brings a hundred or more, and the ceilings of sixteen and then
|
|
15
|
+
* forty-eight each cut a real scene in half. Past this the extras are dropped.
|
|
16
|
+
*
|
|
17
|
+
* What it does NOT buy: a scene that genuinely lights a fragment from a hundred
|
|
18
|
+
* lamps pays for a hundred iterations of the loop. The distance test below
|
|
19
|
+
* rejects most of them in a few instructions, which is what makes a rig of this
|
|
20
|
+
* size affordable at all; clustering is what replaces the linear walk when a
|
|
21
|
+
* scene wants every one of them close enough to matter.
|
|
12
22
|
*/
|
|
13
|
-
export declare const MAX_LIGHTS =
|
|
23
|
+
export declare const MAX_LIGHTS = 128;
|
|
14
24
|
/** Floats in the whole buffer. */
|
|
15
25
|
export declare const LIGHTS_FLOATS: number;
|
|
16
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAwCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B
|
|
1
|
+
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../src/shaders/lights.ts"],"names":[],"mappings":"AAwCA;yEACyE;AACzE,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,+CAA+C;AAC/C,eAAO,MAAM,YAAY,KAAK,CAAA;AAC9B;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,MAAM,CAAA;AAC7B,kCAAkC;AAClC,eAAO,MAAM,aAAa,QAA2C,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,qJAQhC,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,idAa9B,CAAA;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,EAC5B,UAAU,SAAK,GACd,MAAM,CAiGR;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0FhE"}
|