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/src/engine.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type AssetReader,
|
|
15
15
|
} from "./asset-reader"
|
|
16
16
|
import { DEFAULT_SHADER_WGSL } from "./shaders/default"
|
|
17
|
-
import {
|
|
17
|
+
import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut"
|
|
18
18
|
import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
19
19
|
import { FACE_SHADER_WGSL } from "./shaders/face"
|
|
20
20
|
import { HAIR_SHADER_WGSL } from "./shaders/hair"
|
|
@@ -78,21 +78,23 @@ export const DEFAULT_BLOOM_OPTIONS: BloomOptions = {
|
|
|
78
78
|
knee: 0.5,
|
|
79
79
|
radius: 4.0,
|
|
80
80
|
color: new Vec3(1.0, 0.7247558832168579, 0.6487361788749695),
|
|
81
|
-
intensity: 0.
|
|
81
|
+
intensity: 0.05,
|
|
82
82
|
clamp: 0.0,
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** Blender Color Management / View (rendering.txt: Filmic, exposure, gamma). `look` is reserved for future curve tweaks. */
|
|
86
86
|
export type ViewTransformOptions = {
|
|
87
|
-
/** Stops applied before Filmic: `linear *= 2^exposure
|
|
87
|
+
/** Stops applied before Filmic: `linear *= 2^exposure`. */
|
|
88
88
|
exposure: number
|
|
89
89
|
/** After Filmic, display gamma (`pow(rgb, 1/gamma)`). */
|
|
90
90
|
gamma: number
|
|
91
91
|
look: "default" | "medium_high_contrast"
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
95
|
+
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
94
96
|
export const DEFAULT_VIEW_TRANSFORM: ViewTransformOptions = {
|
|
95
|
-
exposure:
|
|
97
|
+
exposure: 0.6,
|
|
96
98
|
gamma: 1.0,
|
|
97
99
|
look: "medium_high_contrast",
|
|
98
100
|
}
|
|
@@ -212,6 +214,13 @@ export class Engine {
|
|
|
212
214
|
private hdrResolveTexture!: GPUTexture
|
|
213
215
|
private static readonly MULTISAMPLE_COUNT = 4
|
|
214
216
|
private static readonly HDR_FORMAT: GPUTextureFormat = "rgba16float"
|
|
217
|
+
/** Single-channel mask written alongside HDR color — 1 = model geometry (contributes
|
|
218
|
+
* to bloom), 0 = ground (never blooms). Sampled by the bloom blit pass to gate the
|
|
219
|
+
* prefilter so ground brightness can't halo the scene. */
|
|
220
|
+
private static readonly BLOOM_MASK_FORMAT: GPUTextureFormat = "r8unorm"
|
|
221
|
+
private multisampleMaskTexture!: GPUTexture
|
|
222
|
+
private maskResolveTexture!: GPUTexture
|
|
223
|
+
private maskResolveView!: GPUTextureView
|
|
215
224
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
216
225
|
private compositePassDescriptor!: GPURenderPassDescriptor
|
|
217
226
|
private compositePipeline!: GPURenderPipeline
|
|
@@ -256,11 +265,9 @@ export class Engine {
|
|
|
256
265
|
private hasGround = false
|
|
257
266
|
private shadowMapTexture!: GPUTexture
|
|
258
267
|
private shadowMapDepthView!: GPUTextureView
|
|
259
|
-
private
|
|
260
|
-
private
|
|
261
|
-
private
|
|
262
|
-
private ltcMagLutView!: GPUTextureView
|
|
263
|
-
private static readonly SHADOW_MAP_SIZE = 4096
|
|
268
|
+
private brdfLutTexture!: GPUTexture
|
|
269
|
+
private brdfLutView!: GPUTextureView
|
|
270
|
+
private static readonly SHADOW_MAP_SIZE = 2048
|
|
264
271
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
265
272
|
private shadowLightVPBuffer!: GPUBuffer
|
|
266
273
|
private shadowLightVPMatrix = new Float32Array(16)
|
|
@@ -287,6 +294,8 @@ export class Engine {
|
|
|
287
294
|
private modelInstances = new Map<string, ModelInstance>()
|
|
288
295
|
private materialSampler!: GPUSampler
|
|
289
296
|
private textureCache = new Map<string, GPUTexture>()
|
|
297
|
+
private mipBlitPipeline: GPURenderPipeline | null = null
|
|
298
|
+
private mipBlitSampler: GPUSampler | null = null
|
|
290
299
|
private _nextDefaultModelId = 0
|
|
291
300
|
|
|
292
301
|
// IK and physics enabled at engine level (same for all models)
|
|
@@ -428,9 +437,12 @@ export class Engine {
|
|
|
428
437
|
private writeBloomUniforms(): void {
|
|
429
438
|
const b = this.bloomSettings
|
|
430
439
|
const bu = this.bloomBlitUniformData
|
|
431
|
-
// EEVEE prefilter: threshold,
|
|
440
|
+
// EEVEE prefilter: threshold, knee_half, clamp (0 → disabled), _unused
|
|
441
|
+
// Blender halves the knee before passing to the shader (eevee_bloom.c: knee * 0.5f).
|
|
442
|
+
// The blit shader's quadratic soft-knee curve uses knee_half as the offset from threshold,
|
|
443
|
+
// so the soft ramp spans [threshold - knee/2 .. threshold + knee/2] — NOT [threshold - knee .. threshold + knee].
|
|
432
444
|
bu[0] = b.threshold
|
|
433
|
-
bu[1] = b.knee
|
|
445
|
+
bu[1] = b.knee * 0.5
|
|
434
446
|
bu[2] = b.clamp
|
|
435
447
|
bu[3] = 0.0
|
|
436
448
|
this.device.queue.writeBuffer(this.bloomBlitUniformBuffer, 0, bu)
|
|
@@ -473,51 +485,24 @@ export class Engine {
|
|
|
473
485
|
Engine.instance = this
|
|
474
486
|
}
|
|
475
487
|
|
|
476
|
-
// One-shot bake of EEVEE's BRDF
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
this.dfgLutView = this.dfgLutTexture.createView()
|
|
487
|
-
|
|
488
|
-
const module = this.device.createShaderModule({ label: "DFG LUT bake", code: DFG_LUT_WGSL })
|
|
489
|
-
const pipeline = this.device.createRenderPipeline({
|
|
490
|
-
label: "DFG LUT bake pipeline",
|
|
491
|
-
layout: "auto",
|
|
492
|
-
vertex: { module, entryPoint: "vs" },
|
|
493
|
-
fragment: { module, entryPoint: "fs", targets: [{ format: "rg16float" }] },
|
|
494
|
-
primitive: { topology: "triangle-list" },
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
const enc = this.device.createCommandEncoder({ label: "DFG LUT bake encoder" })
|
|
498
|
-
const pass = enc.beginRenderPass({
|
|
499
|
-
colorAttachments: [
|
|
500
|
-
{ view: this.dfgLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
501
|
-
],
|
|
502
|
-
})
|
|
503
|
-
pass.setPipeline(pipeline)
|
|
504
|
-
pass.draw(3, 1, 0, 0)
|
|
505
|
-
pass.end()
|
|
506
|
-
this.device.queue.submit([enc.finish()])
|
|
507
|
-
}
|
|
488
|
+
// One-shot bake of EEVEE's combined BRDF LUT — DFG (bsdf_lut_frag.glsl) packed
|
|
489
|
+
// with ltc_mag_ggx (eevee_lut.c) into a single 64×64 rgba8unorm texture:
|
|
490
|
+
// .rg = split-sum DFG → F_brdf_*_scatter
|
|
491
|
+
// .ba = LTC magnitude → ltc_brdf_scale_from_lut
|
|
492
|
+
// One texture fetch per fragment replaces the previous 2–3 taps. rgba8unorm
|
|
493
|
+
// (vs rgba16float) halves sample bandwidth; DFG/LTC values fit [0,1] cleanly.
|
|
494
|
+
private bakeBrdfLut() {
|
|
495
|
+
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
496
|
+
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).")
|
|
497
|
+
}
|
|
508
498
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
this.ltcMagLutTexture = this.device.createTexture({
|
|
513
|
-
label: "LTC mag LUT",
|
|
499
|
+
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
500
|
+
const ltcTemp = this.device.createTexture({
|
|
501
|
+
label: "LTC mag LUT (bake input)",
|
|
514
502
|
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
515
503
|
format: "rg16float",
|
|
516
504
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
517
505
|
})
|
|
518
|
-
this.ltcMagLutView = this.ltcMagLutTexture.createView()
|
|
519
|
-
|
|
520
|
-
// Float32 → float16 bits. rg16float writeTexture expects packed half floats.
|
|
521
506
|
const n = LTC_MAG_LUT_DATA.length
|
|
522
507
|
const half = new Uint16Array(n)
|
|
523
508
|
const f32 = new Float32Array(1)
|
|
@@ -527,22 +512,58 @@ export class Engine {
|
|
|
527
512
|
const x = u32[0]
|
|
528
513
|
const sign = (x >>> 16) & 0x8000
|
|
529
514
|
let exp = ((x >>> 23) & 0xff) - 127 + 15
|
|
530
|
-
|
|
515
|
+
const mant = x & 0x7fffff
|
|
531
516
|
if (exp <= 0) {
|
|
532
|
-
half[i] = sign
|
|
517
|
+
half[i] = sign
|
|
533
518
|
} else if (exp >= 31) {
|
|
534
|
-
half[i] = sign | 0x7c00
|
|
519
|
+
half[i] = sign | 0x7c00
|
|
535
520
|
} else {
|
|
536
521
|
half[i] = sign | (exp << 10) | (mant >>> 13)
|
|
537
522
|
}
|
|
538
523
|
}
|
|
539
|
-
|
|
540
524
|
this.device.queue.writeTexture(
|
|
541
|
-
{ texture:
|
|
525
|
+
{ texture: ltcTemp },
|
|
542
526
|
half,
|
|
543
527
|
{ bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE },
|
|
544
|
-
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 }
|
|
528
|
+
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 },
|
|
545
529
|
)
|
|
530
|
+
|
|
531
|
+
this.brdfLutTexture = this.device.createTexture({
|
|
532
|
+
label: "BRDF LUT (DFG + LTC packed)",
|
|
533
|
+
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
534
|
+
format: "rgba8unorm",
|
|
535
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
536
|
+
})
|
|
537
|
+
this.brdfLutView = this.brdfLutTexture.createView()
|
|
538
|
+
|
|
539
|
+
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL })
|
|
540
|
+
const pipeline = this.device.createRenderPipeline({
|
|
541
|
+
label: "BRDF LUT bake pipeline",
|
|
542
|
+
layout: "auto",
|
|
543
|
+
vertex: { module, entryPoint: "vs" },
|
|
544
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
545
|
+
primitive: { topology: "triangle-list" },
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
const bakeBindGroup = this.device.createBindGroup({
|
|
549
|
+
label: "BRDF LUT bake bind group",
|
|
550
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
551
|
+
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" })
|
|
555
|
+
const pass = enc.beginRenderPass({
|
|
556
|
+
colorAttachments: [
|
|
557
|
+
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
558
|
+
],
|
|
559
|
+
})
|
|
560
|
+
pass.setPipeline(pipeline)
|
|
561
|
+
pass.setBindGroup(0, bakeBindGroup)
|
|
562
|
+
pass.draw(3, 1, 0, 0)
|
|
563
|
+
pass.end()
|
|
564
|
+
this.device.queue.submit([enc.finish()])
|
|
565
|
+
|
|
566
|
+
ltcTemp.destroy()
|
|
546
567
|
}
|
|
547
568
|
|
|
548
569
|
private createRenderPipeline(config: {
|
|
@@ -551,11 +572,13 @@ export class Engine {
|
|
|
551
572
|
shaderModule: GPUShaderModule
|
|
552
573
|
vertexBuffers: GPUVertexBufferLayout[]
|
|
553
574
|
fragmentTarget?: GPUColorTargetState
|
|
575
|
+
fragmentTargets?: GPUColorTargetState[]
|
|
554
576
|
fragmentEntryPoint?: string
|
|
555
577
|
cullMode?: GPUCullMode
|
|
556
578
|
depthStencil?: GPUDepthStencilState
|
|
557
579
|
multisample?: GPUMultisampleState
|
|
558
580
|
}): GPURenderPipeline {
|
|
581
|
+
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined)
|
|
559
582
|
return this.device.createRenderPipeline({
|
|
560
583
|
label: config.label,
|
|
561
584
|
layout: config.layout,
|
|
@@ -563,11 +586,11 @@ export class Engine {
|
|
|
563
586
|
module: config.shaderModule,
|
|
564
587
|
buffers: config.vertexBuffers,
|
|
565
588
|
},
|
|
566
|
-
fragment:
|
|
589
|
+
fragment: targets
|
|
567
590
|
? {
|
|
568
591
|
module: config.shaderModule,
|
|
569
592
|
entryPoint: config.fragmentEntryPoint,
|
|
570
|
-
targets
|
|
593
|
+
targets,
|
|
571
594
|
}
|
|
572
595
|
: undefined,
|
|
573
596
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
@@ -580,6 +603,7 @@ export class Engine {
|
|
|
580
603
|
this.materialSampler = this.device.createSampler({
|
|
581
604
|
magFilter: "linear",
|
|
582
605
|
minFilter: "linear",
|
|
606
|
+
mipmapFilter: "linear",
|
|
583
607
|
addressModeU: "repeat",
|
|
584
608
|
addressModeV: "repeat",
|
|
585
609
|
})
|
|
@@ -641,6 +665,12 @@ export class Engine {
|
|
|
641
665
|
},
|
|
642
666
|
}
|
|
643
667
|
|
|
668
|
+
// Bloom mask target — r8unorm has no alpha channel, so src-alpha blending is invalid.
|
|
669
|
+
// Use replace mode: depth test already rejects occluded fragments, so last-writer-wins
|
|
670
|
+
// on surviving pixels gives the right result (ground writes 0; models/outlines write 1).
|
|
671
|
+
const maskBlend: GPUColorTargetState = { format: Engine.BLOOM_MASK_FORMAT }
|
|
672
|
+
const sceneTargets: GPUColorTargetState[] = [standardBlend, maskBlend]
|
|
673
|
+
|
|
644
674
|
const shaderModule = this.device.createShaderModule({
|
|
645
675
|
label: "default model shader",
|
|
646
676
|
code: DEFAULT_SHADER_WGSL,
|
|
@@ -697,7 +727,6 @@ export class Engine {
|
|
|
697
727
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
698
728
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
699
729
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
700
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
701
730
|
],
|
|
702
731
|
})
|
|
703
732
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -730,7 +759,7 @@ export class Engine {
|
|
|
730
759
|
layout: mainPipelineLayout,
|
|
731
760
|
shaderModule,
|
|
732
761
|
vertexBuffers: fullVertexBuffers,
|
|
733
|
-
|
|
762
|
+
fragmentTargets: sceneTargets,
|
|
734
763
|
cullMode: "none",
|
|
735
764
|
depthStencil: {
|
|
736
765
|
format: "depth24plus-stencil8",
|
|
@@ -744,7 +773,7 @@ export class Engine {
|
|
|
744
773
|
layout: mainPipelineLayout,
|
|
745
774
|
shaderModule: faceShaderModule,
|
|
746
775
|
vertexBuffers: fullVertexBuffers,
|
|
747
|
-
|
|
776
|
+
fragmentTargets: sceneTargets,
|
|
748
777
|
cullMode: "none",
|
|
749
778
|
depthStencil: {
|
|
750
779
|
format: "depth24plus-stencil8",
|
|
@@ -758,7 +787,7 @@ export class Engine {
|
|
|
758
787
|
layout: mainPipelineLayout,
|
|
759
788
|
shaderModule: hairShaderModule,
|
|
760
789
|
vertexBuffers: fullVertexBuffers,
|
|
761
|
-
|
|
790
|
+
fragmentTargets: sceneTargets,
|
|
762
791
|
cullMode: "none",
|
|
763
792
|
depthStencil: {
|
|
764
793
|
format: "depth24plus-stencil8",
|
|
@@ -772,7 +801,7 @@ export class Engine {
|
|
|
772
801
|
layout: mainPipelineLayout,
|
|
773
802
|
shaderModule: clothSmoothShaderModule,
|
|
774
803
|
vertexBuffers: fullVertexBuffers,
|
|
775
|
-
|
|
804
|
+
fragmentTargets: sceneTargets,
|
|
776
805
|
cullMode: "none",
|
|
777
806
|
depthStencil: {
|
|
778
807
|
format: "depth24plus-stencil8",
|
|
@@ -786,7 +815,7 @@ export class Engine {
|
|
|
786
815
|
layout: mainPipelineLayout,
|
|
787
816
|
shaderModule: clothRoughShaderModule,
|
|
788
817
|
vertexBuffers: fullVertexBuffers,
|
|
789
|
-
|
|
818
|
+
fragmentTargets: sceneTargets,
|
|
790
819
|
cullMode: "none",
|
|
791
820
|
depthStencil: {
|
|
792
821
|
format: "depth24plus-stencil8",
|
|
@@ -800,7 +829,7 @@ export class Engine {
|
|
|
800
829
|
layout: mainPipelineLayout,
|
|
801
830
|
shaderModule: metalShaderModule,
|
|
802
831
|
vertexBuffers: fullVertexBuffers,
|
|
803
|
-
|
|
832
|
+
fragmentTargets: sceneTargets,
|
|
804
833
|
cullMode: "none",
|
|
805
834
|
depthStencil: {
|
|
806
835
|
format: "depth24plus-stencil8",
|
|
@@ -814,7 +843,7 @@ export class Engine {
|
|
|
814
843
|
layout: mainPipelineLayout,
|
|
815
844
|
shaderModule: bodyShaderModule,
|
|
816
845
|
vertexBuffers: fullVertexBuffers,
|
|
817
|
-
|
|
846
|
+
fragmentTargets: sceneTargets,
|
|
818
847
|
cullMode: "none",
|
|
819
848
|
depthStencil: {
|
|
820
849
|
format: "depth24plus-stencil8",
|
|
@@ -828,7 +857,7 @@ export class Engine {
|
|
|
828
857
|
layout: mainPipelineLayout,
|
|
829
858
|
shaderModule: eyeShaderModule,
|
|
830
859
|
vertexBuffers: fullVertexBuffers,
|
|
831
|
-
|
|
860
|
+
fragmentTargets: sceneTargets,
|
|
832
861
|
cullMode: "none",
|
|
833
862
|
depthStencil: {
|
|
834
863
|
format: "depth24plus-stencil8",
|
|
@@ -842,7 +871,7 @@ export class Engine {
|
|
|
842
871
|
layout: mainPipelineLayout,
|
|
843
872
|
shaderModule: stockingsShaderModule,
|
|
844
873
|
vertexBuffers: fullVertexBuffers,
|
|
845
|
-
|
|
874
|
+
fragmentTargets: sceneTargets,
|
|
846
875
|
cullMode: "none",
|
|
847
876
|
depthStencil: {
|
|
848
877
|
format: "depth24plus-stencil8",
|
|
@@ -907,10 +936,8 @@ export class Engine {
|
|
|
907
936
|
})
|
|
908
937
|
this.shadowMapDepthView = this.shadowMapTexture.createView()
|
|
909
938
|
|
|
910
|
-
// One-shot bake of Blender EEVEE's BRDF
|
|
911
|
-
this.
|
|
912
|
-
// Upload static LTC GGX magnitude LUT for direct-specular energy compensation.
|
|
913
|
-
this.uploadLtcMagLut()
|
|
939
|
+
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
940
|
+
this.bakeBrdfLut()
|
|
914
941
|
|
|
915
942
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
916
943
|
this.perFrameBindGroup = this.device.createBindGroup({
|
|
@@ -923,8 +950,7 @@ export class Engine {
|
|
|
923
950
|
{ binding: 3, resource: this.shadowMapDepthView },
|
|
924
951
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
925
952
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
926
|
-
{ binding: 9, resource: this.
|
|
927
|
-
{ binding: 10, resource: this.ltcMagLutView },
|
|
953
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
928
954
|
],
|
|
929
955
|
})
|
|
930
956
|
|
|
@@ -989,7 +1015,8 @@ export class Engine {
|
|
|
989
1015
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
990
1016
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
991
1017
|
}
|
|
992
|
-
|
|
1018
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
1019
|
+
@fragment fn fs(i: VO) -> FSOut {
|
|
993
1020
|
let n = normalize(i.normal);
|
|
994
1021
|
let centerDist = length(i.worldPos.xz);
|
|
995
1022
|
let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
|
|
@@ -1027,7 +1054,10 @@ export class Engine {
|
|
|
1027
1054
|
var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
1028
1055
|
baseColor *= noiseTint;
|
|
1029
1056
|
let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
|
|
1030
|
-
|
|
1057
|
+
var out: FSOut;
|
|
1058
|
+
out.color = vec4f(finalColor * edgeFade, edgeFade);
|
|
1059
|
+
out.mask = 0.0;
|
|
1060
|
+
return out;
|
|
1031
1061
|
}
|
|
1032
1062
|
`,
|
|
1033
1063
|
})
|
|
@@ -1036,7 +1066,7 @@ export class Engine {
|
|
|
1036
1066
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
1037
1067
|
shaderModule: groundShadowShader,
|
|
1038
1068
|
vertexBuffers: fullVertexBuffers,
|
|
1039
|
-
|
|
1069
|
+
fragmentTargets: sceneTargets,
|
|
1040
1070
|
cullMode: "back",
|
|
1041
1071
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
1042
1072
|
})
|
|
@@ -1149,8 +1179,12 @@ export class Engine {
|
|
|
1149
1179
|
return output;
|
|
1150
1180
|
}
|
|
1151
1181
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1182
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
1183
|
+
@fragment fn fs() -> FSOut {
|
|
1184
|
+
var out: FSOut;
|
|
1185
|
+
out.color = material.edgeColor;
|
|
1186
|
+
out.mask = 1.0;
|
|
1187
|
+
return out;
|
|
1154
1188
|
}
|
|
1155
1189
|
`,
|
|
1156
1190
|
})
|
|
@@ -1160,7 +1194,7 @@ export class Engine {
|
|
|
1160
1194
|
layout: outlinePipelineLayout,
|
|
1161
1195
|
shaderModule: outlineShaderModule,
|
|
1162
1196
|
vertexBuffers: outlineVertexBuffers,
|
|
1163
|
-
|
|
1197
|
+
fragmentTargets: sceneTargets,
|
|
1164
1198
|
cullMode: "back",
|
|
1165
1199
|
depthStencil: {
|
|
1166
1200
|
format: "depth24plus-stencil8",
|
|
@@ -1197,6 +1231,7 @@ export class Engine {
|
|
|
1197
1231
|
entries: [
|
|
1198
1232
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
1199
1233
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1234
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
1200
1235
|
],
|
|
1201
1236
|
})
|
|
1202
1237
|
this.bloomDownsampleBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -1230,6 +1265,7 @@ export class Engine {
|
|
|
1230
1265
|
code: `${bloomFullscreenVs}
|
|
1231
1266
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
1232
1267
|
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
1268
|
+
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
1233
1269
|
|
|
1234
1270
|
fn luminance(c: vec3f) -> f32 {
|
|
1235
1271
|
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
@@ -1240,8 +1276,11 @@ export class Engine {
|
|
|
1240
1276
|
let s = textureLoad(hdrTex, cc, 0);
|
|
1241
1277
|
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
1242
1278
|
let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
|
|
1279
|
+
// Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
|
|
1280
|
+
let mask = textureLoad(maskTex, cc, 0).r;
|
|
1281
|
+
let masked = rgb * mask;
|
|
1243
1282
|
// Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
1244
|
-
return select(
|
|
1283
|
+
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
1245
1284
|
}
|
|
1246
1285
|
|
|
1247
1286
|
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
@@ -1339,7 +1378,9 @@ export class Engine {
|
|
|
1339
1378
|
})
|
|
1340
1379
|
|
|
1341
1380
|
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] })
|
|
1342
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1381
|
+
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1382
|
+
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
1383
|
+
})
|
|
1343
1384
|
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] })
|
|
1344
1385
|
|
|
1345
1386
|
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
@@ -1392,9 +1433,14 @@ export class Engine {
|
|
|
1392
1433
|
// viewU[0] = (exposure, gamma, _, _); viewU[1] = (tint.rgb, intensity)
|
|
1393
1434
|
|
|
1394
1435
|
fn filmic(x: f32) -> f32 {
|
|
1436
|
+
// Re-fit against Blender 3.6 Filmic MHC anchors (sobotka/filmic-blender
|
|
1437
|
+
// look_medium-high-contrast.spi1d). Previous curve was compressed:
|
|
1438
|
+
// midtones too bright, highlights too dim — flattened contrast, read
|
|
1439
|
+
// as "washed-out" on saturated surfaces (hair especially).
|
|
1440
|
+
// Reference checkpoints: linear 0.18 → ~0.395, linear 1.0 → ~0.83.
|
|
1395
1441
|
var lut = array<f32, 14>(
|
|
1396
|
-
0.
|
|
1397
|
-
0.
|
|
1442
|
+
0.0028, 0.0068, 0.0151, 0.0313, 0.0610, 0.1120, 0.1920,
|
|
1443
|
+
0.3060, 0.4590, 0.6310, 0.8200, 0.9070, 0.9620, 0.9890
|
|
1398
1444
|
);
|
|
1399
1445
|
let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
|
|
1400
1446
|
let i = u32(t);
|
|
@@ -1415,7 +1461,8 @@ export class Engine {
|
|
|
1415
1461
|
let fullSz = vec2f(textureDimensions(hdrTex));
|
|
1416
1462
|
let bloomSz = vec2f(textureDimensions(bloomTex));
|
|
1417
1463
|
// Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.
|
|
1418
|
-
|
|
1464
|
+
// fragCoord.xy is already at pixel center (e.g. 0.5, 0.5 for first pixel).
|
|
1465
|
+
let bloomUv = fragCoord.xy / max(fullSz, vec2f(1.0));
|
|
1419
1466
|
let tint = viewU[1].xyz;
|
|
1420
1467
|
let intensity = viewU[1].w;
|
|
1421
1468
|
let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
|
|
@@ -1587,15 +1634,29 @@ export class Engine {
|
|
|
1587
1634
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1588
1635
|
})
|
|
1589
1636
|
|
|
1637
|
+
// Bloom-mask MRT attachments — same dims + MSAA as HDR so they share the render pass.
|
|
1638
|
+
// MS buffer gets resolved into maskResolveTexture, which the bloom blit pass samples.
|
|
1639
|
+
this.multisampleMaskTexture = this.device.createTexture({
|
|
1640
|
+
label: "multisample bloom mask",
|
|
1641
|
+
size: [width, height],
|
|
1642
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
1643
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1644
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1645
|
+
})
|
|
1646
|
+
this.maskResolveTexture = this.device.createTexture({
|
|
1647
|
+
label: "bloom mask resolve",
|
|
1648
|
+
size: [width, height],
|
|
1649
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1650
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1651
|
+
})
|
|
1652
|
+
this.maskResolveView = this.maskResolveTexture.createView()
|
|
1653
|
+
|
|
1590
1654
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
1591
1655
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
1592
1656
|
const bw = Math.max(1, Math.floor(width / 2))
|
|
1593
1657
|
const bh = Math.max(1, Math.floor(height / 2))
|
|
1594
1658
|
const shortSide = Math.max(1, Math.min(bw, bh))
|
|
1595
|
-
this.bloomMipCount = Math.max(
|
|
1596
|
-
1,
|
|
1597
|
-
Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1),
|
|
1598
|
-
)
|
|
1659
|
+
this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1))
|
|
1599
1660
|
this.bloomDownTexture = this.device.createTexture({
|
|
1600
1661
|
label: "bloom down pyramid",
|
|
1601
1662
|
size: [bw, bh],
|
|
@@ -1612,16 +1673,12 @@ export class Engine {
|
|
|
1612
1673
|
})
|
|
1613
1674
|
this.bloomDownMipViews = []
|
|
1614
1675
|
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
1615
|
-
this.bloomDownMipViews.push(
|
|
1616
|
-
this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
|
|
1617
|
-
)
|
|
1676
|
+
this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
|
|
1618
1677
|
}
|
|
1619
1678
|
this.bloomUpMipViews = []
|
|
1620
1679
|
const upLevels = Math.max(1, this.bloomMipCount - 1)
|
|
1621
1680
|
for (let i = 0; i < upLevels; i++) {
|
|
1622
|
-
this.bloomUpMipViews.push(
|
|
1623
|
-
this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
|
|
1624
|
-
)
|
|
1681
|
+
this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
|
|
1625
1682
|
}
|
|
1626
1683
|
|
|
1627
1684
|
this.depthTexture = this.device.createTexture({
|
|
@@ -1642,9 +1699,17 @@ export class Engine {
|
|
|
1642
1699
|
storeOp: "store",
|
|
1643
1700
|
}
|
|
1644
1701
|
|
|
1702
|
+
const maskAttachment: GPURenderPassColorAttachment = {
|
|
1703
|
+
view: this.multisampleMaskTexture.createView(),
|
|
1704
|
+
resolveTarget: this.maskResolveView,
|
|
1705
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1706
|
+
loadOp: "clear",
|
|
1707
|
+
storeOp: "store",
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1645
1710
|
this.renderPassDescriptor = {
|
|
1646
1711
|
label: "renderPass",
|
|
1647
|
-
colorAttachments: [colorAttachment],
|
|
1712
|
+
colorAttachments: [colorAttachment, maskAttachment],
|
|
1648
1713
|
depthStencilAttachment: {
|
|
1649
1714
|
view: depthTextureView,
|
|
1650
1715
|
depthClearValue: 1.0,
|
|
@@ -1679,6 +1744,7 @@ export class Engine {
|
|
|
1679
1744
|
entries: [
|
|
1680
1745
|
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
1681
1746
|
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
1747
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
1682
1748
|
],
|
|
1683
1749
|
})
|
|
1684
1750
|
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
@@ -2534,10 +2600,12 @@ export class Engine {
|
|
|
2534
2600
|
colorSpaceConversion: "none",
|
|
2535
2601
|
})
|
|
2536
2602
|
|
|
2603
|
+
const mipLevelCount = Math.floor(Math.log2(Math.max(imageBitmap.width, imageBitmap.height))) + 1
|
|
2537
2604
|
const texture = this.device.createTexture({
|
|
2538
2605
|
label: `texture: ${cacheKey}`,
|
|
2539
2606
|
size: [imageBitmap.width, imageBitmap.height],
|
|
2540
2607
|
format: "rgba8unorm-srgb",
|
|
2608
|
+
mipLevelCount,
|
|
2541
2609
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
2542
2610
|
})
|
|
2543
2611
|
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
@@ -2545,6 +2613,8 @@ export class Engine {
|
|
|
2545
2613
|
imageBitmap.height,
|
|
2546
2614
|
])
|
|
2547
2615
|
|
|
2616
|
+
if (mipLevelCount > 1) this.generateMipmaps(texture, mipLevelCount)
|
|
2617
|
+
|
|
2548
2618
|
this.textureCache.set(cacheKey, texture)
|
|
2549
2619
|
inst.textureCacheKeys.push(cacheKey)
|
|
2550
2620
|
return texture
|
|
@@ -2553,6 +2623,66 @@ export class Engine {
|
|
|
2553
2623
|
}
|
|
2554
2624
|
}
|
|
2555
2625
|
|
|
2626
|
+
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
2627
|
+
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
2628
|
+
private generateMipmaps(texture: GPUTexture, mipLevelCount: number) {
|
|
2629
|
+
if (!this.mipBlitPipeline || !this.mipBlitSampler) {
|
|
2630
|
+
this.mipBlitSampler = this.device.createSampler({
|
|
2631
|
+
magFilter: "linear",
|
|
2632
|
+
minFilter: "linear",
|
|
2633
|
+
addressModeU: "clamp-to-edge",
|
|
2634
|
+
addressModeV: "clamp-to-edge",
|
|
2635
|
+
})
|
|
2636
|
+
const module = this.device.createShaderModule({
|
|
2637
|
+
label: "mipmap blit",
|
|
2638
|
+
code: /* wgsl */ `
|
|
2639
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
2640
|
+
@group(0) @binding(1) var samp: sampler;
|
|
2641
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
2642
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
2643
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
2644
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
2645
|
+
}
|
|
2646
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
2647
|
+
let dstDims = vec2f(textureDimensions(src)) * 0.5;
|
|
2648
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
2649
|
+
return textureSampleLevel(src, samp, uv, 0.0);
|
|
2650
|
+
}
|
|
2651
|
+
`,
|
|
2652
|
+
})
|
|
2653
|
+
this.mipBlitPipeline = this.device.createRenderPipeline({
|
|
2654
|
+
label: "mipmap blit pipeline",
|
|
2655
|
+
layout: "auto",
|
|
2656
|
+
vertex: { module, entryPoint: "vs" },
|
|
2657
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm-srgb" }] },
|
|
2658
|
+
primitive: { topology: "triangle-list" },
|
|
2659
|
+
})
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const encoder = this.device.createCommandEncoder({ label: "mipgen" })
|
|
2663
|
+
for (let level = 1; level < mipLevelCount; level++) {
|
|
2664
|
+
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 })
|
|
2665
|
+
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 })
|
|
2666
|
+
const bindGroup = this.device.createBindGroup({
|
|
2667
|
+
layout: this.mipBlitPipeline.getBindGroupLayout(0),
|
|
2668
|
+
entries: [
|
|
2669
|
+
{ binding: 0, resource: srcView },
|
|
2670
|
+
{ binding: 1, resource: this.mipBlitSampler },
|
|
2671
|
+
],
|
|
2672
|
+
})
|
|
2673
|
+
const pass = encoder.beginRenderPass({
|
|
2674
|
+
colorAttachments: [
|
|
2675
|
+
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
2676
|
+
],
|
|
2677
|
+
})
|
|
2678
|
+
pass.setPipeline(this.mipBlitPipeline)
|
|
2679
|
+
pass.setBindGroup(0, bindGroup)
|
|
2680
|
+
pass.draw(3)
|
|
2681
|
+
pass.end()
|
|
2682
|
+
}
|
|
2683
|
+
this.device.queue.submit([encoder.finish()])
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2556
2686
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
2557
2687
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
2558
2688
|
pass.setPipeline(this.groundShadowPipeline)
|