reze-engine 0.1.14 → 0.1.16
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 +4 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +503 -491
- package/package.json +1 -1
- package/src/engine.ts +2330 -2321
package/dist/engine.js
CHANGED
|
@@ -81,124 +81,128 @@ export class Engine {
|
|
|
81
81
|
});
|
|
82
82
|
const shaderModule = this.device.createShaderModule({
|
|
83
83
|
label: "model shaders",
|
|
84
|
-
code: /* wgsl */ `
|
|
85
|
-
struct CameraUniforms {
|
|
86
|
-
view: mat4x4f,
|
|
87
|
-
projection: mat4x4f,
|
|
88
|
-
viewPos: vec3f,
|
|
89
|
-
_padding: f32,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
struct Light {
|
|
93
|
-
direction: vec3f,
|
|
94
|
-
_padding1: f32,
|
|
95
|
-
color: vec3f,
|
|
96
|
-
intensity: f32,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
struct LightUniforms {
|
|
100
|
-
ambient: f32,
|
|
101
|
-
lightCount: f32,
|
|
102
|
-
_padding1: f32,
|
|
103
|
-
_padding2: f32,
|
|
104
|
-
lights: array<Light, 4>,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
struct MaterialUniforms {
|
|
108
|
-
alpha: f32,
|
|
109
|
-
alphaMultiplier: f32,
|
|
110
|
-
rimIntensity: f32,
|
|
111
|
-
rimPower: f32,
|
|
112
|
-
rimColor: vec3f,
|
|
113
|
-
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
struct VertexOutput {
|
|
117
|
-
@builtin(position) position: vec4f,
|
|
118
|
-
@location(0) normal: vec3f,
|
|
119
|
-
@location(1) uv: vec2f,
|
|
120
|
-
@location(2) worldPos: vec3f,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
124
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
125
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
126
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
127
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
128
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
129
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
130
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
131
|
-
|
|
132
|
-
@vertex fn vs(
|
|
133
|
-
@location(0) position: vec3f,
|
|
134
|
-
@location(1) normal: vec3f,
|
|
135
|
-
@location(2) uv: vec2f,
|
|
136
|
-
@location(3) joints0: vec4<u32>,
|
|
137
|
-
@location(4) weights0: vec4<f32>
|
|
138
|
-
) -> VertexOutput {
|
|
139
|
-
var output: VertexOutput;
|
|
140
|
-
let pos4 = vec4f(position, 1.0);
|
|
141
|
-
|
|
142
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
143
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
144
|
-
var normalizedWeights: vec4f;
|
|
145
|
-
if (weightSum > 0.0001) {
|
|
146
|
-
normalizedWeights = weights0 / weightSum;
|
|
147
|
-
} else {
|
|
148
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
152
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
153
|
-
for (var i = 0u; i < 4u; i++) {
|
|
154
|
-
let j = joints0[i];
|
|
155
|
-
let w = normalizedWeights[i];
|
|
156
|
-
let m = skinMats[j];
|
|
157
|
-
skinnedPos += (m * pos4) * w;
|
|
158
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
159
|
-
skinnedNrm += (r3 * normal) * w;
|
|
160
|
-
}
|
|
161
|
-
let worldPos = skinnedPos.xyz;
|
|
162
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
163
|
-
output.normal = normalize(skinnedNrm);
|
|
164
|
-
output.uv = uv;
|
|
165
|
-
output.worldPos = worldPos;
|
|
166
|
-
return output;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
170
|
-
let n = normalize(input.normal);
|
|
171
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
172
|
-
|
|
173
|
-
var lightAccum = vec3f(light.ambient);
|
|
174
|
-
let numLights = u32(light.lightCount);
|
|
175
|
-
for (var i = 0u; i < numLights; i++) {
|
|
176
|
-
let l = -light.lights[i].direction;
|
|
177
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
178
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
179
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
180
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
181
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Rim light calculation
|
|
185
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
186
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
187
|
-
rimFactor = pow(rimFactor, material.rimPower);
|
|
188
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
189
|
-
|
|
190
|
-
let color = albedo * lightAccum + rimLight;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
84
|
+
code: /* wgsl */ `
|
|
85
|
+
struct CameraUniforms {
|
|
86
|
+
view: mat4x4f,
|
|
87
|
+
projection: mat4x4f,
|
|
88
|
+
viewPos: vec3f,
|
|
89
|
+
_padding: f32,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
struct Light {
|
|
93
|
+
direction: vec3f,
|
|
94
|
+
_padding1: f32,
|
|
95
|
+
color: vec3f,
|
|
96
|
+
intensity: f32,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
struct LightUniforms {
|
|
100
|
+
ambient: f32,
|
|
101
|
+
lightCount: f32,
|
|
102
|
+
_padding1: f32,
|
|
103
|
+
_padding2: f32,
|
|
104
|
+
lights: array<Light, 4>,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
struct MaterialUniforms {
|
|
108
|
+
alpha: f32,
|
|
109
|
+
alphaMultiplier: f32,
|
|
110
|
+
rimIntensity: f32,
|
|
111
|
+
rimPower: f32,
|
|
112
|
+
rimColor: vec3f,
|
|
113
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
struct VertexOutput {
|
|
117
|
+
@builtin(position) position: vec4f,
|
|
118
|
+
@location(0) normal: vec3f,
|
|
119
|
+
@location(1) uv: vec2f,
|
|
120
|
+
@location(2) worldPos: vec3f,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
124
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
125
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
126
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
127
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
128
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
129
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
130
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
131
|
+
|
|
132
|
+
@vertex fn vs(
|
|
133
|
+
@location(0) position: vec3f,
|
|
134
|
+
@location(1) normal: vec3f,
|
|
135
|
+
@location(2) uv: vec2f,
|
|
136
|
+
@location(3) joints0: vec4<u32>,
|
|
137
|
+
@location(4) weights0: vec4<f32>
|
|
138
|
+
) -> VertexOutput {
|
|
139
|
+
var output: VertexOutput;
|
|
140
|
+
let pos4 = vec4f(position, 1.0);
|
|
141
|
+
|
|
142
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
143
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
144
|
+
var normalizedWeights: vec4f;
|
|
145
|
+
if (weightSum > 0.0001) {
|
|
146
|
+
normalizedWeights = weights0 / weightSum;
|
|
147
|
+
} else {
|
|
148
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
152
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
153
|
+
for (var i = 0u; i < 4u; i++) {
|
|
154
|
+
let j = joints0[i];
|
|
155
|
+
let w = normalizedWeights[i];
|
|
156
|
+
let m = skinMats[j];
|
|
157
|
+
skinnedPos += (m * pos4) * w;
|
|
158
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
159
|
+
skinnedNrm += (r3 * normal) * w;
|
|
160
|
+
}
|
|
161
|
+
let worldPos = skinnedPos.xyz;
|
|
162
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
163
|
+
output.normal = normalize(skinnedNrm);
|
|
164
|
+
output.uv = uv;
|
|
165
|
+
output.worldPos = worldPos;
|
|
166
|
+
return output;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
170
|
+
let n = normalize(input.normal);
|
|
171
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
172
|
+
|
|
173
|
+
var lightAccum = vec3f(light.ambient);
|
|
174
|
+
let numLights = u32(light.lightCount);
|
|
175
|
+
for (var i = 0u; i < numLights; i++) {
|
|
176
|
+
let l = -light.lights[i].direction;
|
|
177
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
178
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
179
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
180
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
181
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Rim light calculation
|
|
185
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
186
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
187
|
+
rimFactor = pow(rimFactor, material.rimPower);
|
|
188
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
189
|
+
|
|
190
|
+
let color = albedo * lightAccum + rimLight;
|
|
191
|
+
|
|
192
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
193
|
+
if (material.isOverEyes > 0.5) {
|
|
194
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (finalAlpha < 0.001) {
|
|
198
|
+
discard;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
202
|
+
}
|
|
198
203
|
`,
|
|
199
204
|
});
|
|
200
205
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
201
|
-
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
202
206
|
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
203
207
|
label: "shared material bind group layout",
|
|
204
208
|
entries: [
|
|
@@ -286,71 +290,77 @@ export class Engine {
|
|
|
286
290
|
});
|
|
287
291
|
const outlineShaderModule = this.device.createShaderModule({
|
|
288
292
|
label: "outline shaders",
|
|
289
|
-
code: /* wgsl */ `
|
|
290
|
-
struct CameraUniforms {
|
|
291
|
-
view: mat4x4f,
|
|
292
|
-
projection: mat4x4f,
|
|
293
|
-
viewPos: vec3f,
|
|
294
|
-
_padding: f32,
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
struct MaterialUniforms {
|
|
298
|
-
edgeColor: vec4f,
|
|
299
|
-
edgeSize: f32,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
306
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
307
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
308
|
-
|
|
309
|
-
struct VertexOutput {
|
|
310
|
-
@builtin(position) position: vec4f,
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
@vertex fn vs(
|
|
314
|
-
@location(0) position: vec3f,
|
|
315
|
-
@location(1) normal: vec3f,
|
|
316
|
-
@location(3) joints0: vec4<u32>,
|
|
317
|
-
@location(4) weights0: vec4<f32>
|
|
318
|
-
) -> VertexOutput {
|
|
319
|
-
var output: VertexOutput;
|
|
320
|
-
let pos4 = vec4f(position, 1.0);
|
|
321
|
-
|
|
322
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
323
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
324
|
-
var normalizedWeights: vec4f;
|
|
325
|
-
if (weightSum > 0.0001) {
|
|
326
|
-
normalizedWeights = weights0 / weightSum;
|
|
327
|
-
} else {
|
|
328
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
332
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
333
|
-
for (var i = 0u; i < 4u; i++) {
|
|
334
|
-
let j = joints0[i];
|
|
335
|
-
let w = normalizedWeights[i];
|
|
336
|
-
let m = skinMats[j];
|
|
337
|
-
skinnedPos += (m * pos4) * w;
|
|
338
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
339
|
-
skinnedNrm += (r3 * normal) * w;
|
|
340
|
-
}
|
|
341
|
-
let worldPos = skinnedPos.xyz;
|
|
342
|
-
let worldNormal = normalize(skinnedNrm);
|
|
343
|
-
|
|
344
|
-
// MMD invert hull: expand vertices outward along normals
|
|
345
|
-
let scaleFactor = 0.01;
|
|
346
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
347
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
348
|
-
return output;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
352
|
-
|
|
353
|
-
|
|
293
|
+
code: /* wgsl */ `
|
|
294
|
+
struct CameraUniforms {
|
|
295
|
+
view: mat4x4f,
|
|
296
|
+
projection: mat4x4f,
|
|
297
|
+
viewPos: vec3f,
|
|
298
|
+
_padding: f32,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
struct MaterialUniforms {
|
|
302
|
+
edgeColor: vec4f,
|
|
303
|
+
edgeSize: f32,
|
|
304
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
305
|
+
_padding1: f32,
|
|
306
|
+
_padding2: f32,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
310
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
311
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
312
|
+
|
|
313
|
+
struct VertexOutput {
|
|
314
|
+
@builtin(position) position: vec4f,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
@vertex fn vs(
|
|
318
|
+
@location(0) position: vec3f,
|
|
319
|
+
@location(1) normal: vec3f,
|
|
320
|
+
@location(3) joints0: vec4<u32>,
|
|
321
|
+
@location(4) weights0: vec4<f32>
|
|
322
|
+
) -> VertexOutput {
|
|
323
|
+
var output: VertexOutput;
|
|
324
|
+
let pos4 = vec4f(position, 1.0);
|
|
325
|
+
|
|
326
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
327
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
328
|
+
var normalizedWeights: vec4f;
|
|
329
|
+
if (weightSum > 0.0001) {
|
|
330
|
+
normalizedWeights = weights0 / weightSum;
|
|
331
|
+
} else {
|
|
332
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
336
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
337
|
+
for (var i = 0u; i < 4u; i++) {
|
|
338
|
+
let j = joints0[i];
|
|
339
|
+
let w = normalizedWeights[i];
|
|
340
|
+
let m = skinMats[j];
|
|
341
|
+
skinnedPos += (m * pos4) * w;
|
|
342
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
343
|
+
skinnedNrm += (r3 * normal) * w;
|
|
344
|
+
}
|
|
345
|
+
let worldPos = skinnedPos.xyz;
|
|
346
|
+
let worldNormal = normalize(skinnedNrm);
|
|
347
|
+
|
|
348
|
+
// MMD invert hull: expand vertices outward along normals
|
|
349
|
+
let scaleFactor = 0.01;
|
|
350
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
351
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
352
|
+
return output;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
356
|
+
var color = material.edgeColor;
|
|
357
|
+
|
|
358
|
+
if (material.isOverEyes > 0.5) {
|
|
359
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return color;
|
|
363
|
+
}
|
|
354
364
|
`,
|
|
355
365
|
});
|
|
356
366
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -416,9 +426,9 @@ export class Engine {
|
|
|
416
426
|
count: this.sampleCount,
|
|
417
427
|
},
|
|
418
428
|
});
|
|
419
|
-
//
|
|
420
|
-
this.
|
|
421
|
-
label: "hair outline pipeline",
|
|
429
|
+
// Unified hair outline pipeline: single pass without stencil testing, uses depth test "less-equal" to draw everywhere hair exists
|
|
430
|
+
this.hairUnifiedOutlinePipeline = this.device.createRenderPipeline({
|
|
431
|
+
label: "unified hair outline pipeline",
|
|
422
432
|
layout: outlinePipelineLayout,
|
|
423
433
|
vertex: {
|
|
424
434
|
module: outlineShaderModule,
|
|
@@ -474,44 +484,28 @@ export class Engine {
|
|
|
474
484
|
depthStencil: {
|
|
475
485
|
format: "depth24plus-stencil8",
|
|
476
486
|
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
477
|
-
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
depthFailOp: "keep",
|
|
482
|
-
passOp: "keep",
|
|
483
|
-
},
|
|
484
|
-
stencilBack: {
|
|
485
|
-
compare: "not-equal",
|
|
486
|
-
failOp: "keep",
|
|
487
|
-
depthFailOp: "keep",
|
|
488
|
-
passOp: "keep",
|
|
489
|
-
},
|
|
487
|
+
depthCompare: "less-equal", // Only draw where hair depth exists (no stencil test needed)
|
|
488
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
489
|
+
depthBiasSlopeScale: 0.0,
|
|
490
|
+
depthBiasClamp: 0.0,
|
|
490
491
|
},
|
|
491
492
|
multisample: {
|
|
492
493
|
count: this.sampleCount,
|
|
493
494
|
},
|
|
494
495
|
});
|
|
495
|
-
//
|
|
496
|
-
this.
|
|
497
|
-
label: "
|
|
498
|
-
layout:
|
|
496
|
+
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
497
|
+
this.eyePipeline = this.device.createRenderPipeline({
|
|
498
|
+
label: "eye overlay pipeline",
|
|
499
|
+
layout: sharedPipelineLayout,
|
|
499
500
|
vertex: {
|
|
500
|
-
module:
|
|
501
|
+
module: shaderModule,
|
|
501
502
|
buffers: [
|
|
502
503
|
{
|
|
503
504
|
arrayStride: 8 * 4,
|
|
504
505
|
attributes: [
|
|
505
|
-
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
format: "float32x3",
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
shaderLocation: 1,
|
|
512
|
-
offset: 3 * 4,
|
|
513
|
-
format: "float32x3",
|
|
514
|
-
},
|
|
506
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
507
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
508
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
515
509
|
],
|
|
516
510
|
},
|
|
517
511
|
{
|
|
@@ -525,7 +519,7 @@ export class Engine {
|
|
|
525
519
|
],
|
|
526
520
|
},
|
|
527
521
|
fragment: {
|
|
528
|
-
module:
|
|
522
|
+
module: shaderModule,
|
|
529
523
|
targets: [
|
|
530
524
|
{
|
|
531
525
|
format: this.presentationFormat,
|
|
@@ -544,47 +538,86 @@ export class Engine {
|
|
|
544
538
|
},
|
|
545
539
|
],
|
|
546
540
|
},
|
|
547
|
-
primitive: {
|
|
548
|
-
cullMode: "back",
|
|
549
|
-
},
|
|
541
|
+
primitive: { cullMode: "none" },
|
|
550
542
|
depthStencil: {
|
|
551
543
|
format: "depth24plus-stencil8",
|
|
552
544
|
depthWriteEnabled: false, // Don't write depth
|
|
553
|
-
depthCompare: "less
|
|
554
|
-
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
555
|
-
depthBiasSlopeScale: 0.0,
|
|
556
|
-
depthBiasClamp: 0.0,
|
|
545
|
+
depthCompare: "less", // Respect existing depth
|
|
557
546
|
stencilFront: {
|
|
558
|
-
compare: "
|
|
547
|
+
compare: "always",
|
|
559
548
|
failOp: "keep",
|
|
560
549
|
depthFailOp: "keep",
|
|
561
|
-
passOp: "
|
|
550
|
+
passOp: "replace", // Write stencil value 1
|
|
562
551
|
},
|
|
563
552
|
stencilBack: {
|
|
564
|
-
compare: "
|
|
553
|
+
compare: "always",
|
|
565
554
|
failOp: "keep",
|
|
566
555
|
depthFailOp: "keep",
|
|
567
|
-
passOp: "
|
|
556
|
+
passOp: "replace",
|
|
568
557
|
},
|
|
569
558
|
},
|
|
570
|
-
multisample: {
|
|
571
|
-
count: this.sampleCount,
|
|
572
|
-
},
|
|
559
|
+
multisample: { count: this.sampleCount },
|
|
573
560
|
});
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
561
|
+
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
562
|
+
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
563
|
+
label: "depth only shader",
|
|
564
|
+
code: /* wgsl */ `
|
|
565
|
+
struct CameraUniforms {
|
|
566
|
+
view: mat4x4f,
|
|
567
|
+
projection: mat4x4f,
|
|
568
|
+
viewPos: vec3f,
|
|
569
|
+
_padding: f32,
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
573
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
574
|
+
|
|
575
|
+
@vertex fn vs(
|
|
576
|
+
@location(0) position: vec3f,
|
|
577
|
+
@location(1) normal: vec3f,
|
|
578
|
+
@location(3) joints0: vec4<u32>,
|
|
579
|
+
@location(4) weights0: vec4<f32>
|
|
580
|
+
) -> @builtin(position) vec4f {
|
|
581
|
+
let pos4 = vec4f(position, 1.0);
|
|
582
|
+
|
|
583
|
+
// Normalize weights
|
|
584
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
585
|
+
var normalizedWeights: vec4f;
|
|
586
|
+
if (weightSum > 0.0001) {
|
|
587
|
+
normalizedWeights = weights0 / weightSum;
|
|
588
|
+
} else {
|
|
589
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
593
|
+
for (var i = 0u; i < 4u; i++) {
|
|
594
|
+
let j = joints0[i];
|
|
595
|
+
let w = normalizedWeights[i];
|
|
596
|
+
let m = skinMats[j];
|
|
597
|
+
skinnedPos += (m * pos4) * w;
|
|
598
|
+
}
|
|
599
|
+
let worldPos = skinnedPos.xyz;
|
|
600
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
601
|
+
return clipPos;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
605
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
606
|
+
}
|
|
607
|
+
`,
|
|
608
|
+
});
|
|
609
|
+
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
610
|
+
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
611
|
+
label: "hair depth pre-pass",
|
|
578
612
|
layout: sharedPipelineLayout,
|
|
579
613
|
vertex: {
|
|
580
|
-
module:
|
|
614
|
+
module: depthOnlyShaderModule,
|
|
581
615
|
buffers: [
|
|
582
616
|
{
|
|
583
617
|
arrayStride: 8 * 4,
|
|
584
618
|
attributes: [
|
|
585
619
|
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
586
620
|
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
587
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
588
621
|
],
|
|
589
622
|
},
|
|
590
623
|
{
|
|
@@ -598,48 +631,26 @@ export class Engine {
|
|
|
598
631
|
],
|
|
599
632
|
},
|
|
600
633
|
fragment: {
|
|
601
|
-
module:
|
|
634
|
+
module: depthOnlyShaderModule,
|
|
635
|
+
entryPoint: "fs",
|
|
602
636
|
targets: [
|
|
603
637
|
{
|
|
604
638
|
format: this.presentationFormat,
|
|
605
|
-
|
|
606
|
-
color: {
|
|
607
|
-
srcFactor: "src-alpha",
|
|
608
|
-
dstFactor: "one-minus-src-alpha",
|
|
609
|
-
operation: "add",
|
|
610
|
-
},
|
|
611
|
-
alpha: {
|
|
612
|
-
srcFactor: "one",
|
|
613
|
-
dstFactor: "one-minus-src-alpha",
|
|
614
|
-
operation: "add",
|
|
615
|
-
},
|
|
616
|
-
},
|
|
639
|
+
writeMask: 0, // Disable all color writes - we only care about depth
|
|
617
640
|
},
|
|
618
641
|
],
|
|
619
642
|
},
|
|
620
643
|
primitive: { cullMode: "none" },
|
|
621
644
|
depthStencil: {
|
|
622
645
|
format: "depth24plus-stencil8",
|
|
623
|
-
depthWriteEnabled: true,
|
|
646
|
+
depthWriteEnabled: true,
|
|
624
647
|
depthCompare: "less",
|
|
625
|
-
stencilFront: {
|
|
626
|
-
compare: "equal", // Only render where stencil == 1
|
|
627
|
-
failOp: "keep",
|
|
628
|
-
depthFailOp: "keep",
|
|
629
|
-
passOp: "keep",
|
|
630
|
-
},
|
|
631
|
-
stencilBack: {
|
|
632
|
-
compare: "equal",
|
|
633
|
-
failOp: "keep",
|
|
634
|
-
depthFailOp: "keep",
|
|
635
|
-
passOp: "keep",
|
|
636
|
-
},
|
|
637
648
|
},
|
|
638
649
|
multisample: { count: this.sampleCount },
|
|
639
650
|
});
|
|
640
|
-
//
|
|
641
|
-
this.
|
|
642
|
-
label: "hair pipeline (over
|
|
651
|
+
// Unified hair pipeline for over-eyes (stencil == 1): single pass with dynamic branching
|
|
652
|
+
this.hairUnifiedPipelineOverEyes = this.device.createRenderPipeline({
|
|
653
|
+
label: "unified hair pipeline (over eyes)",
|
|
643
654
|
layout: sharedPipelineLayout,
|
|
644
655
|
vertex: {
|
|
645
656
|
module: shaderModule,
|
|
@@ -685,16 +696,16 @@ export class Engine {
|
|
|
685
696
|
primitive: { cullMode: "none" },
|
|
686
697
|
depthStencil: {
|
|
687
698
|
format: "depth24plus-stencil8",
|
|
688
|
-
depthWriteEnabled:
|
|
689
|
-
depthCompare: "
|
|
699
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
700
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
690
701
|
stencilFront: {
|
|
691
|
-
compare: "
|
|
702
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
692
703
|
failOp: "keep",
|
|
693
704
|
depthFailOp: "keep",
|
|
694
705
|
passOp: "keep",
|
|
695
706
|
},
|
|
696
707
|
stencilBack: {
|
|
697
|
-
compare: "
|
|
708
|
+
compare: "equal",
|
|
698
709
|
failOp: "keep",
|
|
699
710
|
depthFailOp: "keep",
|
|
700
711
|
passOp: "keep",
|
|
@@ -702,9 +713,9 @@ export class Engine {
|
|
|
702
713
|
},
|
|
703
714
|
multisample: { count: this.sampleCount },
|
|
704
715
|
});
|
|
705
|
-
//
|
|
706
|
-
this.
|
|
707
|
-
label: "
|
|
716
|
+
// Unified pipeline for hair over non-eyes (stencil != 1)
|
|
717
|
+
this.hairUnifiedPipelineOverNonEyes = this.device.createRenderPipeline({
|
|
718
|
+
label: "unified hair pipeline (over non-eyes)",
|
|
708
719
|
layout: sharedPipelineLayout,
|
|
709
720
|
vertex: {
|
|
710
721
|
module: shaderModule,
|
|
@@ -750,19 +761,19 @@ export class Engine {
|
|
|
750
761
|
primitive: { cullMode: "none" },
|
|
751
762
|
depthStencil: {
|
|
752
763
|
format: "depth24plus-stencil8",
|
|
753
|
-
depthWriteEnabled: false, // Don't write depth
|
|
754
|
-
depthCompare: "
|
|
764
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
765
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
755
766
|
stencilFront: {
|
|
756
|
-
compare: "
|
|
767
|
+
compare: "not-equal", // Only render where stencil != 1 (over non-eyes)
|
|
757
768
|
failOp: "keep",
|
|
758
769
|
depthFailOp: "keep",
|
|
759
|
-
passOp: "
|
|
770
|
+
passOp: "keep",
|
|
760
771
|
},
|
|
761
772
|
stencilBack: {
|
|
762
|
-
compare: "
|
|
773
|
+
compare: "not-equal",
|
|
763
774
|
failOp: "keep",
|
|
764
775
|
depthFailOp: "keep",
|
|
765
|
-
passOp: "
|
|
776
|
+
passOp: "keep",
|
|
766
777
|
},
|
|
767
778
|
},
|
|
768
779
|
multisample: { count: this.sampleCount },
|
|
@@ -772,31 +783,31 @@ export class Engine {
|
|
|
772
783
|
createSkinMatrixComputePipeline() {
|
|
773
784
|
const computeShader = this.device.createShaderModule({
|
|
774
785
|
label: "skin matrix compute",
|
|
775
|
-
code: /* wgsl */ `
|
|
776
|
-
struct BoneCountUniform {
|
|
777
|
-
count: u32,
|
|
778
|
-
_padding1: u32,
|
|
779
|
-
_padding2: u32,
|
|
780
|
-
_padding3: u32,
|
|
781
|
-
_padding4: vec4<u32>,
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
785
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
786
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
787
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
788
|
-
|
|
789
|
-
@compute @workgroup_size(64)
|
|
790
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
791
|
-
let boneIndex = globalId.x;
|
|
792
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
793
|
-
if (boneIndex >= boneCount.count) {
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
let worldMat = worldMatrices[boneIndex];
|
|
797
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
798
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
799
|
-
}
|
|
786
|
+
code: /* wgsl */ `
|
|
787
|
+
struct BoneCountUniform {
|
|
788
|
+
count: u32,
|
|
789
|
+
_padding1: u32,
|
|
790
|
+
_padding2: u32,
|
|
791
|
+
_padding3: u32,
|
|
792
|
+
_padding4: vec4<u32>,
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
796
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
797
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
798
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
799
|
+
|
|
800
|
+
@compute @workgroup_size(64)
|
|
801
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
802
|
+
let boneIndex = globalId.x;
|
|
803
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
804
|
+
if (boneIndex >= boneCount.count) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
let worldMat = worldMatrices[boneIndex];
|
|
808
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
809
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
810
|
+
}
|
|
800
811
|
`,
|
|
801
812
|
});
|
|
802
813
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -850,143 +861,143 @@ export class Engine {
|
|
|
850
861
|
// Bloom extraction shader (extracts bright areas)
|
|
851
862
|
const bloomExtractShader = this.device.createShaderModule({
|
|
852
863
|
label: "bloom extract",
|
|
853
|
-
code: /* wgsl */ `
|
|
854
|
-
struct VertexOutput {
|
|
855
|
-
@builtin(position) position: vec4f,
|
|
856
|
-
@location(0) uv: vec2f,
|
|
857
|
-
};
|
|
858
|
-
|
|
859
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
860
|
-
var output: VertexOutput;
|
|
861
|
-
// Generate fullscreen quad from vertex index
|
|
862
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
863
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
864
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
865
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
866
|
-
return output;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
struct BloomExtractUniforms {
|
|
870
|
-
threshold: f32,
|
|
871
|
-
_padding1: f32,
|
|
872
|
-
_padding2: f32,
|
|
873
|
-
_padding3: f32,
|
|
874
|
-
_padding4: f32,
|
|
875
|
-
_padding5: f32,
|
|
876
|
-
_padding6: f32,
|
|
877
|
-
_padding7: f32,
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
881
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
882
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
883
|
-
|
|
884
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
885
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
886
|
-
// Extract bright areas above threshold
|
|
887
|
-
let threshold = extractUniforms.threshold;
|
|
888
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
889
|
-
return vec4f(bloom, color.a);
|
|
890
|
-
}
|
|
864
|
+
code: /* wgsl */ `
|
|
865
|
+
struct VertexOutput {
|
|
866
|
+
@builtin(position) position: vec4f,
|
|
867
|
+
@location(0) uv: vec2f,
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
871
|
+
var output: VertexOutput;
|
|
872
|
+
// Generate fullscreen quad from vertex index
|
|
873
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
874
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
875
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
876
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
877
|
+
return output;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
struct BloomExtractUniforms {
|
|
881
|
+
threshold: f32,
|
|
882
|
+
_padding1: f32,
|
|
883
|
+
_padding2: f32,
|
|
884
|
+
_padding3: f32,
|
|
885
|
+
_padding4: f32,
|
|
886
|
+
_padding5: f32,
|
|
887
|
+
_padding6: f32,
|
|
888
|
+
_padding7: f32,
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
892
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
893
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
894
|
+
|
|
895
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
896
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
897
|
+
// Extract bright areas above threshold
|
|
898
|
+
let threshold = extractUniforms.threshold;
|
|
899
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
900
|
+
return vec4f(bloom, color.a);
|
|
901
|
+
}
|
|
891
902
|
`,
|
|
892
903
|
});
|
|
893
904
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
894
905
|
const bloomBlurShader = this.device.createShaderModule({
|
|
895
906
|
label: "bloom blur",
|
|
896
|
-
code: /* wgsl */ `
|
|
897
|
-
struct VertexOutput {
|
|
898
|
-
@builtin(position) position: vec4f,
|
|
899
|
-
@location(0) uv: vec2f,
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
903
|
-
var output: VertexOutput;
|
|
904
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
905
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
906
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
907
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
908
|
-
return output;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
struct BlurUniforms {
|
|
912
|
-
direction: vec2f,
|
|
913
|
-
_padding1: f32,
|
|
914
|
-
_padding2: f32,
|
|
915
|
-
_padding3: f32,
|
|
916
|
-
_padding4: f32,
|
|
917
|
-
_padding5: f32,
|
|
918
|
-
_padding6: f32,
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
922
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
923
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
924
|
-
|
|
925
|
-
// 9-tap gaussian blur
|
|
926
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
927
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
928
|
-
var result = vec4f(0.0);
|
|
929
|
-
|
|
930
|
-
// Gaussian weights for 9-tap filter
|
|
931
|
-
let weights = array<f32, 9>(
|
|
932
|
-
0.01621622, 0.05405405, 0.12162162,
|
|
933
|
-
0.19459459, 0.22702703,
|
|
934
|
-
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
938
|
-
|
|
939
|
-
for (var i = 0u; i < 9u; i++) {
|
|
940
|
-
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
941
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
return result;
|
|
945
|
-
}
|
|
907
|
+
code: /* wgsl */ `
|
|
908
|
+
struct VertexOutput {
|
|
909
|
+
@builtin(position) position: vec4f,
|
|
910
|
+
@location(0) uv: vec2f,
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
914
|
+
var output: VertexOutput;
|
|
915
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
916
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
917
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
918
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
919
|
+
return output;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
struct BlurUniforms {
|
|
923
|
+
direction: vec2f,
|
|
924
|
+
_padding1: f32,
|
|
925
|
+
_padding2: f32,
|
|
926
|
+
_padding3: f32,
|
|
927
|
+
_padding4: f32,
|
|
928
|
+
_padding5: f32,
|
|
929
|
+
_padding6: f32,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
933
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
934
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
935
|
+
|
|
936
|
+
// 9-tap gaussian blur
|
|
937
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
938
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
939
|
+
var result = vec4f(0.0);
|
|
940
|
+
|
|
941
|
+
// Gaussian weights for 9-tap filter
|
|
942
|
+
let weights = array<f32, 9>(
|
|
943
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
944
|
+
0.19459459, 0.22702703,
|
|
945
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
949
|
+
|
|
950
|
+
for (var i = 0u; i < 9u; i++) {
|
|
951
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
952
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return result;
|
|
956
|
+
}
|
|
946
957
|
`,
|
|
947
958
|
});
|
|
948
959
|
// Bloom composition shader (combines original scene with bloom)
|
|
949
960
|
const bloomComposeShader = this.device.createShaderModule({
|
|
950
961
|
label: "bloom compose",
|
|
951
|
-
code: /* wgsl */ `
|
|
952
|
-
struct VertexOutput {
|
|
953
|
-
@builtin(position) position: vec4f,
|
|
954
|
-
@location(0) uv: vec2f,
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
958
|
-
var output: VertexOutput;
|
|
959
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
960
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
961
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
962
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
963
|
-
return output;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
struct BloomComposeUniforms {
|
|
967
|
-
intensity: f32,
|
|
968
|
-
_padding1: f32,
|
|
969
|
-
_padding2: f32,
|
|
970
|
-
_padding3: f32,
|
|
971
|
-
_padding4: f32,
|
|
972
|
-
_padding5: f32,
|
|
973
|
-
_padding6: f32,
|
|
974
|
-
_padding7: f32,
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
978
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
979
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
980
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
981
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
982
|
-
|
|
983
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
984
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
985
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
986
|
-
// Additive blending with intensity control
|
|
987
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
988
|
-
return vec4f(result, scene.a);
|
|
989
|
-
}
|
|
962
|
+
code: /* wgsl */ `
|
|
963
|
+
struct VertexOutput {
|
|
964
|
+
@builtin(position) position: vec4f,
|
|
965
|
+
@location(0) uv: vec2f,
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
969
|
+
var output: VertexOutput;
|
|
970
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
971
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
972
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
973
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
974
|
+
return output;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
struct BloomComposeUniforms {
|
|
978
|
+
intensity: f32,
|
|
979
|
+
_padding1: f32,
|
|
980
|
+
_padding2: f32,
|
|
981
|
+
_padding3: f32,
|
|
982
|
+
_padding4: f32,
|
|
983
|
+
_padding5: f32,
|
|
984
|
+
_padding6: f32,
|
|
985
|
+
_padding7: f32,
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
989
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
990
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
991
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
992
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
993
|
+
|
|
994
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
995
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
996
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
997
|
+
// Additive blending with intensity control
|
|
998
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
999
|
+
return vec4f(result, scene.a);
|
|
1000
|
+
}
|
|
990
1001
|
`,
|
|
991
1002
|
});
|
|
992
1003
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1194,9 +1205,9 @@ export class Engine {
|
|
|
1194
1205
|
depthClearValue: 1.0,
|
|
1195
1206
|
depthLoadOp: "clear",
|
|
1196
1207
|
depthStoreOp: "store",
|
|
1197
|
-
stencilClearValue: 0,
|
|
1198
|
-
stencilLoadOp: "clear",
|
|
1199
|
-
stencilStoreOp: "
|
|
1208
|
+
stencilClearValue: 0,
|
|
1209
|
+
stencilLoadOp: "clear",
|
|
1210
|
+
stencilStoreOp: "discard", // Discard stencil after frame to save bandwidth (we only use it during rendering)
|
|
1200
1211
|
},
|
|
1201
1212
|
};
|
|
1202
1213
|
this.camera.aspect = width / height;
|
|
@@ -1444,7 +1455,7 @@ export class Engine {
|
|
|
1444
1455
|
materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
|
|
1445
1456
|
materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
|
|
1446
1457
|
materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
|
|
1447
|
-
materialUniformData[7] = 0.0;
|
|
1458
|
+
materialUniformData[7] = 0.0;
|
|
1448
1459
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1449
1460
|
label: `material uniform: ${mat.name}`,
|
|
1450
1461
|
size: materialUniformData.byteLength,
|
|
@@ -1476,22 +1487,34 @@ export class Engine {
|
|
|
1476
1487
|
});
|
|
1477
1488
|
}
|
|
1478
1489
|
else if (mat.isHair) {
|
|
1479
|
-
//
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1490
|
+
// Hair materials: create bind groups for unified pipeline with dynamic branching
|
|
1491
|
+
const materialUniformDataHair = new Float32Array(8);
|
|
1492
|
+
materialUniformDataHair[0] = materialAlpha;
|
|
1493
|
+
materialUniformDataHair[1] = 1.0; // alphaMultiplier: base value, shader will adjust
|
|
1494
|
+
materialUniformDataHair[2] = this.rimLightIntensity;
|
|
1495
|
+
materialUniformDataHair[3] = this.rimLightPower;
|
|
1496
|
+
materialUniformDataHair[4] = this.rimLightColor[0]; // rimColor.r
|
|
1497
|
+
materialUniformDataHair[5] = this.rimLightColor[1]; // rimColor.g
|
|
1498
|
+
materialUniformDataHair[6] = this.rimLightColor[2]; // rimColor.b
|
|
1499
|
+
materialUniformDataHair[7] = 0.0;
|
|
1500
|
+
// Create uniform buffers for both modes
|
|
1489
1501
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1490
1502
|
label: `material uniform (over eyes): ${mat.name}`,
|
|
1491
|
-
size:
|
|
1503
|
+
size: materialUniformDataHair.byteLength,
|
|
1492
1504
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1493
1505
|
});
|
|
1506
|
+
const materialUniformDataOverEyes = new Float32Array(materialUniformDataHair);
|
|
1507
|
+
materialUniformDataOverEyes[7] = 1.0;
|
|
1494
1508
|
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes);
|
|
1509
|
+
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1510
|
+
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1511
|
+
size: materialUniformDataHair.byteLength,
|
|
1512
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1513
|
+
});
|
|
1514
|
+
const materialUniformDataOverNonEyes = new Float32Array(materialUniformDataHair);
|
|
1515
|
+
materialUniformDataOverNonEyes[7] = 0.0;
|
|
1516
|
+
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1517
|
+
// Create bind groups for both modes
|
|
1495
1518
|
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1496
1519
|
label: `material bind group (over eyes): ${mat.name}`,
|
|
1497
1520
|
layout: this.hairBindGroupLayout,
|
|
@@ -1506,28 +1529,6 @@ export class Engine {
|
|
|
1506
1529
|
{ binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
|
|
1507
1530
|
],
|
|
1508
1531
|
});
|
|
1509
|
-
this.hairDrawsOverEyes.push({
|
|
1510
|
-
count: matCount,
|
|
1511
|
-
firstIndex: runningFirstIndex,
|
|
1512
|
-
bindGroup: bindGroupOverEyes,
|
|
1513
|
-
isTransparent,
|
|
1514
|
-
});
|
|
1515
|
-
// Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
|
|
1516
|
-
const materialUniformDataOverNonEyes = new Float32Array(8);
|
|
1517
|
-
materialUniformDataOverNonEyes[0] = materialAlpha;
|
|
1518
|
-
materialUniformDataOverNonEyes[1] = 1.0; // alphaMultiplier: 1.0 for over-non-eyes
|
|
1519
|
-
materialUniformDataOverNonEyes[2] = this.rimLightIntensity;
|
|
1520
|
-
materialUniformDataOverNonEyes[3] = this.rimLightPower;
|
|
1521
|
-
materialUniformDataOverNonEyes[4] = this.rimLightColor[0]; // rimColor.r
|
|
1522
|
-
materialUniformDataOverNonEyes[5] = this.rimLightColor[1]; // rimColor.g
|
|
1523
|
-
materialUniformDataOverNonEyes[6] = this.rimLightColor[2]; // rimColor.b
|
|
1524
|
-
materialUniformDataOverNonEyes[7] = 0.0; // _padding1
|
|
1525
|
-
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1526
|
-
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1527
|
-
size: materialUniformDataOverNonEyes.byteLength,
|
|
1528
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1529
|
-
});
|
|
1530
|
-
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1531
1532
|
const bindGroupOverNonEyes = this.device.createBindGroup({
|
|
1532
1533
|
label: `material bind group (over non-eyes): ${mat.name}`,
|
|
1533
1534
|
layout: this.hairBindGroupLayout,
|
|
@@ -1542,6 +1543,13 @@ export class Engine {
|
|
|
1542
1543
|
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1543
1544
|
],
|
|
1544
1545
|
});
|
|
1546
|
+
// Store both bind groups for unified pipeline
|
|
1547
|
+
this.hairDrawsOverEyes.push({
|
|
1548
|
+
count: matCount,
|
|
1549
|
+
firstIndex: runningFirstIndex,
|
|
1550
|
+
bindGroup: bindGroupOverEyes,
|
|
1551
|
+
isTransparent,
|
|
1552
|
+
});
|
|
1545
1553
|
this.hairDrawsOverNonEyes.push({
|
|
1546
1554
|
count: matCount,
|
|
1547
1555
|
firstIndex: runningFirstIndex,
|
|
@@ -1568,11 +1576,14 @@ export class Engine {
|
|
|
1568
1576
|
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1569
1577
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1570
1578
|
const materialUniformData = new Float32Array(8);
|
|
1571
|
-
materialUniformData[0] = mat.edgeColor[0];
|
|
1572
|
-
materialUniformData[1] = mat.edgeColor[1];
|
|
1573
|
-
materialUniformData[2] = mat.edgeColor[2];
|
|
1574
|
-
materialUniformData[3] = mat.edgeColor[3];
|
|
1579
|
+
materialUniformData[0] = mat.edgeColor[0]; // edgeColor.r
|
|
1580
|
+
materialUniformData[1] = mat.edgeColor[1]; // edgeColor.g
|
|
1581
|
+
materialUniformData[2] = mat.edgeColor[2]; // edgeColor.b
|
|
1582
|
+
materialUniformData[3] = mat.edgeColor[3]; // edgeColor.a
|
|
1575
1583
|
materialUniformData[4] = mat.edgeSize;
|
|
1584
|
+
materialUniformData[5] = 0.0; // isOverEyes: 0.0 for all (unified pipeline doesn't use stencil)
|
|
1585
|
+
materialUniformData[6] = 0.0; // _padding1
|
|
1586
|
+
materialUniformData[7] = 0.0; // _padding2
|
|
1576
1587
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1577
1588
|
label: `outline material uniform: ${mat.name}`,
|
|
1578
1589
|
size: materialUniformData.byteLength,
|
|
@@ -1686,8 +1697,7 @@ export class Engine {
|
|
|
1686
1697
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1687
1698
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1688
1699
|
this.drawCallCount = 0;
|
|
1689
|
-
// PASS 1: Opaque non-eye, non-hair
|
|
1690
|
-
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1700
|
+
// PASS 1: Opaque non-eye, non-hair
|
|
1691
1701
|
pass.setPipeline(this.pipeline);
|
|
1692
1702
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1693
1703
|
if (draw.count > 0) {
|
|
@@ -1706,27 +1716,29 @@ export class Engine {
|
|
|
1706
1716
|
this.drawCallCount++;
|
|
1707
1717
|
}
|
|
1708
1718
|
}
|
|
1709
|
-
// PASS 3: Hair rendering -
|
|
1710
|
-
|
|
1711
|
-
//
|
|
1712
|
-
this.
|
|
1713
|
-
|
|
1714
|
-
if (this.hairDrawsOverEyes.length > 0) {
|
|
1715
|
-
pass.setPipeline(this.hairMultiplyPipeline);
|
|
1716
|
-
pass.setStencilReference(1);
|
|
1719
|
+
// PASS 3: Hair rendering with depth pre-pass and unified pipeline
|
|
1720
|
+
this.drawOutlines(pass, false);
|
|
1721
|
+
// 3a: Hair depth pre-pass (eliminates overdraw by rejecting fragments early)
|
|
1722
|
+
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
1723
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
1717
1724
|
for (const draw of this.hairDrawsOverEyes) {
|
|
1718
1725
|
if (draw.count > 0) {
|
|
1719
1726
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1720
1727
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1721
|
-
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1731
|
+
if (draw.count > 0) {
|
|
1732
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
1733
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1722
1734
|
}
|
|
1723
1735
|
}
|
|
1724
1736
|
}
|
|
1725
|
-
// 3b: Hair
|
|
1726
|
-
if (this.
|
|
1727
|
-
pass.setPipeline(this.
|
|
1737
|
+
// 3b: Hair shading pass with unified pipeline and dynamic branching
|
|
1738
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
1739
|
+
pass.setPipeline(this.hairUnifiedPipelineOverEyes);
|
|
1728
1740
|
pass.setStencilReference(1);
|
|
1729
|
-
for (const draw of this.
|
|
1741
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1730
1742
|
if (draw.count > 0) {
|
|
1731
1743
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1732
1744
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1734,20 +1746,20 @@ export class Engine {
|
|
|
1734
1746
|
}
|
|
1735
1747
|
}
|
|
1736
1748
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
// Over eyes
|
|
1740
|
-
pass.setPipeline(this.hairOutlineOverEyesPipeline);
|
|
1749
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
1750
|
+
pass.setPipeline(this.hairUnifiedPipelineOverNonEyes);
|
|
1741
1751
|
pass.setStencilReference(1);
|
|
1742
|
-
for (const draw of this.
|
|
1752
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1743
1753
|
if (draw.count > 0) {
|
|
1744
1754
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1745
1755
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1756
|
+
this.drawCallCount++;
|
|
1746
1757
|
}
|
|
1747
1758
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1759
|
+
}
|
|
1760
|
+
// 3c: Hair outlines - unified single pass without stencil testing
|
|
1761
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
1762
|
+
pass.setPipeline(this.hairUnifiedOutlinePipeline);
|
|
1751
1763
|
for (const draw of this.hairOutlineDraws) {
|
|
1752
1764
|
if (draw.count > 0) {
|
|
1753
1765
|
pass.setBindGroup(0, draw.bindGroup);
|
|
@@ -1764,7 +1776,7 @@ export class Engine {
|
|
|
1764
1776
|
this.drawCallCount++;
|
|
1765
1777
|
}
|
|
1766
1778
|
}
|
|
1767
|
-
this.drawOutlines(pass, true);
|
|
1779
|
+
this.drawOutlines(pass, true);
|
|
1768
1780
|
pass.end();
|
|
1769
1781
|
this.device.queue.submit([encoder.finish()]);
|
|
1770
1782
|
// Apply bloom post-processing
|