reze-engine 0.1.7 → 0.1.8
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 +18 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +750 -331
- package/package.json +1 -1
- package/src/engine.ts +2300 -1829
package/dist/engine.js
CHANGED
|
@@ -10,6 +10,9 @@ export class Engine {
|
|
|
10
10
|
this.vertexCount = 0;
|
|
11
11
|
this.resizeObserver = null;
|
|
12
12
|
this.sampleCount = 4; // MSAA 4x
|
|
13
|
+
// Bloom settings
|
|
14
|
+
this.bloomThreshold = 0.3;
|
|
15
|
+
this.bloomIntensity = 0.14;
|
|
13
16
|
this.currentModel = null;
|
|
14
17
|
this.modelDir = "";
|
|
15
18
|
this.physics = null;
|
|
@@ -60,6 +63,8 @@ export class Engine {
|
|
|
60
63
|
this.setupCamera();
|
|
61
64
|
this.setupLighting();
|
|
62
65
|
this.createPipelines();
|
|
66
|
+
this.createFullscreenQuad();
|
|
67
|
+
this.createBloomPipelines();
|
|
63
68
|
this.setupResize();
|
|
64
69
|
}
|
|
65
70
|
// Step 2: Create shaders and render pipelines
|
|
@@ -72,226 +77,225 @@ export class Engine {
|
|
|
72
77
|
});
|
|
73
78
|
const shaderModule = this.device.createShaderModule({
|
|
74
79
|
label: "model shaders",
|
|
75
|
-
code: /* wgsl */ `
|
|
76
|
-
struct CameraUniforms {
|
|
77
|
-
view: mat4x4f,
|
|
78
|
-
projection: mat4x4f,
|
|
79
|
-
viewPos: vec3f,
|
|
80
|
-
_padding: f32,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
struct Light {
|
|
84
|
-
direction: vec3f,
|
|
85
|
-
_padding1: f32,
|
|
86
|
-
color: vec3f,
|
|
87
|
-
intensity: f32,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
struct LightUniforms {
|
|
91
|
-
ambient: f32,
|
|
92
|
-
lightCount: f32,
|
|
93
|
-
_padding1: f32,
|
|
94
|
-
_padding2: f32,
|
|
95
|
-
lights: array<Light, 4>,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
struct MaterialUniforms {
|
|
99
|
-
alpha: f32,
|
|
100
|
-
_padding1: f32,
|
|
101
|
-
_padding2: f32,
|
|
102
|
-
_padding3: f32,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
struct VertexOutput {
|
|
106
|
-
@builtin(position) position: vec4f,
|
|
107
|
-
@location(0) normal: vec3f,
|
|
108
|
-
@location(1) uv: vec2f,
|
|
109
|
-
@location(2) worldPos: vec3f,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
113
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
114
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
115
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
116
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
117
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
118
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
119
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
120
|
-
|
|
121
|
-
@vertex fn vs(
|
|
122
|
-
@location(0) position: vec3f,
|
|
123
|
-
@location(1) normal: vec3f,
|
|
124
|
-
@location(2) uv: vec2f,
|
|
125
|
-
@location(3) joints0: vec4<u32>,
|
|
126
|
-
@location(4) weights0: vec4<f32>
|
|
127
|
-
) -> VertexOutput {
|
|
128
|
-
var output: VertexOutput;
|
|
129
|
-
let pos4 = vec4f(position, 1.0);
|
|
130
|
-
|
|
131
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
132
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
133
|
-
var normalizedWeights: vec4f;
|
|
134
|
-
if (weightSum > 0.0001) {
|
|
135
|
-
normalizedWeights = weights0 / weightSum;
|
|
136
|
-
} else {
|
|
137
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
141
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
142
|
-
for (var i = 0u; i < 4u; i++) {
|
|
143
|
-
let j = joints0[i];
|
|
144
|
-
let w = normalizedWeights[i];
|
|
145
|
-
let m = skinMats[j];
|
|
146
|
-
skinnedPos += (m * pos4) * w;
|
|
147
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
148
|
-
skinnedNrm += (r3 * normal) * w;
|
|
149
|
-
}
|
|
150
|
-
let worldPos = skinnedPos.xyz;
|
|
151
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
152
|
-
output.normal = normalize(skinnedNrm);
|
|
153
|
-
output.uv = uv;
|
|
154
|
-
output.worldPos = worldPos;
|
|
155
|
-
return output;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
159
|
-
let n = normalize(input.normal);
|
|
160
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
161
|
-
|
|
162
|
-
var lightAccum = vec3f(light.ambient);
|
|
163
|
-
let numLights = u32(light.lightCount);
|
|
164
|
-
for (var i = 0u; i < numLights; i++) {
|
|
165
|
-
let l = -light.lights[i].direction;
|
|
166
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
167
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
168
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
169
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
170
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let color = albedo * lightAccum;
|
|
174
|
-
let finalAlpha = material.alpha;
|
|
175
|
-
if (finalAlpha < 0.001) {
|
|
176
|
-
discard;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
180
|
-
}
|
|
80
|
+
code: /* wgsl */ `
|
|
81
|
+
struct CameraUniforms {
|
|
82
|
+
view: mat4x4f,
|
|
83
|
+
projection: mat4x4f,
|
|
84
|
+
viewPos: vec3f,
|
|
85
|
+
_padding: f32,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
struct Light {
|
|
89
|
+
direction: vec3f,
|
|
90
|
+
_padding1: f32,
|
|
91
|
+
color: vec3f,
|
|
92
|
+
intensity: f32,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
struct LightUniforms {
|
|
96
|
+
ambient: f32,
|
|
97
|
+
lightCount: f32,
|
|
98
|
+
_padding1: f32,
|
|
99
|
+
_padding2: f32,
|
|
100
|
+
lights: array<Light, 4>,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
struct MaterialUniforms {
|
|
104
|
+
alpha: f32,
|
|
105
|
+
_padding1: f32,
|
|
106
|
+
_padding2: f32,
|
|
107
|
+
_padding3: f32,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
struct VertexOutput {
|
|
111
|
+
@builtin(position) position: vec4f,
|
|
112
|
+
@location(0) normal: vec3f,
|
|
113
|
+
@location(1) uv: vec2f,
|
|
114
|
+
@location(2) worldPos: vec3f,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
118
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
119
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
120
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
121
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
122
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
123
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
124
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
125
|
+
|
|
126
|
+
@vertex fn vs(
|
|
127
|
+
@location(0) position: vec3f,
|
|
128
|
+
@location(1) normal: vec3f,
|
|
129
|
+
@location(2) uv: vec2f,
|
|
130
|
+
@location(3) joints0: vec4<u32>,
|
|
131
|
+
@location(4) weights0: vec4<f32>
|
|
132
|
+
) -> VertexOutput {
|
|
133
|
+
var output: VertexOutput;
|
|
134
|
+
let pos4 = vec4f(position, 1.0);
|
|
135
|
+
|
|
136
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
137
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
138
|
+
var normalizedWeights: vec4f;
|
|
139
|
+
if (weightSum > 0.0001) {
|
|
140
|
+
normalizedWeights = weights0 / weightSum;
|
|
141
|
+
} else {
|
|
142
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
146
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
147
|
+
for (var i = 0u; i < 4u; i++) {
|
|
148
|
+
let j = joints0[i];
|
|
149
|
+
let w = normalizedWeights[i];
|
|
150
|
+
let m = skinMats[j];
|
|
151
|
+
skinnedPos += (m * pos4) * w;
|
|
152
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
153
|
+
skinnedNrm += (r3 * normal) * w;
|
|
154
|
+
}
|
|
155
|
+
let worldPos = skinnedPos.xyz;
|
|
156
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
157
|
+
output.normal = normalize(skinnedNrm);
|
|
158
|
+
output.uv = uv;
|
|
159
|
+
output.worldPos = worldPos;
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
164
|
+
let n = normalize(input.normal);
|
|
165
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
166
|
+
|
|
167
|
+
var lightAccum = vec3f(light.ambient);
|
|
168
|
+
let numLights = u32(light.lightCount);
|
|
169
|
+
for (var i = 0u; i < numLights; i++) {
|
|
170
|
+
let l = -light.lights[i].direction;
|
|
171
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
172
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
173
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
174
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
175
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let color = albedo * lightAccum;
|
|
179
|
+
let finalAlpha = material.alpha;
|
|
180
|
+
if (finalAlpha < 0.001) {
|
|
181
|
+
discard;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
185
|
+
}
|
|
181
186
|
`,
|
|
182
187
|
});
|
|
183
188
|
// Create a separate shader for hair-over-eyes that outputs pre-multiplied color for darkening effect
|
|
184
189
|
const hairMultiplyShaderModule = this.device.createShaderModule({
|
|
185
190
|
label: "hair multiply shaders",
|
|
186
|
-
code: /* wgsl */ `
|
|
187
|
-
struct CameraUniforms {
|
|
188
|
-
view: mat4x4f,
|
|
189
|
-
projection: mat4x4f,
|
|
190
|
-
viewPos: vec3f,
|
|
191
|
-
_padding: f32,
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
struct Light {
|
|
195
|
-
direction: vec3f,
|
|
196
|
-
_padding1: f32,
|
|
197
|
-
color: vec3f,
|
|
198
|
-
intensity: f32,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
struct LightUniforms {
|
|
202
|
-
ambient: f32,
|
|
203
|
-
lightCount: f32,
|
|
204
|
-
_padding1: f32,
|
|
205
|
-
_padding2: f32,
|
|
206
|
-
lights: array<Light, 4>,
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
struct MaterialUniforms {
|
|
210
|
-
alpha: f32,
|
|
211
|
-
_padding1: f32,
|
|
212
|
-
_padding2: f32,
|
|
213
|
-
_padding3: f32,
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
struct VertexOutput {
|
|
217
|
-
@builtin(position) position: vec4f,
|
|
218
|
-
@location(0) normal: vec3f,
|
|
219
|
-
@location(1) uv: vec2f,
|
|
220
|
-
@location(2) worldPos: vec3f,
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
224
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
225
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
226
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
227
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
228
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
229
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
230
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
231
|
-
|
|
232
|
-
@vertex fn vs(
|
|
233
|
-
@location(0) position: vec3f,
|
|
234
|
-
@location(1) normal: vec3f,
|
|
235
|
-
@location(2) uv: vec2f,
|
|
236
|
-
@location(3) joints0: vec4<u32>,
|
|
237
|
-
@location(4) weights0: vec4<f32>
|
|
238
|
-
) -> VertexOutput {
|
|
239
|
-
var output: VertexOutput;
|
|
240
|
-
let pos4 = vec4f(position, 1.0);
|
|
241
|
-
|
|
242
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
243
|
-
var normalizedWeights: vec4f;
|
|
244
|
-
if (weightSum > 0.0001) {
|
|
245
|
-
normalizedWeights = weights0 / weightSum;
|
|
246
|
-
} else {
|
|
247
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
251
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
252
|
-
for (var i = 0u; i < 4u; i++) {
|
|
253
|
-
let j = joints0[i];
|
|
254
|
-
let w = normalizedWeights[i];
|
|
255
|
-
let m = skinMats[j];
|
|
256
|
-
skinnedPos += (m * pos4) * w;
|
|
257
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
258
|
-
skinnedNrm += (r3 * normal) * w;
|
|
259
|
-
}
|
|
260
|
-
let worldPos = skinnedPos.xyz;
|
|
261
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
262
|
-
output.normal = normalize(skinnedNrm);
|
|
263
|
-
output.uv = uv;
|
|
264
|
-
output.worldPos = worldPos;
|
|
265
|
-
return output;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
269
|
-
let n = normalize(input.normal);
|
|
270
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
271
|
-
|
|
272
|
-
var lightAccum = vec3f(light.ambient);
|
|
273
|
-
let numLights = u32(light.lightCount);
|
|
274
|
-
for (var i = 0u; i < numLights; i++) {
|
|
275
|
-
let l = -light.lights[i].direction;
|
|
276
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
277
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
278
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
279
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
280
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let color = albedo * lightAccum;
|
|
284
|
-
let finalAlpha = material.alpha;
|
|
285
|
-
if (finalAlpha < 0.001) {
|
|
286
|
-
discard;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// For hair-over-eyes effect: simple half-transparent overlay
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
191
|
+
code: /* wgsl */ `
|
|
192
|
+
struct CameraUniforms {
|
|
193
|
+
view: mat4x4f,
|
|
194
|
+
projection: mat4x4f,
|
|
195
|
+
viewPos: vec3f,
|
|
196
|
+
_padding: f32,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
struct Light {
|
|
200
|
+
direction: vec3f,
|
|
201
|
+
_padding1: f32,
|
|
202
|
+
color: vec3f,
|
|
203
|
+
intensity: f32,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
struct LightUniforms {
|
|
207
|
+
ambient: f32,
|
|
208
|
+
lightCount: f32,
|
|
209
|
+
_padding1: f32,
|
|
210
|
+
_padding2: f32,
|
|
211
|
+
lights: array<Light, 4>,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
struct MaterialUniforms {
|
|
215
|
+
alpha: f32,
|
|
216
|
+
_padding1: f32,
|
|
217
|
+
_padding2: f32,
|
|
218
|
+
_padding3: f32,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
struct VertexOutput {
|
|
222
|
+
@builtin(position) position: vec4f,
|
|
223
|
+
@location(0) normal: vec3f,
|
|
224
|
+
@location(1) uv: vec2f,
|
|
225
|
+
@location(2) worldPos: vec3f,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
229
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
230
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
231
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
232
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
233
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
234
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
235
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
236
|
+
|
|
237
|
+
@vertex fn vs(
|
|
238
|
+
@location(0) position: vec3f,
|
|
239
|
+
@location(1) normal: vec3f,
|
|
240
|
+
@location(2) uv: vec2f,
|
|
241
|
+
@location(3) joints0: vec4<u32>,
|
|
242
|
+
@location(4) weights0: vec4<f32>
|
|
243
|
+
) -> VertexOutput {
|
|
244
|
+
var output: VertexOutput;
|
|
245
|
+
let pos4 = vec4f(position, 1.0);
|
|
246
|
+
|
|
247
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
248
|
+
var normalizedWeights: vec4f;
|
|
249
|
+
if (weightSum > 0.0001) {
|
|
250
|
+
normalizedWeights = weights0 / weightSum;
|
|
251
|
+
} else {
|
|
252
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
256
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
257
|
+
for (var i = 0u; i < 4u; i++) {
|
|
258
|
+
let j = joints0[i];
|
|
259
|
+
let w = normalizedWeights[i];
|
|
260
|
+
let m = skinMats[j];
|
|
261
|
+
skinnedPos += (m * pos4) * w;
|
|
262
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
263
|
+
skinnedNrm += (r3 * normal) * w;
|
|
264
|
+
}
|
|
265
|
+
let worldPos = skinnedPos.xyz;
|
|
266
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
267
|
+
output.normal = normalize(skinnedNrm);
|
|
268
|
+
output.uv = uv;
|
|
269
|
+
output.worldPos = worldPos;
|
|
270
|
+
return output;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
274
|
+
let n = normalize(input.normal);
|
|
275
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
276
|
+
|
|
277
|
+
var lightAccum = vec3f(light.ambient);
|
|
278
|
+
let numLights = u32(light.lightCount);
|
|
279
|
+
for (var i = 0u; i < numLights; i++) {
|
|
280
|
+
let l = -light.lights[i].direction;
|
|
281
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
282
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
283
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
284
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
285
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let color = albedo * lightAccum;
|
|
289
|
+
let finalAlpha = material.alpha;
|
|
290
|
+
if (finalAlpha < 0.001) {
|
|
291
|
+
discard;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// For hair-over-eyes effect: simple half-transparent overlay - Use 50% opacity to create a semi-transparent hair color overlay
|
|
295
|
+
let overlayAlpha = finalAlpha * 0.5;
|
|
296
|
+
|
|
297
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
|
|
298
|
+
}
|
|
295
299
|
`,
|
|
296
300
|
});
|
|
297
301
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -383,72 +387,72 @@ export class Engine {
|
|
|
383
387
|
});
|
|
384
388
|
const outlineShaderModule = this.device.createShaderModule({
|
|
385
389
|
label: "outline shaders",
|
|
386
|
-
code: /* wgsl */ `
|
|
387
|
-
struct CameraUniforms {
|
|
388
|
-
view: mat4x4f,
|
|
389
|
-
projection: mat4x4f,
|
|
390
|
-
viewPos: vec3f,
|
|
391
|
-
_padding: f32,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
struct MaterialUniforms {
|
|
395
|
-
edgeColor: vec4f,
|
|
396
|
-
edgeSize: f32,
|
|
397
|
-
_padding1: f32,
|
|
398
|
-
_padding2: f32,
|
|
399
|
-
_padding3: f32,
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
403
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
404
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
405
|
-
|
|
406
|
-
struct VertexOutput {
|
|
407
|
-
@builtin(position) position: vec4f,
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
@vertex fn vs(
|
|
411
|
-
@location(0) position: vec3f,
|
|
412
|
-
@location(1) normal: vec3f,
|
|
413
|
-
@location(2) uv: vec2f,
|
|
414
|
-
@location(3) joints0: vec4<u32>,
|
|
415
|
-
@location(4) weights0: vec4<f32>
|
|
416
|
-
) -> VertexOutput {
|
|
417
|
-
var output: VertexOutput;
|
|
418
|
-
let pos4 = vec4f(position, 1.0);
|
|
419
|
-
|
|
420
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
421
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
422
|
-
var normalizedWeights: vec4f;
|
|
423
|
-
if (weightSum > 0.0001) {
|
|
424
|
-
normalizedWeights = weights0 / weightSum;
|
|
425
|
-
} else {
|
|
426
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
430
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
431
|
-
for (var i = 0u; i < 4u; i++) {
|
|
432
|
-
let j = joints0[i];
|
|
433
|
-
let w = normalizedWeights[i];
|
|
434
|
-
let m = skinMats[j];
|
|
435
|
-
skinnedPos += (m * pos4) * w;
|
|
436
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
437
|
-
skinnedNrm += (r3 * normal) * w;
|
|
438
|
-
}
|
|
439
|
-
let worldPos = skinnedPos.xyz;
|
|
440
|
-
let worldNormal = normalize(skinnedNrm);
|
|
441
|
-
|
|
442
|
-
// MMD invert hull: expand vertices outward along normals
|
|
443
|
-
let scaleFactor = 0.01;
|
|
444
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
445
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
446
|
-
return output;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
450
|
-
return material.edgeColor;
|
|
451
|
-
}
|
|
390
|
+
code: /* wgsl */ `
|
|
391
|
+
struct CameraUniforms {
|
|
392
|
+
view: mat4x4f,
|
|
393
|
+
projection: mat4x4f,
|
|
394
|
+
viewPos: vec3f,
|
|
395
|
+
_padding: f32,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
struct MaterialUniforms {
|
|
399
|
+
edgeColor: vec4f,
|
|
400
|
+
edgeSize: f32,
|
|
401
|
+
_padding1: f32,
|
|
402
|
+
_padding2: f32,
|
|
403
|
+
_padding3: f32,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
407
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
408
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
409
|
+
|
|
410
|
+
struct VertexOutput {
|
|
411
|
+
@builtin(position) position: vec4f,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
@vertex fn vs(
|
|
415
|
+
@location(0) position: vec3f,
|
|
416
|
+
@location(1) normal: vec3f,
|
|
417
|
+
@location(2) uv: vec2f,
|
|
418
|
+
@location(3) joints0: vec4<u32>,
|
|
419
|
+
@location(4) weights0: vec4<f32>
|
|
420
|
+
) -> VertexOutput {
|
|
421
|
+
var output: VertexOutput;
|
|
422
|
+
let pos4 = vec4f(position, 1.0);
|
|
423
|
+
|
|
424
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
425
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
426
|
+
var normalizedWeights: vec4f;
|
|
427
|
+
if (weightSum > 0.0001) {
|
|
428
|
+
normalizedWeights = weights0 / weightSum;
|
|
429
|
+
} else {
|
|
430
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
434
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
435
|
+
for (var i = 0u; i < 4u; i++) {
|
|
436
|
+
let j = joints0[i];
|
|
437
|
+
let w = normalizedWeights[i];
|
|
438
|
+
let m = skinMats[j];
|
|
439
|
+
skinnedPos += (m * pos4) * w;
|
|
440
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
441
|
+
skinnedNrm += (r3 * normal) * w;
|
|
442
|
+
}
|
|
443
|
+
let worldPos = skinnedPos.xyz;
|
|
444
|
+
let worldNormal = normalize(skinnedNrm);
|
|
445
|
+
|
|
446
|
+
// MMD invert hull: expand vertices outward along normals
|
|
447
|
+
let scaleFactor = 0.01;
|
|
448
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
449
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
450
|
+
return output;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
454
|
+
return material.edgeColor;
|
|
455
|
+
}
|
|
452
456
|
`,
|
|
453
457
|
});
|
|
454
458
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -519,8 +523,7 @@ export class Engine {
|
|
|
519
523
|
count: this.sampleCount,
|
|
520
524
|
},
|
|
521
525
|
});
|
|
522
|
-
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1)
|
|
523
|
-
// Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
526
|
+
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1) - Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
524
527
|
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
525
528
|
label: "hair outline pipeline",
|
|
526
529
|
layout: outlinePipelineLayout,
|
|
@@ -601,8 +604,7 @@ export class Engine {
|
|
|
601
604
|
count: this.sampleCount,
|
|
602
605
|
},
|
|
603
606
|
});
|
|
604
|
-
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists
|
|
605
|
-
// Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
607
|
+
// 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
|
|
606
608
|
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
607
609
|
label: "hair outline over eyes pipeline",
|
|
608
610
|
layout: outlinePipelineLayout,
|
|
@@ -718,8 +720,7 @@ export class Engine {
|
|
|
718
720
|
format: this.presentationFormat,
|
|
719
721
|
blend: {
|
|
720
722
|
color: {
|
|
721
|
-
// Simple half-transparent overlay effect
|
|
722
|
-
// Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
723
|
+
// Simple half-transparent overlay effect - Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
723
724
|
srcFactor: "src-alpha",
|
|
724
725
|
dstFactor: "one-minus-src-alpha",
|
|
725
726
|
operation: "add",
|
|
@@ -888,31 +889,31 @@ export class Engine {
|
|
|
888
889
|
createSkinMatrixComputePipeline() {
|
|
889
890
|
const computeShader = this.device.createShaderModule({
|
|
890
891
|
label: "skin matrix compute",
|
|
891
|
-
code: /* wgsl */ `
|
|
892
|
-
struct BoneCountUniform {
|
|
893
|
-
count: u32,
|
|
894
|
-
_padding1: u32,
|
|
895
|
-
_padding2: u32,
|
|
896
|
-
_padding3: u32,
|
|
897
|
-
_padding4: vec4<u32>,
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
901
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
902
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
903
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
904
|
-
|
|
905
|
-
@compute @workgroup_size(64)
|
|
906
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
907
|
-
let boneIndex = globalId.x;
|
|
908
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
909
|
-
if (boneIndex >= boneCount.count) {
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
let worldMat = worldMatrices[boneIndex];
|
|
913
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
914
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
915
|
-
}
|
|
892
|
+
code: /* wgsl */ `
|
|
893
|
+
struct BoneCountUniform {
|
|
894
|
+
count: u32,
|
|
895
|
+
_padding1: u32,
|
|
896
|
+
_padding2: u32,
|
|
897
|
+
_padding3: u32,
|
|
898
|
+
_padding4: vec4<u32>,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
902
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
903
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
904
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
905
|
+
|
|
906
|
+
@compute @workgroup_size(64)
|
|
907
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
908
|
+
let boneIndex = globalId.x;
|
|
909
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
910
|
+
if (boneIndex >= boneCount.count) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
let worldMat = worldMatrices[boneIndex];
|
|
914
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
915
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
916
|
+
}
|
|
916
917
|
`,
|
|
917
918
|
});
|
|
918
919
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -923,6 +924,271 @@ export class Engine {
|
|
|
923
924
|
},
|
|
924
925
|
});
|
|
925
926
|
}
|
|
927
|
+
// Create fullscreen quad for post-processing
|
|
928
|
+
createFullscreenQuad() {
|
|
929
|
+
// Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
|
|
930
|
+
const quadVertices = new Float32Array([
|
|
931
|
+
// Triangle 1
|
|
932
|
+
-1.0,
|
|
933
|
+
-1.0,
|
|
934
|
+
0.0,
|
|
935
|
+
0.0, // bottom-left
|
|
936
|
+
1.0,
|
|
937
|
+
-1.0,
|
|
938
|
+
1.0,
|
|
939
|
+
0.0, // bottom-right
|
|
940
|
+
-1.0,
|
|
941
|
+
1.0,
|
|
942
|
+
0.0,
|
|
943
|
+
1.0, // top-left
|
|
944
|
+
// Triangle 2
|
|
945
|
+
-1.0,
|
|
946
|
+
1.0,
|
|
947
|
+
0.0,
|
|
948
|
+
1.0, // top-left
|
|
949
|
+
1.0,
|
|
950
|
+
-1.0,
|
|
951
|
+
1.0,
|
|
952
|
+
0.0, // bottom-right
|
|
953
|
+
1.0,
|
|
954
|
+
1.0,
|
|
955
|
+
1.0,
|
|
956
|
+
1.0, // top-right
|
|
957
|
+
]);
|
|
958
|
+
this.fullscreenQuadBuffer = this.device.createBuffer({
|
|
959
|
+
label: "fullscreen quad",
|
|
960
|
+
size: quadVertices.byteLength,
|
|
961
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
962
|
+
});
|
|
963
|
+
this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices);
|
|
964
|
+
}
|
|
965
|
+
// Create bloom post-processing pipelines
|
|
966
|
+
createBloomPipelines() {
|
|
967
|
+
// Bloom extraction shader (extracts bright areas)
|
|
968
|
+
const bloomExtractShader = this.device.createShaderModule({
|
|
969
|
+
label: "bloom extract",
|
|
970
|
+
code: /* wgsl */ `
|
|
971
|
+
struct VertexOutput {
|
|
972
|
+
@builtin(position) position: vec4f,
|
|
973
|
+
@location(0) uv: vec2f,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
977
|
+
var output: VertexOutput;
|
|
978
|
+
// Generate fullscreen quad from vertex index
|
|
979
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
980
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
981
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
982
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
983
|
+
return output;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
struct BloomExtractUniforms {
|
|
987
|
+
threshold: f32,
|
|
988
|
+
_padding1: f32,
|
|
989
|
+
_padding2: f32,
|
|
990
|
+
_padding3: f32,
|
|
991
|
+
_padding4: f32,
|
|
992
|
+
_padding5: f32,
|
|
993
|
+
_padding6: f32,
|
|
994
|
+
_padding7: f32,
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
998
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
999
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
1000
|
+
|
|
1001
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1002
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
1003
|
+
// Extract bright areas above threshold
|
|
1004
|
+
let threshold = extractUniforms.threshold;
|
|
1005
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
1006
|
+
return vec4f(bloom, color.a);
|
|
1007
|
+
}
|
|
1008
|
+
`,
|
|
1009
|
+
});
|
|
1010
|
+
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
1011
|
+
const bloomBlurShader = this.device.createShaderModule({
|
|
1012
|
+
label: "bloom blur",
|
|
1013
|
+
code: /* wgsl */ `
|
|
1014
|
+
struct VertexOutput {
|
|
1015
|
+
@builtin(position) position: vec4f,
|
|
1016
|
+
@location(0) uv: vec2f,
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1020
|
+
var output: VertexOutput;
|
|
1021
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1022
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1023
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1024
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1025
|
+
return output;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
struct BlurUniforms {
|
|
1029
|
+
direction: vec2f,
|
|
1030
|
+
_padding1: f32,
|
|
1031
|
+
_padding2: f32,
|
|
1032
|
+
_padding3: f32,
|
|
1033
|
+
_padding4: f32,
|
|
1034
|
+
_padding5: f32,
|
|
1035
|
+
_padding6: f32,
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1039
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
1040
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
1041
|
+
|
|
1042
|
+
// 9-tap gaussian blur
|
|
1043
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1044
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1045
|
+
var result = vec4f(0.0);
|
|
1046
|
+
|
|
1047
|
+
// Gaussian weights for 9-tap filter
|
|
1048
|
+
let weights = array<f32, 9>(
|
|
1049
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
1050
|
+
0.19459459, 0.22702703,
|
|
1051
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
1055
|
+
|
|
1056
|
+
for (var i = 0u; i < 9u; i++) {
|
|
1057
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
1058
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
`,
|
|
1064
|
+
});
|
|
1065
|
+
// Bloom composition shader (combines original scene with bloom)
|
|
1066
|
+
const bloomComposeShader = this.device.createShaderModule({
|
|
1067
|
+
label: "bloom compose",
|
|
1068
|
+
code: /* wgsl */ `
|
|
1069
|
+
struct VertexOutput {
|
|
1070
|
+
@builtin(position) position: vec4f,
|
|
1071
|
+
@location(0) uv: vec2f,
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1075
|
+
var output: VertexOutput;
|
|
1076
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1077
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1078
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1079
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1080
|
+
return output;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
struct BloomComposeUniforms {
|
|
1084
|
+
intensity: f32,
|
|
1085
|
+
_padding1: f32,
|
|
1086
|
+
_padding2: f32,
|
|
1087
|
+
_padding3: f32,
|
|
1088
|
+
_padding4: f32,
|
|
1089
|
+
_padding5: f32,
|
|
1090
|
+
_padding6: f32,
|
|
1091
|
+
_padding7: f32,
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1095
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1096
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1097
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1098
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1099
|
+
|
|
1100
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1101
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1102
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1103
|
+
// Additive blending with intensity control
|
|
1104
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1105
|
+
return vec4f(result, scene.a);
|
|
1106
|
+
}
|
|
1107
|
+
`,
|
|
1108
|
+
});
|
|
1109
|
+
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
1110
|
+
const blurDirectionBuffer = this.device.createBuffer({
|
|
1111
|
+
label: "blur direction",
|
|
1112
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1113
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1114
|
+
});
|
|
1115
|
+
// Create uniform buffer for bloom intensity (minimum 32 bytes for WebGPU)
|
|
1116
|
+
const bloomIntensityBuffer = this.device.createBuffer({
|
|
1117
|
+
label: "bloom intensity",
|
|
1118
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1119
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1120
|
+
});
|
|
1121
|
+
// Create uniform buffer for bloom threshold (minimum 32 bytes for WebGPU)
|
|
1122
|
+
const bloomThresholdBuffer = this.device.createBuffer({
|
|
1123
|
+
label: "bloom threshold",
|
|
1124
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1125
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1126
|
+
});
|
|
1127
|
+
// Set default bloom values
|
|
1128
|
+
const intensityData = new Float32Array(8); // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1129
|
+
intensityData[0] = this.bloomIntensity;
|
|
1130
|
+
this.device.queue.writeBuffer(bloomIntensityBuffer, 0, intensityData);
|
|
1131
|
+
const thresholdData = new Float32Array(8); // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1132
|
+
thresholdData[0] = this.bloomThreshold;
|
|
1133
|
+
this.device.queue.writeBuffer(bloomThresholdBuffer, 0, thresholdData);
|
|
1134
|
+
// Create linear sampler for post-processing
|
|
1135
|
+
const linearSampler = this.device.createSampler({
|
|
1136
|
+
magFilter: "linear",
|
|
1137
|
+
minFilter: "linear",
|
|
1138
|
+
addressModeU: "clamp-to-edge",
|
|
1139
|
+
addressModeV: "clamp-to-edge",
|
|
1140
|
+
});
|
|
1141
|
+
// Bloom extraction pipeline
|
|
1142
|
+
this.bloomExtractPipeline = this.device.createRenderPipeline({
|
|
1143
|
+
label: "bloom extract",
|
|
1144
|
+
layout: "auto",
|
|
1145
|
+
vertex: {
|
|
1146
|
+
module: bloomExtractShader,
|
|
1147
|
+
entryPoint: "vs",
|
|
1148
|
+
},
|
|
1149
|
+
fragment: {
|
|
1150
|
+
module: bloomExtractShader,
|
|
1151
|
+
entryPoint: "fs",
|
|
1152
|
+
targets: [{ format: this.presentationFormat }],
|
|
1153
|
+
},
|
|
1154
|
+
primitive: { topology: "triangle-list" },
|
|
1155
|
+
});
|
|
1156
|
+
// Bloom blur pipeline
|
|
1157
|
+
this.bloomBlurPipeline = this.device.createRenderPipeline({
|
|
1158
|
+
label: "bloom blur",
|
|
1159
|
+
layout: "auto",
|
|
1160
|
+
vertex: {
|
|
1161
|
+
module: bloomBlurShader,
|
|
1162
|
+
entryPoint: "vs",
|
|
1163
|
+
},
|
|
1164
|
+
fragment: {
|
|
1165
|
+
module: bloomBlurShader,
|
|
1166
|
+
entryPoint: "fs",
|
|
1167
|
+
targets: [{ format: this.presentationFormat }],
|
|
1168
|
+
},
|
|
1169
|
+
primitive: { topology: "triangle-list" },
|
|
1170
|
+
});
|
|
1171
|
+
// Bloom composition pipeline
|
|
1172
|
+
this.bloomComposePipeline = this.device.createRenderPipeline({
|
|
1173
|
+
label: "bloom compose",
|
|
1174
|
+
layout: "auto",
|
|
1175
|
+
vertex: {
|
|
1176
|
+
module: bloomComposeShader,
|
|
1177
|
+
entryPoint: "vs",
|
|
1178
|
+
},
|
|
1179
|
+
fragment: {
|
|
1180
|
+
module: bloomComposeShader,
|
|
1181
|
+
entryPoint: "fs",
|
|
1182
|
+
targets: [{ format: this.presentationFormat }],
|
|
1183
|
+
},
|
|
1184
|
+
primitive: { topology: "triangle-list" },
|
|
1185
|
+
});
|
|
1186
|
+
// Store buffers and sampler for later use
|
|
1187
|
+
this.blurDirectionBuffer = blurDirectionBuffer;
|
|
1188
|
+
this.bloomIntensityBuffer = bloomIntensityBuffer;
|
|
1189
|
+
this.bloomThresholdBuffer = bloomThresholdBuffer;
|
|
1190
|
+
this.linearSampler = linearSampler;
|
|
1191
|
+
}
|
|
926
1192
|
// Step 3: Setup canvas resize handling
|
|
927
1193
|
setupResize() {
|
|
928
1194
|
this.resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
@@ -952,17 +1218,47 @@ export class Engine {
|
|
|
952
1218
|
format: "depth24plus-stencil8",
|
|
953
1219
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
954
1220
|
});
|
|
1221
|
+
// Create scene render texture (non-multisampled for post-processing)
|
|
1222
|
+
this.sceneRenderTexture = this.device.createTexture({
|
|
1223
|
+
label: "scene render texture",
|
|
1224
|
+
size: [width, height],
|
|
1225
|
+
format: this.presentationFormat,
|
|
1226
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1227
|
+
});
|
|
1228
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView();
|
|
1229
|
+
// Create bloom textures (half resolution for performance)
|
|
1230
|
+
const bloomWidth = Math.floor(width / 2);
|
|
1231
|
+
const bloomHeight = Math.floor(height / 2);
|
|
1232
|
+
this.bloomExtractTexture = this.device.createTexture({
|
|
1233
|
+
label: "bloom extract",
|
|
1234
|
+
size: [bloomWidth, bloomHeight],
|
|
1235
|
+
format: this.presentationFormat,
|
|
1236
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1237
|
+
});
|
|
1238
|
+
this.bloomBlurTexture1 = this.device.createTexture({
|
|
1239
|
+
label: "bloom blur 1",
|
|
1240
|
+
size: [bloomWidth, bloomHeight],
|
|
1241
|
+
format: this.presentationFormat,
|
|
1242
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1243
|
+
});
|
|
1244
|
+
this.bloomBlurTexture2 = this.device.createTexture({
|
|
1245
|
+
label: "bloom blur 2",
|
|
1246
|
+
size: [bloomWidth, bloomHeight],
|
|
1247
|
+
format: this.presentationFormat,
|
|
1248
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1249
|
+
});
|
|
955
1250
|
const depthTextureView = this.depthTexture.createView();
|
|
1251
|
+
// Render scene to texture instead of directly to canvas
|
|
956
1252
|
const colorAttachment = this.sampleCount > 1
|
|
957
1253
|
? {
|
|
958
1254
|
view: this.multisampleTexture.createView(),
|
|
959
|
-
resolveTarget: this.
|
|
1255
|
+
resolveTarget: this.sceneRenderTextureView,
|
|
960
1256
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
961
1257
|
loadOp: "clear",
|
|
962
1258
|
storeOp: "store",
|
|
963
1259
|
}
|
|
964
1260
|
: {
|
|
965
|
-
view: this.
|
|
1261
|
+
view: this.sceneRenderTextureView,
|
|
966
1262
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
967
1263
|
loadOp: "clear",
|
|
968
1264
|
storeOp: "store",
|
|
@@ -1217,8 +1513,7 @@ export class Engine {
|
|
|
1217
1513
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1218
1514
|
});
|
|
1219
1515
|
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
|
|
1220
|
-
// Create bind groups using the shared bind group layout
|
|
1221
|
-
// All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1516
|
+
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1222
1517
|
const bindGroup = this.device.createBindGroup({
|
|
1223
1518
|
label: `material bind group: ${mat.name}`,
|
|
1224
1519
|
layout: this.hairBindGroupLayout,
|
|
@@ -1266,8 +1561,7 @@ export class Engine {
|
|
|
1266
1561
|
isTransparent,
|
|
1267
1562
|
});
|
|
1268
1563
|
}
|
|
1269
|
-
// Outline for all materials (including transparent)
|
|
1270
|
-
// Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1564
|
+
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1271
1565
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1272
1566
|
const materialUniformData = new Float32Array(8);
|
|
1273
1567
|
materialUniformData[0] = mat.edgeColor[0];
|
|
@@ -1388,7 +1682,7 @@ export class Engine {
|
|
|
1388
1682
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1389
1683
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1390
1684
|
this.drawCallCount = 0;
|
|
1391
|
-
//
|
|
1685
|
+
// PASS 1: Opaque non-eye, non-hair (face, body, etc)
|
|
1392
1686
|
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1393
1687
|
pass.setPipeline(this.pipeline);
|
|
1394
1688
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
@@ -1398,7 +1692,7 @@ export class Engine {
|
|
|
1398
1692
|
this.drawCallCount++;
|
|
1399
1693
|
}
|
|
1400
1694
|
}
|
|
1401
|
-
//
|
|
1695
|
+
// PASS 2: Eyes (writes stencil = 1)
|
|
1402
1696
|
pass.setPipeline(this.eyePipeline);
|
|
1403
1697
|
pass.setStencilReference(1); // Set stencil reference value to 1
|
|
1404
1698
|
for (const draw of this.eyeDraws) {
|
|
@@ -1408,8 +1702,7 @@ export class Engine {
|
|
|
1408
1702
|
this.drawCallCount++;
|
|
1409
1703
|
}
|
|
1410
1704
|
}
|
|
1411
|
-
//
|
|
1412
|
-
// Draw hair geometry first to establish depth
|
|
1705
|
+
// PASS 3a: Hair over eyes (stencil == 1, multiply blend) - Draw hair geometry first to establish depth
|
|
1413
1706
|
pass.setPipeline(this.hairMultiplyPipeline);
|
|
1414
1707
|
pass.setStencilReference(1); // Check against stencil value 1
|
|
1415
1708
|
for (const draw of this.hairDraws) {
|
|
@@ -1419,9 +1712,7 @@ export class Engine {
|
|
|
1419
1712
|
this.drawCallCount++;
|
|
1420
1713
|
}
|
|
1421
1714
|
}
|
|
1422
|
-
//
|
|
1423
|
-
// Use depth compare "less-equal" with the hair depth to only draw outline where hair exists
|
|
1424
|
-
// The outline is expanded outward, so we need to ensure it only appears near the hair edge
|
|
1715
|
+
// PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair)
|
|
1425
1716
|
pass.setPipeline(this.hairOutlineOverEyesPipeline);
|
|
1426
1717
|
pass.setStencilReference(1); // Check against stencil value 1 (with equal test)
|
|
1427
1718
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -1430,7 +1721,7 @@ export class Engine {
|
|
|
1430
1721
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1431
1722
|
}
|
|
1432
1723
|
}
|
|
1433
|
-
//
|
|
1724
|
+
// PASS 3b: Hair over non-eyes (stencil != 1, opaque)
|
|
1434
1725
|
pass.setPipeline(this.hairOpaquePipeline);
|
|
1435
1726
|
pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
|
|
1436
1727
|
for (const draw of this.hairDraws) {
|
|
@@ -1440,8 +1731,7 @@ export class Engine {
|
|
|
1440
1731
|
this.drawCallCount++;
|
|
1441
1732
|
}
|
|
1442
1733
|
}
|
|
1443
|
-
//
|
|
1444
|
-
// Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1734
|
+
// PASS 3b.5: Hair outlines over non-eyes (stencil != 1) - Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1445
1735
|
pass.setPipeline(this.hairOutlinePipeline);
|
|
1446
1736
|
pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
|
|
1447
1737
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -1451,7 +1741,7 @@ export class Engine {
|
|
|
1451
1741
|
}
|
|
1452
1742
|
}
|
|
1453
1743
|
this.drawOutlines(pass, false); // Opaque outlines
|
|
1454
|
-
//
|
|
1744
|
+
// PASS 4: Transparent non-eye, non-hair
|
|
1455
1745
|
pass.setPipeline(this.pipeline);
|
|
1456
1746
|
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1457
1747
|
if (draw.count > 0) {
|
|
@@ -1463,9 +1753,136 @@ export class Engine {
|
|
|
1463
1753
|
this.drawOutlines(pass, true); // Transparent outlines
|
|
1464
1754
|
pass.end();
|
|
1465
1755
|
this.device.queue.submit([encoder.finish()]);
|
|
1756
|
+
// Apply bloom post-processing
|
|
1757
|
+
this.applyBloom();
|
|
1466
1758
|
this.updateStats(performance.now() - currentTime);
|
|
1467
1759
|
}
|
|
1468
1760
|
}
|
|
1761
|
+
// Apply bloom post-processing
|
|
1762
|
+
applyBloom() {
|
|
1763
|
+
if (!this.sceneRenderTexture || !this.bloomExtractTexture) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
// Update bloom parameters
|
|
1767
|
+
const thresholdData = new Float32Array(8);
|
|
1768
|
+
thresholdData[0] = this.bloomThreshold;
|
|
1769
|
+
this.device.queue.writeBuffer(this.bloomThresholdBuffer, 0, thresholdData);
|
|
1770
|
+
const intensityData = new Float32Array(8);
|
|
1771
|
+
intensityData[0] = this.bloomIntensity;
|
|
1772
|
+
this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData);
|
|
1773
|
+
const encoder = this.device.createCommandEncoder();
|
|
1774
|
+
const width = this.canvas.width;
|
|
1775
|
+
const height = this.canvas.height;
|
|
1776
|
+
const bloomWidth = Math.floor(width / 2);
|
|
1777
|
+
const bloomHeight = Math.floor(height / 2);
|
|
1778
|
+
// Pass 1: Extract bright areas (downsample to half resolution)
|
|
1779
|
+
const extractPass = encoder.beginRenderPass({
|
|
1780
|
+
label: "bloom extract",
|
|
1781
|
+
colorAttachments: [
|
|
1782
|
+
{
|
|
1783
|
+
view: this.bloomExtractTexture.createView(),
|
|
1784
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1785
|
+
loadOp: "clear",
|
|
1786
|
+
storeOp: "store",
|
|
1787
|
+
},
|
|
1788
|
+
],
|
|
1789
|
+
});
|
|
1790
|
+
const extractBindGroup = this.device.createBindGroup({
|
|
1791
|
+
layout: this.bloomExtractPipeline.getBindGroupLayout(0),
|
|
1792
|
+
entries: [
|
|
1793
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
1794
|
+
{ binding: 1, resource: this.linearSampler },
|
|
1795
|
+
{ binding: 2, resource: { buffer: this.bloomThresholdBuffer } },
|
|
1796
|
+
],
|
|
1797
|
+
});
|
|
1798
|
+
extractPass.setPipeline(this.bloomExtractPipeline);
|
|
1799
|
+
extractPass.setBindGroup(0, extractBindGroup);
|
|
1800
|
+
extractPass.draw(6, 1, 0, 0);
|
|
1801
|
+
extractPass.end();
|
|
1802
|
+
// Pass 2: Horizontal blur
|
|
1803
|
+
const hBlurData = new Float32Array(4); // vec2f + padding = 4 floats
|
|
1804
|
+
hBlurData[0] = 1.0;
|
|
1805
|
+
hBlurData[1] = 0.0;
|
|
1806
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, hBlurData);
|
|
1807
|
+
const blurHPass = encoder.beginRenderPass({
|
|
1808
|
+
label: "bloom blur horizontal",
|
|
1809
|
+
colorAttachments: [
|
|
1810
|
+
{
|
|
1811
|
+
view: this.bloomBlurTexture1.createView(),
|
|
1812
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1813
|
+
loadOp: "clear",
|
|
1814
|
+
storeOp: "store",
|
|
1815
|
+
},
|
|
1816
|
+
],
|
|
1817
|
+
});
|
|
1818
|
+
const blurHBindGroup = this.device.createBindGroup({
|
|
1819
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
1820
|
+
entries: [
|
|
1821
|
+
{ binding: 0, resource: this.bloomExtractTexture.createView() },
|
|
1822
|
+
{ binding: 1, resource: this.linearSampler },
|
|
1823
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
1824
|
+
],
|
|
1825
|
+
});
|
|
1826
|
+
blurHPass.setPipeline(this.bloomBlurPipeline);
|
|
1827
|
+
blurHPass.setBindGroup(0, blurHBindGroup);
|
|
1828
|
+
blurHPass.draw(6, 1, 0, 0);
|
|
1829
|
+
blurHPass.end();
|
|
1830
|
+
// Pass 3: Vertical blur
|
|
1831
|
+
const vBlurData = new Float32Array(4); // vec2f + padding = 4 floats
|
|
1832
|
+
vBlurData[0] = 0.0;
|
|
1833
|
+
vBlurData[1] = 1.0;
|
|
1834
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, vBlurData);
|
|
1835
|
+
const blurVPass = encoder.beginRenderPass({
|
|
1836
|
+
label: "bloom blur vertical",
|
|
1837
|
+
colorAttachments: [
|
|
1838
|
+
{
|
|
1839
|
+
view: this.bloomBlurTexture2.createView(),
|
|
1840
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1841
|
+
loadOp: "clear",
|
|
1842
|
+
storeOp: "store",
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
});
|
|
1846
|
+
const blurVBindGroup = this.device.createBindGroup({
|
|
1847
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
1848
|
+
entries: [
|
|
1849
|
+
{ binding: 0, resource: this.bloomBlurTexture1.createView() },
|
|
1850
|
+
{ binding: 1, resource: this.linearSampler },
|
|
1851
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
1852
|
+
],
|
|
1853
|
+
});
|
|
1854
|
+
blurVPass.setPipeline(this.bloomBlurPipeline);
|
|
1855
|
+
blurVPass.setBindGroup(0, blurVBindGroup);
|
|
1856
|
+
blurVPass.draw(6, 1, 0, 0);
|
|
1857
|
+
blurVPass.end();
|
|
1858
|
+
// Pass 4: Compose scene + bloom to canvas
|
|
1859
|
+
const composePass = encoder.beginRenderPass({
|
|
1860
|
+
label: "bloom compose",
|
|
1861
|
+
colorAttachments: [
|
|
1862
|
+
{
|
|
1863
|
+
view: this.context.getCurrentTexture().createView(),
|
|
1864
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1865
|
+
loadOp: "clear",
|
|
1866
|
+
storeOp: "store",
|
|
1867
|
+
},
|
|
1868
|
+
],
|
|
1869
|
+
});
|
|
1870
|
+
const composeBindGroup = this.device.createBindGroup({
|
|
1871
|
+
layout: this.bloomComposePipeline.getBindGroupLayout(0),
|
|
1872
|
+
entries: [
|
|
1873
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
1874
|
+
{ binding: 1, resource: this.linearSampler },
|
|
1875
|
+
{ binding: 2, resource: this.bloomBlurTexture2.createView() },
|
|
1876
|
+
{ binding: 3, resource: this.linearSampler },
|
|
1877
|
+
{ binding: 4, resource: { buffer: this.bloomIntensityBuffer } },
|
|
1878
|
+
],
|
|
1879
|
+
});
|
|
1880
|
+
composePass.setPipeline(this.bloomComposePipeline);
|
|
1881
|
+
composePass.setBindGroup(0, composeBindGroup);
|
|
1882
|
+
composePass.draw(6, 1, 0, 0);
|
|
1883
|
+
composePass.end();
|
|
1884
|
+
this.device.queue.submit([encoder.finish()]);
|
|
1885
|
+
}
|
|
1469
1886
|
// Update camera uniform buffer each frame
|
|
1470
1887
|
updateCameraUniforms() {
|
|
1471
1888
|
const viewMatrix = this.camera.getViewMatrix();
|
|
@@ -1482,10 +1899,12 @@ export class Engine {
|
|
|
1482
1899
|
updateRenderTarget() {
|
|
1483
1900
|
const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
|
|
1484
1901
|
if (this.sampleCount > 1) {
|
|
1485
|
-
|
|
1902
|
+
// Resolve to scene render texture for post-processing
|
|
1903
|
+
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
1486
1904
|
}
|
|
1487
1905
|
else {
|
|
1488
|
-
|
|
1906
|
+
// Render directly to scene render texture
|
|
1907
|
+
colorAttachment.view = this.sceneRenderTextureView;
|
|
1489
1908
|
}
|
|
1490
1909
|
}
|
|
1491
1910
|
// Update model pose and physics
|