reze-engine 0.24.1 → 0.25.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 +5 -1
- package/dist/engine.d.ts +127 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +422 -34
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -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 +143 -141
- package/dist/shaders/passes/composite.d.ts +23 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +62 -16
- package/dist/shaders/passes/depth-prepass.d.ts +2 -0
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -0
- package/dist/shaders/passes/depth-prepass.js +56 -0
- package/dist/shaders/passes/ground.d.ts +1 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +8 -0
- package/package.json +1 -1
- package/src/engine.ts +459 -34
- package/src/graph/slots.ts +11 -2
- package/src/index.ts +2 -0
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/composite.ts +89 -16
- package/src/shaders/passes/depth-prepass.ts +57 -0
- package/src/shaders/passes/ground.ts +8 -0
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/dist/engine.js
CHANGED
|
@@ -12,10 +12,11 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
|
|
|
12
12
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
|
|
13
13
|
import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground";
|
|
14
14
|
import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline";
|
|
15
|
+
import { TRANSPARENT_DEPTH_PREPASS_WGSL } from "./shaders/passes/depth-prepass";
|
|
15
16
|
import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection";
|
|
16
17
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
17
18
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
18
|
-
import {
|
|
19
|
+
import { buildCompositeShader } from "./shaders/passes/composite";
|
|
19
20
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick";
|
|
20
21
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap";
|
|
21
22
|
import { compileGraph } from "./graph/compile";
|
|
@@ -147,6 +148,91 @@ export const DEFAULT_ENGINE_OPTIONS = {
|
|
|
147
148
|
camera: { distance: 26.6, target: new Vec3(0, 12.5, 0), fov: Math.PI / 4 },
|
|
148
149
|
onRaycast: undefined,
|
|
149
150
|
};
|
|
151
|
+
// ── Sheer-material detection ──────────────────────────────────────────────────
|
|
152
|
+
// PMX carries no "translucent" flag: a see-through veil usually has diffuse
|
|
153
|
+
// alpha 1.0 and does its transparency entirely in the TEXTURE's alpha channel.
|
|
154
|
+
// Classifying by material alpha alone put such cloth in the opaque bucket,
|
|
155
|
+
// where it draws in PMX order with depth writes — anything the engine draws
|
|
156
|
+
// after it (the hair render-class draws LAST for the eye-stencil effect) got
|
|
157
|
+
// depth-rejected behind the veil, so you saw the body through it but not the
|
|
158
|
+
// hair. These helpers measure a material's real coverage by sampling the
|
|
159
|
+
// texture's alpha at the material's own triangle CENTROIDS — centroids, not
|
|
160
|
+
// vertices, because hair-card corners sit in transparent texture margins and
|
|
161
|
+
// vertex sampling would misclassify hair (which must stay opaque-bucket for
|
|
162
|
+
// stencil interplay and shadows).
|
|
163
|
+
/** Downsampled alpha plane of a decoded texture (≤128², nearest-sampled). */
|
|
164
|
+
function buildAlphaSampler(source, rgba, width, height) {
|
|
165
|
+
try {
|
|
166
|
+
const w = Math.max(1, Math.min(128, width));
|
|
167
|
+
const h = Math.max(1, Math.min(128, height));
|
|
168
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
169
|
+
const cx = canvas.getContext("2d", { willReadFrequently: true });
|
|
170
|
+
if (!cx)
|
|
171
|
+
return null;
|
|
172
|
+
if (source) {
|
|
173
|
+
cx.drawImage(source, 0, 0, w, h);
|
|
174
|
+
}
|
|
175
|
+
else if (rgba) {
|
|
176
|
+
const tmp = new OffscreenCanvas(width, height);
|
|
177
|
+
const tcx = tmp.getContext("2d");
|
|
178
|
+
if (!tcx)
|
|
179
|
+
return null;
|
|
180
|
+
tcx.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
|
|
181
|
+
cx.drawImage(tmp, 0, 0, w, h);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const img = cx.getImageData(0, 0, w, h).data;
|
|
187
|
+
const a = new Uint8ClampedArray(w * h);
|
|
188
|
+
for (let i = 0; i < w * h; i++)
|
|
189
|
+
a[i] = img[i * 4 + 3];
|
|
190
|
+
return { a, w, h };
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
197
|
+
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
198
|
+
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
199
|
+
* they classify two distinct things:
|
|
200
|
+
* sheer (avg low) — a veil: mostly see-through everywhere
|
|
201
|
+
* partial (translucentFrac up) — mostly-opaque cloth with sheer REGIONS,
|
|
202
|
+
* e.g. a lace skirt panel
|
|
203
|
+
* Both route to the transparent bucket; only `sheer` is excluded from the
|
|
204
|
+
* shadow map and outline pass. */
|
|
205
|
+
const SHEER_ALPHA_THRESHOLD = 0.7;
|
|
206
|
+
const PARTIAL_TRANSLUCENT_FRAC = 0.15;
|
|
207
|
+
function materialAlphaStats(verts, indices, firstIndex, count, sampler) {
|
|
208
|
+
if (!sampler)
|
|
209
|
+
return { avg: 1, translucentFrac: 0 };
|
|
210
|
+
const triCount = Math.floor(count / 3);
|
|
211
|
+
if (triCount === 0)
|
|
212
|
+
return { avg: 1, translucentFrac: 0 };
|
|
213
|
+
const step = Math.max(1, Math.floor(triCount / 400));
|
|
214
|
+
let sum = 0;
|
|
215
|
+
let translucent = 0;
|
|
216
|
+
let n = 0;
|
|
217
|
+
for (let t = 0; t < triCount; t += step) {
|
|
218
|
+
const i0 = indices[firstIndex + t * 3];
|
|
219
|
+
const i1 = indices[firstIndex + t * 3 + 1];
|
|
220
|
+
const i2 = indices[firstIndex + t * 3 + 2];
|
|
221
|
+
const u = (verts[i0 * 8 + 6] + verts[i1 * 8 + 6] + verts[i2 * 8 + 6]) / 3;
|
|
222
|
+
const v = (verts[i0 * 8 + 7] + verts[i1 * 8 + 7] + verts[i2 * 8 + 7]) / 3;
|
|
223
|
+
// Wrap (MMD UVs may tile), then nearest-sample the downsampled plane.
|
|
224
|
+
const x = Math.min(sampler.w - 1, Math.max(0, Math.floor((u - Math.floor(u)) * sampler.w)));
|
|
225
|
+
const y = Math.min(sampler.h - 1, Math.max(0, Math.floor((v - Math.floor(v)) * sampler.h)));
|
|
226
|
+
const a = sampler.a[y * sampler.w + x];
|
|
227
|
+
sum += a;
|
|
228
|
+
if (a > 8 && a < 247)
|
|
229
|
+
translucent++;
|
|
230
|
+
n++;
|
|
231
|
+
}
|
|
232
|
+
if (n === 0)
|
|
233
|
+
return { avg: 1, translucentFrac: 0 };
|
|
234
|
+
return { avg: sum / n / 255, translucentFrac: translucent / n };
|
|
235
|
+
}
|
|
150
236
|
export class Engine {
|
|
151
237
|
static getInstance() {
|
|
152
238
|
if (!Engine.instance) {
|
|
@@ -188,12 +274,19 @@ export class Engine {
|
|
|
188
274
|
// unchanged).
|
|
189
275
|
this.hdrFormat = "rgba16float";
|
|
190
276
|
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
191
|
-
this.compositeUniformData = new Float32Array(
|
|
277
|
+
this.compositeUniformData = new Float32Array(28);
|
|
192
278
|
/** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
|
|
193
279
|
this.backgroundColor = null;
|
|
194
280
|
// 360 backdrop (equirectangular skybox, sampled by view ray in composite).
|
|
195
281
|
this.backdropEquirectTexture = null;
|
|
196
282
|
this.backdropEquirectView = null;
|
|
283
|
+
// User WGSL background effect (background mode 3, setBackgroundEffect). The
|
|
284
|
+
// composite pipelines are REBUILT with the user code injected; params live in
|
|
285
|
+
// their own uniform buffer so setBackgroundEffectParam is a write, not a
|
|
286
|
+
// recompile (the same instant tier as setStyleParam).
|
|
287
|
+
this.backgroundEffect = null;
|
|
288
|
+
/** time=0 origin for the active effect — reset each setBackgroundEffect. */
|
|
289
|
+
this.bgEffectEpochMs = 0;
|
|
197
290
|
this.compositeBloomView = null;
|
|
198
291
|
this.bloomBlitUniformData = new Float32Array(4);
|
|
199
292
|
this.bloomUpsampleUniformData = new Float32Array(4);
|
|
@@ -210,6 +303,11 @@ export class Engine {
|
|
|
210
303
|
this.pendingPick = null;
|
|
211
304
|
this.modelInstances = new Map();
|
|
212
305
|
this.textureCache = new Map();
|
|
306
|
+
// Downsampled CPU alpha channel per texture (≤128², ~16KB) — kept so materials
|
|
307
|
+
// can be classified as SHEER at load by sampling alpha at their own UVs (the
|
|
308
|
+
// GPU texture can't be read back cheaply, and PMX diffuse alpha is usually 1.0
|
|
309
|
+
// even for see-through cloth: the translucency lives in the texture).
|
|
310
|
+
this.textureAlphaCache = new Map();
|
|
213
311
|
this.mipBlitPipeline = null;
|
|
214
312
|
this.mipBlitSampler = null;
|
|
215
313
|
this._nextDefaultModelId = 0;
|
|
@@ -239,6 +337,8 @@ export class Engine {
|
|
|
239
337
|
};
|
|
240
338
|
this.animationFrameId = null;
|
|
241
339
|
this.renderLoopCallback = null;
|
|
340
|
+
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
341
|
+
this.outlineEnabled = true;
|
|
242
342
|
/** When set, render resolution is pinned to this size instead of tracking the
|
|
243
343
|
* canvas's CSS size × devicePixelRatio (see setRenderSize). */
|
|
244
344
|
this.fixedRenderSize = null;
|
|
@@ -537,7 +637,12 @@ export class Engine {
|
|
|
537
637
|
u[8] = bg?.x ?? 0;
|
|
538
638
|
u[9] = bg?.y ?? 0;
|
|
539
639
|
u[10] = bg?.z ?? 0;
|
|
640
|
+
// Base-layer mode; a user effect is a separate LAYER flagged at u[25] and
|
|
641
|
+
// over-composited onto whichever base is active.
|
|
540
642
|
u[11] = this.backdropEquirectView ? 2 : bg ? 1 : 0;
|
|
643
|
+
u[25] = this.backgroundEffect ? 1 : 0;
|
|
644
|
+
u[26] = this.canvas.width;
|
|
645
|
+
u[27] = this.canvas.height;
|
|
541
646
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u);
|
|
542
647
|
}
|
|
543
648
|
/**
|
|
@@ -552,6 +657,9 @@ export class Engine {
|
|
|
552
657
|
if (this.device && this.compositeUniformBuffer)
|
|
553
658
|
this.writeCompositeViewUniforms();
|
|
554
659
|
}
|
|
660
|
+
setOutlineEnabled(on) {
|
|
661
|
+
this.outlineEnabled = on;
|
|
662
|
+
}
|
|
555
663
|
rebuildCompositeBindGroup() {
|
|
556
664
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView)
|
|
557
665
|
return;
|
|
@@ -566,6 +674,7 @@ export class Engine {
|
|
|
566
674
|
{ binding: 4, resource: this.maskResolveView },
|
|
567
675
|
{ binding: 5, resource: this.filmicLutView },
|
|
568
676
|
{ binding: 6, resource: this.backdropEquirectView ?? this.fallbackEquirectView },
|
|
677
|
+
{ binding: 7, resource: { buffer: this.backgroundEffect?.paramsBuffer ?? this.bgParamsDummyBuffer } },
|
|
569
678
|
],
|
|
570
679
|
});
|
|
571
680
|
}
|
|
@@ -613,6 +722,165 @@ export class Engine {
|
|
|
613
722
|
if (this.device && this.compositeUniformBuffer)
|
|
614
723
|
this.writeCompositeViewUniforms();
|
|
615
724
|
}
|
|
725
|
+
makeCompositePipeline(module, applyGamma, label) {
|
|
726
|
+
return this.device.createRenderPipeline({
|
|
727
|
+
label,
|
|
728
|
+
layout: this.compositePipelineLayout,
|
|
729
|
+
vertex: { module, entryPoint: "vs" },
|
|
730
|
+
fragment: {
|
|
731
|
+
module,
|
|
732
|
+
entryPoint: "fs",
|
|
733
|
+
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
734
|
+
targets: [{ format: this.presentationFormat }],
|
|
735
|
+
},
|
|
736
|
+
primitive: { topology: "triangle-list" },
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Install a WGSL background effect (shadertoy-style) as a LAYER between the
|
|
741
|
+
* base background and the scene: rendered per-pixel in the composite pass and
|
|
742
|
+
* over-composited onto whichever base is active (solid color, 360 equirect,
|
|
743
|
+
* or transparency) — its alpha lets the base show through, so a starfield is
|
|
744
|
+
* stars over the user's background color. Display-space: never affects
|
|
745
|
+
* lighting, bloom, or tonemapping, and is captured by offline export like any
|
|
746
|
+
* background.
|
|
747
|
+
*
|
|
748
|
+
* `wgsl` must define:
|
|
749
|
+
*
|
|
750
|
+
* fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
|
|
751
|
+
*
|
|
752
|
+
* where `ray` is the pixel's normalized world-space view direction (LH, +Z
|
|
753
|
+
* forward — what the skybox samples by), `uv` is 0..1 bottom-left origin,
|
|
754
|
+
* `time` is seconds since apply, and `bgResolution()` gives the canvas size.
|
|
755
|
+
* Return sRGB + alpha. Declared `params` arrive as `params.<name>` (number →
|
|
756
|
+
* f32, Vec3 → vec3f) and are later tweaked without recompiling via
|
|
757
|
+
* setBackgroundEffectParam.
|
|
758
|
+
*
|
|
759
|
+
* Compiles off the hot path (async pipelines): on failure the previous
|
|
760
|
+
* background is KEPT and diagnostics are returned with line numbers relative
|
|
761
|
+
* to the user's WGSL. Pass null to remove the effect.
|
|
762
|
+
*/
|
|
763
|
+
async setBackgroundEffect(wgsl, params) {
|
|
764
|
+
if (!this.device)
|
|
765
|
+
return { ok: false, diagnostics: ["setBackgroundEffect requires init() to have run"] };
|
|
766
|
+
if (wgsl === null) {
|
|
767
|
+
this.backgroundEffect?.paramsBuffer?.destroy();
|
|
768
|
+
this.backgroundEffect = null;
|
|
769
|
+
const module = this.device.createShaderModule({ label: "composite shader", code: buildCompositeShader(null) });
|
|
770
|
+
this.compositePipelineIdentity = this.makeCompositePipeline(module, false, "composite pipeline (gamma=1)");
|
|
771
|
+
this.compositePipelineGamma = this.makeCompositePipeline(module, true, "composite pipeline (gamma!=1)");
|
|
772
|
+
this.rebuildCompositeBindGroup();
|
|
773
|
+
this.writeCompositeViewUniforms();
|
|
774
|
+
return { ok: true, diagnostics: [] };
|
|
775
|
+
}
|
|
776
|
+
// ── Params: codegen a WGSL struct and mirror its uniform layout on the CPU.
|
|
777
|
+
// Fields are emitted in declaration order; offsets follow WGSL's natural
|
|
778
|
+
// uniform rules (f32 align 4, vec3f align 16 size 12), computed identically
|
|
779
|
+
// on both sides so no reordering is needed.
|
|
780
|
+
const entries = Object.entries(params ?? {});
|
|
781
|
+
const layout = new Map();
|
|
782
|
+
const fields = [];
|
|
783
|
+
let cursor = 0;
|
|
784
|
+
for (const [name, value] of entries) {
|
|
785
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
786
|
+
return { ok: false, diagnostics: [`invalid param name "${name}" (must be a WGSL identifier)`] };
|
|
787
|
+
}
|
|
788
|
+
const isVec = typeof value !== "number";
|
|
789
|
+
const align = isVec ? 16 : 4;
|
|
790
|
+
const offset = Math.ceil(cursor / align) * align;
|
|
791
|
+
layout.set(name, { offset: offset / 4, comps: isVec ? 3 : 1 });
|
|
792
|
+
fields.push(` ${name}: ${isVec ? "vec3f" : "f32"},`);
|
|
793
|
+
cursor = offset + (isVec ? 12 : 4);
|
|
794
|
+
}
|
|
795
|
+
const paramsData = new Float32Array(Math.max(4, Math.ceil(cursor / 16) * 4));
|
|
796
|
+
for (const [name, value] of entries) {
|
|
797
|
+
const slot = layout.get(name);
|
|
798
|
+
if (typeof value === "number")
|
|
799
|
+
paramsData[slot.offset] = value;
|
|
800
|
+
else {
|
|
801
|
+
paramsData[slot.offset] = value.x;
|
|
802
|
+
paramsData[slot.offset + 1] = value.y;
|
|
803
|
+
paramsData[slot.offset + 2] = value.z;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const paramsDecl = entries.length
|
|
807
|
+
? `struct BgParams {\n${fields.join("\n")}\n}\n@group(0) @binding(7) var<uniform> params: BgParams;\n`
|
|
808
|
+
: "";
|
|
809
|
+
// ── Compile with validation captured, not thrown at the console. Line
|
|
810
|
+
// numbers in diagnostics are rebased to the USER's source.
|
|
811
|
+
const source = buildCompositeShader({ wgsl, paramsDecl });
|
|
812
|
+
const userLineOffset = source.slice(0, source.indexOf(wgsl)).split("\n").length - 1;
|
|
813
|
+
this.device.pushErrorScope("validation");
|
|
814
|
+
const module = this.device.createShaderModule({ label: "composite shader (bg effect)", code: source });
|
|
815
|
+
const info = await module.getCompilationInfo();
|
|
816
|
+
const scopeErr = await this.device.popErrorScope();
|
|
817
|
+
const diagnostics = info.messages
|
|
818
|
+
.filter((m) => m.type === "error")
|
|
819
|
+
.map((m) => `${Math.max(0, m.lineNum - userLineOffset)}:${m.linePos} ${m.message}`);
|
|
820
|
+
if (diagnostics.length === 0 && scopeErr)
|
|
821
|
+
diagnostics.push(scopeErr.message);
|
|
822
|
+
if (diagnostics.length > 0)
|
|
823
|
+
return { ok: false, diagnostics };
|
|
824
|
+
let identity;
|
|
825
|
+
let gamma;
|
|
826
|
+
try {
|
|
827
|
+
const make = (applyGamma, label) => this.device.createRenderPipelineAsync({
|
|
828
|
+
label,
|
|
829
|
+
layout: this.compositePipelineLayout,
|
|
830
|
+
vertex: { module, entryPoint: "vs" },
|
|
831
|
+
fragment: {
|
|
832
|
+
module,
|
|
833
|
+
entryPoint: "fs",
|
|
834
|
+
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
835
|
+
targets: [{ format: this.presentationFormat }],
|
|
836
|
+
},
|
|
837
|
+
primitive: { topology: "triangle-list" },
|
|
838
|
+
});
|
|
839
|
+
[identity, gamma] = await Promise.all([
|
|
840
|
+
make(false, "composite pipeline (bg effect, gamma=1)"),
|
|
841
|
+
make(true, "composite pipeline (bg effect, gamma!=1)"),
|
|
842
|
+
]);
|
|
843
|
+
}
|
|
844
|
+
catch (e) {
|
|
845
|
+
return { ok: false, diagnostics: [e instanceof Error ? e.message : String(e)] };
|
|
846
|
+
}
|
|
847
|
+
// ── Swap — only now does the old effect (and its params buffer) go away.
|
|
848
|
+
this.backgroundEffect?.paramsBuffer?.destroy();
|
|
849
|
+
let paramsBuffer = null;
|
|
850
|
+
if (entries.length) {
|
|
851
|
+
paramsBuffer = this.device.createBuffer({
|
|
852
|
+
label: "bg effect params",
|
|
853
|
+
size: paramsData.byteLength,
|
|
854
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
855
|
+
});
|
|
856
|
+
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
857
|
+
}
|
|
858
|
+
this.backgroundEffect = { wgsl, paramLayout: layout, paramsBuffer, paramsData };
|
|
859
|
+
this.compositePipelineIdentity = identity;
|
|
860
|
+
this.compositePipelineGamma = gamma;
|
|
861
|
+
this.bgEffectEpochMs = performance.now();
|
|
862
|
+
this.rebuildCompositeBindGroup();
|
|
863
|
+
this.writeCompositeViewUniforms();
|
|
864
|
+
return { ok: true, diagnostics: [] };
|
|
865
|
+
}
|
|
866
|
+
/** Write one background-effect param (declared at setBackgroundEffect) — a
|
|
867
|
+
* uniform write, no recompile; the instant tier, like setStyleParam. */
|
|
868
|
+
setBackgroundEffectParam(name, value) {
|
|
869
|
+
const fx = this.backgroundEffect;
|
|
870
|
+
if (!fx || !fx.paramsBuffer)
|
|
871
|
+
return;
|
|
872
|
+
const slot = fx.paramLayout.get(name);
|
|
873
|
+
if (!slot)
|
|
874
|
+
return;
|
|
875
|
+
if (typeof value === "number")
|
|
876
|
+
fx.paramsData[slot.offset] = value;
|
|
877
|
+
else {
|
|
878
|
+
fx.paramsData[slot.offset] = value.x;
|
|
879
|
+
fx.paramsData[slot.offset + 1] = value.y;
|
|
880
|
+
fx.paramsData[slot.offset + 2] = value.z;
|
|
881
|
+
}
|
|
882
|
+
this.device.queue.writeBuffer(fx.paramsBuffer, 0, fx.paramsData);
|
|
883
|
+
}
|
|
616
884
|
/** Patch bloom; GPU uniforms update immediately if `init()` has run. */
|
|
617
885
|
setBloomOptions(patch) {
|
|
618
886
|
const b = this.bloomSettings;
|
|
@@ -1052,6 +1320,44 @@ export class Engine {
|
|
|
1052
1320
|
depthCompare: "less-equal",
|
|
1053
1321
|
},
|
|
1054
1322
|
});
|
|
1323
|
+
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
1324
|
+
this.neutralPipelineNoDepthWrite = this.createRenderPipeline({
|
|
1325
|
+
label: "neutral base pipeline (no depth write)",
|
|
1326
|
+
layout: mainPipelineLayout,
|
|
1327
|
+
shaderModule: neutralModule,
|
|
1328
|
+
vertexBuffers: fullVertexBuffers,
|
|
1329
|
+
fragmentTargets: sceneTargets,
|
|
1330
|
+
cullMode: "none",
|
|
1331
|
+
depthStencil: {
|
|
1332
|
+
format: "depth24plus-stencil8",
|
|
1333
|
+
depthWriteEnabled: false,
|
|
1334
|
+
depthCompare: "less-equal",
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
1338
|
+
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
1339
|
+
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
1340
|
+
const prepassModule = this.device.createShaderModule({
|
|
1341
|
+
label: "transparent depth prepass",
|
|
1342
|
+
code: TRANSPARENT_DEPTH_PREPASS_WGSL,
|
|
1343
|
+
});
|
|
1344
|
+
this.transparentDepthPrepassPipeline = this.device.createRenderPipeline({
|
|
1345
|
+
label: "transparent depth prepass",
|
|
1346
|
+
layout: mainPipelineLayout,
|
|
1347
|
+
vertex: { module: prepassModule, entryPoint: "vs", buffers: fullVertexBuffers },
|
|
1348
|
+
fragment: {
|
|
1349
|
+
module: prepassModule,
|
|
1350
|
+
entryPoint: "fs",
|
|
1351
|
+
targets: sceneTargets.map((t) => ({ format: t.format, writeMask: 0 })),
|
|
1352
|
+
},
|
|
1353
|
+
primitive: { cullMode: "none" },
|
|
1354
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
1355
|
+
depthStencil: {
|
|
1356
|
+
format: "depth24plus-stencil8",
|
|
1357
|
+
depthWriteEnabled: true,
|
|
1358
|
+
depthCompare: "less-equal",
|
|
1359
|
+
},
|
|
1360
|
+
});
|
|
1055
1361
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
1056
1362
|
size: 64,
|
|
1057
1363
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -1180,6 +1486,16 @@ export class Engine {
|
|
|
1180
1486
|
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
1181
1487
|
depthWriteEnabled: false,
|
|
1182
1488
|
depthCompare: "less-equal",
|
|
1489
|
+
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1490
|
+
// carry their surface's exact depth, so against this model's paired
|
|
1491
|
+
// near-coplanar skirt layers the hulls WON depth ties in patches —
|
|
1492
|
+
// the black shapes on the dress. A small constant bias makes hulls lose
|
|
1493
|
+
// every tie; silhouette rims compare against the far background and are
|
|
1494
|
+
// unaffected. No slope term — slope explodes at silhouettes and would
|
|
1495
|
+
// erase the rims themselves (previous regression).
|
|
1496
|
+
depthBias: 4,
|
|
1497
|
+
depthBiasSlopeScale: 0,
|
|
1498
|
+
depthBiasClamp: 0,
|
|
1183
1499
|
// Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
|
|
1184
1500
|
// the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
|
|
1185
1501
|
// edge color overwrite them would re-introduce the dark almond we just killed.
|
|
@@ -1350,9 +1666,15 @@ export class Engine {
|
|
|
1350
1666
|
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
1351
1667
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
1352
1668
|
label: "composite view uniforms",
|
|
1353
|
-
//
|
|
1354
|
-
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray
|
|
1355
|
-
|
|
1669
|
+
// 7 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
1670
|
+
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
|
|
1671
|
+
// (time, _, canvas width, canvas height) for user background effects.
|
|
1672
|
+
size: 112,
|
|
1673
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1674
|
+
});
|
|
1675
|
+
this.bgParamsDummyBuffer = this.device.createBuffer({
|
|
1676
|
+
label: "bg effect params (dummy)",
|
|
1677
|
+
size: 16,
|
|
1356
1678
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1357
1679
|
});
|
|
1358
1680
|
this.compositeBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -1369,6 +1691,9 @@ export class Engine {
|
|
|
1369
1691
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1370
1692
|
// 360 backdrop equirect (PhotoDome-style skybox) — 1×1 fallback when unset.
|
|
1371
1693
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
1694
|
+
// User background-effect params — dummy buffer when no effect is set. The
|
|
1695
|
+
// layout is explicit, so the base shader legally ignores the binding.
|
|
1696
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1372
1697
|
],
|
|
1373
1698
|
});
|
|
1374
1699
|
this.fallbackEquirectTexture = this.device.createTexture({
|
|
@@ -1378,27 +1703,15 @@ export class Engine {
|
|
|
1378
1703
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
1379
1704
|
});
|
|
1380
1705
|
this.fallbackEquirectView = this.fallbackEquirectTexture.createView();
|
|
1381
|
-
|
|
1382
|
-
label: "composite shader",
|
|
1383
|
-
code: COMPOSITE_SHADER_WGSL,
|
|
1384
|
-
});
|
|
1385
|
-
const compositePipelineLayout = this.device.createPipelineLayout({
|
|
1706
|
+
this.compositePipelineLayout = this.device.createPipelineLayout({
|
|
1386
1707
|
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
1387
1708
|
});
|
|
1388
|
-
const
|
|
1389
|
-
label,
|
|
1390
|
-
|
|
1391
|
-
vertex: { module: compositeShader, entryPoint: "vs" },
|
|
1392
|
-
fragment: {
|
|
1393
|
-
module: compositeShader,
|
|
1394
|
-
entryPoint: "fs",
|
|
1395
|
-
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
1396
|
-
targets: [{ format: this.presentationFormat }],
|
|
1397
|
-
},
|
|
1398
|
-
primitive: { topology: "triangle-list" },
|
|
1709
|
+
const compositeShader = this.device.createShaderModule({
|
|
1710
|
+
label: "composite shader",
|
|
1711
|
+
code: buildCompositeShader(null),
|
|
1399
1712
|
});
|
|
1400
|
-
this.compositePipelineIdentity = makeCompositePipeline(false, "composite pipeline (gamma=1)");
|
|
1401
|
-
this.compositePipelineGamma = makeCompositePipeline(true, "composite pipeline (gamma!=1)");
|
|
1713
|
+
this.compositePipelineIdentity = this.makeCompositePipeline(compositeShader, false, "composite pipeline (gamma=1)");
|
|
1714
|
+
this.compositePipelineGamma = this.makeCompositePipeline(compositeShader, true, "composite pipeline (gamma!=1)");
|
|
1402
1715
|
// GPU vertex-morph compute pipeline (shared by all models; per-model bind groups).
|
|
1403
1716
|
// Bindings: 0-4 read-only storage (base pos, CSR rowStart/colMorph/colOffset, weights),
|
|
1404
1717
|
// 5 read-write storage (vertex buffer), 6 uniform (params).
|
|
@@ -2208,6 +2521,7 @@ export class Engine {
|
|
|
2208
2521
|
if (!shared) {
|
|
2209
2522
|
tex.destroy();
|
|
2210
2523
|
this.textureCache.delete(path);
|
|
2524
|
+
this.textureAlphaCache.delete(path);
|
|
2211
2525
|
}
|
|
2212
2526
|
}
|
|
2213
2527
|
for (const buf of inst.gpuBuffers) {
|
|
@@ -2698,12 +3012,17 @@ export class Engine {
|
|
|
2698
3012
|
const prefix = `${inst.name}: `;
|
|
2699
3013
|
// 1-based so that (0,0) = clear color = "no hit"
|
|
2700
3014
|
const modelId = this.modelInstances.size + 1;
|
|
3015
|
+
const texLogicalPath = (texIndex) => texIndex < 0 || texIndex >= textures.length
|
|
3016
|
+
? null
|
|
3017
|
+
: joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path));
|
|
2701
3018
|
const loadTextureByIndex = async (texIndex) => {
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
const logicalPath = joinAssetPath(inst.basePath, normalizeAssetPath(textures[texIndex].path));
|
|
2705
|
-
return this.createTextureFromLogicalPath(inst, logicalPath);
|
|
3019
|
+
const logicalPath = texLogicalPath(texIndex);
|
|
3020
|
+
return logicalPath ? this.createTextureFromLogicalPath(inst, logicalPath) : null;
|
|
2706
3021
|
};
|
|
3022
|
+
// Mesh data for sheerness sampling (8 floats/vertex; uv at +6). See
|
|
3023
|
+
// materialIsSheer — classification happens per material below.
|
|
3024
|
+
const meshVertices = model.getVertices();
|
|
3025
|
+
const meshIndices = model.getIndices();
|
|
2707
3026
|
// 頭 bone index for the eye shader's rear-view gate (-1 when absent).
|
|
2708
3027
|
const headBoneIndex = model.getSkeleton().bones.findIndex((b) => b.name === "頭");
|
|
2709
3028
|
let currentIndexOffset = 0;
|
|
@@ -2719,7 +3038,27 @@ export class Engine {
|
|
|
2719
3038
|
diffuseTexture = this.fallbackMaterialTexture;
|
|
2720
3039
|
}
|
|
2721
3040
|
const materialAlpha = mat.diffuse[3];
|
|
2722
|
-
|
|
3041
|
+
// Transparent bucket when the MATERIAL says so — or when the TEXTURE does
|
|
3042
|
+
// (sheer cloth almost always ships with diffuse alpha 1.0 and carries its
|
|
3043
|
+
// translucency in texture alpha). Transparent-bucket draws happen after
|
|
3044
|
+
// the opaque bucket (and after the late-drawn hair render-class), so a
|
|
3045
|
+
// veil composites over the hair behind it instead of depth-rejecting it;
|
|
3046
|
+
// they are also excluded from the shadow map, so sheer cloth stops
|
|
3047
|
+
// casting the solid shadow of an opaque sheet.
|
|
3048
|
+
const diffusePath = texLogicalPath(mat.diffuseTextureIndex);
|
|
3049
|
+
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null;
|
|
3050
|
+
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler);
|
|
3051
|
+
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD;
|
|
3052
|
+
const partial = !sheer && stats.translucentFrac > PARTIAL_TRANSLUCENT_FRAC;
|
|
3053
|
+
// Transparent bucket: drawn after the opaque bucket (and hair) with depth
|
|
3054
|
+
// WRITE OFF, so a lace panel folded in front of itself blends both layers
|
|
3055
|
+
// consistently instead of a triangle-order patchwork of single/double
|
|
3056
|
+
// coverage. Partial-sheer cloth still casts shadows and keeps its outline;
|
|
3057
|
+
// fully sheer cloth (veil) does neither.
|
|
3058
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || partial;
|
|
3059
|
+
// Load-time classification log — one line per material, cheap and
|
|
3060
|
+
// invaluable when a model renders wrong (bucket/outline/sheer disputes).
|
|
3061
|
+
console.info(`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)} sheerFrac=${stats.translucentFrac.toFixed(2)} ${sheer ? "SHEER" : partial ? "PARTIAL" : "opaque"} bucket=${isTransparent ? "transparent" : "opaque"} edge=${(mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 ? (sheer ? "skipped(sheer)" : "on") : "off"}`);
|
|
2723
3062
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
2724
3063
|
// rare and not implemented — treated as none, like a failed load.
|
|
2725
3064
|
let sphereMode = mat.sphereMode === 1 || mat.sphereMode === 2 ? mat.sphereMode : 0;
|
|
@@ -2759,8 +3098,14 @@ export class Engine {
|
|
|
2759
3098
|
materialName: mat.name,
|
|
2760
3099
|
groupId: null,
|
|
2761
3100
|
baseBindGroupEntries,
|
|
3101
|
+
castsShadow: !sheer,
|
|
2762
3102
|
});
|
|
2763
|
-
|
|
3103
|
+
// No inverted-hull outline for SHEER materials: the outline shader draws a
|
|
3104
|
+
// solid edgeColor silhouette (it never samples texture alpha), so a
|
|
3105
|
+
// see-through veil dragged a near-black hull over the cloth behind it —
|
|
3106
|
+
// broken black shapes that waved with physics and flickered with camera
|
|
3107
|
+
// angle. A solid rim on see-through fabric is wrong in principle.
|
|
3108
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 && !sheer) {
|
|
2764
3109
|
const materialUniformData = new Float32Array([
|
|
2765
3110
|
mat.edgeColor[0],
|
|
2766
3111
|
mat.edgeColor[1],
|
|
@@ -2895,6 +3240,9 @@ export class Engine {
|
|
|
2895
3240
|
return null;
|
|
2896
3241
|
}
|
|
2897
3242
|
}
|
|
3243
|
+
// CPU alpha sampler for sheerness classification (see textureAlphaCache).
|
|
3244
|
+
// Canvas 2D premultiplies RGB on readback, but the ALPHA channel is exact.
|
|
3245
|
+
this.textureAlphaCache.set(cacheKey, buildAlphaSampler(source, rgba, width, height));
|
|
2898
3246
|
const mipLevelCount = Math.floor(Math.log2(Math.max(width, height))) + 1;
|
|
2899
3247
|
const texture = this.device.createTexture({
|
|
2900
3248
|
label: `texture: ${cacheKey}`,
|
|
@@ -3640,9 +3988,13 @@ export class Engine {
|
|
|
3640
3988
|
return { ok: false, diagnostics, slotMap: result.slotMap };
|
|
3641
3989
|
}
|
|
3642
3990
|
let pipeline;
|
|
3991
|
+
let pipelineNoDepthWrite;
|
|
3643
3992
|
let overEyesPipeline;
|
|
3644
3993
|
try {
|
|
3645
3994
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false);
|
|
3995
|
+
// Transparent-bucket draws don't write depth (self-overlap must blend, not
|
|
3996
|
+
// patchwork) — same shading, different depth state.
|
|
3997
|
+
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false);
|
|
3646
3998
|
if (renderClass === "hair")
|
|
3647
3999
|
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true);
|
|
3648
4000
|
}
|
|
@@ -3666,6 +4018,7 @@ export class Engine {
|
|
|
3666
4018
|
renderClass,
|
|
3667
4019
|
alphaMode,
|
|
3668
4020
|
pipeline,
|
|
4021
|
+
pipelineNoDepthWrite,
|
|
3669
4022
|
overEyesPipeline,
|
|
3670
4023
|
uniformBuffer,
|
|
3671
4024
|
slotMap: result.slotMap,
|
|
@@ -3724,14 +4077,14 @@ export class Engine {
|
|
|
3724
4077
|
ground: 4,
|
|
3725
4078
|
};
|
|
3726
4079
|
inst.drawCalls.sort((a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b));
|
|
3727
|
-
inst.shadowDrawCalls = inst.drawCalls.filter((d) => d.type === "opaque");
|
|
4080
|
+
inst.shadowDrawCalls = inst.drawCalls.filter((d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true);
|
|
3728
4081
|
}
|
|
3729
4082
|
/**
|
|
3730
4083
|
* Render-class pipeline state. A group's compiled graph swaps the fragment shading; the
|
|
3731
4084
|
* render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
|
|
3732
4085
|
* eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
|
|
3733
4086
|
*/
|
|
3734
|
-
createRenderClassPipeline(renderClass, module, overEyes) {
|
|
4087
|
+
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true) {
|
|
3735
4088
|
const base = {
|
|
3736
4089
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
3737
4090
|
layout: this.mainPipelineLayout,
|
|
@@ -3741,7 +4094,7 @@ export class Engine {
|
|
|
3741
4094
|
};
|
|
3742
4095
|
const plainDepth = {
|
|
3743
4096
|
format: "depth24plus-stencil8",
|
|
3744
|
-
depthWriteEnabled:
|
|
4097
|
+
depthWriteEnabled: depthWrite,
|
|
3745
4098
|
depthCompare: "less-equal",
|
|
3746
4099
|
};
|
|
3747
4100
|
let depthStencil = plainDepth;
|
|
@@ -3788,6 +4141,12 @@ export class Engine {
|
|
|
3788
4141
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
3789
4142
|
// the neutral base (ungrouped materials render the default graph).
|
|
3790
4143
|
pipelineForDrawCall(inst, dc) {
|
|
4144
|
+
// Transparent draws WRITE depth — MMD semantics: PMX triangle/material order
|
|
4145
|
+
// is the author's compositing order, and a fold HIDES its far side rather
|
|
4146
|
+
// than blending it (the far side shades dark — light-averted — so letting it
|
|
4147
|
+
// show through read as gray fold-shaped stains; depth-write-off made every
|
|
4148
|
+
// fold do that). The no-write twins stay available for a future true-OIT
|
|
4149
|
+
// path but are deliberately unused.
|
|
3791
4150
|
if (dc.groupId) {
|
|
3792
4151
|
const install = inst.styleGroups.get(dc.groupId);
|
|
3793
4152
|
if (install)
|
|
@@ -3828,6 +4187,8 @@ export class Engine {
|
|
|
3828
4187
|
* rebind group 0/1 correctly if needed.
|
|
3829
4188
|
*/
|
|
3830
4189
|
drawOutlines(pass, inst, type) {
|
|
4190
|
+
if (!this.outlineEnabled)
|
|
4191
|
+
return;
|
|
3831
4192
|
let bound = false;
|
|
3832
4193
|
for (const draw of inst.drawCalls) {
|
|
3833
4194
|
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw))
|
|
@@ -3856,12 +4217,35 @@ export class Engine {
|
|
|
3856
4217
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
3857
4218
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
3858
4219
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
4220
|
+
// Order matters (the black-hull saga): transparent color draws don't write
|
|
4221
|
+
// depth (self-overlap must double-blend, not patchwork), so ALL outlines
|
|
4222
|
+
// draw after the transparent depth PREPASS has recorded the fabric's depth
|
|
4223
|
+
// — otherwise every hull behind a sheer skirt shows straight through it.
|
|
3859
4224
|
this.drawMaterials(pass, inst, "opaque");
|
|
3860
|
-
this.drawOutlines(pass, inst, "opaque-outline");
|
|
3861
4225
|
this.drawHairOverEyes(pass, inst);
|
|
3862
4226
|
this.drawMaterials(pass, inst, "transparent");
|
|
4227
|
+
this.drawOutlines(pass, inst, "opaque-outline");
|
|
3863
4228
|
this.drawOutlines(pass, inst, "transparent-outline");
|
|
3864
4229
|
}
|
|
4230
|
+
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
4231
|
+
* Unused while transparent draws write depth themselves (MMD parity) — kept
|
|
4232
|
+
* for the dormant no-write/OIT path. */
|
|
4233
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4234
|
+
drawTransparentDepthPrepass(pass, inst) {
|
|
4235
|
+
let bound = false;
|
|
4236
|
+
for (const draw of inst.drawCalls) {
|
|
4237
|
+
if (draw.type !== "transparent" || !this.shouldRenderDrawCall(inst, draw))
|
|
4238
|
+
continue;
|
|
4239
|
+
if (!bound) {
|
|
4240
|
+
pass.setPipeline(this.transparentDepthPrepassPipeline);
|
|
4241
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
4242
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
4243
|
+
bound = true;
|
|
4244
|
+
}
|
|
4245
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
4246
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
3865
4249
|
/**
|
|
3866
4250
|
* Second hair pass for the see-through-hair effect. Re-draws every hair-class grouped
|
|
3867
4251
|
* opaque draw with its compiled over-eyes pipeline — stencil-matched to `EYE_VALUE`,
|
|
@@ -3913,7 +4297,7 @@ export class Engine {
|
|
|
3913
4297
|
// is LEFT-HANDED (+Z forward, see Mat4.lookAtInto), so the world-space
|
|
3914
4298
|
// right/up/FORWARD vectors are rows 0/1/2 of its rotation block directly
|
|
3915
4299
|
// (column-major storage: row i = values[i], values[i+4], values[i+8]).
|
|
3916
|
-
if (this.backdropEquirectView && this.compositeUniformBuffer) {
|
|
4300
|
+
if ((this.backdropEquirectView || this.backgroundEffect) && this.compositeUniformBuffer) {
|
|
3917
4301
|
const v = viewMatrix.values;
|
|
3918
4302
|
const u = this.compositeUniformData;
|
|
3919
4303
|
const tanHalf = Math.tan((this.camera.fov ?? Math.PI / 4) / 2);
|
|
@@ -3930,6 +4314,10 @@ export class Engine {
|
|
|
3930
4314
|
u[21] = v[6];
|
|
3931
4315
|
u[22] = v[10];
|
|
3932
4316
|
u[23] = 0;
|
|
4317
|
+
// Effect clock + canvas size (viewU[6]) — written on the same refresh.
|
|
4318
|
+
u[24] = (performance.now() - this.bgEffectEpochMs) / 1000;
|
|
4319
|
+
u[26] = this.canvas.width;
|
|
4320
|
+
u[27] = this.canvas.height;
|
|
3933
4321
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u);
|
|
3934
4322
|
}
|
|
3935
4323
|
}
|