reze-engine 0.11.1 → 0.11.3
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/dist/engine.d.ts +5 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +72 -425
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/shaders/body.d.ts +1 -1
- package/dist/shaders/body.d.ts.map +1 -1
- package/dist/shaders/body.js +27 -60
- 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 +4 -16
- 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 +5 -17
- package/dist/shaders/default.d.ts +1 -1
- package/dist/shaders/default.d.ts.map +1 -1
- package/dist/shaders/default.js +20 -34
- package/dist/shaders/dfg_lut.d.ts +1 -1
- package/dist/shaders/dfg_lut.d.ts.map +1 -1
- package/dist/shaders/dfg_lut.js +1 -1
- package/dist/shaders/eye.d.ts +1 -1
- package/dist/shaders/eye.d.ts.map +1 -1
- package/dist/shaders/eye.js +22 -35
- package/dist/shaders/face.d.ts +1 -1
- package/dist/shaders/face.d.ts.map +1 -1
- package/dist/shaders/face.js +21 -57
- package/dist/shaders/hair.d.ts +1 -1
- package/dist/shaders/hair.d.ts.map +1 -1
- package/dist/shaders/hair.js +7 -27
- package/dist/shaders/materials/body.d.ts +2 -0
- package/dist/shaders/materials/body.d.ts.map +1 -0
- package/dist/shaders/materials/body.js +199 -0
- package/dist/shaders/materials/cloth_rough.d.ts +2 -0
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_rough.js +178 -0
- package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
- package/dist/shaders/materials/cloth_smooth.js +174 -0
- package/dist/shaders/materials/default.d.ts +2 -0
- package/dist/shaders/materials/default.d.ts.map +1 -0
- package/dist/shaders/materials/default.js +171 -0
- package/dist/shaders/materials/eye.d.ts +2 -0
- package/dist/shaders/materials/eye.d.ts.map +1 -0
- package/dist/shaders/materials/eye.js +146 -0
- package/dist/shaders/materials/face.d.ts +2 -0
- package/dist/shaders/materials/face.d.ts.map +1 -0
- package/dist/shaders/materials/face.js +199 -0
- package/dist/shaders/materials/hair.d.ts +2 -0
- package/dist/shaders/materials/hair.d.ts.map +1 -0
- package/dist/shaders/materials/hair.js +176 -0
- package/dist/shaders/materials/metal.d.ts +2 -0
- package/dist/shaders/materials/metal.d.ts.map +1 -0
- package/dist/shaders/materials/metal.js +183 -0
- package/dist/shaders/materials/nodes.d.ts +2 -0
- package/dist/shaders/materials/nodes.d.ts.map +1 -0
- package/{src/shaders/nodes.ts → dist/shaders/materials/nodes.js} +32 -16
- package/dist/shaders/materials/stockings.d.ts +2 -0
- package/dist/shaders/materials/stockings.d.ts.map +1 -0
- package/dist/shaders/materials/stockings.js +244 -0
- package/dist/shaders/metal.d.ts +1 -1
- package/dist/shaders/metal.d.ts.map +1 -1
- package/dist/shaders/metal.js +4 -17
- package/dist/shaders/nodes.d.ts +1 -1
- package/dist/shaders/nodes.d.ts.map +1 -1
- package/dist/shaders/nodes.js +0 -9
- package/dist/shaders/passes/bloom.d.ts +4 -0
- package/dist/shaders/passes/bloom.d.ts.map +1 -0
- package/dist/shaders/passes/bloom.js +117 -0
- package/dist/shaders/passes/composite.d.ts +2 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -0
- package/dist/shaders/passes/composite.js +61 -0
- package/dist/shaders/passes/ground.d.ts +2 -0
- package/dist/shaders/passes/ground.d.ts.map +1 -0
- package/dist/shaders/passes/ground.js +93 -0
- package/dist/shaders/passes/mipmap.d.ts +2 -0
- package/dist/shaders/passes/mipmap.d.ts.map +1 -0
- package/dist/shaders/passes/mipmap.js +16 -0
- package/dist/shaders/passes/outline.d.ts +2 -0
- package/dist/shaders/passes/outline.d.ts.map +1 -0
- package/dist/shaders/passes/outline.js +83 -0
- package/dist/shaders/passes/pick.d.ts +2 -0
- package/dist/shaders/passes/pick.d.ts.map +1 -0
- package/dist/shaders/passes/pick.js +39 -0
- package/dist/shaders/passes/shadow.d.ts +2 -0
- package/dist/shaders/passes/shadow.d.ts.map +1 -0
- package/dist/shaders/passes/shadow.js +16 -0
- package/dist/shaders/stockings.d.ts +1 -1
- package/dist/shaders/stockings.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/engine.ts +112 -449
- package/src/index.ts +3 -2
- package/src/shaders/dfg_lut.ts +1 -1
- package/src/shaders/{body.ts → materials/body.ts} +27 -60
- package/src/shaders/{cloth_rough.ts → materials/cloth_rough.ts} +4 -16
- package/src/shaders/{cloth_smooth.ts → materials/cloth_smooth.ts} +5 -17
- package/src/shaders/{default.ts → materials/default.ts} +21 -34
- package/src/shaders/{eye.ts → materials/eye.ts} +23 -35
- package/src/shaders/{face.ts → materials/face.ts} +21 -57
- package/src/shaders/{hair.ts → materials/hair.ts} +7 -27
- package/src/shaders/{metal.ts → materials/metal.ts} +15 -19
- package/src/shaders/materials/nodes.ts +483 -0
- package/src/shaders/passes/bloom.ts +121 -0
- package/src/shaders/passes/composite.ts +62 -0
- package/src/shaders/passes/ground.ts +94 -0
- package/src/shaders/passes/mipmap.ts +17 -0
- package/src/shaders/passes/outline.ts +84 -0
- package/src/shaders/passes/pick.ts +40 -0
- package/src/shaders/passes/shadow.ts +17 -0
- package/src/shaders/classify.ts +0 -25
- /package/src/shaders/{stockings.ts → materials/stockings.ts} +0 -0
package/src/engine.ts
CHANGED
|
@@ -13,18 +13,51 @@ import {
|
|
|
13
13
|
normalizeAssetPath,
|
|
14
14
|
type AssetReader,
|
|
15
15
|
} from "./asset-reader"
|
|
16
|
-
import { DEFAULT_SHADER_WGSL } from "./shaders/default"
|
|
16
|
+
import { DEFAULT_SHADER_WGSL } from "./shaders/materials/default"
|
|
17
|
+
import { FACE_SHADER_WGSL } from "./shaders/materials/face"
|
|
18
|
+
import { HAIR_SHADER_WGSL } from "./shaders/materials/hair"
|
|
19
|
+
import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/materials/cloth_smooth"
|
|
20
|
+
import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/materials/cloth_rough"
|
|
21
|
+
import { METAL_SHADER_WGSL } from "./shaders/materials/metal"
|
|
22
|
+
import { BODY_SHADER_WGSL } from "./shaders/materials/body"
|
|
23
|
+
import { EYE_SHADER_WGSL } from "./shaders/materials/eye"
|
|
24
|
+
import { STOCKINGS_SHADER_WGSL } from "./shaders/materials/stockings"
|
|
17
25
|
import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut"
|
|
18
26
|
import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import {
|
|
27
|
+
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
|
|
28
|
+
import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground"
|
|
29
|
+
import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline"
|
|
30
|
+
import {
|
|
31
|
+
BLOOM_BLIT_SHADER_WGSL,
|
|
32
|
+
BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
33
|
+
BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
34
|
+
} from "./shaders/passes/bloom"
|
|
35
|
+
import { COMPOSITE_SHADER_WGSL } from "./shaders/passes/composite"
|
|
36
|
+
import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
37
|
+
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
38
|
+
|
|
39
|
+
// Material preset dispatch. Consumers supply a MaterialPresetMap assigning material names
|
|
40
|
+
// to presets; unmapped materials fall back to "default" (Principled BSDF).
|
|
41
|
+
export type MaterialPreset =
|
|
42
|
+
| "default"
|
|
43
|
+
| "face"
|
|
44
|
+
| "hair"
|
|
45
|
+
| "body"
|
|
46
|
+
| "eye"
|
|
47
|
+
| "stockings"
|
|
48
|
+
| "metal"
|
|
49
|
+
| "cloth_smooth"
|
|
50
|
+
| "cloth_rough"
|
|
51
|
+
|
|
52
|
+
export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
|
|
53
|
+
|
|
54
|
+
function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): MaterialPreset {
|
|
55
|
+
if (!map) return "default"
|
|
56
|
+
for (const [preset, names] of Object.entries(map)) {
|
|
57
|
+
if (names && names.includes(materialName)) return preset as MaterialPreset
|
|
58
|
+
}
|
|
59
|
+
return "default"
|
|
60
|
+
}
|
|
28
61
|
|
|
29
62
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
|
|
30
63
|
|
|
@@ -84,15 +117,17 @@ export const DEFAULT_BLOOM_OPTIONS: BloomOptions = {
|
|
|
84
117
|
|
|
85
118
|
/** Blender Color Management / View (rendering.txt: Filmic, exposure, gamma). `look` is reserved for future curve tweaks. */
|
|
86
119
|
export type ViewTransformOptions = {
|
|
87
|
-
/** Stops applied before Filmic: `linear *= 2^exposure
|
|
120
|
+
/** Stops applied before Filmic: `linear *= 2^exposure`. */
|
|
88
121
|
exposure: number
|
|
89
122
|
/** After Filmic, display gamma (`pow(rgb, 1/gamma)`). */
|
|
90
123
|
gamma: number
|
|
91
124
|
look: "default" | "medium_high_contrast"
|
|
92
125
|
}
|
|
93
126
|
|
|
127
|
+
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
128
|
+
// exposure 0.3, gamma 1.0, sRGB display, no curves.
|
|
94
129
|
export const DEFAULT_VIEW_TRANSFORM: ViewTransformOptions = {
|
|
95
|
-
exposure:
|
|
130
|
+
exposure: 0.6,
|
|
96
131
|
gamma: 1.0,
|
|
97
132
|
look: "medium_high_contrast",
|
|
98
133
|
}
|
|
@@ -221,11 +256,15 @@ export class Engine {
|
|
|
221
256
|
private maskResolveView!: GPUTextureView
|
|
222
257
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
223
258
|
private compositePassDescriptor!: GPURenderPassDescriptor
|
|
224
|
-
|
|
259
|
+
// Two specialized composite pipelines via WGSL pipeline-override constants.
|
|
260
|
+
// Identity variant skips the gamma pow entirely at shader-compile time —
|
|
261
|
+
// Safari's Metal backend won't fold pow(x, 1) to identity.
|
|
262
|
+
private compositePipelineIdentity!: GPURenderPipeline
|
|
263
|
+
private compositePipelineGamma!: GPURenderPipeline
|
|
225
264
|
private compositeBindGroupLayout!: GPUBindGroupLayout
|
|
226
265
|
private compositeBindGroup!: GPUBindGroup
|
|
227
266
|
private compositeUniformBuffer!: GPUBuffer
|
|
228
|
-
// [exposure,
|
|
267
|
+
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
229
268
|
private readonly compositeUniformData = new Float32Array(8)
|
|
230
269
|
|
|
231
270
|
// EEVEE-style bloom pyramid (mirrors Blender 3.6 effect_bloom_frag.glsl):
|
|
@@ -401,7 +440,10 @@ export class Engine {
|
|
|
401
440
|
const effIntensity = b.enabled ? b.intensity : 0.0
|
|
402
441
|
const u = this.compositeUniformData
|
|
403
442
|
u[0] = v.exposure
|
|
404
|
-
|
|
443
|
+
// Store 1/gamma so the shader avoids a per-pixel divide. Safari's Metal
|
|
444
|
+
// compiler doesn't fold `pow(x, 1/g)` into identity when g=1, so also emit
|
|
445
|
+
// a uniform branch that skips the pow entirely in the common case.
|
|
446
|
+
u[1] = 1.0 / Math.max(v.gamma, 1e-4)
|
|
405
447
|
u[2] = 0.0
|
|
406
448
|
u[3] = 0.0
|
|
407
449
|
u[4] = b.color.x
|
|
@@ -435,9 +477,12 @@ export class Engine {
|
|
|
435
477
|
private writeBloomUniforms(): void {
|
|
436
478
|
const b = this.bloomSettings
|
|
437
479
|
const bu = this.bloomBlitUniformData
|
|
438
|
-
// EEVEE prefilter: threshold,
|
|
480
|
+
// EEVEE prefilter: threshold, knee_half, clamp (0 → disabled), _unused
|
|
481
|
+
// Blender halves the knee before passing to the shader (eevee_bloom.c: knee * 0.5f).
|
|
482
|
+
// The blit shader's quadratic soft-knee curve uses knee_half as the offset from threshold,
|
|
483
|
+
// so the soft ramp spans [threshold - knee/2 .. threshold + knee/2] — NOT [threshold - knee .. threshold + knee].
|
|
439
484
|
bu[0] = b.threshold
|
|
440
|
-
bu[1] = b.knee
|
|
485
|
+
bu[1] = b.knee * 0.5
|
|
441
486
|
bu[2] = b.clamp
|
|
442
487
|
bu[3] = 0.0
|
|
443
488
|
this.device.queue.writeBuffer(this.bloomBlitUniformBuffer, 0, bu)
|
|
@@ -520,7 +565,7 @@ export class Engine {
|
|
|
520
565
|
{ texture: ltcTemp },
|
|
521
566
|
half,
|
|
522
567
|
{ bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE },
|
|
523
|
-
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 }
|
|
568
|
+
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 },
|
|
524
569
|
)
|
|
525
570
|
|
|
526
571
|
this.brdfLutTexture = this.device.createTexture({
|
|
@@ -888,21 +933,7 @@ export class Engine {
|
|
|
888
933
|
})
|
|
889
934
|
const shadowShader = this.device.createShaderModule({
|
|
890
935
|
label: "shadow depth",
|
|
891
|
-
code:
|
|
892
|
-
struct LightVP { viewProj: mat4x4f, };
|
|
893
|
-
@group(0) @binding(0) var<uniform> lp: LightVP;
|
|
894
|
-
@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
|
|
895
|
-
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
896
|
-
@location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
|
|
897
|
-
let pos4 = vec4f(position, 1.0);
|
|
898
|
-
let ws = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
899
|
-
let inv = select(1.0, 1.0 / ws, ws > 0.0001);
|
|
900
|
-
let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
|
|
901
|
-
var sp = vec4f(0.0);
|
|
902
|
-
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
903
|
-
return lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
904
|
-
}
|
|
905
|
-
`,
|
|
936
|
+
code: SHADOW_DEPTH_SHADER_WGSL,
|
|
906
937
|
})
|
|
907
938
|
this.shadowDepthPipeline = this.device.createRenderPipeline({
|
|
908
939
|
label: "shadow depth pipeline",
|
|
@@ -962,99 +993,7 @@ export class Engine {
|
|
|
962
993
|
})
|
|
963
994
|
const groundShadowShader = this.device.createShaderModule({
|
|
964
995
|
label: "ground shadow",
|
|
965
|
-
code:
|
|
966
|
-
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
967
|
-
struct Light { direction: vec4f, color: vec4f, };
|
|
968
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
969
|
-
struct GroundShadowMat {
|
|
970
|
-
diffuseColor: vec3f, fadeStart: f32,
|
|
971
|
-
fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
|
|
972
|
-
gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, _pad: f32,
|
|
973
|
-
gridLineColor: vec3f, _pad2: f32,
|
|
974
|
-
};
|
|
975
|
-
struct LightVP { viewProj: mat4x4f, };
|
|
976
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
977
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
978
|
-
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
979
|
-
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
980
|
-
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
981
|
-
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
982
|
-
|
|
983
|
-
// Hash-based noise for frosted/matte surface
|
|
984
|
-
fn hash2(p: vec2f) -> f32 {
|
|
985
|
-
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
|
|
986
|
-
p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
|
|
987
|
-
return fract((p3.x + p3.y) * p3.z);
|
|
988
|
-
}
|
|
989
|
-
fn valueNoise(p: vec2f) -> f32 {
|
|
990
|
-
let i = floor(p);
|
|
991
|
-
let f = fract(p);
|
|
992
|
-
let u = f * f * (3.0 - 2.0 * f);
|
|
993
|
-
return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),
|
|
994
|
-
mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);
|
|
995
|
-
}
|
|
996
|
-
fn fbmNoise(p: vec2f) -> f32 {
|
|
997
|
-
var v = 0.0;
|
|
998
|
-
var a = 0.5;
|
|
999
|
-
var pp = p;
|
|
1000
|
-
for (var i = 0; i < 4; i++) {
|
|
1001
|
-
v += a * valueNoise(pp);
|
|
1002
|
-
pp *= 2.0;
|
|
1003
|
-
a *= 0.5;
|
|
1004
|
-
}
|
|
1005
|
-
return v;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
|
|
1009
|
-
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
|
|
1010
|
-
var o: VO; o.worldPos = position; o.normal = normal;
|
|
1011
|
-
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
1012
|
-
}
|
|
1013
|
-
struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
|
|
1014
|
-
@fragment fn fs(i: VO) -> FSOut {
|
|
1015
|
-
let n = normalize(i.normal);
|
|
1016
|
-
let centerDist = length(i.worldPos.xz);
|
|
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));
|
|
1018
|
-
|
|
1019
|
-
// Shadow sampling
|
|
1020
|
-
let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
1021
|
-
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
1022
|
-
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
1023
|
-
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
1024
|
-
let st = material.pcfTexel;
|
|
1025
|
-
let compareZ = ndc.z - 0.0035;
|
|
1026
|
-
var vis = 0.0;
|
|
1027
|
-
for (var y = -2; y <= 2; y++) {
|
|
1028
|
-
for (var x = -2; x <= 2; x++) {
|
|
1029
|
-
vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
vis *= 0.04;
|
|
1033
|
-
|
|
1034
|
-
// Frosted/matte micro-texture (磨砂)
|
|
1035
|
-
let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
|
|
1036
|
-
let noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;
|
|
1037
|
-
|
|
1038
|
-
// Grid lines — anti-aliased via screen-space derivatives
|
|
1039
|
-
let gp = i.worldPos.xz / material.gridSpacing;
|
|
1040
|
-
let gridFrac = abs(fract(gp - 0.5) - 0.5);
|
|
1041
|
-
let gridDeriv = fwidth(gp);
|
|
1042
|
-
let halfLine = material.gridLineWidth * 0.5;
|
|
1043
|
-
let gridLine = 1.0 - min(
|
|
1044
|
-
smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
|
|
1045
|
-
smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
|
|
1046
|
-
);
|
|
1047
|
-
let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
|
|
1048
|
-
let dark = (1.0 - vis) * material.shadowStrength;
|
|
1049
|
-
var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
|
|
1050
|
-
baseColor *= noiseTint;
|
|
1051
|
-
let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
|
|
1052
|
-
var out: FSOut;
|
|
1053
|
-
out.color = vec4f(finalColor * edgeFade, edgeFade);
|
|
1054
|
-
out.mask = 0.0;
|
|
1055
|
-
return out;
|
|
1056
|
-
}
|
|
1057
|
-
`,
|
|
996
|
+
code: GROUND_SHADOW_SHADER_WGSL,
|
|
1058
997
|
})
|
|
1059
998
|
this.groundShadowPipeline = this.createRenderPipeline({
|
|
1060
999
|
label: "ground shadow pipeline",
|
|
@@ -1098,90 +1037,7 @@ export class Engine {
|
|
|
1098
1037
|
|
|
1099
1038
|
const outlineShaderModule = this.device.createShaderModule({
|
|
1100
1039
|
label: "outline shaders",
|
|
1101
|
-
code:
|
|
1102
|
-
struct CameraUniforms {
|
|
1103
|
-
view: mat4x4f,
|
|
1104
|
-
projection: mat4x4f,
|
|
1105
|
-
viewPos: vec3f,
|
|
1106
|
-
_padding: f32,
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
struct MaterialUniforms {
|
|
1110
|
-
edgeColor: vec4f,
|
|
1111
|
-
edgeSize: f32,
|
|
1112
|
-
_padding1: f32,
|
|
1113
|
-
_padding2: f32,
|
|
1114
|
-
_padding3: f32,
|
|
1115
|
-
};
|
|
1116
|
-
|
|
1117
|
-
// group 0: per-frame
|
|
1118
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
1119
|
-
// group 1: per-instance
|
|
1120
|
-
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
1121
|
-
// group 2: per-material
|
|
1122
|
-
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
1123
|
-
|
|
1124
|
-
struct VertexOutput {
|
|
1125
|
-
@builtin(position) position: vec4f,
|
|
1126
|
-
};
|
|
1127
|
-
|
|
1128
|
-
@vertex fn vs(
|
|
1129
|
-
@location(0) position: vec3f,
|
|
1130
|
-
@location(1) normal: vec3f,
|
|
1131
|
-
@location(3) joints0: vec4<u32>,
|
|
1132
|
-
@location(4) weights0: vec4<f32>
|
|
1133
|
-
) -> VertexOutput {
|
|
1134
|
-
var output: VertexOutput;
|
|
1135
|
-
let pos4 = vec4f(position, 1.0);
|
|
1136
|
-
|
|
1137
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
1138
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
1139
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
1140
|
-
|
|
1141
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
1142
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
1143
|
-
for (var i = 0u; i < 4u; i++) {
|
|
1144
|
-
let j = joints0[i];
|
|
1145
|
-
let w = normalizedWeights[i];
|
|
1146
|
-
let m = skinMats[j];
|
|
1147
|
-
skinnedPos += (m * pos4) * w;
|
|
1148
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
1149
|
-
skinnedNrm += (r3 * normal) * w;
|
|
1150
|
-
}
|
|
1151
|
-
let worldPos = skinnedPos.xyz;
|
|
1152
|
-
let worldNormal = normalize(skinnedNrm);
|
|
1153
|
-
|
|
1154
|
-
// Screen-space outline extrusion — MMD-style pixel-stable edge line.
|
|
1155
|
-
// 1. Project position and normal-as-direction to clip space.
|
|
1156
|
-
// 2. Normalize the 2D clip-space normal, aspect-compensated so "one pixel horizontally"
|
|
1157
|
-
// matches "one pixel vertically" (otherwise wide viewports squash the outline in X).
|
|
1158
|
-
// 3. Offset clip-space xy by (normal * edgeSize * edgeScale), then multiply by w
|
|
1159
|
-
// so the perspective divide cancels out → offset stays constant in NDC regardless
|
|
1160
|
-
// of depth, matching how MMD / babylon-mmd style outlines look identical when zooming.
|
|
1161
|
-
// 4. edgeScale is in NDC-y units per PMX edgeSize. ≈ 0.006 gives ~3px at 1080p; it's
|
|
1162
|
-
// tied to viewport HEIGHT so resizing the window keeps pixel thickness stable.
|
|
1163
|
-
let viewProj = camera.projection * camera.view;
|
|
1164
|
-
let clipPos = viewProj * vec4f(worldPos, 1.0);
|
|
1165
|
-
let clipNormal = (viewProj * vec4f(worldNormal, 0.0)).xy;
|
|
1166
|
-
// projection is column-major: proj[0][0] = 1/(aspect·tan(fov/2)), proj[1][1] = 1/tan(fov/2).
|
|
1167
|
-
// Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).
|
|
1168
|
-
let aspect = camera.projection[1][1] / camera.projection[0][0];
|
|
1169
|
-
let pixelDir = normalize(vec2f(clipNormal.x * aspect, clipNormal.y));
|
|
1170
|
-
let ndcDir = vec2f(pixelDir.x / aspect, pixelDir.y);
|
|
1171
|
-
let edgeScale = 0.0016;
|
|
1172
|
-
let offset = ndcDir * material.edgeSize * edgeScale * clipPos.w;
|
|
1173
|
-
output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
|
|
1174
|
-
return output;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
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;
|
|
1183
|
-
}
|
|
1184
|
-
`,
|
|
1040
|
+
code: OUTLINE_SHADER_WGSL,
|
|
1185
1041
|
})
|
|
1186
1042
|
|
|
1187
1043
|
this.outlinePipeline = this.createRenderPipeline({
|
|
@@ -1246,134 +1102,25 @@ export class Engine {
|
|
|
1246
1102
|
],
|
|
1247
1103
|
})
|
|
1248
1104
|
|
|
1249
|
-
const bloomFullscreenVs = /* wgsl */ `
|
|
1250
|
-
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
1251
|
-
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
1252
|
-
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
1253
|
-
return vec4f(x, y, 0.0, 1.0);
|
|
1254
|
-
}
|
|
1255
|
-
`
|
|
1256
|
-
|
|
1257
|
-
// Blit: full-res HDR → half-res. Karis 4-tap firefly average + EEVEE quadratic knee threshold + clamp.
|
|
1258
1105
|
const bloomBlitShader = this.device.createShaderModule({
|
|
1259
1106
|
label: "bloom blit (Karis prefilter)",
|
|
1260
|
-
code:
|
|
1261
|
-
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
1262
|
-
@group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
|
|
1263
|
-
@group(0) @binding(2) var maskTex: texture_2d<f32>;
|
|
1264
|
-
|
|
1265
|
-
fn luminance(c: vec3f) -> f32 {
|
|
1266
|
-
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
1267
|
-
}
|
|
1268
|
-
fn fetch(c: vec2<i32>, clampV: f32) -> vec3f {
|
|
1269
|
-
let d = vec2<i32>(textureDimensions(hdrTex));
|
|
1270
|
-
let cc = clamp(c, vec2<i32>(0), d - vec2<i32>(1));
|
|
1271
|
-
let s = textureLoad(hdrTex, cc, 0);
|
|
1272
|
-
// Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
|
|
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;
|
|
1277
|
-
// Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
|
|
1278
|
-
return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
1282
|
-
let dst = vec2<i32>(p.xy - vec2f(0.5));
|
|
1283
|
-
let base = dst * 2;
|
|
1284
|
-
let clampV = prefilter.z;
|
|
1285
|
-
let a = fetch(base + vec2<i32>(0, 0), clampV);
|
|
1286
|
-
let b = fetch(base + vec2<i32>(1, 0), clampV);
|
|
1287
|
-
let c = fetch(base + vec2<i32>(0, 1), clampV);
|
|
1288
|
-
let d = fetch(base + vec2<i32>(1, 1), clampV);
|
|
1289
|
-
// Karis partial average: weight each tap by 1/(1+luma) — suppresses fireflies.
|
|
1290
|
-
let wa = 1.0 / (1.0 + luminance(a));
|
|
1291
|
-
let wb = 1.0 / (1.0 + luminance(b));
|
|
1292
|
-
let wc = 1.0 / (1.0 + luminance(c));
|
|
1293
|
-
let wd = 1.0 / (1.0 + luminance(d));
|
|
1294
|
-
let avg = (a * wa + b * wb + c * wc + d * wd) / max(wa + wb + wc + wd, 1e-6);
|
|
1295
|
-
// EEVEE quadratic threshold (brightness = max-channel, then soft-knee curve).
|
|
1296
|
-
let bright = max(avg.r, max(avg.g, avg.b));
|
|
1297
|
-
let soft = clamp(bright - prefilter.x + prefilter.y, 0.0, 2.0 * prefilter.y);
|
|
1298
|
-
let q = (soft * soft) / (4.0 * max(prefilter.y, 1e-4) + 1e-6);
|
|
1299
|
-
let contrib = max(q, bright - prefilter.x) / max(bright, 1e-4);
|
|
1300
|
-
return vec4f(max(avg * contrib, vec3f(0.0)), 1.0);
|
|
1301
|
-
}
|
|
1302
|
-
`,
|
|
1107
|
+
code: BLOOM_BLIT_SHADER_WGSL,
|
|
1303
1108
|
})
|
|
1304
1109
|
|
|
1305
|
-
// Downsample: Jimenez/COD 13-tap dual-box — 5 weighted 2×2 averages, rejects nyquist ringing.
|
|
1306
1110
|
const bloomDownsampleShader = this.device.createShaderModule({
|
|
1307
1111
|
label: "bloom downsample 13-tap",
|
|
1308
|
-
code:
|
|
1309
|
-
@group(0) @binding(0) var srcTex: texture_2d<f32>;
|
|
1310
|
-
@group(0) @binding(1) var srcSamp: sampler;
|
|
1311
|
-
|
|
1312
|
-
fn samp(uv: vec2f, off: vec2f) -> vec3f {
|
|
1313
|
-
return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
1317
|
-
let srcDims = vec2f(textureDimensions(srcTex));
|
|
1318
|
-
let t = 1.0 / srcDims;
|
|
1319
|
-
// fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
|
|
1320
|
-
let dstDims = srcDims * 0.5;
|
|
1321
|
-
let uv = p.xy / max(dstDims, vec2f(1.0));
|
|
1322
|
-
let A = samp(uv, t * vec2f(-2.0, -2.0));
|
|
1323
|
-
let B = samp(uv, t * vec2f( 0.0, -2.0));
|
|
1324
|
-
let C = samp(uv, t * vec2f( 2.0, -2.0));
|
|
1325
|
-
let D = samp(uv, t * vec2f(-1.0, -1.0));
|
|
1326
|
-
let E = samp(uv, t * vec2f( 1.0, -1.0));
|
|
1327
|
-
let F = samp(uv, t * vec2f(-2.0, 0.0));
|
|
1328
|
-
let G = samp(uv, t * vec2f( 0.0, 0.0));
|
|
1329
|
-
let H = samp(uv, t * vec2f( 2.0, 0.0));
|
|
1330
|
-
let I = samp(uv, t * vec2f(-1.0, 1.0));
|
|
1331
|
-
let J = samp(uv, t * vec2f( 1.0, 1.0));
|
|
1332
|
-
let K = samp(uv, t * vec2f(-2.0, 2.0));
|
|
1333
|
-
let L = samp(uv, t * vec2f( 0.0, 2.0));
|
|
1334
|
-
let M = samp(uv, t * vec2f( 2.0, 2.0));
|
|
1335
|
-
var o = (D + E + I + J) * (0.5 / 4.0);
|
|
1336
|
-
o = o + (A + B + G + F) * (0.125 / 4.0);
|
|
1337
|
-
o = o + (B + C + H + G) * (0.125 / 4.0);
|
|
1338
|
-
o = o + (F + G + L + K) * (0.125 / 4.0);
|
|
1339
|
-
o = o + (G + H + M + L) * (0.125 / 4.0);
|
|
1340
|
-
return vec4f(o, 1.0);
|
|
1341
|
-
}
|
|
1342
|
-
`,
|
|
1112
|
+
code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
|
|
1343
1113
|
})
|
|
1344
1114
|
|
|
1345
|
-
// Upsample: 9-tap tent, progressively added to matching downsample mip. Blender radius = sample scale.
|
|
1346
1115
|
const bloomUpsampleShader = this.device.createShaderModule({
|
|
1347
1116
|
label: "bloom upsample 9-tap tent",
|
|
1348
|
-
code:
|
|
1349
|
-
@group(0) @binding(0) var srcTex: texture_2d<f32>; // coarser accumulator
|
|
1350
|
-
@group(0) @binding(1) var baseTex: texture_2d<f32>; // matching downsample mip
|
|
1351
|
-
@group(0) @binding(2) var srcSamp: sampler;
|
|
1352
|
-
@group(0) @binding(3) var<uniform> upU: vec4<f32>; // sampleScale, _, _, _
|
|
1353
|
-
|
|
1354
|
-
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
|
1355
|
-
let srcDims = vec2f(textureDimensions(srcTex));
|
|
1356
|
-
let baseDims = vec2f(textureDimensions(baseTex));
|
|
1357
|
-
let uv = p.xy / max(baseDims, vec2f(1.0));
|
|
1358
|
-
let t = upU.x / srcDims;
|
|
1359
|
-
var o = textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, -1.0), 0.0).rgb * 1.0;
|
|
1360
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, -1.0), 0.0).rgb * 2.0;
|
|
1361
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, -1.0), 0.0).rgb * 1.0;
|
|
1362
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 0.0), 0.0).rgb * 2.0;
|
|
1363
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 0.0), 0.0).rgb * 4.0;
|
|
1364
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 0.0), 0.0).rgb * 2.0;
|
|
1365
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 1.0), 0.0).rgb * 1.0;
|
|
1366
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 1.0), 0.0).rgb * 2.0;
|
|
1367
|
-
o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 1.0), 0.0).rgb * 1.0;
|
|
1368
|
-
o = o * (1.0 / 16.0);
|
|
1369
|
-
let base = textureSampleLevel(baseTex, srcSamp, uv, 0.0).rgb;
|
|
1370
|
-
return vec4f(o + base, 1.0);
|
|
1371
|
-
}
|
|
1372
|
-
`,
|
|
1117
|
+
code: BLOOM_UPSAMPLE_SHADER_WGSL,
|
|
1373
1118
|
})
|
|
1374
1119
|
|
|
1375
1120
|
const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] })
|
|
1376
|
-
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1121
|
+
const bloomDownLayout = this.device.createPipelineLayout({
|
|
1122
|
+
bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
|
|
1123
|
+
})
|
|
1377
1124
|
const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] })
|
|
1378
1125
|
|
|
1379
1126
|
this.bloomBlitPipeline = this.device.createRenderPipeline({
|
|
@@ -1418,62 +1165,27 @@ export class Engine {
|
|
|
1418
1165
|
|
|
1419
1166
|
const compositeShader = this.device.createShaderModule({
|
|
1420
1167
|
label: "composite shader",
|
|
1421
|
-
code:
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
return vec4f(x, y, 0.0, 1.0);
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
@fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
|
|
1446
|
-
let hdr = textureLoad(hdrTex, vec2<i32>(fragCoord.xy), 0);
|
|
1447
|
-
let a = max(hdr.a, 1e-6);
|
|
1448
|
-
let straight = hdr.rgb / a;
|
|
1449
|
-
let fullSz = vec2f(textureDimensions(hdrTex));
|
|
1450
|
-
let bloomSz = vec2f(textureDimensions(bloomTex));
|
|
1451
|
-
// Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.
|
|
1452
|
-
let bloomUv = (fragCoord.xy + vec2f(0.5)) / max(fullSz, vec2f(1.0));
|
|
1453
|
-
let tint = viewU[1].xyz;
|
|
1454
|
-
let intensity = viewU[1].w;
|
|
1455
|
-
let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
|
|
1456
|
-
let combined = straight + bloom;
|
|
1457
|
-
let exposed = combined * exp2(viewU[0].x);
|
|
1458
|
-
let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
|
|
1459
|
-
let g = max(viewU[0].y, 1e-4);
|
|
1460
|
-
let disp = pow(max(tm, vec3f(0.0)), vec3f(1.0 / g));
|
|
1461
|
-
return vec4f(disp * hdr.a, hdr.a);
|
|
1462
|
-
}
|
|
1463
|
-
`,
|
|
1464
|
-
})
|
|
1465
|
-
|
|
1466
|
-
this.compositePipeline = this.device.createRenderPipeline({
|
|
1467
|
-
label: "composite pipeline",
|
|
1468
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.compositeBindGroupLayout] }),
|
|
1469
|
-
vertex: { module: compositeShader, entryPoint: "vs" },
|
|
1470
|
-
fragment: {
|
|
1471
|
-
module: compositeShader,
|
|
1472
|
-
entryPoint: "fs",
|
|
1473
|
-
targets: [{ format: this.presentationFormat }],
|
|
1474
|
-
},
|
|
1475
|
-
primitive: { topology: "triangle-list" },
|
|
1476
|
-
})
|
|
1168
|
+
code: COMPOSITE_SHADER_WGSL,
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
const compositePipelineLayout = this.device.createPipelineLayout({
|
|
1172
|
+
bindGroupLayouts: [this.compositeBindGroupLayout],
|
|
1173
|
+
})
|
|
1174
|
+
const makeCompositePipeline = (applyGamma: boolean, label: string): GPURenderPipeline =>
|
|
1175
|
+
this.device.createRenderPipeline({
|
|
1176
|
+
label,
|
|
1177
|
+
layout: compositePipelineLayout,
|
|
1178
|
+
vertex: { module: compositeShader, entryPoint: "vs" },
|
|
1179
|
+
fragment: {
|
|
1180
|
+
module: compositeShader,
|
|
1181
|
+
entryPoint: "fs",
|
|
1182
|
+
constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
|
|
1183
|
+
targets: [{ format: this.presentationFormat }],
|
|
1184
|
+
},
|
|
1185
|
+
primitive: { topology: "triangle-list" },
|
|
1186
|
+
})
|
|
1187
|
+
this.compositePipelineIdentity = makeCompositePipeline(false, "composite pipeline (gamma=1)")
|
|
1188
|
+
this.compositePipelineGamma = makeCompositePipeline(true, "composite pipeline (gamma!=1)")
|
|
1477
1189
|
|
|
1478
1190
|
this.bloomPassDescriptor = {
|
|
1479
1191
|
label: "bloom pass",
|
|
@@ -1487,47 +1199,9 @@ export class Engine {
|
|
|
1487
1199
|
],
|
|
1488
1200
|
} as GPURenderPassDescriptor
|
|
1489
1201
|
|
|
1490
|
-
// GPU picking: encode (modelIndex, materialIndex) as color
|
|
1491
1202
|
const pickShaderModule = this.device.createShaderModule({
|
|
1492
1203
|
label: "pick shader",
|
|
1493
|
-
code:
|
|
1494
|
-
struct CameraUniforms {
|
|
1495
|
-
view: mat4x4f,
|
|
1496
|
-
projection: mat4x4f,
|
|
1497
|
-
viewPos: vec3f,
|
|
1498
|
-
_padding: f32,
|
|
1499
|
-
};
|
|
1500
|
-
struct PickId {
|
|
1501
|
-
modelId: f32,
|
|
1502
|
-
materialId: f32,
|
|
1503
|
-
_p1: f32,
|
|
1504
|
-
_p2: f32,
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
1508
|
-
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
1509
|
-
@group(2) @binding(0) var<uniform> pickId: PickId;
|
|
1510
|
-
|
|
1511
|
-
@vertex fn vs(
|
|
1512
|
-
@location(0) position: vec3f,
|
|
1513
|
-
@location(1) normal: vec3f,
|
|
1514
|
-
@location(2) uv: vec2f,
|
|
1515
|
-
@location(3) joints0: vec4<u32>,
|
|
1516
|
-
@location(4) weights0: vec4<f32>
|
|
1517
|
-
) -> @builtin(position) vec4f {
|
|
1518
|
-
let pos4 = vec4f(position, 1.0);
|
|
1519
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
1520
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
1521
|
-
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
1522
|
-
var sp = vec4f(0.0);
|
|
1523
|
-
for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
|
|
1524
|
-
return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
1528
|
-
return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
|
|
1529
|
-
}
|
|
1530
|
-
`,
|
|
1204
|
+
code: PICK_SHADER_WGSL,
|
|
1531
1205
|
})
|
|
1532
1206
|
|
|
1533
1207
|
this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -1643,10 +1317,7 @@ export class Engine {
|
|
|
1643
1317
|
const bw = Math.max(1, Math.floor(width / 2))
|
|
1644
1318
|
const bh = Math.max(1, Math.floor(height / 2))
|
|
1645
1319
|
const shortSide = Math.max(1, Math.min(bw, bh))
|
|
1646
|
-
this.bloomMipCount = Math.max(
|
|
1647
|
-
1,
|
|
1648
|
-
Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1),
|
|
1649
|
-
)
|
|
1320
|
+
this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1))
|
|
1650
1321
|
this.bloomDownTexture = this.device.createTexture({
|
|
1651
1322
|
label: "bloom down pyramid",
|
|
1652
1323
|
size: [bw, bh],
|
|
@@ -1663,16 +1334,12 @@ export class Engine {
|
|
|
1663
1334
|
})
|
|
1664
1335
|
this.bloomDownMipViews = []
|
|
1665
1336
|
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
1666
|
-
this.bloomDownMipViews.push(
|
|
1667
|
-
this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
|
|
1668
|
-
)
|
|
1337
|
+
this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
|
|
1669
1338
|
}
|
|
1670
1339
|
this.bloomUpMipViews = []
|
|
1671
1340
|
const upLevels = Math.max(1, this.bloomMipCount - 1)
|
|
1672
1341
|
for (let i = 0; i < upLevels; i++) {
|
|
1673
|
-
this.bloomUpMipViews.push(
|
|
1674
|
-
this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
|
|
1675
|
-
)
|
|
1342
|
+
this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
|
|
1676
1343
|
}
|
|
1677
1344
|
|
|
1678
1345
|
this.depthTexture = this.device.createTexture({
|
|
@@ -1685,12 +1352,16 @@ export class Engine {
|
|
|
1685
1352
|
|
|
1686
1353
|
const depthTextureView = this.depthTexture.createView()
|
|
1687
1354
|
|
|
1355
|
+
// storeOp="discard" on MSAA views keeps per-sample data in Apple TBDR tile memory —
|
|
1356
|
+
// only the resolveTarget (hdrResolveTexture / maskResolveView) gets written to RAM.
|
|
1357
|
+
// With storeOp="store" Safari's Metal backend spills the full MS buffer every frame
|
|
1358
|
+
// (rgba16f × 4 samples on a 4K canvas ≈ 256 MB/frame of dead bandwidth).
|
|
1688
1359
|
const colorAttachment: GPURenderPassColorAttachment = {
|
|
1689
1360
|
view: this.multisampleTexture.createView(),
|
|
1690
1361
|
resolveTarget: this.hdrResolveTexture.createView(),
|
|
1691
1362
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1692
1363
|
loadOp: "clear",
|
|
1693
|
-
storeOp: "
|
|
1364
|
+
storeOp: "discard",
|
|
1694
1365
|
}
|
|
1695
1366
|
|
|
1696
1367
|
const maskAttachment: GPURenderPassColorAttachment = {
|
|
@@ -1698,7 +1369,7 @@ export class Engine {
|
|
|
1698
1369
|
resolveTarget: this.maskResolveView,
|
|
1699
1370
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1700
1371
|
loadOp: "clear",
|
|
1701
|
-
storeOp: "
|
|
1372
|
+
storeOp: "discard",
|
|
1702
1373
|
}
|
|
1703
1374
|
|
|
1704
1375
|
this.renderPassDescriptor = {
|
|
@@ -1708,7 +1379,8 @@ export class Engine {
|
|
|
1708
1379
|
view: depthTextureView,
|
|
1709
1380
|
depthClearValue: 1.0,
|
|
1710
1381
|
depthLoadOp: "clear",
|
|
1711
|
-
|
|
1382
|
+
// Main-pass depth is not sampled later (shadow uses its own map, composite is depthless).
|
|
1383
|
+
depthStoreOp: "discard",
|
|
1712
1384
|
stencilClearValue: 0,
|
|
1713
1385
|
stencilLoadOp: "clear",
|
|
1714
1386
|
stencilStoreOp: "discard",
|
|
@@ -2629,20 +2301,7 @@ export class Engine {
|
|
|
2629
2301
|
})
|
|
2630
2302
|
const module = this.device.createShaderModule({
|
|
2631
2303
|
label: "mipmap blit",
|
|
2632
|
-
code:
|
|
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
|
-
`,
|
|
2304
|
+
code: MIPMAP_BLIT_SHADER_WGSL,
|
|
2646
2305
|
})
|
|
2647
2306
|
this.mipBlitPipeline = this.device.createRenderPipeline({
|
|
2648
2307
|
label: "mipmap blit pipeline",
|
|
@@ -2665,7 +2324,9 @@ export class Engine {
|
|
|
2665
2324
|
],
|
|
2666
2325
|
})
|
|
2667
2326
|
const pass = encoder.beginRenderPass({
|
|
2668
|
-
colorAttachments: [
|
|
2327
|
+
colorAttachments: [
|
|
2328
|
+
{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
|
|
2329
|
+
],
|
|
2669
2330
|
})
|
|
2670
2331
|
pass.setPipeline(this.mipBlitPipeline)
|
|
2671
2332
|
pass.setBindGroup(0, bindGroup)
|
|
@@ -2911,7 +2572,9 @@ export class Engine {
|
|
|
2911
2572
|
const compositeAttachment = (this.compositePassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
2912
2573
|
compositeAttachment.view = this.context.getCurrentTexture().createView()
|
|
2913
2574
|
const cpass = encoder.beginRenderPass(this.compositePassDescriptor)
|
|
2914
|
-
|
|
2575
|
+
const compositePipeline =
|
|
2576
|
+
this.viewTransform.gamma === 1.0 ? this.compositePipelineIdentity : this.compositePipelineGamma
|
|
2577
|
+
cpass.setPipeline(compositePipeline)
|
|
2915
2578
|
cpass.setBindGroup(0, this.compositeBindGroup)
|
|
2916
2579
|
cpass.draw(3)
|
|
2917
2580
|
cpass.end()
|