reze-engine 0.24.1 → 0.25.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 +5 -1
- package/dist/engine.d.ts +127 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +422 -34
- package/dist/gpu-profile.d.ts +19 -0
- package/dist/gpu-profile.d.ts.map +1 -0
- package/dist/gpu-profile.js +120 -0
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/physics/profile.d.ts +18 -0
- package/dist/physics/profile.d.ts.map +1 -0
- package/dist/physics/profile.js +44 -0
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +143 -141
- package/dist/shaders/passes/composite.d.ts +23 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +62 -16
- package/dist/shaders/passes/depth-prepass.d.ts +2 -0
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -0
- package/dist/shaders/passes/depth-prepass.js +56 -0
- package/dist/shaders/passes/ground.d.ts +1 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +8 -0
- package/package.json +1 -1
- package/src/engine.ts +459 -34
- package/src/graph/slots.ts +11 -2
- package/src/index.ts +2 -0
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/composite.ts +89 -16
- package/src/shaders/passes/depth-prepass.ts +57 -0
- package/src/shaders/passes/ground.ts +8 -0
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
package/src/graph/slots.ts
CHANGED
|
@@ -76,14 +76,23 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
76
76
|
? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
|
|
77
77
|
: " if (alpha < 0.001) { discard; }"
|
|
78
78
|
const gate = renderClass === "eye" ? EYE_REAR_GATE : ""
|
|
79
|
+
// Double-sided shading, winding-independent: a normal pointing away from the
|
|
80
|
+
// camera means we're seeing the surface's other side — flip it. Only genuinely
|
|
81
|
+
// camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
|
|
82
|
+
// untouched), unlike a front_facing test, which PMX's clockwise winding
|
|
83
|
+
// inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
|
|
84
|
+
// shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
|
|
85
|
+
// by design, so its visible fragments are back faces with intended normals.
|
|
86
|
+
const flip =
|
|
87
|
+
renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);"
|
|
79
88
|
return `@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
80
89
|
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
81
90
|
// MMD alpha semantics: material alpha × texture alpha.
|
|
82
91
|
let alpha = material.alpha * tex_s.a;
|
|
83
92
|
${discard}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
let v = normalize(camera.viewPos - input.worldPos)
|
|
94
|
+
var n = safe_normal(input.normal);
|
|
95
|
+
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
87
96
|
${gate}
|
|
88
97
|
let l = -light.lights[0].direction.xyz;
|
|
89
98
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export {
|
|
|
13
13
|
type GizmoDragEvent,
|
|
14
14
|
type GizmoDragCallback,
|
|
15
15
|
type GizmoDragKind,
|
|
16
|
+
type BackgroundEffectParamValue,
|
|
17
|
+
type BackgroundEffectResult,
|
|
16
18
|
} from "./engine"
|
|
17
19
|
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload"
|
|
18
20
|
export {
|
|
@@ -1,205 +1,207 @@
|
|
|
1
|
-
// Shared WGSL blocks concatenated by every material shader.
|
|
2
|
-
// Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
|
|
3
|
-
// away from the per-material fragment code so each material file only contains what
|
|
4
|
-
// makes it visually distinct.
|
|
5
|
-
//
|
|
6
|
-
// Concat order in every material:
|
|
7
|
-
// NODES_WGSL (nodes.ts — math/noise/BSDF helpers)
|
|
8
|
-
// COMMON_BINDINGS_WGSL (uniform structs + @group/@binding declarations)
|
|
9
|
-
// SAMPLE_SHADOW_WGSL (3×3 PCF shadow sampler; reads bindings above)
|
|
10
|
-
// COMMON_VS_WGSL (skinning vertex shader; reads bindings above)
|
|
11
|
-
// <material's own constants + @fragment fn fs>
|
|
12
|
-
//
|
|
13
|
-
// WGSL is a whole-module compile — declaration order at module scope doesn't matter,
|
|
14
|
-
// but the readable order is: types → bindings → helpers → entry points.
|
|
15
|
-
|
|
16
|
-
// ─── Uniform structs + bind group layout ────────────────────────────
|
|
17
|
-
// Every material pipeline uses the same bind group layout, so the same bindings are
|
|
18
|
-
// declared here once. Groups:
|
|
19
|
-
// group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
|
|
20
|
-
// group(1): per-model skinning
|
|
21
|
-
// group(2): per-material (diffuse texture + material uniforms)
|
|
22
|
-
|
|
23
|
-
export const COMMON_BINDINGS_WGSL = /* wgsl */ `
|
|
24
|
-
|
|
25
|
-
struct CameraUniforms {
|
|
26
|
-
view: mat4x4f,
|
|
27
|
-
projection: mat4x4f,
|
|
28
|
-
viewPos: vec3f,
|
|
29
|
-
_padding: f32,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
struct Light {
|
|
33
|
-
direction: vec4f,
|
|
34
|
-
color: vec4f,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
struct LightUniforms {
|
|
38
|
-
ambientColor: vec4f,
|
|
39
|
-
lights: array<Light, 4>,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Per-material uniforms. Every material binds this layout even if it ignores fields;
|
|
43
|
-
// the engine keeps one bind group layout across all material pipelines. The PMX
|
|
44
|
-
// classic-material fields (ambient/specular/shininess) are carried for graph nodes that
|
|
45
|
-
// want them; most graphs read only diffuseColor + alpha.
|
|
46
|
-
struct MaterialUniforms {
|
|
47
|
-
diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
|
|
48
|
-
alpha: f32, // 0 → discard; <1 → transparent draw call
|
|
49
|
-
ambient: vec3f, // PMX ambient rgb
|
|
50
|
-
shininess: f32, // PMX specular power
|
|
51
|
-
specular: vec3f, // PMX specular rgb
|
|
52
|
-
sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
|
|
53
|
-
// Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
|
|
54
|
-
// the post-alpha-eye stencil by camera-vs-face hemisphere.
|
|
55
|
-
headBoneIndex: f32,
|
|
56
|
-
_pad0: f32,
|
|
57
|
-
_pad1: f32,
|
|
58
|
-
_pad2: f32,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
struct VertexOutput {
|
|
62
|
-
@builtin(position) position: vec4f,
|
|
63
|
-
@location(0) normal: vec3f,
|
|
64
|
-
@location(1) uv: vec2f,
|
|
65
|
-
@location(2) worldPos: vec3f,
|
|
66
|
-
// Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
|
|
67
|
-
// textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
|
|
68
|
-
// worldPos so the pattern rides with the surface — otherwise the mesh swims through a
|
|
69
|
-
// world-static noise field under any skinning deformation or root (センター) motion.
|
|
70
|
-
// At rest skinMats are identity so restPos == worldPos, which is why existing noise-
|
|
71
|
-
// scale constants stay valid without retuning.
|
|
72
|
-
@location(3) restPos: vec3f,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
struct LightVP { viewProj: mat4x4f, };
|
|
76
|
-
|
|
77
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
78
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
79
|
-
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
80
|
-
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
81
|
-
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
82
|
-
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
83
|
-
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
84
|
-
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
85
|
-
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
86
|
-
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
87
|
-
// Reserved for future sphere/toon graph nodes; graphs that don't read them get the
|
|
88
|
-
// 1×1 white fallback bound here.
|
|
89
|
-
@group(2) @binding(2) var toonTexture: texture_2d<f32>;
|
|
90
|
-
@group(2) @binding(3) var sphereTexture: texture_2d<f32>;
|
|
91
|
-
|
|
92
|
-
// Four-bone blended normals can cancel to ~zero on physics-driven parts
|
|
93
|
-
// (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
|
|
94
|
-
// which poisons the whole shading stack and flashes through bloom. Fall
|
|
95
|
-
// back to up for degenerate normals instead.
|
|
96
|
-
fn safe_normal(nIn: vec3f) -> vec3f {
|
|
97
|
-
let l2 = dot(nIn, nIn);
|
|
98
|
-
if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
|
|
99
|
-
return nIn * inverseSqrt(l2);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
`;
|
|
103
|
-
|
|
104
|
-
// ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
|
|
105
|
-
//
|
|
106
|
-
// doesn't unroll nested shadow loops
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
let
|
|
117
|
-
let
|
|
118
|
-
let
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
let
|
|
122
|
-
let
|
|
123
|
-
let
|
|
124
|
-
let
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@location(
|
|
145
|
-
@location(
|
|
146
|
-
@location(
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
output.
|
|
164
|
-
output.
|
|
165
|
-
output.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
// Shared WGSL blocks concatenated by every material shader.
|
|
2
|
+
// Splits the boilerplate (uniform structs, bind group layout, skinning VS, PCF shadow)
|
|
3
|
+
// away from the per-material fragment code so each material file only contains what
|
|
4
|
+
// makes it visually distinct.
|
|
5
|
+
//
|
|
6
|
+
// Concat order in every material:
|
|
7
|
+
// NODES_WGSL (nodes.ts — math/noise/BSDF helpers)
|
|
8
|
+
// COMMON_BINDINGS_WGSL (uniform structs + @group/@binding declarations)
|
|
9
|
+
// SAMPLE_SHADOW_WGSL (3×3 PCF shadow sampler; reads bindings above)
|
|
10
|
+
// COMMON_VS_WGSL (skinning vertex shader; reads bindings above)
|
|
11
|
+
// <material's own constants + @fragment fn fs>
|
|
12
|
+
//
|
|
13
|
+
// WGSL is a whole-module compile — declaration order at module scope doesn't matter,
|
|
14
|
+
// but the readable order is: types → bindings → helpers → entry points.
|
|
15
|
+
|
|
16
|
+
// ─── Uniform structs + bind group layout ────────────────────────────
|
|
17
|
+
// Every material pipeline uses the same bind group layout, so the same bindings are
|
|
18
|
+
// declared here once. Groups:
|
|
19
|
+
// group(0): per-frame scene (camera, lights, shadow map, BRDF LUT via nodes.ts)
|
|
20
|
+
// group(1): per-model skinning
|
|
21
|
+
// group(2): per-material (diffuse texture + material uniforms)
|
|
22
|
+
|
|
23
|
+
export const COMMON_BINDINGS_WGSL = /* wgsl */ `
|
|
24
|
+
|
|
25
|
+
struct CameraUniforms {
|
|
26
|
+
view: mat4x4f,
|
|
27
|
+
projection: mat4x4f,
|
|
28
|
+
viewPos: vec3f,
|
|
29
|
+
_padding: f32,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
struct Light {
|
|
33
|
+
direction: vec4f,
|
|
34
|
+
color: vec4f,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
struct LightUniforms {
|
|
38
|
+
ambientColor: vec4f,
|
|
39
|
+
lights: array<Light, 4>,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Per-material uniforms. Every material binds this layout even if it ignores fields;
|
|
43
|
+
// the engine keeps one bind group layout across all material pipelines. The PMX
|
|
44
|
+
// classic-material fields (ambient/specular/shininess) are carried for graph nodes that
|
|
45
|
+
// want them; most graphs read only diffuseColor + alpha.
|
|
46
|
+
struct MaterialUniforms {
|
|
47
|
+
diffuseColor: vec3f, // PMX diffuse rgb — the material_diffuse node reads this
|
|
48
|
+
alpha: f32, // 0 → discard; <1 → transparent draw call
|
|
49
|
+
ambient: vec3f, // PMX ambient rgb
|
|
50
|
+
shininess: f32, // PMX specular power
|
|
51
|
+
specular: vec3f, // PMX specular rgb
|
|
52
|
+
sphereMode: f32, // 0 none · 1 multiply (sph) · 2 add (spa)
|
|
53
|
+
// Skeleton index of the 頭 (head) bone, or -1. Lets the eye shader gate
|
|
54
|
+
// the post-alpha-eye stencil by camera-vs-face hemisphere.
|
|
55
|
+
headBoneIndex: f32,
|
|
56
|
+
_pad0: f32,
|
|
57
|
+
_pad1: f32,
|
|
58
|
+
_pad2: f32,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
struct VertexOutput {
|
|
62
|
+
@builtin(position) position: vec4f,
|
|
63
|
+
@location(0) normal: vec3f,
|
|
64
|
+
@location(1) uv: vec2f,
|
|
65
|
+
@location(2) worldPos: vec3f,
|
|
66
|
+
// Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural
|
|
67
|
+
// textures (noise bump, sparkle, Generated-coord gradients) key off this instead of
|
|
68
|
+
// worldPos so the pattern rides with the surface — otherwise the mesh swims through a
|
|
69
|
+
// world-static noise field under any skinning deformation or root (センター) motion.
|
|
70
|
+
// At rest skinMats are identity so restPos == worldPos, which is why existing noise-
|
|
71
|
+
// scale constants stay valid without retuning.
|
|
72
|
+
@location(3) restPos: vec3f,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
struct LightVP { viewProj: mat4x4f, };
|
|
76
|
+
|
|
77
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
78
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
79
|
+
@group(0) @binding(2) var diffuseSampler: sampler;
|
|
80
|
+
@group(0) @binding(3) var shadowMap: texture_depth_2d;
|
|
81
|
+
@group(0) @binding(4) var shadowSampler: sampler_comparison;
|
|
82
|
+
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
83
|
+
// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).
|
|
84
|
+
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
85
|
+
@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;
|
|
86
|
+
@group(2) @binding(1) var<uniform> material: MaterialUniforms;
|
|
87
|
+
// Reserved for future sphere/toon graph nodes; graphs that don't read them get the
|
|
88
|
+
// 1×1 white fallback bound here.
|
|
89
|
+
@group(2) @binding(2) var toonTexture: texture_2d<f32>;
|
|
90
|
+
@group(2) @binding(3) var sphereTexture: texture_2d<f32>;
|
|
91
|
+
|
|
92
|
+
// Four-bone blended normals can cancel to ~zero on physics-driven parts
|
|
93
|
+
// (opposing bone rotations at 50/50 weights) — normalize(0) is 0/0 = NaN,
|
|
94
|
+
// which poisons the whole shading stack and flashes through bloom. Fall
|
|
95
|
+
// back to up for degenerate normals instead.
|
|
96
|
+
fn safe_normal(nIn: vec3f) -> vec3f {
|
|
97
|
+
let l2 = dot(nIn, nIn);
|
|
98
|
+
if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }
|
|
99
|
+
return nIn * inverseSqrt(l2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
// ─── Shadow sampler (3×3 PCF) ───────────────────────────────────────
|
|
105
|
+
// 4096-map (MUST match Engine.SHADOW_MAP_SIZE), normal-bias 0.08, depth-bias
|
|
106
|
+
// 0.001. Unrolled — Safari's Metal backend doesn't unroll nested shadow loops
|
|
107
|
+
// reliably. The texel size was stale at 1/2048 after the map grew to 4096:
|
|
108
|
+
// PCF taps landed TWO texels apart, quantizing self-shadow edges into
|
|
109
|
+
// texel-sized gray/black squares that crawled with the animation.
|
|
110
|
+
|
|
111
|
+
export const SAMPLE_SHADOW_WGSL = /* wgsl */ `
|
|
112
|
+
|
|
113
|
+
fn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {
|
|
114
|
+
if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }
|
|
115
|
+
let biasedPos = worldPos + n * 0.08;
|
|
116
|
+
let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);
|
|
117
|
+
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
118
|
+
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
119
|
+
let cmpZ = ndc.z - 0.001;
|
|
120
|
+
let ts = 1.0 / 4096.0;
|
|
121
|
+
let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);
|
|
122
|
+
let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);
|
|
123
|
+
let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);
|
|
124
|
+
let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);
|
|
125
|
+
let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);
|
|
126
|
+
let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);
|
|
127
|
+
let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);
|
|
128
|
+
let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);
|
|
129
|
+
let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);
|
|
130
|
+
return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
// ─── Skinning vertex shader ─────────────────────────────────────────
|
|
136
|
+
// Four-bone linear blend skinning. Renormalizes weights when they don't sum to 1
|
|
137
|
+
// (PMX models occasionally ship with unnormalized weights on extras like hair tips).
|
|
138
|
+
// VS normalize on the outgoing normal is skipped — interpolation denormalizes it
|
|
139
|
+
// anyway and every fragment shader does `normalize(input.normal)` as its first line.
|
|
140
|
+
|
|
141
|
+
export const COMMON_VS_WGSL = /* wgsl */ `
|
|
142
|
+
|
|
143
|
+
@vertex fn vs(
|
|
144
|
+
@location(0) position: vec3f,
|
|
145
|
+
@location(1) normal: vec3f,
|
|
146
|
+
@location(2) uv: vec2f,
|
|
147
|
+
@location(3) joints0: vec4<u32>,
|
|
148
|
+
@location(4) weights0: vec4<f32>
|
|
149
|
+
) -> VertexOutput {
|
|
150
|
+
var output: VertexOutput;
|
|
151
|
+
let pos4 = vec4f(position, 1.0);
|
|
152
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
153
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
154
|
+
let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
155
|
+
var skinnedPos = vec4f(0.0);
|
|
156
|
+
var skinnedNrm = vec3f(0.0);
|
|
157
|
+
for (var i = 0u; i < 4u; i++) {
|
|
158
|
+
let m = skinMats[joints0[i]];
|
|
159
|
+
let w = nw[i];
|
|
160
|
+
skinnedPos += (m * pos4) * w;
|
|
161
|
+
skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;
|
|
162
|
+
}
|
|
163
|
+
output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);
|
|
164
|
+
output.normal = skinnedNrm;
|
|
165
|
+
output.uv = uv;
|
|
166
|
+
output.worldPos = skinnedPos.xyz;
|
|
167
|
+
output.restPos = position;
|
|
168
|
+
return output;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
// ─── FS output struct ───────────────────────────────────────────────
|
|
174
|
+
// Location 0: final radiance+alpha (blended into rg11b10ufloat; the HDR target
|
|
175
|
+
// has no alpha channel, but the blend equation still uses the .a you write here
|
|
176
|
+
// as the src-alpha factor that premultiplies rgb into the HDR target).
|
|
177
|
+
// Location 1: auxiliary rg8unorm carrying
|
|
178
|
+
// .r = bloom mask (1 = contributes to bloom, 0 = skip — e.g. ground).
|
|
179
|
+
// .g = accumulated canvas alpha — the channel that used to live in hdr.a
|
|
180
|
+
// before the switch to rg11b10ufloat. Sampled by composite to
|
|
181
|
+
// un-premultiply color for tonemap and to set the final drawable alpha
|
|
182
|
+
// (needed for the `premultiplied` canvas alphaMode that blends the
|
|
183
|
+
// WebGPU surface over the page background).
|
|
184
|
+
// FS output at location 1 must be vec4f — the blend state references src.a, and
|
|
185
|
+
// WebGPU requires the fragment output to provide an alpha component even though
|
|
186
|
+
// the rg8unorm target only stores .r and .g (extra components are discarded).
|
|
187
|
+
// Materials write mask = vec4f(1.0, 1.0, 0.0, color.a); ground writes
|
|
188
|
+
// vec4f(0.0, 1.0, 0.0, edgeFade). With src.a coming from the 4th component and
|
|
189
|
+
// src-alpha blending enabled:
|
|
190
|
+
// out.r = mask_r · src.a + dst.r · (1-src.a) (bloom mask, weighted by alpha)
|
|
191
|
+
// out.g = 1.0 · src.a + dst.g · (1-src.a) (canonical premultiplied alpha-over)
|
|
192
|
+
|
|
193
|
+
export const COMMON_FS_OUT_WGSL = /* wgsl */ `
|
|
194
|
+
|
|
195
|
+
struct FSOut {
|
|
196
|
+
@location(0) color: vec4f,
|
|
197
|
+
@location(1) mask: vec4f,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
// ─── Convenience: full shared prelude ───────────────────────────────
|
|
203
|
+
// Material files compose this as `${NODES_WGSL}${COMMON_MATERIAL_PRELUDE_WGSL}` to
|
|
204
|
+
// pull in everything structural. Each material then adds its own constants + fs().
|
|
205
|
+
|
|
206
|
+
export const COMMON_MATERIAL_PRELUDE_WGSL =
|
|
207
|
+
COMMON_BINDINGS_WGSL + SAMPLE_SHADOW_WGSL + COMMON_VS_WGSL + COMMON_FS_OUT_WGSL
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
// Composite: HDR scene + bloom pyramid → Filmic tone map → gamma → swapchain.
|
|
2
2
|
// Bloom tint/intensity applied at combine (EEVEE treats them as combine-stage params, not prefilter).
|
|
3
|
+
//
|
|
4
|
+
// The shader is a TEMPLATE: buildCompositeShader() emits either the base pass or
|
|
5
|
+
// a variant with a user background effect injected (setBackgroundEffect). The
|
|
6
|
+
// effect is background mode 3, a sibling of the 360 equirect (mode 2) — it reuses
|
|
7
|
+
// the same per-pixel view-ray reconstruction and composites in display space
|
|
8
|
+
// under the scene, so it never affects lighting, bloom, or tonemapping.
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
/** What a user background effect must define, documented once:
|
|
11
|
+
*
|
|
12
|
+
* fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
|
|
13
|
+
*
|
|
14
|
+
* - `ray` — normalized world-space view direction of this pixel (left-handed,
|
|
15
|
+
* +Z forward; identical to what the 360 skybox samples by).
|
|
16
|
+
* - `uv` — 0..1 across the canvas, origin bottom-left (shadertoy-style).
|
|
17
|
+
* - `time` — seconds since the effect was applied.
|
|
18
|
+
* - `bgResolution()` — canvas size in pixels, for aspect correction.
|
|
19
|
+
* - declared params arrive as `params.<name>` (f32 or vec3f).
|
|
20
|
+
* Return display-space sRGB + alpha, 0..1. The effect is a LAYER: it is
|
|
21
|
+
* over-composited onto the base background (solid color / 360 equirect /
|
|
22
|
+
* transparent) and sits behind the scene — alpha 0 lets the base show
|
|
23
|
+
* through, so e.g. a starfield returns stars with a transparent sky. */
|
|
24
|
+
export type CompositeEffectSource = {
|
|
25
|
+
/** User WGSL defining `background(...)` (plus any helpers it wants). */
|
|
26
|
+
wgsl: string
|
|
27
|
+
/** Codegen'd `struct BgParams {...}` + binding decl; empty when no params. */
|
|
28
|
+
paramsDecl: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const COMPOSITE_HEAD = /* wgsl */ `
|
|
5
32
|
// Pipeline-override constant: the engine creates two composite pipelines, one
|
|
6
33
|
// with APPLY_GAMMA=false (gamma=1 fast path) and one with APPLY_GAMMA=true.
|
|
7
34
|
// The 'if (APPLY_GAMMA)' below is resolved at pipeline-compile time — the
|
|
@@ -12,7 +39,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
12
39
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
13
40
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
14
41
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
15
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
42
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 7>;
|
|
16
43
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
17
44
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
18
45
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -29,10 +56,12 @@ override APPLY_GAMMA: bool = true;
|
|
|
29
56
|
@group(0) @binding(5) var filmicLut: texture_2d<f32>;
|
|
30
57
|
// viewU[0] = (exposure, invGamma, _, _); viewU[1] = (tint.rgb, intensity)
|
|
31
58
|
// viewU[2] = (background.rgb, mode) — display-space sRGB, composited UNDER the
|
|
32
|
-
// scene post-tonemap. mode: 0 transparent (DOM shows),
|
|
33
|
-
// 2 = 360 equirect skybox sampled by view ray.
|
|
59
|
+
// scene post-tonemap. BASE-layer mode: 0 transparent (DOM shows),
|
|
60
|
+
// 1 solid color, 2 = 360 equirect skybox sampled by view ray. A user
|
|
61
|
+
// WGSL effect is a separate LAYER over the base (viewU[6].y flag).
|
|
34
62
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
35
|
-
// viewU[5] = (camera forward, _) — refreshed per frame while
|
|
63
|
+
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
64
|
+
// viewU[6] = (time seconds, effect on/off, canvas width, canvas height).
|
|
36
65
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
37
66
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
38
67
|
|
|
@@ -50,6 +79,11 @@ fn filmic(x: f32) -> f32 {
|
|
|
50
79
|
return textureSampleLevel(filmicLut, bloomSamp, vec2f(u, 0.5), 0.0).r;
|
|
51
80
|
}
|
|
52
81
|
|
|
82
|
+
/** Canvas size in pixels — for user background effects (aspect correction). */
|
|
83
|
+
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
const COMPOSITE_BODY = /* wgsl */ `
|
|
53
87
|
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
54
88
|
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
55
89
|
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
@@ -76,21 +110,60 @@ fn filmic(x: f32) -> f32 {
|
|
|
76
110
|
if (APPLY_GAMMA) {
|
|
77
111
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
78
112
|
}
|
|
79
|
-
// Composite over the background in display space (premultiplied out).
|
|
113
|
+
// Composite over the background in display space (premultiplied out). The
|
|
114
|
+
// background is TWO layers: a base (transparent / solid color / 360 equirect)
|
|
115
|
+
// and an optional user WGSL effect over-composited onto it.
|
|
80
116
|
let bg = viewU[2];
|
|
81
|
-
var bgRgb = bg.rgb;
|
|
82
117
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
118
|
+
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
119
|
+
let fxOn = viewU[6].y > 0.5;
|
|
120
|
+
if (bg.w > 1.5 || fxOn) {
|
|
121
|
+
// The equirect and any effect both need this pixel's world-space view ray,
|
|
122
|
+
// rebuilt from the camera basis. The dome sits at infinity (no parallax) —
|
|
123
|
+
// PhotoDome-style, display-only.
|
|
87
124
|
let ndc = vec2f(fragCoord.x / fullSz.x * 2.0 - 1.0, 1.0 - fragCoord.y / fullSz.y * 2.0);
|
|
88
125
|
let dir = normalize(viewU[5].xyz + ndc.x * viewU[3].w * viewU[3].xyz + ndc.y * viewU[4].w * viewU[4].xyz);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
if (bg.w > 1.5) {
|
|
127
|
+
// LH world (+Z forward): longitude = atan2(x, z), Babylon-PhotoDome convention.
|
|
128
|
+
let su = 0.5 + atan2(dir.x, dir.z) * 0.15915494309; // 1/(2π)
|
|
129
|
+
let sv = 0.5 - asin(clamp(dir.y, -1.0, 1.0)) * 0.31830988618; // 1/π
|
|
130
|
+
bgPm = textureSampleLevel(bgEquirect, bloomSamp, vec2f(su, sv), 0.0).rgb;
|
|
131
|
+
}
|
|
132
|
+
BG_EFFECT_CALL
|
|
93
133
|
}
|
|
94
|
-
return vec4f(disp * alpha +
|
|
134
|
+
return vec4f(disp * alpha + bgPm * (1.0 - alpha), alpha + bgA * (1.0 - alpha));
|
|
95
135
|
}
|
|
96
136
|
`
|
|
137
|
+
|
|
138
|
+
// Base variant: no effect installed, the flag is never set — the ray block only
|
|
139
|
+
// runs for the equirect, and there is nothing to add. (`dir` may go unused when
|
|
140
|
+
// this compiles with mode<2 shaders; WGSL is fine with an unused let.)
|
|
141
|
+
const NO_EFFECT_CALL = `_ = dir;`
|
|
142
|
+
|
|
143
|
+
// uv flipped to bottom-left origin (shadertoy convention); clamped so a stray
|
|
144
|
+
// effect can't push negatives/NaN into the premultiplied composite. Standard
|
|
145
|
+
// OVER onto the base layer.
|
|
146
|
+
const EFFECT_CALL = /* wgsl */ `
|
|
147
|
+
if (fxOn) {
|
|
148
|
+
let bgUv = vec2f(fragCoord.x / fullSz.x, 1.0 - fragCoord.y / fullSz.y);
|
|
149
|
+
let fx = clamp(background(dir, bgUv, viewU[6].x), vec4f(0.0), vec4f(1.0));
|
|
150
|
+
bgPm = fx.rgb * fx.a + bgPm * (1.0 - fx.a);
|
|
151
|
+
bgA = fx.a + bgA * (1.0 - fx.a);
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
export function buildCompositeShader(effect?: CompositeEffectSource | null): string {
|
|
156
|
+
if (!effect) return COMPOSITE_HEAD + COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL)
|
|
157
|
+
return (
|
|
158
|
+
COMPOSITE_HEAD +
|
|
159
|
+
"\n// ── user background effect (setBackgroundEffect) ──\n" +
|
|
160
|
+
effect.paramsDecl +
|
|
161
|
+
"\n" +
|
|
162
|
+
effect.wgsl +
|
|
163
|
+
"\n" +
|
|
164
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim())
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
169
|
+
export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null)
|