reze-engine 0.11.0 → 0.11.2
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 +14 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +206 -77
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +58 -47
- 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 +45 -42
- 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 +30 -26
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +47 -43
- 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 +2 -2
- package/src/engine.ts +227 -97
- package/src/shaders/body.ts +58 -47
- package/src/shaders/cloth_rough.ts +38 -20
- package/src/shaders/cloth_smooth.ts +33 -18
- package/src/shaders/default.ts +46 -42
- package/src/shaders/dfg_lut.ts +32 -28
- package/src/shaders/eye.ts +48 -43
- 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,11 +21,13 @@ 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
|
+
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
28
|
+
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
27
29
|
export const DEFAULT_VIEW_TRANSFORM = {
|
|
28
|
-
exposure:
|
|
30
|
+
exposure: 0.6,
|
|
29
31
|
gamma: 1.0,
|
|
30
32
|
look: "medium_high_contrast",
|
|
31
33
|
};
|
|
@@ -66,6 +68,8 @@ export class Engine {
|
|
|
66
68
|
this.pendingPick = null;
|
|
67
69
|
this.modelInstances = new Map();
|
|
68
70
|
this.textureCache = new Map();
|
|
71
|
+
this.mipBlitPipeline = null;
|
|
72
|
+
this.mipBlitSampler = null;
|
|
69
73
|
this._nextDefaultModelId = 0;
|
|
70
74
|
// IK and physics enabled at engine level (same for all models)
|
|
71
75
|
this.ikEnabled = true;
|
|
@@ -234,9 +238,12 @@ export class Engine {
|
|
|
234
238
|
writeBloomUniforms() {
|
|
235
239
|
const b = this.bloomSettings;
|
|
236
240
|
const bu = this.bloomBlitUniformData;
|
|
237
|
-
// EEVEE prefilter: threshold,
|
|
241
|
+
// EEVEE prefilter: threshold, knee_half, clamp (0 → disabled), _unused
|
|
242
|
+
// Blender halves the knee before passing to the shader (eevee_bloom.c: knee * 0.5f).
|
|
243
|
+
// The blit shader's quadratic soft-knee curve uses knee_half as the offset from threshold,
|
|
244
|
+
// so the soft ramp spans [threshold - knee/2 .. threshold + knee/2] — NOT [threshold - knee .. threshold + knee].
|
|
238
245
|
bu[0] = b.threshold;
|
|
239
|
-
bu[1] = b.knee;
|
|
246
|
+
bu[1] = b.knee * 0.5;
|
|
240
247
|
bu[2] = b.clamp;
|
|
241
248
|
bu[3] = 0.0;
|
|
242
249
|
this.device.queue.writeBuffer(this.bloomBlitUniformBuffer, 0, bu);
|
|
@@ -273,47 +280,23 @@ export class Engine {
|
|
|
273
280
|
this.setupResize();
|
|
274
281
|
Engine.instance = this;
|
|
275
282
|
}
|
|
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",
|
|
283
|
+
// One-shot bake of EEVEE's combined BRDF LUT — DFG (bsdf_lut_frag.glsl) packed
|
|
284
|
+
// with ltc_mag_ggx (eevee_lut.c) into a single 64×64 rgba8unorm texture:
|
|
285
|
+
// .rg = split-sum DFG → F_brdf_*_scatter
|
|
286
|
+
// .ba = LTC magnitude → ltc_brdf_scale_from_lut
|
|
287
|
+
// One texture fetch per fragment replaces the previous 2–3 taps. rgba8unorm
|
|
288
|
+
// (vs rgba16float) halves sample bandwidth; DFG/LTC values fit [0,1] cleanly.
|
|
289
|
+
bakeBrdfLut() {
|
|
290
|
+
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
291
|
+
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).");
|
|
292
|
+
}
|
|
293
|
+
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
294
|
+
const ltcTemp = this.device.createTexture({
|
|
295
|
+
label: "LTC mag LUT (bake input)",
|
|
311
296
|
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
312
297
|
format: "rg16float",
|
|
313
298
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
314
299
|
});
|
|
315
|
-
this.ltcMagLutView = this.ltcMagLutTexture.createView();
|
|
316
|
-
// Float32 → float16 bits. rg16float writeTexture expects packed half floats.
|
|
317
300
|
const n = LTC_MAG_LUT_DATA.length;
|
|
318
301
|
const half = new Uint16Array(n);
|
|
319
302
|
const f32 = new Float32Array(1);
|
|
@@ -323,20 +306,53 @@ export class Engine {
|
|
|
323
306
|
const x = u32[0];
|
|
324
307
|
const sign = (x >>> 16) & 0x8000;
|
|
325
308
|
let exp = ((x >>> 23) & 0xff) - 127 + 15;
|
|
326
|
-
|
|
309
|
+
const mant = x & 0x7fffff;
|
|
327
310
|
if (exp <= 0) {
|
|
328
|
-
half[i] = sign;
|
|
311
|
+
half[i] = sign;
|
|
329
312
|
}
|
|
330
313
|
else if (exp >= 31) {
|
|
331
|
-
half[i] = sign | 0x7c00;
|
|
314
|
+
half[i] = sign | 0x7c00;
|
|
332
315
|
}
|
|
333
316
|
else {
|
|
334
317
|
half[i] = sign | (exp << 10) | (mant >>> 13);
|
|
335
318
|
}
|
|
336
319
|
}
|
|
337
|
-
this.device.queue.writeTexture({ texture:
|
|
320
|
+
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 });
|
|
321
|
+
this.brdfLutTexture = this.device.createTexture({
|
|
322
|
+
label: "BRDF LUT (DFG + LTC packed)",
|
|
323
|
+
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
324
|
+
format: "rgba8unorm",
|
|
325
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
326
|
+
});
|
|
327
|
+
this.brdfLutView = this.brdfLutTexture.createView();
|
|
328
|
+
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL });
|
|
329
|
+
const pipeline = this.device.createRenderPipeline({
|
|
330
|
+
label: "BRDF LUT bake pipeline",
|
|
331
|
+
layout: "auto",
|
|
332
|
+
vertex: { module, entryPoint: "vs" },
|
|
333
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
334
|
+
primitive: { topology: "triangle-list" },
|
|
335
|
+
});
|
|
336
|
+
const bakeBindGroup = this.device.createBindGroup({
|
|
337
|
+
label: "BRDF LUT bake bind group",
|
|
338
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
339
|
+
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
340
|
+
});
|
|
341
|
+
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" });
|
|
342
|
+
const pass = enc.beginRenderPass({
|
|
343
|
+
colorAttachments: [
|
|
344
|
+
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
pass.setPipeline(pipeline);
|
|
348
|
+
pass.setBindGroup(0, bakeBindGroup);
|
|
349
|
+
pass.draw(3, 1, 0, 0);
|
|
350
|
+
pass.end();
|
|
351
|
+
this.device.queue.submit([enc.finish()]);
|
|
352
|
+
ltcTemp.destroy();
|
|
338
353
|
}
|
|
339
354
|
createRenderPipeline(config) {
|
|
355
|
+
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined);
|
|
340
356
|
return this.device.createRenderPipeline({
|
|
341
357
|
label: config.label,
|
|
342
358
|
layout: config.layout,
|
|
@@ -344,11 +360,11 @@ export class Engine {
|
|
|
344
360
|
module: config.shaderModule,
|
|
345
361
|
buffers: config.vertexBuffers,
|
|
346
362
|
},
|
|
347
|
-
fragment:
|
|
363
|
+
fragment: targets
|
|
348
364
|
? {
|
|
349
365
|
module: config.shaderModule,
|
|
350
366
|
entryPoint: config.fragmentEntryPoint,
|
|
351
|
-
targets
|
|
367
|
+
targets,
|
|
352
368
|
}
|
|
353
369
|
: undefined,
|
|
354
370
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
@@ -360,6 +376,7 @@ export class Engine {
|
|
|
360
376
|
this.materialSampler = this.device.createSampler({
|
|
361
377
|
magFilter: "linear",
|
|
362
378
|
minFilter: "linear",
|
|
379
|
+
mipmapFilter: "linear",
|
|
363
380
|
addressModeU: "repeat",
|
|
364
381
|
addressModeV: "repeat",
|
|
365
382
|
});
|
|
@@ -417,6 +434,11 @@ export class Engine {
|
|
|
417
434
|
},
|
|
418
435
|
},
|
|
419
436
|
};
|
|
437
|
+
// Bloom mask target — r8unorm has no alpha channel, so src-alpha blending is invalid.
|
|
438
|
+
// Use replace mode: depth test already rejects occluded fragments, so last-writer-wins
|
|
439
|
+
// on surviving pixels gives the right result (ground writes 0; models/outlines write 1).
|
|
440
|
+
const maskBlend = { format: Engine.BLOOM_MASK_FORMAT };
|
|
441
|
+
const sceneTargets = [standardBlend, maskBlend];
|
|
420
442
|
const shaderModule = this.device.createShaderModule({
|
|
421
443
|
label: "default model shader",
|
|
422
444
|
code: DEFAULT_SHADER_WGSL,
|
|
@@ -464,7 +486,6 @@ export class Engine {
|
|
|
464
486
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
465
487
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
466
488
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
467
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
468
489
|
],
|
|
469
490
|
});
|
|
470
491
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -494,7 +515,7 @@ export class Engine {
|
|
|
494
515
|
layout: mainPipelineLayout,
|
|
495
516
|
shaderModule,
|
|
496
517
|
vertexBuffers: fullVertexBuffers,
|
|
497
|
-
|
|
518
|
+
fragmentTargets: sceneTargets,
|
|
498
519
|
cullMode: "none",
|
|
499
520
|
depthStencil: {
|
|
500
521
|
format: "depth24plus-stencil8",
|
|
@@ -507,7 +528,7 @@ export class Engine {
|
|
|
507
528
|
layout: mainPipelineLayout,
|
|
508
529
|
shaderModule: faceShaderModule,
|
|
509
530
|
vertexBuffers: fullVertexBuffers,
|
|
510
|
-
|
|
531
|
+
fragmentTargets: sceneTargets,
|
|
511
532
|
cullMode: "none",
|
|
512
533
|
depthStencil: {
|
|
513
534
|
format: "depth24plus-stencil8",
|
|
@@ -520,7 +541,7 @@ export class Engine {
|
|
|
520
541
|
layout: mainPipelineLayout,
|
|
521
542
|
shaderModule: hairShaderModule,
|
|
522
543
|
vertexBuffers: fullVertexBuffers,
|
|
523
|
-
|
|
544
|
+
fragmentTargets: sceneTargets,
|
|
524
545
|
cullMode: "none",
|
|
525
546
|
depthStencil: {
|
|
526
547
|
format: "depth24plus-stencil8",
|
|
@@ -533,7 +554,7 @@ export class Engine {
|
|
|
533
554
|
layout: mainPipelineLayout,
|
|
534
555
|
shaderModule: clothSmoothShaderModule,
|
|
535
556
|
vertexBuffers: fullVertexBuffers,
|
|
536
|
-
|
|
557
|
+
fragmentTargets: sceneTargets,
|
|
537
558
|
cullMode: "none",
|
|
538
559
|
depthStencil: {
|
|
539
560
|
format: "depth24plus-stencil8",
|
|
@@ -546,7 +567,7 @@ export class Engine {
|
|
|
546
567
|
layout: mainPipelineLayout,
|
|
547
568
|
shaderModule: clothRoughShaderModule,
|
|
548
569
|
vertexBuffers: fullVertexBuffers,
|
|
549
|
-
|
|
570
|
+
fragmentTargets: sceneTargets,
|
|
550
571
|
cullMode: "none",
|
|
551
572
|
depthStencil: {
|
|
552
573
|
format: "depth24plus-stencil8",
|
|
@@ -559,7 +580,7 @@ export class Engine {
|
|
|
559
580
|
layout: mainPipelineLayout,
|
|
560
581
|
shaderModule: metalShaderModule,
|
|
561
582
|
vertexBuffers: fullVertexBuffers,
|
|
562
|
-
|
|
583
|
+
fragmentTargets: sceneTargets,
|
|
563
584
|
cullMode: "none",
|
|
564
585
|
depthStencil: {
|
|
565
586
|
format: "depth24plus-stencil8",
|
|
@@ -572,7 +593,7 @@ export class Engine {
|
|
|
572
593
|
layout: mainPipelineLayout,
|
|
573
594
|
shaderModule: bodyShaderModule,
|
|
574
595
|
vertexBuffers: fullVertexBuffers,
|
|
575
|
-
|
|
596
|
+
fragmentTargets: sceneTargets,
|
|
576
597
|
cullMode: "none",
|
|
577
598
|
depthStencil: {
|
|
578
599
|
format: "depth24plus-stencil8",
|
|
@@ -585,7 +606,7 @@ export class Engine {
|
|
|
585
606
|
layout: mainPipelineLayout,
|
|
586
607
|
shaderModule: eyeShaderModule,
|
|
587
608
|
vertexBuffers: fullVertexBuffers,
|
|
588
|
-
|
|
609
|
+
fragmentTargets: sceneTargets,
|
|
589
610
|
cullMode: "none",
|
|
590
611
|
depthStencil: {
|
|
591
612
|
format: "depth24plus-stencil8",
|
|
@@ -598,7 +619,7 @@ export class Engine {
|
|
|
598
619
|
layout: mainPipelineLayout,
|
|
599
620
|
shaderModule: stockingsShaderModule,
|
|
600
621
|
vertexBuffers: fullVertexBuffers,
|
|
601
|
-
|
|
622
|
+
fragmentTargets: sceneTargets,
|
|
602
623
|
cullMode: "none",
|
|
603
624
|
depthStencil: {
|
|
604
625
|
format: "depth24plus-stencil8",
|
|
@@ -661,10 +682,8 @@ export class Engine {
|
|
|
661
682
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
662
683
|
});
|
|
663
684
|
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();
|
|
685
|
+
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
686
|
+
this.bakeBrdfLut();
|
|
668
687
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
669
688
|
this.perFrameBindGroup = this.device.createBindGroup({
|
|
670
689
|
label: "main per-frame bind group",
|
|
@@ -676,8 +695,7 @@ export class Engine {
|
|
|
676
695
|
{ binding: 3, resource: this.shadowMapDepthView },
|
|
677
696
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
678
697
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
679
|
-
{ binding: 9, resource: this.
|
|
680
|
-
{ binding: 10, resource: this.ltcMagLutView },
|
|
698
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
681
699
|
],
|
|
682
700
|
});
|
|
683
701
|
this.groundShadowBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -741,7 +759,8 @@ export class Engine {
|
|
|
741
759
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
742
760
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
743
761
|
}
|
|
744
|
-
|
|
762
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
763
|
+
@fragment fn fs(i: VO) -> FSOut {
|
|
745
764
|
let n = normalize(i.normal);
|
|
746
765
|
let centerDist = length(i.worldPos.xz);
|
|
747
766
|
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 +798,10 @@ export class Engine {
|
|
|
779
798
|
var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
780
799
|
baseColor *= noiseTint;
|
|
781
800
|
let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
|
|
782
|
-
|
|
801
|
+
var out: FSOut;
|
|
802
|
+
out.color = vec4f(finalColor * edgeFade, edgeFade);
|
|
803
|
+
out.mask = 0.0;
|
|
804
|
+
return out;
|
|
783
805
|
}
|
|
784
806
|
`,
|
|
785
807
|
});
|
|
@@ -788,7 +810,7 @@ export class Engine {
|
|
|
788
810
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
789
811
|
shaderModule: groundShadowShader,
|
|
790
812
|
vertexBuffers: fullVertexBuffers,
|
|
791
|
-
|
|
813
|
+
fragmentTargets: sceneTargets,
|
|
792
814
|
cullMode: "back",
|
|
793
815
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
794
816
|
});
|
|
@@ -897,8 +919,12 @@ export class Engine {
|
|
|
897
919
|
return output;
|
|
898
920
|
}
|
|
899
921
|
|
|
900
|
-
|
|
901
|
-
|
|
922
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
923
|
+
@fragment fn fs() -> FSOut {
|
|
924
|
+
var out: FSOut;
|
|
925
|
+
out.color = material.edgeColor;
|
|
926
|
+
out.mask = 1.0;
|
|
927
|
+
return out;
|
|
902
928
|
}
|
|
903
929
|
`,
|
|
904
930
|
});
|
|
@@ -907,7 +933,7 @@ export class Engine {
|
|
|
907
933
|
layout: outlinePipelineLayout,
|
|
908
934
|
shaderModule: outlineShaderModule,
|
|
909
935
|
vertexBuffers: outlineVertexBuffers,
|
|
910
|
-
|
|
936
|
+
fragmentTargets: sceneTargets,
|
|
911
937
|
cullMode: "back",
|
|
912
938
|
depthStencil: {
|
|
913
939
|
format: "depth24plus-stencil8",
|
|
@@ -942,6 +968,7 @@ export class Engine {
|
|
|
942
968
|
entries: [
|
|
943
969
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
944
970
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
971
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
945
972
|
],
|
|
946
973
|
});
|
|
947
974
|
this.bloomDownsampleBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -973,6 +1000,7 @@ export class Engine {
|
|
|
973
1000
|
code: `${bloomFullscreenVs}
|
|
974
1001
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
975
1002
|
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
1003
|
+
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
976
1004
|
|
|
977
1005
|
fn luminance(c: vec3f) -> f32 {
|
|
978
1006
|
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
@@ -983,8 +1011,11 @@ export class Engine {
|
|
|
983
1011
|
let s = textureLoad(hdrTex, cc, 0);
|
|
984
1012
|
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
985
1013
|
let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
|
|
1014
|
+
// Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
|
|
1015
|
+
let mask = textureLoad(maskTex, cc, 0).r;
|
|
1016
|
+
let masked = rgb * mask;
|
|
986
1017
|
// Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
987
|
-
return select(
|
|
1018
|
+
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
988
1019
|
}
|
|
989
1020
|
|
|
990
1021
|
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
@@ -1079,7 +1110,9 @@ export class Engine {
|
|
|
1079
1110
|
`,
|
|
1080
1111
|
});
|
|
1081
1112
|
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] });
|
|
1082
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1113
|
+
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1114
|
+
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
1115
|
+
});
|
|
1083
1116
|
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] });
|
|
1084
1117
|
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
1085
1118
|
label: "bloom blit pipeline",
|
|
@@ -1129,9 +1162,14 @@ export class Engine {
|
|
|
1129
1162
|
// viewU[0] = (exposure, gamma, _, _); viewU[1] = (tint.rgb, intensity)
|
|
1130
1163
|
|
|
1131
1164
|
fn filmic(x: f32) -> f32 {
|
|
1165
|
+
// Re-fit against Blender 3.6 Filmic MHC anchors (sobotka/filmic-blender
|
|
1166
|
+
// look_medium-high-contrast.spi1d). Previous curve was compressed:
|
|
1167
|
+
// midtones too bright, highlights too dim — flattened contrast, read
|
|
1168
|
+
// as "washed-out" on saturated surfaces (hair especially).
|
|
1169
|
+
// Reference checkpoints: linear 0.18 → ~0.395, linear 1.0 → ~0.83.
|
|
1132
1170
|
var lut = array<f32, 14>(
|
|
1133
|
-
0.
|
|
1134
|
-
0.
|
|
1171
|
+
0.0028, 0.0068, 0.0151, 0.0313, 0.0610, 0.1120, 0.1920,
|
|
1172
|
+
0.3060, 0.4590, 0.6310, 0.8200, 0.9070, 0.9620, 0.9890
|
|
1135
1173
|
);
|
|
1136
1174
|
let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
|
|
1137
1175
|
let i = u32(t);
|
|
@@ -1152,7 +1190,8 @@ export class Engine {
|
|
|
1152
1190
|
let fullSz = vec2f(textureDimensions(hdrTex));
|
|
1153
1191
|
let bloomSz = vec2f(textureDimensions(bloomTex));
|
|
1154
1192
|
// Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.
|
|
1155
|
-
|
|
1193
|
+
// fragCoord.xy is already at pixel center (e.g. 0.5, 0.5 for first pixel).
|
|
1194
|
+
let bloomUv = fragCoord.xy / max(fullSz, vec2f(1.0));
|
|
1156
1195
|
let tint = viewU[1].xyz;
|
|
1157
1196
|
let intensity = viewU[1].w;
|
|
1158
1197
|
let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
|
|
@@ -1308,6 +1347,22 @@ export class Engine {
|
|
|
1308
1347
|
format: Engine.HDR_FORMAT,
|
|
1309
1348
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1310
1349
|
});
|
|
1350
|
+
// Bloom-mask MRT attachments — same dims + MSAA as HDR so they share the render pass.
|
|
1351
|
+
// MS buffer gets resolved into maskResolveTexture, which the bloom blit pass samples.
|
|
1352
|
+
this.multisampleMaskTexture = this.device.createTexture({
|
|
1353
|
+
label: "multisample bloom mask",
|
|
1354
|
+
size: [width, height],
|
|
1355
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
1356
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1357
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1358
|
+
});
|
|
1359
|
+
this.maskResolveTexture = this.device.createTexture({
|
|
1360
|
+
label: "bloom mask resolve",
|
|
1361
|
+
size: [width, height],
|
|
1362
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1363
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1364
|
+
});
|
|
1365
|
+
this.maskResolveView = this.maskResolveTexture.createView();
|
|
1311
1366
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
1312
1367
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
1313
1368
|
const bw = Math.max(1, Math.floor(width / 2));
|
|
@@ -1352,9 +1407,16 @@ export class Engine {
|
|
|
1352
1407
|
loadOp: "clear",
|
|
1353
1408
|
storeOp: "store",
|
|
1354
1409
|
};
|
|
1410
|
+
const maskAttachment = {
|
|
1411
|
+
view: this.multisampleMaskTexture.createView(),
|
|
1412
|
+
resolveTarget: this.maskResolveView,
|
|
1413
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1414
|
+
loadOp: "clear",
|
|
1415
|
+
storeOp: "store",
|
|
1416
|
+
};
|
|
1355
1417
|
this.renderPassDescriptor = {
|
|
1356
1418
|
label: "renderPass",
|
|
1357
|
-
colorAttachments: [colorAttachment],
|
|
1419
|
+
colorAttachments: [colorAttachment, maskAttachment],
|
|
1358
1420
|
depthStencilAttachment: {
|
|
1359
1421
|
view: depthTextureView,
|
|
1360
1422
|
depthClearValue: 1.0,
|
|
@@ -1386,6 +1448,7 @@ export class Engine {
|
|
|
1386
1448
|
entries: [
|
|
1387
1449
|
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
1388
1450
|
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
1451
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
1389
1452
|
],
|
|
1390
1453
|
});
|
|
1391
1454
|
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
@@ -2116,16 +2179,20 @@ export class Engine {
|
|
|
2116
2179
|
premultiplyAlpha: "none",
|
|
2117
2180
|
colorSpaceConversion: "none",
|
|
2118
2181
|
});
|
|
2182
|
+
const mipLevelCount = Math.floor(Math.log2(Math.max(imageBitmap.width, imageBitmap.height))) + 1;
|
|
2119
2183
|
const texture = this.device.createTexture({
|
|
2120
2184
|
label: `texture: ${cacheKey}`,
|
|
2121
2185
|
size: [imageBitmap.width, imageBitmap.height],
|
|
2122
2186
|
format: "rgba8unorm-srgb",
|
|
2187
|
+
mipLevelCount,
|
|
2123
2188
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
2124
2189
|
});
|
|
2125
2190
|
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
2126
2191
|
imageBitmap.width,
|
|
2127
2192
|
imageBitmap.height,
|
|
2128
2193
|
]);
|
|
2194
|
+
if (mipLevelCount > 1)
|
|
2195
|
+
this.generateMipmaps(texture, mipLevelCount);
|
|
2129
2196
|
this.textureCache.set(cacheKey, texture);
|
|
2130
2197
|
inst.textureCacheKeys.push(cacheKey);
|
|
2131
2198
|
return texture;
|
|
@@ -2134,6 +2201,64 @@ export class Engine {
|
|
|
2134
2201
|
return null;
|
|
2135
2202
|
}
|
|
2136
2203
|
}
|
|
2204
|
+
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
2205
|
+
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
2206
|
+
generateMipmaps(texture, mipLevelCount) {
|
|
2207
|
+
if (!this.mipBlitPipeline || !this.mipBlitSampler) {
|
|
2208
|
+
this.mipBlitSampler = this.device.createSampler({
|
|
2209
|
+
magFilter: "linear",
|
|
2210
|
+
minFilter: "linear",
|
|
2211
|
+
addressModeU: "clamp-to-edge",
|
|
2212
|
+
addressModeV: "clamp-to-edge",
|
|
2213
|
+
});
|
|
2214
|
+
const module = this.device.createShaderModule({
|
|
2215
|
+
label: "mipmap blit",
|
|
2216
|
+
code: /* wgsl */ `
|
|
2217
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
2218
|
+
@group(0) @binding(1) var samp: sampler;
|
|
2219
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
2220
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
2221
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
2222
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
2223
|
+
}
|
|
2224
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
2225
|
+
let dstDims = vec2f(textureDimensions(src)) * 0.5;
|
|
2226
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
2227
|
+
return textureSampleLevel(src, samp, uv, 0.0);
|
|
2228
|
+
}
|
|
2229
|
+
`,
|
|
2230
|
+
});
|
|
2231
|
+
this.mipBlitPipeline = this.device.createRenderPipeline({
|
|
2232
|
+
label: "mipmap blit pipeline",
|
|
2233
|
+
layout: "auto",
|
|
2234
|
+
vertex: { module, entryPoint: "vs" },
|
|
2235
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm-srgb" }] },
|
|
2236
|
+
primitive: { topology: "triangle-list" },
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
const encoder = this.device.createCommandEncoder({ label: "mipgen" });
|
|
2240
|
+
for (let level = 1; level < mipLevelCount; level++) {
|
|
2241
|
+
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
2242
|
+
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
|
|
2243
|
+
const bindGroup = this.device.createBindGroup({
|
|
2244
|
+
layout: this.mipBlitPipeline.getBindGroupLayout(0),
|
|
2245
|
+
entries: [
|
|
2246
|
+
{ binding: 0, resource: srcView },
|
|
2247
|
+
{ binding: 1, resource: this.mipBlitSampler },
|
|
2248
|
+
],
|
|
2249
|
+
});
|
|
2250
|
+
const pass = encoder.beginRenderPass({
|
|
2251
|
+
colorAttachments: [
|
|
2252
|
+
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
2253
|
+
],
|
|
2254
|
+
});
|
|
2255
|
+
pass.setPipeline(this.mipBlitPipeline);
|
|
2256
|
+
pass.setBindGroup(0, bindGroup);
|
|
2257
|
+
pass.draw(3);
|
|
2258
|
+
pass.end();
|
|
2259
|
+
}
|
|
2260
|
+
this.device.queue.submit([encoder.finish()]);
|
|
2261
|
+
}
|
|
2137
2262
|
renderGround(pass) {
|
|
2138
2263
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall)
|
|
2139
2264
|
return;
|
|
@@ -2467,5 +2592,9 @@ export class Engine {
|
|
|
2467
2592
|
Engine.instance = null;
|
|
2468
2593
|
Engine.MULTISAMPLE_COUNT = 4;
|
|
2469
2594
|
Engine.HDR_FORMAT = "rgba16float";
|
|
2595
|
+
/** Single-channel mask written alongside HDR color — 1 = model geometry (contributes
|
|
2596
|
+
* to bloom), 0 = ground (never blooms). Sampled by the bloom blit pass to gate the
|
|
2597
|
+
* prefilter so ground brightness can't halo the scene. */
|
|
2598
|
+
Engine.BLOOM_MASK_FORMAT = "r8unorm";
|
|
2470
2599
|
Engine.BLOOM_MAX_LEVELS = 7;
|
|
2471
|
-
Engine.SHADOW_MAP_SIZE =
|
|
2600
|
+
Engine.SHADOW_MAP_SIZE = 2048;
|