reze-engine 0.11.0 → 0.11.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 +40 -22
- package/dist/engine.d.ts +13 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +184 -70
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +44 -21
- package/dist/shaders/cloth_rough.d.ts +1 -1
- package/dist/shaders/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/cloth_rough.js +38 -20
- package/dist/shaders/cloth_smooth.d.ts +1 -1
- package/dist/shaders/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/cloth_smooth.js +33 -18
- package/dist/shaders/default.d.ts +1 -1
- package/dist/shaders/default.d.ts.map +1 -1
- package/dist/shaders/default.js +29 -12
- package/dist/shaders/dfg_lut.d.ts +2 -3
- package/dist/shaders/dfg_lut.d.ts.map +1 -1
- package/dist/shaders/dfg_lut.js +29 -25
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +29 -12
- package/dist/shaders/face.d.ts +1 -1
- package/dist/shaders/face.d.ts.map +1 -1
- package/dist/shaders/face.js +47 -23
- package/dist/shaders/hair.d.ts +1 -1
- package/dist/shaders/hair.d.ts.map +1 -1
- package/dist/shaders/hair.js +42 -32
- package/dist/shaders/metal.d.ts +1 -1
- package/dist/shaders/metal.d.ts.map +1 -1
- package/dist/shaders/metal.js +35 -19
- package/dist/shaders/nodes.d.ts +1 -1
- package/dist/shaders/nodes.d.ts.map +1 -1
- package/dist/shaders/nodes.js +79 -37
- package/dist/shaders/stockings.d.ts +1 -1
- package/dist/shaders/stockings.d.ts.map +1 -1
- package/dist/shaders/stockings.js +30 -15
- package/package.json +1 -1
- package/src/engine.ts +200 -78
- package/src/shaders/body.ts +44 -21
- package/src/shaders/cloth_rough.ts +38 -20
- package/src/shaders/cloth_smooth.ts +33 -18
- package/src/shaders/default.ts +29 -12
- package/src/shaders/dfg_lut.ts +31 -27
- package/src/shaders/eye.ts +29 -12
- package/src/shaders/face.ts +47 -23
- package/src/shaders/hair.ts +42 -32
- package/src/shaders/metal.ts +35 -19
- package/src/shaders/nodes.ts +79 -37
- package/src/shaders/stockings.ts +30 -15
package/dist/engine.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PmxLoader } from "./pmx-loader";
|
|
|
4
4
|
import { Physics } from "./physics";
|
|
5
5
|
import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
|
|
6
6
|
import { DEFAULT_SHADER_WGSL } from "./shaders/default";
|
|
7
|
-
import {
|
|
7
|
+
import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut";
|
|
8
8
|
import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
|
|
9
9
|
import { FACE_SHADER_WGSL } from "./shaders/face";
|
|
10
10
|
import { HAIR_SHADER_WGSL } from "./shaders/hair";
|
|
@@ -21,7 +21,7 @@ export const DEFAULT_BLOOM_OPTIONS = {
|
|
|
21
21
|
knee: 0.5,
|
|
22
22
|
radius: 4.0,
|
|
23
23
|
color: new Vec3(1.0, 0.7247558832168579, 0.6487361788749695),
|
|
24
|
-
intensity: 0.
|
|
24
|
+
intensity: 0.05,
|
|
25
25
|
clamp: 0.0,
|
|
26
26
|
};
|
|
27
27
|
export const DEFAULT_VIEW_TRANSFORM = {
|
|
@@ -66,6 +66,8 @@ export class Engine {
|
|
|
66
66
|
this.pendingPick = null;
|
|
67
67
|
this.modelInstances = new Map();
|
|
68
68
|
this.textureCache = new Map();
|
|
69
|
+
this.mipBlitPipeline = null;
|
|
70
|
+
this.mipBlitSampler = null;
|
|
69
71
|
this._nextDefaultModelId = 0;
|
|
70
72
|
// IK and physics enabled at engine level (same for all models)
|
|
71
73
|
this.ikEnabled = true;
|
|
@@ -273,47 +275,23 @@ export class Engine {
|
|
|
273
275
|
this.setupResize();
|
|
274
276
|
Engine.instance = this;
|
|
275
277
|
}
|
|
276
|
-
// One-shot bake of EEVEE's BRDF
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
label: "DFG LUT bake pipeline",
|
|
290
|
-
layout: "auto",
|
|
291
|
-
vertex: { module, entryPoint: "vs" },
|
|
292
|
-
fragment: { module, entryPoint: "fs", targets: [{ format: "rg16float" }] },
|
|
293
|
-
primitive: { topology: "triangle-list" },
|
|
294
|
-
});
|
|
295
|
-
const enc = this.device.createCommandEncoder({ label: "DFG LUT bake encoder" });
|
|
296
|
-
const pass = enc.beginRenderPass({
|
|
297
|
-
colorAttachments: [
|
|
298
|
-
{ view: this.dfgLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
299
|
-
],
|
|
300
|
-
});
|
|
301
|
-
pass.setPipeline(pipeline);
|
|
302
|
-
pass.draw(3, 1, 0, 0);
|
|
303
|
-
pass.end();
|
|
304
|
-
this.device.queue.submit([enc.finish()]);
|
|
305
|
-
}
|
|
306
|
-
// Upload Blender's static LTC GGX magnitude LUT (eevee_lut.c ltc_mag_ggx[]).
|
|
307
|
-
// Pairs with the DFG LUT to form ltc_brdf_scale — closure_eval_glossy_lib.glsl:79-81.
|
|
308
|
-
uploadLtcMagLut() {
|
|
309
|
-
this.ltcMagLutTexture = this.device.createTexture({
|
|
310
|
-
label: "LTC mag LUT",
|
|
278
|
+
// One-shot bake of EEVEE's combined BRDF LUT — DFG (bsdf_lut_frag.glsl) packed
|
|
279
|
+
// with ltc_mag_ggx (eevee_lut.c) into a single 64×64 rgba8unorm texture:
|
|
280
|
+
// .rg = split-sum DFG → F_brdf_*_scatter
|
|
281
|
+
// .ba = LTC magnitude → ltc_brdf_scale_from_lut
|
|
282
|
+
// One texture fetch per fragment replaces the previous 2–3 taps. rgba8unorm
|
|
283
|
+
// (vs rgba16float) halves sample bandwidth; DFG/LTC values fit [0,1] cleanly.
|
|
284
|
+
bakeBrdfLut() {
|
|
285
|
+
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
286
|
+
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).");
|
|
287
|
+
}
|
|
288
|
+
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
289
|
+
const ltcTemp = this.device.createTexture({
|
|
290
|
+
label: "LTC mag LUT (bake input)",
|
|
311
291
|
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
312
292
|
format: "rg16float",
|
|
313
293
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
314
294
|
});
|
|
315
|
-
this.ltcMagLutView = this.ltcMagLutTexture.createView();
|
|
316
|
-
// Float32 → float16 bits. rg16float writeTexture expects packed half floats.
|
|
317
295
|
const n = LTC_MAG_LUT_DATA.length;
|
|
318
296
|
const half = new Uint16Array(n);
|
|
319
297
|
const f32 = new Float32Array(1);
|
|
@@ -323,20 +301,53 @@ export class Engine {
|
|
|
323
301
|
const x = u32[0];
|
|
324
302
|
const sign = (x >>> 16) & 0x8000;
|
|
325
303
|
let exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
326
|
-
|
|
304
|
+
const mant = x & 0x7fffff;
|
|
327
305
|
if (exp <= 0) {
|
|
328
|
-
half[i] = sign;
|
|
306
|
+
half[i] = sign;
|
|
329
307
|
}
|
|
330
308
|
else if (exp >= 31) {
|
|
331
|
-
half[i] = sign | 0x7c00;
|
|
309
|
+
half[i] = sign | 0x7c00;
|
|
332
310
|
}
|
|
333
311
|
else {
|
|
334
312
|
half[i] = sign | (exp << 10) | (mant >>> 13);
|
|
335
313
|
}
|
|
336
314
|
}
|
|
337
|
-
this.device.queue.writeTexture({ texture:
|
|
315
|
+
this.device.queue.writeTexture({ texture: ltcTemp }, half, { bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE }, { width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 });
|
|
316
|
+
this.brdfLutTexture = this.device.createTexture({
|
|
317
|
+
label: "BRDF LUT (DFG + LTC packed)",
|
|
318
|
+
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
319
|
+
format: "rgba8unorm",
|
|
320
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
321
|
+
});
|
|
322
|
+
this.brdfLutView = this.brdfLutTexture.createView();
|
|
323
|
+
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL });
|
|
324
|
+
const pipeline = this.device.createRenderPipeline({
|
|
325
|
+
label: "BRDF LUT bake pipeline",
|
|
326
|
+
layout: "auto",
|
|
327
|
+
vertex: { module, entryPoint: "vs" },
|
|
328
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
329
|
+
primitive: { topology: "triangle-list" },
|
|
330
|
+
});
|
|
331
|
+
const bakeBindGroup = this.device.createBindGroup({
|
|
332
|
+
label: "BRDF LUT bake bind group",
|
|
333
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
334
|
+
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
335
|
+
});
|
|
336
|
+
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" });
|
|
337
|
+
const pass = enc.beginRenderPass({
|
|
338
|
+
colorAttachments: [
|
|
339
|
+
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
pass.setPipeline(pipeline);
|
|
343
|
+
pass.setBindGroup(0, bakeBindGroup);
|
|
344
|
+
pass.draw(3, 1, 0, 0);
|
|
345
|
+
pass.end();
|
|
346
|
+
this.device.queue.submit([enc.finish()]);
|
|
347
|
+
ltcTemp.destroy();
|
|
338
348
|
}
|
|
339
349
|
createRenderPipeline(config) {
|
|
350
|
+
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
|
|
340
351
|
return this.device.createRenderPipeline({
|
|
341
352
|
label: config.label,
|
|
342
353
|
layout: config.layout,
|
|
@@ -344,11 +355,11 @@ export class Engine {
|
|
|
344
355
|
module: config.shaderModule,
|
|
345
356
|
buffers: config.vertexBuffers,
|
|
346
357
|
},
|
|
347
|
-
fragment:
|
|
358
|
+
fragment: targets
|
|
348
359
|
? {
|
|
349
360
|
module: config.shaderModule,
|
|
350
361
|
entryPoint: config.fragmentEntryPoint,
|
|
351
|
-
targets
|
|
362
|
+
targets,
|
|
352
363
|
}
|
|
353
364
|
: undefined,
|
|
354
365
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
@@ -360,6 +371,7 @@ export class Engine {
|
|
|
360
371
|
this.materialSampler = this.device.createSampler({
|
|
361
372
|
magFilter: "linear",
|
|
362
373
|
minFilter: "linear",
|
|
374
|
+
mipmapFilter: "linear",
|
|
363
375
|
addressModeU: "repeat",
|
|
364
376
|
addressModeV: "repeat",
|
|
365
377
|
});
|
|
@@ -417,6 +429,11 @@ export class Engine {
|
|
|
417
429
|
},
|
|
418
430
|
},
|
|
419
431
|
};
|
|
432
|
+
// Bloom mask target — r8unorm has no alpha channel, so src-alpha blending is invalid.
|
|
433
|
+
// Use replace mode: depth test already rejects occluded fragments, so last-writer-wins
|
|
434
|
+
// on surviving pixels gives the right result (ground writes 0; models/outlines write 1).
|
|
435
|
+
const maskBlend = { format: Engine.BLOOM_MASK_FORMAT };
|
|
436
|
+
const sceneTargets = [standardBlend, maskBlend];
|
|
420
437
|
const shaderModule = this.device.createShaderModule({
|
|
421
438
|
label: "default model shader",
|
|
422
439
|
code: DEFAULT_SHADER_WGSL,
|
|
@@ -464,7 +481,6 @@ export class Engine {
|
|
|
464
481
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
465
482
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
466
483
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
467
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
468
484
|
],
|
|
469
485
|
});
|
|
470
486
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -494,7 +510,7 @@ export class Engine {
|
|
|
494
510
|
layout: mainPipelineLayout,
|
|
495
511
|
shaderModule,
|
|
496
512
|
vertexBuffers: fullVertexBuffers,
|
|
497
|
-
|
|
513
|
+
fragmentTargets: sceneTargets,
|
|
498
514
|
cullMode: "none",
|
|
499
515
|
depthStencil: {
|
|
500
516
|
format: "depth24plus-stencil8",
|
|
@@ -507,7 +523,7 @@ export class Engine {
|
|
|
507
523
|
layout: mainPipelineLayout,
|
|
508
524
|
shaderModule: faceShaderModule,
|
|
509
525
|
vertexBuffers: fullVertexBuffers,
|
|
510
|
-
|
|
526
|
+
fragmentTargets: sceneTargets,
|
|
511
527
|
cullMode: "none",
|
|
512
528
|
depthStencil: {
|
|
513
529
|
format: "depth24plus-stencil8",
|
|
@@ -520,7 +536,7 @@ export class Engine {
|
|
|
520
536
|
layout: mainPipelineLayout,
|
|
521
537
|
shaderModule: hairShaderModule,
|
|
522
538
|
vertexBuffers: fullVertexBuffers,
|
|
523
|
-
|
|
539
|
+
fragmentTargets: sceneTargets,
|
|
524
540
|
cullMode: "none",
|
|
525
541
|
depthStencil: {
|
|
526
542
|
format: "depth24plus-stencil8",
|
|
@@ -533,7 +549,7 @@ export class Engine {
|
|
|
533
549
|
layout: mainPipelineLayout,
|
|
534
550
|
shaderModule: clothSmoothShaderModule,
|
|
535
551
|
vertexBuffers: fullVertexBuffers,
|
|
536
|
-
|
|
552
|
+
fragmentTargets: sceneTargets,
|
|
537
553
|
cullMode: "none",
|
|
538
554
|
depthStencil: {
|
|
539
555
|
format: "depth24plus-stencil8",
|
|
@@ -546,7 +562,7 @@ export class Engine {
|
|
|
546
562
|
layout: mainPipelineLayout,
|
|
547
563
|
shaderModule: clothRoughShaderModule,
|
|
548
564
|
vertexBuffers: fullVertexBuffers,
|
|
549
|
-
|
|
565
|
+
fragmentTargets: sceneTargets,
|
|
550
566
|
cullMode: "none",
|
|
551
567
|
depthStencil: {
|
|
552
568
|
format: "depth24plus-stencil8",
|
|
@@ -559,7 +575,7 @@ export class Engine {
|
|
|
559
575
|
layout: mainPipelineLayout,
|
|
560
576
|
shaderModule: metalShaderModule,
|
|
561
577
|
vertexBuffers: fullVertexBuffers,
|
|
562
|
-
|
|
578
|
+
fragmentTargets: sceneTargets,
|
|
563
579
|
cullMode: "none",
|
|
564
580
|
depthStencil: {
|
|
565
581
|
format: "depth24plus-stencil8",
|
|
@@ -572,7 +588,7 @@ export class Engine {
|
|
|
572
588
|
layout: mainPipelineLayout,
|
|
573
589
|
shaderModule: bodyShaderModule,
|
|
574
590
|
vertexBuffers: fullVertexBuffers,
|
|
575
|
-
|
|
591
|
+
fragmentTargets: sceneTargets,
|
|
576
592
|
cullMode: "none",
|
|
577
593
|
depthStencil: {
|
|
578
594
|
format: "depth24plus-stencil8",
|
|
@@ -585,7 +601,7 @@ export class Engine {
|
|
|
585
601
|
layout: mainPipelineLayout,
|
|
586
602
|
shaderModule: eyeShaderModule,
|
|
587
603
|
vertexBuffers: fullVertexBuffers,
|
|
588
|
-
|
|
604
|
+
fragmentTargets: sceneTargets,
|
|
589
605
|
cullMode: "none",
|
|
590
606
|
depthStencil: {
|
|
591
607
|
format: "depth24plus-stencil8",
|
|
@@ -598,7 +614,7 @@ export class Engine {
|
|
|
598
614
|
layout: mainPipelineLayout,
|
|
599
615
|
shaderModule: stockingsShaderModule,
|
|
600
616
|
vertexBuffers: fullVertexBuffers,
|
|
601
|
-
|
|
617
|
+
fragmentTargets: sceneTargets,
|
|
602
618
|
cullMode: "none",
|
|
603
619
|
depthStencil: {
|
|
604
620
|
format: "depth24plus-stencil8",
|
|
@@ -661,10 +677,8 @@ export class Engine {
|
|
|
661
677
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
662
678
|
});
|
|
663
679
|
this.shadowMapDepthView = this.shadowMapTexture.createView();
|
|
664
|
-
// One-shot bake of Blender EEVEE's BRDF
|
|
665
|
-
this.
|
|
666
|
-
// Upload static LTC GGX magnitude LUT for direct-specular energy compensation.
|
|
667
|
-
this.uploadLtcMagLut();
|
|
680
|
+
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
681
|
+
this.bakeBrdfLut();
|
|
668
682
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
669
683
|
this.perFrameBindGroup = this.device.createBindGroup({
|
|
670
684
|
label: "main per-frame bind group",
|
|
@@ -676,8 +690,7 @@ export class Engine {
|
|
|
676
690
|
{ binding: 3, resource: this.shadowMapDepthView },
|
|
677
691
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
678
692
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
679
|
-
{ binding: 9, resource: this.
|
|
680
|
-
{ binding: 10, resource: this.ltcMagLutView },
|
|
693
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
681
694
|
],
|
|
682
695
|
});
|
|
683
696
|
this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -741,7 +754,8 @@ export class Engine {
|
|
|
741
754
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
742
755
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
743
756
|
}
|
|
744
|
-
|
|
757
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
758
|
+
@fragment fn fs(i: VO) -> FSOut {
|
|
745
759
|
let n = normalize(i.normal);
|
|
746
760
|
let centerDist = length(i.worldPos.xz);
|
|
747
761
|
let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
|
|
@@ -779,7 +793,10 @@ export class Engine {
|
|
|
779
793
|
var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
780
794
|
baseColor *= noiseTint;
|
|
781
795
|
let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
|
|
782
|
-
|
|
796
|
+
var out: FSOut;
|
|
797
|
+
out.color = vec4f(finalColor * edgeFade, edgeFade);
|
|
798
|
+
out.mask = 0.0;
|
|
799
|
+
return out;
|
|
783
800
|
}
|
|
784
801
|
`,
|
|
785
802
|
});
|
|
@@ -788,7 +805,7 @@ export class Engine {
|
|
|
788
805
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
789
806
|
shaderModule: groundShadowShader,
|
|
790
807
|
vertexBuffers: fullVertexBuffers,
|
|
791
|
-
|
|
808
|
+
fragmentTargets: sceneTargets,
|
|
792
809
|
cullMode: "back",
|
|
793
810
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
794
811
|
});
|
|
@@ -897,8 +914,12 @@ export class Engine {
|
|
|
897
914
|
return output;
|
|
898
915
|
}
|
|
899
916
|
|
|
900
|
-
|
|
901
|
-
|
|
917
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
918
|
+
@fragment fn fs() -> FSOut {
|
|
919
|
+
var out: FSOut;
|
|
920
|
+
out.color = material.edgeColor;
|
|
921
|
+
out.mask = 1.0;
|
|
922
|
+
return out;
|
|
902
923
|
}
|
|
903
924
|
`,
|
|
904
925
|
});
|
|
@@ -907,7 +928,7 @@ export class Engine {
|
|
|
907
928
|
layout: outlinePipelineLayout,
|
|
908
929
|
shaderModule: outlineShaderModule,
|
|
909
930
|
vertexBuffers: outlineVertexBuffers,
|
|
910
|
-
|
|
931
|
+
fragmentTargets: sceneTargets,
|
|
911
932
|
cullMode: "back",
|
|
912
933
|
depthStencil: {
|
|
913
934
|
format: "depth24plus-stencil8",
|
|
@@ -942,6 +963,7 @@ export class Engine {
|
|
|
942
963
|
entries: [
|
|
943
964
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
944
965
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
966
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
945
967
|
],
|
|
946
968
|
});
|
|
947
969
|
this.bloomDownsampleBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -973,6 +995,7 @@ export class Engine {
|
|
|
973
995
|
code: `${bloomFullscreenVs}
|
|
974
996
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
975
997
|
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
998
|
+
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
976
999
|
|
|
977
1000
|
fn luminance(c: vec3f) -> f32 {
|
|
978
1001
|
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
@@ -983,8 +1006,11 @@ export class Engine {
|
|
|
983
1006
|
let s = textureLoad(hdrTex, cc, 0);
|
|
984
1007
|
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
985
1008
|
let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
|
|
1009
|
+
// Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
|
|
1010
|
+
let mask = textureLoad(maskTex, cc, 0).r;
|
|
1011
|
+
let masked = rgb * mask;
|
|
986
1012
|
// Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
987
|
-
return select(
|
|
1013
|
+
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
988
1014
|
}
|
|
989
1015
|
|
|
990
1016
|
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
@@ -1308,6 +1334,22 @@ export class Engine {
|
|
|
1308
1334
|
format: Engine.HDR_FORMAT,
|
|
1309
1335
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1310
1336
|
});
|
|
1337
|
+
// Bloom-mask MRT attachments — same dims + MSAA as HDR so they share the render pass.
|
|
1338
|
+
// MS buffer gets resolved into maskResolveTexture, which the bloom blit pass samples.
|
|
1339
|
+
this.multisampleMaskTexture = this.device.createTexture({
|
|
1340
|
+
label: "multisample bloom mask",
|
|
1341
|
+
size: [width, height],
|
|
1342
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
1343
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1344
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1345
|
+
});
|
|
1346
|
+
this.maskResolveTexture = this.device.createTexture({
|
|
1347
|
+
label: "bloom mask resolve",
|
|
1348
|
+
size: [width, height],
|
|
1349
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1350
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1351
|
+
});
|
|
1352
|
+
this.maskResolveView = this.maskResolveTexture.createView();
|
|
1311
1353
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
1312
1354
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
1313
1355
|
const bw = Math.max(1, Math.floor(width / 2));
|
|
@@ -1352,9 +1394,16 @@ export class Engine {
|
|
|
1352
1394
|
loadOp: "clear",
|
|
1353
1395
|
storeOp: "store",
|
|
1354
1396
|
};
|
|
1397
|
+
const maskAttachment = {
|
|
1398
|
+
view: this.multisampleMaskTexture.createView(),
|
|
1399
|
+
resolveTarget: this.maskResolveView,
|
|
1400
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1401
|
+
loadOp: "clear",
|
|
1402
|
+
storeOp: "store",
|
|
1403
|
+
};
|
|
1355
1404
|
this.renderPassDescriptor = {
|
|
1356
1405
|
label: "renderPass",
|
|
1357
|
-
colorAttachments: [colorAttachment],
|
|
1406
|
+
colorAttachments: [colorAttachment, maskAttachment],
|
|
1358
1407
|
depthStencilAttachment: {
|
|
1359
1408
|
view: depthTextureView,
|
|
1360
1409
|
depthClearValue: 1.0,
|
|
@@ -1386,6 +1435,7 @@ export class Engine {
|
|
|
1386
1435
|
entries: [
|
|
1387
1436
|
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
1388
1437
|
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
1438
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
1389
1439
|
],
|
|
1390
1440
|
});
|
|
1391
1441
|
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
@@ -2116,16 +2166,20 @@ export class Engine {
|
|
|
2116
2166
|
premultiplyAlpha: "none",
|
|
2117
2167
|
colorSpaceConversion: "none",
|
|
2118
2168
|
});
|
|
2169
|
+
const mipLevelCount = Math.floor(Math.log2(Math.max(imageBitmap.width, imageBitmap.height))) + 1;
|
|
2119
2170
|
const texture = this.device.createTexture({
|
|
2120
2171
|
label: `texture: ${cacheKey}`,
|
|
2121
2172
|
size: [imageBitmap.width, imageBitmap.height],
|
|
2122
2173
|
format: "rgba8unorm-srgb",
|
|
2174
|
+
mipLevelCount,
|
|
2123
2175
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
2124
2176
|
});
|
|
2125
2177
|
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
2126
2178
|
imageBitmap.width,
|
|
2127
2179
|
imageBitmap.height,
|
|
2128
2180
|
]);
|
|
2181
|
+
if (mipLevelCount > 1)
|
|
2182
|
+
this.generateMipmaps(texture, mipLevelCount);
|
|
2129
2183
|
this.textureCache.set(cacheKey, texture);
|
|
2130
2184
|
inst.textureCacheKeys.push(cacheKey);
|
|
2131
2185
|
return texture;
|
|
@@ -2134,6 +2188,62 @@ export class Engine {
|
|
|
2134
2188
|
return null;
|
|
2135
2189
|
}
|
|
2136
2190
|
}
|
|
2191
|
+
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
2192
|
+
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
2193
|
+
generateMipmaps(texture, mipLevelCount) {
|
|
2194
|
+
if (!this.mipBlitPipeline || !this.mipBlitSampler) {
|
|
2195
|
+
this.mipBlitSampler = this.device.createSampler({
|
|
2196
|
+
magFilter: "linear",
|
|
2197
|
+
minFilter: "linear",
|
|
2198
|
+
addressModeU: "clamp-to-edge",
|
|
2199
|
+
addressModeV: "clamp-to-edge",
|
|
2200
|
+
});
|
|
2201
|
+
const module = this.device.createShaderModule({
|
|
2202
|
+
label: "mipmap blit",
|
|
2203
|
+
code: /* wgsl */ `
|
|
2204
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
2205
|
+
@group(0) @binding(1) var samp: sampler;
|
|
2206
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
2207
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
2208
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
2209
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
2210
|
+
}
|
|
2211
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
2212
|
+
let dstDims = vec2f(textureDimensions(src)) * 0.5;
|
|
2213
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
2214
|
+
return textureSampleLevel(src, samp, uv, 0.0);
|
|
2215
|
+
}
|
|
2216
|
+
`,
|
|
2217
|
+
});
|
|
2218
|
+
this.mipBlitPipeline = this.device.createRenderPipeline({
|
|
2219
|
+
label: "mipmap blit pipeline",
|
|
2220
|
+
layout: "auto",
|
|
2221
|
+
vertex: { module, entryPoint: "vs" },
|
|
2222
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm-srgb" }] },
|
|
2223
|
+
primitive: { topology: "triangle-list" },
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
const encoder = this.device.createCommandEncoder({ label: "mipgen" });
|
|
2227
|
+
for (let level = 1; level < mipLevelCount; level++) {
|
|
2228
|
+
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
2229
|
+
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
|
|
2230
|
+
const bindGroup = this.device.createBindGroup({
|
|
2231
|
+
layout: this.mipBlitPipeline.getBindGroupLayout(0),
|
|
2232
|
+
entries: [
|
|
2233
|
+
{ binding: 0, resource: srcView },
|
|
2234
|
+
{ binding: 1, resource: this.mipBlitSampler },
|
|
2235
|
+
],
|
|
2236
|
+
});
|
|
2237
|
+
const pass = encoder.beginRenderPass({
|
|
2238
|
+
colorAttachments: [{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }],
|
|
2239
|
+
});
|
|
2240
|
+
pass.setPipeline(this.mipBlitPipeline);
|
|
2241
|
+
pass.setBindGroup(0, bindGroup);
|
|
2242
|
+
pass.draw(3);
|
|
2243
|
+
pass.end();
|
|
2244
|
+
}
|
|
2245
|
+
this.device.queue.submit([encoder.finish()]);
|
|
2246
|
+
}
|
|
2137
2247
|
renderGround(pass) {
|
|
2138
2248
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
2139
2249
|
return;
|
|
@@ -2467,5 +2577,9 @@ export class Engine {
|
|
|
2467
2577
|
Engine.instance = null;
|
|
2468
2578
|
Engine.MULTISAMPLE_COUNT = 4;
|
|
2469
2579
|
Engine.HDR_FORMAT = "rgba16float";
|
|
2580
|
+
/** Single-channel mask written alongside HDR color — 1 = model geometry (contributes
|
|
2581
|
+
* to bloom), 0 = ground (never blooms). Sampled by the bloom blit pass to gate the
|
|
2582
|
+
* prefilter so ground brightness can't halo the scene. */
|
|
2583
|
+
Engine.BLOOM_MASK_FORMAT = "r8unorm";
|
|
2470
2584
|
Engine.BLOOM_MAX_LEVELS = 7;
|
|
2471
|
-
Engine.SHADOW_MAP_SIZE =
|
|
2585
|
+
Engine.SHADOW_MAP_SIZE = 2048;
|