reze-engine 0.2.14 → 0.2.15
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 +71 -71
- package/dist/engine.d.ts +3 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +385 -371
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +2544 -2527
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1054 -1054
- package/src/vmd-loader.ts +179 -179
- package/dist/pool.d.ts +0 -38
- package/dist/pool.d.ts.map +0 -1
- package/dist/pool.js +0 -422
package/dist/engine.js
CHANGED
|
@@ -18,6 +18,7 @@ export class Engine {
|
|
|
18
18
|
this.BLOOM_DOWNSCALE_FACTOR = 2;
|
|
19
19
|
// Ambient light settings
|
|
20
20
|
this.ambient = 1.0;
|
|
21
|
+
this.ambientColor = new Vec3(1.0, 1.0, 1.0);
|
|
21
22
|
// Bloom settings
|
|
22
23
|
this.bloomThreshold = 0.3;
|
|
23
24
|
this.bloomIntensity = 0.12;
|
|
@@ -60,6 +61,7 @@ export class Engine {
|
|
|
60
61
|
this.canvas = canvas;
|
|
61
62
|
if (options) {
|
|
62
63
|
this.ambient = options.ambient ?? 1.0;
|
|
64
|
+
this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
|
|
63
65
|
this.bloomIntensity = options.bloomIntensity ?? 0.12;
|
|
64
66
|
this.rimLightIntensity = options.rimLightIntensity ?? 0.45;
|
|
65
67
|
this.cameraDistance = options.cameraDistance ?? 26.6;
|
|
@@ -101,121 +103,122 @@ export class Engine {
|
|
|
101
103
|
});
|
|
102
104
|
const shaderModule = this.device.createShaderModule({
|
|
103
105
|
label: "model shaders",
|
|
104
|
-
code: /* wgsl */ `
|
|
105
|
-
struct CameraUniforms {
|
|
106
|
-
view: mat4x4f,
|
|
107
|
-
projection: mat4x4f,
|
|
108
|
-
viewPos: vec3f,
|
|
109
|
-
_padding: f32,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
struct Light {
|
|
113
|
-
direction: vec3f,
|
|
114
|
-
_padding1: f32,
|
|
115
|
-
color: vec3f,
|
|
116
|
-
intensity: f32,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
struct LightUniforms {
|
|
120
|
-
ambient: f32,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@
|
|
139
|
-
@location(
|
|
140
|
-
@location(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@group(0) @binding(
|
|
145
|
-
@group(0) @binding(
|
|
146
|
-
@group(0) @binding(
|
|
147
|
-
@group(0) @binding(
|
|
148
|
-
@group(0) @binding(
|
|
149
|
-
@group(0) @binding(
|
|
150
|
-
@group(0) @binding(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@location(
|
|
155
|
-
@location(
|
|
156
|
-
@location(
|
|
157
|
-
@location(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let
|
|
165
|
-
let
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
var
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
output.
|
|
180
|
-
output.
|
|
181
|
-
output.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let
|
|
203
|
-
let
|
|
204
|
-
let
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
rimFactor =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
106
|
+
code: /* wgsl */ `
|
|
107
|
+
struct CameraUniforms {
|
|
108
|
+
view: mat4x4f,
|
|
109
|
+
projection: mat4x4f,
|
|
110
|
+
viewPos: vec3f,
|
|
111
|
+
_padding: f32,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
struct Light {
|
|
115
|
+
direction: vec3f,
|
|
116
|
+
_padding1: f32,
|
|
117
|
+
color: vec3f,
|
|
118
|
+
intensity: f32,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
struct LightUniforms {
|
|
122
|
+
ambient: f32,
|
|
123
|
+
ambientColor: vec3f,
|
|
124
|
+
lightCount: f32,
|
|
125
|
+
_padding1: f32,
|
|
126
|
+
_padding2: f32,
|
|
127
|
+
lights: array<Light, 4>,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
struct MaterialUniforms {
|
|
131
|
+
alpha: f32,
|
|
132
|
+
alphaMultiplier: f32,
|
|
133
|
+
rimIntensity: f32,
|
|
134
|
+
_padding1: f32,
|
|
135
|
+
rimColor: vec3f,
|
|
136
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
struct VertexOutput {
|
|
140
|
+
@builtin(position) position: vec4f,
|
|
141
|
+
@location(0) normal: vec3f,
|
|
142
|
+
@location(1) uv: vec2f,
|
|
143
|
+
@location(2) worldPos: vec3f,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
147
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
148
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
149
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
150
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
151
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
152
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
153
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
154
|
+
|
|
155
|
+
@vertex fn vs(
|
|
156
|
+
@location(0) position: vec3f,
|
|
157
|
+
@location(1) normal: vec3f,
|
|
158
|
+
@location(2) uv: vec2f,
|
|
159
|
+
@location(3) joints0: vec4<u32>,
|
|
160
|
+
@location(4) weights0: vec4<f32>
|
|
161
|
+
) -> VertexOutput {
|
|
162
|
+
var output: VertexOutput;
|
|
163
|
+
let pos4 = vec4f(position, 1.0);
|
|
164
|
+
|
|
165
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
166
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
167
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
168
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
169
|
+
|
|
170
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
171
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
172
|
+
for (var i = 0u; i < 4u; i++) {
|
|
173
|
+
let j = joints0[i];
|
|
174
|
+
let w = normalizedWeights[i];
|
|
175
|
+
let m = skinMats[j];
|
|
176
|
+
skinnedPos += (m * pos4) * w;
|
|
177
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
178
|
+
skinnedNrm += (r3 * normal) * w;
|
|
179
|
+
}
|
|
180
|
+
let worldPos = skinnedPos.xyz;
|
|
181
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
182
|
+
output.normal = normalize(skinnedNrm);
|
|
183
|
+
output.uv = uv;
|
|
184
|
+
output.worldPos = worldPos;
|
|
185
|
+
return output;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
189
|
+
// Early alpha test - discard before expensive calculations
|
|
190
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
191
|
+
if (material.isOverEyes > 0.5) {
|
|
192
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
193
|
+
}
|
|
194
|
+
if (finalAlpha < 0.001) {
|
|
195
|
+
discard;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let n = normalize(input.normal);
|
|
199
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
200
|
+
|
|
201
|
+
var lightAccum = light.ambient * light.ambientColor;
|
|
202
|
+
let numLights = u32(light.lightCount);
|
|
203
|
+
for (var i = 0u; i < numLights; i++) {
|
|
204
|
+
let l = -light.lights[i].direction;
|
|
205
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
206
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
207
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
208
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
209
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Rim light calculation
|
|
213
|
+
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
214
|
+
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
215
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
216
|
+
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
217
|
+
|
|
218
|
+
let color = albedo * lightAccum + rimLight;
|
|
219
|
+
|
|
220
|
+
return vec4f(color, finalAlpha);
|
|
221
|
+
}
|
|
219
222
|
`,
|
|
220
223
|
});
|
|
221
224
|
// Create explicit bind group layout for all pipelines using the main shader
|
|
@@ -305,73 +308,73 @@ export class Engine {
|
|
|
305
308
|
});
|
|
306
309
|
const outlineShaderModule = this.device.createShaderModule({
|
|
307
310
|
label: "outline shaders",
|
|
308
|
-
code: /* wgsl */ `
|
|
309
|
-
struct CameraUniforms {
|
|
310
|
-
view: mat4x4f,
|
|
311
|
-
projection: mat4x4f,
|
|
312
|
-
viewPos: vec3f,
|
|
313
|
-
_padding: f32,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
struct MaterialUniforms {
|
|
317
|
-
edgeColor: vec4f,
|
|
318
|
-
edgeSize: f32,
|
|
319
|
-
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
320
|
-
_padding1: f32,
|
|
321
|
-
_padding2: f32,
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
325
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
326
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
327
|
-
|
|
328
|
-
struct VertexOutput {
|
|
329
|
-
@builtin(position) position: vec4f,
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
@vertex fn vs(
|
|
333
|
-
@location(0) position: vec3f,
|
|
334
|
-
@location(1) normal: vec3f,
|
|
335
|
-
@location(3) joints0: vec4<u32>,
|
|
336
|
-
@location(4) weights0: vec4<f32>
|
|
337
|
-
) -> VertexOutput {
|
|
338
|
-
var output: VertexOutput;
|
|
339
|
-
let pos4 = vec4f(position, 1.0);
|
|
340
|
-
|
|
341
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
342
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
343
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
344
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
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
|
-
}
|
|
311
|
+
code: /* wgsl */ `
|
|
312
|
+
struct CameraUniforms {
|
|
313
|
+
view: mat4x4f,
|
|
314
|
+
projection: mat4x4f,
|
|
315
|
+
viewPos: vec3f,
|
|
316
|
+
_padding: f32,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
struct MaterialUniforms {
|
|
320
|
+
edgeColor: vec4f,
|
|
321
|
+
edgeSize: f32,
|
|
322
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
323
|
+
_padding1: f32,
|
|
324
|
+
_padding2: f32,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
328
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
329
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
330
|
+
|
|
331
|
+
struct VertexOutput {
|
|
332
|
+
@builtin(position) position: vec4f,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
@vertex fn vs(
|
|
336
|
+
@location(0) position: vec3f,
|
|
337
|
+
@location(1) normal: vec3f,
|
|
338
|
+
@location(3) joints0: vec4<u32>,
|
|
339
|
+
@location(4) weights0: vec4<f32>
|
|
340
|
+
) -> VertexOutput {
|
|
341
|
+
var output: VertexOutput;
|
|
342
|
+
let pos4 = vec4f(position, 1.0);
|
|
343
|
+
|
|
344
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
345
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
346
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
347
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
348
|
+
|
|
349
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
350
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
351
|
+
for (var i = 0u; i < 4u; i++) {
|
|
352
|
+
let j = joints0[i];
|
|
353
|
+
let w = normalizedWeights[i];
|
|
354
|
+
let m = skinMats[j];
|
|
355
|
+
skinnedPos += (m * pos4) * w;
|
|
356
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
357
|
+
skinnedNrm += (r3 * normal) * w;
|
|
358
|
+
}
|
|
359
|
+
let worldPos = skinnedPos.xyz;
|
|
360
|
+
let worldNormal = normalize(skinnedNrm);
|
|
361
|
+
|
|
362
|
+
// MMD invert hull: expand vertices outward along normals
|
|
363
|
+
let scaleFactor = 0.01;
|
|
364
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
365
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
366
|
+
return output;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
370
|
+
var color = material.edgeColor;
|
|
371
|
+
|
|
372
|
+
if (material.isOverEyes > 0.5) {
|
|
373
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return color;
|
|
377
|
+
}
|
|
375
378
|
`,
|
|
376
379
|
});
|
|
377
380
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -575,45 +578,45 @@ export class Engine {
|
|
|
575
578
|
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
576
579
|
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
577
580
|
label: "depth only shader",
|
|
578
|
-
code: /* wgsl */ `
|
|
579
|
-
struct CameraUniforms {
|
|
580
|
-
view: mat4x4f,
|
|
581
|
-
projection: mat4x4f,
|
|
582
|
-
viewPos: vec3f,
|
|
583
|
-
_padding: f32,
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
587
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
588
|
-
|
|
589
|
-
@vertex fn vs(
|
|
590
|
-
@location(0) position: vec3f,
|
|
591
|
-
@location(1) normal: vec3f,
|
|
592
|
-
@location(3) joints0: vec4<u32>,
|
|
593
|
-
@location(4) weights0: vec4<f32>
|
|
594
|
-
) -> @builtin(position) vec4f {
|
|
595
|
-
let pos4 = vec4f(position, 1.0);
|
|
596
|
-
|
|
597
|
-
// Branchless weight normalization (avoids GPU branch divergence)
|
|
598
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
599
|
-
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
600
|
-
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
601
|
-
|
|
602
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
603
|
-
for (var i = 0u; i < 4u; i++) {
|
|
604
|
-
let j = joints0[i];
|
|
605
|
-
let w = normalizedWeights[i];
|
|
606
|
-
let m = skinMats[j];
|
|
607
|
-
skinnedPos += (m * pos4) * w;
|
|
608
|
-
}
|
|
609
|
-
let worldPos = skinnedPos.xyz;
|
|
610
|
-
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
611
|
-
return clipPos;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
615
|
-
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
616
|
-
}
|
|
581
|
+
code: /* wgsl */ `
|
|
582
|
+
struct CameraUniforms {
|
|
583
|
+
view: mat4x4f,
|
|
584
|
+
projection: mat4x4f,
|
|
585
|
+
viewPos: vec3f,
|
|
586
|
+
_padding: f32,
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
590
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
591
|
+
|
|
592
|
+
@vertex fn vs(
|
|
593
|
+
@location(0) position: vec3f,
|
|
594
|
+
@location(1) normal: vec3f,
|
|
595
|
+
@location(3) joints0: vec4<u32>,
|
|
596
|
+
@location(4) weights0: vec4<f32>
|
|
597
|
+
) -> @builtin(position) vec4f {
|
|
598
|
+
let pos4 = vec4f(position, 1.0);
|
|
599
|
+
|
|
600
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
601
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
602
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
603
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
604
|
+
|
|
605
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
606
|
+
for (var i = 0u; i < 4u; i++) {
|
|
607
|
+
let j = joints0[i];
|
|
608
|
+
let w = normalizedWeights[i];
|
|
609
|
+
let m = skinMats[j];
|
|
610
|
+
skinnedPos += (m * pos4) * w;
|
|
611
|
+
}
|
|
612
|
+
let worldPos = skinnedPos.xyz;
|
|
613
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
614
|
+
return clipPos;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
618
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
619
|
+
}
|
|
617
620
|
`,
|
|
618
621
|
});
|
|
619
622
|
// Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
|
|
@@ -796,30 +799,30 @@ export class Engine {
|
|
|
796
799
|
createSkinMatrixComputePipeline() {
|
|
797
800
|
const computeShader = this.device.createShaderModule({
|
|
798
801
|
label: "skin matrix compute",
|
|
799
|
-
code: /* wgsl */ `
|
|
800
|
-
struct BoneCountUniform {
|
|
801
|
-
count: u32,
|
|
802
|
-
_padding1: u32,
|
|
803
|
-
_padding2: u32,
|
|
804
|
-
_padding3: u32,
|
|
805
|
-
_padding4: vec4<u32>,
|
|
806
|
-
};
|
|
807
|
-
|
|
808
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
809
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
810
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
811
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
812
|
-
|
|
813
|
-
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
814
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
815
|
-
let boneIndex = globalId.x;
|
|
816
|
-
if (boneIndex >= boneCount.count) {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
let worldMat = worldMatrices[boneIndex];
|
|
820
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
821
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
822
|
-
}
|
|
802
|
+
code: /* wgsl */ `
|
|
803
|
+
struct BoneCountUniform {
|
|
804
|
+
count: u32,
|
|
805
|
+
_padding1: u32,
|
|
806
|
+
_padding2: u32,
|
|
807
|
+
_padding3: u32,
|
|
808
|
+
_padding4: vec4<u32>,
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
812
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
813
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
814
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
815
|
+
|
|
816
|
+
@compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
|
|
817
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
818
|
+
let boneIndex = globalId.x;
|
|
819
|
+
if (boneIndex >= boneCount.count) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
let worldMat = worldMatrices[boneIndex];
|
|
823
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
824
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
825
|
+
}
|
|
823
826
|
`,
|
|
824
827
|
});
|
|
825
828
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -873,140 +876,140 @@ export class Engine {
|
|
|
873
876
|
// Bloom extraction shader (extracts bright areas)
|
|
874
877
|
const bloomExtractShader = this.device.createShaderModule({
|
|
875
878
|
label: "bloom extract",
|
|
876
|
-
code: /* wgsl */ `
|
|
877
|
-
struct VertexOutput {
|
|
878
|
-
@builtin(position) position: vec4f,
|
|
879
|
-
@location(0) uv: vec2f,
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
883
|
-
var output: VertexOutput;
|
|
884
|
-
// Generate fullscreen quad from vertex index
|
|
885
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
886
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
887
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
888
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
889
|
-
return output;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
struct BloomExtractUniforms {
|
|
893
|
-
threshold: f32,
|
|
894
|
-
_padding1: f32,
|
|
895
|
-
_padding2: f32,
|
|
896
|
-
_padding3: f32,
|
|
897
|
-
_padding4: f32,
|
|
898
|
-
_padding5: f32,
|
|
899
|
-
_padding6: f32,
|
|
900
|
-
_padding7: f32,
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
904
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
905
|
-
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
906
|
-
|
|
907
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
908
|
-
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
909
|
-
// Extract bright areas above threshold
|
|
910
|
-
let threshold = extractUniforms.threshold;
|
|
911
|
-
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
912
|
-
return vec4f(bloom, color.a);
|
|
913
|
-
}
|
|
879
|
+
code: /* wgsl */ `
|
|
880
|
+
struct VertexOutput {
|
|
881
|
+
@builtin(position) position: vec4f,
|
|
882
|
+
@location(0) uv: vec2f,
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
886
|
+
var output: VertexOutput;
|
|
887
|
+
// Generate fullscreen quad from vertex index
|
|
888
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
889
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
890
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
891
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
892
|
+
return output;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
struct BloomExtractUniforms {
|
|
896
|
+
threshold: f32,
|
|
897
|
+
_padding1: f32,
|
|
898
|
+
_padding2: f32,
|
|
899
|
+
_padding3: f32,
|
|
900
|
+
_padding4: f32,
|
|
901
|
+
_padding5: f32,
|
|
902
|
+
_padding6: f32,
|
|
903
|
+
_padding7: f32,
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
907
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
908
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
909
|
+
|
|
910
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
911
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
912
|
+
// Extract bright areas above threshold
|
|
913
|
+
let threshold = extractUniforms.threshold;
|
|
914
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
915
|
+
return vec4f(bloom, color.a);
|
|
916
|
+
}
|
|
914
917
|
`,
|
|
915
918
|
});
|
|
916
919
|
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
917
920
|
const bloomBlurShader = this.device.createShaderModule({
|
|
918
921
|
label: "bloom blur",
|
|
919
|
-
code: /* wgsl */ `
|
|
920
|
-
struct VertexOutput {
|
|
921
|
-
@builtin(position) position: vec4f,
|
|
922
|
-
@location(0) uv: vec2f,
|
|
923
|
-
};
|
|
924
|
-
|
|
925
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
926
|
-
var output: VertexOutput;
|
|
927
|
-
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
928
|
-
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
929
|
-
output.position = vec4f(x, y, 0.0, 1.0);
|
|
930
|
-
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
931
|
-
return output;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
struct BlurUniforms {
|
|
935
|
-
direction: vec2f,
|
|
936
|
-
_padding1: f32,
|
|
937
|
-
_padding2: f32,
|
|
938
|
-
_padding3: f32,
|
|
939
|
-
_padding4: f32,
|
|
940
|
-
_padding5: f32,
|
|
941
|
-
_padding6: f32,
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
945
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
946
|
-
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
947
|
-
|
|
948
|
-
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
949
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
950
|
-
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
951
|
-
|
|
952
|
-
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
953
|
-
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
954
|
-
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
955
|
-
let weight0 = 0.38774; // Center sample
|
|
956
|
-
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
957
|
-
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
958
|
-
|
|
959
|
-
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
960
|
-
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
961
|
-
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
962
|
-
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
963
|
-
|
|
964
|
-
return result;
|
|
965
|
-
}
|
|
922
|
+
code: /* wgsl */ `
|
|
923
|
+
struct VertexOutput {
|
|
924
|
+
@builtin(position) position: vec4f,
|
|
925
|
+
@location(0) uv: vec2f,
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
929
|
+
var output: VertexOutput;
|
|
930
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
931
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
932
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
933
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
934
|
+
return output;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
struct BlurUniforms {
|
|
938
|
+
direction: vec2f,
|
|
939
|
+
_padding1: f32,
|
|
940
|
+
_padding2: f32,
|
|
941
|
+
_padding3: f32,
|
|
942
|
+
_padding4: f32,
|
|
943
|
+
_padding5: f32,
|
|
944
|
+
_padding6: f32,
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
948
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
949
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
950
|
+
|
|
951
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
952
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
953
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
954
|
+
|
|
955
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
956
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
957
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
958
|
+
let weight0 = 0.38774; // Center sample
|
|
959
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
960
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
961
|
+
|
|
962
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
963
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
964
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
965
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
966
|
+
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
966
969
|
`,
|
|
967
970
|
});
|
|
968
971
|
// Bloom composition shader (combines original scene with bloom)
|
|
969
972
|
const bloomComposeShader = this.device.createShaderModule({
|
|
970
973
|
label: "bloom compose",
|
|
971
|
-
code: /* wgsl */ `
|
|
972
|
-
struct VertexOutput {
|
|
973
|
-
@builtin(position) position: vec4f,
|
|
974
|
-
@location(0) uv: vec2f,
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
978
|
-
var output: VertexOutput;
|
|
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 BloomComposeUniforms {
|
|
987
|
-
intensity: 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 sceneTexture: texture_2d<f32>;
|
|
998
|
-
@group(0) @binding(1) var sceneSampler: sampler;
|
|
999
|
-
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1000
|
-
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1001
|
-
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1002
|
-
|
|
1003
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1004
|
-
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1005
|
-
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1006
|
-
// Additive blending with intensity control
|
|
1007
|
-
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1008
|
-
return vec4f(result, scene.a);
|
|
1009
|
-
}
|
|
974
|
+
code: /* wgsl */ `
|
|
975
|
+
struct VertexOutput {
|
|
976
|
+
@builtin(position) position: vec4f,
|
|
977
|
+
@location(0) uv: vec2f,
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
981
|
+
var output: VertexOutput;
|
|
982
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
983
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
984
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
985
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
986
|
+
return output;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
struct BloomComposeUniforms {
|
|
990
|
+
intensity: f32,
|
|
991
|
+
_padding1: f32,
|
|
992
|
+
_padding2: f32,
|
|
993
|
+
_padding3: f32,
|
|
994
|
+
_padding4: f32,
|
|
995
|
+
_padding5: f32,
|
|
996
|
+
_padding6: f32,
|
|
997
|
+
_padding7: f32,
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1001
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1002
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1003
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1004
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1005
|
+
|
|
1006
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1007
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1008
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1009
|
+
// Additive blending with intensity control
|
|
1010
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1011
|
+
return vec4f(result, scene.a);
|
|
1012
|
+
}
|
|
1010
1013
|
`,
|
|
1011
1014
|
});
|
|
1012
1015
|
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
@@ -1240,6 +1243,7 @@ export class Engine {
|
|
|
1240
1243
|
});
|
|
1241
1244
|
this.lightCount = 0;
|
|
1242
1245
|
this.setAmbient(this.ambient);
|
|
1246
|
+
this.setAmbientColor(this.ambientColor);
|
|
1243
1247
|
this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.02);
|
|
1244
1248
|
this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.015);
|
|
1245
1249
|
this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.01);
|
|
@@ -1249,7 +1253,7 @@ export class Engine {
|
|
|
1249
1253
|
if (this.lightCount >= 4)
|
|
1250
1254
|
return false;
|
|
1251
1255
|
const normalized = direction.normalize();
|
|
1252
|
-
const baseIndex =
|
|
1256
|
+
const baseIndex = 12 + this.lightCount * 8;
|
|
1253
1257
|
this.lightData[baseIndex] = normalized.x;
|
|
1254
1258
|
this.lightData[baseIndex + 1] = normalized.y;
|
|
1255
1259
|
this.lightData[baseIndex + 2] = normalized.z;
|
|
@@ -1259,12 +1263,22 @@ export class Engine {
|
|
|
1259
1263
|
this.lightData[baseIndex + 6] = color.z;
|
|
1260
1264
|
this.lightData[baseIndex + 7] = intensity;
|
|
1261
1265
|
this.lightCount++;
|
|
1262
|
-
|
|
1266
|
+
// lightCount: f32 at offset 28 (index 7)
|
|
1267
|
+
// Layout: ambient (0), padding (1-3), ambientColor (4-6, padding 7), lightCount (8), _padding1 (9), _padding2 (10), lights start at 12
|
|
1268
|
+
this.lightData[8] = this.lightCount;
|
|
1263
1269
|
return true;
|
|
1264
1270
|
}
|
|
1265
1271
|
setAmbient(intensity) {
|
|
1272
|
+
// ambient: f32 at offset 0 (index 0)
|
|
1266
1273
|
this.lightData[0] = intensity;
|
|
1267
1274
|
}
|
|
1275
|
+
setAmbientColor(color) {
|
|
1276
|
+
this.lightData[4] = color.x;
|
|
1277
|
+
this.lightData[5] = color.y;
|
|
1278
|
+
this.lightData[6] = color.z;
|
|
1279
|
+
// Index 7 is padding for vec3f alignment (must be 0)
|
|
1280
|
+
this.lightData[7] = 0.0;
|
|
1281
|
+
}
|
|
1268
1282
|
async loadAnimation(url) {
|
|
1269
1283
|
const frames = await VMDLoader.load(url);
|
|
1270
1284
|
this.animationFrames = frames;
|