reze-engine 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -104
- package/dist/engine.d.ts +11 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +402 -394
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +2472 -2455
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -179
package/dist/engine.js
CHANGED
|
@@ -4,19 +4,20 @@ import { PmxLoader } from "./pmx-loader";
|
|
|
4
4
|
import { Physics } from "./physics";
|
|
5
5
|
import { VMDLoader } from "./vmd-loader";
|
|
6
6
|
export class Engine {
|
|
7
|
-
constructor(canvas) {
|
|
7
|
+
constructor(canvas, options) {
|
|
8
8
|
this.cameraMatrixData = new Float32Array(36);
|
|
9
9
|
this.lightData = new Float32Array(64);
|
|
10
10
|
this.lightCount = 0;
|
|
11
11
|
this.resizeObserver = null;
|
|
12
12
|
this.sampleCount = 4; // MSAA 4x
|
|
13
|
+
// Ambient light settings
|
|
14
|
+
this.ambient = 1.0;
|
|
13
15
|
// Bloom settings
|
|
14
16
|
this.bloomThreshold = 0.3;
|
|
15
17
|
this.bloomIntensity = 0.12;
|
|
16
18
|
// Rim light settings
|
|
17
19
|
this.rimLightIntensity = 0.45;
|
|
18
20
|
this.rimLightPower = 2.0;
|
|
19
|
-
this.rimLightColor = [1.0, 1.0, 1.0];
|
|
20
21
|
this.currentModel = null;
|
|
21
22
|
this.modelDir = "";
|
|
22
23
|
this.physics = null;
|
|
@@ -47,6 +48,13 @@ export class Engine {
|
|
|
47
48
|
this.hairOutlineDraws = [];
|
|
48
49
|
this.transparentNonEyeNonHairOutlineDraws = [];
|
|
49
50
|
this.canvas = canvas;
|
|
51
|
+
if (options) {
|
|
52
|
+
this.ambient = options.ambient ?? 1.0;
|
|
53
|
+
this.bloomThreshold = options.bloomThreshold ?? 0.3;
|
|
54
|
+
this.bloomIntensity = options.bloomIntensity ?? 0.12;
|
|
55
|
+
this.rimLightIntensity = options.rimLightIntensity ?? 0.45;
|
|
56
|
+
this.rimLightPower = options.rimLightPower ?? 2.0;
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
// Step 1: Get WebGPU device and context
|
|
52
60
|
async init() {
|
|
@@ -84,125 +92,125 @@ export class Engine {
|
|
|
84
92
|
});
|
|
85
93
|
const shaderModule = this.device.createShaderModule({
|
|
86
94
|
label: "model shaders",
|
|
87
|
-
code: /* wgsl */ `
|
|
88
|
-
struct CameraUniforms {
|
|
89
|
-
view: mat4x4f,
|
|
90
|
-
projection: mat4x4f,
|
|
91
|
-
viewPos: vec3f,
|
|
92
|
-
_padding: f32,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
struct Light {
|
|
96
|
-
direction: vec3f,
|
|
97
|
-
_padding1: f32,
|
|
98
|
-
color: vec3f,
|
|
99
|
-
intensity: f32,
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
struct LightUniforms {
|
|
103
|
-
ambient: f32,
|
|
104
|
-
lightCount: f32,
|
|
105
|
-
_padding1: f32,
|
|
106
|
-
_padding2: f32,
|
|
107
|
-
lights: array<Light, 4>,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
struct MaterialUniforms {
|
|
111
|
-
alpha: f32,
|
|
112
|
-
alphaMultiplier: f32,
|
|
113
|
-
rimIntensity: f32,
|
|
114
|
-
rimPower: f32,
|
|
115
|
-
rimColor: vec3f,
|
|
116
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
struct VertexOutput {
|
|
120
|
-
@builtin(position) position: vec4f,
|
|
121
|
-
@location(0) normal: vec3f,
|
|
122
|
-
@location(1) uv: vec2f,
|
|
123
|
-
@location(2) worldPos: vec3f,
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
127
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
128
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
129
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
130
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
131
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
132
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
133
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
134
|
-
|
|
135
|
-
@vertex fn vs(
|
|
136
|
-
@location(0) position: vec3f,
|
|
137
|
-
@location(1) normal: vec3f,
|
|
138
|
-
@location(2) uv: vec2f,
|
|
139
|
-
@location(3) joints0: vec4<u32>,
|
|
140
|
-
@location(4) weights0: vec4<f32>
|
|
141
|
-
) -> VertexOutput {
|
|
142
|
-
var output: VertexOutput;
|
|
143
|
-
let pos4 = vec4f(position, 1.0);
|
|
144
|
-
|
|
145
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
146
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
147
|
-
var normalizedWeights: vec4f;
|
|
148
|
-
if (weightSum > 0.0001) {
|
|
149
|
-
normalizedWeights = weights0 / weightSum;
|
|
150
|
-
} else {
|
|
151
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
155
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
156
|
-
for (var i = 0u; i < 4u; i++) {
|
|
157
|
-
let j = joints0[i];
|
|
158
|
-
let w = normalizedWeights[i];
|
|
159
|
-
let m = skinMats[j];
|
|
160
|
-
skinnedPos += (m * pos4) * w;
|
|
161
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
162
|
-
skinnedNrm += (r3 * normal) * w;
|
|
163
|
-
}
|
|
164
|
-
let worldPos = skinnedPos.xyz;
|
|
165
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
166
|
-
output.normal = normalize(skinnedNrm);
|
|
167
|
-
output.uv = uv;
|
|
168
|
-
output.worldPos = worldPos;
|
|
169
|
-
return output;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
173
|
-
let n = normalize(input.normal);
|
|
174
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
175
|
-
|
|
176
|
-
var lightAccum = vec3f(light.ambient);
|
|
177
|
-
let numLights = u32(light.lightCount);
|
|
178
|
-
for (var i = 0u; i < numLights; i++) {
|
|
179
|
-
let l = -light.lights[i].direction;
|
|
180
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
181
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
182
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
183
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
184
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Rim light calculation
|
|
188
|
-
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
189
|
-
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
190
|
-
rimFactor = pow(rimFactor, material.rimPower);
|
|
191
|
-
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
192
|
-
|
|
193
|
-
let color = albedo * lightAccum + rimLight;
|
|
194
|
-
|
|
195
|
-
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
196
|
-
if (material.isOverEyes > 0.5) {
|
|
197
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (finalAlpha < 0.001) {
|
|
201
|
-
discard;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
205
|
-
}
|
|
95
|
+
code: /* wgsl */ `
|
|
96
|
+
struct CameraUniforms {
|
|
97
|
+
view: mat4x4f,
|
|
98
|
+
projection: mat4x4f,
|
|
99
|
+
viewPos: vec3f,
|
|
100
|
+
_padding: f32,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
struct Light {
|
|
104
|
+
direction: vec3f,
|
|
105
|
+
_padding1: f32,
|
|
106
|
+
color: vec3f,
|
|
107
|
+
intensity: f32,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
struct LightUniforms {
|
|
111
|
+
ambient: f32,
|
|
112
|
+
lightCount: f32,
|
|
113
|
+
_padding1: f32,
|
|
114
|
+
_padding2: f32,
|
|
115
|
+
lights: array<Light, 4>,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
struct MaterialUniforms {
|
|
119
|
+
alpha: f32,
|
|
120
|
+
alphaMultiplier: f32,
|
|
121
|
+
rimIntensity: f32,
|
|
122
|
+
rimPower: f32,
|
|
123
|
+
rimColor: vec3f,
|
|
124
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
struct VertexOutput {
|
|
128
|
+
@builtin(position) position: vec4f,
|
|
129
|
+
@location(0) normal: vec3f,
|
|
130
|
+
@location(1) uv: vec2f,
|
|
131
|
+
@location(2) worldPos: vec3f,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
135
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
136
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
137
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
138
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
139
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
140
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
141
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
142
|
+
|
|
143
|
+
@vertex fn vs(
|
|
144
|
+
@location(0) position: vec3f,
|
|
145
|
+
@location(1) normal: vec3f,
|
|
146
|
+
@location(2) uv: vec2f,
|
|
147
|
+
@location(3) joints0: vec4<u32>,
|
|
148
|
+
@location(4) weights0: vec4<f32>
|
|
149
|
+
) -> VertexOutput {
|
|
150
|
+
var output: VertexOutput;
|
|
151
|
+
let pos4 = vec4f(position, 1.0);
|
|
152
|
+
|
|
153
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
154
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
155
|
+
var normalizedWeights: vec4f;
|
|
156
|
+
if (weightSum > 0.0001) {
|
|
157
|
+
normalizedWeights = weights0 / weightSum;
|
|
158
|
+
} else {
|
|
159
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
163
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
164
|
+
for (var i = 0u; i < 4u; i++) {
|
|
165
|
+
let j = joints0[i];
|
|
166
|
+
let w = normalizedWeights[i];
|
|
167
|
+
let m = skinMats[j];
|
|
168
|
+
skinnedPos += (m * pos4) * w;
|
|
169
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
170
|
+
skinnedNrm += (r3 * normal) * w;
|
|
171
|
+
}
|
|
172
|
+
let worldPos = skinnedPos.xyz;
|
|
173
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
174
|
+
output.normal = normalize(skinnedNrm);
|
|
175
|
+
output.uv = uv;
|
|
176
|
+
output.worldPos = worldPos;
|
|
177
|
+
return output;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
181
|
+
let n = normalize(input.normal);
|
|
182
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
183
|
+
|
|
184
|
+
var lightAccum = vec3f(light.ambient);
|
|
185
|
+
let numLights = u32(light.lightCount);
|
|
186
|
+
for (var i = 0u; i < numLights; i++) {
|
|
187
|
+
let l = -light.lights[i].direction;
|
|
188
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
189
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
190
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
191
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
192
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rim light calculation
|
|
196
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
197
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
198
|
+
rimFactor = pow(rimFactor, material.rimPower);
|
|
199
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
200
|
+
|
|
201
|
+
let color = albedo * lightAccum + rimLight;
|
|
202
|
+
|
|
203
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
204
|
+
if (material.isOverEyes > 0.5) {
|
|
205
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (finalAlpha < 0.001) {
|
|
209
|
+
discard;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
213
|
+
}
|
|
206
214
|
`,
|
|
207
215
|
});
|
|
208
216
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -293,77 +301,77 @@ export class Engine {
|
|
|
293
301
|
});
|
|
294
302
|
const outlineShaderModule = this.device.createShaderModule({
|
|
295
303
|
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
|
-
if (material.isOverEyes > 0.5) {
|
|
362
|
-
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return color;
|
|
366
|
-
}
|
|
304
|
+
code: /* wgsl */ `
|
|
305
|
+
struct CameraUniforms {
|
|
306
|
+
view: mat4x4f,
|
|
307
|
+
projection: mat4x4f,
|
|
308
|
+
viewPos: vec3f,
|
|
309
|
+
_padding: f32,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
struct MaterialUniforms {
|
|
313
|
+
edgeColor: vec4f,
|
|
314
|
+
edgeSize: f32,
|
|
315
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
316
|
+
_padding1: f32,
|
|
317
|
+
_padding2: f32,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
321
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
322
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
323
|
+
|
|
324
|
+
struct VertexOutput {
|
|
325
|
+
@builtin(position) position: vec4f,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
@vertex fn vs(
|
|
329
|
+
@location(0) position: vec3f,
|
|
330
|
+
@location(1) normal: vec3f,
|
|
331
|
+
@location(3) joints0: vec4<u32>,
|
|
332
|
+
@location(4) weights0: vec4<f32>
|
|
333
|
+
) -> VertexOutput {
|
|
334
|
+
var output: VertexOutput;
|
|
335
|
+
let pos4 = vec4f(position, 1.0);
|
|
336
|
+
|
|
337
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
338
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
339
|
+
var normalizedWeights: vec4f;
|
|
340
|
+
if (weightSum > 0.0001) {
|
|
341
|
+
normalizedWeights = weights0 / weightSum;
|
|
342
|
+
} else {
|
|
343
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
347
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
348
|
+
for (var i = 0u; i < 4u; i++) {
|
|
349
|
+
let j = joints0[i];
|
|
350
|
+
let w = normalizedWeights[i];
|
|
351
|
+
let m = skinMats[j];
|
|
352
|
+
skinnedPos += (m * pos4) * w;
|
|
353
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
354
|
+
skinnedNrm += (r3 * normal) * w;
|
|
355
|
+
}
|
|
356
|
+
let worldPos = skinnedPos.xyz;
|
|
357
|
+
let worldNormal = normalize(skinnedNrm);
|
|
358
|
+
|
|
359
|
+
// MMD invert hull: expand vertices outward along normals
|
|
360
|
+
let scaleFactor = 0.01;
|
|
361
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
362
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
363
|
+
return output;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
367
|
+
var color = material.edgeColor;
|
|
368
|
+
|
|
369
|
+
if (material.isOverEyes > 0.5) {
|
|
370
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return color;
|
|
374
|
+
}
|
|
367
375
|
`,
|
|
368
376
|
});
|
|
369
377
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -564,49 +572,49 @@ export class Engine {
|
|
|
564
572
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
565
573
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
566
574
|
label: "depth only shader",
|
|
567
|
-
code: /* wgsl */ `
|
|
568
|
-
struct CameraUniforms {
|
|
569
|
-
view: mat4x4f,
|
|
570
|
-
projection: mat4x4f,
|
|
571
|
-
viewPos: vec3f,
|
|
572
|
-
_padding: f32,
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
576
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
577
|
-
|
|
578
|
-
@vertex fn vs(
|
|
579
|
-
@location(0) position: vec3f,
|
|
580
|
-
@location(1) normal: vec3f,
|
|
581
|
-
@location(3) joints0: vec4<u32>,
|
|
582
|
-
@location(4) weights0: vec4<f32>
|
|
583
|
-
) -> @builtin(position) vec4f {
|
|
584
|
-
let pos4 = vec4f(position, 1.0);
|
|
585
|
-
|
|
586
|
-
// Normalize weights
|
|
587
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
588
|
-
var normalizedWeights: vec4f;
|
|
589
|
-
if (weightSum > 0.0001) {
|
|
590
|
-
normalizedWeights = weights0 / weightSum;
|
|
591
|
-
} else {
|
|
592
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
596
|
-
for (var i = 0u; i < 4u; i++) {
|
|
597
|
-
let j = joints0[i];
|
|
598
|
-
let w = normalizedWeights[i];
|
|
599
|
-
let m = skinMats[j];
|
|
600
|
-
skinnedPos += (m * pos4) * w;
|
|
601
|
-
}
|
|
602
|
-
let worldPos = skinnedPos.xyz;
|
|
603
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
604
|
-
return clipPos;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
608
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
609
|
-
}
|
|
575
|
+
code: /* wgsl */ `
|
|
576
|
+
struct CameraUniforms {
|
|
577
|
+
view: mat4x4f,
|
|
578
|
+
projection: mat4x4f,
|
|
579
|
+
viewPos: vec3f,
|
|
580
|
+
_padding: f32,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
584
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
585
|
+
|
|
586
|
+
@vertex fn vs(
|
|
587
|
+
@location(0) position: vec3f,
|
|
588
|
+
@location(1) normal: vec3f,
|
|
589
|
+
@location(3) joints0: vec4<u32>,
|
|
590
|
+
@location(4) weights0: vec4<f32>
|
|
591
|
+
) -> @builtin(position) vec4f {
|
|
592
|
+
let pos4 = vec4f(position, 1.0);
|
|
593
|
+
|
|
594
|
+
// Normalize weights
|
|
595
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
596
|
+
var normalizedWeights: vec4f;
|
|
597
|
+
if (weightSum > 0.0001) {
|
|
598
|
+
normalizedWeights = weights0 / weightSum;
|
|
599
|
+
} else {
|
|
600
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
604
|
+
for (var i = 0u; i < 4u; i++) {
|
|
605
|
+
let j = joints0[i];
|
|
606
|
+
let w = normalizedWeights[i];
|
|
607
|
+
let m = skinMats[j];
|
|
608
|
+
skinnedPos += (m * pos4) * w;
|
|
609
|
+
}
|
|
610
|
+
let worldPos = skinnedPos.xyz;
|
|
611
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
612
|
+
return clipPos;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
616
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
617
|
+
}
|
|
610
618
|
`,
|
|
611
619
|
});
|
|
612
620
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -786,31 +794,31 @@ export class Engine {
|
|
|
786
794
|
createSkinMatrixComputePipeline() {
|
|
787
795
|
const computeShader = this.device.createShaderModule({
|
|
788
796
|
label: "skin matrix compute",
|
|
789
|
-
code: /* wgsl */ `
|
|
790
|
-
struct BoneCountUniform {
|
|
791
|
-
count: u32,
|
|
792
|
-
_padding1: u32,
|
|
793
|
-
_padding2: u32,
|
|
794
|
-
_padding3: u32,
|
|
795
|
-
_padding4: vec4<u32>,
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
799
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
800
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
801
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
802
|
-
|
|
803
|
-
@compute @workgroup_size(64)
|
|
804
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
805
|
-
let boneIndex = globalId.x;
|
|
806
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
807
|
-
if (boneIndex >= boneCount.count) {
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
let worldMat = worldMatrices[boneIndex];
|
|
811
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
812
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
813
|
-
}
|
|
797
|
+
code: /* wgsl */ `
|
|
798
|
+
struct BoneCountUniform {
|
|
799
|
+
count: u32,
|
|
800
|
+
_padding1: u32,
|
|
801
|
+
_padding2: u32,
|
|
802
|
+
_padding3: u32,
|
|
803
|
+
_padding4: vec4<u32>,
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
807
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
808
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
809
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
810
|
+
|
|
811
|
+
@compute @workgroup_size(64)
|
|
812
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
813
|
+
let boneIndex = globalId.x;
|
|
814
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
815
|
+
if (boneIndex >= boneCount.count) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
let worldMat = worldMatrices[boneIndex];
|
|
819
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
820
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
821
|
+
}
|
|
814
822
|
`,
|
|
815
823
|
});
|
|
816
824
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -864,143 +872,143 @@ export class Engine {
|
|
|
864
872
|
// Bloom extraction shader (extracts bright areas)
|
|
865
873
|
const bloomExtractShader = this.device.createShaderModule({
|
|
866
874
|
label: "bloom extract",
|
|
867
|
-
code: /* wgsl */ `
|
|
868
|
-
struct VertexOutput {
|
|
869
|
-
@builtin(position) position: vec4f,
|
|
870
|
-
@location(0) uv: vec2f,
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
874
|
-
var output: VertexOutput;
|
|
875
|
-
// Generate fullscreen quad from vertex index
|
|
876
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
877
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
878
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
879
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
880
|
-
return output;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
struct BloomExtractUniforms {
|
|
884
|
-
threshold: f32,
|
|
885
|
-
_padding1: f32,
|
|
886
|
-
_padding2: f32,
|
|
887
|
-
_padding3: f32,
|
|
888
|
-
_padding4: f32,
|
|
889
|
-
_padding5: f32,
|
|
890
|
-
_padding6: f32,
|
|
891
|
-
_padding7: f32,
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
895
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
896
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
897
|
-
|
|
898
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
899
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
900
|
-
// Extract bright areas above threshold
|
|
901
|
-
let threshold = extractUniforms.threshold;
|
|
902
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
903
|
-
return vec4f(bloom, color.a);
|
|
904
|
-
}
|
|
875
|
+
code: /* wgsl */ `
|
|
876
|
+
struct VertexOutput {
|
|
877
|
+
@builtin(position) position: vec4f,
|
|
878
|
+
@location(0) uv: vec2f,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
882
|
+
var output: VertexOutput;
|
|
883
|
+
// Generate fullscreen quad from vertex index
|
|
884
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
885
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
886
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
887
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
888
|
+
return output;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
struct BloomExtractUniforms {
|
|
892
|
+
threshold: f32,
|
|
893
|
+
_padding1: f32,
|
|
894
|
+
_padding2: f32,
|
|
895
|
+
_padding3: f32,
|
|
896
|
+
_padding4: f32,
|
|
897
|
+
_padding5: f32,
|
|
898
|
+
_padding6: f32,
|
|
899
|
+
_padding7: f32,
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
903
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
904
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
905
|
+
|
|
906
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
907
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
908
|
+
// Extract bright areas above threshold
|
|
909
|
+
let threshold = extractUniforms.threshold;
|
|
910
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
911
|
+
return vec4f(bloom, color.a);
|
|
912
|
+
}
|
|
905
913
|
`,
|
|
906
914
|
});
|
|
907
915
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
908
916
|
const bloomBlurShader = this.device.createShaderModule({
|
|
909
917
|
label: "bloom blur",
|
|
910
|
-
code: /* wgsl */ `
|
|
911
|
-
struct VertexOutput {
|
|
912
|
-
@builtin(position) position: vec4f,
|
|
913
|
-
@location(0) uv: vec2f,
|
|
914
|
-
};
|
|
915
|
-
|
|
916
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
917
|
-
var output: VertexOutput;
|
|
918
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
919
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
920
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
921
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
922
|
-
return output;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
struct BlurUniforms {
|
|
926
|
-
direction: vec2f,
|
|
927
|
-
_padding1: f32,
|
|
928
|
-
_padding2: f32,
|
|
929
|
-
_padding3: f32,
|
|
930
|
-
_padding4: f32,
|
|
931
|
-
_padding5: f32,
|
|
932
|
-
_padding6: f32,
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
936
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
937
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
938
|
-
|
|
939
|
-
// 9-tap gaussian blur
|
|
940
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
941
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
942
|
-
var result = vec4f(0.0);
|
|
943
|
-
|
|
944
|
-
// Gaussian weights for 9-tap filter
|
|
945
|
-
let weights = array<f32, 9>(
|
|
946
|
-
0.01621622, 0.05405405, 0.12162162,
|
|
947
|
-
0.19459459, 0.22702703,
|
|
948
|
-
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
952
|
-
|
|
953
|
-
for (var i = 0u; i < 9u; i++) {
|
|
954
|
-
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
955
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
return result;
|
|
959
|
-
}
|
|
918
|
+
code: /* wgsl */ `
|
|
919
|
+
struct VertexOutput {
|
|
920
|
+
@builtin(position) position: vec4f,
|
|
921
|
+
@location(0) uv: vec2f,
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
925
|
+
var output: VertexOutput;
|
|
926
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
927
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
928
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
929
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
930
|
+
return output;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
struct BlurUniforms {
|
|
934
|
+
direction: vec2f,
|
|
935
|
+
_padding1: f32,
|
|
936
|
+
_padding2: f32,
|
|
937
|
+
_padding3: f32,
|
|
938
|
+
_padding4: f32,
|
|
939
|
+
_padding5: f32,
|
|
940
|
+
_padding6: f32,
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
944
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
945
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
946
|
+
|
|
947
|
+
// 9-tap gaussian blur
|
|
948
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
949
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
950
|
+
var result = vec4f(0.0);
|
|
951
|
+
|
|
952
|
+
// Gaussian weights for 9-tap filter
|
|
953
|
+
let weights = array<f32, 9>(
|
|
954
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
955
|
+
0.19459459, 0.22702703,
|
|
956
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
960
|
+
|
|
961
|
+
for (var i = 0u; i < 9u; i++) {
|
|
962
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
963
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
960
968
|
`,
|
|
961
969
|
});
|
|
962
970
|
// Bloom composition shader (combines original scene with bloom)
|
|
963
971
|
const bloomComposeShader = this.device.createShaderModule({
|
|
964
972
|
label: "bloom compose",
|
|
965
|
-
code: /* wgsl */ `
|
|
966
|
-
struct VertexOutput {
|
|
967
|
-
@builtin(position) position: vec4f,
|
|
968
|
-
@location(0) uv: vec2f,
|
|
969
|
-
};
|
|
970
|
-
|
|
971
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
972
|
-
var output: VertexOutput;
|
|
973
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
974
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
975
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
976
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
977
|
-
return output;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
struct BloomComposeUniforms {
|
|
981
|
-
intensity: f32,
|
|
982
|
-
_padding1: f32,
|
|
983
|
-
_padding2: f32,
|
|
984
|
-
_padding3: f32,
|
|
985
|
-
_padding4: f32,
|
|
986
|
-
_padding5: f32,
|
|
987
|
-
_padding6: f32,
|
|
988
|
-
_padding7: f32,
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
992
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
993
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
994
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
995
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
996
|
-
|
|
997
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
998
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
999
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1000
|
-
// Additive blending with intensity control
|
|
1001
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1002
|
-
return vec4f(result, scene.a);
|
|
1003
|
-
}
|
|
973
|
+
code: /* wgsl */ `
|
|
974
|
+
struct VertexOutput {
|
|
975
|
+
@builtin(position) position: vec4f,
|
|
976
|
+
@location(0) uv: vec2f,
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
980
|
+
var output: VertexOutput;
|
|
981
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
982
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
983
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
984
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
985
|
+
return output;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
struct BloomComposeUniforms {
|
|
989
|
+
intensity: f32,
|
|
990
|
+
_padding1: f32,
|
|
991
|
+
_padding2: f32,
|
|
992
|
+
_padding3: f32,
|
|
993
|
+
_padding4: f32,
|
|
994
|
+
_padding5: f32,
|
|
995
|
+
_padding6: f32,
|
|
996
|
+
_padding7: f32,
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1000
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1001
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1002
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1003
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1004
|
+
|
|
1005
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1006
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1007
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1008
|
+
// Additive blending with intensity control
|
|
1009
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1010
|
+
return vec4f(result, scene.a);
|
|
1011
|
+
}
|
|
1004
1012
|
`,
|
|
1005
1013
|
});
|
|
1006
1014
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1235,7 +1243,7 @@ export class Engine {
|
|
|
1235
1243
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1236
1244
|
});
|
|
1237
1245
|
this.lightCount = 0;
|
|
1238
|
-
this.setAmbient(
|
|
1246
|
+
this.setAmbient(this.ambient);
|
|
1239
1247
|
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.02);
|
|
1240
1248
|
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.015);
|
|
1241
1249
|
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.01);
|
|
@@ -1557,9 +1565,9 @@ export class Engine {
|
|
|
1557
1565
|
materialUniformData[1] = 1.0; // alphaMultiplier: 1.0 for non-hair materials
|
|
1558
1566
|
materialUniformData[2] = this.rimLightIntensity;
|
|
1559
1567
|
materialUniformData[3] = this.rimLightPower;
|
|
1560
|
-
materialUniformData[4] =
|
|
1561
|
-
materialUniformData[5] =
|
|
1562
|
-
materialUniformData[6] =
|
|
1568
|
+
materialUniformData[4] = 1.0; // rimColor.r
|
|
1569
|
+
materialUniformData[5] = 1.0; // rimColor.g
|
|
1570
|
+
materialUniformData[6] = 1.0; // rimColor.b
|
|
1563
1571
|
materialUniformData[7] = 0.0;
|
|
1564
1572
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1565
1573
|
label: `material uniform: ${mat.name}`,
|
|
@@ -1598,9 +1606,9 @@ export class Engine {
|
|
|
1598
1606
|
materialUniformDataHair[1] = 1.0; // alphaMultiplier: base value, shader will adjust
|
|
1599
1607
|
materialUniformDataHair[2] = this.rimLightIntensity;
|
|
1600
1608
|
materialUniformDataHair[3] = this.rimLightPower;
|
|
1601
|
-
materialUniformDataHair[4] =
|
|
1602
|
-
materialUniformDataHair[5] =
|
|
1603
|
-
materialUniformDataHair[6] =
|
|
1609
|
+
materialUniformDataHair[4] = 1.0; // rimColor.r
|
|
1610
|
+
materialUniformDataHair[5] = 1.0; // rimColor.g
|
|
1611
|
+
materialUniformDataHair[6] = 1.0; // rimColor.b
|
|
1604
1612
|
materialUniformDataHair[7] = 0.0;
|
|
1605
1613
|
// Create uniform buffers for both modes
|
|
1606
1614
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|