reze-engine 0.1.15 → 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 +0 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +406 -723
- package/package.json +1 -1
- package/src/engine.ts +2330 -2659
package/dist/engine.js
CHANGED
|
@@ -81,131 +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
|
-
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
204
|
-
}
|
|
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
|
+
}
|
|
205
203
|
`,
|
|
206
204
|
});
|
|
207
205
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
208
|
-
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
209
206
|
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
210
207
|
label: "shared material bind group layout",
|
|
211
208
|
entries: [
|
|
@@ -293,79 +290,77 @@ export class Engine {
|
|
|
293
290
|
});
|
|
294
291
|
const outlineShaderModule = this.device.createShaderModule({
|
|
295
292
|
label: "outline shaders",
|
|
296
|
-
code: /* wgsl */ `
|
|
297
|
-
struct CameraUniforms {
|
|
298
|
-
view: mat4x4f,
|
|
299
|
-
projection: mat4x4f,
|
|
300
|
-
viewPos: vec3f,
|
|
301
|
-
_padding: f32,
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
struct MaterialUniforms {
|
|
305
|
-
edgeColor: vec4f,
|
|
306
|
-
edgeSize: f32,
|
|
307
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
308
|
-
_padding1: f32,
|
|
309
|
-
_padding2: f32,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
313
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
314
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
315
|
-
|
|
316
|
-
struct VertexOutput {
|
|
317
|
-
@builtin(position) position: vec4f,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
@vertex fn vs(
|
|
321
|
-
@location(0) position: vec3f,
|
|
322
|
-
@location(1) normal: vec3f,
|
|
323
|
-
@location(3) joints0: vec4<u32>,
|
|
324
|
-
@location(4) weights0: vec4<f32>
|
|
325
|
-
) -> VertexOutput {
|
|
326
|
-
var output: VertexOutput;
|
|
327
|
-
let pos4 = vec4f(position, 1.0);
|
|
328
|
-
|
|
329
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
330
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
331
|
-
var normalizedWeights: vec4f;
|
|
332
|
-
if (weightSum > 0.0001) {
|
|
333
|
-
normalizedWeights = weights0 / weightSum;
|
|
334
|
-
} else {
|
|
335
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
339
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
340
|
-
for (var i = 0u; i < 4u; i++) {
|
|
341
|
-
let j = joints0[i];
|
|
342
|
-
let w = normalizedWeights[i];
|
|
343
|
-
let m = skinMats[j];
|
|
344
|
-
skinnedPos += (m * pos4) * w;
|
|
345
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
346
|
-
skinnedNrm += (r3 * normal) * w;
|
|
347
|
-
}
|
|
348
|
-
let worldPos = skinnedPos.xyz;
|
|
349
|
-
let worldNormal = normalize(skinnedNrm);
|
|
350
|
-
|
|
351
|
-
// MMD invert hull: expand vertices outward along normals
|
|
352
|
-
let scaleFactor = 0.01;
|
|
353
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
354
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
355
|
-
return output;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
359
|
-
var color = material.edgeColor;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return color;
|
|
368
|
-
}
|
|
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
|
+
}
|
|
369
364
|
`,
|
|
370
365
|
});
|
|
371
366
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -431,165 +426,7 @@ export class Engine {
|
|
|
431
426
|
count: this.sampleCount,
|
|
432
427
|
},
|
|
433
428
|
});
|
|
434
|
-
//
|
|
435
|
-
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
436
|
-
label: "hair outline pipeline",
|
|
437
|
-
layout: outlinePipelineLayout,
|
|
438
|
-
vertex: {
|
|
439
|
-
module: outlineShaderModule,
|
|
440
|
-
buffers: [
|
|
441
|
-
{
|
|
442
|
-
arrayStride: 8 * 4,
|
|
443
|
-
attributes: [
|
|
444
|
-
{
|
|
445
|
-
shaderLocation: 0,
|
|
446
|
-
offset: 0,
|
|
447
|
-
format: "float32x3",
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
shaderLocation: 1,
|
|
451
|
-
offset: 3 * 4,
|
|
452
|
-
format: "float32x3",
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
arrayStride: 4 * 2,
|
|
458
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
arrayStride: 4,
|
|
462
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
},
|
|
466
|
-
fragment: {
|
|
467
|
-
module: outlineShaderModule,
|
|
468
|
-
targets: [
|
|
469
|
-
{
|
|
470
|
-
format: this.presentationFormat,
|
|
471
|
-
blend: {
|
|
472
|
-
color: {
|
|
473
|
-
srcFactor: "src-alpha",
|
|
474
|
-
dstFactor: "one-minus-src-alpha",
|
|
475
|
-
operation: "add",
|
|
476
|
-
},
|
|
477
|
-
alpha: {
|
|
478
|
-
srcFactor: "one",
|
|
479
|
-
dstFactor: "one-minus-src-alpha",
|
|
480
|
-
operation: "add",
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
primitive: {
|
|
487
|
-
cullMode: "back",
|
|
488
|
-
},
|
|
489
|
-
depthStencil: {
|
|
490
|
-
format: "depth24plus-stencil8",
|
|
491
|
-
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
492
|
-
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
493
|
-
stencilFront: {
|
|
494
|
-
compare: "not-equal", // Only render where stencil != 1 (not over eyes)
|
|
495
|
-
failOp: "keep",
|
|
496
|
-
depthFailOp: "keep",
|
|
497
|
-
passOp: "keep",
|
|
498
|
-
},
|
|
499
|
-
stencilBack: {
|
|
500
|
-
compare: "not-equal",
|
|
501
|
-
failOp: "keep",
|
|
502
|
-
depthFailOp: "keep",
|
|
503
|
-
passOp: "keep",
|
|
504
|
-
},
|
|
505
|
-
},
|
|
506
|
-
multisample: {
|
|
507
|
-
count: this.sampleCount,
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists - Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
511
|
-
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
512
|
-
label: "hair outline over eyes pipeline",
|
|
513
|
-
layout: outlinePipelineLayout,
|
|
514
|
-
vertex: {
|
|
515
|
-
module: outlineShaderModule,
|
|
516
|
-
buffers: [
|
|
517
|
-
{
|
|
518
|
-
arrayStride: 8 * 4,
|
|
519
|
-
attributes: [
|
|
520
|
-
{
|
|
521
|
-
shaderLocation: 0,
|
|
522
|
-
offset: 0,
|
|
523
|
-
format: "float32x3",
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
shaderLocation: 1,
|
|
527
|
-
offset: 3 * 4,
|
|
528
|
-
format: "float32x3",
|
|
529
|
-
},
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
arrayStride: 4 * 2,
|
|
534
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
arrayStride: 4,
|
|
538
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
539
|
-
},
|
|
540
|
-
],
|
|
541
|
-
},
|
|
542
|
-
fragment: {
|
|
543
|
-
module: outlineShaderModule,
|
|
544
|
-
targets: [
|
|
545
|
-
{
|
|
546
|
-
format: this.presentationFormat,
|
|
547
|
-
blend: {
|
|
548
|
-
color: {
|
|
549
|
-
srcFactor: "src-alpha",
|
|
550
|
-
dstFactor: "one-minus-src-alpha",
|
|
551
|
-
operation: "add",
|
|
552
|
-
},
|
|
553
|
-
alpha: {
|
|
554
|
-
srcFactor: "one",
|
|
555
|
-
dstFactor: "one-minus-src-alpha",
|
|
556
|
-
operation: "add",
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
],
|
|
561
|
-
},
|
|
562
|
-
primitive: {
|
|
563
|
-
cullMode: "back",
|
|
564
|
-
},
|
|
565
|
-
depthStencil: {
|
|
566
|
-
format: "depth24plus-stencil8",
|
|
567
|
-
depthWriteEnabled: false, // Don't write depth
|
|
568
|
-
depthCompare: "less-equal", // Draw where outline depth <= existing depth (hair depth)
|
|
569
|
-
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
570
|
-
depthBiasSlopeScale: 0.0,
|
|
571
|
-
depthBiasClamp: 0.0,
|
|
572
|
-
stencilFront: {
|
|
573
|
-
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
574
|
-
failOp: "keep",
|
|
575
|
-
depthFailOp: "keep",
|
|
576
|
-
passOp: "keep",
|
|
577
|
-
},
|
|
578
|
-
stencilBack: {
|
|
579
|
-
compare: "equal",
|
|
580
|
-
failOp: "keep",
|
|
581
|
-
depthFailOp: "keep",
|
|
582
|
-
passOp: "keep",
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
multisample: {
|
|
586
|
-
count: this.sampleCount,
|
|
587
|
-
},
|
|
588
|
-
});
|
|
589
|
-
// Unified hair outline pipeline: single pass without stencil testing
|
|
590
|
-
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
591
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
592
|
-
// This eliminates the need for two separate outline passes
|
|
429
|
+
// Unified hair outline pipeline: single pass without stencil testing, uses depth test "less-equal" to draw everywhere hair exists
|
|
593
430
|
this.hairUnifiedOutlinePipeline = this.device.createRenderPipeline({
|
|
594
431
|
label: "unified hair outline pipeline",
|
|
595
432
|
layout: outlinePipelineLayout,
|
|
@@ -656,137 +493,6 @@ export class Engine {
|
|
|
656
493
|
count: this.sampleCount,
|
|
657
494
|
},
|
|
658
495
|
});
|
|
659
|
-
// Unified hair pipeline - can be used for both over-eyes and over-non-eyes
|
|
660
|
-
// The difference is controlled by stencil state and alpha multiplier in material uniform
|
|
661
|
-
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
662
|
-
label: "hair pipeline (over eyes)",
|
|
663
|
-
layout: sharedPipelineLayout,
|
|
664
|
-
vertex: {
|
|
665
|
-
module: shaderModule,
|
|
666
|
-
buffers: [
|
|
667
|
-
{
|
|
668
|
-
arrayStride: 8 * 4,
|
|
669
|
-
attributes: [
|
|
670
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
671
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
672
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
673
|
-
],
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
arrayStride: 4 * 2,
|
|
677
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
arrayStride: 4,
|
|
681
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
682
|
-
},
|
|
683
|
-
],
|
|
684
|
-
},
|
|
685
|
-
fragment: {
|
|
686
|
-
module: shaderModule,
|
|
687
|
-
targets: [
|
|
688
|
-
{
|
|
689
|
-
format: this.presentationFormat,
|
|
690
|
-
blend: {
|
|
691
|
-
color: {
|
|
692
|
-
srcFactor: "src-alpha",
|
|
693
|
-
dstFactor: "one-minus-src-alpha",
|
|
694
|
-
operation: "add",
|
|
695
|
-
},
|
|
696
|
-
alpha: {
|
|
697
|
-
srcFactor: "one",
|
|
698
|
-
dstFactor: "one-minus-src-alpha",
|
|
699
|
-
operation: "add",
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
},
|
|
703
|
-
],
|
|
704
|
-
},
|
|
705
|
-
primitive: { cullMode: "none" },
|
|
706
|
-
depthStencil: {
|
|
707
|
-
format: "depth24plus-stencil8",
|
|
708
|
-
depthWriteEnabled: true, // Write depth so outlines can test against it
|
|
709
|
-
depthCompare: "less",
|
|
710
|
-
stencilFront: {
|
|
711
|
-
compare: "equal", // Only render where stencil == 1
|
|
712
|
-
failOp: "keep",
|
|
713
|
-
depthFailOp: "keep",
|
|
714
|
-
passOp: "keep",
|
|
715
|
-
},
|
|
716
|
-
stencilBack: {
|
|
717
|
-
compare: "equal",
|
|
718
|
-
failOp: "keep",
|
|
719
|
-
depthFailOp: "keep",
|
|
720
|
-
passOp: "keep",
|
|
721
|
-
},
|
|
722
|
-
},
|
|
723
|
-
multisample: { count: this.sampleCount },
|
|
724
|
-
});
|
|
725
|
-
// Hair pipeline for opaque rendering (hair over non-eyes) - uses same shader, different stencil state
|
|
726
|
-
this.hairOpaquePipeline = this.device.createRenderPipeline({
|
|
727
|
-
label: "hair pipeline (over non-eyes)",
|
|
728
|
-
layout: sharedPipelineLayout,
|
|
729
|
-
vertex: {
|
|
730
|
-
module: shaderModule,
|
|
731
|
-
buffers: [
|
|
732
|
-
{
|
|
733
|
-
arrayStride: 8 * 4,
|
|
734
|
-
attributes: [
|
|
735
|
-
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
736
|
-
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
737
|
-
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
738
|
-
],
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
arrayStride: 4 * 2,
|
|
742
|
-
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
arrayStride: 4,
|
|
746
|
-
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
747
|
-
},
|
|
748
|
-
],
|
|
749
|
-
},
|
|
750
|
-
fragment: {
|
|
751
|
-
module: shaderModule,
|
|
752
|
-
targets: [
|
|
753
|
-
{
|
|
754
|
-
format: this.presentationFormat,
|
|
755
|
-
blend: {
|
|
756
|
-
color: {
|
|
757
|
-
srcFactor: "src-alpha",
|
|
758
|
-
dstFactor: "one-minus-src-alpha",
|
|
759
|
-
operation: "add",
|
|
760
|
-
},
|
|
761
|
-
alpha: {
|
|
762
|
-
srcFactor: "one",
|
|
763
|
-
dstFactor: "one-minus-src-alpha",
|
|
764
|
-
operation: "add",
|
|
765
|
-
},
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
|
-
],
|
|
769
|
-
},
|
|
770
|
-
primitive: { cullMode: "none" },
|
|
771
|
-
depthStencil: {
|
|
772
|
-
format: "depth24plus-stencil8",
|
|
773
|
-
depthWriteEnabled: true,
|
|
774
|
-
depthCompare: "less",
|
|
775
|
-
stencilFront: {
|
|
776
|
-
compare: "not-equal", // Only render where stencil != 1
|
|
777
|
-
failOp: "keep",
|
|
778
|
-
depthFailOp: "keep",
|
|
779
|
-
passOp: "keep",
|
|
780
|
-
},
|
|
781
|
-
stencilBack: {
|
|
782
|
-
compare: "not-equal",
|
|
783
|
-
failOp: "keep",
|
|
784
|
-
depthFailOp: "keep",
|
|
785
|
-
passOp: "keep",
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
multisample: { count: this.sampleCount },
|
|
789
|
-
});
|
|
790
496
|
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
791
497
|
this.eyePipeline = this.device.createRenderPipeline({
|
|
792
498
|
label: "eye overlay pipeline",
|
|
@@ -855,57 +561,52 @@ export class Engine {
|
|
|
855
561
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
856
562
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
857
563
|
label: "depth only shader",
|
|
858
|
-
code: /* wgsl */ `
|
|
859
|
-
struct CameraUniforms {
|
|
860
|
-
view: mat4x4f,
|
|
861
|
-
projection: mat4x4f,
|
|
862
|
-
viewPos: vec3f,
|
|
863
|
-
_padding: f32,
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
867
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
868
|
-
|
|
869
|
-
@vertex fn vs(
|
|
870
|
-
@location(0) position: vec3f,
|
|
871
|
-
@location(1) normal: vec3f,
|
|
872
|
-
@location(3) joints0: vec4<u32>,
|
|
873
|
-
@location(4) weights0: vec4<f32>
|
|
874
|
-
) -> @builtin(position) vec4f {
|
|
875
|
-
let pos4 = vec4f(position, 1.0);
|
|
876
|
-
|
|
877
|
-
// Normalize weights
|
|
878
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
879
|
-
var normalizedWeights: vec4f;
|
|
880
|
-
if (weightSum > 0.0001) {
|
|
881
|
-
normalizedWeights = weights0 / weightSum;
|
|
882
|
-
} else {
|
|
883
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
887
|
-
for (var i = 0u; i < 4u; i++) {
|
|
888
|
-
let j = joints0[i];
|
|
889
|
-
let w = normalizedWeights[i];
|
|
890
|
-
let m = skinMats[j];
|
|
891
|
-
skinnedPos += (m * pos4) * w;
|
|
892
|
-
}
|
|
893
|
-
let worldPos = skinnedPos.xyz;
|
|
894
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
895
|
-
return clipPos;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
902
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
903
|
-
}
|
|
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
|
+
}
|
|
904
607
|
`,
|
|
905
608
|
});
|
|
906
|
-
// Hair depth pre-pass pipeline
|
|
907
|
-
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
908
|
-
// Note: Must have a color target to match render pass, but we disable all color writes
|
|
609
|
+
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
909
610
|
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
910
611
|
label: "hair depth pre-pass",
|
|
911
612
|
layout: sharedPipelineLayout,
|
|
@@ -947,11 +648,7 @@ export class Engine {
|
|
|
947
648
|
},
|
|
948
649
|
multisample: { count: this.sampleCount },
|
|
949
650
|
});
|
|
950
|
-
// Unified hair pipeline: single pass with dynamic branching
|
|
951
|
-
// Uses stencil testing to filter fragments, then shader branches on isOverEyes uniform
|
|
952
|
-
// This eliminates the need for separate pipelines - same shader, different stencil states
|
|
953
|
-
// We create two variants: one for over-eyes (stencil == 1) and one for over-non-eyes (stencil != 1)
|
|
954
|
-
// Unified pipeline for hair over eyes (stencil == 1)
|
|
651
|
+
// Unified hair pipeline for over-eyes (stencil == 1): single pass with dynamic branching
|
|
955
652
|
this.hairUnifiedPipelineOverEyes = this.device.createRenderPipeline({
|
|
956
653
|
label: "unified hair pipeline (over eyes)",
|
|
957
654
|
layout: sharedPipelineLayout,
|
|
@@ -1086,31 +783,31 @@ export class Engine {
|
|
|
1086
783
|
createSkinMatrixComputePipeline() {
|
|
1087
784
|
const computeShader = this.device.createShaderModule({
|
|
1088
785
|
label: "skin matrix compute",
|
|
1089
|
-
code: /* wgsl */ `
|
|
1090
|
-
struct BoneCountUniform {
|
|
1091
|
-
count: u32,
|
|
1092
|
-
_padding1: u32,
|
|
1093
|
-
_padding2: u32,
|
|
1094
|
-
_padding3: u32,
|
|
1095
|
-
_padding4: vec4<u32>,
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
1099
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
1100
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
1101
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
1102
|
-
|
|
1103
|
-
@compute @workgroup_size(64)
|
|
1104
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
1105
|
-
let boneIndex = globalId.x;
|
|
1106
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
1107
|
-
if (boneIndex >= boneCount.count) {
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
let worldMat = worldMatrices[boneIndex];
|
|
1111
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
1112
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
1113
|
-
}
|
|
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
|
+
}
|
|
1114
811
|
`,
|
|
1115
812
|
});
|
|
1116
813
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -1164,143 +861,143 @@ export class Engine {
|
|
|
1164
861
|
// Bloom extraction shader (extracts bright areas)
|
|
1165
862
|
const bloomExtractShader = this.device.createShaderModule({
|
|
1166
863
|
label: "bloom extract",
|
|
1167
|
-
code: /* wgsl */ `
|
|
1168
|
-
struct VertexOutput {
|
|
1169
|
-
@builtin(position) position: vec4f,
|
|
1170
|
-
@location(0) uv: vec2f,
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1174
|
-
var output: VertexOutput;
|
|
1175
|
-
// Generate fullscreen quad from vertex index
|
|
1176
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1177
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1178
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1179
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1180
|
-
return output;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
struct BloomExtractUniforms {
|
|
1184
|
-
threshold: f32,
|
|
1185
|
-
_padding1: f32,
|
|
1186
|
-
_padding2: f32,
|
|
1187
|
-
_padding3: f32,
|
|
1188
|
-
_padding4: f32,
|
|
1189
|
-
_padding5: f32,
|
|
1190
|
-
_padding6: f32,
|
|
1191
|
-
_padding7: f32,
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1195
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
1196
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
1197
|
-
|
|
1198
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1199
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
1200
|
-
// Extract bright areas above threshold
|
|
1201
|
-
let threshold = extractUniforms.threshold;
|
|
1202
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
1203
|
-
return vec4f(bloom, color.a);
|
|
1204
|
-
}
|
|
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
|
+
}
|
|
1205
902
|
`,
|
|
1206
903
|
});
|
|
1207
904
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
1208
905
|
const bloomBlurShader = this.device.createShaderModule({
|
|
1209
906
|
label: "bloom blur",
|
|
1210
|
-
code: /* wgsl */ `
|
|
1211
|
-
struct VertexOutput {
|
|
1212
|
-
@builtin(position) position: vec4f,
|
|
1213
|
-
@location(0) uv: vec2f,
|
|
1214
|
-
};
|
|
1215
|
-
|
|
1216
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1217
|
-
var output: VertexOutput;
|
|
1218
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1219
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1220
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1221
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1222
|
-
return output;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
struct BlurUniforms {
|
|
1226
|
-
direction: vec2f,
|
|
1227
|
-
_padding1: f32,
|
|
1228
|
-
_padding2: f32,
|
|
1229
|
-
_padding3: f32,
|
|
1230
|
-
_padding4: f32,
|
|
1231
|
-
_padding5: f32,
|
|
1232
|
-
_padding6: f32,
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1236
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
1237
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
1238
|
-
|
|
1239
|
-
// 9-tap gaussian blur
|
|
1240
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1241
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1242
|
-
var result = vec4f(0.0);
|
|
1243
|
-
|
|
1244
|
-
// Gaussian weights for 9-tap filter
|
|
1245
|
-
let weights = array<f32, 9>(
|
|
1246
|
-
0.01621622, 0.05405405, 0.12162162,
|
|
1247
|
-
0.19459459, 0.22702703,
|
|
1248
|
-
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
1249
|
-
);
|
|
1250
|
-
|
|
1251
|
-
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
1252
|
-
|
|
1253
|
-
for (var i = 0u; i < 9u; i++) {
|
|
1254
|
-
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
1255
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
return result;
|
|
1259
|
-
}
|
|
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
|
+
}
|
|
1260
957
|
`,
|
|
1261
958
|
});
|
|
1262
959
|
// Bloom composition shader (combines original scene with bloom)
|
|
1263
960
|
const bloomComposeShader = this.device.createShaderModule({
|
|
1264
961
|
label: "bloom compose",
|
|
1265
|
-
code: /* wgsl */ `
|
|
1266
|
-
struct VertexOutput {
|
|
1267
|
-
@builtin(position) position: vec4f,
|
|
1268
|
-
@location(0) uv: vec2f,
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1272
|
-
var output: VertexOutput;
|
|
1273
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1274
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1275
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1276
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1277
|
-
return output;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
struct BloomComposeUniforms {
|
|
1281
|
-
intensity: f32,
|
|
1282
|
-
_padding1: f32,
|
|
1283
|
-
_padding2: f32,
|
|
1284
|
-
_padding3: f32,
|
|
1285
|
-
_padding4: f32,
|
|
1286
|
-
_padding5: f32,
|
|
1287
|
-
_padding6: f32,
|
|
1288
|
-
_padding7: f32,
|
|
1289
|
-
};
|
|
1290
|
-
|
|
1291
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1292
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1293
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1294
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1295
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1296
|
-
|
|
1297
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1298
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1299
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1300
|
-
// Additive blending with intensity control
|
|
1301
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1302
|
-
return vec4f(result, scene.a);
|
|
1303
|
-
}
|
|
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
|
+
}
|
|
1304
1001
|
`,
|
|
1305
1002
|
});
|
|
1306
1003
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1508,9 +1205,9 @@ export class Engine {
|
|
|
1508
1205
|
depthClearValue: 1.0,
|
|
1509
1206
|
depthLoadOp: "clear",
|
|
1510
1207
|
depthStoreOp: "store",
|
|
1511
|
-
stencilClearValue: 0,
|
|
1512
|
-
stencilLoadOp: "clear",
|
|
1513
|
-
stencilStoreOp: "
|
|
1208
|
+
stencilClearValue: 0,
|
|
1209
|
+
stencilLoadOp: "clear",
|
|
1210
|
+
stencilStoreOp: "discard", // Discard stencil after frame to save bandwidth (we only use it during rendering)
|
|
1514
1211
|
},
|
|
1515
1212
|
};
|
|
1516
1213
|
this.camera.aspect = width / height;
|
|
@@ -1758,7 +1455,7 @@ export class Engine {
|
|
|
1758
1455
|
materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
|
|
1759
1456
|
materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
|
|
1760
1457
|
materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
|
|
1761
|
-
materialUniformData[7] = 0.0;
|
|
1458
|
+
materialUniformData[7] = 0.0;
|
|
1762
1459
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1763
1460
|
label: `material uniform: ${mat.name}`,
|
|
1764
1461
|
size: materialUniformData.byteLength,
|
|
@@ -1790,9 +1487,7 @@ export class Engine {
|
|
|
1790
1487
|
});
|
|
1791
1488
|
}
|
|
1792
1489
|
else if (mat.isHair) {
|
|
1793
|
-
//
|
|
1794
|
-
// The shader will dynamically branch based on isOverEyes uniform
|
|
1795
|
-
// We still need two uniform buffers (one for each render mode) but can reuse the same bind group structure
|
|
1490
|
+
// Hair materials: create bind groups for unified pipeline with dynamic branching
|
|
1796
1491
|
const materialUniformDataHair = new Float32Array(8);
|
|
1797
1492
|
materialUniformDataHair[0] = materialAlpha;
|
|
1798
1493
|
materialUniformDataHair[1] = 1.0; // alphaMultiplier: base value, shader will adjust
|
|
@@ -1801,15 +1496,15 @@ export class Engine {
|
|
|
1801
1496
|
materialUniformDataHair[4] = this.rimLightColor[0]; // rimColor.r
|
|
1802
1497
|
materialUniformDataHair[5] = this.rimLightColor[1]; // rimColor.g
|
|
1803
1498
|
materialUniformDataHair[6] = this.rimLightColor[2]; // rimColor.b
|
|
1804
|
-
materialUniformDataHair[7] = 0.0;
|
|
1805
|
-
// Create uniform buffers for both modes
|
|
1499
|
+
materialUniformDataHair[7] = 0.0;
|
|
1500
|
+
// Create uniform buffers for both modes
|
|
1806
1501
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1807
1502
|
label: `material uniform (over eyes): ${mat.name}`,
|
|
1808
1503
|
size: materialUniformDataHair.byteLength,
|
|
1809
1504
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1810
1505
|
});
|
|
1811
1506
|
const materialUniformDataOverEyes = new Float32Array(materialUniformDataHair);
|
|
1812
|
-
materialUniformDataOverEyes[7] = 1.0;
|
|
1507
|
+
materialUniformDataOverEyes[7] = 1.0;
|
|
1813
1508
|
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes);
|
|
1814
1509
|
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1815
1510
|
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
@@ -1817,9 +1512,9 @@ export class Engine {
|
|
|
1817
1512
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1818
1513
|
});
|
|
1819
1514
|
const materialUniformDataOverNonEyes = new Float32Array(materialUniformDataHair);
|
|
1820
|
-
materialUniformDataOverNonEyes[7] = 0.0;
|
|
1515
|
+
materialUniformDataOverNonEyes[7] = 0.0;
|
|
1821
1516
|
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1822
|
-
// Create bind groups for both modes
|
|
1517
|
+
// Create bind groups for both modes
|
|
1823
1518
|
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1824
1519
|
label: `material bind group (over eyes): ${mat.name}`,
|
|
1825
1520
|
layout: this.hairBindGroupLayout,
|
|
@@ -1848,7 +1543,7 @@ export class Engine {
|
|
|
1848
1543
|
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1849
1544
|
],
|
|
1850
1545
|
});
|
|
1851
|
-
// Store both bind groups
|
|
1546
|
+
// Store both bind groups for unified pipeline
|
|
1852
1547
|
this.hairDrawsOverEyes.push({
|
|
1853
1548
|
count: matCount,
|
|
1854
1549
|
firstIndex: runningFirstIndex,
|
|
@@ -1886,7 +1581,7 @@ export class Engine {
|
|
|
1886
1581
|
materialUniformData[2] = mat.edgeColor[2]; // edgeColor.b
|
|
1887
1582
|
materialUniformData[3] = mat.edgeColor[3]; // edgeColor.a
|
|
1888
1583
|
materialUniformData[4] = mat.edgeSize;
|
|
1889
|
-
materialUniformData[5] =
|
|
1584
|
+
materialUniformData[5] = 0.0; // isOverEyes: 0.0 for all (unified pipeline doesn't use stencil)
|
|
1890
1585
|
materialUniformData[6] = 0.0; // _padding1
|
|
1891
1586
|
materialUniformData[7] = 0.0; // _padding2
|
|
1892
1587
|
const materialUniformBuffer = this.device.createBuffer({
|
|
@@ -2002,8 +1697,7 @@ export class Engine {
|
|
|
2002
1697
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
2003
1698
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
2004
1699
|
this.drawCallCount = 0;
|
|
2005
|
-
// PASS 1: Opaque non-eye, non-hair
|
|
2006
|
-
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1700
|
+
// PASS 1: Opaque non-eye, non-hair
|
|
2007
1701
|
pass.setPipeline(this.pipeline);
|
|
2008
1702
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
2009
1703
|
if (draw.count > 0) {
|
|
@@ -2022,18 +1716,13 @@ export class Engine {
|
|
|
2022
1716
|
this.drawCallCount++;
|
|
2023
1717
|
}
|
|
2024
1718
|
}
|
|
2025
|
-
// PASS 3: Hair rendering
|
|
2026
|
-
|
|
2027
|
-
//
|
|
2028
|
-
this.drawOutlines(pass, false); // Opaque outlines
|
|
2029
|
-
// 3a: Hair depth pre-pass (depth-only, no color writes)
|
|
2030
|
-
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
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)
|
|
2031
1722
|
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
2032
1723
|
pass.setPipeline(this.hairDepthPipeline);
|
|
2033
|
-
// Render all hair materials for depth (no stencil test needed for depth pass)
|
|
2034
1724
|
for (const draw of this.hairDrawsOverEyes) {
|
|
2035
1725
|
if (draw.count > 0) {
|
|
2036
|
-
// Use the same bind group structure (camera, skin matrices) for depth pass
|
|
2037
1726
|
pass.setBindGroup(0, draw.bindGroup);
|
|
2038
1727
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2039
1728
|
}
|
|
@@ -2045,10 +1734,7 @@ export class Engine {
|
|
|
2045
1734
|
}
|
|
2046
1735
|
}
|
|
2047
1736
|
}
|
|
2048
|
-
// 3b: Hair shading pass
|
|
2049
|
-
// Uses depth test "equal" to only render where depth was written in pre-pass
|
|
2050
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
2051
|
-
// This eliminates one full geometry pass compared to the old approach
|
|
1737
|
+
// 3b: Hair shading pass with unified pipeline and dynamic branching
|
|
2052
1738
|
if (this.hairDrawsOverEyes.length > 0) {
|
|
2053
1739
|
pass.setPipeline(this.hairUnifiedPipelineOverEyes);
|
|
2054
1740
|
pass.setStencilReference(1);
|
|
@@ -2072,9 +1758,6 @@ export class Engine {
|
|
|
2072
1758
|
}
|
|
2073
1759
|
}
|
|
2074
1760
|
// 3c: Hair outlines - unified single pass without stencil testing
|
|
2075
|
-
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
2076
|
-
// Shader branches on isOverEyes uniform to adjust alpha dynamically (currently always 0.0)
|
|
2077
|
-
// This eliminates the need for two separate outline passes
|
|
2078
1761
|
if (this.hairOutlineDraws.length > 0) {
|
|
2079
1762
|
pass.setPipeline(this.hairUnifiedOutlinePipeline);
|
|
2080
1763
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -2093,7 +1776,7 @@ export class Engine {
|
|
|
2093
1776
|
this.drawCallCount++;
|
|
2094
1777
|
}
|
|
2095
1778
|
}
|
|
2096
|
-
this.drawOutlines(pass, true);
|
|
1779
|
+
this.drawOutlines(pass, true);
|
|
2097
1780
|
pass.end();
|
|
2098
1781
|
this.device.queue.submit([encoder.finish()]);
|
|
2099
1782
|
// Apply bloom post-processing
|