reze-engine 0.58.1 → 0.58.3
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/effect-points.d.ts +30 -0
- package/dist/effect-points.d.ts.map +1 -0
- package/dist/effect-points.js +49 -0
- package/dist/engine.d.ts +25 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +199 -35
- package/dist/graph/style-group.d.ts +7 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/light-grid.d.ts +25 -0
- package/dist/light-grid.d.ts.map +1 -0
- package/dist/light-grid.js +91 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +6 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -6
- package/dist/shaders/cast-api.d.ts +1 -1
- package/dist/shaders/cast-layout.d.ts +5 -3
- package/dist/shaders/cast-layout.d.ts.map +1 -1
- package/dist/shaders/cast-layout.js +5 -3
- package/dist/shaders/directives.d.ts +3 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/lights.d.ts +14 -9
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +118 -65
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +2 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -1
- package/dist/shaders/passes/grid.js +2 -0
- package/dist/shaders/passes/particles.d.ts +20 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +36 -1
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +2 -1
- package/dist/shaders/points-api.d.ts +11 -0
- package/dist/shaders/points-api.d.ts.map +1 -0
- package/dist/shaders/points-api.js +58 -0
- package/package.json +1 -1
- package/src/effect-points.ts +67 -0
- package/src/engine.ts +215 -35
- package/src/graph/style-group.ts +12 -1
- package/src/light-grid.ts +139 -0
- package/src/model.ts +12 -0
- package/src/pmx-loader.ts +13 -6
- package/src/shaders/cast-layout.ts +5 -3
- package/src/shaders/directives.ts +8 -0
- package/src/shaders/lights.ts +118 -65
- package/src/shaders/passes/composite.ts +2 -0
- package/src/shaders/passes/grid.ts +2 -0
- package/src/shaders/passes/particles.ts +46 -2
- package/src/shaders/passes/trails.ts +2 -1
- package/src/shaders/points-api.ts +66 -0
package/dist/engine.js
CHANGED
|
@@ -29,7 +29,8 @@ import { packHalf } from "./hdr";
|
|
|
29
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
|
-
import { LIGHT_HEADER, LIGHT_STRIDE, LIGHTS_FLOATS, MAX_LIGHTS, buildLightEmitShader, hasLightEmit, } from "./shaders/lights";
|
|
32
|
+
import { LIGHT_GRID_BASE, LIGHT_HEADER, LIGHT_STRIDE, LIGHTS_FLOATS, MAX_LIGHTS, buildLightEmitShader, hasLightEmit, } from "./shaders/lights";
|
|
33
|
+
import { buildLightGrid } from "./light-grid";
|
|
33
34
|
import { groundShaderWgsl, GROUND_NOISE_BAKE_WGSL, GROUND_NOISE_SIZE } from "./shaders/passes/ground";
|
|
34
35
|
import { outlineShaderWgsl, RZ_OUTLINE_DISSOLVE_OFFSET } from "./shaders/passes/outline";
|
|
35
36
|
import { transparentDepthPrepassWgsl } from "./shaders/passes/depth-prepass";
|
|
@@ -41,7 +42,9 @@ import { boneOverlay, boneMarkerPositions, buildOverlayShapes, jointOverlay, lin
|
|
|
41
42
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
42
43
|
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut";
|
|
43
44
|
import { buildCompositeShader, EFFECT_SCENE_API, buildFieldShader, EFFECT_ANCHORS, EFFECT_SUBJECTS, EFFECT_TRAIL_BASE, EFFECT_TRAIL_SAMPLES, } from "./shaders/passes/composite";
|
|
44
|
-
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, particleEntryPoints, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
45
|
+
import { buildParticleComputeShader, buildParticleRenderShader, PARTICLE_LIGHT_BINDING, PARTICLE_POINTS_BINDING, particleEntryPoints, PARTICLE_INDIRECT_BINDING, PARTICLE_INDIRECT_BYTES, PARTICLE_INDIRECT_DRAW_OFFSET, PARTICLE_STRIDE, } from "./shaders/passes/particles";
|
|
46
|
+
import { MAX_EFFECT_POINTS, POINTS_FLOATS } from "./shaders/points-api";
|
|
47
|
+
import { bonesWithPrefix, pointsData, writeBonePoint } from "./effect-points";
|
|
45
48
|
import { SIM_FORMAT, GRID_MAX, buildSimShader, gridEntryPoint, } from "./shaders/passes/grid";
|
|
46
49
|
import { buildCastResolveShader, buildCastSeedShader, buildCastStepShader, castDistanceUsed, CAST_COVERAGE_FORMAT, CAST_DIST_FORMAT, CAST_FIELD_DIV, CAST_SEED_FORMAT, } from "./shaders/passes/cast-distance";
|
|
47
50
|
import { buildTrailShader, trailEntryPoints, TRAIL_SUBDIVISIONS } from "./shaders/passes/trails";
|
|
@@ -1077,8 +1080,17 @@ export class Engine {
|
|
|
1077
1080
|
* effect is on 今汐" into a bit the shaders can read. */
|
|
1078
1081
|
this.castSlotOf = new Map();
|
|
1079
1082
|
this.effects = [];
|
|
1080
|
-
/**
|
|
1083
|
+
/** Per model, per prefix, the bones `#points` matched. A rig's bones never
|
|
1084
|
+
* change after load, so the names are walked once rather than every frame. */
|
|
1085
|
+
this.pointBones = new WeakMap();
|
|
1086
|
+
/** The CPU copy, from construction rather than init: a host sets a scene's
|
|
1087
|
+
* lamps as soon as its state exists, which can be before the device. */
|
|
1088
|
+
this.lightsData = new Float32Array(LIGHTS_FLOATS);
|
|
1089
|
+
/** The same bytes as words — the grid's lamp bits are u32, not floats. */
|
|
1090
|
+
this.lightsWords = new Uint32Array(this.lightsData.buffer);
|
|
1091
|
+
/** Just the header: the counts and the grid's placement. */
|
|
1081
1092
|
this.lightHeader = new Float32Array(LIGHT_HEADER);
|
|
1093
|
+
this.lightHeaderWords = new Uint32Array(this.lightHeader.buffer);
|
|
1082
1094
|
/** How many of the slots belong to the DOCUMENT. Effects get what follows. */
|
|
1083
1095
|
this.docLightCount = 0;
|
|
1084
1096
|
/** Last frame's anchor world positions, for velocity. Keyed model id → slot. */
|
|
@@ -1151,7 +1163,9 @@ export class Engine {
|
|
|
1151
1163
|
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
1152
1164
|
// even for see-through cloth: the translucency lives in the texture).
|
|
1153
1165
|
this.textureAlphaCache = new Map();
|
|
1154
|
-
|
|
1166
|
+
/** One per format: a render pipeline is bound to its target's format, and
|
|
1167
|
+
* material textures are sRGB while a group's data maps are not. */
|
|
1168
|
+
this.mipBlitPipelines = new Map();
|
|
1155
1169
|
this.mipBlitSampler = null;
|
|
1156
1170
|
this._nextDefaultModelId = 0;
|
|
1157
1171
|
// IK and physics enabled at engine level (same for all models)
|
|
@@ -3044,6 +3058,8 @@ export class Engine {
|
|
|
3044
3058
|
e.particles.computeBinds = b.computeBinds;
|
|
3045
3059
|
e.particles.renderBinds = b.renderBinds;
|
|
3046
3060
|
e.particles.mirrorRenderBinds = b.mirrorRenderBinds;
|
|
3061
|
+
if (e.particles.live && b.countBinds)
|
|
3062
|
+
e.particles.live.binds = b.countBinds;
|
|
3047
3063
|
}
|
|
3048
3064
|
if (e.grid)
|
|
3049
3065
|
e.grid.binds = [this.gridBindGroup(e.grid, 0), this.gridBindGroup(e.grid, 1)];
|
|
@@ -3568,6 +3584,8 @@ export class Engine {
|
|
|
3568
3584
|
paramsBuffer?.destroy();
|
|
3569
3585
|
particles?.buffer.destroy();
|
|
3570
3586
|
particles?.uniform.destroy();
|
|
3587
|
+
particles?.points?.buffer.destroy();
|
|
3588
|
+
particles?.live?.indirect.destroy();
|
|
3571
3589
|
grid?.textures[0].destroy();
|
|
3572
3590
|
grid?.textures[1].destroy();
|
|
3573
3591
|
grid?.uniform.destroy();
|
|
@@ -3921,8 +3939,20 @@ export class Engine {
|
|
|
3921
3939
|
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
3922
3940
|
gridSize: grid?.size ?? 0,
|
|
3923
3941
|
cover: particleEntryPoints(wgsl).cover,
|
|
3942
|
+
live: particleEntryPoints(wgsl).count,
|
|
3924
3943
|
};
|
|
3925
3944
|
const prepass = src.blend === "cutout" && src.cover;
|
|
3945
|
+
const points = d.points
|
|
3946
|
+
? {
|
|
3947
|
+
prefix: d.points,
|
|
3948
|
+
buffer: this.device.createBuffer({
|
|
3949
|
+
label: `points "${d.points}"`,
|
|
3950
|
+
size: POINTS_FLOATS * 4,
|
|
3951
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
3952
|
+
}),
|
|
3953
|
+
data: pointsData(),
|
|
3954
|
+
}
|
|
3955
|
+
: null;
|
|
3926
3956
|
// Sparks want to spawn where a trail is, so the particle stages see the same
|
|
3927
3957
|
// cast buffer the trail draw reads.
|
|
3928
3958
|
const cast = {
|
|
@@ -3948,11 +3978,15 @@ export class Engine {
|
|
|
3948
3978
|
return diagnostics.length ? diagnostics : module;
|
|
3949
3979
|
};
|
|
3950
3980
|
const computeModule = await compile(buildParticleComputeShader(src, cast), "particle compute");
|
|
3951
|
-
if (Array.isArray(computeModule))
|
|
3981
|
+
if (Array.isArray(computeModule)) {
|
|
3982
|
+
points?.buffer.destroy();
|
|
3952
3983
|
return { ok: false, diagnostics: computeModule };
|
|
3984
|
+
}
|
|
3953
3985
|
const renderModule = await compile(buildParticleRenderShader(src, cast), "particle render");
|
|
3954
|
-
if (Array.isArray(renderModule))
|
|
3986
|
+
if (Array.isArray(renderModule)) {
|
|
3987
|
+
points?.buffer.destroy();
|
|
3955
3988
|
return { ok: false, diagnostics: renderModule };
|
|
3989
|
+
}
|
|
3956
3990
|
// COPY_DST so a scheduled effect can empty its pool when its window starts.
|
|
3957
3991
|
const buffer = this.device.createBuffer({
|
|
3958
3992
|
label: "particle pool",
|
|
@@ -3969,13 +4003,23 @@ export class Engine {
|
|
|
3969
4003
|
});
|
|
3970
4004
|
const uniformBytes = new ArrayBuffer(32);
|
|
3971
4005
|
const uniformView = { floats: new Float32Array(uniformBytes), uints: new Uint32Array(uniformBytes) };
|
|
4006
|
+
// The live count's arguments — written by rzCount, read by the step's
|
|
4007
|
+
// dispatch and the quads' draws. Zero until the first step, so a pool
|
|
4008
|
+
// drawn before it has stepped draws nothing rather than everything.
|
|
4009
|
+
const indirect = src.live
|
|
4010
|
+
? this.device.createBuffer({
|
|
4011
|
+
label: "particle indirect",
|
|
4012
|
+
size: PARTICLE_INDIRECT_BYTES,
|
|
4013
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
|
|
4014
|
+
})
|
|
4015
|
+
: null;
|
|
3972
4016
|
// Visibility is per LAYOUT, not shared: a read_write storage buffer may not be
|
|
3973
4017
|
// visible to the vertex stage at all (WebGPU forbids it — a vertex shader
|
|
3974
4018
|
// that could write memory has no defined ordering against the rasteriser).
|
|
3975
4019
|
// Declaring one set of flags for both layouts is what made the pipeline
|
|
3976
4020
|
// layout invalid, and the error surfaces later and unhelpfully as "invalid
|
|
3977
4021
|
// due to a previous error".
|
|
3978
|
-
const layoutFor = (storage, visibility, shadow) => this.device.createBindGroupLayout({
|
|
4022
|
+
const layoutFor = (storage, visibility, shadow, withIndirect) => this.device.createBindGroupLayout({
|
|
3979
4023
|
entries: [
|
|
3980
4024
|
{ binding: 0, visibility, buffer: { type: storage } },
|
|
3981
4025
|
{ binding: 1, visibility, buffer: { type: "uniform" } },
|
|
@@ -3994,6 +4038,9 @@ export class Engine {
|
|
|
3994
4038
|
// author calls it or not. With no grid it is the 1x1 of zeroes.
|
|
3995
4039
|
{ binding: 8, visibility, texture: { sampleType: "float" } },
|
|
3996
4040
|
{ binding: 9, visibility, sampler: { type: "filtering" } },
|
|
4041
|
+
// The named points, ALWAYS, for the grid's reason: the accessor is
|
|
4042
|
+
// compiled in whether or not the effect declared #points.
|
|
4043
|
+
{ binding: PARTICLE_POINTS_BINDING, visibility, buffer: { type: "read-only-storage" } },
|
|
3997
4044
|
// The scene's light, for rzShadow and rzWorldAmbient — the shading
|
|
3998
4045
|
// stage only; the compute module compiles the stubs. See
|
|
3999
4046
|
// scene-light-api.ts.
|
|
@@ -4006,13 +4053,21 @@ export class Engine {
|
|
|
4006
4053
|
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
4007
4054
|
]
|
|
4008
4055
|
: []),
|
|
4056
|
+
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
4057
|
+
// step's layout must not carry them: a bound group's entries count
|
|
4058
|
+
// toward a pass's usage whether the shader reads them or not, and a
|
|
4059
|
+
// buffer may not be writable storage and indirect arguments in one
|
|
4060
|
+
// pass.
|
|
4061
|
+
...(withIndirect
|
|
4062
|
+
? [{ binding: PARTICLE_INDIRECT_BINDING, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }]
|
|
4063
|
+
: []),
|
|
4009
4064
|
],
|
|
4010
4065
|
});
|
|
4011
4066
|
/** One per GRID PARITY, for the reason rebuildFieldBindGroup gives: the grid
|
|
4012
4067
|
* alternates which texture is current, and rebuilding a single group every
|
|
4013
4068
|
* frame is waste for a change that only ever toggles between two known
|
|
4014
4069
|
* states. Both entries are the same view when there is no grid. */
|
|
4015
|
-
const bindFor = (layout, camera, shadow) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
4070
|
+
const bindFor = (layout, camera, shadow, withIndirect) => [0, 1].map((parity) => this.device.createBindGroup({
|
|
4016
4071
|
layout,
|
|
4017
4072
|
entries: [
|
|
4018
4073
|
{ binding: 0, resource: { buffer } },
|
|
@@ -4025,6 +4080,7 @@ export class Engine {
|
|
|
4025
4080
|
...(params.buffer ? [{ binding: 7, resource: { buffer: params.buffer } }] : []),
|
|
4026
4081
|
{ binding: 8, resource: grid ? grid.read[parity] : this.simFallbackView },
|
|
4027
4082
|
{ binding: 9, resource: this.simSampler },
|
|
4083
|
+
{ binding: PARTICLE_POINTS_BINDING, resource: { buffer: points?.buffer ?? this.pointsFallback } },
|
|
4028
4084
|
...(shadow
|
|
4029
4085
|
? [
|
|
4030
4086
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -4034,10 +4090,12 @@ export class Engine {
|
|
|
4034
4090
|
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
4035
4091
|
]
|
|
4036
4092
|
: []),
|
|
4093
|
+
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
4037
4094
|
],
|
|
4038
4095
|
}));
|
|
4039
|
-
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false);
|
|
4040
|
-
const
|
|
4096
|
+
const computeLayout = layoutFor("storage", GPUShaderStage.COMPUTE, false, false);
|
|
4097
|
+
const countLayout = indirect ? layoutFor("storage", GPUShaderStage.COMPUTE, false, true) : null;
|
|
4098
|
+
const renderLayout = layoutFor("read-only-storage", GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, true, false);
|
|
4041
4099
|
// Additive keeps the destination and adds to it, and leaves alpha alone, so
|
|
4042
4100
|
// a glow does not claim coverage it never occluded. The MASK sums with it —
|
|
4043
4101
|
// otherwise an additive effect could never reach the bloom gate. Both live
|
|
@@ -4050,11 +4108,23 @@ export class Engine {
|
|
|
4050
4108
|
const cutout = src.blend === "cutout";
|
|
4051
4109
|
this.device.pushErrorScope("validation");
|
|
4052
4110
|
try {
|
|
4111
|
+
const computePipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [computeLayout] });
|
|
4053
4112
|
const compute = await this.device.createComputePipelineAsync({
|
|
4054
4113
|
label: "particle compute pipeline",
|
|
4055
|
-
layout:
|
|
4114
|
+
layout: computePipelineLayout,
|
|
4056
4115
|
compute: { module: computeModule, entryPoint: "main" },
|
|
4057
4116
|
});
|
|
4117
|
+
const live = indirect && countLayout
|
|
4118
|
+
? {
|
|
4119
|
+
pipeline: await this.device.createComputePipelineAsync({
|
|
4120
|
+
label: "particle count pipeline",
|
|
4121
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [countLayout] }),
|
|
4122
|
+
compute: { module: computeModule, entryPoint: "rzCount" },
|
|
4123
|
+
}),
|
|
4124
|
+
indirect,
|
|
4125
|
+
binds: bindFor(countLayout, this.cameraUniformBuffer, false, true),
|
|
4126
|
+
}
|
|
4127
|
+
: null;
|
|
4058
4128
|
const renderLayoutObj = this.device.createPipelineLayout({ bindGroupLayouts: [renderLayout] });
|
|
4059
4129
|
// The prepass, when the effect gave the cutout a cheap shape: coverage to
|
|
4060
4130
|
// depth, colour targets at writeMask 0 (the depth-prepass class), and
|
|
@@ -4096,6 +4166,8 @@ export class Engine {
|
|
|
4096
4166
|
if (scoped) {
|
|
4097
4167
|
buffer.destroy();
|
|
4098
4168
|
uniform.destroy();
|
|
4169
|
+
points?.buffer.destroy();
|
|
4170
|
+
indirect?.destroy();
|
|
4099
4171
|
return { ok: false, diagnostics: [scoped.message] };
|
|
4100
4172
|
}
|
|
4101
4173
|
return {
|
|
@@ -4111,17 +4183,20 @@ export class Engine {
|
|
|
4111
4183
|
counts: uniformView.uints,
|
|
4112
4184
|
compute,
|
|
4113
4185
|
computeLayout,
|
|
4114
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4186
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
4115
4187
|
depth,
|
|
4116
4188
|
render,
|
|
4117
4189
|
renderLayout,
|
|
4118
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4119
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4190
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
4191
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
4120
4192
|
rebind: () => ({
|
|
4121
|
-
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false),
|
|
4122
|
-
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true),
|
|
4123
|
-
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true),
|
|
4193
|
+
computeBinds: bindFor(computeLayout, this.cameraUniformBuffer, false, false),
|
|
4194
|
+
renderBinds: bindFor(renderLayout, this.cameraUniformBuffer, true, false),
|
|
4195
|
+
mirrorRenderBinds: bindFor(renderLayout, this.mirrorCameraBuffer, true, false),
|
|
4196
|
+
countBinds: countLayout ? bindFor(countLayout, this.cameraUniformBuffer, false, true) : null,
|
|
4124
4197
|
}),
|
|
4198
|
+
points,
|
|
4199
|
+
live,
|
|
4125
4200
|
},
|
|
4126
4201
|
};
|
|
4127
4202
|
}
|
|
@@ -4129,6 +4204,8 @@ export class Engine {
|
|
|
4129
4204
|
await this.device.popErrorScope();
|
|
4130
4205
|
buffer.destroy();
|
|
4131
4206
|
uniform.destroy();
|
|
4207
|
+
points?.buffer.destroy();
|
|
4208
|
+
indirect?.destroy();
|
|
4132
4209
|
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] };
|
|
4133
4210
|
}
|
|
4134
4211
|
}
|
|
@@ -4249,6 +4326,7 @@ export class Engine {
|
|
|
4249
4326
|
this.device.queue.writeBuffer(e.lights.uniform, 0, e.lights.data.buffer);
|
|
4250
4327
|
}
|
|
4251
4328
|
this.lightHeader[0] = Math.min(next, MAX_LIGHTS);
|
|
4329
|
+
this.lightHeader[1] = this.docLightCount;
|
|
4252
4330
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightHeader);
|
|
4253
4331
|
}
|
|
4254
4332
|
/** Run every effect's lightEmit, before the pass that reads the result. */
|
|
@@ -4311,13 +4389,62 @@ export class Engine {
|
|
|
4311
4389
|
p.counts[2] = p.count;
|
|
4312
4390
|
p.counts[3] = this.particleFrame++;
|
|
4313
4391
|
this.device.queue.writeBuffer(p.uniform, 0, p.data.buffer);
|
|
4392
|
+
if (p.points)
|
|
4393
|
+
this.writePoints(p.points);
|
|
4394
|
+
const bind = p.computeBinds[e.grid?.parity ?? 0];
|
|
4395
|
+
if (p.live) {
|
|
4396
|
+
// The frame's live count first, in a pass of its own: a buffer may
|
|
4397
|
+
// not be written as storage and read as indirect arguments inside one
|
|
4398
|
+
// pass, and a pass is the synchronization scope.
|
|
4399
|
+
const count = encoder.beginComputePass({ label: "particle count" });
|
|
4400
|
+
count.setPipeline(p.live.pipeline);
|
|
4401
|
+
count.setBindGroup(0, p.live.binds[e.grid?.parity ?? 0]);
|
|
4402
|
+
count.dispatchWorkgroups(1);
|
|
4403
|
+
count.end();
|
|
4404
|
+
}
|
|
4314
4405
|
const cp = encoder.beginComputePass({ label: "particles" });
|
|
4315
4406
|
cp.setPipeline(p.compute);
|
|
4316
|
-
cp.setBindGroup(0,
|
|
4317
|
-
|
|
4407
|
+
cp.setBindGroup(0, bind);
|
|
4408
|
+
if (p.live)
|
|
4409
|
+
cp.dispatchWorkgroupsIndirect(p.live.indirect, 0);
|
|
4410
|
+
else
|
|
4411
|
+
cp.dispatchWorkgroups(Math.ceil(p.count / 64));
|
|
4318
4412
|
cp.end();
|
|
4319
4413
|
}
|
|
4320
4414
|
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Every bone matching an effect's `#points` prefix, on every visible model,
|
|
4417
|
+
* posed and placed this frame. Every frame rather than on load: a candle a
|
|
4418
|
+
* character carries moves with the hand, and a stage can be moved.
|
|
4419
|
+
*
|
|
4420
|
+
* The order is the models' order, then rig order — stable while nothing is
|
|
4421
|
+
* added or removed, so particle i stays on point i and a flame does not
|
|
4422
|
+
* jump to another wick between frames.
|
|
4423
|
+
*/
|
|
4424
|
+
writePoints(points) {
|
|
4425
|
+
let n = 0;
|
|
4426
|
+
for (const inst of this.modelInstances.values()) {
|
|
4427
|
+
const m = inst.model;
|
|
4428
|
+
if (!m.visible)
|
|
4429
|
+
continue;
|
|
4430
|
+
let byPrefix = this.pointBones.get(m);
|
|
4431
|
+
if (!byPrefix)
|
|
4432
|
+
this.pointBones.set(m, (byPrefix = new Map()));
|
|
4433
|
+
let bones = byPrefix.get(points.prefix);
|
|
4434
|
+
const rig = m.getSkeleton().bones;
|
|
4435
|
+
if (!bones)
|
|
4436
|
+
byPrefix.set(points.prefix, (bones = bonesWithPrefix(rig, points.prefix)));
|
|
4437
|
+
for (const i of bones) {
|
|
4438
|
+
if (n >= MAX_EFFECT_POINTS)
|
|
4439
|
+
break;
|
|
4440
|
+
const world = m.getBoneWorldMatrixAt(i);
|
|
4441
|
+
if (world)
|
|
4442
|
+
writeBonePoint(points.data, n++, world, rig[i].tail, m);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
points.data[0] = n;
|
|
4446
|
+
this.device.queue.writeBuffer(points.buffer, 0, points.data.buffer, 0, (4 + n * 8) * 4);
|
|
4447
|
+
}
|
|
4321
4448
|
/** Draw the pool. Inside the scene pass, so it is depth-tested and pre-bloom. */
|
|
4322
4449
|
renderParticles(pass, view) {
|
|
4323
4450
|
for (const e of this.effects) {
|
|
@@ -4326,14 +4453,21 @@ export class Engine {
|
|
|
4326
4453
|
continue;
|
|
4327
4454
|
const parity = e.grid?.parity ?? 0;
|
|
4328
4455
|
const bind = view === "mirror" ? p.mirrorRenderBinds[parity] : p.renderBinds[parity];
|
|
4456
|
+
// The live count's quads when the effect declared one, else the pool's.
|
|
4457
|
+
const draw = () => {
|
|
4458
|
+
if (p.live)
|
|
4459
|
+
pass.drawIndirect(p.live.indirect, PARTICLE_INDIRECT_DRAW_OFFSET);
|
|
4460
|
+
else
|
|
4461
|
+
pass.draw(6, p.count);
|
|
4462
|
+
};
|
|
4329
4463
|
if (p.depth) {
|
|
4330
4464
|
pass.setPipeline(p.depth);
|
|
4331
4465
|
pass.setBindGroup(0, bind);
|
|
4332
|
-
|
|
4466
|
+
draw();
|
|
4333
4467
|
}
|
|
4334
4468
|
pass.setPipeline(p.render);
|
|
4335
4469
|
pass.setBindGroup(0, bind);
|
|
4336
|
-
|
|
4470
|
+
draw();
|
|
4337
4471
|
}
|
|
4338
4472
|
}
|
|
4339
4473
|
/**
|
|
@@ -4699,6 +4833,8 @@ export class Engine {
|
|
|
4699
4833
|
for (const e of this.effects) {
|
|
4700
4834
|
e.particles?.buffer.destroy();
|
|
4701
4835
|
e.particles?.uniform.destroy();
|
|
4836
|
+
e.particles?.points?.buffer.destroy();
|
|
4837
|
+
e.particles?.live?.indirect.destroy();
|
|
4702
4838
|
e.particles = null;
|
|
4703
4839
|
}
|
|
4704
4840
|
}
|
|
@@ -6062,15 +6198,21 @@ export class Engine {
|
|
|
6062
6198
|
this.bakeFilmicLut();
|
|
6063
6199
|
// BEFORE the bind group below, which binds it. Full size from the start:
|
|
6064
6200
|
// every material pipeline binds this, so sizing it to the light count would
|
|
6065
|
-
// mean rebuilding bind groups whenever a scene gained a lamp.
|
|
6066
|
-
//
|
|
6067
|
-
|
|
6201
|
+
// mean rebuilding bind groups whenever a scene gained a lamp. Uploaded from
|
|
6202
|
+
// the CPU copy, which holds any lamps set before the device existed; the
|
|
6203
|
+
// header after it, from the one writer that owns it.
|
|
6068
6204
|
this.lightsBuffer = this.device.createBuffer({
|
|
6069
6205
|
label: "positional lights",
|
|
6070
6206
|
size: this.lightsData.byteLength,
|
|
6071
6207
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
6072
6208
|
});
|
|
6073
6209
|
this.device.queue.writeBuffer(this.lightsBuffer, 0, this.lightsData);
|
|
6210
|
+
this.allocateLightSlots();
|
|
6211
|
+
this.pointsFallback = this.device.createBuffer({
|
|
6212
|
+
label: "no points",
|
|
6213
|
+
size: 16,
|
|
6214
|
+
usage: GPUBufferUsage.STORAGE,
|
|
6215
|
+
});
|
|
6074
6216
|
// BEFORE the per-frame groups, which bind it. The composite needs this 1x1
|
|
6075
6217
|
// stand-in too, and it used to be created with the composite's own
|
|
6076
6218
|
// resources further down — far enough down that a material group built up
|
|
@@ -8418,10 +8560,23 @@ export class Engine {
|
|
|
8418
8560
|
this.lightsData[b + 14] = 0;
|
|
8419
8561
|
this.lightsData[b + 15] = 0;
|
|
8420
8562
|
}
|
|
8563
|
+
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
8564
|
+
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
8565
|
+
const grid = buildLightGrid(list.map((_, i) => {
|
|
8566
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE;
|
|
8567
|
+
const d = this.lightsData;
|
|
8568
|
+
return { x: d[b], y: d[b + 1], z: d[b + 2], radius: d[b + 3], ax: d[b + 8], ay: d[b + 9], az: d[b + 10], cosOuter: d[b + 11] };
|
|
8569
|
+
}));
|
|
8570
|
+
this.lightHeader.set(grid.origin, 4);
|
|
8571
|
+
this.lightHeader[7] = 1 / grid.cell;
|
|
8572
|
+
this.lightHeaderWords.set(grid.dims, 8);
|
|
8573
|
+
this.lightHeaderWords.set(grid.outside, 12);
|
|
8574
|
+
this.lightsWords.set(grid.cells, LIGHT_GRID_BASE);
|
|
8421
8575
|
// The CPU copy is written either way; only the upload needs a device. A
|
|
8422
8576
|
// scene whose lamps arrive before init keeps them, and init uploads them.
|
|
8423
8577
|
if (list.length && this.device && this.lightsBuffer) {
|
|
8424
8578
|
this.device.queue.writeBuffer(this.lightsBuffer, LIGHT_HEADER * 4, this.lightsData.buffer, LIGHT_HEADER * 4, list.length * LIGHT_STRIDE * 4);
|
|
8579
|
+
this.device.queue.writeBuffer(this.lightsBuffer, LIGHT_GRID_BASE * 4, this.lightsData.buffer, LIGHT_GRID_BASE * 4, grid.cells.byteLength);
|
|
8425
8580
|
}
|
|
8426
8581
|
// The effects' bases move when the document's count does; the header —
|
|
8427
8582
|
// count included — is written there.
|
|
@@ -9849,15 +10004,19 @@ export class Engine {
|
|
|
9849
10004
|
const src = wrapped ? entry.source : entry;
|
|
9850
10005
|
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width);
|
|
9851
10006
|
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height);
|
|
10007
|
+
const mipLevelCount = wrapped && entry.mipmaps ? Math.floor(Math.log2(Math.max(width, height))) + 1 : 1;
|
|
9852
10008
|
const tex = this.device.createTexture({
|
|
9853
10009
|
label: `group map: ${group.id}`,
|
|
9854
10010
|
size: [width, height],
|
|
10011
|
+
mipLevelCount,
|
|
9855
10012
|
// Colour maps decode to linear on sample, the way material textures do;
|
|
9856
10013
|
// data maps must not, or every threshold packed in their channels moves.
|
|
9857
10014
|
format: wrapped && entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
9858
10015
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
9859
10016
|
});
|
|
9860
10017
|
this.device.queue.copyExternalImageToTexture({ source: src }, { texture: tex, premultipliedAlpha: wrapped && entry.premultiplied === true }, [width, height]);
|
|
10018
|
+
if (mipLevelCount > 1)
|
|
10019
|
+
this.generateMipmaps(tex, mipLevelCount);
|
|
9861
10020
|
return tex;
|
|
9862
10021
|
});
|
|
9863
10022
|
}
|
|
@@ -12127,31 +12286,35 @@ export class Engine {
|
|
|
12127
12286
|
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
12128
12287
|
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
12129
12288
|
generateMipmaps(texture, mipLevelCount) {
|
|
12130
|
-
if (!this.
|
|
12289
|
+
if (!this.mipBlitSampler) {
|
|
12131
12290
|
this.mipBlitSampler = this.device.createSampler({
|
|
12132
12291
|
magFilter: "linear",
|
|
12133
12292
|
minFilter: "linear",
|
|
12134
12293
|
addressModeU: "clamp-to-edge",
|
|
12135
12294
|
addressModeV: "clamp-to-edge",
|
|
12136
12295
|
});
|
|
12296
|
+
}
|
|
12297
|
+
let pipeline = this.mipBlitPipelines.get(texture.format);
|
|
12298
|
+
if (!pipeline) {
|
|
12137
12299
|
const module = this.device.createShaderModule({
|
|
12138
12300
|
label: "mipmap blit",
|
|
12139
12301
|
code: MIPMAP_BLIT_SHADER_WGSL,
|
|
12140
12302
|
});
|
|
12141
|
-
|
|
12142
|
-
label:
|
|
12303
|
+
pipeline = this.device.createRenderPipeline({
|
|
12304
|
+
label: `mipmap blit pipeline (${texture.format})`,
|
|
12143
12305
|
layout: "auto",
|
|
12144
12306
|
vertex: { module, entryPoint: "vs" },
|
|
12145
|
-
fragment: { module, entryPoint: "fs", targets: [{ format:
|
|
12307
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: texture.format }] },
|
|
12146
12308
|
primitive: { topology: "triangle-list" },
|
|
12147
12309
|
});
|
|
12310
|
+
this.mipBlitPipelines.set(texture.format, pipeline);
|
|
12148
12311
|
}
|
|
12149
12312
|
const encoder = this.device.createCommandEncoder({ label: "mipgen" });
|
|
12150
12313
|
for (let level = 1; level < mipLevelCount; level++) {
|
|
12151
12314
|
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
12152
12315
|
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
|
|
12153
12316
|
const bindGroup = this.device.createBindGroup({
|
|
12154
|
-
layout:
|
|
12317
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
12155
12318
|
entries: [
|
|
12156
12319
|
{ binding: 0, resource: srcView },
|
|
12157
12320
|
{ binding: 1, resource: this.mipBlitSampler },
|
|
@@ -12162,7 +12325,7 @@ export class Engine {
|
|
|
12162
12325
|
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
12163
12326
|
],
|
|
12164
12327
|
});
|
|
12165
|
-
pass.setPipeline(
|
|
12328
|
+
pass.setPipeline(pipeline);
|
|
12166
12329
|
pass.setBindGroup(0, bindGroup);
|
|
12167
12330
|
pass.draw(3);
|
|
12168
12331
|
pass.end();
|
|
@@ -14987,12 +15150,13 @@ Engine.JIGGLE_DAMPING_SCALE = 0.5;
|
|
|
14987
15150
|
* prefix sum and readback a compacted draw list would need every frame.
|
|
14988
15151
|
*/
|
|
14989
15152
|
/** Ceiling for `#particles`. Past this an author is asking for a stall. */
|
|
14990
|
-
// Raised for the lawn:
|
|
14991
|
-
//
|
|
15153
|
+
// Raised for the lawn: an 80-unit field at 36 blades a unit wants seven
|
|
15154
|
+
// hundred thousand, and a turf one three million. A cutout pool's cost is
|
|
14992
15155
|
// bounded by the pixels it covers, not by the count — the step is one
|
|
14993
|
-
// compute invocation per slot, the pool buffer is 48 bytes each (
|
|
14994
|
-
// the cap
|
|
14995
|
-
|
|
15156
|
+
// compute invocation per slot, the pool buffer is 48 bytes each (96 MiB at
|
|
15157
|
+
// the cap, under the 128 MiB a storage binding may be without asking for
|
|
15158
|
+
// more), and a culled blade is a degenerate quad the rasteriser drops.
|
|
15159
|
+
Engine.MAX_PARTICLES = 2097152;
|
|
14996
15160
|
/**
|
|
14997
15161
|
* The field layer: user background/foreground mounts, ONE TARGET PAIR PER
|
|
14998
15162
|
* RESOLUTION. Index 0 is full, index 1 is half — coarsest last, so the
|
|
@@ -75,6 +75,13 @@ export type GroupImage = {
|
|
|
75
75
|
source: GroupImageSource;
|
|
76
76
|
srgb?: boolean;
|
|
77
77
|
premultiplied?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Give it a mip chain. For a map TILED across a surface — a relief map, a
|
|
80
|
+
* water's ripples — whose far tiles otherwise shrink to a pixel each and
|
|
81
|
+
* sample as noise that sparkles with every camera move. Off by default: a
|
|
82
|
+
* ramp read at a computed coordinate would blur at its own terminator.
|
|
83
|
+
*/
|
|
84
|
+
mipmaps?: boolean;
|
|
78
85
|
};
|
|
79
86
|
export type GroupDiagnostic = {
|
|
80
87
|
groupId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAA;IACjD;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG;
|
|
1
|
+
{"version":3,"file":"style-group.d.ts","sourceRoot":"","sources":["../../src/graph/style-group.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;2CACuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,sFAAsF;IACtF,KAAK,EAAE,WAAW,CAAA;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAA;IACjD;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,CAAA;AAEjF;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,gBAAgB,CAAA;IACxB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,sCAAsC;IACtC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,kFAAkF;IAClF,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,EAAE,CAAA;CAAE,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** One document lamp, as the grid sees it. */
|
|
2
|
+
export type GridLamp = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
radius: number;
|
|
7
|
+
/** Unit aim, away from the lamp. Zero for a point lamp. */
|
|
8
|
+
ax: number;
|
|
9
|
+
ay: number;
|
|
10
|
+
az: number;
|
|
11
|
+
/** Cosine of the half-angle. -1 for a point lamp, which covers everything. */
|
|
12
|
+
cosOuter: number;
|
|
13
|
+
};
|
|
14
|
+
export type LightGrid = {
|
|
15
|
+
origin: [number, number, number];
|
|
16
|
+
/** Edge length of one cubic cell, world units. */
|
|
17
|
+
cell: number;
|
|
18
|
+
dims: [number, number, number];
|
|
19
|
+
/** The lamps that reach past the box, for fragments outside it. */
|
|
20
|
+
outside: Uint32Array;
|
|
21
|
+
/** LIGHT_MASK_WORDS words per cell, x fastest, then y, then z. */
|
|
22
|
+
cells: Uint32Array;
|
|
23
|
+
};
|
|
24
|
+
export declare function buildLightGrid(lamps: GridLamp[], maxCells?: number): LightGrid;
|
|
25
|
+
//# sourceMappingURL=light-grid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"light-grid.d.ts","sourceRoot":"","sources":["../src/light-grid.ts"],"names":[],"mappings":"AA2BA,8CAA8C;AAC9C,MAAM,MAAM,QAAQ,GAAG;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,mEAAmE;IACnE,OAAO,EAAE,WAAW,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,WAAW,CAAA;CACnB,CAAA;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,QAAQ,SAAmB,GAAG,SAAS,CAkFxF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { LIGHT_GRID_CELLS, LIGHT_MASK_WORDS } from "./shaders/lights";
|
|
2
|
+
function setBit(words, base, lamp) {
|
|
3
|
+
words[base + (lamp >> 5)] |= 1 << (lamp & 31);
|
|
4
|
+
}
|
|
5
|
+
export function buildLightGrid(lamps, maxCells = LIGHT_GRID_CELLS) {
|
|
6
|
+
const outside = new Uint32Array(LIGHT_MASK_WORDS);
|
|
7
|
+
const live = lamps
|
|
8
|
+
.map((l, i) => ({ l, i }))
|
|
9
|
+
.filter(({ l }) => l.radius > 0 && [l.x, l.y, l.z, l.radius].every(Number.isFinite));
|
|
10
|
+
if (!live.length)
|
|
11
|
+
return { origin: [0, 0, 0], cell: 1, dims: [0, 0, 0], outside, cells: new Uint32Array(0) };
|
|
12
|
+
const radii = live.map(({ l }) => l.radius).sort((a, b) => a - b);
|
|
13
|
+
const pad = radii[radii.length >> 1];
|
|
14
|
+
const min = [Infinity, Infinity, Infinity];
|
|
15
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
16
|
+
for (const { l } of live) {
|
|
17
|
+
const p = [l.x, l.y, l.z];
|
|
18
|
+
for (let k = 0; k < 3; k++) {
|
|
19
|
+
min[k] = Math.min(min[k], p[k] - pad);
|
|
20
|
+
max[k] = Math.max(max[k], p[k] + pad);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const extent = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
24
|
+
let cell = Math.cbrt((extent[0] * extent[1] * extent[2]) / maxCells);
|
|
25
|
+
let dims;
|
|
26
|
+
// ceil() rounds every axis up, so the first guess can overshoot the budget by
|
|
27
|
+
// a few percent; widen the cell until it fits.
|
|
28
|
+
for (;;) {
|
|
29
|
+
dims = [0, 1, 2].map((k) => Math.max(1, Math.ceil(extent[k] / cell)));
|
|
30
|
+
if (dims[0] * dims[1] * dims[2] <= maxCells)
|
|
31
|
+
break;
|
|
32
|
+
cell *= 1.02;
|
|
33
|
+
}
|
|
34
|
+
const cells = new Uint32Array(dims[0] * dims[1] * dims[2] * LIGHT_MASK_WORDS);
|
|
35
|
+
// Half the cell's diagonal: the sphere a spot's cone is tested against.
|
|
36
|
+
const halfDiagonal = (cell * Math.sqrt(3)) / 2;
|
|
37
|
+
for (const { l, i } of live) {
|
|
38
|
+
// A hair of slack on the reach: the shader finds its cell in f32 from a
|
|
39
|
+
// reciprocal, the builder in f64 from a division, and a fragment on a cell
|
|
40
|
+
// face may land in either neighbour.
|
|
41
|
+
const r = l.radius + cell * 1e-4;
|
|
42
|
+
const p = [l.x, l.y, l.z];
|
|
43
|
+
const lo = [0, 0, 0];
|
|
44
|
+
const hi = [0, 0, 0];
|
|
45
|
+
let leaves = false;
|
|
46
|
+
for (let k = 0; k < 3; k++) {
|
|
47
|
+
const a = Math.floor((p[k] - r - min[k]) / cell);
|
|
48
|
+
const b = Math.floor((p[k] + r - min[k]) / cell);
|
|
49
|
+
if (a < 0 || b >= dims[k])
|
|
50
|
+
leaves = true;
|
|
51
|
+
lo[k] = Math.max(a, 0);
|
|
52
|
+
hi[k] = Math.min(b, dims[k] - 1);
|
|
53
|
+
}
|
|
54
|
+
if (leaves)
|
|
55
|
+
setBit(outside, 0, i);
|
|
56
|
+
if (lo[0] > hi[0] || lo[1] > hi[1] || lo[2] > hi[2])
|
|
57
|
+
continue;
|
|
58
|
+
const spot = l.cosOuter > -1;
|
|
59
|
+
const outer = Math.acos(Math.min(Math.max(l.cosOuter, -1), 1));
|
|
60
|
+
for (let z = lo[2]; z <= hi[2]; z++) {
|
|
61
|
+
for (let y = lo[1]; y <= hi[1]; y++) {
|
|
62
|
+
for (let x = lo[0]; x <= hi[0]; x++) {
|
|
63
|
+
const b0 = [min[0] + x * cell, min[1] + y * cell, min[2] + z * cell];
|
|
64
|
+
// The sphere against the cell's box: the nearest point of the box.
|
|
65
|
+
let d2 = 0;
|
|
66
|
+
for (let k = 0; k < 3; k++) {
|
|
67
|
+
const q = Math.min(Math.max(p[k], b0[k]), b0[k] + cell);
|
|
68
|
+
d2 += (q - p[k]) * (q - p[k]);
|
|
69
|
+
}
|
|
70
|
+
if (d2 > r * r)
|
|
71
|
+
continue;
|
|
72
|
+
if (spot) {
|
|
73
|
+
// The cone against the cell's bounding sphere: the cell can be lit
|
|
74
|
+
// when the angle to its centre is within the cone's half-angle plus
|
|
75
|
+
// the angle that sphere subtends.
|
|
76
|
+
const v = [b0[0] + cell / 2 - p[0], b0[1] + cell / 2 - p[1], b0[2] + cell / 2 - p[2]];
|
|
77
|
+
const len = Math.hypot(v[0], v[1], v[2]);
|
|
78
|
+
if (len > halfDiagonal) {
|
|
79
|
+
const limit = outer + Math.asin(halfDiagonal / len);
|
|
80
|
+
const cos = (v[0] * l.ax + v[1] * l.ay + v[2] * l.az) / len;
|
|
81
|
+
if (limit < Math.PI && Math.acos(Math.min(Math.max(cos, -1), 1)) > limit)
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
setBit(cells, ((z * dims[1] + y) * dims[0] + x) * LIGHT_MASK_WORDS, i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { origin: [min[0], min[1], min[2]], cell, dims, outside, cells };
|
|
91
|
+
}
|