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/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,7 +78,7 @@ 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
|
|
|
@@ -212,6 +212,13 @@ export class Engine {
|
|
|
212
212
|
private hdrResolveTexture!: GPUTexture
|
|
213
213
|
private static readonly MULTISAMPLE_COUNT = 4
|
|
214
214
|
private static readonly HDR_FORMAT: GPUTextureFormat = "rgba16float"
|
|
215
|
+
/** Single-channel mask written alongside HDR color — 1 = model geometry (contributes
|
|
216
|
+
* to bloom), 0 = ground (never blooms). Sampled by the bloom blit pass to gate the
|
|
217
|
+
* prefilter so ground brightness can't halo the scene. */
|
|
218
|
+
private static readonly BLOOM_MASK_FORMAT: GPUTextureFormat = "r8unorm"
|
|
219
|
+
private multisampleMaskTexture!: GPUTexture
|
|
220
|
+
private maskResolveTexture!: GPUTexture
|
|
221
|
+
private maskResolveView!: GPUTextureView
|
|
215
222
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
216
223
|
private compositePassDescriptor!: GPURenderPassDescriptor
|
|
217
224
|
private compositePipeline!: GPURenderPipeline
|
|
@@ -256,11 +263,9 @@ export class Engine {
|
|
|
256
263
|
private hasGround = false
|
|
257
264
|
private shadowMapTexture!: GPUTexture
|
|
258
265
|
private shadowMapDepthView!: GPUTextureView
|
|
259
|
-
private
|
|
260
|
-
private
|
|
261
|
-
private
|
|
262
|
-
private ltcMagLutView!: GPUTextureView
|
|
263
|
-
private static readonly SHADOW_MAP_SIZE = 4096
|
|
266
|
+
private brdfLutTexture!: GPUTexture
|
|
267
|
+
private brdfLutView!: GPUTextureView
|
|
268
|
+
private static readonly SHADOW_MAP_SIZE = 2048
|
|
264
269
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
265
270
|
private shadowLightVPBuffer!: GPUBuffer
|
|
266
271
|
private shadowLightVPMatrix = new Float32Array(16)
|
|
@@ -287,6 +292,8 @@ export class Engine {
|
|
|
287
292
|
private modelInstances = new Map<string, ModelInstance>()
|
|
288
293
|
private materialSampler!: GPUSampler
|
|
289
294
|
private textureCache = new Map<string, GPUTexture>()
|
|
295
|
+
private mipBlitPipeline: GPURenderPipeline | null = null
|
|
296
|
+
private mipBlitSampler: GPUSampler | null = null
|
|
290
297
|
private _nextDefaultModelId = 0
|
|
291
298
|
|
|
292
299
|
// IK and physics enabled at engine level (same for all models)
|
|
@@ -473,51 +480,24 @@ export class Engine {
|
|
|
473
480
|
Engine.instance = this
|
|
474
481
|
}
|
|
475
482
|
|
|
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
|
-
}
|
|
483
|
+
// One-shot bake of EEVEE's combined BRDF LUT — DFG (bsdf_lut_frag.glsl) packed
|
|
484
|
+
// with ltc_mag_ggx (eevee_lut.c) into a single 64×64 rgba8unorm texture:
|
|
485
|
+
// .rg = split-sum DFG → F_brdf_*_scatter
|
|
486
|
+
// .ba = LTC magnitude → ltc_brdf_scale_from_lut
|
|
487
|
+
// One texture fetch per fragment replaces the previous 2–3 taps. rgba8unorm
|
|
488
|
+
// (vs rgba16float) halves sample bandwidth; DFG/LTC values fit [0,1] cleanly.
|
|
489
|
+
private bakeBrdfLut() {
|
|
490
|
+
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
491
|
+
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).")
|
|
492
|
+
}
|
|
508
493
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
this.ltcMagLutTexture = this.device.createTexture({
|
|
513
|
-
label: "LTC mag LUT",
|
|
494
|
+
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
495
|
+
const ltcTemp = this.device.createTexture({
|
|
496
|
+
label: "LTC mag LUT (bake input)",
|
|
514
497
|
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
515
498
|
format: "rg16float",
|
|
516
499
|
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
517
500
|
})
|
|
518
|
-
this.ltcMagLutView = this.ltcMagLutTexture.createView()
|
|
519
|
-
|
|
520
|
-
// Float32 → float16 bits. rg16float writeTexture expects packed half floats.
|
|
521
501
|
const n = LTC_MAG_LUT_DATA.length
|
|
522
502
|
const half = new Uint16Array(n)
|
|
523
503
|
const f32 = new Float32Array(1)
|
|
@@ -527,22 +507,58 @@ export class Engine {
|
|
|
527
507
|
const x = u32[0]
|
|
528
508
|
const sign = (x >>> 16) & 0x8000
|
|
529
509
|
let exp = ((x >>> 23) & 0xff) - 127 + 15
|
|
530
|
-
|
|
510
|
+
const mant = x & 0x7fffff
|
|
531
511
|
if (exp <= 0) {
|
|
532
|
-
half[i] = sign
|
|
512
|
+
half[i] = sign
|
|
533
513
|
} else if (exp >= 31) {
|
|
534
|
-
half[i] = sign | 0x7c00
|
|
514
|
+
half[i] = sign | 0x7c00
|
|
535
515
|
} else {
|
|
536
516
|
half[i] = sign | (exp << 10) | (mant >>> 13)
|
|
537
517
|
}
|
|
538
518
|
}
|
|
539
|
-
|
|
540
519
|
this.device.queue.writeTexture(
|
|
541
|
-
{ texture:
|
|
520
|
+
{ texture: ltcTemp },
|
|
542
521
|
half,
|
|
543
522
|
{ bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE },
|
|
544
523
|
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 }
|
|
545
524
|
)
|
|
525
|
+
|
|
526
|
+
this.brdfLutTexture = this.device.createTexture({
|
|
527
|
+
label: "BRDF LUT (DFG + LTC packed)",
|
|
528
|
+
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
529
|
+
format: "rgba8unorm",
|
|
530
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
531
|
+
})
|
|
532
|
+
this.brdfLutView = this.brdfLutTexture.createView()
|
|
533
|
+
|
|
534
|
+
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL })
|
|
535
|
+
const pipeline = this.device.createRenderPipeline({
|
|
536
|
+
label: "BRDF LUT bake pipeline",
|
|
537
|
+
layout: "auto",
|
|
538
|
+
vertex: { module, entryPoint: "vs" },
|
|
539
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
540
|
+
primitive: { topology: "triangle-list" },
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
const bakeBindGroup = this.device.createBindGroup({
|
|
544
|
+
label: "BRDF LUT bake bind group",
|
|
545
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
546
|
+
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" })
|
|
550
|
+
const pass = enc.beginRenderPass({
|
|
551
|
+
colorAttachments: [
|
|
552
|
+
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
553
|
+
],
|
|
554
|
+
})
|
|
555
|
+
pass.setPipeline(pipeline)
|
|
556
|
+
pass.setBindGroup(0, bakeBindGroup)
|
|
557
|
+
pass.draw(3, 1, 0, 0)
|
|
558
|
+
pass.end()
|
|
559
|
+
this.device.queue.submit([enc.finish()])
|
|
560
|
+
|
|
561
|
+
ltcTemp.destroy()
|
|
546
562
|
}
|
|
547
563
|
|
|
548
564
|
private createRenderPipeline(config: {
|
|
@@ -551,11 +567,13 @@ export class Engine {
|
|
|
551
567
|
shaderModule: GPUShaderModule
|
|
552
568
|
vertexBuffers: GPUVertexBufferLayout[]
|
|
553
569
|
fragmentTarget?: GPUColorTargetState
|
|
570
|
+
fragmentTargets?: GPUColorTargetState[]
|
|
554
571
|
fragmentEntryPoint?: string
|
|
555
572
|
cullMode?: GPUCullMode
|
|
556
573
|
depthStencil?: GPUDepthStencilState
|
|
557
574
|
multisample?: GPUMultisampleState
|
|
558
575
|
}): GPURenderPipeline {
|
|
576
|
+
const targets = config.fragmentTargets ?? (config.fragmentTarget ? [config.fragmentTarget] : undefined)
|
|
559
577
|
return this.device.createRenderPipeline({
|
|
560
578
|
label: config.label,
|
|
561
579
|
layout: config.layout,
|
|
@@ -563,11 +581,11 @@ export class Engine {
|
|
|
563
581
|
module: config.shaderModule,
|
|
564
582
|
buffers: config.vertexBuffers,
|
|
565
583
|
},
|
|
566
|
-
fragment:
|
|
584
|
+
fragment: targets
|
|
567
585
|
? {
|
|
568
586
|
module: config.shaderModule,
|
|
569
587
|
entryPoint: config.fragmentEntryPoint,
|
|
570
|
-
targets
|
|
588
|
+
targets,
|
|
571
589
|
}
|
|
572
590
|
: undefined,
|
|
573
591
|
primitive: { cullMode: config.cullMode ?? "none" },
|
|
@@ -580,6 +598,7 @@ export class Engine {
|
|
|
580
598
|
this.materialSampler = this.device.createSampler({
|
|
581
599
|
magFilter: "linear",
|
|
582
600
|
minFilter: "linear",
|
|
601
|
+
mipmapFilter: "linear",
|
|
583
602
|
addressModeU: "repeat",
|
|
584
603
|
addressModeV: "repeat",
|
|
585
604
|
})
|
|
@@ -641,6 +660,12 @@ export class Engine {
|
|
|
641
660
|
},
|
|
642
661
|
}
|
|
643
662
|
|
|
663
|
+
// Bloom mask target — r8unorm has no alpha channel, so src-alpha blending is invalid.
|
|
664
|
+
// Use replace mode: depth test already rejects occluded fragments, so last-writer-wins
|
|
665
|
+
// on surviving pixels gives the right result (ground writes 0; models/outlines write 1).
|
|
666
|
+
const maskBlend: GPUColorTargetState = { format: Engine.BLOOM_MASK_FORMAT }
|
|
667
|
+
const sceneTargets: GPUColorTargetState[] = [standardBlend, maskBlend]
|
|
668
|
+
|
|
644
669
|
const shaderModule = this.device.createShaderModule({
|
|
645
670
|
label: "default model shader",
|
|
646
671
|
code: DEFAULT_SHADER_WGSL,
|
|
@@ -697,7 +722,6 @@ export class Engine {
|
|
|
697
722
|
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
698
723
|
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
699
724
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
700
|
-
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
701
725
|
],
|
|
702
726
|
})
|
|
703
727
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -730,7 +754,7 @@ export class Engine {
|
|
|
730
754
|
layout: mainPipelineLayout,
|
|
731
755
|
shaderModule,
|
|
732
756
|
vertexBuffers: fullVertexBuffers,
|
|
733
|
-
|
|
757
|
+
fragmentTargets: sceneTargets,
|
|
734
758
|
cullMode: "none",
|
|
735
759
|
depthStencil: {
|
|
736
760
|
format: "depth24plus-stencil8",
|
|
@@ -744,7 +768,7 @@ export class Engine {
|
|
|
744
768
|
layout: mainPipelineLayout,
|
|
745
769
|
shaderModule: faceShaderModule,
|
|
746
770
|
vertexBuffers: fullVertexBuffers,
|
|
747
|
-
|
|
771
|
+
fragmentTargets: sceneTargets,
|
|
748
772
|
cullMode: "none",
|
|
749
773
|
depthStencil: {
|
|
750
774
|
format: "depth24plus-stencil8",
|
|
@@ -758,7 +782,7 @@ export class Engine {
|
|
|
758
782
|
layout: mainPipelineLayout,
|
|
759
783
|
shaderModule: hairShaderModule,
|
|
760
784
|
vertexBuffers: fullVertexBuffers,
|
|
761
|
-
|
|
785
|
+
fragmentTargets: sceneTargets,
|
|
762
786
|
cullMode: "none",
|
|
763
787
|
depthStencil: {
|
|
764
788
|
format: "depth24plus-stencil8",
|
|
@@ -772,7 +796,7 @@ export class Engine {
|
|
|
772
796
|
layout: mainPipelineLayout,
|
|
773
797
|
shaderModule: clothSmoothShaderModule,
|
|
774
798
|
vertexBuffers: fullVertexBuffers,
|
|
775
|
-
|
|
799
|
+
fragmentTargets: sceneTargets,
|
|
776
800
|
cullMode: "none",
|
|
777
801
|
depthStencil: {
|
|
778
802
|
format: "depth24plus-stencil8",
|
|
@@ -786,7 +810,7 @@ export class Engine {
|
|
|
786
810
|
layout: mainPipelineLayout,
|
|
787
811
|
shaderModule: clothRoughShaderModule,
|
|
788
812
|
vertexBuffers: fullVertexBuffers,
|
|
789
|
-
|
|
813
|
+
fragmentTargets: sceneTargets,
|
|
790
814
|
cullMode: "none",
|
|
791
815
|
depthStencil: {
|
|
792
816
|
format: "depth24plus-stencil8",
|
|
@@ -800,7 +824,7 @@ export class Engine {
|
|
|
800
824
|
layout: mainPipelineLayout,
|
|
801
825
|
shaderModule: metalShaderModule,
|
|
802
826
|
vertexBuffers: fullVertexBuffers,
|
|
803
|
-
|
|
827
|
+
fragmentTargets: sceneTargets,
|
|
804
828
|
cullMode: "none",
|
|
805
829
|
depthStencil: {
|
|
806
830
|
format: "depth24plus-stencil8",
|
|
@@ -814,7 +838,7 @@ export class Engine {
|
|
|
814
838
|
layout: mainPipelineLayout,
|
|
815
839
|
shaderModule: bodyShaderModule,
|
|
816
840
|
vertexBuffers: fullVertexBuffers,
|
|
817
|
-
|
|
841
|
+
fragmentTargets: sceneTargets,
|
|
818
842
|
cullMode: "none",
|
|
819
843
|
depthStencil: {
|
|
820
844
|
format: "depth24plus-stencil8",
|
|
@@ -828,7 +852,7 @@ export class Engine {
|
|
|
828
852
|
layout: mainPipelineLayout,
|
|
829
853
|
shaderModule: eyeShaderModule,
|
|
830
854
|
vertexBuffers: fullVertexBuffers,
|
|
831
|
-
|
|
855
|
+
fragmentTargets: sceneTargets,
|
|
832
856
|
cullMode: "none",
|
|
833
857
|
depthStencil: {
|
|
834
858
|
format: "depth24plus-stencil8",
|
|
@@ -842,7 +866,7 @@ export class Engine {
|
|
|
842
866
|
layout: mainPipelineLayout,
|
|
843
867
|
shaderModule: stockingsShaderModule,
|
|
844
868
|
vertexBuffers: fullVertexBuffers,
|
|
845
|
-
|
|
869
|
+
fragmentTargets: sceneTargets,
|
|
846
870
|
cullMode: "none",
|
|
847
871
|
depthStencil: {
|
|
848
872
|
format: "depth24plus-stencil8",
|
|
@@ -907,10 +931,8 @@ export class Engine {
|
|
|
907
931
|
})
|
|
908
932
|
this.shadowMapDepthView = this.shadowMapTexture.createView()
|
|
909
933
|
|
|
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()
|
|
934
|
+
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
935
|
+
this.bakeBrdfLut()
|
|
914
936
|
|
|
915
937
|
// Now that shadow resources exist, create the main per-frame bind group
|
|
916
938
|
this.perFrameBindGroup = this.device.createBindGroup({
|
|
@@ -923,8 +945,7 @@ export class Engine {
|
|
|
923
945
|
{ binding: 3, resource: this.shadowMapDepthView },
|
|
924
946
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
925
947
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
926
|
-
{ binding: 9, resource: this.
|
|
927
|
-
{ binding: 10, resource: this.ltcMagLutView },
|
|
948
|
+
{ binding: 9, resource: this.brdfLutView },
|
|
928
949
|
],
|
|
929
950
|
})
|
|
930
951
|
|
|
@@ -989,7 +1010,8 @@ export class Engine {
|
|
|
989
1010
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
990
1011
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
991
1012
|
}
|
|
992
|
-
|
|
1013
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
1014
|
+
@fragment fn fs(i: VO) -> FSOut {
|
|
993
1015
|
let n = normalize(i.normal);
|
|
994
1016
|
let centerDist = length(i.worldPos.xz);
|
|
995
1017
|
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 +1049,10 @@ export class Engine {
|
|
|
1027
1049
|
var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
1028
1050
|
baseColor *= noiseTint;
|
|
1029
1051
|
let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
|
|
1030
|
-
|
|
1052
|
+
var out: FSOut;
|
|
1053
|
+
out.color = vec4f(finalColor * edgeFade, edgeFade);
|
|
1054
|
+
out.mask = 0.0;
|
|
1055
|
+
return out;
|
|
1031
1056
|
}
|
|
1032
1057
|
`,
|
|
1033
1058
|
})
|
|
@@ -1036,7 +1061,7 @@ export class Engine {
|
|
|
1036
1061
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
1037
1062
|
shaderModule: groundShadowShader,
|
|
1038
1063
|
vertexBuffers: fullVertexBuffers,
|
|
1039
|
-
|
|
1064
|
+
fragmentTargets: sceneTargets,
|
|
1040
1065
|
cullMode: "back",
|
|
1041
1066
|
depthStencil: { format: "depth24plus-stencil8", depthWriteEnabled: true, depthCompare: "less-equal" },
|
|
1042
1067
|
})
|
|
@@ -1149,8 +1174,12 @@ export class Engine {
|
|
|
1149
1174
|
return output;
|
|
1150
1175
|
}
|
|
1151
1176
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1177
|
+
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
1178
|
+
@fragment fn fs() -> FSOut {
|
|
1179
|
+
var out: FSOut;
|
|
1180
|
+
out.color = material.edgeColor;
|
|
1181
|
+
out.mask = 1.0;
|
|
1182
|
+
return out;
|
|
1154
1183
|
}
|
|
1155
1184
|
`,
|
|
1156
1185
|
})
|
|
@@ -1160,7 +1189,7 @@ export class Engine {
|
|
|
1160
1189
|
layout: outlinePipelineLayout,
|
|
1161
1190
|
shaderModule: outlineShaderModule,
|
|
1162
1191
|
vertexBuffers: outlineVertexBuffers,
|
|
1163
|
-
|
|
1192
|
+
fragmentTargets: sceneTargets,
|
|
1164
1193
|
cullMode: "back",
|
|
1165
1194
|
depthStencil: {
|
|
1166
1195
|
format: "depth24plus-stencil8",
|
|
@@ -1197,6 +1226,7 @@ export class Engine {
|
|
|
1197
1226
|
entries: [
|
|
1198
1227
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
1199
1228
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1229
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "unfilterable-float" } },
|
|
1200
1230
|
],
|
|
1201
1231
|
})
|
|
1202
1232
|
this.bloomDownsampleBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -1230,6 +1260,7 @@ export class Engine {
|
|
|
1230
1260
|
code: `${bloomFullscreenVs}
|
|
1231
1261
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
1232
1262
|
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
1263
|
+
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
1233
1264
|
|
|
1234
1265
|
fn luminance(c: vec3f) -> f32 {
|
|
1235
1266
|
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
@@ -1240,8 +1271,11 @@ export class Engine {
|
|
|
1240
1271
|
let s = textureLoad(hdrTex, cc, 0);
|
|
1241
1272
|
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
1242
1273
|
let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
|
|
1274
|
+
// Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
|
|
1275
|
+
let mask = textureLoad(maskTex, cc, 0).r;
|
|
1276
|
+
let masked = rgb * mask;
|
|
1243
1277
|
// Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
1244
|
-
return select(
|
|
1278
|
+
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
1245
1279
|
}
|
|
1246
1280
|
|
|
1247
1281
|
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
@@ -1587,6 +1621,23 @@ export class Engine {
|
|
|
1587
1621
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1588
1622
|
})
|
|
1589
1623
|
|
|
1624
|
+
// Bloom-mask MRT attachments — same dims + MSAA as HDR so they share the render pass.
|
|
1625
|
+
// MS buffer gets resolved into maskResolveTexture, which the bloom blit pass samples.
|
|
1626
|
+
this.multisampleMaskTexture = this.device.createTexture({
|
|
1627
|
+
label: "multisample bloom mask",
|
|
1628
|
+
size: [width, height],
|
|
1629
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
1630
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1631
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1632
|
+
})
|
|
1633
|
+
this.maskResolveTexture = this.device.createTexture({
|
|
1634
|
+
label: "bloom mask resolve",
|
|
1635
|
+
size: [width, height],
|
|
1636
|
+
format: Engine.BLOOM_MASK_FORMAT,
|
|
1637
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1638
|
+
})
|
|
1639
|
+
this.maskResolveView = this.maskResolveTexture.createView()
|
|
1640
|
+
|
|
1590
1641
|
// Bloom pyramid: mip 0 is half-res, each subsequent mip halves again.
|
|
1591
1642
|
// Mip count chosen so the coarsest mip is ≥4 px on the short side, capped at BLOOM_MAX_LEVELS.
|
|
1592
1643
|
const bw = Math.max(1, Math.floor(width / 2))
|
|
@@ -1642,9 +1693,17 @@ export class Engine {
|
|
|
1642
1693
|
storeOp: "store",
|
|
1643
1694
|
}
|
|
1644
1695
|
|
|
1696
|
+
const maskAttachment: GPURenderPassColorAttachment = {
|
|
1697
|
+
view: this.multisampleMaskTexture.createView(),
|
|
1698
|
+
resolveTarget: this.maskResolveView,
|
|
1699
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1700
|
+
loadOp: "clear",
|
|
1701
|
+
storeOp: "store",
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1645
1704
|
this.renderPassDescriptor = {
|
|
1646
1705
|
label: "renderPass",
|
|
1647
|
-
colorAttachments: [colorAttachment],
|
|
1706
|
+
colorAttachments: [colorAttachment, maskAttachment],
|
|
1648
1707
|
depthStencilAttachment: {
|
|
1649
1708
|
view: depthTextureView,
|
|
1650
1709
|
depthClearValue: 1.0,
|
|
@@ -1679,6 +1738,7 @@ export class Engine {
|
|
|
1679
1738
|
entries: [
|
|
1680
1739
|
{ binding: 0, resource: this.hdrResolveTexture.createView() },
|
|
1681
1740
|
{ binding: 1, resource: { buffer: this.bloomBlitUniformBuffer } },
|
|
1741
|
+
{ binding: 2, resource: this.maskResolveView },
|
|
1682
1742
|
],
|
|
1683
1743
|
})
|
|
1684
1744
|
// Downsample[i] reads bloomDown mip (i-1), writes bloomDown mip i. i ∈ [1..N-1].
|
|
@@ -2534,10 +2594,12 @@ export class Engine {
|
|
|
2534
2594
|
colorSpaceConversion: "none",
|
|
2535
2595
|
})
|
|
2536
2596
|
|
|
2597
|
+
const mipLevelCount = Math.floor(Math.log2(Math.max(imageBitmap.width, imageBitmap.height))) + 1
|
|
2537
2598
|
const texture = this.device.createTexture({
|
|
2538
2599
|
label: `texture: ${cacheKey}`,
|
|
2539
2600
|
size: [imageBitmap.width, imageBitmap.height],
|
|
2540
2601
|
format: "rgba8unorm-srgb",
|
|
2602
|
+
mipLevelCount,
|
|
2541
2603
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
2542
2604
|
})
|
|
2543
2605
|
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
@@ -2545,6 +2607,8 @@ export class Engine {
|
|
|
2545
2607
|
imageBitmap.height,
|
|
2546
2608
|
])
|
|
2547
2609
|
|
|
2610
|
+
if (mipLevelCount > 1) this.generateMipmaps(texture, mipLevelCount)
|
|
2611
|
+
|
|
2548
2612
|
this.textureCache.set(cacheKey, texture)
|
|
2549
2613
|
inst.textureCacheKeys.push(cacheKey)
|
|
2550
2614
|
return texture
|
|
@@ -2553,6 +2617,64 @@ export class Engine {
|
|
|
2553
2617
|
}
|
|
2554
2618
|
}
|
|
2555
2619
|
|
|
2620
|
+
// Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
|
|
2621
|
+
// re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
|
|
2622
|
+
private generateMipmaps(texture: GPUTexture, mipLevelCount: number) {
|
|
2623
|
+
if (!this.mipBlitPipeline || !this.mipBlitSampler) {
|
|
2624
|
+
this.mipBlitSampler = this.device.createSampler({
|
|
2625
|
+
magFilter: "linear",
|
|
2626
|
+
minFilter: "linear",
|
|
2627
|
+
addressModeU: "clamp-to-edge",
|
|
2628
|
+
addressModeV: "clamp-to-edge",
|
|
2629
|
+
})
|
|
2630
|
+
const module = this.device.createShaderModule({
|
|
2631
|
+
label: "mipmap blit",
|
|
2632
|
+
code: /* wgsl */ `
|
|
2633
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
2634
|
+
@group(0) @binding(1) var samp: sampler;
|
|
2635
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
2636
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
2637
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
2638
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
2639
|
+
}
|
|
2640
|
+
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
2641
|
+
let dstDims = vec2f(textureDimensions(src)) * 0.5;
|
|
2642
|
+
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
2643
|
+
return textureSampleLevel(src, samp, uv, 0.0);
|
|
2644
|
+
}
|
|
2645
|
+
`,
|
|
2646
|
+
})
|
|
2647
|
+
this.mipBlitPipeline = this.device.createRenderPipeline({
|
|
2648
|
+
label: "mipmap blit pipeline",
|
|
2649
|
+
layout: "auto",
|
|
2650
|
+
vertex: { module, entryPoint: "vs" },
|
|
2651
|
+
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm-srgb" }] },
|
|
2652
|
+
primitive: { topology: "triangle-list" },
|
|
2653
|
+
})
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const encoder = this.device.createCommandEncoder({ label: "mipgen" })
|
|
2657
|
+
for (let level = 1; level < mipLevelCount; level++) {
|
|
2658
|
+
const srcView = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 })
|
|
2659
|
+
const dstView = texture.createView({ baseMipLevel: level, mipLevelCount: 1 })
|
|
2660
|
+
const bindGroup = this.device.createBindGroup({
|
|
2661
|
+
layout: this.mipBlitPipeline.getBindGroupLayout(0),
|
|
2662
|
+
entries: [
|
|
2663
|
+
{ binding: 0, resource: srcView },
|
|
2664
|
+
{ binding: 1, resource: this.mipBlitSampler },
|
|
2665
|
+
],
|
|
2666
|
+
})
|
|
2667
|
+
const pass = encoder.beginRenderPass({
|
|
2668
|
+
colorAttachments: [{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }],
|
|
2669
|
+
})
|
|
2670
|
+
pass.setPipeline(this.mipBlitPipeline)
|
|
2671
|
+
pass.setBindGroup(0, bindGroup)
|
|
2672
|
+
pass.draw(3)
|
|
2673
|
+
pass.end()
|
|
2674
|
+
}
|
|
2675
|
+
this.device.queue.submit([encoder.finish()])
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2556
2678
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
2557
2679
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
2558
2680
|
pass.setPipeline(this.groundShadowPipeline)
|