reze-engine 0.1.3 → 0.1.5
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 +99 -99
- package/dist/engine.d.ts +1 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +239 -252
- package/dist/math.d.ts +2 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +33 -0
- package/dist/model.d.ts +5 -25
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +49 -195
- package/package.json +2 -2
- package/src/camera.ts +358 -358
- package/src/engine.ts +1136 -1158
- package/src/math.ts +546 -505
- package/src/model.ts +418 -586
- package/src/physics.ts +680 -680
- package/src/pmx-loader.ts +1031 -1031
package/dist/engine.js
CHANGED
|
@@ -24,14 +24,6 @@ export class Engine {
|
|
|
24
24
|
this.stats = {
|
|
25
25
|
fps: 0,
|
|
26
26
|
frameTime: 0,
|
|
27
|
-
memoryUsed: 0,
|
|
28
|
-
vertices: 0,
|
|
29
|
-
drawCalls: 0,
|
|
30
|
-
triangles: 0,
|
|
31
|
-
materials: 0,
|
|
32
|
-
textures: 0,
|
|
33
|
-
textureMemory: 0,
|
|
34
|
-
bufferMemory: 0,
|
|
35
27
|
gpuMemory: 0,
|
|
36
28
|
};
|
|
37
29
|
this.animationFrameId = null;
|
|
@@ -74,112 +66,112 @@ export class Engine {
|
|
|
74
66
|
});
|
|
75
67
|
const shaderModule = this.device.createShaderModule({
|
|
76
68
|
label: "model shaders",
|
|
77
|
-
code: /* wgsl */ `
|
|
78
|
-
struct CameraUniforms {
|
|
79
|
-
view: mat4x4f,
|
|
80
|
-
projection: mat4x4f,
|
|
81
|
-
viewPos: vec3f,
|
|
82
|
-
_padding: f32,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
struct Light {
|
|
86
|
-
direction: vec3f,
|
|
87
|
-
_padding1: f32,
|
|
88
|
-
color: vec3f,
|
|
89
|
-
intensity: f32,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
struct LightUniforms {
|
|
93
|
-
ambient: f32,
|
|
94
|
-
lightCount: f32,
|
|
95
|
-
_padding1: f32,
|
|
96
|
-
_padding2: f32,
|
|
97
|
-
lights: array<Light, 4>,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
struct MaterialUniforms {
|
|
101
|
-
alpha: f32,
|
|
102
|
-
_padding1: f32,
|
|
103
|
-
_padding2: f32,
|
|
104
|
-
_padding3: f32,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
struct VertexOutput {
|
|
108
|
-
@builtin(position) position: vec4f,
|
|
109
|
-
@location(0) normal: vec3f,
|
|
110
|
-
@location(1) uv: vec2f,
|
|
111
|
-
@location(2) worldPos: vec3f,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
115
|
-
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
116
|
-
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
117
|
-
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
118
|
-
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
119
|
-
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
120
|
-
@group(0) @binding(6) var toonSampler: sampler;
|
|
121
|
-
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
122
|
-
|
|
123
|
-
@vertex fn vs(
|
|
124
|
-
@location(0) position: vec3f,
|
|
125
|
-
@location(1) normal: vec3f,
|
|
126
|
-
@location(2) uv: vec2f,
|
|
127
|
-
@location(3) joints0: vec4<u32>,
|
|
128
|
-
@location(4) weights0: vec4<f32>
|
|
129
|
-
) -> VertexOutput {
|
|
130
|
-
var output: VertexOutput;
|
|
131
|
-
let pos4 = vec4f(position, 1.0);
|
|
132
|
-
|
|
133
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
134
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
135
|
-
var normalizedWeights: vec4f;
|
|
136
|
-
if (weightSum > 0.0001) {
|
|
137
|
-
normalizedWeights = weights0 / weightSum;
|
|
138
|
-
} else {
|
|
139
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
143
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
144
|
-
for (var i = 0u; i < 4u; i++) {
|
|
145
|
-
let j = joints0[i];
|
|
146
|
-
let w = normalizedWeights[i];
|
|
147
|
-
let m = skinMats[j];
|
|
148
|
-
skinnedPos += (m * pos4) * w;
|
|
149
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
150
|
-
skinnedNrm += (r3 * normal) * w;
|
|
151
|
-
}
|
|
152
|
-
let worldPos = skinnedPos.xyz;
|
|
153
|
-
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
154
|
-
output.normal = normalize(skinnedNrm);
|
|
155
|
-
output.uv = uv;
|
|
156
|
-
output.worldPos = worldPos;
|
|
157
|
-
return output;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
161
|
-
let n = normalize(input.normal);
|
|
162
|
-
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
163
|
-
|
|
164
|
-
var lightAccum = vec3f(light.ambient);
|
|
165
|
-
let numLights = u32(light.lightCount);
|
|
166
|
-
for (var i = 0u; i < numLights; i++) {
|
|
167
|
-
let l = -light.lights[i].direction;
|
|
168
|
-
let nDotL = max(dot(n, l), 0.0);
|
|
169
|
-
let toonUV = vec2f(nDotL, 0.5);
|
|
170
|
-
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
171
|
-
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
172
|
-
lightAccum += toonFactor * radiance * nDotL;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
let color = albedo * lightAccum;
|
|
176
|
-
let finalAlpha = material.alpha;
|
|
177
|
-
if (finalAlpha < 0.001) {
|
|
178
|
-
discard;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
182
|
-
}
|
|
69
|
+
code: /* wgsl */ `
|
|
70
|
+
struct CameraUniforms {
|
|
71
|
+
view: mat4x4f,
|
|
72
|
+
projection: mat4x4f,
|
|
73
|
+
viewPos: vec3f,
|
|
74
|
+
_padding: f32,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
struct Light {
|
|
78
|
+
direction: vec3f,
|
|
79
|
+
_padding1: f32,
|
|
80
|
+
color: vec3f,
|
|
81
|
+
intensity: f32,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
struct LightUniforms {
|
|
85
|
+
ambient: f32,
|
|
86
|
+
lightCount: f32,
|
|
87
|
+
_padding1: f32,
|
|
88
|
+
_padding2: f32,
|
|
89
|
+
lights: array<Light, 4>,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
struct MaterialUniforms {
|
|
93
|
+
alpha: f32,
|
|
94
|
+
_padding1: f32,
|
|
95
|
+
_padding2: f32,
|
|
96
|
+
_padding3: f32,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
struct VertexOutput {
|
|
100
|
+
@builtin(position) position: vec4f,
|
|
101
|
+
@location(0) normal: vec3f,
|
|
102
|
+
@location(1) uv: vec2f,
|
|
103
|
+
@location(2) worldPos: vec3f,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
107
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
108
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
109
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
110
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
111
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
112
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
113
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
114
|
+
|
|
115
|
+
@vertex fn vs(
|
|
116
|
+
@location(0) position: vec3f,
|
|
117
|
+
@location(1) normal: vec3f,
|
|
118
|
+
@location(2) uv: vec2f,
|
|
119
|
+
@location(3) joints0: vec4<u32>,
|
|
120
|
+
@location(4) weights0: vec4<f32>
|
|
121
|
+
) -> VertexOutput {
|
|
122
|
+
var output: VertexOutput;
|
|
123
|
+
let pos4 = vec4f(position, 1.0);
|
|
124
|
+
|
|
125
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
126
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
127
|
+
var normalizedWeights: vec4f;
|
|
128
|
+
if (weightSum > 0.0001) {
|
|
129
|
+
normalizedWeights = weights0 / weightSum;
|
|
130
|
+
} else {
|
|
131
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
135
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
136
|
+
for (var i = 0u; i < 4u; i++) {
|
|
137
|
+
let j = joints0[i];
|
|
138
|
+
let w = normalizedWeights[i];
|
|
139
|
+
let m = skinMats[j];
|
|
140
|
+
skinnedPos += (m * pos4) * w;
|
|
141
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
142
|
+
skinnedNrm += (r3 * normal) * w;
|
|
143
|
+
}
|
|
144
|
+
let worldPos = skinnedPos.xyz;
|
|
145
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
146
|
+
output.normal = normalize(skinnedNrm);
|
|
147
|
+
output.uv = uv;
|
|
148
|
+
output.worldPos = worldPos;
|
|
149
|
+
return output;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
153
|
+
let n = normalize(input.normal);
|
|
154
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
155
|
+
|
|
156
|
+
var lightAccum = vec3f(light.ambient);
|
|
157
|
+
let numLights = u32(light.lightCount);
|
|
158
|
+
for (var i = 0u; i < numLights; i++) {
|
|
159
|
+
let l = -light.lights[i].direction;
|
|
160
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
161
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
162
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
163
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
164
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let color = albedo * lightAccum;
|
|
168
|
+
let finalAlpha = material.alpha;
|
|
169
|
+
if (finalAlpha < 0.001) {
|
|
170
|
+
discard;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
174
|
+
}
|
|
183
175
|
`,
|
|
184
176
|
});
|
|
185
177
|
// Single pipeline for all materials with alpha blending
|
|
@@ -239,72 +231,72 @@ export class Engine {
|
|
|
239
231
|
});
|
|
240
232
|
const outlineShaderModule = this.device.createShaderModule({
|
|
241
233
|
label: "outline shaders",
|
|
242
|
-
code: /* wgsl */ `
|
|
243
|
-
struct CameraUniforms {
|
|
244
|
-
view: mat4x4f,
|
|
245
|
-
projection: mat4x4f,
|
|
246
|
-
viewPos: vec3f,
|
|
247
|
-
_padding: f32,
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
struct MaterialUniforms {
|
|
251
|
-
edgeColor: vec4f,
|
|
252
|
-
edgeSize: f32,
|
|
253
|
-
_padding1: f32,
|
|
254
|
-
_padding2: f32,
|
|
255
|
-
_padding3: f32,
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
259
|
-
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
260
|
-
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
261
|
-
|
|
262
|
-
struct VertexOutput {
|
|
263
|
-
@builtin(position) position: vec4f,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
@vertex fn vs(
|
|
267
|
-
@location(0) position: vec3f,
|
|
268
|
-
@location(1) normal: vec3f,
|
|
269
|
-
@location(2) uv: vec2f,
|
|
270
|
-
@location(3) joints0: vec4<u32>,
|
|
271
|
-
@location(4) weights0: vec4<f32>
|
|
272
|
-
) -> VertexOutput {
|
|
273
|
-
var output: VertexOutput;
|
|
274
|
-
let pos4 = vec4f(position, 1.0);
|
|
275
|
-
|
|
276
|
-
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
277
|
-
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
278
|
-
var normalizedWeights: vec4f;
|
|
279
|
-
if (weightSum > 0.0001) {
|
|
280
|
-
normalizedWeights = weights0 / weightSum;
|
|
281
|
-
} else {
|
|
282
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
286
|
-
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
287
|
-
for (var i = 0u; i < 4u; i++) {
|
|
288
|
-
let j = joints0[i];
|
|
289
|
-
let w = normalizedWeights[i];
|
|
290
|
-
let m = skinMats[j];
|
|
291
|
-
skinnedPos += (m * pos4) * w;
|
|
292
|
-
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
293
|
-
skinnedNrm += (r3 * normal) * w;
|
|
294
|
-
}
|
|
295
|
-
let worldPos = skinnedPos.xyz;
|
|
296
|
-
let worldNormal = normalize(skinnedNrm);
|
|
297
|
-
|
|
298
|
-
// MMD invert hull: expand vertices outward along normals
|
|
299
|
-
let scaleFactor = 0.01;
|
|
300
|
-
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
301
|
-
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
302
|
-
return output;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
@fragment fn fs() -> @location(0) vec4f {
|
|
306
|
-
return material.edgeColor;
|
|
307
|
-
}
|
|
234
|
+
code: /* wgsl */ `
|
|
235
|
+
struct CameraUniforms {
|
|
236
|
+
view: mat4x4f,
|
|
237
|
+
projection: mat4x4f,
|
|
238
|
+
viewPos: vec3f,
|
|
239
|
+
_padding: f32,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
struct MaterialUniforms {
|
|
243
|
+
edgeColor: vec4f,
|
|
244
|
+
edgeSize: f32,
|
|
245
|
+
_padding1: f32,
|
|
246
|
+
_padding2: f32,
|
|
247
|
+
_padding3: f32,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
251
|
+
@group(0) @binding(1) var<uniform> material: MaterialUniforms;
|
|
252
|
+
@group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
|
|
253
|
+
|
|
254
|
+
struct VertexOutput {
|
|
255
|
+
@builtin(position) position: vec4f,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
@vertex fn vs(
|
|
259
|
+
@location(0) position: vec3f,
|
|
260
|
+
@location(1) normal: vec3f,
|
|
261
|
+
@location(2) uv: vec2f,
|
|
262
|
+
@location(3) joints0: vec4<u32>,
|
|
263
|
+
@location(4) weights0: vec4<f32>
|
|
264
|
+
) -> VertexOutput {
|
|
265
|
+
var output: VertexOutput;
|
|
266
|
+
let pos4 = vec4f(position, 1.0);
|
|
267
|
+
|
|
268
|
+
// Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
|
|
269
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
270
|
+
var normalizedWeights: vec4f;
|
|
271
|
+
if (weightSum > 0.0001) {
|
|
272
|
+
normalizedWeights = weights0 / weightSum;
|
|
273
|
+
} else {
|
|
274
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
278
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
279
|
+
for (var i = 0u; i < 4u; i++) {
|
|
280
|
+
let j = joints0[i];
|
|
281
|
+
let w = normalizedWeights[i];
|
|
282
|
+
let m = skinMats[j];
|
|
283
|
+
skinnedPos += (m * pos4) * w;
|
|
284
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
285
|
+
skinnedNrm += (r3 * normal) * w;
|
|
286
|
+
}
|
|
287
|
+
let worldPos = skinnedPos.xyz;
|
|
288
|
+
let worldNormal = normalize(skinnedNrm);
|
|
289
|
+
|
|
290
|
+
// MMD invert hull: expand vertices outward along normals
|
|
291
|
+
let scaleFactor = 0.01;
|
|
292
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
|
|
293
|
+
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
294
|
+
return output;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
298
|
+
return material.edgeColor;
|
|
299
|
+
}
|
|
308
300
|
`,
|
|
309
301
|
});
|
|
310
302
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
@@ -380,31 +372,31 @@ export class Engine {
|
|
|
380
372
|
createSkinMatrixComputePipeline() {
|
|
381
373
|
const computeShader = this.device.createShaderModule({
|
|
382
374
|
label: "skin matrix compute",
|
|
383
|
-
code: /* wgsl */ `
|
|
384
|
-
struct BoneCountUniform {
|
|
385
|
-
count: u32,
|
|
386
|
-
_padding1: u32,
|
|
387
|
-
_padding2: u32,
|
|
388
|
-
_padding3: u32,
|
|
389
|
-
_padding4: vec4<u32>,
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
393
|
-
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
394
|
-
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
395
|
-
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
396
|
-
|
|
397
|
-
@compute @workgroup_size(64)
|
|
398
|
-
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
399
|
-
let boneIndex = globalId.x;
|
|
400
|
-
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
401
|
-
if (boneIndex >= boneCount.count) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
let worldMat = worldMatrices[boneIndex];
|
|
405
|
-
let invBindMat = inverseBindMatrices[boneIndex];
|
|
406
|
-
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
407
|
-
}
|
|
375
|
+
code: /* wgsl */ `
|
|
376
|
+
struct BoneCountUniform {
|
|
377
|
+
count: u32,
|
|
378
|
+
_padding1: u32,
|
|
379
|
+
_padding2: u32,
|
|
380
|
+
_padding3: u32,
|
|
381
|
+
_padding4: vec4<u32>,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
@group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
|
|
385
|
+
@group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
|
|
386
|
+
@group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
|
|
387
|
+
@group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
|
|
388
|
+
|
|
389
|
+
@compute @workgroup_size(64)
|
|
390
|
+
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
|
391
|
+
let boneIndex = globalId.x;
|
|
392
|
+
// Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
|
|
393
|
+
if (boneIndex >= boneCount.count) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let worldMat = worldMatrices[boneIndex];
|
|
397
|
+
let invBindMat = inverseBindMatrices[boneIndex];
|
|
398
|
+
skinMatrices[boneIndex] = worldMat * invBindMat;
|
|
399
|
+
}
|
|
408
400
|
`,
|
|
409
401
|
});
|
|
410
402
|
this.skinMatrixComputePipeline = this.device.createComputePipeline({
|
|
@@ -557,8 +549,8 @@ export class Engine {
|
|
|
557
549
|
this.physics = new Physics(model.getRigidbodies(), model.getJoints());
|
|
558
550
|
await this.setupModelBuffers(model);
|
|
559
551
|
}
|
|
560
|
-
rotateBones(bones, rotations,
|
|
561
|
-
this.currentModel?.rotateBones(bones, rotations,
|
|
552
|
+
rotateBones(bones, rotations, durationMs) {
|
|
553
|
+
this.currentModel?.rotateBones(bones, rotations, durationMs);
|
|
562
554
|
}
|
|
563
555
|
// Step 7: Create vertex, index, and joint buffers
|
|
564
556
|
async setupModelBuffers(model) {
|
|
@@ -646,7 +638,11 @@ export class Engine {
|
|
|
646
638
|
const texture = await loadTextureByIndex(toonTextureIndex);
|
|
647
639
|
if (texture)
|
|
648
640
|
return texture;
|
|
649
|
-
// Default toon texture fallback
|
|
641
|
+
// Default toon texture fallback - cache it
|
|
642
|
+
const defaultToonPath = "__default_toon__";
|
|
643
|
+
const cached = this.textureCache.get(defaultToonPath);
|
|
644
|
+
if (cached)
|
|
645
|
+
return cached;
|
|
650
646
|
const defaultToonData = new Uint8Array(256 * 2 * 4);
|
|
651
647
|
for (let i = 0; i < 256; i++) {
|
|
652
648
|
const factor = i / 255.0;
|
|
@@ -667,7 +663,8 @@ export class Engine {
|
|
|
667
663
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
668
664
|
});
|
|
669
665
|
this.device.queue.writeTexture({ texture: defaultToonTexture }, defaultToonData, { bytesPerRow: 256 * 4 }, [256, 2]);
|
|
670
|
-
this.
|
|
666
|
+
this.textureCache.set(defaultToonPath, defaultToonTexture);
|
|
667
|
+
this.textureSizes.set(defaultToonPath, { width: 256, height: 2 });
|
|
671
668
|
return defaultToonTexture;
|
|
672
669
|
};
|
|
673
670
|
this.materialDraws = [];
|
|
@@ -752,8 +749,8 @@ export class Engine {
|
|
|
752
749
|
runningFirstIndex += matCount;
|
|
753
750
|
}
|
|
754
751
|
}
|
|
755
|
-
// Helper: Load texture from file path
|
|
756
|
-
async createTextureFromPath(path) {
|
|
752
|
+
// Helper: Load texture from file path with optional max size limit
|
|
753
|
+
async createTextureFromPath(path, maxSize = 2048) {
|
|
757
754
|
const cached = this.textureCache.get(path);
|
|
758
755
|
if (cached) {
|
|
759
756
|
return cached;
|
|
@@ -763,22 +760,34 @@ export class Engine {
|
|
|
763
760
|
if (!response.ok) {
|
|
764
761
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
765
762
|
}
|
|
766
|
-
|
|
763
|
+
let imageBitmap = await createImageBitmap(await response.blob(), {
|
|
767
764
|
premultiplyAlpha: "none",
|
|
768
765
|
colorSpaceConversion: "none",
|
|
769
766
|
});
|
|
767
|
+
// Downscale if texture is too large
|
|
768
|
+
let finalWidth = imageBitmap.width;
|
|
769
|
+
let finalHeight = imageBitmap.height;
|
|
770
|
+
if (finalWidth > maxSize || finalHeight > maxSize) {
|
|
771
|
+
const scale = Math.min(maxSize / finalWidth, maxSize / finalHeight);
|
|
772
|
+
finalWidth = Math.floor(finalWidth * scale);
|
|
773
|
+
finalHeight = Math.floor(finalHeight * scale);
|
|
774
|
+
// Create canvas to downscale
|
|
775
|
+
const canvas = new OffscreenCanvas(finalWidth, finalHeight);
|
|
776
|
+
const ctx = canvas.getContext("2d");
|
|
777
|
+
if (ctx) {
|
|
778
|
+
ctx.drawImage(imageBitmap, 0, 0, finalWidth, finalHeight);
|
|
779
|
+
imageBitmap = await createImageBitmap(canvas);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
770
782
|
const texture = this.device.createTexture({
|
|
771
783
|
label: `texture: ${path}`,
|
|
772
|
-
size: [
|
|
784
|
+
size: [finalWidth, finalHeight],
|
|
773
785
|
format: "rgba8unorm",
|
|
774
786
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
775
787
|
});
|
|
776
|
-
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
|
|
777
|
-
imageBitmap.width,
|
|
778
|
-
imageBitmap.height,
|
|
779
|
-
]);
|
|
788
|
+
this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [finalWidth, finalHeight]);
|
|
780
789
|
this.textureCache.set(path, texture);
|
|
781
|
-
this.textureSizes.set(path, { width:
|
|
790
|
+
this.textureSizes.set(path, { width: finalWidth, height: finalHeight });
|
|
782
791
|
return texture;
|
|
783
792
|
}
|
|
784
793
|
catch {
|
|
@@ -914,41 +923,14 @@ export class Engine {
|
|
|
914
923
|
this.stats.fps = Math.round((this.framesSinceLastUpdate / elapsed) * 1000);
|
|
915
924
|
this.framesSinceLastUpdate = 0;
|
|
916
925
|
this.lastFpsUpdate = now;
|
|
917
|
-
const perf = performance;
|
|
918
|
-
if (perf.memory) {
|
|
919
|
-
this.stats.memoryUsed = Math.round(perf.memory.usedJSHeapSize / 1024 / 1024);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
this.stats.vertices = this.vertexCount;
|
|
923
|
-
this.stats.drawCalls = this.drawCallCount;
|
|
924
|
-
// Calculate triangles from index buffer
|
|
925
|
-
if (this.indexBuffer) {
|
|
926
|
-
const indexCount = this.currentModel?.getIndices()?.length || 0;
|
|
927
|
-
this.stats.triangles = Math.floor(indexCount / 3);
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
this.stats.triangles = Math.floor(this.vertexCount / 3);
|
|
931
926
|
}
|
|
932
|
-
//
|
|
933
|
-
this.stats.materials = this.materialDraws.length;
|
|
934
|
-
// Texture stats
|
|
935
|
-
this.stats.textures = this.textureCache.size;
|
|
927
|
+
// Calculate GPU memory: textures + buffers + render targets
|
|
936
928
|
let textureMemoryBytes = 0;
|
|
937
929
|
for (const [path, size] of this.textureSizes.entries()) {
|
|
938
930
|
if (this.textureCache.has(path)) {
|
|
939
|
-
// RGBA8 = 4 bytes per pixel
|
|
940
|
-
textureMemoryBytes += size.width * size.height * 4;
|
|
931
|
+
textureMemoryBytes += size.width * size.height * 4; // RGBA8 = 4 bytes per pixel
|
|
941
932
|
}
|
|
942
933
|
}
|
|
943
|
-
// Add render target textures (multisample + depth)
|
|
944
|
-
if (this.multisampleTexture) {
|
|
945
|
-
const width = this.canvas.width;
|
|
946
|
-
const height = this.canvas.height;
|
|
947
|
-
textureMemoryBytes += width * height * 4 * this.sampleCount; // multisample color
|
|
948
|
-
textureMemoryBytes += width * height * 4; // depth (depth24plus = 4 bytes)
|
|
949
|
-
}
|
|
950
|
-
this.stats.textureMemory = Math.round((textureMemoryBytes / 1024 / 1024) * 100) / 100;
|
|
951
|
-
// Buffer memory estimate
|
|
952
934
|
let bufferMemoryBytes = 0;
|
|
953
935
|
if (this.vertexBuffer) {
|
|
954
936
|
const vertices = this.currentModel?.getVertices();
|
|
@@ -977,10 +959,15 @@ export class Engine {
|
|
|
977
959
|
}
|
|
978
960
|
bufferMemoryBytes += 40 * 4; // cameraUniformBuffer
|
|
979
961
|
bufferMemoryBytes += 64 * 4; // lightUniformBuffer
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
|
|
962
|
+
bufferMemoryBytes += this.materialDraws.length * 4; // Material uniform buffers
|
|
963
|
+
let renderTargetMemoryBytes = 0;
|
|
964
|
+
if (this.multisampleTexture) {
|
|
965
|
+
const width = this.canvas.width;
|
|
966
|
+
const height = this.canvas.height;
|
|
967
|
+
renderTargetMemoryBytes += width * height * 4 * this.sampleCount; // multisample color
|
|
968
|
+
renderTargetMemoryBytes += width * height * 4; // depth
|
|
969
|
+
}
|
|
970
|
+
const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes;
|
|
971
|
+
this.stats.gpuMemory = Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100;
|
|
985
972
|
}
|
|
986
973
|
}
|
package/dist/math.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function easeInOut(t: number): number;
|
|
1
2
|
export declare class Vec3 {
|
|
2
3
|
x: number;
|
|
3
4
|
y: number;
|
|
@@ -28,6 +29,7 @@ export declare class Quat {
|
|
|
28
29
|
rotate(v: Vec3): Vec3;
|
|
29
30
|
static fromTo(from: Vec3, to: Vec3): Quat;
|
|
30
31
|
toArray(): [number, number, number, number];
|
|
32
|
+
static slerp(a: Quat, b: Quat, t: number): Quat;
|
|
31
33
|
static fromEuler(rotX: number, rotY: number, rotZ: number): Quat;
|
|
32
34
|
toEuler(): Vec3;
|
|
33
35
|
}
|
package/dist/math.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"math.d.ts","sourceRoot":"","sources":["../src/math.ts"],"names":[],"mappings":"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"}
|
package/dist/math.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Easing function: ease-in-out quadratic
|
|
2
|
+
export function easeInOut(t) {
|
|
3
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
4
|
+
}
|
|
1
5
|
export class Vec3 {
|
|
2
6
|
constructor(x, y, z) {
|
|
3
7
|
this.x = x;
|
|
@@ -101,6 +105,35 @@ export class Quat {
|
|
|
101
105
|
toArray() {
|
|
102
106
|
return [this.x, this.y, this.z, this.w];
|
|
103
107
|
}
|
|
108
|
+
// Spherical linear interpolation between two quaternions
|
|
109
|
+
static slerp(a, b, t) {
|
|
110
|
+
let cos = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
|
111
|
+
let bx = b.x, by = b.y, bz = b.z, bw = b.w;
|
|
112
|
+
// If dot product is negative, negate one quaternion to take shorter path
|
|
113
|
+
if (cos < 0) {
|
|
114
|
+
cos = -cos;
|
|
115
|
+
bx = -bx;
|
|
116
|
+
by = -by;
|
|
117
|
+
bz = -bz;
|
|
118
|
+
bw = -bw;
|
|
119
|
+
}
|
|
120
|
+
// If quaternions are very close, use linear interpolation
|
|
121
|
+
if (cos > 0.9995) {
|
|
122
|
+
const x = a.x + t * (bx - a.x);
|
|
123
|
+
const y = a.y + t * (by - a.y);
|
|
124
|
+
const z = a.z + t * (bz - a.z);
|
|
125
|
+
const w = a.w + t * (bw - a.w);
|
|
126
|
+
const invLen = 1 / Math.hypot(x, y, z, w);
|
|
127
|
+
return new Quat(x * invLen, y * invLen, z * invLen, w * invLen);
|
|
128
|
+
}
|
|
129
|
+
// Standard SLERP
|
|
130
|
+
const theta0 = Math.acos(cos);
|
|
131
|
+
const sinTheta0 = Math.sin(theta0);
|
|
132
|
+
const theta = theta0 * t;
|
|
133
|
+
const s0 = Math.sin(theta0 - theta) / sinTheta0;
|
|
134
|
+
const s1 = Math.sin(theta) / sinTheta0;
|
|
135
|
+
return new Quat(s0 * a.x + s1 * bx, s0 * a.y + s1 * by, s0 * a.z + s1 * bz, s0 * a.w + s1 * bw);
|
|
136
|
+
}
|
|
104
137
|
// Convert Euler angles to quaternion (ZXY order, left-handed, PMX format)
|
|
105
138
|
static fromEuler(rotX, rotY, rotZ) {
|
|
106
139
|
const cx = Math.cos(rotX * 0.5);
|