reze-engine 0.40.0 → 0.41.1
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/README.md +8 -5
- package/dist/engine.d.ts +87 -31
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +264 -77
- package/dist/graph/presets/body.js +2 -2
- package/dist/graph/presets/cloth_rough.js +2 -2
- package/dist/graph/presets/cloth_smooth.js +2 -2
- package/dist/graph/presets/default.js +2 -2
- package/dist/graph/presets/eye.js +2 -2
- package/dist/graph/presets/face.js +2 -2
- package/dist/graph/presets/hair.js +2 -2
- package/dist/graph/presets/metal.js +2 -2
- package/dist/graph/presets/stockings.js +3 -3
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +436 -10
- package/dist/graph/style-group.d.ts +46 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/shaders/agx-lut.d.ts +11 -0
- package/dist/shaders/agx-lut.d.ts.map +1 -0
- package/dist/shaders/agx-lut.js +29 -0
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -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 +957 -555
- package/dist/shaders/passes/composite.d.ts +27 -12
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +155 -48
- package/package.json +1 -1
- package/src/engine.ts +304 -84
- package/src/graph/presets/body.ts +2 -2
- package/src/graph/presets/cloth_rough.ts +2 -2
- package/src/graph/presets/cloth_smooth.ts +2 -2
- package/src/graph/presets/default.ts +2 -2
- package/src/graph/presets/eye.ts +2 -2
- package/src/graph/presets/face.ts +2 -2
- package/src/graph/presets/hair.ts +2 -2
- package/src/graph/presets/metal.ts +2 -2
- package/src/graph/presets/stockings.ts +3 -3
- package/src/graph/registry.ts +450 -10
- package/src/graph/style-group.ts +44 -0
- package/src/index.ts +4 -2
- package/src/shaders/agx-lut.ts +30 -0
- package/src/shaders/materials/common.ts +10 -0
- package/src/shaders/materials/nodes.ts +962 -560
- package/src/shaders/passes/composite.ts +182 -64
package/src/engine.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
32
32
|
BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
33
33
|
} from "./shaders/passes/bloom"
|
|
34
|
+
import { AGX_LUT_GZ, AGX_LUT_SIZE } from "./shaders/agx-lut"
|
|
34
35
|
import { buildCompositeShader } from "./shaders/passes/composite"
|
|
35
36
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
36
37
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
@@ -84,7 +85,17 @@ const PRESET_NAME_HINTS: Array<[MaterialPreset, string[]]> = [
|
|
|
84
85
|
"face",
|
|
85
86
|
["顔", "颜", "顏", "脸", "臉", "かお", "face", "舌", "tongue", "牙", "牙齿", "齿", "歯", "teeth", "tooth", "口腔", "口内", "mouth", "嘴", "唇", "歯茎", "gums"],
|
|
86
87
|
],
|
|
87
|
-
|
|
88
|
+
// Simplified 发 is listed as compounds, never bare: it also writes 发光 (glow),
|
|
89
|
+
// and hair carries a renderClass, so a chance hit puts an emissive panel in the
|
|
90
|
+
// hair pass. Same reasoning as bare 口 being omitted from face above.
|
|
91
|
+
[
|
|
92
|
+
"hair",
|
|
93
|
+
[
|
|
94
|
+
"前髪", "後髪", "髪", "髮", "頭髪", "もみあげ", "アホ毛", "ヘア",
|
|
95
|
+
"头发", "前发", "后发", "长发", "短发", "发丝", "刘海", "辫", "马尾",
|
|
96
|
+
"hair", "ahoge", "bang",
|
|
97
|
+
],
|
|
98
|
+
],
|
|
88
99
|
["body", ["肌", "皮肤", "skin"]],
|
|
89
100
|
["metal", ["金属", "メタル", "metal", "earring", "耳环", "耳環"]],
|
|
90
101
|
[
|
|
@@ -187,6 +198,9 @@ type GroupInstall = {
|
|
|
187
198
|
/** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
188
199
|
overEyesPipeline?: GPURenderPipeline
|
|
189
200
|
uniformBuffer: GPUBuffer
|
|
201
|
+
/** The group's own image maps, uploaded once per apply and owned here — the
|
|
202
|
+
* install destroys them when it is replaced, so a re-apply cannot leak. */
|
|
203
|
+
images?: (GPUTexture | null)[]
|
|
190
204
|
slotMap: StyleSlot[]
|
|
191
205
|
/** Serialized (graph + renderClass + alphaMode) — lets applyStyleGroups skip recompiling
|
|
192
206
|
* an unchanged group. */
|
|
@@ -235,14 +249,17 @@ export type SunOptions = {
|
|
|
235
249
|
direction?: Vec3
|
|
236
250
|
}
|
|
237
251
|
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
export type
|
|
242
|
-
export type
|
|
252
|
+
/** An effect param: number → f32, vector-like → vec3f (see setEffect).
|
|
253
|
+
* Structural {x,y,z} rather than the Vec3 class so JSON-derived values (a
|
|
254
|
+
* shared scene document's params) pass straight in. */
|
|
255
|
+
export type EffectParamValue = number | { x: number; y: number; z: number }
|
|
256
|
+
export type EffectResult = {
|
|
243
257
|
ok: boolean
|
|
244
258
|
/** Compile/validation errors, line:col relative to the USER's WGSL. */
|
|
245
259
|
diagnostics: string[]
|
|
260
|
+
/** Which mounts the WGSL declared — `fn background` / `fn foreground`. Both
|
|
261
|
+
* false only on a failed compile, since defining neither IS the failure. */
|
|
262
|
+
mounts: { background: boolean; foreground: boolean }
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
export type CameraOptions = {
|
|
@@ -317,7 +334,17 @@ export type ViewTransformOptions = {
|
|
|
317
334
|
exposure: number
|
|
318
335
|
/** After Filmic, display gamma (`pow(rgb, 1/gamma)`). */
|
|
319
336
|
gamma: number
|
|
320
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Which display transform the frame is formed with.
|
|
339
|
+
*
|
|
340
|
+
* "standard" is Blender's Standard: the sRGB encoding and nothing else, which
|
|
341
|
+
* is what NPR and anime work uses — the colours the graph computes are the
|
|
342
|
+
* colours that land, with no film curve reinterpreting them. Both of the
|
|
343
|
+
* reference Wuthering Waves projects render this way.
|
|
344
|
+
*
|
|
345
|
+
* "filmic" is Blender 3.6's Filmic, Medium High Contrast, baked as a LUT.
|
|
346
|
+
*/
|
|
347
|
+
transform: "agx" | "filmic" | "standard"
|
|
321
348
|
}
|
|
322
349
|
|
|
323
350
|
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
@@ -325,7 +352,7 @@ export type ViewTransformOptions = {
|
|
|
325
352
|
export const DEFAULT_VIEW_TRANSFORM: ViewTransformOptions = {
|
|
326
353
|
exposure: 0.6,
|
|
327
354
|
gamma: 1.0,
|
|
328
|
-
|
|
355
|
+
transform: "filmic",
|
|
329
356
|
}
|
|
330
357
|
|
|
331
358
|
/** Color grading applied to the tonemapped scene (ASC CDL — see grade() in
|
|
@@ -772,7 +799,10 @@ export class Engine {
|
|
|
772
799
|
private depthReadView: GPUTextureView | null = null
|
|
773
800
|
private compositeUniformBuffer!: GPUBuffer
|
|
774
801
|
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
775
|
-
|
|
802
|
+
// 11 × vec4f — see the viewU comment in composite.ts. The last one is the
|
|
803
|
+
// camera's world position, which is what lets a foreground effect turn the
|
|
804
|
+
// depth it is handed into a PLACE (bgWorldPos) rather than a distance.
|
|
805
|
+
private readonly compositeUniformData = new Float32Array(44)
|
|
776
806
|
/** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
|
|
777
807
|
private backgroundColor: Vec3 | null = null
|
|
778
808
|
// 360 backdrop (equirectangular skybox, sampled by view ray in composite).
|
|
@@ -780,21 +810,29 @@ export class Engine {
|
|
|
780
810
|
private backdropEquirectView: GPUTextureView | null = null
|
|
781
811
|
private fallbackEquirectTexture!: GPUTexture
|
|
782
812
|
private fallbackEquirectView!: GPUTextureView
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
813
|
+
// The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
|
|
814
|
+
// scene, over it, or both — whichever of background()/foreground() the code
|
|
815
|
+
// defines. The composite pipelines are REBUILT with the user code injected;
|
|
816
|
+
// params live in their own uniform buffer so setEffectParam is a write, not a
|
|
786
817
|
// recompile (the same instant tier as setStyleParam).
|
|
787
|
-
private
|
|
818
|
+
private effect: {
|
|
788
819
|
wgsl: string
|
|
789
820
|
paramLayout: Map<string, { offset: number; comps: 1 | 3 }>
|
|
790
821
|
paramsBuffer: GPUBuffer | null
|
|
791
822
|
paramsData: Float32Array<ArrayBuffer>
|
|
823
|
+
/** Mounted under the scene. */
|
|
824
|
+
hasBackground: boolean
|
|
825
|
+
/** Mounted over the finished frame — and the reason the scene pass has to
|
|
826
|
+
* STORE its depth, which it otherwise discards into tile memory. */
|
|
827
|
+
hasForeground: boolean
|
|
792
828
|
} | null = null
|
|
829
|
+
private agxLutTexture: GPUTexture | null = null
|
|
830
|
+
private agxFallbackTexture!: GPUTexture
|
|
793
831
|
/** Bound at composite binding 7 when no effect (or a param-less one) is set. */
|
|
794
832
|
private bgParamsDummyBuffer!: GPUBuffer
|
|
795
833
|
private compositePipelineLayout!: GPUPipelineLayout
|
|
796
|
-
/** time=0 origin for the active effect — reset each
|
|
797
|
-
private
|
|
834
|
+
/** time=0 origin for the active effect — reset each setEffect. */
|
|
835
|
+
private effectEpochMs = 0
|
|
798
836
|
private compositeBloomView: GPUTextureView | null = null
|
|
799
837
|
|
|
800
838
|
// EEVEE-style bloom pyramid (mirrors Blender 3.6 effect_bloom_frag.glsl):
|
|
@@ -969,7 +1007,7 @@ export class Engine {
|
|
|
969
1007
|
return {
|
|
970
1008
|
exposure: partial?.exposure ?? d.exposure,
|
|
971
1009
|
gamma: partial?.gamma ?? d.gamma,
|
|
972
|
-
|
|
1010
|
+
transform: partial?.transform ?? d.transform,
|
|
973
1011
|
}
|
|
974
1012
|
}
|
|
975
1013
|
|
|
@@ -989,7 +1027,7 @@ export class Engine {
|
|
|
989
1027
|
|
|
990
1028
|
getViewTransformOptions(): ViewTransformOptions {
|
|
991
1029
|
const v = this.viewTransform
|
|
992
|
-
return { exposure: v.exposure, gamma: v.gamma,
|
|
1030
|
+
return { exposure: v.exposure, gamma: v.gamma, transform: v.transform }
|
|
993
1031
|
}
|
|
994
1032
|
|
|
995
1033
|
private colorGrading: ColorGradingOptions = {
|
|
@@ -1032,7 +1070,7 @@ export class Engine {
|
|
|
1032
1070
|
const v = this.viewTransform
|
|
1033
1071
|
if (patch.exposure !== undefined) v.exposure = patch.exposure
|
|
1034
1072
|
if (patch.gamma !== undefined) v.gamma = patch.gamma
|
|
1035
|
-
if (patch.
|
|
1073
|
+
if (patch.transform !== undefined) v.transform = patch.transform
|
|
1036
1074
|
if (this.device && this.compositeUniformBuffer) {
|
|
1037
1075
|
this.writeCompositeViewUniforms()
|
|
1038
1076
|
}
|
|
@@ -1062,10 +1100,12 @@ export class Engine {
|
|
|
1062
1100
|
u[8] = bg?.x ?? 0
|
|
1063
1101
|
u[9] = bg?.y ?? 0
|
|
1064
1102
|
u[10] = bg?.z ?? 0
|
|
1065
|
-
// Base-layer mode
|
|
1066
|
-
//
|
|
1103
|
+
// Base-layer mode only. A user effect is a separate LAYER over whichever
|
|
1104
|
+
// base is active, and needs no flag of its own: the composite pipeline is
|
|
1105
|
+
// rebuilt per effect, so the compiled variant IS the flag.
|
|
1067
1106
|
u[11] = this.backdropEquirectView ? 2 : bg ? 1 : 0
|
|
1068
|
-
|
|
1107
|
+
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
1108
|
+
u[25] = v.transform === "agx" ? 2 : v.transform === "standard" ? 1 : 0
|
|
1069
1109
|
u[26] = this.canvas.width
|
|
1070
1110
|
u[27] = this.canvas.height
|
|
1071
1111
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -1133,9 +1173,10 @@ export class Engine {
|
|
|
1133
1173
|
{ binding: 4, resource: this.maskResolveView },
|
|
1134
1174
|
{ binding: 5, resource: this.filmicLutView },
|
|
1135
1175
|
{ binding: 6, resource: this.backdropEquirectView ?? this.fallbackEquirectView },
|
|
1136
|
-
{ binding: 7, resource: { buffer: this.
|
|
1176
|
+
{ binding: 7, resource: { buffer: this.effect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
1137
1177
|
{ binding: 8, resource: this.depthReadView },
|
|
1138
1178
|
{ binding: 9, resource: { buffer: this.dofUniformBuffer } },
|
|
1179
|
+
{ binding: 10, resource: (this.agxLutTexture ?? this.agxFallbackTexture).createView({ dimension: "3d" }) },
|
|
1139
1180
|
],
|
|
1140
1181
|
})
|
|
1141
1182
|
}
|
|
@@ -1200,45 +1241,72 @@ export class Engine {
|
|
|
1200
1241
|
}
|
|
1201
1242
|
|
|
1202
1243
|
/**
|
|
1203
|
-
* Install
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
* or transparency) — its alpha lets the base show through, so a starfield is
|
|
1207
|
-
* stars over the user's background color. Display-space: never affects
|
|
1208
|
-
* lighting, bloom, or tonemapping, and is captured by offline export like any
|
|
1209
|
-
* background.
|
|
1210
|
-
*
|
|
1211
|
-
* `wgsl` must define:
|
|
1244
|
+
* Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
|
|
1245
|
+
* composite pass. ONE effect per scene, and the code says where it mounts by
|
|
1246
|
+
* which of these it defines — either, or both in one file:
|
|
1212
1247
|
*
|
|
1213
1248
|
* fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
|
|
1249
|
+
* fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f
|
|
1250
|
+
*
|
|
1251
|
+
* `background` is a LAYER between the base background and the scene,
|
|
1252
|
+
* over-composited onto whichever base is active (solid color, 360 equirect, or
|
|
1253
|
+
* transparency) — its alpha lets the base show through, so a starfield is
|
|
1254
|
+
* stars over the user's background color. `foreground` composites over the
|
|
1255
|
+
* finished frame instead, which is where rain, snow, petals and fog live, and
|
|
1256
|
+
* is handed `depth`: the camera-space distance in metres of whatever the scene
|
|
1257
|
+
* drew at that pixel (the far plane where it drew nothing). Compare a
|
|
1258
|
+
* particle's own distance against it and the model occludes it; fog just reads
|
|
1259
|
+
* it, since fog's alpha IS a function of distance.
|
|
1214
1260
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1261
|
+
* `ray` is the pixel's normalized world-space view direction (LH, +Z forward —
|
|
1262
|
+
* what the skybox samples by), `uv` is 0..1 bottom-left origin, `time` is
|
|
1263
|
+
* seconds since apply, and `bgResolution()` gives the canvas size. Return sRGB
|
|
1264
|
+
* + alpha; alpha is the only "how much does this replace" control there is.
|
|
1265
|
+
* Declared `params` arrive as `params.<name>` (number → f32, Vec3 → vec3f),
|
|
1266
|
+
* shared by both mounts, and are later tweaked without recompiling via
|
|
1267
|
+
* setEffectParam.
|
|
1221
1268
|
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1269
|
+
* Both mounts are display-space: neither affects lighting, bloom or
|
|
1270
|
+
* tonemapping, and both are captured by offline export. A foreground makes the
|
|
1271
|
+
* scene pass STORE its depth buffer (it otherwise discards it into tile
|
|
1272
|
+
* memory) for as long as one is installed.
|
|
1273
|
+
*
|
|
1274
|
+
* Compiles off the hot path (async pipelines): on failure the previous effect
|
|
1275
|
+
* is KEPT and diagnostics are returned with line numbers relative to the
|
|
1276
|
+
* user's WGSL. Pass null to remove the effect.
|
|
1225
1277
|
*/
|
|
1226
|
-
async
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
): Promise<BackgroundEffectResult> {
|
|
1230
|
-
if (!this.device) return { ok: false, diagnostics: ["setBackgroundEffect requires init() to have run"] }
|
|
1278
|
+
async setEffect(wgsl: string | null, params?: Record<string, EffectParamValue>): Promise<EffectResult> {
|
|
1279
|
+
const noMounts = { background: false, foreground: false }
|
|
1280
|
+
if (!this.device) return { ok: false, diagnostics: ["setEffect requires init() to have run"], mounts: noMounts }
|
|
1231
1281
|
|
|
1232
1282
|
if (wgsl === null) {
|
|
1233
|
-
this.
|
|
1234
|
-
this.
|
|
1283
|
+
this.effect?.paramsBuffer?.destroy()
|
|
1284
|
+
this.effect = null
|
|
1235
1285
|
const module = this.device.createShaderModule({ label: "composite shader", code: buildCompositeShader(null) })
|
|
1236
1286
|
this.compositePipelineIdentity = this.makeCompositePipeline(module, false, "composite pipeline (gamma=1)")
|
|
1237
1287
|
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)")
|
|
1238
1288
|
this.rebuildCompositeBindGroup()
|
|
1239
1289
|
this.writeCompositeViewUniforms()
|
|
1240
|
-
return { ok: true, diagnostics: [] }
|
|
1290
|
+
return { ok: true, diagnostics: [], mounts: noMounts }
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ── Which mounts did the author ask for? A declaration, not a setting: the
|
|
1294
|
+
// entry points present in the source are the ones compiled in. Matching the
|
|
1295
|
+
// `fn` keyword is enough to be safe against a `foreground` LOCAL or a call
|
|
1296
|
+
// to one — those never follow `fn`.
|
|
1297
|
+
const hasBackground = /\bfn\s+background\s*\(/.test(wgsl)
|
|
1298
|
+
const hasForeground = /\bfn\s+foreground\s*\(/.test(wgsl)
|
|
1299
|
+
if (!hasBackground && !hasForeground) {
|
|
1300
|
+
return {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
diagnostics: [
|
|
1303
|
+
"an effect must define fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f " +
|
|
1304
|
+
"or fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f (or both)",
|
|
1305
|
+
],
|
|
1306
|
+
mounts: noMounts,
|
|
1307
|
+
}
|
|
1241
1308
|
}
|
|
1309
|
+
const mounts = { background: hasBackground, foreground: hasForeground }
|
|
1242
1310
|
|
|
1243
1311
|
// ── Params: codegen a WGSL struct and mirror its uniform layout on the CPU.
|
|
1244
1312
|
// Fields are emitted in declaration order; offsets follow WGSL's natural
|
|
@@ -1250,7 +1318,7 @@ export class Engine {
|
|
|
1250
1318
|
let cursor = 0
|
|
1251
1319
|
for (const [name, value] of entries) {
|
|
1252
1320
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
1253
|
-
return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`] }
|
|
1321
|
+
return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`], mounts }
|
|
1254
1322
|
}
|
|
1255
1323
|
const isVec = typeof value !== "number"
|
|
1256
1324
|
const align = isVec ? 16 : 4
|
|
@@ -1270,22 +1338,22 @@ export class Engine {
|
|
|
1270
1338
|
}
|
|
1271
1339
|
}
|
|
1272
1340
|
const paramsDecl = entries.length
|
|
1273
|
-
? `struct
|
|
1341
|
+
? `struct EffectParams {\n${fields.join("\n")}\n}\n@group(0) @binding(7) var<uniform> params: EffectParams;\n`
|
|
1274
1342
|
: ""
|
|
1275
1343
|
|
|
1276
1344
|
// ── Compile with validation captured, not thrown at the console. Line
|
|
1277
1345
|
// numbers in diagnostics are rebased to the USER's source.
|
|
1278
|
-
const source = buildCompositeShader({ wgsl, paramsDecl })
|
|
1346
|
+
const source = buildCompositeShader({ wgsl, paramsDecl, hasBackground, hasForeground })
|
|
1279
1347
|
const userLineOffset = source.slice(0, source.indexOf(wgsl)).split("\n").length - 1
|
|
1280
1348
|
this.device.pushErrorScope("validation")
|
|
1281
|
-
const module = this.device.createShaderModule({ label: "composite shader (
|
|
1349
|
+
const module = this.device.createShaderModule({ label: "composite shader (effect)", code: source })
|
|
1282
1350
|
const info = await module.getCompilationInfo()
|
|
1283
1351
|
const scopeErr = await this.device.popErrorScope()
|
|
1284
1352
|
const diagnostics = info.messages
|
|
1285
1353
|
.filter((m) => m.type === "error")
|
|
1286
1354
|
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`)
|
|
1287
1355
|
if (diagnostics.length === 0 && scopeErr) diagnostics.push(scopeErr.message)
|
|
1288
|
-
if (diagnostics.length > 0) return { ok: false, diagnostics }
|
|
1356
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics, mounts }
|
|
1289
1357
|
let identity: GPURenderPipeline
|
|
1290
1358
|
let gamma: GPURenderPipeline
|
|
1291
1359
|
try {
|
|
@@ -1303,37 +1371,42 @@ export class Engine {
|
|
|
1303
1371
|
primitive: { topology: "triangle-list" },
|
|
1304
1372
|
})
|
|
1305
1373
|
;[identity, gamma] = await Promise.all([
|
|
1306
|
-
make(false, "composite pipeline (
|
|
1307
|
-
make(true, "composite pipeline (
|
|
1374
|
+
make(false, "composite pipeline (effect, gamma=1)"),
|
|
1375
|
+
make(true, "composite pipeline (effect, gamma!=1)"),
|
|
1308
1376
|
])
|
|
1309
1377
|
} catch (e) {
|
|
1310
|
-
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] }
|
|
1378
|
+
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)], mounts }
|
|
1311
1379
|
}
|
|
1312
1380
|
|
|
1313
1381
|
// ── Swap — only now does the old effect (and its params buffer) go away.
|
|
1314
|
-
this.
|
|
1382
|
+
this.effect?.paramsBuffer?.destroy()
|
|
1315
1383
|
let paramsBuffer: GPUBuffer | null = null
|
|
1316
1384
|
if (entries.length) {
|
|
1317
1385
|
paramsBuffer = this.device.createBuffer({
|
|
1318
|
-
label: "
|
|
1386
|
+
label: "effect params",
|
|
1319
1387
|
size: paramsData.byteLength,
|
|
1320
1388
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1321
1389
|
})
|
|
1322
1390
|
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData)
|
|
1323
1391
|
}
|
|
1324
|
-
this.
|
|
1392
|
+
this.effect = { wgsl, paramLayout: layout, paramsBuffer, paramsData, hasBackground, hasForeground }
|
|
1325
1393
|
this.compositePipelineIdentity = identity
|
|
1326
1394
|
this.compositePipelineGamma = gamma
|
|
1327
|
-
this.
|
|
1395
|
+
this.effectEpochMs = performance.now()
|
|
1328
1396
|
this.rebuildCompositeBindGroup()
|
|
1329
1397
|
this.writeCompositeViewUniforms()
|
|
1330
|
-
return { ok: true, diagnostics: [] }
|
|
1398
|
+
return { ok: true, diagnostics: [], mounts }
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** Which mounts the installed effect declared. Both false when none is set. */
|
|
1402
|
+
getEffectMounts(): { background: boolean; foreground: boolean } {
|
|
1403
|
+
return { background: this.effect?.hasBackground ?? false, foreground: this.effect?.hasForeground ?? false }
|
|
1331
1404
|
}
|
|
1332
1405
|
|
|
1333
|
-
/** Write one
|
|
1334
|
-
*
|
|
1335
|
-
|
|
1336
|
-
const fx = this.
|
|
1406
|
+
/** Write one effect param (declared at setEffect) — a uniform write, no
|
|
1407
|
+
* recompile; the instant tier, like setStyleParam. */
|
|
1408
|
+
setEffectParam(name: string, value: EffectParamValue): void {
|
|
1409
|
+
const fx = this.effect
|
|
1337
1410
|
if (!fx || !fx.paramsBuffer) return
|
|
1338
1411
|
const slot = fx.paramLayout.get(name)
|
|
1339
1412
|
if (!slot) return
|
|
@@ -1407,7 +1480,10 @@ export class Engine {
|
|
|
1407
1480
|
if (!this.device || !this.dofUniformBuffer) return
|
|
1408
1481
|
const d = this.depthOfField
|
|
1409
1482
|
const u = this.dofUniformData
|
|
1410
|
-
|
|
1483
|
+
// `d.enabled &&`, because a foreground effect also drives this write (for
|
|
1484
|
+
// projA/projB alone) and auto-focus walks every visible character's bones —
|
|
1485
|
+
// work nothing would read with the gather switched off.
|
|
1486
|
+
const auto = d.enabled && d.focusMode === "auto" ? this.getModelBodyFocus() : null
|
|
1411
1487
|
u[0] = d.enabled ? 1 : 0
|
|
1412
1488
|
u[1] = auto?.distance ?? Math.max(d.focusDistance, 0.05)
|
|
1413
1489
|
// In auto mode the authored range is a floor — the sharp band never cuts
|
|
@@ -1591,6 +1667,37 @@ export class Engine {
|
|
|
1591
1667
|
// smooth gradients) while still passing through every anchor (look preserved) and staying
|
|
1592
1668
|
// monotone (no tonemap overshoot/ringing). Domain is uniform in log2 space: anchor k sits
|
|
1593
1669
|
// at t=k, k=0..13 (t = log2(linear)+10). See composite.ts::filmic for the sampling map.
|
|
1670
|
+
/**
|
|
1671
|
+
* Decompress and upload Blender's AgX cube.
|
|
1672
|
+
*
|
|
1673
|
+
* Deliberately off the critical path: it is 723 KB once inflated, and a frame
|
|
1674
|
+
* rendered before it lands should show the scene under whatever transform is
|
|
1675
|
+
* already there rather than wait. Until then binding 10 holds a 1×1×1 stand-in,
|
|
1676
|
+
* which is only ever sampled if someone selects AgX in that window.
|
|
1677
|
+
*/
|
|
1678
|
+
private async loadAgxLut(): Promise<void> {
|
|
1679
|
+
try {
|
|
1680
|
+
const packed = Uint8Array.from(atob(AGX_LUT_GZ), (ch) => ch.charCodeAt(0))
|
|
1681
|
+
const stream = new Blob([packed]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
1682
|
+
const bytes = new Uint8Array(await new Response(stream).arrayBuffer())
|
|
1683
|
+
const n = AGX_LUT_SIZE
|
|
1684
|
+
if (bytes.byteLength !== n * n * n * 4) throw new Error(`AgX LUT is ${bytes.byteLength} bytes, expected ${n ** 3 * 4}`)
|
|
1685
|
+
const tex = this.device.createTexture({
|
|
1686
|
+
label: "AgX 57³ LUT",
|
|
1687
|
+
size: [n, n, n],
|
|
1688
|
+
dimension: "3d",
|
|
1689
|
+
format: "rgb10a2unorm",
|
|
1690
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
1691
|
+
})
|
|
1692
|
+
// .cube order is red fastest, which is exactly a 3D texture's own layout.
|
|
1693
|
+
this.device.queue.writeTexture({ texture: tex }, bytes, { bytesPerRow: n * 4, rowsPerImage: n }, [n, n, n])
|
|
1694
|
+
this.agxLutTexture = tex
|
|
1695
|
+
this.rebuildCompositeBindGroup()
|
|
1696
|
+
} catch {
|
|
1697
|
+
// A missing LUT costs AgX, not the renderer — the other transforms stand.
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1594
1701
|
private bakeFilmicLut() {
|
|
1595
1702
|
const anchors = [
|
|
1596
1703
|
0.0028, 0.0068, 0.0151, 0.0313, 0.061, 0.112, 0.192, 0.306, 0.459, 0.631, 0.82, 0.907, 0.962, 0.989,
|
|
@@ -1864,6 +1971,13 @@ export class Engine {
|
|
|
1864
1971
|
// StyleUniforms for compiled graph shaders (adjust-tier sliders). Hand-written
|
|
1865
1972
|
// presets simply don't declare it — a layout may carry bindings a shader ignores.
|
|
1866
1973
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1974
|
+
// Style-group image maps. A PMX material carries one image; a
|
|
1975
|
+
// Blender-authored look needs a lightmap or ramp beside it, and those
|
|
1976
|
+
// belong to the GROUP rather than to the model's own material data.
|
|
1977
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1978
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1979
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1980
|
+
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1867
1981
|
],
|
|
1868
1982
|
})
|
|
1869
1983
|
|
|
@@ -1996,6 +2110,14 @@ export class Engine {
|
|
|
1996
2110
|
|
|
1997
2111
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
1998
2112
|
this.bakeBrdfLut()
|
|
2113
|
+
this.agxFallbackTexture = this.device.createTexture({
|
|
2114
|
+
label: "AgX LUT fallback",
|
|
2115
|
+
size: [1, 1, 1],
|
|
2116
|
+
dimension: "3d",
|
|
2117
|
+
format: "rgb10a2unorm",
|
|
2118
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
2119
|
+
})
|
|
2120
|
+
void this.loadAgxLut()
|
|
1999
2121
|
this.bakeFilmicLut()
|
|
2000
2122
|
|
|
2001
2123
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
@@ -2286,11 +2408,12 @@ export class Engine {
|
|
|
2286
2408
|
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
2287
2409
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
2288
2410
|
label: "composite view uniforms",
|
|
2289
|
-
//
|
|
2411
|
+
// 11 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
2290
2412
|
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
|
|
2291
|
-
// (time, _, canvas width, canvas height) for user
|
|
2292
|
-
//
|
|
2293
|
-
|
|
2413
|
+
// (time, _, canvas width, canvas height) for user effects · three grade
|
|
2414
|
+
// vectors (CDL offset+contrast, power+saturation, slope+flag) · camera
|
|
2415
|
+
// world position, for an effect placing itself in the scene.
|
|
2416
|
+
size: 176,
|
|
2294
2417
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2295
2418
|
})
|
|
2296
2419
|
this.dofUniformBuffer = this.device.createBuffer({
|
|
@@ -2330,6 +2453,9 @@ export class Engine {
|
|
|
2330
2453
|
texture: { sampleType: "depth", viewDimension: "2d", multisampled: true },
|
|
2331
2454
|
},
|
|
2332
2455
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
2456
|
+
// AgX's 57³ cube. Decompressed and uploaded off the critical path, so a
|
|
2457
|
+
// 1×1×1 stand-in keeps the bind group valid until it arrives.
|
|
2458
|
+
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: "3d" } },
|
|
2333
2459
|
],
|
|
2334
2460
|
})
|
|
2335
2461
|
this.fallbackEquirectTexture = this.device.createTexture({
|
|
@@ -3010,6 +3136,13 @@ export class Engine {
|
|
|
3010
3136
|
return this.cameraAnimation !== null
|
|
3011
3137
|
}
|
|
3012
3138
|
|
|
3139
|
+
/** Seconds the loaded camera VMD runs for — its last keyframe — or 0 with none
|
|
3140
|
+
* loaded. A timeline cannot draw a lane to scale without it, and the camera's
|
|
3141
|
+
* length is its own: it does not have to match any model's clip. */
|
|
3142
|
+
getCameraVmdDuration(): number {
|
|
3143
|
+
return this.cameraAnimation?.duration ?? 0
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3013
3146
|
/** Drop the loaded camera VMD and return to orbit control. */
|
|
3014
3147
|
clearCameraVmd(): void {
|
|
3015
3148
|
this.cameraAnimation = null
|
|
@@ -3238,7 +3371,7 @@ export class Engine {
|
|
|
3238
3371
|
// explicit destroy). Per-model group buffers are torn down in removeModel; the shared
|
|
3239
3372
|
// zero buffer is engine-owned.
|
|
3240
3373
|
this.forEachInstance((inst) => {
|
|
3241
|
-
for (const install of inst.styleGroups.values())
|
|
3374
|
+
for (const install of inst.styleGroups.values()) this.destroyInstall(install)
|
|
3242
3375
|
inst.styleGroups.clear()
|
|
3243
3376
|
})
|
|
3244
3377
|
this.zeroStyleBuffer?.destroy()
|
|
@@ -3387,7 +3520,7 @@ export class Engine {
|
|
|
3387
3520
|
buf.destroy()
|
|
3388
3521
|
}
|
|
3389
3522
|
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
3390
|
-
for (const install of inst.styleGroups.values())
|
|
3523
|
+
for (const install of inst.styleGroups.values()) this.destroyInstall(install)
|
|
3391
3524
|
this.modelInstances.delete(name)
|
|
3392
3525
|
}
|
|
3393
3526
|
|
|
@@ -3469,11 +3602,58 @@ export class Engine {
|
|
|
3469
3602
|
|
|
3470
3603
|
// Build a material's bind group with binding(4) pointing at a given StyleUniforms buffer
|
|
3471
3604
|
// (the group's buffer when grouped, or the shared zero buffer when ungrouped).
|
|
3472
|
-
|
|
3605
|
+
/** A group's uniform buffer and its maps have the same lifetime — freeing one
|
|
3606
|
+
* without the other is how a re-apply leaks GPU memory a frame at a time. */
|
|
3607
|
+
private destroyInstall(install: GroupInstall): void {
|
|
3608
|
+
install.uniformBuffer.destroy()
|
|
3609
|
+
for (const tex of install.images ?? []) tex?.destroy()
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
/** Upload a group's image maps. Sources are decoded images the host already
|
|
3613
|
+
* holds; the engine never fetches, matching how models and motions arrive. */
|
|
3614
|
+
private uploadGroupImages(group: StyleGroup): (GPUTexture | null)[] | undefined {
|
|
3615
|
+
if (!group.images?.length) return undefined
|
|
3616
|
+
return group.images.slice(0, 4).map((entry) => {
|
|
3617
|
+
if (!entry) return null
|
|
3618
|
+
const wrapped = "source" in entry
|
|
3619
|
+
const src = wrapped ? entry.source : entry
|
|
3620
|
+
const width = Math.max(1, "naturalWidth" in src ? src.naturalWidth : src.width)
|
|
3621
|
+
const height = Math.max(1, "naturalHeight" in src ? src.naturalHeight : src.height)
|
|
3622
|
+
const tex = this.device.createTexture({
|
|
3623
|
+
label: `group map: ${group.id}`,
|
|
3624
|
+
size: [width, height],
|
|
3625
|
+
// Colour maps decode to linear on sample, the way material textures do;
|
|
3626
|
+
// data maps must not, or every threshold packed in their channels moves.
|
|
3627
|
+
format: wrapped && entry.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
3628
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
3629
|
+
})
|
|
3630
|
+
this.device.queue.copyExternalImageToTexture(
|
|
3631
|
+
{ source: src },
|
|
3632
|
+
{ texture: tex, premultipliedAlpha: wrapped && entry.premultiplied === true },
|
|
3633
|
+
[width, height],
|
|
3634
|
+
)
|
|
3635
|
+
return tex
|
|
3636
|
+
})
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
private createMaterialBindGroup(
|
|
3640
|
+
label: string,
|
|
3641
|
+
baseEntries: GPUBindGroupEntry[],
|
|
3642
|
+
styleBuffer: GPUBuffer,
|
|
3643
|
+
groupImages?: (GPUTexture | null)[],
|
|
3644
|
+
): GPUBindGroup {
|
|
3645
|
+
// Every material bind group in the engine is built here, which is why the
|
|
3646
|
+
// group's maps are threaded through this one function rather than patched in
|
|
3647
|
+
// at each call site — an unset slot reads white, never stale.
|
|
3648
|
+
const slots: GPUBindGroupEntry[] = []
|
|
3649
|
+
for (let i = 0; i < 4; i++) {
|
|
3650
|
+
const tex = groupImages?.[i] ?? this.fallbackMaterialTexture
|
|
3651
|
+
slots.push({ binding: 5 + i, resource: tex.createView() })
|
|
3652
|
+
}
|
|
3473
3653
|
return this.device.createBindGroup({
|
|
3474
3654
|
label,
|
|
3475
3655
|
layout: this.mainPerMaterialBindGroupLayout,
|
|
3476
|
-
entries: [...baseEntries, { binding: 4, resource: { buffer: styleBuffer } }],
|
|
3656
|
+
entries: [...baseEntries, { binding: 4, resource: { buffer: styleBuffer } }, ...slots],
|
|
3477
3657
|
})
|
|
3478
3658
|
}
|
|
3479
3659
|
|
|
@@ -5213,12 +5393,20 @@ export class Engine {
|
|
|
5213
5393
|
this.updateShadowLightVP()
|
|
5214
5394
|
|
|
5215
5395
|
// Depth of field's entire disabled cost is this branch: depth stays in
|
|
5216
|
-
// TBDR tile memory (discard) unless the composite
|
|
5217
|
-
// frame.
|
|
5218
|
-
//
|
|
5396
|
+
// TBDR tile memory (discard) unless something in the composite reads it this
|
|
5397
|
+
// frame. Two things can — the DoF gather, and the depth handed to a
|
|
5398
|
+
// foreground effect — and either one makes the pass store it.
|
|
5399
|
+
//
|
|
5400
|
+
// The uniform refresh is shared for the same reason: linearDepth() inverts
|
|
5401
|
+
// the z-buffer with projA/projB out of dofU[2], which track the camera's
|
|
5402
|
+
// near/far and so must be rewritten every frame either reader is live. A
|
|
5403
|
+
// foreground with a stale pair would read metres from the wrong frustum. The
|
|
5404
|
+
// write leaves dofU[0].x at 0 while DoF is off, so refreshing it does not
|
|
5405
|
+
// switch the gather on.
|
|
5219
5406
|
const dofOn = this.depthOfField.enabled
|
|
5220
|
-
|
|
5221
|
-
|
|
5407
|
+
const depthRead = dofOn || (this.effect?.hasForeground ?? false)
|
|
5408
|
+
this.renderPassDescriptor.depthStencilAttachment!.depthStoreOp = depthRead ? "store" : "discard"
|
|
5409
|
+
if (depthRead) this.writeDepthOfFieldUniforms()
|
|
5222
5410
|
|
|
5223
5411
|
const encoder = this.device.createCommandEncoder()
|
|
5224
5412
|
|
|
@@ -5437,7 +5625,7 @@ export class Engine {
|
|
|
5437
5625
|
for (const [id, install] of inst.styleGroups) {
|
|
5438
5626
|
if (!nextIds.has(id)) {
|
|
5439
5627
|
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1)
|
|
5440
|
-
|
|
5628
|
+
this.destroyInstall(install)
|
|
5441
5629
|
inst.styleGroups.delete(id)
|
|
5442
5630
|
}
|
|
5443
5631
|
}
|
|
@@ -5474,7 +5662,7 @@ export class Engine {
|
|
|
5474
5662
|
const install = inst?.styleGroups.get(groupId)
|
|
5475
5663
|
if (!inst || !install) return
|
|
5476
5664
|
inst.styleGroupGen.set(groupId, (inst.styleGroupGen.get(groupId) ?? 0) + 1) // discard in-flight compile
|
|
5477
|
-
|
|
5665
|
+
this.destroyInstall(install)
|
|
5478
5666
|
inst.styleGroups.delete(groupId)
|
|
5479
5667
|
this.assignDrawCallGroups(inst, this.currentClaims(inst))
|
|
5480
5668
|
}
|
|
@@ -5485,7 +5673,7 @@ export class Engine {
|
|
|
5485
5673
|
if (!inst) return
|
|
5486
5674
|
for (const [id, install] of inst.styleGroups) {
|
|
5487
5675
|
inst.styleGroupGen.set(id, (inst.styleGroupGen.get(id) ?? 0) + 1)
|
|
5488
|
-
|
|
5676
|
+
this.destroyInstall(install)
|
|
5489
5677
|
}
|
|
5490
5678
|
inst.styleGroups.clear()
|
|
5491
5679
|
this.assignDrawCallGroups(inst, new Map())
|
|
@@ -5585,6 +5773,9 @@ export class Engine {
|
|
|
5585
5773
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
5586
5774
|
})
|
|
5587
5775
|
|
|
5776
|
+
// The outgoing install's maps go with it — a re-apply that changes images
|
|
5777
|
+
// would otherwise strand the old textures for the life of the model.
|
|
5778
|
+
const previousImages = inst.styleGroups.get(group.id)?.images
|
|
5588
5779
|
inst.styleGroups.set(group.id, {
|
|
5589
5780
|
group,
|
|
5590
5781
|
renderClass,
|
|
@@ -5593,9 +5784,31 @@ export class Engine {
|
|
|
5593
5784
|
pipelineNoDepthWrite,
|
|
5594
5785
|
overEyesPipeline,
|
|
5595
5786
|
uniformBuffer,
|
|
5787
|
+
images: this.uploadGroupImages(group),
|
|
5596
5788
|
slotMap: result.slotMap,
|
|
5597
5789
|
signature,
|
|
5598
5790
|
})
|
|
5791
|
+
// Rebind this group's draw calls before the old textures go.
|
|
5792
|
+
//
|
|
5793
|
+
// assignDrawCallGroups only rebuilds a bind group when a material CHANGES
|
|
5794
|
+
// group, which is the wrong test here: swapping the graph on a group a
|
|
5795
|
+
// material already belongs to leaves its id alone while replacing the maps
|
|
5796
|
+
// underneath it. The draw call then kept a bind group holding the outgoing
|
|
5797
|
+
// textures — destroyed on the next line — so a graph swap either sampled the
|
|
5798
|
+
// old maps, or the fallback white where the previous graph had none (which
|
|
5799
|
+
// reads as a blown-out white material through any screen or add), or tripped
|
|
5800
|
+
// a validation error on a destroyed texture. Only a reload cleared it.
|
|
5801
|
+
const install = inst.styleGroups.get(group.id)
|
|
5802
|
+
for (const dc of inst.drawCalls) {
|
|
5803
|
+
if (!dc.baseBindGroupEntries || dc.groupId !== group.id) continue
|
|
5804
|
+
dc.bindGroup = this.createMaterialBindGroup(
|
|
5805
|
+
`material: ${dc.materialName}`,
|
|
5806
|
+
dc.baseBindGroupEntries,
|
|
5807
|
+
uniformBuffer,
|
|
5808
|
+
install?.images,
|
|
5809
|
+
)
|
|
5810
|
+
}
|
|
5811
|
+
for (const tex of previousImages ?? []) tex?.destroy()
|
|
5599
5812
|
this.writeGroupDefaults(uniformBuffer, group, result.slotMap)
|
|
5600
5813
|
return { ok: true, diagnostics, slotMap: result.slotMap }
|
|
5601
5814
|
}
|
|
@@ -5616,6 +5829,7 @@ export class Engine {
|
|
|
5616
5829
|
`material: ${dc.materialName}`,
|
|
5617
5830
|
dc.baseBindGroupEntries,
|
|
5618
5831
|
install ? install.uniformBuffer : this.zeroStyleBuffer,
|
|
5832
|
+
install?.images,
|
|
5619
5833
|
)
|
|
5620
5834
|
}
|
|
5621
5835
|
this.sortDrawCalls(inst)
|
|
@@ -5881,7 +6095,7 @@ export class Engine {
|
|
|
5881
6095
|
// is LEFT-HANDED (+Z forward, see Mat4.lookAtInto), so the world-space
|
|
5882
6096
|
// right/up/FORWARD vectors are rows 0/1/2 of its rotation block directly
|
|
5883
6097
|
// (column-major storage: row i = values[i], values[i+4], values[i+8]).
|
|
5884
|
-
if ((this.backdropEquirectView || this.
|
|
6098
|
+
if ((this.backdropEquirectView || this.effect) && this.compositeUniformBuffer) {
|
|
5885
6099
|
const v = viewMatrix.values
|
|
5886
6100
|
const u = this.compositeUniformData
|
|
5887
6101
|
const tanHalf = Math.tan((this.camera.fov ?? Math.PI / 4) / 2)
|
|
@@ -5899,9 +6113,15 @@ export class Engine {
|
|
|
5899
6113
|
u[22] = v[10]
|
|
5900
6114
|
u[23] = 0
|
|
5901
6115
|
// Effect clock + canvas size (viewU[6]) — written on the same refresh.
|
|
5902
|
-
u[24] = (performance.now() - this.
|
|
6116
|
+
u[24] = (performance.now() - this.effectEpochMs) / 1000
|
|
5903
6117
|
u[26] = this.canvas.width
|
|
5904
6118
|
u[27] = this.canvas.height
|
|
6119
|
+
// Camera world position (viewU[10]) — the other half of bgWorldPos. It
|
|
6120
|
+
// rides this refresh rather than writeCompositeViewUniforms because it
|
|
6121
|
+
// changes every frame the camera does, exactly like the basis above.
|
|
6122
|
+
u[40] = cameraPos.x
|
|
6123
|
+
u[41] = cameraPos.y
|
|
6124
|
+
u[42] = cameraPos.z
|
|
5905
6125
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u)
|
|
5906
6126
|
}
|
|
5907
6127
|
}
|