@plasius/gpu-renderer 0.2.45 → 0.2.47
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/CHANGELOG.md +38 -0
- package/README.md +71 -0
- package/dist/index.cjs +2580 -1705
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2578 -1705
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/canonical-wavefront-meshes.js +226 -0
- package/src/index.d.ts +64 -0
- package/src/index.js +2 -0
- package/src/native-scene-renderer.js +550 -0
- package/src/native-scene-shader.js +178 -0
- package/src/renderer-webgpu-runtime.js +24 -19
- package/src/wavefront-adaptive-byte-constants.js +28 -0
- package/src/wavefront-adaptive-metadata.js +185 -0
- package/src/wavefront-adaptive-shader.js +28 -0
package/dist/index.cjs
CHANGED
|
@@ -28,10 +28,12 @@ __export(index_exports, {
|
|
|
28
28
|
bucketFeedbackGameFrameTime: () => bucketFeedbackGameFrameTime,
|
|
29
29
|
bucketFeedbackGameViewport: () => bucketFeedbackGameViewport,
|
|
30
30
|
createAnimatedSceneRenderer: () => createAnimatedSceneRenderer,
|
|
31
|
+
createCanonicalWavefrontMeshInputs: () => createCanonicalWavefrontMeshInputs,
|
|
31
32
|
createDefaultWavefrontSceneObjects: () => createDefaultWavefrontSceneObjects,
|
|
32
33
|
createFeedbackGameDiagnosticSnapshot: () => createFeedbackGameDiagnosticSnapshot,
|
|
33
34
|
createGpuRenderer: () => createGpuRenderer,
|
|
34
35
|
createMediumStack: () => createMediumStack,
|
|
36
|
+
createNativeSceneRenderer: () => createNativeSceneRenderer,
|
|
35
37
|
createProfessionalAnimatedSceneRenderer: () => createProfessionalAnimatedSceneRenderer,
|
|
36
38
|
createRayTracingRenderPlan: () => createRayTracingRenderPlan,
|
|
37
39
|
createRendererDebugHooks: () => createRendererDebugHooks,
|
|
@@ -91,1864 +93,2735 @@ __export(index_exports, {
|
|
|
91
93
|
});
|
|
92
94
|
module.exports = __toCommonJS(index_exports);
|
|
93
95
|
|
|
94
|
-
// src/
|
|
95
|
-
var
|
|
96
|
+
// src/renderer-constants.js
|
|
97
|
+
var DEFAULT_CLEAR_COLOR = Object.freeze([0.07, 0.11, 0.18, 1]);
|
|
98
|
+
var DEFAULT_CANVAS_SELECTOR = "canvas[data-plasius-gpu-renderer]";
|
|
99
|
+
var rendererDebugOwner = "renderer";
|
|
100
|
+
var rendererWorkerQueueClass = "render";
|
|
101
|
+
var defaultRendererWorkerProfile = "realtime";
|
|
102
|
+
var rendererRepresentationBands = Object.freeze([
|
|
103
|
+
"near",
|
|
104
|
+
"mid",
|
|
105
|
+
"far",
|
|
106
|
+
"horizon"
|
|
107
|
+
]);
|
|
108
|
+
var rendererAccelerationStructureUpdateClasses = Object.freeze([
|
|
109
|
+
"static",
|
|
110
|
+
"rigid-dynamic",
|
|
111
|
+
"deforming",
|
|
112
|
+
"proxy"
|
|
113
|
+
]);
|
|
114
|
+
var rendererRayTracingStageOrder = Object.freeze([
|
|
115
|
+
"primaryVisibility",
|
|
116
|
+
"shadowAssist",
|
|
117
|
+
"opaqueFoundation",
|
|
118
|
+
"rtDirectLighting",
|
|
119
|
+
"rtReflections",
|
|
120
|
+
"rtGlobalIllumination",
|
|
121
|
+
"denoiseTemporal",
|
|
122
|
+
"transparents",
|
|
123
|
+
"composition",
|
|
124
|
+
"present"
|
|
125
|
+
]);
|
|
126
|
+
var rendererWavefrontBufferSchemaVersion = 1;
|
|
127
|
+
var rendererWavefrontQueuePairStrategy = "ping-pong-active-next";
|
|
128
|
+
var rendererWavefrontHitTypes = Object.freeze([
|
|
129
|
+
"surface",
|
|
130
|
+
"emissive",
|
|
131
|
+
"environment",
|
|
132
|
+
"transparent",
|
|
133
|
+
"miss"
|
|
134
|
+
]);
|
|
135
|
+
var rendererWavefrontPassOrder = Object.freeze([
|
|
136
|
+
"generatePrimaryRays",
|
|
137
|
+
"intersectActiveQueue",
|
|
138
|
+
"resolveSurfaceRecords",
|
|
139
|
+
"accumulateTerminalRadiance",
|
|
140
|
+
"scatterContinuations",
|
|
141
|
+
"compactAndSwapQueues"
|
|
142
|
+
]);
|
|
96
143
|
|
|
97
|
-
// src/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
var GLB_BIN_CHUNK = 5130562;
|
|
102
|
-
var COMPONENT_READERS = Object.freeze({
|
|
103
|
-
5120: { size: 1, read: (view, offset) => view.getInt8(offset), normalizedDivisor: 127 },
|
|
104
|
-
5121: { size: 1, read: (view, offset) => view.getUint8(offset), normalizedDivisor: 255 },
|
|
105
|
-
5122: { size: 2, read: (view, offset) => view.getInt16(offset, true), normalizedDivisor: 32767 },
|
|
106
|
-
5123: { size: 2, read: (view, offset) => view.getUint16(offset, true), normalizedDivisor: 65535 },
|
|
107
|
-
5125: { size: 4, read: (view, offset) => view.getUint32(offset, true), normalizedDivisor: 4294967295 },
|
|
108
|
-
5126: { size: 4, read: (view, offset) => view.getFloat32(offset, true), normalizedDivisor: 1 }
|
|
109
|
-
});
|
|
110
|
-
var ACCESSOR_COMPONENTS = Object.freeze({
|
|
111
|
-
SCALAR: 1,
|
|
112
|
-
VEC2: 2,
|
|
113
|
-
VEC3: 3,
|
|
114
|
-
VEC4: 4,
|
|
115
|
-
MAT4: 16
|
|
116
|
-
});
|
|
117
|
-
function identityMatrix() {
|
|
118
|
-
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
119
|
-
}
|
|
120
|
-
function multiplyMatrix(a, b) {
|
|
121
|
-
const out = new Array(16).fill(0);
|
|
122
|
-
for (let column = 0; column < 4; column += 1) {
|
|
123
|
-
for (let row = 0; row < 4; row += 1) {
|
|
124
|
-
out[column * 4 + row] = a[0 * 4 + row] * b[column * 4 + 0] + a[1 * 4 + row] * b[column * 4 + 1] + a[2 * 4 + row] * b[column * 4 + 2] + a[3 * 4 + row] * b[column * 4 + 3];
|
|
125
|
-
}
|
|
144
|
+
// src/renderer-validation.js
|
|
145
|
+
function readPositiveNumber(name, value) {
|
|
146
|
+
if (value === void 0) {
|
|
147
|
+
return void 0;
|
|
126
148
|
}
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return [
|
|
132
|
-
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
133
|
-
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
134
|
-
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
|
|
135
|
-
];
|
|
149
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
150
|
+
throw new Error(`${name} must be a finite number greater than zero.`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
136
153
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const z2 = z + z;
|
|
143
|
-
const xx = x * x2;
|
|
144
|
-
const xy = x * y2;
|
|
145
|
-
const xz = x * z2;
|
|
146
|
-
const yy = y * y2;
|
|
147
|
-
const yz = y * z2;
|
|
148
|
-
const zz = z * z2;
|
|
149
|
-
const wx = w * x2;
|
|
150
|
-
const wy = w * y2;
|
|
151
|
-
const wz = w * z2;
|
|
152
|
-
return [
|
|
153
|
-
(1 - (yy + zz)) * sx,
|
|
154
|
-
(xy + wz) * sx,
|
|
155
|
-
(xz - wy) * sx,
|
|
156
|
-
0,
|
|
157
|
-
(xy - wz) * sy,
|
|
158
|
-
(1 - (xx + zz)) * sy,
|
|
159
|
-
(yz + wx) * sy,
|
|
160
|
-
0,
|
|
161
|
-
(xz + wy) * sz,
|
|
162
|
-
(yz - wx) * sz,
|
|
163
|
-
(1 - (xx + yy)) * sz,
|
|
164
|
-
0,
|
|
165
|
-
translation[0] ?? 0,
|
|
166
|
-
translation[1] ?? 0,
|
|
167
|
-
translation[2] ?? 0,
|
|
168
|
-
1
|
|
169
|
-
];
|
|
154
|
+
function readPositiveInteger(name, value) {
|
|
155
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
156
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
170
159
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
(quaternion[2] ?? 0) / length,
|
|
177
|
-
(quaternion[3] ?? 1) / length
|
|
178
|
-
];
|
|
160
|
+
function readNonNegativeInteger(name, value) {
|
|
161
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
162
|
+
throw new Error(`${name} must be a non-negative integer.`);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
179
165
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
166
|
+
function assertRendererIdentifier(name, value) {
|
|
167
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
168
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
169
|
+
}
|
|
170
|
+
return value.trim();
|
|
182
171
|
}
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
|
|
173
|
+
// src/renderer-webgpu-runtime.js
|
|
174
|
+
function clamp01(value) {
|
|
175
|
+
return Math.min(1, Math.max(0, value));
|
|
185
176
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
const dot2 = a.reduce((total, value, index) => total + value * (b[index] ?? 0), 0);
|
|
189
|
-
if (dot2 < 0) {
|
|
190
|
-
end = b.map((value) => -value);
|
|
191
|
-
}
|
|
192
|
-
return normalizeQuaternion(lerpArray(a, end, t));
|
|
177
|
+
function parseHexChannel(channel) {
|
|
178
|
+
return parseInt(channel, 16) / 255;
|
|
193
179
|
}
|
|
194
|
-
function
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const view = new DataView(buffer);
|
|
199
|
-
if (view.byteLength < 20 || view.getUint32(0, true) !== GLB_MAGIC || view.getUint32(4, true) !== GLB_VERSION) {
|
|
200
|
-
return null;
|
|
180
|
+
function normalizeColor(value) {
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
const [r = 0, g = 0, b = 0, a = 1] = value;
|
|
183
|
+
return [clamp01(Number(r) || 0), clamp01(Number(g) || 0), clamp01(Number(b) || 0), clamp01(Number(a) || 0)];
|
|
201
184
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
185
|
+
if (typeof value === "string") {
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
if (/^#[0-9a-f]{3}$/i.test(trimmed)) {
|
|
188
|
+
const r = trimmed[1];
|
|
189
|
+
const g = trimmed[2];
|
|
190
|
+
const b = trimmed[3];
|
|
191
|
+
return [
|
|
192
|
+
parseHexChannel(r + r),
|
|
193
|
+
parseHexChannel(g + g),
|
|
194
|
+
parseHexChannel(b + b),
|
|
195
|
+
1
|
|
196
|
+
];
|
|
212
197
|
}
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
198
|
+
if (/^#[0-9a-f]{6}$/i.test(trimmed)) {
|
|
199
|
+
return [
|
|
200
|
+
parseHexChannel(trimmed.slice(1, 3)),
|
|
201
|
+
parseHexChannel(trimmed.slice(3, 5)),
|
|
202
|
+
parseHexChannel(trimmed.slice(5, 7)),
|
|
203
|
+
1
|
|
204
|
+
];
|
|
218
205
|
}
|
|
219
|
-
offset = end;
|
|
220
|
-
}
|
|
221
|
-
if (!json || !binary) {
|
|
222
|
-
return null;
|
|
223
206
|
}
|
|
224
|
-
return
|
|
207
|
+
return [...DEFAULT_CLEAR_COLOR];
|
|
225
208
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
const bufferView = document.json.bufferViews?.[accessor.bufferView];
|
|
232
|
-
const component = COMPONENT_READERS[accessor.componentType];
|
|
233
|
-
const componentCount = ACCESSOR_COMPONENTS[accessor.type] ?? 1;
|
|
234
|
-
if (!bufferView || !component) {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
const view = new DataView(
|
|
238
|
-
document.binary.buffer,
|
|
239
|
-
document.binary.byteOffset + (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0),
|
|
240
|
-
bufferView.byteLength ?? 0
|
|
241
|
-
);
|
|
242
|
-
const stride = bufferView.byteStride ?? component.size * componentCount;
|
|
243
|
-
const values = [];
|
|
244
|
-
for (let index = 0; index < accessor.count; index += 1) {
|
|
245
|
-
const item = [];
|
|
246
|
-
const baseOffset = index * stride;
|
|
247
|
-
for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
|
|
248
|
-
const raw = component.read(view, baseOffset + componentIndex * component.size);
|
|
249
|
-
item.push(accessor.normalized && accessor.componentType !== 5126 ? raw / component.normalizedDivisor : raw);
|
|
250
|
-
}
|
|
251
|
-
values.push(componentCount === 1 ? item[0] : item);
|
|
209
|
+
function now() {
|
|
210
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
211
|
+
return performance.now();
|
|
252
212
|
}
|
|
253
|
-
return
|
|
213
|
+
return Date.now();
|
|
254
214
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
215
|
+
function normalizeFrameId(value) {
|
|
216
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
217
|
+
throw new Error("frameIdFactory must return a non-empty string.");
|
|
218
|
+
}
|
|
219
|
+
return value.trim();
|
|
257
220
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
221
|
+
function resolveTargetFrameTimeMs(options, event) {
|
|
222
|
+
const {
|
|
223
|
+
targetFrameTimeMs: fixedTargetFrameTimeMs,
|
|
224
|
+
targetFrameRate,
|
|
225
|
+
getTargetFrameTimeMs
|
|
226
|
+
} = options;
|
|
227
|
+
if (typeof getTargetFrameTimeMs === "function") {
|
|
228
|
+
const resolved = getTargetFrameTimeMs(event);
|
|
229
|
+
return readPositiveNumber("getTargetFrameTimeMs()", resolved);
|
|
261
230
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
function imageHasBuffer(document, image) {
|
|
266
|
-
if (!image) {
|
|
267
|
-
return false;
|
|
231
|
+
if (fixedTargetFrameTimeMs !== void 0) {
|
|
232
|
+
return fixedTargetFrameTimeMs;
|
|
268
233
|
}
|
|
269
|
-
if (
|
|
270
|
-
return
|
|
234
|
+
if (targetFrameRate !== void 0) {
|
|
235
|
+
return 1e3 / targetFrameRate;
|
|
271
236
|
}
|
|
272
|
-
return
|
|
237
|
+
return void 0;
|
|
273
238
|
}
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
239
|
+
function createRendererDebugHooks(options = {}) {
|
|
240
|
+
const {
|
|
241
|
+
debugSession,
|
|
242
|
+
targetFrameTimeMs,
|
|
243
|
+
targetFrameRate,
|
|
244
|
+
getTargetFrameTimeMs,
|
|
245
|
+
onFrameStart,
|
|
246
|
+
onFrameComplete
|
|
247
|
+
} = options;
|
|
248
|
+
if (!debugSession || typeof debugSession.recordFrame !== "function") {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"debugSession must expose recordFrame(sample). Use @plasius/gpu-debug createGpuDebugSession()."
|
|
251
|
+
);
|
|
281
252
|
}
|
|
282
|
-
|
|
283
|
-
|
|
253
|
+
const fixedTargetFrameTimeMs = readPositiveNumber(
|
|
254
|
+
"targetFrameTimeMs",
|
|
255
|
+
targetFrameTimeMs
|
|
256
|
+
);
|
|
257
|
+
const fixedTargetFrameRate = readPositiveNumber(
|
|
258
|
+
"targetFrameRate",
|
|
259
|
+
targetFrameRate
|
|
260
|
+
);
|
|
261
|
+
if (fixedTargetFrameTimeMs !== void 0 && fixedTargetFrameRate !== void 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"Provide either targetFrameTimeMs or targetFrameRate, not both."
|
|
264
|
+
);
|
|
284
265
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
266
|
+
if (getTargetFrameTimeMs !== void 0 && typeof getTargetFrameTimeMs !== "function") {
|
|
267
|
+
throw new Error("getTargetFrameTimeMs must be a function when provided.");
|
|
268
|
+
}
|
|
269
|
+
const resolvedOptions = {
|
|
270
|
+
targetFrameTimeMs: fixedTargetFrameTimeMs,
|
|
271
|
+
targetFrameRate: fixedTargetFrameRate,
|
|
272
|
+
getTargetFrameTimeMs
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
onFrameStart(event) {
|
|
276
|
+
if (typeof onFrameStart === "function") {
|
|
277
|
+
onFrameStart({
|
|
278
|
+
...event,
|
|
279
|
+
owner: rendererDebugOwner
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
onFrameComplete(event) {
|
|
284
|
+
const resolvedTargetFrameTimeMs = resolveTargetFrameTimeMs(
|
|
285
|
+
resolvedOptions,
|
|
286
|
+
event
|
|
287
|
+
);
|
|
288
|
+
if (typeof event.frameTimeMs === "number" && Number.isFinite(event.frameTimeMs) && event.frameTimeMs > 0) {
|
|
289
|
+
debugSession.recordFrame({
|
|
290
|
+
frameId: event.frameId,
|
|
291
|
+
frameTimeMs: event.frameTimeMs,
|
|
292
|
+
targetFrameTimeMs: resolvedTargetFrameTimeMs
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (typeof onFrameComplete === "function") {
|
|
296
|
+
onFrameComplete({
|
|
297
|
+
...event,
|
|
298
|
+
owner: rendererDebugOwner,
|
|
299
|
+
targetFrameTimeMs: resolvedTargetFrameTimeMs
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
297
304
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
function readNavigator(navigatorOverride) {
|
|
306
|
+
const currentNavigator = navigatorOverride ?? globalThis.navigator;
|
|
307
|
+
if (!currentNavigator || typeof currentNavigator !== "object") {
|
|
308
|
+
throw new Error("Navigator unavailable. Provide a browser-like navigator object.");
|
|
301
309
|
}
|
|
302
|
-
return
|
|
303
|
-
override.translation ?? node.translation ?? [0, 0, 0],
|
|
304
|
-
override.rotation ?? node.rotation ?? [0, 0, 0, 1],
|
|
305
|
-
override.scale ?? node.scale ?? [1, 1, 1]
|
|
306
|
-
);
|
|
310
|
+
return currentNavigator;
|
|
307
311
|
}
|
|
308
|
-
function
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
parents.add(child);
|
|
313
|
-
}
|
|
312
|
+
function readDocument(documentOverride) {
|
|
313
|
+
const doc = documentOverride ?? globalThis.document;
|
|
314
|
+
if (!doc || typeof doc !== "object") {
|
|
315
|
+
throw new Error("Document unavailable. Provide a browser-like document object.");
|
|
314
316
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const worldMatrix = multiplyMatrix(parentMatrix, nodeLocalMatrix(node, overridesByName.get(node.name) ?? {}));
|
|
323
|
-
worldMatrices[nodeIndex] = worldMatrix;
|
|
324
|
-
for (const child of node.children ?? []) {
|
|
325
|
-
visit(child, worldMatrix);
|
|
326
|
-
}
|
|
317
|
+
return doc;
|
|
318
|
+
}
|
|
319
|
+
function resolveCanvas(canvasOrSelector, documentOverride) {
|
|
320
|
+
if (canvasOrSelector && typeof canvasOrSelector === "object") {
|
|
321
|
+
return canvasOrSelector;
|
|
327
322
|
}
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
const doc = readDocument(documentOverride);
|
|
324
|
+
const selector = typeof canvasOrSelector === "string" && canvasOrSelector.trim().length > 0 ? canvasOrSelector : DEFAULT_CANVAS_SELECTOR;
|
|
325
|
+
const resolved = doc.querySelector(selector);
|
|
326
|
+
if (!resolved) {
|
|
327
|
+
throw new Error(`Unable to find canvas for selector "${selector}".`);
|
|
330
328
|
}
|
|
331
|
-
return
|
|
329
|
+
return resolved;
|
|
332
330
|
}
|
|
333
|
-
function
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
function readGpu(navigatorOverride) {
|
|
332
|
+
const currentNavigator = readNavigator(navigatorOverride);
|
|
333
|
+
const gpu = currentNavigator.gpu;
|
|
334
|
+
if (!gpu || typeof gpu.requestAdapter !== "function") {
|
|
335
|
+
throw new Error("WebGPU runtime unavailable. navigator.gpu is missing.");
|
|
337
336
|
}
|
|
338
|
-
return
|
|
337
|
+
return gpu;
|
|
339
338
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
for (const vertex of vertices) {
|
|
344
|
-
for (let axis = 0; axis < 3; axis += 1) {
|
|
345
|
-
min[axis] = Math.min(min[axis], vertex[axis]);
|
|
346
|
-
max[axis] = Math.max(max[axis], vertex[axis]);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (!Number.isFinite(min[0])) {
|
|
350
|
-
return { min: [0, 0, 0], max: [0, 1, 0], size: [1, 1, 1] };
|
|
339
|
+
function configureContext(context, device, format, alphaMode) {
|
|
340
|
+
if (typeof context.configure !== "function") {
|
|
341
|
+
throw new Error("Canvas WebGPU context does not support configure().");
|
|
351
342
|
}
|
|
343
|
+
context.configure({
|
|
344
|
+
device,
|
|
345
|
+
format,
|
|
346
|
+
alphaMode
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function createRenderPassDescriptor(view, clearColor) {
|
|
352
350
|
return {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
colorAttachments: [
|
|
352
|
+
{
|
|
353
|
+
view,
|
|
354
|
+
loadOp: "clear",
|
|
355
|
+
clearValue: {
|
|
356
|
+
r: clearColor[0],
|
|
357
|
+
g: clearColor[1],
|
|
358
|
+
b: clearColor[2],
|
|
359
|
+
a: clearColor[3]
|
|
360
|
+
},
|
|
361
|
+
storeOp: "store"
|
|
362
|
+
}
|
|
363
|
+
]
|
|
356
364
|
};
|
|
357
365
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const duration = input.at(-1) || 0;
|
|
365
|
-
const sampleTime = duration > 0 ? timeSeconds % duration : 0;
|
|
366
|
-
let frame = 0;
|
|
367
|
-
while (frame < input.length - 2 && sampleTime > input[frame + 1]) {
|
|
368
|
-
frame += 1;
|
|
366
|
+
function supportsWebGpu(options = {}) {
|
|
367
|
+
try {
|
|
368
|
+
const gpu = readGpu(options.navigator);
|
|
369
|
+
return Boolean(gpu);
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
369
372
|
}
|
|
370
|
-
const start = input[frame] ?? 0;
|
|
371
|
-
const end = input[frame + 1] ?? start;
|
|
372
|
-
const t = end > start ? (sampleTime - start) / (end - start) : 0;
|
|
373
|
-
const startValue = Array.isArray(output[frame]) ? output[frame] : [output[frame] ?? 0];
|
|
374
|
-
const endValue = Array.isArray(output[frame + 1]) ? output[frame + 1] : startValue;
|
|
375
|
-
return channel.target.path === "rotation" ? nlerpQuaternion(startValue, endValue, t) : lerpArray(startValue, endValue, t);
|
|
376
373
|
}
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
|
|
374
|
+
async function createGpuRenderer(options = {}) {
|
|
375
|
+
const {
|
|
376
|
+
canvas,
|
|
377
|
+
navigator: navigatorOverride,
|
|
378
|
+
document: documentOverride,
|
|
379
|
+
powerPreference = "high-performance",
|
|
380
|
+
alpha = true,
|
|
381
|
+
format,
|
|
382
|
+
clearColor = DEFAULT_CLEAR_COLOR,
|
|
383
|
+
requestAnimationFrame = globalThis.requestAnimationFrame?.bind(globalThis),
|
|
384
|
+
cancelAnimationFrame = globalThis.cancelAnimationFrame?.bind(globalThis),
|
|
385
|
+
frameIdFactory,
|
|
386
|
+
onFrameStart,
|
|
387
|
+
encodeFrame,
|
|
388
|
+
onBeforeEncode,
|
|
389
|
+
onAfterSubmit,
|
|
390
|
+
onFrameComplete
|
|
391
|
+
} = options;
|
|
392
|
+
const gpu = readGpu(navigatorOverride);
|
|
393
|
+
const adapter = await gpu.requestAdapter({ powerPreference });
|
|
394
|
+
if (!adapter) {
|
|
395
|
+
throw new Error("Unable to obtain GPU adapter.");
|
|
380
396
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
397
|
+
const device = await adapter.requestDevice();
|
|
398
|
+
const targetCanvas = resolveCanvas(canvas, documentOverride);
|
|
399
|
+
const context = targetCanvas.getContext?.("webgpu");
|
|
400
|
+
if (!context) {
|
|
401
|
+
throw new Error("Unable to obtain WebGPU canvas context.");
|
|
402
|
+
}
|
|
403
|
+
const resolvedFormat = format || (typeof gpu.getPreferredCanvasFormat === "function" ? gpu.getPreferredCanvasFormat() : "bgra8unorm");
|
|
404
|
+
configureContext(context, device, resolvedFormat, alpha ? "premultiplied" : "opaque");
|
|
405
|
+
let running = false;
|
|
406
|
+
let destroyed = false;
|
|
407
|
+
let frame = 0;
|
|
408
|
+
let lastTimestamp = 0;
|
|
409
|
+
let rafId = null;
|
|
410
|
+
let clear = normalizeColor(clearColor);
|
|
411
|
+
let xrActive = false;
|
|
412
|
+
let detachXrBinding = null;
|
|
413
|
+
const renderOnce = (timestamp = now()) => {
|
|
414
|
+
if (destroyed) {
|
|
415
|
+
throw new Error("Renderer was destroyed.");
|
|
385
416
|
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
417
|
+
const frameNumber = frame + 1;
|
|
418
|
+
const frameId = normalizeFrameId(
|
|
419
|
+
typeof frameIdFactory === "function" ? frameIdFactory({
|
|
420
|
+
frame: frameNumber,
|
|
421
|
+
timestamp,
|
|
422
|
+
canvas: targetCanvas,
|
|
423
|
+
xrActive
|
|
424
|
+
}) : `renderer.frame.${frameNumber}`
|
|
425
|
+
);
|
|
426
|
+
const frameTimeMs = lastTimestamp > 0 ? Math.max(0, timestamp - lastTimestamp) : void 0;
|
|
427
|
+
if (typeof onFrameStart === "function") {
|
|
428
|
+
onFrameStart({
|
|
429
|
+
frame: frameNumber,
|
|
430
|
+
frameId,
|
|
431
|
+
frameTimeMs,
|
|
432
|
+
timestamp,
|
|
433
|
+
device,
|
|
434
|
+
context,
|
|
435
|
+
canvas: targetCanvas,
|
|
436
|
+
xrActive
|
|
437
|
+
});
|
|
389
438
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
continue;
|
|
439
|
+
const texture = context.getCurrentTexture?.();
|
|
440
|
+
if (!texture || typeof texture.createView !== "function") {
|
|
441
|
+
throw new Error("WebGPU context returned an invalid current texture.");
|
|
394
442
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
443
|
+
const encoder = device.createCommandEncoder({
|
|
444
|
+
label: `plasius.gpu-renderer.frame.${frame}`
|
|
445
|
+
});
|
|
446
|
+
const view = texture.createView();
|
|
447
|
+
if (typeof encodeFrame === "function") {
|
|
448
|
+
encodeFrame({ device, context, encoder, view, canvas: targetCanvas, timestamp });
|
|
449
|
+
} else {
|
|
450
|
+
const pass = encoder.beginRenderPass(createRenderPassDescriptor(view, clear));
|
|
451
|
+
if (typeof onBeforeEncode === "function") {
|
|
452
|
+
onBeforeEncode({
|
|
453
|
+
frame,
|
|
454
|
+
frameNumber,
|
|
455
|
+
frameId,
|
|
456
|
+
frameTimeMs,
|
|
457
|
+
timestamp,
|
|
458
|
+
device,
|
|
459
|
+
context,
|
|
460
|
+
encoder,
|
|
461
|
+
pass,
|
|
462
|
+
canvas: targetCanvas,
|
|
463
|
+
xrActive
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (typeof pass.end === "function") {
|
|
467
|
+
pass.end();
|
|
468
|
+
}
|
|
413
469
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
470
|
+
const commandBuffer = encoder.finish();
|
|
471
|
+
device.queue.submit([commandBuffer]);
|
|
472
|
+
frame = frameNumber;
|
|
473
|
+
lastTimestamp = timestamp;
|
|
474
|
+
if (typeof onAfterSubmit === "function") {
|
|
475
|
+
onAfterSubmit({
|
|
476
|
+
frame: frameNumber,
|
|
477
|
+
frameNumber,
|
|
478
|
+
frameId,
|
|
479
|
+
frameTimeMs,
|
|
480
|
+
timestamp,
|
|
481
|
+
device,
|
|
482
|
+
context,
|
|
483
|
+
canvas: targetCanvas,
|
|
484
|
+
xrActive
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (typeof onFrameComplete === "function") {
|
|
488
|
+
onFrameComplete({
|
|
489
|
+
frame: frameNumber,
|
|
490
|
+
frameId,
|
|
491
|
+
frameTimeMs,
|
|
492
|
+
timestamp,
|
|
493
|
+
device,
|
|
494
|
+
context,
|
|
495
|
+
canvas: targetCanvas,
|
|
496
|
+
xrActive
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
frame: frameNumber,
|
|
501
|
+
frameId,
|
|
502
|
+
frameTimeMs,
|
|
503
|
+
timestamp
|
|
504
|
+
};
|
|
424
505
|
};
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (!clipPayload) {
|
|
429
|
-
return overrides;
|
|
430
|
-
}
|
|
431
|
-
for (const channel of clipPayload.animation.channels ?? []) {
|
|
432
|
-
const targetNode = clipPayload.document.json.nodes?.[channel.target?.node];
|
|
433
|
-
const sampler = clipPayload.animation.samplers?.[channel.sampler];
|
|
434
|
-
if (!targetNode?.name || !sampler) {
|
|
435
|
-
continue;
|
|
506
|
+
const tick = (timestamp) => {
|
|
507
|
+
if (!running || destroyed) {
|
|
508
|
+
return;
|
|
436
509
|
}
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
510
|
+
renderOnce(timestamp);
|
|
511
|
+
if (typeof requestAnimationFrame === "function") {
|
|
512
|
+
rafId = requestAnimationFrame(tick);
|
|
440
513
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const nodes = document.json.nodes ?? [];
|
|
455
|
-
const meshNodeIndex = nodes.findIndex((node) => node.mesh !== void 0 && node.skin !== void 0);
|
|
456
|
-
const meshNode = nodes[meshNodeIndex];
|
|
457
|
-
const mesh = document.json.meshes?.[meshNode?.mesh];
|
|
458
|
-
const primitive = mesh?.primitives?.find((candidate) => candidate.attributes?.POSITION !== void 0);
|
|
459
|
-
const skin = document.json.skins?.[meshNode?.skin];
|
|
460
|
-
if (!meshNode || !primitive || !skin) {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
const positions = readAccessor(document, primitive.attributes.POSITION);
|
|
464
|
-
const normals = primitive.attributes.NORMAL === void 0 ? [] : readAccessor(document, primitive.attributes.NORMAL);
|
|
465
|
-
const texcoords = primitive.attributes.TEXCOORD_0 === void 0 ? [] : readAccessor(document, primitive.attributes.TEXCOORD_0);
|
|
466
|
-
const joints = primitive.attributes.JOINTS_0 === void 0 ? positions.map(() => [0, 0, 0, 0]) : readAccessor(document, primitive.attributes.JOINTS_0);
|
|
467
|
-
const weights = primitive.attributes.WEIGHTS_0 === void 0 ? positions.map(() => [1, 0, 0, 0]) : readAccessor(document, primitive.attributes.WEIGHTS_0).map(normalizeWeights);
|
|
468
|
-
const indices = primitive.indices === void 0 ? positions.map((_, index) => index) : readAccessor(document, primitive.indices);
|
|
469
|
-
const inverseBindMatrices = skin.inverseBindMatrices === void 0 ? skin.joints.map(() => identityMatrix()) : readMatrices(document, skin.inverseBindMatrices);
|
|
470
|
-
const clips = clipRefs.map(createClipPayload).filter(Boolean);
|
|
471
|
-
const bindWorldMatrices = computeWorldMatrices(nodes);
|
|
472
|
-
const bindVertices = skinVertices({
|
|
473
|
-
positions,
|
|
474
|
-
joints,
|
|
475
|
-
weights,
|
|
476
|
-
skin,
|
|
477
|
-
inverseBindMatrices,
|
|
478
|
-
worldMatrices: bindWorldMatrices
|
|
479
|
-
});
|
|
480
|
-
const bounds = deriveBounds(bindVertices);
|
|
481
|
-
const materialTextureMetadata = extractMaterialTextureMetadata(document, primitive);
|
|
482
|
-
const clipRootMotionDistances = new Map(clips.map((clip) => [clip.id, rootTranslationDistance(clip)]));
|
|
483
|
-
return {
|
|
484
|
-
name: mesh.name ?? meshNode.name ?? "skinned-gltf-model",
|
|
485
|
-
vertexCount: positions.length,
|
|
486
|
-
triangleCount: Math.floor(indices.length / 3),
|
|
487
|
-
jointCount: skin.joints.length,
|
|
488
|
-
animatedNodeCount: new Set(clips.flatMap((clip) => [...clip.nodeNames])).size,
|
|
489
|
-
clipCount: clips.length,
|
|
490
|
-
materialTextureMetadata,
|
|
491
|
-
textureCount: materialTextureMetadata.textureCount,
|
|
492
|
-
materialCount: materialTextureMetadata.materialCount,
|
|
493
|
-
hasBaseColorTexture: materialTextureMetadata.hasBaseColorTexture,
|
|
494
|
-
hasNormalTexture: materialTextureMetadata.hasNormalTexture,
|
|
495
|
-
hasUv: texcoords.length === positions.length,
|
|
496
|
-
hasNormals: normals.length === positions.length,
|
|
497
|
-
professionalRenderable: materialTextureMetadata.hasBaseColorTexture && materialTextureMetadata.hasNormalTexture && materialTextureMetadata.missingTextureReferences.length === 0 && texcoords.length === positions.length && normals.length === positions.length && skin.joints.length > 0,
|
|
498
|
-
clips,
|
|
499
|
-
sample(activeClipId, clipTimeMs) {
|
|
500
|
-
const clip = clips.find((candidate) => candidate.id === activeClipId) ?? clips[0] ?? null;
|
|
501
|
-
const overrides = sampleClipOverrides(clip, Math.max(0, clipTimeMs) / 1e3);
|
|
502
|
-
const worldMatrices = computeWorldMatrices(nodes, overrides);
|
|
503
|
-
const vertices = skinVertices({
|
|
504
|
-
positions,
|
|
505
|
-
joints,
|
|
506
|
-
weights,
|
|
507
|
-
skin,
|
|
508
|
-
inverseBindMatrices,
|
|
509
|
-
worldMatrices
|
|
510
|
-
});
|
|
511
|
-
return {
|
|
512
|
-
vertices,
|
|
513
|
-
indices,
|
|
514
|
-
bounds: deriveBounds(vertices),
|
|
515
|
-
bindBounds: bounds,
|
|
516
|
-
activeClipRenderable: Boolean(clip),
|
|
517
|
-
activeClipDurationSeconds: clip?.durationSeconds ?? 0,
|
|
518
|
-
activeClipRootTranslationDistance: clipRootMotionDistances.get(clip?.id) ?? 0
|
|
519
|
-
};
|
|
514
|
+
};
|
|
515
|
+
const start = () => {
|
|
516
|
+
if (destroyed) {
|
|
517
|
+
throw new Error("Renderer was destroyed.");
|
|
518
|
+
}
|
|
519
|
+
if (running) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
running = true;
|
|
523
|
+
if (typeof requestAnimationFrame === "function") {
|
|
524
|
+
rafId = requestAnimationFrame(tick);
|
|
525
|
+
} else {
|
|
526
|
+
renderOnce();
|
|
520
527
|
}
|
|
528
|
+
return true;
|
|
521
529
|
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const jointIndices = joints[vertexIndex] ?? [0, 0, 0, 0];
|
|
526
|
-
const jointWeights = weights[vertexIndex] ?? [1, 0, 0, 0];
|
|
527
|
-
const skinned = [0, 0, 0];
|
|
528
|
-
for (let influence = 0; influence < 4; influence += 1) {
|
|
529
|
-
const weight = jointWeights[influence] ?? 0;
|
|
530
|
-
if (weight <= 0) {
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
const jointNodeIndex = skin.joints[jointIndices[influence] ?? 0] ?? skin.joints[0];
|
|
534
|
-
const jointMatrix = multiplyMatrix(worldMatrices[jointNodeIndex] ?? identityMatrix(), inverseBindMatrices[jointIndices[influence] ?? 0] ?? identityMatrix());
|
|
535
|
-
const transformed = transformPoint(jointMatrix, position);
|
|
536
|
-
skinned[0] += transformed[0] * weight;
|
|
537
|
-
skinned[1] += transformed[1] * weight;
|
|
538
|
-
skinned[2] += transformed[2] * weight;
|
|
530
|
+
const stop = () => {
|
|
531
|
+
if (!running) {
|
|
532
|
+
return false;
|
|
539
533
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
534
|
+
running = false;
|
|
535
|
+
if (rafId !== null && typeof cancelAnimationFrame === "function") {
|
|
536
|
+
cancelAnimationFrame(rafId);
|
|
537
|
+
}
|
|
538
|
+
rafId = null;
|
|
539
|
+
return true;
|
|
540
|
+
};
|
|
541
|
+
const resize = (cssWidth, cssHeight, devicePixelRatio = globalThis.devicePixelRatio ?? 1) => {
|
|
542
|
+
const width = Math.max(1, Math.floor(cssWidth * devicePixelRatio));
|
|
543
|
+
const height = Math.max(1, Math.floor(cssHeight * devicePixelRatio));
|
|
544
|
+
targetCanvas.width = width;
|
|
545
|
+
targetCanvas.height = height;
|
|
546
|
+
if (targetCanvas.style) {
|
|
547
|
+
targetCanvas.style.width = `${Math.max(1, Math.floor(cssWidth))}px`;
|
|
548
|
+
targetCanvas.style.height = `${Math.max(1, Math.floor(cssHeight))}px`;
|
|
549
|
+
}
|
|
550
|
+
return { width, height };
|
|
551
|
+
};
|
|
552
|
+
const setClearColor = (value) => {
|
|
553
|
+
clear = normalizeColor(value);
|
|
554
|
+
return [...clear];
|
|
555
|
+
};
|
|
556
|
+
const setXrActive = (active) => {
|
|
557
|
+
xrActive = Boolean(active);
|
|
558
|
+
};
|
|
559
|
+
const getSnapshot = () => {
|
|
560
|
+
const width = Number(targetCanvas.width) || 0;
|
|
561
|
+
const height = Number(targetCanvas.height) || 0;
|
|
562
|
+
return {
|
|
563
|
+
running,
|
|
564
|
+
frame,
|
|
565
|
+
lastTimestamp,
|
|
566
|
+
format: resolvedFormat,
|
|
567
|
+
width,
|
|
568
|
+
height,
|
|
569
|
+
xrActive
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
const renderer = {
|
|
573
|
+
canvas: targetCanvas,
|
|
574
|
+
context,
|
|
575
|
+
device,
|
|
576
|
+
format: resolvedFormat,
|
|
577
|
+
renderOnce,
|
|
578
|
+
start,
|
|
579
|
+
stop,
|
|
580
|
+
resize,
|
|
581
|
+
setClearColor,
|
|
582
|
+
setXrActive,
|
|
583
|
+
getSnapshot,
|
|
584
|
+
bindXrManager(xrManager, bindOptions = {}) {
|
|
585
|
+
if (detachXrBinding) {
|
|
586
|
+
detachXrBinding();
|
|
587
|
+
}
|
|
588
|
+
detachXrBinding = bindRendererToXrManager(renderer, xrManager, bindOptions);
|
|
589
|
+
return detachXrBinding;
|
|
590
|
+
},
|
|
591
|
+
destroy() {
|
|
592
|
+
stop();
|
|
593
|
+
destroyed = true;
|
|
594
|
+
if (detachXrBinding) {
|
|
595
|
+
detachXrBinding();
|
|
596
|
+
detachXrBinding = null;
|
|
597
|
+
}
|
|
598
|
+
if (typeof context.unconfigure === "function") {
|
|
599
|
+
context.unconfigure();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
return renderer;
|
|
576
604
|
}
|
|
577
|
-
function
|
|
578
|
-
if (
|
|
579
|
-
return
|
|
605
|
+
function snapshotFromXrManager(xrManager) {
|
|
606
|
+
if (xrManager && typeof xrManager.getState === "function") {
|
|
607
|
+
return xrManager.getState();
|
|
580
608
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
Number.isFinite(Number(value[1])) ? Number(value[1]) : fallback[1],
|
|
584
|
-
Number.isFinite(Number(value[2])) ? Number(value[2]) : fallback[2]
|
|
585
|
-
];
|
|
586
|
-
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
587
|
-
if (length <= 1e-6) {
|
|
588
|
-
return [...fallback];
|
|
609
|
+
if (xrManager?.store && typeof xrManager.store.getSnapshot === "function") {
|
|
610
|
+
return xrManager.store.getSnapshot();
|
|
589
611
|
}
|
|
590
|
-
return
|
|
591
|
-
}
|
|
592
|
-
function clamp(value, min, max) {
|
|
593
|
-
return Math.min(max, Math.max(min, value));
|
|
594
|
-
}
|
|
595
|
-
function bezierY([, y1, , y2], t) {
|
|
596
|
-
const u = 1 - t;
|
|
597
|
-
return 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t;
|
|
598
|
-
}
|
|
599
|
-
function durationFromBeats(beats) {
|
|
600
|
-
return beats.reduce((total, beat) => total + Math.max(0, beat.durationMs ?? 0), 0);
|
|
612
|
+
return null;
|
|
601
613
|
}
|
|
602
|
-
function
|
|
603
|
-
if (!
|
|
604
|
-
|
|
614
|
+
function bindRendererToXrManager(renderer, xrManager, options = {}) {
|
|
615
|
+
if (!xrManager || typeof xrManager.subscribe !== "function") {
|
|
616
|
+
throw new Error("XR manager must expose subscribe(listener). Use @plasius/gpu-xr createXrManager().");
|
|
605
617
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (loopTimeMs < cursor + durationMs) {
|
|
613
|
-
return { beat, beatTimeMs: loopTimeMs - cursor, durationMs };
|
|
618
|
+
const { onSessionStart, onSessionEnd } = options;
|
|
619
|
+
let previousSession = null;
|
|
620
|
+
const applyState = (state) => {
|
|
621
|
+
const session = state?.activeSession ?? null;
|
|
622
|
+
if (session === previousSession) {
|
|
623
|
+
return;
|
|
614
624
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return { beat: fallback, beatTimeMs: fallback?.durationMs ?? 0, durationMs: fallback?.durationMs ?? 1 };
|
|
619
|
-
}
|
|
620
|
-
function resolveRoutePosition(route, timeMs) {
|
|
621
|
-
if (!route.length) {
|
|
622
|
-
return [0, 0, 0];
|
|
623
|
-
}
|
|
624
|
-
if (route.length === 1 || timeMs <= (route[0].arriveMs ?? 0)) {
|
|
625
|
-
return [...route[0].position];
|
|
626
|
-
}
|
|
627
|
-
for (let index = 1; index < route.length; index += 1) {
|
|
628
|
-
const previous = route[index - 1];
|
|
629
|
-
const next = route[index];
|
|
630
|
-
const start = previous.arriveMs ?? 0;
|
|
631
|
-
const end = next.arriveMs ?? start + 1;
|
|
632
|
-
if (timeMs <= end) {
|
|
633
|
-
return lerp3(previous.position, next.position, clamp01((timeMs - start) / Math.max(1, end - start)));
|
|
625
|
+
previousSession = session;
|
|
626
|
+
if (typeof renderer.setXrActive === "function") {
|
|
627
|
+
renderer.setXrActive(Boolean(session));
|
|
634
628
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
function resolveRouteForward(route, timeMs) {
|
|
639
|
-
if (route.length < 2) {
|
|
640
|
-
return [0, 0, -1];
|
|
641
|
-
}
|
|
642
|
-
let previous = route[0];
|
|
643
|
-
let next = route[1];
|
|
644
|
-
for (let index = 1; index < route.length; index += 1) {
|
|
645
|
-
next = route[index];
|
|
646
|
-
if (timeMs <= (next.arriveMs ?? 0)) {
|
|
647
|
-
previous = route[index - 1];
|
|
648
|
-
break;
|
|
629
|
+
if (session && typeof onSessionStart === "function") {
|
|
630
|
+
onSessionStart(session, renderer);
|
|
649
631
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
function distance3(a = [0, 0, 0], b = [0, 0, 0]) {
|
|
658
|
-
return Math.hypot(
|
|
659
|
-
(b[0] ?? 0) - (a[0] ?? 0),
|
|
660
|
-
(b[1] ?? 0) - (a[1] ?? 0),
|
|
661
|
-
(b[2] ?? 0) - (a[2] ?? 0)
|
|
662
|
-
);
|
|
632
|
+
if (!session && typeof onSessionEnd === "function") {
|
|
633
|
+
onSessionEnd(renderer);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
applyState(snapshotFromXrManager(xrManager));
|
|
637
|
+
return xrManager.subscribe(applyState);
|
|
663
638
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
639
|
+
var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
|
|
640
|
+
|
|
641
|
+
// src/native-scene-shader.js
|
|
642
|
+
var nativeSceneShader = (
|
|
643
|
+
/* wgsl */
|
|
644
|
+
`
|
|
645
|
+
struct Frame {
|
|
646
|
+
eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
|
|
647
|
+
settings: vec4f, sun: vec4f,
|
|
648
|
+
lightRight: vec4f, lightUp: vec4f, lightForward: vec4f, lightOrigin: vec4f,
|
|
649
|
+
viewport: vec4f, wakes: array<vec4f, 4>, info: vec4f,
|
|
650
|
+
};
|
|
651
|
+
@group(0) @binding(0) var<uniform> frame: Frame;
|
|
652
|
+
@group(1) @binding(0) var shadowMap: texture_depth_2d;
|
|
653
|
+
@group(1) @binding(1) var shadowSampler: sampler_comparison;
|
|
654
|
+
@group(2) @binding(0) var reflectionMap: texture_2d<f32>;
|
|
655
|
+
@group(2) @binding(1) var reflectionSampler: sampler;
|
|
656
|
+
struct Surface {
|
|
657
|
+
@builtin(position) clip: vec4f,
|
|
658
|
+
@location(0) world: vec3f, @location(1) normal: vec3f,
|
|
659
|
+
@location(2) colour: vec3f, @location(3) material: vec3f,
|
|
660
|
+
};
|
|
661
|
+
fn project(p: vec3f) -> vec4f {
|
|
662
|
+
let d = p - frame.eye.xyz;
|
|
663
|
+
let z = dot(d, frame.forward.xyz);
|
|
664
|
+
return vec4f(dot(d, frame.right.xyz) / (frame.settings.x * frame.settings.y),
|
|
665
|
+
dot(d, frame.up.xyz) / frame.settings.x, (z - 0.08) * 500.0 / 499.92, z);
|
|
666
|
+
}
|
|
667
|
+
fn lightProject(p: vec3f) -> vec4f {
|
|
668
|
+
let d = p - frame.lightOrigin.xyz;
|
|
669
|
+
return vec4f(dot(d, frame.lightRight.xyz) / 34.0,
|
|
670
|
+
dot(d, frame.lightUp.xyz) / 34.0, dot(d, frame.lightForward.xyz) / 100.0, 1.0);
|
|
671
|
+
}
|
|
672
|
+
@vertex fn surfaceVertex(@location(0) p: vec3f, @location(1) n: vec3f,
|
|
673
|
+
@location(2) c: vec3f, @location(3) m: vec3f) -> Surface {
|
|
674
|
+
return Surface(project(p), p, n, c, m);
|
|
675
|
+
}
|
|
676
|
+
@vertex fn shadowVertex(@location(0) p: vec3f) -> @builtin(position) vec4f {
|
|
677
|
+
return lightProject(p);
|
|
678
|
+
}
|
|
679
|
+
@vertex fn fullscreenVertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
680
|
+
let positions = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
|
|
681
|
+
return vec4f(positions[i], 1, 1);
|
|
682
|
+
}
|
|
683
|
+
fn cameraRay(pixel: vec2f) -> vec3f {
|
|
684
|
+
let uv = pixel / frame.viewport.xy * 2.0 - 1.0;
|
|
685
|
+
return normalize(frame.forward.xyz + frame.right.xyz * uv.x * frame.settings.x * frame.settings.y
|
|
686
|
+
- frame.up.xyz * uv.y * frame.settings.x);
|
|
687
|
+
}
|
|
688
|
+
fn display(c: vec3f) -> vec3f {
|
|
689
|
+
let x = max(c * frame.settings.w, vec3f(0));
|
|
690
|
+
let mapped = clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3f(0), vec3f(1));
|
|
691
|
+
return pow(mapped, vec3f(1.0 / 2.2));
|
|
692
|
+
}
|
|
693
|
+
fn hash(p: vec3f) -> f32 { return fract(sin(dot(p, vec3f(127.1, 311.7, 74.7))) * 43758.5453); }
|
|
694
|
+
fn noise(p: vec3f) -> f32 {
|
|
695
|
+
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
|
|
696
|
+
return mix(mix(mix(hash(i), hash(i + vec3f(1,0,0)), u.x),
|
|
697
|
+
mix(hash(i + vec3f(0,1,0)), hash(i + vec3f(1,1,0)), u.x), u.y),
|
|
698
|
+
mix(mix(hash(i + vec3f(0,0,1)), hash(i + vec3f(1,0,1)), u.x),
|
|
699
|
+
mix(hash(i + vec3f(0,1,1)), hash(i + vec3f(1,1,1)), u.x), u.y), u.z);
|
|
700
|
+
}
|
|
701
|
+
fn sky(ray: vec3f) -> vec3f {
|
|
702
|
+
let elevation = pow(clamp(ray.y * 0.7 + 0.16, 0.0, 1.0), 0.6);
|
|
703
|
+
var c = mix(vec3f(0.42, 0.53, 0.61), vec3f(0.07, 0.19, 0.37), elevation);
|
|
704
|
+
let sunAngle = dot(ray, frame.sun.xyz);
|
|
705
|
+
c += vec3f(1.0, 0.73, 0.43) * pow(max(sunAngle, 0.0), 64.0) * 0.35;
|
|
706
|
+
c += vec3f(4.0, 3.2, 2.1) * smoothstep(0.99965, 0.99985, sunAngle);
|
|
707
|
+
let cloudPoint = ray / max(0.12, ray.y) * 2.1 + vec3f(0, 0, 8);
|
|
708
|
+
let cloud = smoothstep(0.49, 0.77, noise(cloudPoint) * 0.65 + noise(cloudPoint * 2.1) * 0.35);
|
|
709
|
+
c = mix(c, vec3f(0.85, 0.86, 0.82), cloud * smoothstep(0.03, 0.35, ray.y) * 0.65);
|
|
710
|
+
return c;
|
|
711
|
+
}
|
|
712
|
+
@fragment fn skyFragment(@builtin(position) pixel: vec4f) -> @location(0) vec4f {
|
|
713
|
+
return vec4f(display(sky(cameraRay(pixel.xy))), 1);
|
|
714
|
+
}
|
|
715
|
+
fn visibility(p: vec3f, n: vec3f) -> f32 {
|
|
716
|
+
let q = lightProject(p + n * 0.035).xyz;
|
|
717
|
+
let uv = vec2f(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5);
|
|
718
|
+
if (any(uv < vec2f(0.005)) || any(uv > vec2f(0.995)) || q.z < 0.0 || q.z > 1.0) { return 1.0; }
|
|
719
|
+
var result = 0.0;
|
|
720
|
+
for (var y = -1; y <= 1; y++) {
|
|
721
|
+
for (var x = -1; x <= 1; x++) {
|
|
722
|
+
result += textureSampleCompareLevel(shadowMap, shadowSampler, uv + vec2f(f32(x), f32(y)) / 2048.0, q.z - 0.0006);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return result / 9.0;
|
|
726
|
+
}
|
|
727
|
+
fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
|
|
728
|
+
let l = frame.sun.xyz; let h = normalize(l + v);
|
|
729
|
+
let nl = max(dot(n,l),0.0); let nv = max(dot(n,v),0.001);
|
|
730
|
+
let nh = max(dot(n,h),0.0); let vh = max(dot(v,h),0.0);
|
|
731
|
+
let a = max(0.035, roughness * roughness); let a2 = a * a;
|
|
732
|
+
let d = a2 / max(0.00001, 3.14159265 * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
|
|
733
|
+
let k = pow(roughness + 1.0, 2.0) / 8.0;
|
|
734
|
+
let g = (nl / (nl * (1.0-k) + k)) * (nv / (nv * (1.0-k) + k));
|
|
735
|
+
let f0 = mix(vec3f(0.04), c, metal);
|
|
736
|
+
let f = f0 + (1.0-f0) * pow(1.0-vh, 5.0);
|
|
737
|
+
return ((1.0-f) * (1.0-metal) * c / 3.14159265 + d * g * f / max(0.001, 4.0*nl*nv)) * nl;
|
|
738
|
+
}
|
|
739
|
+
fn fog(c: vec3f, p: vec3f) -> vec3f {
|
|
740
|
+
let amount = 1.0 - exp(-distance(frame.eye.xyz, p) * 0.0018);
|
|
741
|
+
return mix(c, vec3f(0.32, 0.44, 0.55), amount);
|
|
742
|
+
}
|
|
743
|
+
fn surfaceRadiance(s: Surface, front: bool) -> vec3f {
|
|
744
|
+
let facing = select(select(-1.0,1.0,front),select(1.0,-1.0,front),frame.info.y>0.5);
|
|
745
|
+
var n = normalize(s.normal) * facing;
|
|
746
|
+
let v = normalize(frame.eye.xyz - s.world);
|
|
747
|
+
var c = s.colour;
|
|
748
|
+
if (s.material.z > 2.5) {
|
|
749
|
+
let rock = smoothstep(0.45,0.68,noise(s.world*0.27));
|
|
750
|
+
c = mix(c,vec3f(0.16,0.145,0.11),rock*0.7);
|
|
751
|
+
c *= 0.75+0.45*noise(s.world*2.4);
|
|
752
|
+
n = normalize(n+vec3f(noise(s.world*4.0)-0.5,0,noise(s.world*4.0+vec3f(13))-0.5)*0.28);
|
|
753
|
+
} else if (s.material.z > 0.5 && s.material.z < 1.5) {
|
|
754
|
+
let grain = noise(s.world * vec3f(18, 3, 1.6));
|
|
755
|
+
c *= 0.82 + 0.30 * grain;
|
|
756
|
+
} else if (s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
|
|
757
|
+
let hemi = mix(vec3f(0.06,0.05,0.035), vec3f(0.16,0.22,0.29), n.y * 0.5 + 0.5);
|
|
758
|
+
let direct = brdf(c, n, v, s.material.x, s.material.y) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
|
|
759
|
+
let ambient = c * hemi * (1.0 - s.material.y * 0.7);
|
|
760
|
+
let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, s.material.y) * (1.0-s.material.x) * 0.30;
|
|
761
|
+
return fog(direct + ambient + env, s.world);
|
|
762
|
+
}
|
|
763
|
+
@fragment fn surfaceFragment(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
764
|
+
return vec4f(display(surfaceRadiance(s,front)),1);
|
|
765
|
+
}
|
|
766
|
+
fn wave(p: vec2f) -> f32 {
|
|
767
|
+
let t = frame.settings.z;
|
|
768
|
+
return sin(dot(p,vec2f(0.35,0.23))-t*0.85)*0.13
|
|
769
|
+
+ sin(dot(p,vec2f(-0.61,0.42))+t*1.12)*0.065
|
|
770
|
+
+ sin(dot(p,vec2f(1.2,0.83))-t*1.4)*0.025
|
|
771
|
+
+ sin(dot(p,vec2f(3.1,-2.1))+t*1.8)*0.009;
|
|
772
|
+
}
|
|
773
|
+
struct WaterResult { @location(0) colour: vec4f, @builtin(frag_depth) depth: f32 };
|
|
774
|
+
@fragment fn waterFragment(@builtin(position) pixel: vec4f) -> WaterResult {
|
|
775
|
+
let ray = cameraRay(pixel.xy);
|
|
776
|
+
if (ray.y > -0.001) { discard; }
|
|
777
|
+
var dist = (frame.viewport.z - frame.eye.y) / ray.y;
|
|
778
|
+
var p = frame.eye.xyz + ray * dist;
|
|
779
|
+
for (var i=0; i<3; i++) {
|
|
780
|
+
dist = (frame.viewport.z + wave(p.xz) - frame.eye.y) / ray.y;
|
|
781
|
+
p = frame.eye.xyz + ray * dist;
|
|
782
|
+
}
|
|
783
|
+
if (dist < 0.08 || dist > 499.0) { discard; }
|
|
784
|
+
let eps = 0.03;
|
|
785
|
+
let waveNormal = normalize(vec3f((wave(p.xz-vec2f(eps,0))-wave(p.xz+vec2f(eps,0)))/(2.0*eps),
|
|
786
|
+
1.0, (wave(p.xz-vec2f(0,eps))-wave(p.xz+vec2f(0,eps)))/(2.0*eps)));
|
|
787
|
+
let n = normalize(mix(waveNormal,vec3f(0,1,0),smoothstep(35.0,150.0,dist)));
|
|
788
|
+
let v = -ray;
|
|
789
|
+
let fresnel = 0.02 + 0.98 * pow(1.0-max(dot(n,v),0.0),5.0);
|
|
790
|
+
let uv = clamp(pixel.xy / frame.viewport.xy + n.xz * 0.018, vec2f(0.002), vec2f(0.998));
|
|
791
|
+
// Reflection target is HDR linear; tone mapping happens once after mixing.
|
|
792
|
+
let reflected = textureSampleLevel(reflectionMap, reflectionSampler, uv, 0).rgb;
|
|
793
|
+
let shadow = visibility(p, n);
|
|
794
|
+
var c = mix(vec3f(0.008,0.05,0.065) * (0.5+0.5*shadow), reflected, fresnel);
|
|
795
|
+
c += brdf(vec3f(0.01),n,v,0.14,0.0) * vec3f(2.5,2.15,1.7) * shadow;
|
|
796
|
+
var foam = 0.0;
|
|
797
|
+
for (var i=0u; i<4u; i++) {
|
|
798
|
+
if (f32(i) >= frame.info.x) { break; }
|
|
799
|
+
let w = frame.wakes[i]; let delta = p.xz - w.xy;
|
|
800
|
+
let along = dot(delta, vec2f(sin(w.z), cos(w.z)));
|
|
801
|
+
let across = abs(dot(delta,vec2f(cos(w.z),-sin(w.z))));
|
|
802
|
+
let ribbon = exp(-pow((across - max(0.0,-along)*0.28 - 0.65)*4.0,2.0));
|
|
803
|
+
foam += ribbon * (1.0-smoothstep(-1.0,0.5,along)) * exp(min(0.0,along)*0.25) * (0.4+0.6*noise(vec3f(p.x*5.0,p.z*5.0,frame.settings.z*0.5)));
|
|
804
|
+
}
|
|
805
|
+
c = mix(c,vec3f(0.48,0.61,0.62),clamp(foam*0.42,0.0,0.55));
|
|
806
|
+
let clip = project(p);
|
|
807
|
+
return WaterResult(vec4f(display(fog(c,p)),1),clip.z/clip.w);
|
|
808
|
+
}
|
|
809
|
+
// Keep reflection radiance linear, including sky. Reusing the lighting calculation
|
|
810
|
+
// rather than sampling a screenshot avoids double tone mapping on the water.
|
|
811
|
+
@fragment fn reflectionSky(@builtin(position) pixel: vec4f) -> @location(0) vec4f {
|
|
812
|
+
return vec4f(sky(cameraRay(pixel.xy)),1);
|
|
813
|
+
}
|
|
814
|
+
@fragment fn reflectionSurface(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
815
|
+
if (s.world.y < frame.viewport.z) { discard; }
|
|
816
|
+
return vec4f(surfaceRadiance(s,front),1);
|
|
817
|
+
}
|
|
818
|
+
`
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// src/native-scene-renderer.js
|
|
822
|
+
var STRIDE = 12;
|
|
823
|
+
var UNIFORM_BYTES = 256;
|
|
824
|
+
var SHADOW_SIZE = 2048;
|
|
825
|
+
var sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
826
|
+
var cross = (a, b) => [
|
|
827
|
+
a[1] * b[2] - a[2] * b[1],
|
|
828
|
+
a[2] * b[0] - a[0] * b[2],
|
|
829
|
+
a[0] * b[1] - a[1] * b[0]
|
|
830
|
+
];
|
|
831
|
+
var unit = (a) => {
|
|
832
|
+
const length = Math.hypot(...a);
|
|
833
|
+
return a.map((v) => v / length);
|
|
834
|
+
};
|
|
835
|
+
var finiteVector = (a) => Array.isArray(a) && a.length === 3 && a.every(Number.isFinite);
|
|
836
|
+
function validateFrame(frame, maxVertices) {
|
|
837
|
+
const { vertices, camera, time = 0 } = frame ?? {};
|
|
838
|
+
if (!(vertices instanceof Float32Array) || vertices.length % (STRIDE * 3) !== 0 || vertices.length > maxVertices * STRIDE || !vertices.every(Number.isFinite)) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`vertices must contain bounded, finite interleaved triangles (12 floats per vertex); received ${vertices?.length ?? 0} floats; first nonfinite component ${vertices instanceof Float32Array ? vertices.findIndex((v) => !Number.isFinite(v)) : "unknown"}.`
|
|
841
|
+
);
|
|
680
842
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const rootDistance = Number(profile.rootTranslationDistance ?? profile.rootTranslation?.distance ?? 0);
|
|
686
|
-
if (Number.isFinite(rootDistance) && rootDistance > 0) {
|
|
687
|
-
return rootDistance;
|
|
843
|
+
if (!finiteVector(camera?.eye) || !finiteVector(camera?.target) || Math.hypot(...sub(camera.eye, camera.target)) < 1e-3 || !Number.isFinite(camera.fov ?? 54) || (camera.fov ?? 54) < 10 || (camera.fov ?? 54) > 120) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
"camera must provide distinct finite eye/target vectors and a 10\u2013120 degree fov."
|
|
846
|
+
);
|
|
688
847
|
}
|
|
689
|
-
|
|
690
|
-
if (Number.isFinite(
|
|
691
|
-
|
|
848
|
+
if (!Number.isFinite(time)) throw new Error("time must be finite.");
|
|
849
|
+
if (!Number.isFinite(frame.waterLevel ?? 0))
|
|
850
|
+
throw new Error("waterLevel must be finite.");
|
|
851
|
+
if (frame.wakes !== void 0 && (!Array.isArray(frame.wakes) || frame.wakes.length > 4 || frame.wakes.some(
|
|
852
|
+
(w) => !Array.isArray(w) || w.length !== 3 || !w.every(Number.isFinite)
|
|
853
|
+
))) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
"wakes must contain at most four finite [x, z, heading] entries."
|
|
856
|
+
);
|
|
692
857
|
}
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
858
|
+
return { ...frame, time };
|
|
859
|
+
}
|
|
860
|
+
function frameUniforms(frame, width, height, reflected = false) {
|
|
861
|
+
const eye = [...frame.camera.eye];
|
|
862
|
+
let forward = unit(sub(frame.camera.target, eye));
|
|
863
|
+
const referenceUp = Math.abs(forward[1]) > 0.999 ? [0, 0, 1] : [0, 1, 0];
|
|
864
|
+
const right = unit(cross(forward, referenceUp));
|
|
865
|
+
const up = unit(cross(right, forward));
|
|
866
|
+
const waterLevel = frame.waterLevel ?? 0;
|
|
867
|
+
if (reflected) {
|
|
868
|
+
eye[1] = 2 * waterLevel - eye[1];
|
|
869
|
+
forward = [forward[0], -forward[1], forward[2]];
|
|
870
|
+
right[1] *= -1;
|
|
871
|
+
up[1] *= -1;
|
|
872
|
+
}
|
|
873
|
+
const sun = unit([-0.62, 0.48, 0.62]);
|
|
874
|
+
const lightForward = sun.map((x) => -x);
|
|
875
|
+
const lightRight = unit(cross(lightForward, [0, 1, 0]));
|
|
876
|
+
const lightUp = unit(cross(lightRight, lightForward));
|
|
877
|
+
const uniforms = new Float32Array(64);
|
|
878
|
+
uniforms.set(eye, 0);
|
|
879
|
+
uniforms.set(right, 4);
|
|
880
|
+
uniforms.set(up, 8);
|
|
881
|
+
uniforms.set(forward, 12);
|
|
882
|
+
uniforms.set(
|
|
883
|
+
[
|
|
884
|
+
Math.tan((frame.camera.fov ?? 54) * Math.PI / 360),
|
|
885
|
+
width / height,
|
|
886
|
+
frame.time,
|
|
887
|
+
0.86
|
|
888
|
+
],
|
|
889
|
+
16
|
|
890
|
+
);
|
|
891
|
+
uniforms.set(sun, 20);
|
|
892
|
+
uniforms.set(lightRight, 24);
|
|
893
|
+
uniforms.set(lightUp, 28);
|
|
894
|
+
uniforms.set(lightForward, 32);
|
|
895
|
+
uniforms.set([sun[0] * 45, sun[1] * 45, sun[2] * 45 + 4], 36);
|
|
896
|
+
uniforms.set([width, height, waterLevel, 0], 40);
|
|
897
|
+
(frame.wakes ?? []).forEach(
|
|
898
|
+
(wake, index) => uniforms.set([...wake, 0], 44 + index * 4)
|
|
899
|
+
);
|
|
900
|
+
uniforms.set([(frame.wakes ?? []).length, reflected ? 1 : 0, 0, 0], 60);
|
|
901
|
+
return uniforms;
|
|
902
|
+
}
|
|
903
|
+
async function createNativeSceneRenderer(options = {}) {
|
|
904
|
+
const maxVertices = options.maxVertices ?? 3e5;
|
|
905
|
+
if (!Number.isInteger(maxVertices) || maxVertices < 3 || maxVertices > 6e5) {
|
|
906
|
+
throw new Error("maxVertices must be an integer between 3 and 600000.");
|
|
907
|
+
}
|
|
908
|
+
let base;
|
|
909
|
+
let destroyed = false;
|
|
910
|
+
let available = true;
|
|
911
|
+
let targets = null;
|
|
912
|
+
let vertexBuffer = null;
|
|
913
|
+
let vertexCapacity = 0;
|
|
914
|
+
let pendingFrame = null;
|
|
915
|
+
let submittedFrames = 0;
|
|
916
|
+
let vertexCount = 0;
|
|
917
|
+
const owned = /* @__PURE__ */ new Set();
|
|
918
|
+
const own = (resource) => {
|
|
919
|
+
owned.add(resource);
|
|
920
|
+
return resource;
|
|
921
|
+
};
|
|
922
|
+
const release = (resource) => {
|
|
923
|
+
resource.destroy();
|
|
924
|
+
owned.delete(resource);
|
|
925
|
+
};
|
|
926
|
+
const destroy = () => {
|
|
927
|
+
if (destroyed) return;
|
|
928
|
+
destroyed = true;
|
|
929
|
+
base?.destroy();
|
|
930
|
+
for (const resource of owned) resource.destroy();
|
|
931
|
+
owned.clear();
|
|
932
|
+
base?.device.destroy?.();
|
|
933
|
+
};
|
|
934
|
+
try {
|
|
935
|
+
let ensureTargets = function() {
|
|
936
|
+
const limit = Math.min(device.limits.maxTextureDimension2D, 3840);
|
|
937
|
+
const requestedWidth = Math.max(1, Number(base.canvas.width) || 1);
|
|
938
|
+
const requestedHeight = Math.max(1, Number(base.canvas.height) || 1);
|
|
939
|
+
const scale2 = Math.min(
|
|
940
|
+
1,
|
|
941
|
+
limit / requestedWidth,
|
|
942
|
+
limit / requestedHeight,
|
|
943
|
+
Math.sqrt(1920 * 1080 / (requestedWidth * requestedHeight))
|
|
944
|
+
);
|
|
945
|
+
const width = Math.max(1, Math.floor(requestedWidth * scale2));
|
|
946
|
+
const height = Math.max(1, Math.floor(requestedHeight * scale2));
|
|
947
|
+
base.canvas.width = width;
|
|
948
|
+
base.canvas.height = height;
|
|
949
|
+
if (targets?.width === width && targets?.height === height) return;
|
|
950
|
+
if (targets) for (const texture2 of targets.textures) release(texture2);
|
|
951
|
+
const reflectionWidth = Math.min(1280, Math.max(1, Math.ceil(width / 2)));
|
|
952
|
+
const reflectionHeight = Math.max(
|
|
953
|
+
1,
|
|
954
|
+
Math.round(height * reflectionWidth / width)
|
|
955
|
+
);
|
|
956
|
+
const texture = (label, w, h, pixelFormat, sampleCount = 1, usage = 16) => own(
|
|
957
|
+
device.createTexture({
|
|
958
|
+
label,
|
|
959
|
+
size: [w, h],
|
|
960
|
+
format: pixelFormat,
|
|
961
|
+
sampleCount,
|
|
962
|
+
usage
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
const colour = texture("native.msaa-colour", width, height, format, 4);
|
|
966
|
+
const depth = texture(
|
|
967
|
+
"native.msaa-depth",
|
|
968
|
+
width,
|
|
969
|
+
height,
|
|
970
|
+
"depth32float",
|
|
971
|
+
4
|
|
972
|
+
);
|
|
973
|
+
const reflection = texture(
|
|
974
|
+
"native.reflection",
|
|
975
|
+
reflectionWidth,
|
|
976
|
+
reflectionHeight,
|
|
977
|
+
"rgba16float",
|
|
978
|
+
1,
|
|
979
|
+
20
|
|
980
|
+
);
|
|
981
|
+
const reflectionDepth = texture(
|
|
982
|
+
"native.reflection-depth",
|
|
983
|
+
reflectionWidth,
|
|
984
|
+
reflectionHeight,
|
|
985
|
+
"depth32float"
|
|
986
|
+
);
|
|
987
|
+
const reflectionView = reflection.createView();
|
|
988
|
+
targets = {
|
|
989
|
+
width,
|
|
990
|
+
height,
|
|
991
|
+
reflectionWidth,
|
|
992
|
+
reflectionHeight,
|
|
993
|
+
colour: colour.createView(),
|
|
994
|
+
depth: depth.createView(),
|
|
995
|
+
reflection: reflectionView,
|
|
996
|
+
reflectionDepth: reflectionDepth.createView(),
|
|
997
|
+
textures: [colour, depth, reflection, reflectionDepth],
|
|
998
|
+
reflectionGroup: device.createBindGroup({
|
|
999
|
+
layout: reflectionLayout,
|
|
1000
|
+
entries: [
|
|
1001
|
+
{ binding: 0, resource: reflectionView },
|
|
1002
|
+
{ binding: 1, resource: reflectionSampler }
|
|
1003
|
+
]
|
|
1004
|
+
})
|
|
1005
|
+
};
|
|
1006
|
+
}, encode = function({ encoder, view }) {
|
|
1007
|
+
const depthAttachment = (depthView) => ({
|
|
1008
|
+
view: depthView,
|
|
1009
|
+
depthLoadOp: "clear",
|
|
1010
|
+
depthClearValue: 1,
|
|
1011
|
+
depthStoreOp: "store"
|
|
1012
|
+
});
|
|
1013
|
+
const drawGeometry = (pass2, p, uniforms) => {
|
|
1014
|
+
pass2.setPipeline(p);
|
|
1015
|
+
pass2.setBindGroup(0, uniforms);
|
|
1016
|
+
if (p !== shadowPipeline) pass2.setBindGroup(1, shadowGroup);
|
|
1017
|
+
if (vertexCount > 0) {
|
|
1018
|
+
pass2.setVertexBuffer(0, vertexBuffer);
|
|
1019
|
+
pass2.draw(vertexCount);
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
const shadowPass = encoder.beginRenderPass({
|
|
1023
|
+
label: "native.shadow",
|
|
1024
|
+
colorAttachments: [],
|
|
1025
|
+
depthStencilAttachment: depthAttachment(shadowView)
|
|
1026
|
+
});
|
|
1027
|
+
drawGeometry(shadowPass, shadowPipeline, mainGroup);
|
|
1028
|
+
shadowPass.end();
|
|
1029
|
+
const reflectedPass = encoder.beginRenderPass({
|
|
1030
|
+
label: "native.reflection",
|
|
1031
|
+
colorAttachments: [
|
|
1032
|
+
{
|
|
1033
|
+
view: targets.reflection,
|
|
1034
|
+
loadOp: "clear",
|
|
1035
|
+
storeOp: "store",
|
|
1036
|
+
clearValue: [0, 0, 0, 1]
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
depthStencilAttachment: depthAttachment(targets.reflectionDepth)
|
|
1040
|
+
});
|
|
1041
|
+
reflectedPass.setPipeline(reflectedSkyPipeline);
|
|
1042
|
+
reflectedPass.setBindGroup(0, reflectedGroup);
|
|
1043
|
+
reflectedPass.draw(3);
|
|
1044
|
+
drawGeometry(reflectedPass, reflectedSurfacePipeline, reflectedGroup);
|
|
1045
|
+
reflectedPass.end();
|
|
1046
|
+
const pass = encoder.beginRenderPass({
|
|
1047
|
+
label: "native.scene",
|
|
1048
|
+
colorAttachments: [
|
|
1049
|
+
{
|
|
1050
|
+
view: targets.colour,
|
|
1051
|
+
resolveTarget: view,
|
|
1052
|
+
loadOp: "clear",
|
|
1053
|
+
storeOp: "discard",
|
|
1054
|
+
clearValue: [0, 0, 0, 1]
|
|
1055
|
+
}
|
|
1056
|
+
],
|
|
1057
|
+
depthStencilAttachment: depthAttachment(targets.depth)
|
|
1058
|
+
});
|
|
1059
|
+
pass.setPipeline(skyPipeline);
|
|
1060
|
+
pass.setBindGroup(0, mainGroup);
|
|
1061
|
+
pass.draw(3);
|
|
1062
|
+
if (pendingFrame.water !== false) {
|
|
1063
|
+
pass.setPipeline(waterPipeline);
|
|
1064
|
+
pass.setBindGroup(1, shadowGroup);
|
|
1065
|
+
pass.setBindGroup(2, targets.reflectionGroup);
|
|
1066
|
+
pass.draw(3);
|
|
1067
|
+
}
|
|
1068
|
+
drawGeometry(pass, surfacePipeline, mainGroup);
|
|
1069
|
+
pass.end();
|
|
1070
|
+
};
|
|
1071
|
+
base = await createGpuRenderer({
|
|
1072
|
+
...options,
|
|
1073
|
+
alpha: false,
|
|
1074
|
+
encodeFrame: encode
|
|
1075
|
+
});
|
|
1076
|
+
const { device, format } = base;
|
|
1077
|
+
device.lost?.then(() => {
|
|
1078
|
+
available = false;
|
|
1079
|
+
if (!destroyed)
|
|
1080
|
+
options.onUnavailable?.(
|
|
1081
|
+
"WebGPU device lost. Reload the demo to reconnect."
|
|
1082
|
+
);
|
|
1083
|
+
});
|
|
1084
|
+
const module2 = device.createShaderModule({
|
|
1085
|
+
label: "plasius.native-scene",
|
|
1086
|
+
code: nativeSceneShader
|
|
1087
|
+
});
|
|
1088
|
+
const compilation = await module2.getCompilationInfo?.();
|
|
1089
|
+
const shaderErrors = compilation?.messages.filter((m) => m.type === "error") ?? [];
|
|
1090
|
+
if (shaderErrors.length)
|
|
1091
|
+
throw new Error(
|
|
1092
|
+
`Native scene shader compilation failed: ${shaderErrors.map((m) => `${m.lineNum}: ${m.message}`).join("; ")}`
|
|
1093
|
+
);
|
|
1094
|
+
const uniformLayout = device.createBindGroupLayout({
|
|
1095
|
+
entries: [
|
|
1096
|
+
{
|
|
1097
|
+
binding: 0,
|
|
1098
|
+
visibility: 3,
|
|
1099
|
+
buffer: { type: "uniform", minBindingSize: UNIFORM_BYTES }
|
|
1100
|
+
}
|
|
1101
|
+
]
|
|
1102
|
+
});
|
|
1103
|
+
const shadowLayout = device.createBindGroupLayout({
|
|
1104
|
+
entries: [
|
|
1105
|
+
{ binding: 0, visibility: 2, texture: { sampleType: "depth" } },
|
|
1106
|
+
{ binding: 1, visibility: 2, sampler: { type: "comparison" } }
|
|
1107
|
+
]
|
|
1108
|
+
});
|
|
1109
|
+
const reflectionLayout = device.createBindGroupLayout({
|
|
1110
|
+
entries: [
|
|
1111
|
+
{ binding: 0, visibility: 2, texture: { sampleType: "float" } },
|
|
1112
|
+
{ binding: 1, visibility: 2, sampler: { type: "filtering" } }
|
|
1113
|
+
]
|
|
1114
|
+
});
|
|
1115
|
+
const uniformBuffer = own(
|
|
1116
|
+
device.createBuffer({
|
|
1117
|
+
label: "native.frame",
|
|
1118
|
+
size: UNIFORM_BYTES,
|
|
1119
|
+
usage: 72
|
|
1120
|
+
})
|
|
1121
|
+
);
|
|
1122
|
+
const reflectionBuffer = own(
|
|
1123
|
+
device.createBuffer({
|
|
1124
|
+
label: "native.reflected-frame",
|
|
1125
|
+
size: UNIFORM_BYTES,
|
|
1126
|
+
usage: 72
|
|
1127
|
+
})
|
|
1128
|
+
);
|
|
1129
|
+
const bindUniform = (buffer) => device.createBindGroup({
|
|
1130
|
+
layout: uniformLayout,
|
|
1131
|
+
entries: [{ binding: 0, resource: { buffer } }]
|
|
1132
|
+
});
|
|
1133
|
+
const mainGroup = bindUniform(uniformBuffer);
|
|
1134
|
+
const reflectedGroup = bindUniform(reflectionBuffer);
|
|
1135
|
+
const shadowTexture = own(
|
|
1136
|
+
device.createTexture({
|
|
1137
|
+
label: "native.shadow",
|
|
1138
|
+
size: [SHADOW_SIZE, SHADOW_SIZE],
|
|
1139
|
+
format: "depth32float",
|
|
1140
|
+
usage: 20
|
|
1141
|
+
})
|
|
1142
|
+
);
|
|
1143
|
+
const shadowView = shadowTexture.createView();
|
|
1144
|
+
const shadowGroup = device.createBindGroup({
|
|
1145
|
+
layout: shadowLayout,
|
|
1146
|
+
entries: [
|
|
1147
|
+
{ binding: 0, resource: shadowView },
|
|
1148
|
+
{
|
|
1149
|
+
binding: 1,
|
|
1150
|
+
resource: device.createSampler({
|
|
1151
|
+
compare: "less-equal",
|
|
1152
|
+
minFilter: "linear",
|
|
1153
|
+
magFilter: "linear"
|
|
1154
|
+
})
|
|
1155
|
+
}
|
|
1156
|
+
]
|
|
1157
|
+
});
|
|
1158
|
+
const reflectionSampler = device.createSampler({
|
|
1159
|
+
minFilter: "linear",
|
|
1160
|
+
magFilter: "linear"
|
|
1161
|
+
});
|
|
1162
|
+
const attributes = [0, 1, 2, 3].map((i) => ({
|
|
1163
|
+
shaderLocation: i,
|
|
1164
|
+
offset: i * 12,
|
|
1165
|
+
format: "float32x3"
|
|
1166
|
+
}));
|
|
1167
|
+
const createPipeline = device.createRenderPipelineAsync?.bind(device) ?? device.createRenderPipeline.bind(device);
|
|
1168
|
+
const pipeline = (label, vertex, fragment, colourFormat, samples, layouts, geometry = false) => createPipeline({
|
|
1169
|
+
label: `native.${label}`,
|
|
1170
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: layouts }),
|
|
1171
|
+
vertex: {
|
|
1172
|
+
module: module2,
|
|
1173
|
+
entryPoint: vertex,
|
|
1174
|
+
...geometry ? {
|
|
1175
|
+
buffers: [
|
|
1176
|
+
{
|
|
1177
|
+
arrayStride: STRIDE * 4,
|
|
1178
|
+
attributes: vertex === "shadowVertex" ? attributes.slice(0, 1) : attributes
|
|
1179
|
+
}
|
|
1180
|
+
]
|
|
1181
|
+
} : {}
|
|
1182
|
+
},
|
|
1183
|
+
...fragment ? {
|
|
1184
|
+
fragment: {
|
|
1185
|
+
module: module2,
|
|
1186
|
+
entryPoint: fragment,
|
|
1187
|
+
targets: [{ format: colourFormat }]
|
|
1188
|
+
}
|
|
1189
|
+
} : {},
|
|
1190
|
+
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
1191
|
+
depthStencil: {
|
|
1192
|
+
format: "depth32float",
|
|
1193
|
+
depthWriteEnabled: label !== "sky" && label !== "reflection-sky",
|
|
1194
|
+
depthCompare: "less-equal"
|
|
1195
|
+
},
|
|
1196
|
+
multisample: { count: samples }
|
|
1197
|
+
});
|
|
1198
|
+
const shadowPipeline = await pipeline(
|
|
1199
|
+
"shadow",
|
|
1200
|
+
"shadowVertex",
|
|
1201
|
+
null,
|
|
1202
|
+
null,
|
|
1203
|
+
1,
|
|
1204
|
+
[uniformLayout],
|
|
1205
|
+
true
|
|
1206
|
+
);
|
|
1207
|
+
const surfacePipeline = await pipeline(
|
|
1208
|
+
"surface",
|
|
1209
|
+
"surfaceVertex",
|
|
1210
|
+
"surfaceFragment",
|
|
1211
|
+
format,
|
|
1212
|
+
4,
|
|
1213
|
+
[uniformLayout, shadowLayout],
|
|
1214
|
+
true
|
|
1215
|
+
);
|
|
1216
|
+
const skyPipeline = await pipeline(
|
|
1217
|
+
"sky",
|
|
1218
|
+
"fullscreenVertex",
|
|
1219
|
+
"skyFragment",
|
|
1220
|
+
format,
|
|
1221
|
+
4,
|
|
1222
|
+
[uniformLayout]
|
|
1223
|
+
);
|
|
1224
|
+
const waterPipeline = await pipeline(
|
|
1225
|
+
"water",
|
|
1226
|
+
"fullscreenVertex",
|
|
1227
|
+
"waterFragment",
|
|
1228
|
+
format,
|
|
1229
|
+
4,
|
|
1230
|
+
[uniformLayout, shadowLayout, reflectionLayout]
|
|
1231
|
+
);
|
|
1232
|
+
const reflectedSurfacePipeline = await pipeline(
|
|
1233
|
+
"reflection-surface",
|
|
1234
|
+
"surfaceVertex",
|
|
1235
|
+
"reflectionSurface",
|
|
1236
|
+
"rgba16float",
|
|
1237
|
+
1,
|
|
1238
|
+
[uniformLayout, shadowLayout],
|
|
1239
|
+
true
|
|
1240
|
+
);
|
|
1241
|
+
const reflectedSkyPipeline = await pipeline(
|
|
1242
|
+
"reflection-sky",
|
|
1243
|
+
"fullscreenVertex",
|
|
1244
|
+
"reflectionSky",
|
|
1245
|
+
"rgba16float",
|
|
1246
|
+
1,
|
|
1247
|
+
[uniformLayout]
|
|
1248
|
+
);
|
|
1249
|
+
return {
|
|
1250
|
+
render(input) {
|
|
1251
|
+
if (destroyed) throw new Error("Native scene renderer was destroyed.");
|
|
1252
|
+
if (!available) throw new Error("WebGPU device was lost.");
|
|
1253
|
+
pendingFrame = validateFrame(input, maxVertices);
|
|
1254
|
+
const data = pendingFrame.vertices;
|
|
1255
|
+
ensureTargets();
|
|
1256
|
+
if (data.byteLength > vertexCapacity) {
|
|
1257
|
+
if (vertexBuffer) release(vertexBuffer);
|
|
1258
|
+
vertexCapacity = Math.min(
|
|
1259
|
+
maxVertices * STRIDE * 4,
|
|
1260
|
+
Math.max(4096, 2 ** Math.ceil(Math.log2(data.byteLength)))
|
|
1261
|
+
);
|
|
1262
|
+
vertexBuffer = own(
|
|
1263
|
+
device.createBuffer({
|
|
1264
|
+
label: "native.surface-vertices",
|
|
1265
|
+
size: vertexCapacity,
|
|
1266
|
+
usage: 40
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
vertexCount = data.length / STRIDE;
|
|
1271
|
+
if (data.byteLength) device.queue.writeBuffer(vertexBuffer, 0, data);
|
|
1272
|
+
device.queue.writeBuffer(
|
|
1273
|
+
uniformBuffer,
|
|
1274
|
+
0,
|
|
1275
|
+
frameUniforms(pendingFrame, targets.width, targets.height)
|
|
1276
|
+
);
|
|
1277
|
+
device.queue.writeBuffer(
|
|
1278
|
+
reflectionBuffer,
|
|
1279
|
+
0,
|
|
1280
|
+
frameUniforms(
|
|
1281
|
+
pendingFrame,
|
|
1282
|
+
targets.reflectionWidth,
|
|
1283
|
+
targets.reflectionHeight,
|
|
1284
|
+
true
|
|
1285
|
+
)
|
|
1286
|
+
);
|
|
1287
|
+
base.renderOnce(pendingFrame.time * 1e3);
|
|
1288
|
+
submittedFrames++;
|
|
1289
|
+
return this.getSnapshot();
|
|
1290
|
+
},
|
|
1291
|
+
getSnapshot() {
|
|
1292
|
+
return {
|
|
1293
|
+
backend: "webgpu-raster",
|
|
1294
|
+
available: available && !destroyed,
|
|
1295
|
+
submittedFrames,
|
|
1296
|
+
vertexCount,
|
|
1297
|
+
shadowMapSize: SHADOW_SIZE,
|
|
1298
|
+
samples: 4,
|
|
1299
|
+
width: targets?.width ?? 0,
|
|
1300
|
+
height: targets?.height ?? 0
|
|
1301
|
+
};
|
|
1302
|
+
},
|
|
1303
|
+
destroy
|
|
1304
|
+
};
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
destroy();
|
|
1307
|
+
throw error;
|
|
704
1308
|
}
|
|
705
|
-
return profile.worldDisplacementAllowed === true || profile.motionMode === "root-authored" || profile.motionMode === "calibrated-in-place" || profile.motionMode === "jump";
|
|
706
1309
|
}
|
|
707
|
-
|
|
708
|
-
|
|
1310
|
+
|
|
1311
|
+
// src/canonical-wavefront-meshes.js
|
|
1312
|
+
var STANDARD_TEXTURE_BINDINGS = Object.freeze([
|
|
1313
|
+
["baseColor", "baseColorTexture"],
|
|
1314
|
+
["metallicRoughness", "metallicRoughnessTexture"],
|
|
1315
|
+
["normal", "normalTexture"],
|
|
1316
|
+
["occlusion", "occlusionTexture"],
|
|
1317
|
+
["emissive", "emissiveTexture"]
|
|
1318
|
+
]);
|
|
1319
|
+
var EXTENSION_TEXTURE_BINDINGS = Object.freeze([
|
|
1320
|
+
["clearcoat", "clearcoat", "clearcoatTexture"],
|
|
1321
|
+
["clearcoatRoughness", "clearcoat", "clearcoatRoughnessTexture"],
|
|
1322
|
+
["clearcoatNormal", "clearcoat", "clearcoatNormalTexture"],
|
|
1323
|
+
["transmission", "transmission", "transmissionTexture"],
|
|
1324
|
+
["sheenColor", "sheen", "sheenColorTexture"],
|
|
1325
|
+
["sheenRoughness", "sheen", "sheenRoughnessTexture"]
|
|
1326
|
+
]);
|
|
1327
|
+
function requireArray(value, label) {
|
|
1328
|
+
if (!Array.isArray(value)) {
|
|
1329
|
+
throw new TypeError(`${label} must be an array.`);
|
|
1330
|
+
}
|
|
1331
|
+
return value;
|
|
709
1332
|
}
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1333
|
+
function requireMap(value, label) {
|
|
1334
|
+
if (!value || typeof value !== "object" || typeof value.get !== "function") {
|
|
1335
|
+
throw new TypeError(`${label} must provide a ReadonlyMap-compatible get method.`);
|
|
1336
|
+
}
|
|
1337
|
+
return value;
|
|
1338
|
+
}
|
|
1339
|
+
function textureSample(binding, textures, label, extra = {}) {
|
|
1340
|
+
if (binding === void 0 || binding === null) {
|
|
1341
|
+
return void 0;
|
|
1342
|
+
}
|
|
1343
|
+
const sample = textures.get(binding.textureId);
|
|
1344
|
+
if (!sample) {
|
|
1345
|
+
throw new Error(`Missing decoded texture sample ${binding.textureId} for ${label}.`);
|
|
1346
|
+
}
|
|
1347
|
+
const width = Number(sample.width);
|
|
1348
|
+
const height = Number(sample.height);
|
|
1349
|
+
const data = sample.data;
|
|
1350
|
+
const dataLength = data && typeof data.length === "number" ? data.length : -1;
|
|
1351
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
1352
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} has invalid dimensions.`);
|
|
1353
|
+
}
|
|
1354
|
+
if (width > 4096 || height > 4096 || width * height > 4096 * 4096) {
|
|
1355
|
+
throw new RangeError(`Decoded texture sample ${binding.textureId} exceeds the 4096-pixel resource limit.`);
|
|
1356
|
+
}
|
|
1357
|
+
if (!(Array.isArray(data) || ArrayBuffer.isView(data)) || dataLength < width * height * 4) {
|
|
1358
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} must contain width \xD7 height \xD7 4 RGBA values.`);
|
|
1359
|
+
}
|
|
1360
|
+
return Object.freeze({
|
|
1361
|
+
...sample,
|
|
1362
|
+
...binding.texCoordSet === void 0 ? {} : { texCoord: binding.texCoordSet },
|
|
1363
|
+
...extra
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
function cloneMaterialExtensions(material, textures) {
|
|
1367
|
+
const extensions = { ...material.extensions ?? {} };
|
|
1368
|
+
if (material.workflow === "unlit") {
|
|
1369
|
+
const unlitKey = "KHR_materials_unlit";
|
|
1370
|
+
const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
|
|
1371
|
+
if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
|
|
1372
|
+
throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
|
|
736
1373
|
}
|
|
737
|
-
|
|
738
|
-
|
|
1374
|
+
extensions[unlitKey] = { ...unlitValue, enabled: true };
|
|
1375
|
+
}
|
|
1376
|
+
for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
|
|
1377
|
+
const binding = material.textures?.[bindingName];
|
|
1378
|
+
if (!binding) {
|
|
1379
|
+
continue;
|
|
739
1380
|
}
|
|
740
|
-
|
|
741
|
-
|
|
1381
|
+
const extensionKey = `KHR_materials_${extensionName}`;
|
|
1382
|
+
const extensionValue = extensions[extensionKey] ?? extensions[extensionName] ?? {};
|
|
1383
|
+
if (typeof extensionValue !== "object" || extensionValue === null || Array.isArray(extensionValue)) {
|
|
1384
|
+
throw new TypeError(`Canonical material extension ${extensionKey} must be an object.`);
|
|
742
1385
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
endMs: cursorMs + durationMs,
|
|
747
|
-
durationMs,
|
|
748
|
-
startPosition,
|
|
749
|
-
endPosition,
|
|
750
|
-
movementType,
|
|
751
|
-
movementProfile: profile,
|
|
752
|
-
movementDistance: segmentDistance,
|
|
753
|
-
distancePerLoop,
|
|
754
|
-
loopCount,
|
|
755
|
-
expectedSpeed,
|
|
756
|
-
actualSpeed,
|
|
757
|
-
footSlideWarning: Number(profile?.footSlideTolerance ?? 0) < 0 ? "invalid-foot-slide-tolerance" : null,
|
|
758
|
-
warnings
|
|
1386
|
+
extensions[extensionKey] = {
|
|
1387
|
+
...extensionValue,
|
|
1388
|
+
[textureKey]: textureSample(binding, textures, `${extensionKey}.${textureKey}`)
|
|
759
1389
|
};
|
|
760
|
-
cursorMs = entry.endMs;
|
|
761
|
-
currentPosition = [...endPosition];
|
|
762
|
-
if (targetPoint) {
|
|
763
|
-
const nextRouteIndex = route.findIndex((point) => point.id === targetPoint.id);
|
|
764
|
-
if (nextRouteIndex >= 0) {
|
|
765
|
-
routeCursor = nextRouteIndex;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
return entry;
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
function resolveTimelineBeat(timeline, loopTimeMs) {
|
|
772
|
-
if (!timeline.length) {
|
|
773
|
-
return null;
|
|
774
1390
|
}
|
|
775
|
-
return
|
|
1391
|
+
return extensions;
|
|
776
1392
|
}
|
|
777
|
-
function
|
|
778
|
-
if (!
|
|
779
|
-
return
|
|
1393
|
+
function mapMaterial(material, materialRefId, textures) {
|
|
1394
|
+
if (!material) {
|
|
1395
|
+
return { materialRefId };
|
|
780
1396
|
}
|
|
781
|
-
if (
|
|
782
|
-
|
|
1397
|
+
if (material.alphaMode === "mask") {
|
|
1398
|
+
throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
|
|
783
1399
|
}
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
function resolveTimelineForward(entry) {
|
|
787
|
-
if (!entry) {
|
|
788
|
-
return [0, 0, -1];
|
|
1400
|
+
if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
|
|
1401
|
+
throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
|
|
789
1402
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1403
|
+
const mapped = {
|
|
1404
|
+
materialRefId,
|
|
1405
|
+
...material.alphaMode === "blend" ? { materialKind: "transparent" } : {},
|
|
1406
|
+
baseColor: material.baseColorFactor,
|
|
1407
|
+
emission: material.emissiveFactor ? [...material.emissiveFactor, 1] : [0, 0, 0, 1],
|
|
1408
|
+
roughness: material.roughnessFactor,
|
|
1409
|
+
metallic: material.metallicFactor,
|
|
1410
|
+
opacity: material.alphaMode === "opaque" ? 1 : material.baseColorFactor?.[3],
|
|
1411
|
+
clearcoat: material.clearcoatFactor,
|
|
1412
|
+
clearcoatRoughness: material.clearcoatRoughnessFactor,
|
|
1413
|
+
transmission: material.transmissionFactor,
|
|
1414
|
+
sheenColor: material.sheenColorFactor,
|
|
1415
|
+
extensions: cloneMaterialExtensions(material, textures)
|
|
1416
|
+
};
|
|
1417
|
+
for (const [bindingName, inputName] of STANDARD_TEXTURE_BINDINGS) {
|
|
1418
|
+
const binding = material.textures?.[bindingName];
|
|
1419
|
+
if (!binding) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const extra = bindingName === "normal" && material.normalScale !== void 0 ? { scale: material.normalScale } : bindingName === "occlusion" && material.occlusionStrength !== void 0 ? { strength: material.occlusionStrength } : {};
|
|
1423
|
+
mapped[inputName] = textureSample(binding, textures, inputName, extra);
|
|
799
1424
|
}
|
|
800
|
-
return
|
|
1425
|
+
return mapped;
|
|
801
1426
|
}
|
|
802
|
-
function
|
|
803
|
-
if (typeof
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1427
|
+
function createCanonicalWavefrontMeshInputs({ document, geometry, textures = /* @__PURE__ */ new Map() } = {}) {
|
|
1428
|
+
if (!document || typeof document !== "object") {
|
|
1429
|
+
throw new TypeError("Canonical model document is required.");
|
|
1430
|
+
}
|
|
1431
|
+
const meshes = requireArray(document.meshes, "Canonical model meshes");
|
|
1432
|
+
const materials = requireArray(document.materials, "Canonical model materials");
|
|
1433
|
+
const geometryInputs = requireArray(geometry, "Renderer-ready geometry");
|
|
1434
|
+
const textureSamples = requireMap(textures, "Decoded texture samples");
|
|
1435
|
+
const geometryByPrimitiveId = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const entry of geometryInputs) {
|
|
1437
|
+
if (!entry || typeof entry.primitiveId !== "string" || entry.primitiveId.length === 0) {
|
|
1438
|
+
throw new TypeError("Renderer-ready geometry requires a non-empty primitiveId.");
|
|
807
1439
|
}
|
|
808
|
-
|
|
1440
|
+
if (geometryByPrimitiveId.has(entry.primitiveId)) {
|
|
1441
|
+
throw new Error(`Duplicate renderer-ready geometry for primitive ${entry.primitiveId}.`);
|
|
1442
|
+
}
|
|
1443
|
+
geometryByPrimitiveId.set(entry.primitiveId, entry);
|
|
809
1444
|
}
|
|
810
|
-
|
|
811
|
-
|
|
1445
|
+
const materialsById = /* @__PURE__ */ new Map();
|
|
1446
|
+
const materialRefIds = /* @__PURE__ */ new Map();
|
|
1447
|
+
materials.forEach((material, index) => {
|
|
1448
|
+
if (!material || typeof material.id !== "string" || material.id.length === 0) {
|
|
1449
|
+
throw new TypeError("Canonical materials require a non-empty id.");
|
|
1450
|
+
}
|
|
1451
|
+
if (materialsById.has(material.id)) {
|
|
1452
|
+
throw new Error(`Duplicate canonical material ${material.id}.`);
|
|
1453
|
+
}
|
|
1454
|
+
materialsById.set(material.id, material);
|
|
1455
|
+
materialRefIds.set(material.id, index + 1);
|
|
1456
|
+
});
|
|
1457
|
+
const output = [];
|
|
1458
|
+
const consumedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1459
|
+
const canonicalPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1460
|
+
for (const mesh of meshes) {
|
|
1461
|
+
if (!mesh || typeof mesh.id !== "string") {
|
|
1462
|
+
throw new TypeError("Canonical meshes require a string id.");
|
|
1463
|
+
}
|
|
1464
|
+
for (const primitive of requireArray(mesh.primitives, `Primitives for mesh ${mesh.id}`)) {
|
|
1465
|
+
if (!primitive || typeof primitive.id !== "string" || primitive.id.length === 0) {
|
|
1466
|
+
throw new TypeError(`Canonical mesh ${mesh.id} contains a primitive without an id.`);
|
|
1467
|
+
}
|
|
1468
|
+
if (canonicalPrimitiveIds.has(primitive.id)) {
|
|
1469
|
+
throw new Error(`Duplicate canonical primitive ${primitive.id}.`);
|
|
1470
|
+
}
|
|
1471
|
+
canonicalPrimitiveIds.add(primitive.id);
|
|
1472
|
+
if (primitive.topology !== "triangles") {
|
|
1473
|
+
throw new Error(`Unsupported canonical primitive topology: ${primitive.topology}.`);
|
|
1474
|
+
}
|
|
1475
|
+
const source = geometryByPrimitiveId.get(primitive.id);
|
|
1476
|
+
if (!source) {
|
|
1477
|
+
throw new Error(`Missing renderer-ready geometry for primitive ${primitive.id}.`);
|
|
1478
|
+
}
|
|
1479
|
+
consumedPrimitiveIds.add(primitive.id);
|
|
1480
|
+
let material = null;
|
|
1481
|
+
let materialRefId = 0;
|
|
1482
|
+
if (primitive.materialId !== void 0) {
|
|
1483
|
+
material = materialsById.get(primitive.materialId);
|
|
1484
|
+
if (!material) {
|
|
1485
|
+
throw new Error(`Missing canonical material ${primitive.materialId} for primitive ${primitive.id}.`);
|
|
1486
|
+
}
|
|
1487
|
+
materialRefId = materialRefIds.get(primitive.materialId);
|
|
1488
|
+
}
|
|
1489
|
+
output.push(Object.freeze({
|
|
1490
|
+
id: output.length + 1,
|
|
1491
|
+
positions: source.positions,
|
|
1492
|
+
...source.indices === void 0 ? {} : { indices: source.indices },
|
|
1493
|
+
...source.normals === void 0 ? {} : { normals: source.normals },
|
|
1494
|
+
...(source.texcoords ?? source.uvs) === void 0 ? {} : { uvs: source.texcoords ?? source.uvs },
|
|
1495
|
+
...mapMaterial(material, materialRefId, textureSamples)
|
|
1496
|
+
}));
|
|
1497
|
+
}
|
|
812
1498
|
}
|
|
813
|
-
|
|
1499
|
+
for (const primitiveId of geometryByPrimitiveId.keys()) {
|
|
1500
|
+
if (!consumedPrimitiveIds.has(primitiveId)) {
|
|
1501
|
+
throw new Error(`Renderer-ready geometry references unknown primitive ${primitiveId}.`);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return Object.freeze(output);
|
|
814
1505
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
1506
|
+
|
|
1507
|
+
// src/animated-scene-renderer.js
|
|
1508
|
+
var import_gpu_camera = require("@plasius/gpu-camera");
|
|
1509
|
+
|
|
1510
|
+
// src/animated-gltf-model.js
|
|
1511
|
+
var GLB_MAGIC = 1179937895;
|
|
1512
|
+
var GLB_VERSION = 2;
|
|
1513
|
+
var GLB_JSON_CHUNK = 1313821514;
|
|
1514
|
+
var GLB_BIN_CHUNK = 5130562;
|
|
1515
|
+
var COMPONENT_READERS = Object.freeze({
|
|
1516
|
+
5120: { size: 1, read: (view, offset) => view.getInt8(offset), normalizedDivisor: 127 },
|
|
1517
|
+
5121: { size: 1, read: (view, offset) => view.getUint8(offset), normalizedDivisor: 255 },
|
|
1518
|
+
5122: { size: 2, read: (view, offset) => view.getInt16(offset, true), normalizedDivisor: 32767 },
|
|
1519
|
+
5123: { size: 2, read: (view, offset) => view.getUint16(offset, true), normalizedDivisor: 65535 },
|
|
1520
|
+
5125: { size: 4, read: (view, offset) => view.getUint32(offset, true), normalizedDivisor: 4294967295 },
|
|
1521
|
+
5126: { size: 4, read: (view, offset) => view.getFloat32(offset, true), normalizedDivisor: 1 }
|
|
1522
|
+
});
|
|
1523
|
+
var ACCESSOR_COMPONENTS = Object.freeze({
|
|
1524
|
+
SCALAR: 1,
|
|
1525
|
+
VEC2: 2,
|
|
1526
|
+
VEC3: 3,
|
|
1527
|
+
VEC4: 4,
|
|
1528
|
+
MAT4: 16
|
|
1529
|
+
});
|
|
1530
|
+
function identityMatrix() {
|
|
1531
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
828
1532
|
}
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
ctx.fillRect(-scale2 * 0.36, -scale2 * 0.035, scale2 * 0.72, scale2 * 0.07);
|
|
835
|
-
}
|
|
836
|
-
function drawProp(ctx, prop, cameraPosition, width, height) {
|
|
837
|
-
const projected = project(prop.position, cameraPosition, width, height);
|
|
838
|
-
const { x, y, scale: scale2 } = projected;
|
|
839
|
-
ctx.save();
|
|
840
|
-
ctx.translate(x, y);
|
|
841
|
-
ctx.lineWidth = Math.max(1, scale2 * 0.04);
|
|
842
|
-
if (prop.kind === "crop-row") {
|
|
843
|
-
ctx.fillStyle = "#8b633f";
|
|
844
|
-
ctx.fillRect(-scale2 * 1.05, -scale2 * 0.08, scale2 * 2.1, scale2 * 0.16);
|
|
845
|
-
ctx.fillStyle = "#6f4a30";
|
|
846
|
-
ctx.fillRect(-scale2 * 0.92, -scale2 * 0.035, scale2 * 1.84, scale2 * 0.07);
|
|
847
|
-
ctx.strokeStyle = "#4f6f32";
|
|
848
|
-
ctx.beginPath();
|
|
849
|
-
ctx.moveTo(-scale2 * 0.8, 0);
|
|
850
|
-
ctx.lineTo(scale2 * 0.8, 0);
|
|
851
|
-
ctx.stroke();
|
|
852
|
-
ctx.fillStyle = "#6f9f43";
|
|
853
|
-
for (let i = -3; i <= 3; i += 1) {
|
|
854
|
-
ctx.fillRect(i * scale2 * 0.22, -scale2 * 0.18, scale2 * 0.06, scale2 * 0.2);
|
|
855
|
-
}
|
|
856
|
-
} else if (prop.kind === "fence-segment") {
|
|
857
|
-
ctx.strokeStyle = "#896b4d";
|
|
858
|
-
ctx.beginPath();
|
|
859
|
-
ctx.moveTo(-scale2 * 0.5, 0);
|
|
860
|
-
ctx.lineTo(scale2 * 0.5, 0);
|
|
861
|
-
ctx.moveTo(-scale2 * 0.35, -scale2 * 0.25);
|
|
862
|
-
ctx.lineTo(-scale2 * 0.35, scale2 * 0.1);
|
|
863
|
-
ctx.moveTo(scale2 * 0.35, -scale2 * 0.25);
|
|
864
|
-
ctx.lineTo(scale2 * 0.35, scale2 * 0.1);
|
|
865
|
-
ctx.stroke();
|
|
866
|
-
} else if (prop.kind === "cart") {
|
|
867
|
-
drawGroundShadow(ctx, scale2);
|
|
868
|
-
ctx.fillStyle = "#7b5238";
|
|
869
|
-
ctx.fillRect(-scale2 * 0.52, -scale2 * 0.38, scale2 * 1.04, scale2 * 0.36);
|
|
870
|
-
ctx.fillStyle = "#a4774c";
|
|
871
|
-
ctx.fillRect(-scale2 * 0.44, -scale2 * 0.32, scale2 * 0.88, scale2 * 0.08);
|
|
872
|
-
ctx.fillStyle = "#25201c";
|
|
873
|
-
ctx.beginPath();
|
|
874
|
-
ctx.arc(-scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
875
|
-
ctx.arc(scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
876
|
-
ctx.fill();
|
|
877
|
-
} else if (prop.kind === "small-tree") {
|
|
878
|
-
drawGroundShadow(ctx, scale2);
|
|
879
|
-
ctx.fillStyle = "#5b3b24";
|
|
880
|
-
ctx.fillRect(-scale2 * 0.07, -scale2 * 0.56, scale2 * 0.14, scale2 * 0.56);
|
|
881
|
-
ctx.fillStyle = "#2f6d3b";
|
|
882
|
-
ctx.beginPath();
|
|
883
|
-
ctx.arc(-scale2 * 0.16, -scale2 * 0.72, scale2 * 0.25, 0, Math.PI * 2);
|
|
884
|
-
ctx.fill();
|
|
885
|
-
ctx.fillStyle = "#3f8746";
|
|
886
|
-
ctx.beginPath();
|
|
887
|
-
ctx.arc(scale2 * 0.13, -scale2 * 0.76, scale2 * 0.28, 0, Math.PI * 2);
|
|
888
|
-
ctx.fill();
|
|
889
|
-
ctx.fillStyle = "#4f9a50";
|
|
890
|
-
ctx.beginPath();
|
|
891
|
-
ctx.arc(0, -scale2 * 0.98, scale2 * 0.26, 0, Math.PI * 2);
|
|
892
|
-
ctx.fill();
|
|
893
|
-
} else {
|
|
894
|
-
drawGroundShadow(ctx, scale2);
|
|
895
|
-
ctx.fillStyle = prop.kind === "crate" ? "#9a6a3d" : "#d0c39f";
|
|
896
|
-
ctx.fillRect(-scale2 * 0.2, -scale2 * 0.32, scale2 * 0.4, scale2 * 0.32);
|
|
897
|
-
if (prop.kind === "crate") {
|
|
898
|
-
ctx.strokeStyle = "#6e4829";
|
|
899
|
-
ctx.beginPath();
|
|
900
|
-
ctx.moveTo(-scale2 * 0.2, -scale2 * 0.16);
|
|
901
|
-
ctx.lineTo(scale2 * 0.2, -scale2 * 0.16);
|
|
902
|
-
ctx.moveTo(0, -scale2 * 0.32);
|
|
903
|
-
ctx.lineTo(0, 0);
|
|
904
|
-
ctx.stroke();
|
|
1533
|
+
function multiplyMatrix(a, b) {
|
|
1534
|
+
const out = new Array(16).fill(0);
|
|
1535
|
+
for (let column = 0; column < 4; column += 1) {
|
|
1536
|
+
for (let row = 0; row < 4; row += 1) {
|
|
1537
|
+
out[column * 4 + row] = a[0 * 4 + row] * b[column * 4 + 0] + a[1 * 4 + row] * b[column * 4 + 1] + a[2 * 4 + row] * b[column * 4 + 2] + a[3 * 4 + row] * b[column * 4 + 3];
|
|
905
1538
|
}
|
|
906
1539
|
}
|
|
907
|
-
|
|
908
|
-
return projected;
|
|
1540
|
+
return out;
|
|
909
1541
|
}
|
|
910
|
-
function
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
ctx.lineWidth = Math.max(2, scale2 * 0.05);
|
|
918
|
-
ctx.lineCap = "round";
|
|
919
|
-
drawGroundShadow(ctx, scale2);
|
|
920
|
-
ctx.strokeStyle = "#4c2f25";
|
|
921
|
-
ctx.beginPath();
|
|
922
|
-
ctx.moveTo(-scale2 * 0.12, -scale2 * 0.34);
|
|
923
|
-
ctx.lineTo(-scale2 * (0.18 + stride * 0.04), -scale2 * 0.02);
|
|
924
|
-
ctx.moveTo(scale2 * 0.12, -scale2 * 0.34);
|
|
925
|
-
ctx.lineTo(scale2 * (0.18 - stride * 0.04), -scale2 * 0.02);
|
|
926
|
-
ctx.stroke();
|
|
927
|
-
ctx.fillStyle = "#6f8f62";
|
|
928
|
-
ctx.beginPath();
|
|
929
|
-
ctx.moveTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
930
|
-
ctx.lineTo(scale2 * 0.3, -scale2 * 0.86);
|
|
931
|
-
ctx.lineTo(scale2 * 0.42, -scale2 * 0.24);
|
|
932
|
-
ctx.lineTo(-scale2 * 0.42, -scale2 * 0.24);
|
|
933
|
-
ctx.lineTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
934
|
-
ctx.fill();
|
|
935
|
-
ctx.fillStyle = "#d8b18c";
|
|
936
|
-
ctx.beginPath();
|
|
937
|
-
ctx.arc(0, -scale2 * 1.16, scale2 * 0.18, 0, Math.PI * 2);
|
|
938
|
-
ctx.fill();
|
|
939
|
-
ctx.fillStyle = "#5a3a2b";
|
|
940
|
-
ctx.fillRect(-scale2 * 0.2, -scale2 * 1.28, scale2 * 0.4, scale2 * 0.16);
|
|
941
|
-
ctx.fillRect(-scale2 * 0.23, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
942
|
-
ctx.fillRect(scale2 * 0.13, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
943
|
-
if (headLook?.status === "active" || headLook?.status === "returning") {
|
|
944
|
-
ctx.strokeStyle = "rgba(255, 246, 210, 0.74)";
|
|
945
|
-
ctx.lineWidth = Math.max(1, scale2 * 0.025);
|
|
946
|
-
ctx.beginPath();
|
|
947
|
-
ctx.moveTo(0, -scale2 * 1.16);
|
|
948
|
-
ctx.lineTo(Math.sin(headYaw) * scale2 * 0.32, -scale2 * 1.16 - scale2 * 0.08);
|
|
949
|
-
ctx.stroke();
|
|
950
|
-
}
|
|
951
|
-
ctx.strokeStyle = "#6b4935";
|
|
952
|
-
ctx.beginPath();
|
|
953
|
-
ctx.moveTo(-scale2 * 0.24, -scale2 * 0.76);
|
|
954
|
-
ctx.lineTo(-scale2 * (0.46 + stride * 0.04), -scale2 * (0.5 + stride * 0.06));
|
|
955
|
-
ctx.moveTo(scale2 * 0.24, -scale2 * 0.76);
|
|
956
|
-
ctx.lineTo(scale2 * (0.46 - stride * 0.04), -scale2 * (0.5 - stride * 0.06));
|
|
957
|
-
ctx.stroke();
|
|
958
|
-
ctx.fillStyle = "#d6c09a";
|
|
959
|
-
ctx.fillRect(-scale2 * 0.16, -scale2 * 0.86, scale2 * 0.32, scale2 * 0.18);
|
|
960
|
-
ctx.restore();
|
|
961
|
-
return projected;
|
|
1542
|
+
function transformPoint(matrix, point) {
|
|
1543
|
+
const [x, y, z] = point;
|
|
1544
|
+
return [
|
|
1545
|
+
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
1546
|
+
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
1547
|
+
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
|
|
1548
|
+
];
|
|
962
1549
|
}
|
|
963
|
-
function
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const
|
|
1550
|
+
function composeMatrix(translation = [0, 0, 0], rotation = [0, 0, 0, 1], scale2 = [1, 1, 1]) {
|
|
1551
|
+
const [x, y, z, w] = normalizeQuaternion(rotation);
|
|
1552
|
+
const [sx, sy, sz] = scale2;
|
|
1553
|
+
const x2 = x + x;
|
|
1554
|
+
const y2 = y + y;
|
|
1555
|
+
const z2 = z + z;
|
|
1556
|
+
const xx = x * x2;
|
|
1557
|
+
const xy = x * y2;
|
|
1558
|
+
const xz = x * z2;
|
|
1559
|
+
const yy = y * y2;
|
|
1560
|
+
const yz = y * z2;
|
|
1561
|
+
const zz = z * z2;
|
|
1562
|
+
const wx = w * x2;
|
|
1563
|
+
const wy = w * y2;
|
|
1564
|
+
const wz = w * z2;
|
|
969
1565
|
return [
|
|
970
|
-
|
|
971
|
-
|
|
1566
|
+
(1 - (yy + zz)) * sx,
|
|
1567
|
+
(xy + wz) * sx,
|
|
1568
|
+
(xz - wy) * sx,
|
|
1569
|
+
0,
|
|
1570
|
+
(xy - wz) * sy,
|
|
1571
|
+
(1 - (xx + zz)) * sy,
|
|
1572
|
+
(yz + wx) * sy,
|
|
1573
|
+
0,
|
|
1574
|
+
(xz + wy) * sz,
|
|
1575
|
+
(yz - wx) * sz,
|
|
1576
|
+
(1 - (xx + yy)) * sz,
|
|
1577
|
+
0,
|
|
1578
|
+
translation[0] ?? 0,
|
|
1579
|
+
translation[1] ?? 0,
|
|
1580
|
+
translation[2] ?? 0,
|
|
1581
|
+
1
|
|
972
1582
|
];
|
|
973
1583
|
}
|
|
974
|
-
function
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1584
|
+
function normalizeQuaternion(quaternion = [0, 0, 0, 1]) {
|
|
1585
|
+
const length = Math.hypot(quaternion[0] ?? 0, quaternion[1] ?? 0, quaternion[2] ?? 0, quaternion[3] ?? 1) || 1;
|
|
1586
|
+
return [
|
|
1587
|
+
(quaternion[0] ?? 0) / length,
|
|
1588
|
+
(quaternion[1] ?? 0) / length,
|
|
1589
|
+
(quaternion[2] ?? 0) / length,
|
|
1590
|
+
(quaternion[3] ?? 1) / length
|
|
1591
|
+
];
|
|
1592
|
+
}
|
|
1593
|
+
function lerp(a, b, t) {
|
|
1594
|
+
return a + (b - a) * t;
|
|
1595
|
+
}
|
|
1596
|
+
function lerpArray(a, b, t) {
|
|
1597
|
+
return a.map((value, index) => lerp(value, b[index] ?? value, t));
|
|
1598
|
+
}
|
|
1599
|
+
function nlerpQuaternion(a, b, t) {
|
|
1600
|
+
let end = b;
|
|
1601
|
+
const dot2 = a.reduce((total, value, index) => total + value * (b[index] ?? 0), 0);
|
|
1602
|
+
if (dot2 < 0) {
|
|
1603
|
+
end = b.map((value) => -value);
|
|
979
1604
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1605
|
+
return normalizeQuaternion(lerpArray(a, end, t));
|
|
1606
|
+
}
|
|
1607
|
+
function parseGlb(buffer) {
|
|
1608
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
1609
|
+
return null;
|
|
982
1610
|
}
|
|
983
|
-
|
|
984
|
-
|
|
1611
|
+
const view = new DataView(buffer);
|
|
1612
|
+
if (view.byteLength < 20 || view.getUint32(0, true) !== GLB_MAGIC || view.getUint32(4, true) !== GLB_VERSION) {
|
|
1613
|
+
return null;
|
|
985
1614
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const base = triangleIndex * 3;
|
|
997
|
-
const a = sample.vertices[sample.indices[base]];
|
|
998
|
-
const b = sample.vertices[sample.indices[base + 1]];
|
|
999
|
-
const c = sample.vertices[sample.indices[base + 2]];
|
|
1000
|
-
if (!a || !b || !c) {
|
|
1001
|
-
continue;
|
|
1615
|
+
let offset = 12;
|
|
1616
|
+
let json = null;
|
|
1617
|
+
let binary = null;
|
|
1618
|
+
while (offset + 8 <= view.byteLength) {
|
|
1619
|
+
const length = view.getUint32(offset, true);
|
|
1620
|
+
const type = view.getUint32(offset + 4, true);
|
|
1621
|
+
const start = offset + 8;
|
|
1622
|
+
const end = start + length;
|
|
1623
|
+
if (end > view.byteLength) {
|
|
1624
|
+
return null;
|
|
1002
1625
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1011
|
-
triangles.sort((left, right) => left.depth - right.depth);
|
|
1012
|
-
ctx.save();
|
|
1013
|
-
drawGroundShadow(ctx, projection.scale);
|
|
1014
|
-
for (const triangle of triangles) {
|
|
1015
|
-
const [ax, ay] = pointForModelVertex(triangle.a, projection, sample);
|
|
1016
|
-
const [bx, by] = pointForModelVertex(triangle.b, projection, sample);
|
|
1017
|
-
const [cx, cy] = pointForModelVertex(triangle.c, projection, sample);
|
|
1018
|
-
ctx.fillStyle = triangle.fill;
|
|
1019
|
-
ctx.beginPath();
|
|
1020
|
-
ctx.moveTo(ax, ay);
|
|
1021
|
-
ctx.lineTo(bx, by);
|
|
1022
|
-
ctx.lineTo(cx, cy);
|
|
1023
|
-
ctx.closePath?.();
|
|
1024
|
-
ctx.fill();
|
|
1626
|
+
if (type === GLB_JSON_CHUNK) {
|
|
1627
|
+
const text = new TextDecoder().decode(new Uint8Array(buffer, start, length)).trim();
|
|
1628
|
+
json = JSON.parse(text);
|
|
1629
|
+
} else if (type === GLB_BIN_CHUNK) {
|
|
1630
|
+
binary = new Uint8Array(buffer, start, length);
|
|
1631
|
+
}
|
|
1632
|
+
offset = end;
|
|
1025
1633
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
};
|
|
1634
|
+
if (!json || !binary) {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
return { json, binary };
|
|
1031
1638
|
}
|
|
1032
|
-
function
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
ctx.fillStyle = "#d8e8d0";
|
|
1037
|
-
ctx.fillRect(0, 0, width, height);
|
|
1038
|
-
ctx.fillStyle = "#9fc47a";
|
|
1039
|
-
ctx.fillRect(0, height * 0.52, width, height * 0.48);
|
|
1040
|
-
ctx.strokeStyle = "#caa66d";
|
|
1041
|
-
ctx.lineWidth = Math.max(2, width * 6e-3);
|
|
1042
|
-
ctx.beginPath();
|
|
1043
|
-
ctx.moveTo(width * 0.35, height);
|
|
1044
|
-
ctx.quadraticCurveTo(width * 0.53, height * 0.58, width * 0.62, 0);
|
|
1045
|
-
ctx.stroke();
|
|
1046
|
-
const projectedProps = props.map((prop, index) => ({
|
|
1047
|
-
prop,
|
|
1048
|
-
id: propIdentity(prop, index),
|
|
1049
|
-
projected: project(prop.position, snapshot.cameraPosition, width, height)
|
|
1050
|
-
})).sort((a, b) => b.projected.depth - a.projected.depth);
|
|
1051
|
-
const propGroundAnchors = [];
|
|
1052
|
-
for (const { prop, id } of projectedProps) {
|
|
1053
|
-
const projected = drawProp(ctx, prop, snapshot.cameraPosition, width, height);
|
|
1054
|
-
propGroundAnchors.push({
|
|
1055
|
-
id,
|
|
1056
|
-
kind: prop.kind,
|
|
1057
|
-
groundY: projected.groundY,
|
|
1058
|
-
depth: projected.depth,
|
|
1059
|
-
visible: projected.visible
|
|
1060
|
-
});
|
|
1639
|
+
function readAccessor(document, accessorIndex) {
|
|
1640
|
+
const accessor = document.json.accessors?.[accessorIndex];
|
|
1641
|
+
if (!accessor) {
|
|
1642
|
+
return [];
|
|
1061
1643
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1644
|
+
const bufferView = document.json.bufferViews?.[accessor.bufferView];
|
|
1645
|
+
const component = COMPONENT_READERS[accessor.componentType];
|
|
1646
|
+
const componentCount = ACCESSOR_COMPONENTS[accessor.type] ?? 1;
|
|
1647
|
+
if (!bufferView || !component) {
|
|
1648
|
+
return [];
|
|
1649
|
+
}
|
|
1650
|
+
const view = new DataView(
|
|
1651
|
+
document.binary.buffer,
|
|
1652
|
+
document.binary.byteOffset + (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0),
|
|
1653
|
+
bufferView.byteLength ?? 0
|
|
1654
|
+
);
|
|
1655
|
+
const stride = bufferView.byteStride ?? component.size * componentCount;
|
|
1656
|
+
const values = [];
|
|
1657
|
+
for (let index = 0; index < accessor.count; index += 1) {
|
|
1658
|
+
const item = [];
|
|
1659
|
+
const baseOffset = index * stride;
|
|
1660
|
+
for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
|
|
1661
|
+
const raw = component.read(view, baseOffset + componentIndex * component.size);
|
|
1662
|
+
item.push(accessor.normalized && accessor.componentType !== 5126 ? raw / component.normalizedDivisor : raw);
|
|
1663
|
+
}
|
|
1664
|
+
values.push(componentCount === 1 ? item[0] : item);
|
|
1665
|
+
}
|
|
1666
|
+
return values;
|
|
1667
|
+
}
|
|
1668
|
+
function readMatrices(document, accessorIndex) {
|
|
1669
|
+
return readAccessor(document, accessorIndex).map((matrix) => Array.from(matrix));
|
|
1670
|
+
}
|
|
1671
|
+
function textureImage(document, textureIndex) {
|
|
1672
|
+
if (typeof textureIndex !== "number") {
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
const texture = document.json.textures?.[textureIndex];
|
|
1676
|
+
return typeof texture?.source === "number" ? document.json.images?.[texture.source] ?? null : null;
|
|
1677
|
+
}
|
|
1678
|
+
function imageHasBuffer(document, image) {
|
|
1679
|
+
if (!image) {
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
if (typeof image.uri === "string" && image.uri.trim().length > 0) {
|
|
1683
|
+
return true;
|
|
1684
|
+
}
|
|
1685
|
+
return typeof image.bufferView === "number" && Boolean(document.json.bufferViews?.[image.bufferView]);
|
|
1686
|
+
}
|
|
1687
|
+
function extractMaterialTextureMetadata(document, primitive) {
|
|
1688
|
+
const material = document.json.materials?.[primitive?.material];
|
|
1689
|
+
const baseColorTextureIndex = material?.pbrMetallicRoughness?.baseColorTexture?.index;
|
|
1690
|
+
const normalTextureIndex = material?.normalTexture?.index;
|
|
1691
|
+
const missingTextureReferences = [];
|
|
1692
|
+
if (typeof baseColorTextureIndex === "number" && !imageHasBuffer(document, textureImage(document, baseColorTextureIndex))) {
|
|
1693
|
+
missingTextureReferences.push("baseColorTexture");
|
|
1694
|
+
}
|
|
1695
|
+
if (typeof normalTextureIndex === "number" && !imageHasBuffer(document, textureImage(document, normalTextureIndex))) {
|
|
1696
|
+
missingTextureReferences.push("normalTexture");
|
|
1697
|
+
}
|
|
1698
|
+
return Object.freeze({
|
|
1699
|
+
materialCount: document.json.materials?.length ?? 0,
|
|
1700
|
+
textureCount: document.json.textures?.length ?? 0,
|
|
1701
|
+
imageCount: document.json.images?.length ?? 0,
|
|
1702
|
+
embeddedImageCount: (document.json.images ?? []).filter((image) => typeof image.bufferView === "number").length,
|
|
1703
|
+
baseColorTextureCount: typeof baseColorTextureIndex === "number" ? 1 : 0,
|
|
1704
|
+
normalTextureCount: typeof normalTextureIndex === "number" ? 1 : 0,
|
|
1705
|
+
hasBaseColorTexture: typeof baseColorTextureIndex === "number",
|
|
1706
|
+
hasNormalTexture: typeof normalTextureIndex === "number",
|
|
1707
|
+
missingTextureReferences: Object.freeze(missingTextureReferences),
|
|
1708
|
+
materialName: material?.name ?? ""
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
function nodeLocalMatrix(node, override = {}) {
|
|
1712
|
+
if (node.matrix && !override.translation && !override.rotation && !override.scale) {
|
|
1713
|
+
return [...node.matrix];
|
|
1714
|
+
}
|
|
1715
|
+
return composeMatrix(
|
|
1716
|
+
override.translation ?? node.translation ?? [0, 0, 0],
|
|
1717
|
+
override.rotation ?? node.rotation ?? [0, 0, 0, 1],
|
|
1718
|
+
override.scale ?? node.scale ?? [1, 1, 1]
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
function computeWorldMatrices(nodes, overridesByName = /* @__PURE__ */ new Map()) {
|
|
1722
|
+
const parents = /* @__PURE__ */ new Set();
|
|
1723
|
+
for (const node of nodes) {
|
|
1724
|
+
for (const child of node.children ?? []) {
|
|
1725
|
+
parents.add(child);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const roots = nodes.map((_, index) => index).filter((index) => !parents.has(index));
|
|
1729
|
+
const worldMatrices = nodes.map(() => identityMatrix());
|
|
1730
|
+
function visit(nodeIndex, parentMatrix) {
|
|
1731
|
+
const node = nodes[nodeIndex];
|
|
1732
|
+
if (!node) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const worldMatrix = multiplyMatrix(parentMatrix, nodeLocalMatrix(node, overridesByName.get(node.name) ?? {}));
|
|
1736
|
+
worldMatrices[nodeIndex] = worldMatrix;
|
|
1737
|
+
for (const child of node.children ?? []) {
|
|
1738
|
+
visit(child, worldMatrix);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
for (const root of roots) {
|
|
1742
|
+
visit(root, identityMatrix());
|
|
1743
|
+
}
|
|
1744
|
+
return worldMatrices;
|
|
1745
|
+
}
|
|
1746
|
+
function normalizeWeights(weights) {
|
|
1747
|
+
const total = weights.reduce((sum, value) => sum + Math.max(0, value), 0);
|
|
1748
|
+
if (total <= 0) {
|
|
1749
|
+
return [1, 0, 0, 0];
|
|
1750
|
+
}
|
|
1751
|
+
return weights.map((value) => Math.max(0, value) / total);
|
|
1752
|
+
}
|
|
1753
|
+
function deriveBounds(vertices) {
|
|
1754
|
+
const min = [Infinity, Infinity, Infinity];
|
|
1755
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
1756
|
+
for (const vertex of vertices) {
|
|
1757
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
1758
|
+
min[axis] = Math.min(min[axis], vertex[axis]);
|
|
1759
|
+
max[axis] = Math.max(max[axis], vertex[axis]);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (!Number.isFinite(min[0])) {
|
|
1763
|
+
return { min: [0, 0, 0], max: [0, 1, 0], size: [1, 1, 1] };
|
|
1092
1764
|
}
|
|
1093
1765
|
return {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
modelRenderable,
|
|
1098
|
-
fallbackProxyActive,
|
|
1099
|
-
skinnedVertexCount,
|
|
1100
|
-
skinnedTriangleCount,
|
|
1101
|
-
skinnedJointCount,
|
|
1102
|
-
skinnedAnimatedNodeCount,
|
|
1103
|
-
skinnedClipCount,
|
|
1104
|
-
activeClipRenderable,
|
|
1105
|
-
propGroundAnchors
|
|
1766
|
+
min,
|
|
1767
|
+
max,
|
|
1768
|
+
size: [max[0] - min[0], max[1] - min[1], max[2] - min[2]]
|
|
1106
1769
|
};
|
|
1107
1770
|
}
|
|
1108
|
-
function
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
options.clipAssets ?? options.animationAdventure?.clipAssets ?? []
|
|
1113
|
-
);
|
|
1114
|
-
} catch {
|
|
1771
|
+
function sampleChannel(channel, sampler, document, timeSeconds) {
|
|
1772
|
+
const input = readAccessor(document, sampler.input);
|
|
1773
|
+
const output = readAccessor(document, sampler.output);
|
|
1774
|
+
if (!input.length || !output.length) {
|
|
1115
1775
|
return null;
|
|
1116
1776
|
}
|
|
1777
|
+
const duration = input.at(-1) || 0;
|
|
1778
|
+
const sampleTime = duration > 0 ? timeSeconds % duration : 0;
|
|
1779
|
+
let frame = 0;
|
|
1780
|
+
while (frame < input.length - 2 && sampleTime > input[frame + 1]) {
|
|
1781
|
+
frame += 1;
|
|
1782
|
+
}
|
|
1783
|
+
const start = input[frame] ?? 0;
|
|
1784
|
+
const end = input[frame + 1] ?? start;
|
|
1785
|
+
const t = end > start ? (sampleTime - start) / (end - start) : 0;
|
|
1786
|
+
const startValue = Array.isArray(output[frame]) ? output[frame] : [output[frame] ?? 0];
|
|
1787
|
+
const endValue = Array.isArray(output[frame + 1]) ? output[frame + 1] : startValue;
|
|
1788
|
+
return channel.target.path === "rotation" ? nlerpQuaternion(startValue, endValue, t) : lerpArray(startValue, endValue, t);
|
|
1117
1789
|
}
|
|
1118
|
-
function
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (!ctx) {
|
|
1122
|
-
throw new Error("Animated scene renderer requires a 2D canvas context.");
|
|
1790
|
+
function rootTranslationDistance(clipPayload) {
|
|
1791
|
+
if (!clipPayload) {
|
|
1792
|
+
return 0;
|
|
1123
1793
|
}
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const clipProfiles = resolveClipProfiles(clipAssets);
|
|
1129
|
-
const beatTimeline = buildBeatTimeline(beats, route, clipProfiles);
|
|
1130
|
-
const characterModel = resolveCharacterModel(options);
|
|
1131
|
-
let camera = {
|
|
1132
|
-
...DEFAULT_CAMERA,
|
|
1133
|
-
...definedCameraOverrides(options.camera ?? options.animationAdventure?.camera)
|
|
1134
|
-
};
|
|
1135
|
-
const loopDurationMs = Math.max(beatTimeline.at(-1)?.endMs ?? durationFromBeats(beats), 1);
|
|
1136
|
-
const movementWarnings = beatTimeline.flatMap((entry) => entry.warnings);
|
|
1137
|
-
const requestFrame = options.requestAnimationFrame ?? globalThis.requestAnimationFrame?.bind(globalThis);
|
|
1138
|
-
const cancelFrame = options.cancelAnimationFrame ?? globalThis.cancelAnimationFrame?.bind(globalThis);
|
|
1139
|
-
let frameHandle;
|
|
1140
|
-
let running = false;
|
|
1141
|
-
let frame = 0;
|
|
1142
|
-
let startedAtMs;
|
|
1143
|
-
let lastTimestamp;
|
|
1144
|
-
let pendingCameraControl = null;
|
|
1145
|
-
let activeCameraControl = false;
|
|
1146
|
-
let headLookWeight = 0;
|
|
1147
|
-
let cameraPosition = route[0] ? [
|
|
1148
|
-
route[0].position[0] + (camera.offset?.[0] ?? 0),
|
|
1149
|
-
route[0].position[1] + (camera.offset?.[1] ?? 0),
|
|
1150
|
-
route[0].position[2] + (camera.offset?.[2] ?? 0)
|
|
1151
|
-
] : [0, 2.4, 5.5];
|
|
1152
|
-
let cameraRigFrame = (0, import_gpu_camera.resolveCameraRigFrame)({
|
|
1153
|
-
viewMode: "editor",
|
|
1154
|
-
anchors: {
|
|
1155
|
-
target: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
1156
|
-
head: route[0]?.position ? [route[0].position[0], route[0].position[1] + 1.65, route[0].position[2]] : [0, 1.65, 0],
|
|
1157
|
-
forward: [0, 0, -1]
|
|
1158
|
-
},
|
|
1159
|
-
camera: {
|
|
1160
|
-
id: "animation-adventure",
|
|
1161
|
-
transform: {
|
|
1162
|
-
position: [...cameraPosition],
|
|
1163
|
-
target: route[0]?.position ? [...route[0].position] : [0, 0, 0]
|
|
1164
|
-
}
|
|
1165
|
-
},
|
|
1166
|
-
constraints: camera.constraints
|
|
1167
|
-
});
|
|
1168
|
-
let snapshot = {
|
|
1169
|
-
frame,
|
|
1170
|
-
running,
|
|
1171
|
-
activeClipId: beats[0]?.clipId ?? "",
|
|
1172
|
-
activeBeatId: beats[0]?.id ?? "",
|
|
1173
|
-
activeMovementMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
1174
|
-
blendProgress: 0,
|
|
1175
|
-
clipTimeMs: 0,
|
|
1176
|
-
characterPosition: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
1177
|
-
cameraPosition: [...cameraPosition],
|
|
1178
|
-
cameraViewMode: resolveCameraViewMode(camera),
|
|
1179
|
-
cameraTransform: cameraRigFrame.transform,
|
|
1180
|
-
targetDistance: cameraRigFrame.targetDistance,
|
|
1181
|
-
headLook: cameraRigFrame.headLook,
|
|
1182
|
-
characterGroundY: 0,
|
|
1183
|
-
characterVisible: false,
|
|
1184
|
-
modelLoaded: Boolean(characterModel),
|
|
1185
|
-
modelRenderable: false,
|
|
1186
|
-
fallbackProxyActive: true,
|
|
1187
|
-
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
1188
|
-
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
1189
|
-
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
1190
|
-
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
1191
|
-
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
1192
|
-
activeClipRenderable: false,
|
|
1193
|
-
propGroundAnchors: [],
|
|
1194
|
-
movementValidation: {
|
|
1195
|
-
status: movementWarnings.length ? "warning" : "passed",
|
|
1196
|
-
warnings: movementWarnings,
|
|
1197
|
-
activeBeatId: beats[0]?.id ?? "",
|
|
1198
|
-
activeClipId: beats[0]?.clipId ?? "",
|
|
1199
|
-
motionMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
1200
|
-
rootMotionSource: beatTimeline[0]?.movementProfile?.motionMode ?? "none",
|
|
1201
|
-
expectedSpeed: beatTimeline[0]?.expectedSpeed ?? 0,
|
|
1202
|
-
actualSpeed: beatTimeline[0]?.actualSpeed ?? 0,
|
|
1203
|
-
movementDistance: beatTimeline[0]?.movementDistance ?? 0,
|
|
1204
|
-
loopCount: beatTimeline[0]?.loopCount ?? 1,
|
|
1205
|
-
footSlideWarning: beatTimeline[0]?.footSlideWarning ?? null
|
|
1206
|
-
},
|
|
1207
|
-
frameState: "initialized"
|
|
1208
|
-
};
|
|
1209
|
-
function renderOnce(timestamp = nowMs()) {
|
|
1210
|
-
if (startedAtMs === void 0) {
|
|
1211
|
-
startedAtMs = timestamp;
|
|
1794
|
+
let distance = 0;
|
|
1795
|
+
for (const channel of clipPayload.animation.channels ?? []) {
|
|
1796
|
+
if (channel.target?.path !== "translation") {
|
|
1797
|
+
continue;
|
|
1212
1798
|
}
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
lastTimestamp = timestamp;
|
|
1217
|
-
frame += 1;
|
|
1218
|
-
const timelineEntry = resolveTimelineBeat(beatTimeline, loopTimeMs);
|
|
1219
|
-
const fallbackBeat = resolveBeat(beats, loopTimeMs);
|
|
1220
|
-
const beat = timelineEntry?.beat ?? fallbackBeat.beat;
|
|
1221
|
-
const beatTimeMs = timelineEntry ? loopTimeMs - timelineEntry.startMs : fallbackBeat.beatTimeMs;
|
|
1222
|
-
const durationMs = timelineEntry?.durationMs ?? fallbackBeat.durationMs;
|
|
1223
|
-
const blend = beat?.blend ?? { inMs: 0, outMs: 0 };
|
|
1224
|
-
const blendIn = blend.inMs > 0 ? clamp01(beatTimeMs / blend.inMs) : 1;
|
|
1225
|
-
const blendOut = blend.outMs > 0 ? clamp01((durationMs - beatTimeMs) / blend.outMs) : 1;
|
|
1226
|
-
const blendProgress = clamp01(Math.min(blendIn, blendOut));
|
|
1227
|
-
const characterPosition = timelineEntry ? resolveTimelinePosition(timelineEntry, beatTimeMs) : resolveRoutePosition(route, loopTimeMs);
|
|
1228
|
-
const characterForward = timelineEntry ? resolveTimelineForward(timelineEntry) : resolveRouteForward(route, loopTimeMs);
|
|
1229
|
-
const headAnchor = camera.headBoneAvailable === false ? void 0 : [
|
|
1230
|
-
characterPosition[0],
|
|
1231
|
-
characterPosition[1] + (camera.headHeight ?? 1.65),
|
|
1232
|
-
characterPosition[2]
|
|
1233
|
-
];
|
|
1234
|
-
const lookAheadTimeMs = (loopTimeMs + camera.lookAheadMs) % loopDurationMs;
|
|
1235
|
-
const lookAheadEntry = resolveTimelineBeat(beatTimeline, lookAheadTimeMs);
|
|
1236
|
-
const lookAheadPosition = lookAheadEntry ? resolveTimelinePosition(lookAheadEntry, lookAheadTimeMs - lookAheadEntry.startMs) : resolveRoutePosition(route, lookAheadTimeMs);
|
|
1237
|
-
const desiredCamera = [
|
|
1238
|
-
lookAheadPosition[0] + (camera.offset?.[0] ?? 0),
|
|
1239
|
-
lookAheadPosition[1] + (camera.offset?.[1] ?? 0),
|
|
1240
|
-
lookAheadPosition[2] + (camera.offset?.[2] ?? 0)
|
|
1241
|
-
];
|
|
1242
|
-
const smoothing = bezierY(camera.cubicBezier, clamp01(frameTimeMs / Math.max(1, camera.lagMs)));
|
|
1243
|
-
cameraPosition = lerp3(cameraPosition, desiredCamera, smoothing);
|
|
1244
|
-
const cameraViewMode = resolveCameraViewMode(camera);
|
|
1245
|
-
const rigViewMode = cameraViewMode === "spectator" ? "editor" : cameraViewMode;
|
|
1246
|
-
cameraRigFrame = (0, import_gpu_camera.resolveCameraRigFrame)({
|
|
1247
|
-
viewMode: rigViewMode,
|
|
1248
|
-
anchors: {
|
|
1249
|
-
target: characterPosition,
|
|
1250
|
-
...headAnchor ? { head: headAnchor } : {},
|
|
1251
|
-
forward: characterForward
|
|
1252
|
-
},
|
|
1253
|
-
camera: {
|
|
1254
|
-
id: "animation-adventure",
|
|
1255
|
-
transform: {
|
|
1256
|
-
position: [...cameraPosition],
|
|
1257
|
-
target: lookAheadPosition
|
|
1258
|
-
}
|
|
1259
|
-
},
|
|
1260
|
-
constraints: camera.constraints,
|
|
1261
|
-
control: pendingCameraControl,
|
|
1262
|
-
activeControl: activeCameraControl
|
|
1263
|
-
});
|
|
1264
|
-
if (activeCameraControl && cameraRigFrame.headLook.status === "active") {
|
|
1265
|
-
headLookWeight = cameraRigFrame.headLook.weight;
|
|
1266
|
-
} else {
|
|
1267
|
-
const returnMs = Math.max(1, camera.headLook?.returnMs ?? DEFAULT_CAMERA.headLook.returnMs);
|
|
1268
|
-
headLookWeight = lerp2(headLookWeight, 0, clamp01(frameTimeMs / returnMs));
|
|
1799
|
+
const nodeName = clipPayload.document.json.nodes?.[channel.target.node]?.name ?? "";
|
|
1800
|
+
if (!/(^|:)Hips$/u.test(nodeName) && nodeName !== "mixamorigHips") {
|
|
1801
|
+
continue;
|
|
1269
1802
|
}
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
};
|
|
1275
|
-
cameraPosition = [...cameraRigFrame.transform.position];
|
|
1276
|
-
pendingCameraControl = null;
|
|
1277
|
-
activeCameraControl = false;
|
|
1278
|
-
snapshot = {
|
|
1279
|
-
frame,
|
|
1280
|
-
running,
|
|
1281
|
-
activeClipId: beat?.clipId ?? "",
|
|
1282
|
-
activeBeatId: beat?.id ?? "",
|
|
1283
|
-
activeMovementMode: timelineEntry?.movementType ?? "stationary",
|
|
1284
|
-
blendProgress,
|
|
1285
|
-
clipTimeMs: beatTimeMs,
|
|
1286
|
-
characterPosition,
|
|
1287
|
-
cameraPosition: [...cameraPosition],
|
|
1288
|
-
cameraViewMode,
|
|
1289
|
-
cameraTransform: cameraRigFrame.transform,
|
|
1290
|
-
targetDistance: cameraRigFrame.targetDistance,
|
|
1291
|
-
headLook,
|
|
1292
|
-
characterGroundY: 0,
|
|
1293
|
-
characterVisible: false,
|
|
1294
|
-
modelLoaded: Boolean(characterModel),
|
|
1295
|
-
modelRenderable: false,
|
|
1296
|
-
fallbackProxyActive: true,
|
|
1297
|
-
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
1298
|
-
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
1299
|
-
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
1300
|
-
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
1301
|
-
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
1302
|
-
activeClipRenderable: false,
|
|
1303
|
-
propGroundAnchors: [],
|
|
1304
|
-
movementValidation: {
|
|
1305
|
-
status: movementWarnings.length ? "warning" : "passed",
|
|
1306
|
-
warnings: [...movementWarnings],
|
|
1307
|
-
activeBeatId: beat?.id ?? "",
|
|
1308
|
-
activeClipId: beat?.clipId ?? "",
|
|
1309
|
-
motionMode: timelineEntry?.movementType ?? "stationary",
|
|
1310
|
-
rootMotionSource: timelineEntry?.movementProfile?.motionMode ?? "none",
|
|
1311
|
-
expectedSpeed: timelineEntry?.expectedSpeed ?? 0,
|
|
1312
|
-
actualSpeed: timelineEntry?.actualSpeed ?? 0,
|
|
1313
|
-
movementDistance: timelineEntry?.movementDistance ?? 0,
|
|
1314
|
-
loopCount: timelineEntry?.loopCount ?? 1,
|
|
1315
|
-
footSlideWarning: timelineEntry?.footSlideWarning ?? null
|
|
1316
|
-
},
|
|
1317
|
-
frameState: running ? "running" : "rendered-once"
|
|
1318
|
-
};
|
|
1319
|
-
const visibility = renderScene(canvas, ctx, snapshot, props, characterModel);
|
|
1320
|
-
snapshot = {
|
|
1321
|
-
...snapshot,
|
|
1322
|
-
...visibility
|
|
1323
|
-
};
|
|
1324
|
-
return snapshot;
|
|
1325
|
-
}
|
|
1326
|
-
function tick(timestamp) {
|
|
1327
|
-
if (!running) {
|
|
1328
|
-
return;
|
|
1803
|
+
const sampler = clipPayload.animation.samplers?.[channel.sampler];
|
|
1804
|
+
const output = sampler ? readAccessor(clipPayload.document, sampler.output) : [];
|
|
1805
|
+
if (output.length < 2) {
|
|
1806
|
+
continue;
|
|
1329
1807
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1808
|
+
const start = Array.isArray(output[0]) ? output[0] : [0, 0, 0];
|
|
1809
|
+
const end = Array.isArray(output.at(-1)) ? output.at(-1) : start;
|
|
1810
|
+
distance = Math.max(distance, Math.hypot((end[0] ?? 0) - (start[0] ?? 0), (end[2] ?? 0) - (start[2] ?? 0)));
|
|
1811
|
+
}
|
|
1812
|
+
return distance;
|
|
1813
|
+
}
|
|
1814
|
+
function createClipPayload(clipRef) {
|
|
1815
|
+
const document = parseGlb(clipRef?.asset);
|
|
1816
|
+
const animation = document?.json.animations?.[0];
|
|
1817
|
+
if (!document || !animation) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
const nodeNames = /* @__PURE__ */ new Set();
|
|
1821
|
+
let durationSeconds = 0;
|
|
1822
|
+
for (const channel of animation.channels ?? []) {
|
|
1823
|
+
const nodeName = document.json.nodes?.[channel.target?.node]?.name;
|
|
1824
|
+
if (nodeName) {
|
|
1825
|
+
nodeNames.add(nodeName);
|
|
1333
1826
|
}
|
|
1827
|
+
const sampler = animation.samplers?.[channel.sampler];
|
|
1828
|
+
const input = sampler ? readAccessor(document, sampler.input) : [];
|
|
1829
|
+
durationSeconds = Math.max(durationSeconds, input.at(-1) ?? 0);
|
|
1334
1830
|
}
|
|
1335
1831
|
return {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1832
|
+
id: clipRef.id,
|
|
1833
|
+
document,
|
|
1834
|
+
animation,
|
|
1835
|
+
durationSeconds,
|
|
1836
|
+
nodeNames
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function sampleClipOverrides(clipPayload, timeSeconds) {
|
|
1840
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1841
|
+
if (!clipPayload) {
|
|
1842
|
+
return overrides;
|
|
1843
|
+
}
|
|
1844
|
+
for (const channel of clipPayload.animation.channels ?? []) {
|
|
1845
|
+
const targetNode = clipPayload.document.json.nodes?.[channel.target?.node];
|
|
1846
|
+
const sampler = clipPayload.animation.samplers?.[channel.sampler];
|
|
1847
|
+
if (!targetNode?.name || !sampler) {
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const value = sampleChannel(channel, sampler, clipPayload.document, timeSeconds);
|
|
1851
|
+
if (!value) {
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
const existing = overrides.get(targetNode.name) ?? {};
|
|
1855
|
+
overrides.set(targetNode.name, {
|
|
1856
|
+
...existing,
|
|
1857
|
+
[channel.target.path]: value
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
return overrides;
|
|
1861
|
+
}
|
|
1862
|
+
function createAnimatedGltfModel(modelAsset, clipRefs = []) {
|
|
1863
|
+
const document = parseGlb(modelAsset);
|
|
1864
|
+
if (!document) {
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
const nodes = document.json.nodes ?? [];
|
|
1868
|
+
const meshNodeIndex = nodes.findIndex((node) => node.mesh !== void 0 && node.skin !== void 0);
|
|
1869
|
+
const meshNode = nodes[meshNodeIndex];
|
|
1870
|
+
const mesh = document.json.meshes?.[meshNode?.mesh];
|
|
1871
|
+
const primitive = mesh?.primitives?.find((candidate) => candidate.attributes?.POSITION !== void 0);
|
|
1872
|
+
const skin = document.json.skins?.[meshNode?.skin];
|
|
1873
|
+
if (!meshNode || !primitive || !skin) {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
const positions = readAccessor(document, primitive.attributes.POSITION);
|
|
1877
|
+
const normals = primitive.attributes.NORMAL === void 0 ? [] : readAccessor(document, primitive.attributes.NORMAL);
|
|
1878
|
+
const texcoords = primitive.attributes.TEXCOORD_0 === void 0 ? [] : readAccessor(document, primitive.attributes.TEXCOORD_0);
|
|
1879
|
+
const joints = primitive.attributes.JOINTS_0 === void 0 ? positions.map(() => [0, 0, 0, 0]) : readAccessor(document, primitive.attributes.JOINTS_0);
|
|
1880
|
+
const weights = primitive.attributes.WEIGHTS_0 === void 0 ? positions.map(() => [1, 0, 0, 0]) : readAccessor(document, primitive.attributes.WEIGHTS_0).map(normalizeWeights);
|
|
1881
|
+
const indices = primitive.indices === void 0 ? positions.map((_, index) => index) : readAccessor(document, primitive.indices);
|
|
1882
|
+
const inverseBindMatrices = skin.inverseBindMatrices === void 0 ? skin.joints.map(() => identityMatrix()) : readMatrices(document, skin.inverseBindMatrices);
|
|
1883
|
+
const clips = clipRefs.map(createClipPayload).filter(Boolean);
|
|
1884
|
+
const bindWorldMatrices = computeWorldMatrices(nodes);
|
|
1885
|
+
const bindVertices = skinVertices({
|
|
1886
|
+
positions,
|
|
1887
|
+
joints,
|
|
1888
|
+
weights,
|
|
1889
|
+
skin,
|
|
1890
|
+
inverseBindMatrices,
|
|
1891
|
+
worldMatrices: bindWorldMatrices
|
|
1892
|
+
});
|
|
1893
|
+
const bounds = deriveBounds(bindVertices);
|
|
1894
|
+
const materialTextureMetadata = extractMaterialTextureMetadata(document, primitive);
|
|
1895
|
+
const clipRootMotionDistances = new Map(clips.map((clip) => [clip.id, rootTranslationDistance(clip)]));
|
|
1896
|
+
return {
|
|
1897
|
+
name: mesh.name ?? meshNode.name ?? "skinned-gltf-model",
|
|
1898
|
+
vertexCount: positions.length,
|
|
1899
|
+
triangleCount: Math.floor(indices.length / 3),
|
|
1900
|
+
jointCount: skin.joints.length,
|
|
1901
|
+
animatedNodeCount: new Set(clips.flatMap((clip) => [...clip.nodeNames])).size,
|
|
1902
|
+
clipCount: clips.length,
|
|
1903
|
+
materialTextureMetadata,
|
|
1904
|
+
textureCount: materialTextureMetadata.textureCount,
|
|
1905
|
+
materialCount: materialTextureMetadata.materialCount,
|
|
1906
|
+
hasBaseColorTexture: materialTextureMetadata.hasBaseColorTexture,
|
|
1907
|
+
hasNormalTexture: materialTextureMetadata.hasNormalTexture,
|
|
1908
|
+
hasUv: texcoords.length === positions.length,
|
|
1909
|
+
hasNormals: normals.length === positions.length,
|
|
1910
|
+
professionalRenderable: materialTextureMetadata.hasBaseColorTexture && materialTextureMetadata.hasNormalTexture && materialTextureMetadata.missingTextureReferences.length === 0 && texcoords.length === positions.length && normals.length === positions.length && skin.joints.length > 0,
|
|
1911
|
+
clips,
|
|
1912
|
+
sample(activeClipId, clipTimeMs) {
|
|
1913
|
+
const clip = clips.find((candidate) => candidate.id === activeClipId) ?? clips[0] ?? null;
|
|
1914
|
+
const overrides = sampleClipOverrides(clip, Math.max(0, clipTimeMs) / 1e3);
|
|
1915
|
+
const worldMatrices = computeWorldMatrices(nodes, overrides);
|
|
1916
|
+
const vertices = skinVertices({
|
|
1917
|
+
positions,
|
|
1918
|
+
joints,
|
|
1919
|
+
weights,
|
|
1920
|
+
skin,
|
|
1921
|
+
inverseBindMatrices,
|
|
1922
|
+
worldMatrices
|
|
1923
|
+
});
|
|
1364
1924
|
return {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
},
|
|
1373
|
-
headLook: {
|
|
1374
|
-
...snapshot.headLook,
|
|
1375
|
-
target: [...snapshot.headLook.target]
|
|
1376
|
-
},
|
|
1377
|
-
movementValidation: {
|
|
1378
|
-
...snapshot.movementValidation,
|
|
1379
|
-
warnings: [...snapshot.movementValidation?.warnings ?? []]
|
|
1380
|
-
},
|
|
1381
|
-
propGroundAnchors: snapshot.propGroundAnchors.map((anchor) => ({ ...anchor }))
|
|
1382
|
-
};
|
|
1383
|
-
},
|
|
1384
|
-
setCamera(nextCamera = {}) {
|
|
1385
|
-
camera = { ...camera, ...nextCamera };
|
|
1386
|
-
snapshot = {
|
|
1387
|
-
...snapshot,
|
|
1388
|
-
cameraViewMode: resolveCameraViewMode(camera)
|
|
1389
|
-
};
|
|
1390
|
-
},
|
|
1391
|
-
setCameraViewMode(viewMode) {
|
|
1392
|
-
camera = { ...camera, viewMode };
|
|
1393
|
-
snapshot = {
|
|
1394
|
-
...snapshot,
|
|
1395
|
-
cameraViewMode: resolveCameraViewMode(camera)
|
|
1925
|
+
vertices,
|
|
1926
|
+
indices,
|
|
1927
|
+
bounds: deriveBounds(vertices),
|
|
1928
|
+
bindBounds: bounds,
|
|
1929
|
+
activeClipRenderable: Boolean(clip),
|
|
1930
|
+
activeClipDurationSeconds: clip?.durationSeconds ?? 0,
|
|
1931
|
+
activeClipRootTranslationDistance: clipRootMotionDistances.get(clip?.id) ?? 0
|
|
1396
1932
|
};
|
|
1397
|
-
},
|
|
1398
|
-
applyCameraControl(control, options2 = {}) {
|
|
1399
|
-
pendingCameraControl = control;
|
|
1400
|
-
activeCameraControl = options2.activeControl !== false;
|
|
1401
|
-
},
|
|
1402
|
-
destroy() {
|
|
1403
|
-
running = false;
|
|
1404
|
-
if (frameHandle !== void 0 && cancelFrame) {
|
|
1405
|
-
cancelFrame(frameHandle);
|
|
1406
|
-
}
|
|
1407
|
-
frameHandle = void 0;
|
|
1408
|
-
snapshot = { ...snapshot, running: false, frameState: "destroyed" };
|
|
1409
1933
|
}
|
|
1410
1934
|
};
|
|
1411
1935
|
}
|
|
1936
|
+
function skinVertices({ positions, joints, weights, skin, inverseBindMatrices, worldMatrices }) {
|
|
1937
|
+
return positions.map((position, vertexIndex) => {
|
|
1938
|
+
const jointIndices = joints[vertexIndex] ?? [0, 0, 0, 0];
|
|
1939
|
+
const jointWeights = weights[vertexIndex] ?? [1, 0, 0, 0];
|
|
1940
|
+
const skinned = [0, 0, 0];
|
|
1941
|
+
for (let influence = 0; influence < 4; influence += 1) {
|
|
1942
|
+
const weight = jointWeights[influence] ?? 0;
|
|
1943
|
+
if (weight <= 0) {
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
const jointNodeIndex = skin.joints[jointIndices[influence] ?? 0] ?? skin.joints[0];
|
|
1947
|
+
const jointMatrix = multiplyMatrix(worldMatrices[jointNodeIndex] ?? identityMatrix(), inverseBindMatrices[jointIndices[influence] ?? 0] ?? identityMatrix());
|
|
1948
|
+
const transformed = transformPoint(jointMatrix, position);
|
|
1949
|
+
skinned[0] += transformed[0] * weight;
|
|
1950
|
+
skinned[1] += transformed[1] * weight;
|
|
1951
|
+
skinned[2] += transformed[2] * weight;
|
|
1952
|
+
}
|
|
1953
|
+
return skinned;
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1412
1956
|
|
|
1413
|
-
// src/renderer
|
|
1414
|
-
var
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
])
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
])
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
]
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1957
|
+
// src/animated-scene-renderer.js
|
|
1958
|
+
var DEFAULT_CAMERA = Object.freeze({
|
|
1959
|
+
mode: "lagged-follow",
|
|
1960
|
+
viewMode: "spectator",
|
|
1961
|
+
cubicBezier: [0.22, 0.61, 0.36, 1],
|
|
1962
|
+
lagMs: 240,
|
|
1963
|
+
lookAheadMs: 320,
|
|
1964
|
+
offset: [0, 2.4, 5.5],
|
|
1965
|
+
constraints: Object.freeze({
|
|
1966
|
+
maxDistance: 10,
|
|
1967
|
+
firstPersonHeadOffset: 0.05
|
|
1968
|
+
}),
|
|
1969
|
+
headLook: Object.freeze({
|
|
1970
|
+
enabled: true,
|
|
1971
|
+
returnMs: 240
|
|
1972
|
+
})
|
|
1973
|
+
});
|
|
1974
|
+
function nowMs() {
|
|
1975
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
1976
|
+
}
|
|
1977
|
+
function clamp012(value) {
|
|
1978
|
+
return Math.min(1, Math.max(0, value));
|
|
1979
|
+
}
|
|
1980
|
+
function lerp2(a, b, t) {
|
|
1981
|
+
return a + (b - a) * t;
|
|
1982
|
+
}
|
|
1983
|
+
function lerp3(a, b, t) {
|
|
1984
|
+
return [
|
|
1985
|
+
lerp2(a[0] ?? 0, b[0] ?? 0, t),
|
|
1986
|
+
lerp2(a[1] ?? 0, b[1] ?? 0, t),
|
|
1987
|
+
lerp2(a[2] ?? 0, b[2] ?? 0, t)
|
|
1988
|
+
];
|
|
1989
|
+
}
|
|
1990
|
+
function normalize3(value, fallback = [0, 0, -1]) {
|
|
1991
|
+
if (!Array.isArray(value) || value.length < 3) {
|
|
1992
|
+
return [...fallback];
|
|
1993
|
+
}
|
|
1994
|
+
const vector = [
|
|
1995
|
+
Number.isFinite(Number(value[0])) ? Number(value[0]) : fallback[0],
|
|
1996
|
+
Number.isFinite(Number(value[1])) ? Number(value[1]) : fallback[1],
|
|
1997
|
+
Number.isFinite(Number(value[2])) ? Number(value[2]) : fallback[2]
|
|
1998
|
+
];
|
|
1999
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
2000
|
+
if (length <= 1e-6) {
|
|
2001
|
+
return [...fallback];
|
|
2002
|
+
}
|
|
2003
|
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
2004
|
+
}
|
|
2005
|
+
function clamp(value, min, max) {
|
|
2006
|
+
return Math.min(max, Math.max(min, value));
|
|
2007
|
+
}
|
|
2008
|
+
function bezierY([, y1, , y2], t) {
|
|
2009
|
+
const u = 1 - t;
|
|
2010
|
+
return 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t;
|
|
2011
|
+
}
|
|
2012
|
+
function durationFromBeats(beats) {
|
|
2013
|
+
return beats.reduce((total, beat) => total + Math.max(0, beat.durationMs ?? 0), 0);
|
|
2014
|
+
}
|
|
2015
|
+
function definedCameraOverrides(camera) {
|
|
2016
|
+
if (!camera || typeof camera !== "object") {
|
|
2017
|
+
return {};
|
|
2018
|
+
}
|
|
2019
|
+
return Object.fromEntries(Object.entries(camera).filter(([, value]) => value !== void 0));
|
|
2020
|
+
}
|
|
2021
|
+
function resolveBeat(beats, loopTimeMs) {
|
|
2022
|
+
let cursor = 0;
|
|
2023
|
+
for (const beat of beats) {
|
|
2024
|
+
const durationMs = Math.max(1, beat.durationMs ?? 1);
|
|
2025
|
+
if (loopTimeMs < cursor + durationMs) {
|
|
2026
|
+
return { beat, beatTimeMs: loopTimeMs - cursor, durationMs };
|
|
2027
|
+
}
|
|
2028
|
+
cursor += durationMs;
|
|
2029
|
+
}
|
|
2030
|
+
const fallback = beats.at(-1);
|
|
2031
|
+
return { beat: fallback, beatTimeMs: fallback?.durationMs ?? 0, durationMs: fallback?.durationMs ?? 1 };
|
|
2032
|
+
}
|
|
2033
|
+
function resolveRoutePosition(route, timeMs) {
|
|
2034
|
+
if (!route.length) {
|
|
2035
|
+
return [0, 0, 0];
|
|
2036
|
+
}
|
|
2037
|
+
if (route.length === 1 || timeMs <= (route[0].arriveMs ?? 0)) {
|
|
2038
|
+
return [...route[0].position];
|
|
2039
|
+
}
|
|
2040
|
+
for (let index = 1; index < route.length; index += 1) {
|
|
2041
|
+
const previous = route[index - 1];
|
|
2042
|
+
const next = route[index];
|
|
2043
|
+
const start = previous.arriveMs ?? 0;
|
|
2044
|
+
const end = next.arriveMs ?? start + 1;
|
|
2045
|
+
if (timeMs <= end) {
|
|
2046
|
+
return lerp3(previous.position, next.position, clamp012((timeMs - start) / Math.max(1, end - start)));
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return [...route.at(-1).position];
|
|
2050
|
+
}
|
|
2051
|
+
function resolveRouteForward(route, timeMs) {
|
|
2052
|
+
if (route.length < 2) {
|
|
2053
|
+
return [0, 0, -1];
|
|
2054
|
+
}
|
|
2055
|
+
let previous = route[0];
|
|
2056
|
+
let next = route[1];
|
|
2057
|
+
for (let index = 1; index < route.length; index += 1) {
|
|
2058
|
+
next = route[index];
|
|
2059
|
+
if (timeMs <= (next.arriveMs ?? 0)) {
|
|
2060
|
+
previous = route[index - 1];
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return normalize3([
|
|
2065
|
+
(next.position?.[0] ?? 0) - (previous.position?.[0] ?? 0),
|
|
2066
|
+
(next.position?.[1] ?? 0) - (previous.position?.[1] ?? 0),
|
|
2067
|
+
(next.position?.[2] ?? 0) - (previous.position?.[2] ?? 0)
|
|
2068
|
+
]);
|
|
2069
|
+
}
|
|
2070
|
+
function distance3(a = [0, 0, 0], b = [0, 0, 0]) {
|
|
2071
|
+
return Math.hypot(
|
|
2072
|
+
(b[0] ?? 0) - (a[0] ?? 0),
|
|
2073
|
+
(b[1] ?? 0) - (a[1] ?? 0),
|
|
2074
|
+
(b[2] ?? 0) - (a[2] ?? 0)
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
function movementRequirementType(requirement, beat) {
|
|
2078
|
+
if (requirement?.type) {
|
|
2079
|
+
return requirement.type;
|
|
2080
|
+
}
|
|
2081
|
+
if (beat?.kind === "locomotion") {
|
|
2082
|
+
return beat?.rootMotion === "in-place" ? "stationary" : "travel";
|
|
2083
|
+
}
|
|
2084
|
+
if (beat?.kind === "action" || beat?.kind === "idle") {
|
|
2085
|
+
return "stationary";
|
|
2086
|
+
}
|
|
2087
|
+
const clipId = String(beat?.clipId ?? "").toLowerCase();
|
|
2088
|
+
if (/(idle|gesture|dig|plant|water|pick|serve|swing|fire|block|hoe)/iu.test(clipId)) {
|
|
2089
|
+
return "stationary";
|
|
2090
|
+
}
|
|
2091
|
+
if (/(walk|run|jump|climb|crawl|locomotion)/iu.test(clipId)) {
|
|
2092
|
+
return clipId.includes("jump") ? "jump" : "travel";
|
|
2093
|
+
}
|
|
2094
|
+
return beat?.pathPointId ? "travel" : "stationary";
|
|
2095
|
+
}
|
|
2096
|
+
function movementDistancePerLoop(profile = {}) {
|
|
2097
|
+
profile ??= {};
|
|
2098
|
+
const rootDistance = Number(profile.rootTranslationDistance ?? profile.rootTranslation?.distance ?? 0);
|
|
2099
|
+
if (Number.isFinite(rootDistance) && rootDistance > 0) {
|
|
2100
|
+
return rootDistance;
|
|
2101
|
+
}
|
|
2102
|
+
const strideLength = Number(profile.strideLength ?? profile.strideLengthMeters ?? 0);
|
|
2103
|
+
if (Number.isFinite(strideLength) && strideLength > 0) {
|
|
2104
|
+
return strideLength;
|
|
2105
|
+
}
|
|
2106
|
+
return 0;
|
|
2107
|
+
}
|
|
2108
|
+
function clipDurationMs(profile = {}, fallbackMs = 1) {
|
|
2109
|
+
profile ??= {};
|
|
2110
|
+
const duration = Number(profile.durationMs ?? 0);
|
|
2111
|
+
return Number.isFinite(duration) && duration > 0 ? duration : Math.max(1, fallbackMs);
|
|
2112
|
+
}
|
|
2113
|
+
function profileAllowsDisplacement(profile = {}, movementType) {
|
|
2114
|
+
profile ??= {};
|
|
2115
|
+
if (movementType === "stationary") {
|
|
2116
|
+
return profile.worldDisplacementAllowed !== true;
|
|
2117
|
+
}
|
|
2118
|
+
return profile.worldDisplacementAllowed === true || profile.motionMode === "root-authored" || profile.motionMode === "calibrated-in-place" || profile.motionMode === "jump";
|
|
2119
|
+
}
|
|
2120
|
+
function resolveClipProfiles(clipAssets = []) {
|
|
2121
|
+
return new Map(clipAssets.map((clip) => [clip.id, clip.movementProfile ?? clip.profile ?? null]));
|
|
2122
|
+
}
|
|
2123
|
+
function buildBeatTimeline(beats, route, clipProfiles) {
|
|
2124
|
+
const routePoints = new Map(route.map((point) => [point.id, point]));
|
|
2125
|
+
let cursorMs = 0;
|
|
2126
|
+
let routeCursor = 0;
|
|
2127
|
+
let currentPosition = route[0]?.position ? [...route[0].position] : [0, 0, 0];
|
|
2128
|
+
return beats.map((beat) => {
|
|
2129
|
+
const requirement = beat.movementRequirement ?? beat.movement ?? null;
|
|
2130
|
+
const movementType = movementRequirementType(requirement, beat);
|
|
2131
|
+
const movesThroughWorld = movementType === "travel" || movementType === "jump" || movementType === "root-authored";
|
|
2132
|
+
const explicitTargetPoint = beat.pathPointId ? routePoints.get(beat.pathPointId) : null;
|
|
2133
|
+
const inferredTargetPoint = movesThroughWorld ? route[Math.min(route.length - 1, routeCursor + 1)] : null;
|
|
2134
|
+
const targetPoint = explicitTargetPoint ?? inferredTargetPoint ?? null;
|
|
2135
|
+
const targetPosition = targetPoint?.position ? [...targetPoint.position] : [...currentPosition];
|
|
2136
|
+
const startPosition = [...currentPosition];
|
|
2137
|
+
const endPosition = movesThroughWorld ? targetPosition : [...currentPosition];
|
|
2138
|
+
const segmentDistance = Number(requirement?.distance ?? distance3(startPosition, endPosition));
|
|
2139
|
+
const profile = clipProfiles.get(beat.clipId) ?? null;
|
|
2140
|
+
const distancePerLoop = movementDistancePerLoop(profile);
|
|
2141
|
+
const loopCount = movesThroughWorld && distancePerLoop > 0 ? Math.max(1, Math.ceil(segmentDistance / distancePerLoop)) : 1;
|
|
2142
|
+
const derivedDurationMs = movesThroughWorld && distancePerLoop > 0 ? loopCount * clipDurationMs(profile, beat.durationMs) : Math.max(1, beat.durationMs ?? 1);
|
|
2143
|
+
const durationMs = Math.max(1, beat.validatedDurationMs ?? requirement?.validatedDurationMs ?? derivedDurationMs);
|
|
2144
|
+
const expectedSpeed = Number(profile?.expectedSpeed ?? (durationMs > 0 ? segmentDistance / (durationMs / 1e3) : 0));
|
|
2145
|
+
const actualSpeed = durationMs > 0 ? segmentDistance / (durationMs / 1e3) : 0;
|
|
2146
|
+
const warnings = [];
|
|
2147
|
+
if (movesThroughWorld && !profileAllowsDisplacement(profile, movementType)) {
|
|
2148
|
+
warnings.push(`clip '${beat.clipId}' does not allow ${movementType} displacement`);
|
|
2149
|
+
}
|
|
2150
|
+
if (movesThroughWorld && distancePerLoop <= 0 && profile?.motionMode !== "root-authored") {
|
|
2151
|
+
warnings.push(`clip '${beat.clipId}' has no root or calibrated stride distance`);
|
|
2152
|
+
}
|
|
2153
|
+
if (movementType === "stationary" && profile?.worldDisplacementAllowed === true) {
|
|
2154
|
+
warnings.push(`stationary beat '${beat.id}' uses a displacement-capable clip`);
|
|
2155
|
+
}
|
|
2156
|
+
const entry = {
|
|
2157
|
+
beat,
|
|
2158
|
+
startMs: cursorMs,
|
|
2159
|
+
endMs: cursorMs + durationMs,
|
|
2160
|
+
durationMs,
|
|
2161
|
+
startPosition,
|
|
2162
|
+
endPosition,
|
|
2163
|
+
movementType,
|
|
2164
|
+
movementProfile: profile,
|
|
2165
|
+
movementDistance: segmentDistance,
|
|
2166
|
+
distancePerLoop,
|
|
2167
|
+
loopCount,
|
|
2168
|
+
expectedSpeed,
|
|
2169
|
+
actualSpeed,
|
|
2170
|
+
footSlideWarning: Number(profile?.footSlideTolerance ?? 0) < 0 ? "invalid-foot-slide-tolerance" : null,
|
|
2171
|
+
warnings
|
|
2172
|
+
};
|
|
2173
|
+
cursorMs = entry.endMs;
|
|
2174
|
+
currentPosition = [...endPosition];
|
|
2175
|
+
if (targetPoint) {
|
|
2176
|
+
const nextRouteIndex = route.findIndex((point) => point.id === targetPoint.id);
|
|
2177
|
+
if (nextRouteIndex >= 0) {
|
|
2178
|
+
routeCursor = nextRouteIndex;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return entry;
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
function resolveTimelineBeat(timeline, loopTimeMs) {
|
|
2185
|
+
if (!timeline.length) {
|
|
2186
|
+
return null;
|
|
1465
2187
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
2188
|
+
return timeline.find((entry) => loopTimeMs < entry.endMs) ?? timeline.at(-1);
|
|
2189
|
+
}
|
|
2190
|
+
function resolveTimelinePosition(entry, beatTimeMs) {
|
|
2191
|
+
if (!entry) {
|
|
2192
|
+
return [0, 0, 0];
|
|
1468
2193
|
}
|
|
1469
|
-
|
|
2194
|
+
if (entry.movementType !== "travel" && entry.movementType !== "jump" && entry.movementType !== "root-authored") {
|
|
2195
|
+
return [...entry.startPosition];
|
|
2196
|
+
}
|
|
2197
|
+
return lerp3(entry.startPosition, entry.endPosition, clamp012(beatTimeMs / Math.max(1, entry.durationMs)));
|
|
1470
2198
|
}
|
|
1471
|
-
function
|
|
1472
|
-
if (!
|
|
1473
|
-
|
|
2199
|
+
function resolveTimelineForward(entry) {
|
|
2200
|
+
if (!entry) {
|
|
2201
|
+
return [0, 0, -1];
|
|
1474
2202
|
}
|
|
1475
|
-
return
|
|
2203
|
+
return normalize3([
|
|
2204
|
+
(entry.endPosition[0] ?? 0) - (entry.startPosition[0] ?? 0),
|
|
2205
|
+
(entry.endPosition[1] ?? 0) - (entry.startPosition[1] ?? 0),
|
|
2206
|
+
(entry.endPosition[2] ?? 0) - (entry.startPosition[2] ?? 0)
|
|
2207
|
+
]);
|
|
1476
2208
|
}
|
|
1477
|
-
function
|
|
1478
|
-
if (
|
|
1479
|
-
|
|
2209
|
+
function resolveCameraViewMode(camera) {
|
|
2210
|
+
if (camera?.mode === "lagged-follow") {
|
|
2211
|
+
return camera.viewMode ?? "spectator";
|
|
1480
2212
|
}
|
|
1481
|
-
return
|
|
2213
|
+
return camera?.viewMode ?? camera?.mode ?? "spectator";
|
|
1482
2214
|
}
|
|
1483
|
-
function
|
|
1484
|
-
if (typeof
|
|
1485
|
-
|
|
2215
|
+
function resolveCanvas2(canvas) {
|
|
2216
|
+
if (typeof canvas === "string") {
|
|
2217
|
+
const found = globalThis.document?.querySelector?.(canvas);
|
|
2218
|
+
if (!found) {
|
|
2219
|
+
throw new Error(`Animated scene canvas '${canvas}' was not found.`);
|
|
2220
|
+
}
|
|
2221
|
+
return found;
|
|
1486
2222
|
}
|
|
1487
|
-
|
|
2223
|
+
if (!canvas) {
|
|
2224
|
+
throw new Error("Animated scene renderer requires a canvas.");
|
|
2225
|
+
}
|
|
2226
|
+
return canvas;
|
|
1488
2227
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2228
|
+
function project(position, cameraPosition, width, height) {
|
|
2229
|
+
const x = position[0] - cameraPosition[0];
|
|
2230
|
+
const depth = Math.max(0.2, cameraPosition[2] - (position[2] ?? 0));
|
|
2231
|
+
const scale2 = clamp(34 + 150 / (depth + 1.2), 34, 104);
|
|
2232
|
+
const groundY = height * 0.82 - depth * height * 0.045 - (position[1] ?? 0) * scale2;
|
|
2233
|
+
return {
|
|
2234
|
+
x: width * 0.5 + x * scale2,
|
|
2235
|
+
y: groundY,
|
|
2236
|
+
groundY,
|
|
2237
|
+
scale: scale2,
|
|
2238
|
+
depth,
|
|
2239
|
+
visible: groundY > -height * 0.2 && groundY < height * 1.18
|
|
2240
|
+
};
|
|
1493
2241
|
}
|
|
1494
|
-
function
|
|
1495
|
-
return
|
|
2242
|
+
function propIdentity(prop, index) {
|
|
2243
|
+
return prop.id ?? `${prop.kind}-${index}`;
|
|
1496
2244
|
}
|
|
1497
|
-
function
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2245
|
+
function drawGroundShadow(ctx, scale2) {
|
|
2246
|
+
ctx.fillStyle = "rgba(44, 38, 28, 0.18)";
|
|
2247
|
+
ctx.fillRect(-scale2 * 0.36, -scale2 * 0.035, scale2 * 0.72, scale2 * 0.07);
|
|
2248
|
+
}
|
|
2249
|
+
function drawProp(ctx, prop, cameraPosition, width, height) {
|
|
2250
|
+
const projected = project(prop.position, cameraPosition, width, height);
|
|
2251
|
+
const { x, y, scale: scale2 } = projected;
|
|
2252
|
+
ctx.save();
|
|
2253
|
+
ctx.translate(x, y);
|
|
2254
|
+
ctx.lineWidth = Math.max(1, scale2 * 0.04);
|
|
2255
|
+
if (prop.kind === "crop-row") {
|
|
2256
|
+
ctx.fillStyle = "#8b633f";
|
|
2257
|
+
ctx.fillRect(-scale2 * 1.05, -scale2 * 0.08, scale2 * 2.1, scale2 * 0.16);
|
|
2258
|
+
ctx.fillStyle = "#6f4a30";
|
|
2259
|
+
ctx.fillRect(-scale2 * 0.92, -scale2 * 0.035, scale2 * 1.84, scale2 * 0.07);
|
|
2260
|
+
ctx.strokeStyle = "#4f6f32";
|
|
2261
|
+
ctx.beginPath();
|
|
2262
|
+
ctx.moveTo(-scale2 * 0.8, 0);
|
|
2263
|
+
ctx.lineTo(scale2 * 0.8, 0);
|
|
2264
|
+
ctx.stroke();
|
|
2265
|
+
ctx.fillStyle = "#6f9f43";
|
|
2266
|
+
for (let i = -3; i <= 3; i += 1) {
|
|
2267
|
+
ctx.fillRect(i * scale2 * 0.22, -scale2 * 0.18, scale2 * 0.06, scale2 * 0.2);
|
|
1514
2268
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2269
|
+
} else if (prop.kind === "fence-segment") {
|
|
2270
|
+
ctx.strokeStyle = "#896b4d";
|
|
2271
|
+
ctx.beginPath();
|
|
2272
|
+
ctx.moveTo(-scale2 * 0.5, 0);
|
|
2273
|
+
ctx.lineTo(scale2 * 0.5, 0);
|
|
2274
|
+
ctx.moveTo(-scale2 * 0.35, -scale2 * 0.25);
|
|
2275
|
+
ctx.lineTo(-scale2 * 0.35, scale2 * 0.1);
|
|
2276
|
+
ctx.moveTo(scale2 * 0.35, -scale2 * 0.25);
|
|
2277
|
+
ctx.lineTo(scale2 * 0.35, scale2 * 0.1);
|
|
2278
|
+
ctx.stroke();
|
|
2279
|
+
} else if (prop.kind === "cart") {
|
|
2280
|
+
drawGroundShadow(ctx, scale2);
|
|
2281
|
+
ctx.fillStyle = "#7b5238";
|
|
2282
|
+
ctx.fillRect(-scale2 * 0.52, -scale2 * 0.38, scale2 * 1.04, scale2 * 0.36);
|
|
2283
|
+
ctx.fillStyle = "#a4774c";
|
|
2284
|
+
ctx.fillRect(-scale2 * 0.44, -scale2 * 0.32, scale2 * 0.88, scale2 * 0.08);
|
|
2285
|
+
ctx.fillStyle = "#25201c";
|
|
2286
|
+
ctx.beginPath();
|
|
2287
|
+
ctx.arc(-scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
2288
|
+
ctx.arc(scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
2289
|
+
ctx.fill();
|
|
2290
|
+
} else if (prop.kind === "small-tree") {
|
|
2291
|
+
drawGroundShadow(ctx, scale2);
|
|
2292
|
+
ctx.fillStyle = "#5b3b24";
|
|
2293
|
+
ctx.fillRect(-scale2 * 0.07, -scale2 * 0.56, scale2 * 0.14, scale2 * 0.56);
|
|
2294
|
+
ctx.fillStyle = "#2f6d3b";
|
|
2295
|
+
ctx.beginPath();
|
|
2296
|
+
ctx.arc(-scale2 * 0.16, -scale2 * 0.72, scale2 * 0.25, 0, Math.PI * 2);
|
|
2297
|
+
ctx.fill();
|
|
2298
|
+
ctx.fillStyle = "#3f8746";
|
|
2299
|
+
ctx.beginPath();
|
|
2300
|
+
ctx.arc(scale2 * 0.13, -scale2 * 0.76, scale2 * 0.28, 0, Math.PI * 2);
|
|
2301
|
+
ctx.fill();
|
|
2302
|
+
ctx.fillStyle = "#4f9a50";
|
|
2303
|
+
ctx.beginPath();
|
|
2304
|
+
ctx.arc(0, -scale2 * 0.98, scale2 * 0.26, 0, Math.PI * 2);
|
|
2305
|
+
ctx.fill();
|
|
2306
|
+
} else {
|
|
2307
|
+
drawGroundShadow(ctx, scale2);
|
|
2308
|
+
ctx.fillStyle = prop.kind === "crate" ? "#9a6a3d" : "#d0c39f";
|
|
2309
|
+
ctx.fillRect(-scale2 * 0.2, -scale2 * 0.32, scale2 * 0.4, scale2 * 0.32);
|
|
2310
|
+
if (prop.kind === "crate") {
|
|
2311
|
+
ctx.strokeStyle = "#6e4829";
|
|
2312
|
+
ctx.beginPath();
|
|
2313
|
+
ctx.moveTo(-scale2 * 0.2, -scale2 * 0.16);
|
|
2314
|
+
ctx.lineTo(scale2 * 0.2, -scale2 * 0.16);
|
|
2315
|
+
ctx.moveTo(0, -scale2 * 0.32);
|
|
2316
|
+
ctx.lineTo(0, 0);
|
|
2317
|
+
ctx.stroke();
|
|
1522
2318
|
}
|
|
1523
2319
|
}
|
|
1524
|
-
|
|
2320
|
+
ctx.restore();
|
|
2321
|
+
return projected;
|
|
1525
2322
|
}
|
|
1526
|
-
function
|
|
1527
|
-
|
|
1528
|
-
|
|
2323
|
+
function drawCharacter(ctx, characterPosition, cameraPosition, width, height, gaitPhase, headLook) {
|
|
2324
|
+
const projected = project(characterPosition, cameraPosition, width, height);
|
|
2325
|
+
const { x, y, scale: scale2 } = projected;
|
|
2326
|
+
const stride = Math.sin(gaitPhase * Math.PI * 2);
|
|
2327
|
+
const headYaw = (headLook?.yaw ?? 0) * (headLook?.weight ?? 0);
|
|
2328
|
+
ctx.save();
|
|
2329
|
+
ctx.translate(x, y);
|
|
2330
|
+
ctx.lineWidth = Math.max(2, scale2 * 0.05);
|
|
2331
|
+
ctx.lineCap = "round";
|
|
2332
|
+
drawGroundShadow(ctx, scale2);
|
|
2333
|
+
ctx.strokeStyle = "#4c2f25";
|
|
2334
|
+
ctx.beginPath();
|
|
2335
|
+
ctx.moveTo(-scale2 * 0.12, -scale2 * 0.34);
|
|
2336
|
+
ctx.lineTo(-scale2 * (0.18 + stride * 0.04), -scale2 * 0.02);
|
|
2337
|
+
ctx.moveTo(scale2 * 0.12, -scale2 * 0.34);
|
|
2338
|
+
ctx.lineTo(scale2 * (0.18 - stride * 0.04), -scale2 * 0.02);
|
|
2339
|
+
ctx.stroke();
|
|
2340
|
+
ctx.fillStyle = "#6f8f62";
|
|
2341
|
+
ctx.beginPath();
|
|
2342
|
+
ctx.moveTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
2343
|
+
ctx.lineTo(scale2 * 0.3, -scale2 * 0.86);
|
|
2344
|
+
ctx.lineTo(scale2 * 0.42, -scale2 * 0.24);
|
|
2345
|
+
ctx.lineTo(-scale2 * 0.42, -scale2 * 0.24);
|
|
2346
|
+
ctx.lineTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
2347
|
+
ctx.fill();
|
|
2348
|
+
ctx.fillStyle = "#d8b18c";
|
|
2349
|
+
ctx.beginPath();
|
|
2350
|
+
ctx.arc(0, -scale2 * 1.16, scale2 * 0.18, 0, Math.PI * 2);
|
|
2351
|
+
ctx.fill();
|
|
2352
|
+
ctx.fillStyle = "#5a3a2b";
|
|
2353
|
+
ctx.fillRect(-scale2 * 0.2, -scale2 * 1.28, scale2 * 0.4, scale2 * 0.16);
|
|
2354
|
+
ctx.fillRect(-scale2 * 0.23, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
2355
|
+
ctx.fillRect(scale2 * 0.13, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
2356
|
+
if (headLook?.status === "active" || headLook?.status === "returning") {
|
|
2357
|
+
ctx.strokeStyle = "rgba(255, 246, 210, 0.74)";
|
|
2358
|
+
ctx.lineWidth = Math.max(1, scale2 * 0.025);
|
|
2359
|
+
ctx.beginPath();
|
|
2360
|
+
ctx.moveTo(0, -scale2 * 1.16);
|
|
2361
|
+
ctx.lineTo(Math.sin(headYaw) * scale2 * 0.32, -scale2 * 1.16 - scale2 * 0.08);
|
|
2362
|
+
ctx.stroke();
|
|
1529
2363
|
}
|
|
1530
|
-
|
|
2364
|
+
ctx.strokeStyle = "#6b4935";
|
|
2365
|
+
ctx.beginPath();
|
|
2366
|
+
ctx.moveTo(-scale2 * 0.24, -scale2 * 0.76);
|
|
2367
|
+
ctx.lineTo(-scale2 * (0.46 + stride * 0.04), -scale2 * (0.5 + stride * 0.06));
|
|
2368
|
+
ctx.moveTo(scale2 * 0.24, -scale2 * 0.76);
|
|
2369
|
+
ctx.lineTo(scale2 * (0.46 - stride * 0.04), -scale2 * (0.5 - stride * 0.06));
|
|
2370
|
+
ctx.stroke();
|
|
2371
|
+
ctx.fillStyle = "#d6c09a";
|
|
2372
|
+
ctx.fillRect(-scale2 * 0.16, -scale2 * 0.86, scale2 * 0.32, scale2 * 0.18);
|
|
2373
|
+
ctx.restore();
|
|
2374
|
+
return projected;
|
|
1531
2375
|
}
|
|
1532
|
-
function
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2376
|
+
function pointForModelVertex(vertex, projection, sample) {
|
|
2377
|
+
const bounds = sample.bounds;
|
|
2378
|
+
const height = Math.max(0.1, bounds.size[1] || sample.bindBounds.size[1] || 1);
|
|
2379
|
+
const modelScale = projection.scale * 1.3 / height;
|
|
2380
|
+
const centerX = (bounds.min[0] + bounds.max[0]) * 0.5;
|
|
2381
|
+
const centerZ = (bounds.min[2] + bounds.max[2]) * 0.5;
|
|
2382
|
+
return [
|
|
2383
|
+
projection.x + (vertex[0] - centerX) * modelScale,
|
|
2384
|
+
projection.groundY - (vertex[1] - bounds.min[1]) * modelScale + (vertex[2] - centerZ) * modelScale * 0.16
|
|
2385
|
+
];
|
|
1537
2386
|
}
|
|
1538
|
-
function
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
} = options;
|
|
1544
|
-
if (typeof getTargetFrameTimeMs === "function") {
|
|
1545
|
-
const resolved = getTargetFrameTimeMs(event);
|
|
1546
|
-
return readPositiveNumber("getTargetFrameTimeMs()", resolved);
|
|
2387
|
+
function modelFillForHeight(vertex, sample) {
|
|
2388
|
+
const height = Math.max(0.1, sample.bounds.size[1] || 1);
|
|
2389
|
+
const t = (vertex[1] - sample.bounds.min[1]) / height;
|
|
2390
|
+
if (t > 0.82) {
|
|
2391
|
+
return "#6d4a36";
|
|
1547
2392
|
}
|
|
1548
|
-
if (
|
|
1549
|
-
return
|
|
2393
|
+
if (t > 0.58) {
|
|
2394
|
+
return "#d7b28f";
|
|
1550
2395
|
}
|
|
1551
|
-
if (
|
|
1552
|
-
return
|
|
2396
|
+
if (t > 0.25) {
|
|
2397
|
+
return "#6f8f62";
|
|
1553
2398
|
}
|
|
1554
|
-
return
|
|
2399
|
+
return "#5e4636";
|
|
1555
2400
|
}
|
|
1556
|
-
function
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
if (fixedTargetFrameTimeMs !== void 0 && fixedTargetFrameRate !== void 0) {
|
|
1579
|
-
throw new Error(
|
|
1580
|
-
"Provide either targetFrameTimeMs or targetFrameRate, not both."
|
|
1581
|
-
);
|
|
2401
|
+
function drawSkinnedGltfCharacter(ctx, model, snapshot, width, height) {
|
|
2402
|
+
const projection = project(snapshot.characterPosition, snapshot.cameraPosition, width, height);
|
|
2403
|
+
const sample = model.sample(snapshot.activeClipId, snapshot.clipTimeMs);
|
|
2404
|
+
const maxTriangles = 12e3;
|
|
2405
|
+
const triangleCount = Math.floor(sample.indices.length / 3);
|
|
2406
|
+
const stride = Math.max(1, Math.ceil(triangleCount / maxTriangles));
|
|
2407
|
+
const triangles = [];
|
|
2408
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += stride) {
|
|
2409
|
+
const base = triangleIndex * 3;
|
|
2410
|
+
const a = sample.vertices[sample.indices[base]];
|
|
2411
|
+
const b = sample.vertices[sample.indices[base + 1]];
|
|
2412
|
+
const c = sample.vertices[sample.indices[base + 2]];
|
|
2413
|
+
if (!a || !b || !c) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
triangles.push({
|
|
2417
|
+
a,
|
|
2418
|
+
b,
|
|
2419
|
+
c,
|
|
2420
|
+
depth: (a[2] + b[2] + c[2]) / 3,
|
|
2421
|
+
fill: modelFillForHeight(a, sample)
|
|
2422
|
+
});
|
|
1582
2423
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
2424
|
+
triangles.sort((left, right) => left.depth - right.depth);
|
|
2425
|
+
ctx.save();
|
|
2426
|
+
drawGroundShadow(ctx, projection.scale);
|
|
2427
|
+
for (const triangle of triangles) {
|
|
2428
|
+
const [ax, ay] = pointForModelVertex(triangle.a, projection, sample);
|
|
2429
|
+
const [bx, by] = pointForModelVertex(triangle.b, projection, sample);
|
|
2430
|
+
const [cx, cy] = pointForModelVertex(triangle.c, projection, sample);
|
|
2431
|
+
ctx.fillStyle = triangle.fill;
|
|
2432
|
+
ctx.beginPath();
|
|
2433
|
+
ctx.moveTo(ax, ay);
|
|
2434
|
+
ctx.lineTo(bx, by);
|
|
2435
|
+
ctx.lineTo(cx, cy);
|
|
2436
|
+
ctx.closePath?.();
|
|
2437
|
+
ctx.fill();
|
|
1585
2438
|
}
|
|
1586
|
-
|
|
1587
|
-
targetFrameTimeMs: fixedTargetFrameTimeMs,
|
|
1588
|
-
targetFrameRate: fixedTargetFrameRate,
|
|
1589
|
-
getTargetFrameTimeMs
|
|
1590
|
-
};
|
|
2439
|
+
ctx.restore();
|
|
1591
2440
|
return {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
onFrameStart({
|
|
1595
|
-
...event,
|
|
1596
|
-
owner: rendererDebugOwner
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
},
|
|
1600
|
-
onFrameComplete(event) {
|
|
1601
|
-
const resolvedTargetFrameTimeMs = resolveTargetFrameTimeMs(
|
|
1602
|
-
resolvedOptions,
|
|
1603
|
-
event
|
|
1604
|
-
);
|
|
1605
|
-
if (typeof event.frameTimeMs === "number" && Number.isFinite(event.frameTimeMs) && event.frameTimeMs > 0) {
|
|
1606
|
-
debugSession.recordFrame({
|
|
1607
|
-
frameId: event.frameId,
|
|
1608
|
-
frameTimeMs: event.frameTimeMs,
|
|
1609
|
-
targetFrameTimeMs: resolvedTargetFrameTimeMs
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
if (typeof onFrameComplete === "function") {
|
|
1613
|
-
onFrameComplete({
|
|
1614
|
-
...event,
|
|
1615
|
-
owner: rendererDebugOwner,
|
|
1616
|
-
targetFrameTimeMs: resolvedTargetFrameTimeMs
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
2441
|
+
projection,
|
|
2442
|
+
sample
|
|
1620
2443
|
};
|
|
1621
2444
|
}
|
|
1622
|
-
function
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
if (!gpu || typeof gpu.requestAdapter !== "function") {
|
|
1652
|
-
throw new Error("WebGPU runtime unavailable. navigator.gpu is missing.");
|
|
2445
|
+
function renderScene(canvas, ctx, snapshot, props, characterModel) {
|
|
2446
|
+
const width = canvas.width || 1;
|
|
2447
|
+
const height = canvas.height || 1;
|
|
2448
|
+
ctx.clearRect(0, 0, width, height);
|
|
2449
|
+
ctx.fillStyle = "#d8e8d0";
|
|
2450
|
+
ctx.fillRect(0, 0, width, height);
|
|
2451
|
+
ctx.fillStyle = "#9fc47a";
|
|
2452
|
+
ctx.fillRect(0, height * 0.52, width, height * 0.48);
|
|
2453
|
+
ctx.strokeStyle = "#caa66d";
|
|
2454
|
+
ctx.lineWidth = Math.max(2, width * 6e-3);
|
|
2455
|
+
ctx.beginPath();
|
|
2456
|
+
ctx.moveTo(width * 0.35, height);
|
|
2457
|
+
ctx.quadraticCurveTo(width * 0.53, height * 0.58, width * 0.62, 0);
|
|
2458
|
+
ctx.stroke();
|
|
2459
|
+
const projectedProps = props.map((prop, index) => ({
|
|
2460
|
+
prop,
|
|
2461
|
+
id: propIdentity(prop, index),
|
|
2462
|
+
projected: project(prop.position, snapshot.cameraPosition, width, height)
|
|
2463
|
+
})).sort((a, b) => b.projected.depth - a.projected.depth);
|
|
2464
|
+
const propGroundAnchors = [];
|
|
2465
|
+
for (const { prop, id } of projectedProps) {
|
|
2466
|
+
const projected = drawProp(ctx, prop, snapshot.cameraPosition, width, height);
|
|
2467
|
+
propGroundAnchors.push({
|
|
2468
|
+
id,
|
|
2469
|
+
kind: prop.kind,
|
|
2470
|
+
groundY: projected.groundY,
|
|
2471
|
+
depth: projected.depth,
|
|
2472
|
+
visible: projected.visible
|
|
2473
|
+
});
|
|
1653
2474
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
2475
|
+
let characterProjection;
|
|
2476
|
+
let modelRenderable = false;
|
|
2477
|
+
let fallbackProxyActive = true;
|
|
2478
|
+
let skinnedVertexCount = 0;
|
|
2479
|
+
let skinnedTriangleCount = 0;
|
|
2480
|
+
let skinnedJointCount = 0;
|
|
2481
|
+
let skinnedAnimatedNodeCount = 0;
|
|
2482
|
+
let skinnedClipCount = 0;
|
|
2483
|
+
let activeClipRenderable = false;
|
|
2484
|
+
if (characterModel) {
|
|
2485
|
+
const drawn = drawSkinnedGltfCharacter(ctx, characterModel, snapshot, width, height);
|
|
2486
|
+
characterProjection = drawn.projection;
|
|
2487
|
+
modelRenderable = true;
|
|
2488
|
+
fallbackProxyActive = false;
|
|
2489
|
+
skinnedVertexCount = characterModel.vertexCount;
|
|
2490
|
+
skinnedTriangleCount = characterModel.triangleCount;
|
|
2491
|
+
skinnedJointCount = characterModel.jointCount;
|
|
2492
|
+
skinnedAnimatedNodeCount = characterModel.animatedNodeCount;
|
|
2493
|
+
skinnedClipCount = characterModel.clipCount;
|
|
2494
|
+
activeClipRenderable = drawn.sample.activeClipRenderable;
|
|
2495
|
+
} else {
|
|
2496
|
+
characterProjection = drawCharacter(
|
|
2497
|
+
ctx,
|
|
2498
|
+
snapshot.characterPosition,
|
|
2499
|
+
snapshot.cameraPosition,
|
|
2500
|
+
width,
|
|
2501
|
+
height,
|
|
2502
|
+
snapshot.clipTimeMs / 900,
|
|
2503
|
+
snapshot.headLook
|
|
2504
|
+
);
|
|
1659
2505
|
}
|
|
1660
|
-
context.configure({
|
|
1661
|
-
device,
|
|
1662
|
-
format,
|
|
1663
|
-
alphaMode
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
function createRenderPassDescriptor(view, clearColor) {
|
|
1667
2506
|
return {
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
]
|
|
2507
|
+
characterGroundY: characterProjection.groundY,
|
|
2508
|
+
characterVisible: characterProjection.visible,
|
|
2509
|
+
modelLoaded: Boolean(characterModel),
|
|
2510
|
+
modelRenderable,
|
|
2511
|
+
fallbackProxyActive,
|
|
2512
|
+
skinnedVertexCount,
|
|
2513
|
+
skinnedTriangleCount,
|
|
2514
|
+
skinnedJointCount,
|
|
2515
|
+
skinnedAnimatedNodeCount,
|
|
2516
|
+
skinnedClipCount,
|
|
2517
|
+
activeClipRenderable,
|
|
2518
|
+
propGroundAnchors
|
|
1681
2519
|
};
|
|
1682
2520
|
}
|
|
1683
|
-
function
|
|
2521
|
+
function resolveCharacterModel(options) {
|
|
1684
2522
|
try {
|
|
1685
|
-
|
|
1686
|
-
|
|
2523
|
+
return createAnimatedGltfModel(
|
|
2524
|
+
options.modelAsset ?? options.animationAdventure?.modelAsset,
|
|
2525
|
+
options.clipAssets ?? options.animationAdventure?.clipAssets ?? []
|
|
2526
|
+
);
|
|
1687
2527
|
} catch {
|
|
1688
|
-
return
|
|
2528
|
+
return null;
|
|
1689
2529
|
}
|
|
1690
2530
|
}
|
|
1691
|
-
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
powerPreference = "high-performance",
|
|
1697
|
-
alpha = true,
|
|
1698
|
-
format,
|
|
1699
|
-
clearColor = DEFAULT_CLEAR_COLOR,
|
|
1700
|
-
requestAnimationFrame = globalThis.requestAnimationFrame?.bind(globalThis),
|
|
1701
|
-
cancelAnimationFrame = globalThis.cancelAnimationFrame?.bind(globalThis),
|
|
1702
|
-
frameIdFactory,
|
|
1703
|
-
onFrameStart,
|
|
1704
|
-
onBeforeEncode,
|
|
1705
|
-
onAfterSubmit,
|
|
1706
|
-
onFrameComplete
|
|
1707
|
-
} = options;
|
|
1708
|
-
const gpu = readGpu(navigatorOverride);
|
|
1709
|
-
const adapter = await gpu.requestAdapter({ powerPreference });
|
|
1710
|
-
if (!adapter) {
|
|
1711
|
-
throw new Error("Unable to obtain GPU adapter.");
|
|
1712
|
-
}
|
|
1713
|
-
const device = await adapter.requestDevice();
|
|
1714
|
-
const targetCanvas = resolveCanvas2(canvas, documentOverride);
|
|
1715
|
-
const context = targetCanvas.getContext?.("webgpu");
|
|
1716
|
-
if (!context) {
|
|
1717
|
-
throw new Error("Unable to obtain WebGPU canvas context.");
|
|
2531
|
+
function createAnimatedSceneRenderer(options = {}) {
|
|
2532
|
+
const canvas = resolveCanvas2(options.canvas);
|
|
2533
|
+
const ctx = canvas.getContext?.("2d");
|
|
2534
|
+
if (!ctx) {
|
|
2535
|
+
throw new Error("Animated scene renderer requires a 2D canvas context.");
|
|
1718
2536
|
}
|
|
1719
|
-
const
|
|
1720
|
-
|
|
2537
|
+
const route = [...options.route ?? options.animationAdventure?.route ?? []];
|
|
2538
|
+
const beats = [...options.beats ?? options.animationAdventure?.beats ?? []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
2539
|
+
const props = [...options.props ?? options.animationAdventure?.props ?? []];
|
|
2540
|
+
const clipAssets = options.clipAssets ?? options.animationAdventure?.clipAssets ?? [];
|
|
2541
|
+
const clipProfiles = resolveClipProfiles(clipAssets);
|
|
2542
|
+
const beatTimeline = buildBeatTimeline(beats, route, clipProfiles);
|
|
2543
|
+
const characterModel = resolveCharacterModel(options);
|
|
2544
|
+
let camera = {
|
|
2545
|
+
...DEFAULT_CAMERA,
|
|
2546
|
+
...definedCameraOverrides(options.camera ?? options.animationAdventure?.camera)
|
|
2547
|
+
};
|
|
2548
|
+
const loopDurationMs = Math.max(beatTimeline.at(-1)?.endMs ?? durationFromBeats(beats), 1);
|
|
2549
|
+
const movementWarnings = beatTimeline.flatMap((entry) => entry.warnings);
|
|
2550
|
+
const requestFrame = options.requestAnimationFrame ?? globalThis.requestAnimationFrame?.bind(globalThis);
|
|
2551
|
+
const cancelFrame = options.cancelAnimationFrame ?? globalThis.cancelAnimationFrame?.bind(globalThis);
|
|
2552
|
+
let frameHandle;
|
|
1721
2553
|
let running = false;
|
|
1722
|
-
let destroyed = false;
|
|
1723
2554
|
let frame = 0;
|
|
1724
|
-
let
|
|
1725
|
-
let
|
|
1726
|
-
let
|
|
1727
|
-
let
|
|
1728
|
-
let
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
frameId,
|
|
1791
|
-
frameTimeMs,
|
|
1792
|
-
timestamp,
|
|
1793
|
-
device,
|
|
1794
|
-
context,
|
|
1795
|
-
canvas: targetCanvas,
|
|
1796
|
-
xrActive
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
if (typeof onFrameComplete === "function") {
|
|
1800
|
-
onFrameComplete({
|
|
1801
|
-
frame: frameNumber,
|
|
1802
|
-
frameId,
|
|
1803
|
-
frameTimeMs,
|
|
1804
|
-
timestamp,
|
|
1805
|
-
device,
|
|
1806
|
-
context,
|
|
1807
|
-
canvas: targetCanvas,
|
|
1808
|
-
xrActive
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
return {
|
|
1812
|
-
frame: frameNumber,
|
|
1813
|
-
frameId,
|
|
1814
|
-
frameTimeMs,
|
|
1815
|
-
timestamp
|
|
1816
|
-
};
|
|
1817
|
-
};
|
|
1818
|
-
const tick = (timestamp) => {
|
|
1819
|
-
if (!running || destroyed) {
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
renderOnce(timestamp);
|
|
1823
|
-
if (typeof requestAnimationFrame === "function") {
|
|
1824
|
-
rafId = requestAnimationFrame(tick);
|
|
1825
|
-
}
|
|
2555
|
+
let startedAtMs;
|
|
2556
|
+
let lastTimestamp;
|
|
2557
|
+
let pendingCameraControl = null;
|
|
2558
|
+
let activeCameraControl = false;
|
|
2559
|
+
let headLookWeight = 0;
|
|
2560
|
+
let cameraPosition = route[0] ? [
|
|
2561
|
+
route[0].position[0] + (camera.offset?.[0] ?? 0),
|
|
2562
|
+
route[0].position[1] + (camera.offset?.[1] ?? 0),
|
|
2563
|
+
route[0].position[2] + (camera.offset?.[2] ?? 0)
|
|
2564
|
+
] : [0, 2.4, 5.5];
|
|
2565
|
+
let cameraRigFrame = (0, import_gpu_camera.resolveCameraRigFrame)({
|
|
2566
|
+
viewMode: "editor",
|
|
2567
|
+
anchors: {
|
|
2568
|
+
target: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
2569
|
+
head: route[0]?.position ? [route[0].position[0], route[0].position[1] + 1.65, route[0].position[2]] : [0, 1.65, 0],
|
|
2570
|
+
forward: [0, 0, -1]
|
|
2571
|
+
},
|
|
2572
|
+
camera: {
|
|
2573
|
+
id: "animation-adventure",
|
|
2574
|
+
transform: {
|
|
2575
|
+
position: [...cameraPosition],
|
|
2576
|
+
target: route[0]?.position ? [...route[0].position] : [0, 0, 0]
|
|
2577
|
+
}
|
|
2578
|
+
},
|
|
2579
|
+
constraints: camera.constraints
|
|
2580
|
+
});
|
|
2581
|
+
let snapshot = {
|
|
2582
|
+
frame,
|
|
2583
|
+
running,
|
|
2584
|
+
activeClipId: beats[0]?.clipId ?? "",
|
|
2585
|
+
activeBeatId: beats[0]?.id ?? "",
|
|
2586
|
+
activeMovementMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
2587
|
+
blendProgress: 0,
|
|
2588
|
+
clipTimeMs: 0,
|
|
2589
|
+
characterPosition: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
2590
|
+
cameraPosition: [...cameraPosition],
|
|
2591
|
+
cameraViewMode: resolveCameraViewMode(camera),
|
|
2592
|
+
cameraTransform: cameraRigFrame.transform,
|
|
2593
|
+
targetDistance: cameraRigFrame.targetDistance,
|
|
2594
|
+
headLook: cameraRigFrame.headLook,
|
|
2595
|
+
characterGroundY: 0,
|
|
2596
|
+
characterVisible: false,
|
|
2597
|
+
modelLoaded: Boolean(characterModel),
|
|
2598
|
+
modelRenderable: false,
|
|
2599
|
+
fallbackProxyActive: true,
|
|
2600
|
+
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
2601
|
+
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
2602
|
+
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
2603
|
+
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
2604
|
+
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
2605
|
+
activeClipRenderable: false,
|
|
2606
|
+
propGroundAnchors: [],
|
|
2607
|
+
movementValidation: {
|
|
2608
|
+
status: movementWarnings.length ? "warning" : "passed",
|
|
2609
|
+
warnings: movementWarnings,
|
|
2610
|
+
activeBeatId: beats[0]?.id ?? "",
|
|
2611
|
+
activeClipId: beats[0]?.clipId ?? "",
|
|
2612
|
+
motionMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
2613
|
+
rootMotionSource: beatTimeline[0]?.movementProfile?.motionMode ?? "none",
|
|
2614
|
+
expectedSpeed: beatTimeline[0]?.expectedSpeed ?? 0,
|
|
2615
|
+
actualSpeed: beatTimeline[0]?.actualSpeed ?? 0,
|
|
2616
|
+
movementDistance: beatTimeline[0]?.movementDistance ?? 0,
|
|
2617
|
+
loopCount: beatTimeline[0]?.loopCount ?? 1,
|
|
2618
|
+
footSlideWarning: beatTimeline[0]?.footSlideWarning ?? null
|
|
2619
|
+
},
|
|
2620
|
+
frameState: "initialized"
|
|
1826
2621
|
};
|
|
1827
|
-
|
|
1828
|
-
if (
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1831
|
-
if (running) {
|
|
1832
|
-
return false;
|
|
2622
|
+
function renderOnce(timestamp = nowMs()) {
|
|
2623
|
+
if (startedAtMs === void 0) {
|
|
2624
|
+
startedAtMs = timestamp;
|
|
1833
2625
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2626
|
+
const elapsedMs = Math.max(0, timestamp - startedAtMs);
|
|
2627
|
+
const loopTimeMs = elapsedMs % loopDurationMs;
|
|
2628
|
+
const frameTimeMs = lastTimestamp === void 0 ? 16.67 : Math.max(0, timestamp - lastTimestamp);
|
|
2629
|
+
lastTimestamp = timestamp;
|
|
2630
|
+
frame += 1;
|
|
2631
|
+
const timelineEntry = resolveTimelineBeat(beatTimeline, loopTimeMs);
|
|
2632
|
+
const fallbackBeat = resolveBeat(beats, loopTimeMs);
|
|
2633
|
+
const beat = timelineEntry?.beat ?? fallbackBeat.beat;
|
|
2634
|
+
const beatTimeMs = timelineEntry ? loopTimeMs - timelineEntry.startMs : fallbackBeat.beatTimeMs;
|
|
2635
|
+
const durationMs = timelineEntry?.durationMs ?? fallbackBeat.durationMs;
|
|
2636
|
+
const blend = beat?.blend ?? { inMs: 0, outMs: 0 };
|
|
2637
|
+
const blendIn = blend.inMs > 0 ? clamp012(beatTimeMs / blend.inMs) : 1;
|
|
2638
|
+
const blendOut = blend.outMs > 0 ? clamp012((durationMs - beatTimeMs) / blend.outMs) : 1;
|
|
2639
|
+
const blendProgress = clamp012(Math.min(blendIn, blendOut));
|
|
2640
|
+
const characterPosition = timelineEntry ? resolveTimelinePosition(timelineEntry, beatTimeMs) : resolveRoutePosition(route, loopTimeMs);
|
|
2641
|
+
const characterForward = timelineEntry ? resolveTimelineForward(timelineEntry) : resolveRouteForward(route, loopTimeMs);
|
|
2642
|
+
const headAnchor = camera.headBoneAvailable === false ? void 0 : [
|
|
2643
|
+
characterPosition[0],
|
|
2644
|
+
characterPosition[1] + (camera.headHeight ?? 1.65),
|
|
2645
|
+
characterPosition[2]
|
|
2646
|
+
];
|
|
2647
|
+
const lookAheadTimeMs = (loopTimeMs + camera.lookAheadMs) % loopDurationMs;
|
|
2648
|
+
const lookAheadEntry = resolveTimelineBeat(beatTimeline, lookAheadTimeMs);
|
|
2649
|
+
const lookAheadPosition = lookAheadEntry ? resolveTimelinePosition(lookAheadEntry, lookAheadTimeMs - lookAheadEntry.startMs) : resolveRoutePosition(route, lookAheadTimeMs);
|
|
2650
|
+
const desiredCamera = [
|
|
2651
|
+
lookAheadPosition[0] + (camera.offset?.[0] ?? 0),
|
|
2652
|
+
lookAheadPosition[1] + (camera.offset?.[1] ?? 0),
|
|
2653
|
+
lookAheadPosition[2] + (camera.offset?.[2] ?? 0)
|
|
2654
|
+
];
|
|
2655
|
+
const smoothing = bezierY(camera.cubicBezier, clamp012(frameTimeMs / Math.max(1, camera.lagMs)));
|
|
2656
|
+
cameraPosition = lerp3(cameraPosition, desiredCamera, smoothing);
|
|
2657
|
+
const cameraViewMode = resolveCameraViewMode(camera);
|
|
2658
|
+
const rigViewMode = cameraViewMode === "spectator" ? "editor" : cameraViewMode;
|
|
2659
|
+
cameraRigFrame = (0, import_gpu_camera.resolveCameraRigFrame)({
|
|
2660
|
+
viewMode: rigViewMode,
|
|
2661
|
+
anchors: {
|
|
2662
|
+
target: characterPosition,
|
|
2663
|
+
...headAnchor ? { head: headAnchor } : {},
|
|
2664
|
+
forward: characterForward
|
|
2665
|
+
},
|
|
2666
|
+
camera: {
|
|
2667
|
+
id: "animation-adventure",
|
|
2668
|
+
transform: {
|
|
2669
|
+
position: [...cameraPosition],
|
|
2670
|
+
target: lookAheadPosition
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
constraints: camera.constraints,
|
|
2674
|
+
control: pendingCameraControl,
|
|
2675
|
+
activeControl: activeCameraControl
|
|
2676
|
+
});
|
|
2677
|
+
if (activeCameraControl && cameraRigFrame.headLook.status === "active") {
|
|
2678
|
+
headLookWeight = cameraRigFrame.headLook.weight;
|
|
1837
2679
|
} else {
|
|
1838
|
-
|
|
2680
|
+
const returnMs = Math.max(1, camera.headLook?.returnMs ?? DEFAULT_CAMERA.headLook.returnMs);
|
|
2681
|
+
headLookWeight = lerp2(headLookWeight, 0, clamp012(frameTimeMs / returnMs));
|
|
1839
2682
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2683
|
+
const headLook = {
|
|
2684
|
+
...cameraRigFrame.headLook,
|
|
2685
|
+
status: cameraRigFrame.headLook.status === "unavailable" ? "unavailable" : headLookWeight > 1e-3 && !activeCameraControl ? "returning" : cameraRigFrame.headLook.status,
|
|
2686
|
+
weight: headLookWeight
|
|
2687
|
+
};
|
|
2688
|
+
cameraPosition = [...cameraRigFrame.transform.position];
|
|
2689
|
+
pendingCameraControl = null;
|
|
2690
|
+
activeCameraControl = false;
|
|
2691
|
+
snapshot = {
|
|
2692
|
+
frame,
|
|
2693
|
+
running,
|
|
2694
|
+
activeClipId: beat?.clipId ?? "",
|
|
2695
|
+
activeBeatId: beat?.id ?? "",
|
|
2696
|
+
activeMovementMode: timelineEntry?.movementType ?? "stationary",
|
|
2697
|
+
blendProgress,
|
|
2698
|
+
clipTimeMs: beatTimeMs,
|
|
2699
|
+
characterPosition,
|
|
2700
|
+
cameraPosition: [...cameraPosition],
|
|
2701
|
+
cameraViewMode,
|
|
2702
|
+
cameraTransform: cameraRigFrame.transform,
|
|
2703
|
+
targetDistance: cameraRigFrame.targetDistance,
|
|
2704
|
+
headLook,
|
|
2705
|
+
characterGroundY: 0,
|
|
2706
|
+
characterVisible: false,
|
|
2707
|
+
modelLoaded: Boolean(characterModel),
|
|
2708
|
+
modelRenderable: false,
|
|
2709
|
+
fallbackProxyActive: true,
|
|
2710
|
+
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
2711
|
+
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
2712
|
+
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
2713
|
+
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
2714
|
+
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
2715
|
+
activeClipRenderable: false,
|
|
2716
|
+
propGroundAnchors: [],
|
|
2717
|
+
movementValidation: {
|
|
2718
|
+
status: movementWarnings.length ? "warning" : "passed",
|
|
2719
|
+
warnings: [...movementWarnings],
|
|
2720
|
+
activeBeatId: beat?.id ?? "",
|
|
2721
|
+
activeClipId: beat?.clipId ?? "",
|
|
2722
|
+
motionMode: timelineEntry?.movementType ?? "stationary",
|
|
2723
|
+
rootMotionSource: timelineEntry?.movementProfile?.motionMode ?? "none",
|
|
2724
|
+
expectedSpeed: timelineEntry?.expectedSpeed ?? 0,
|
|
2725
|
+
actualSpeed: timelineEntry?.actualSpeed ?? 0,
|
|
2726
|
+
movementDistance: timelineEntry?.movementDistance ?? 0,
|
|
2727
|
+
loopCount: timelineEntry?.loopCount ?? 1,
|
|
2728
|
+
footSlideWarning: timelineEntry?.footSlideWarning ?? null
|
|
2729
|
+
},
|
|
2730
|
+
frameState: running ? "running" : "rendered-once"
|
|
2731
|
+
};
|
|
2732
|
+
const visibility = renderScene(canvas, ctx, snapshot, props, characterModel);
|
|
2733
|
+
snapshot = {
|
|
2734
|
+
...snapshot,
|
|
2735
|
+
...visibility
|
|
2736
|
+
};
|
|
2737
|
+
return snapshot;
|
|
2738
|
+
}
|
|
2739
|
+
function tick(timestamp) {
|
|
1843
2740
|
if (!running) {
|
|
1844
|
-
return
|
|
1845
|
-
}
|
|
1846
|
-
running = false;
|
|
1847
|
-
if (rafId !== null && typeof cancelAnimationFrame === "function") {
|
|
1848
|
-
cancelAnimationFrame(rafId);
|
|
2741
|
+
return;
|
|
1849
2742
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
const resize = (cssWidth, cssHeight, devicePixelRatio = globalThis.devicePixelRatio ?? 1) => {
|
|
1854
|
-
const width = Math.max(1, Math.floor(cssWidth * devicePixelRatio));
|
|
1855
|
-
const height = Math.max(1, Math.floor(cssHeight * devicePixelRatio));
|
|
1856
|
-
targetCanvas.width = width;
|
|
1857
|
-
targetCanvas.height = height;
|
|
1858
|
-
if (targetCanvas.style) {
|
|
1859
|
-
targetCanvas.style.width = `${Math.max(1, Math.floor(cssWidth))}px`;
|
|
1860
|
-
targetCanvas.style.height = `${Math.max(1, Math.floor(cssHeight))}px`;
|
|
2743
|
+
renderOnce(timestamp);
|
|
2744
|
+
if (running && requestFrame) {
|
|
2745
|
+
frameHandle = requestFrame(tick);
|
|
1861
2746
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
return {
|
|
1875
|
-
running,
|
|
1876
|
-
frame,
|
|
1877
|
-
lastTimestamp,
|
|
1878
|
-
format: resolvedFormat,
|
|
1879
|
-
width,
|
|
1880
|
-
height,
|
|
1881
|
-
xrActive
|
|
1882
|
-
};
|
|
1883
|
-
};
|
|
1884
|
-
const renderer = {
|
|
1885
|
-
canvas: targetCanvas,
|
|
1886
|
-
context,
|
|
1887
|
-
device,
|
|
1888
|
-
format: resolvedFormat,
|
|
1889
|
-
renderOnce,
|
|
1890
|
-
start,
|
|
1891
|
-
stop,
|
|
1892
|
-
resize,
|
|
1893
|
-
setClearColor,
|
|
1894
|
-
setXrActive,
|
|
1895
|
-
getSnapshot,
|
|
1896
|
-
bindXrManager(xrManager, bindOptions = {}) {
|
|
1897
|
-
if (detachXrBinding) {
|
|
1898
|
-
detachXrBinding();
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
start() {
|
|
2750
|
+
if (running) {
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
running = true;
|
|
2754
|
+
snapshot = { ...snapshot, running: true, frameState: "running" };
|
|
2755
|
+
if (requestFrame) {
|
|
2756
|
+
frameHandle = requestFrame(tick);
|
|
2757
|
+
} else {
|
|
2758
|
+
renderOnce();
|
|
1899
2759
|
}
|
|
1900
|
-
detachXrBinding = bindRendererToXrManager(renderer, xrManager, bindOptions);
|
|
1901
|
-
return detachXrBinding;
|
|
1902
2760
|
},
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2761
|
+
resize(width, height, devicePixelRatio = 1) {
|
|
2762
|
+
const ratio = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
2763
|
+
canvas.width = Math.max(1, Math.floor(width * ratio));
|
|
2764
|
+
canvas.height = Math.max(1, Math.floor(height * ratio));
|
|
2765
|
+
if (canvas.style) {
|
|
2766
|
+
canvas.style.width = `${width}px`;
|
|
2767
|
+
canvas.style.height = `${height}px`;
|
|
1909
2768
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
2769
|
+
const visibility = renderScene(canvas, ctx, snapshot, props, characterModel);
|
|
2770
|
+
snapshot = {
|
|
2771
|
+
...snapshot,
|
|
2772
|
+
...visibility
|
|
2773
|
+
};
|
|
2774
|
+
},
|
|
2775
|
+
renderOnce,
|
|
2776
|
+
getSnapshot() {
|
|
2777
|
+
return {
|
|
2778
|
+
...snapshot,
|
|
2779
|
+
characterPosition: [...snapshot.characterPosition],
|
|
2780
|
+
cameraPosition: [...snapshot.cameraPosition],
|
|
2781
|
+
cameraTransform: {
|
|
2782
|
+
position: [...snapshot.cameraTransform.position],
|
|
2783
|
+
target: [...snapshot.cameraTransform.target],
|
|
2784
|
+
up: [...snapshot.cameraTransform.up]
|
|
2785
|
+
},
|
|
2786
|
+
headLook: {
|
|
2787
|
+
...snapshot.headLook,
|
|
2788
|
+
target: [...snapshot.headLook.target]
|
|
2789
|
+
},
|
|
2790
|
+
movementValidation: {
|
|
2791
|
+
...snapshot.movementValidation,
|
|
2792
|
+
warnings: [...snapshot.movementValidation?.warnings ?? []]
|
|
2793
|
+
},
|
|
2794
|
+
propGroundAnchors: snapshot.propGroundAnchors.map((anchor) => ({ ...anchor }))
|
|
2795
|
+
};
|
|
2796
|
+
},
|
|
2797
|
+
setCamera(nextCamera = {}) {
|
|
2798
|
+
camera = { ...camera, ...nextCamera };
|
|
2799
|
+
snapshot = {
|
|
2800
|
+
...snapshot,
|
|
2801
|
+
cameraViewMode: resolveCameraViewMode(camera)
|
|
2802
|
+
};
|
|
2803
|
+
},
|
|
2804
|
+
setCameraViewMode(viewMode) {
|
|
2805
|
+
camera = { ...camera, viewMode };
|
|
2806
|
+
snapshot = {
|
|
2807
|
+
...snapshot,
|
|
2808
|
+
cameraViewMode: resolveCameraViewMode(camera)
|
|
2809
|
+
};
|
|
2810
|
+
},
|
|
2811
|
+
applyCameraControl(control, options2 = {}) {
|
|
2812
|
+
pendingCameraControl = control;
|
|
2813
|
+
activeCameraControl = options2.activeControl !== false;
|
|
2814
|
+
},
|
|
2815
|
+
destroy() {
|
|
2816
|
+
running = false;
|
|
2817
|
+
if (frameHandle !== void 0 && cancelFrame) {
|
|
2818
|
+
cancelFrame(frameHandle);
|
|
1912
2819
|
}
|
|
2820
|
+
frameHandle = void 0;
|
|
2821
|
+
snapshot = { ...snapshot, running: false, frameState: "destroyed" };
|
|
1913
2822
|
}
|
|
1914
2823
|
};
|
|
1915
|
-
return renderer;
|
|
1916
|
-
}
|
|
1917
|
-
function snapshotFromXrManager(xrManager) {
|
|
1918
|
-
if (xrManager && typeof xrManager.getState === "function") {
|
|
1919
|
-
return xrManager.getState();
|
|
1920
|
-
}
|
|
1921
|
-
if (xrManager?.store && typeof xrManager.store.getSnapshot === "function") {
|
|
1922
|
-
return xrManager.store.getSnapshot();
|
|
1923
|
-
}
|
|
1924
|
-
return null;
|
|
1925
|
-
}
|
|
1926
|
-
function bindRendererToXrManager(renderer, xrManager, options = {}) {
|
|
1927
|
-
if (!xrManager || typeof xrManager.subscribe !== "function") {
|
|
1928
|
-
throw new Error("XR manager must expose subscribe(listener). Use @plasius/gpu-xr createXrManager().");
|
|
1929
|
-
}
|
|
1930
|
-
const { onSessionStart, onSessionEnd } = options;
|
|
1931
|
-
let previousSession = null;
|
|
1932
|
-
const applyState = (state) => {
|
|
1933
|
-
const session = state?.activeSession ?? null;
|
|
1934
|
-
if (session === previousSession) {
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
previousSession = session;
|
|
1938
|
-
if (typeof renderer.setXrActive === "function") {
|
|
1939
|
-
renderer.setXrActive(Boolean(session));
|
|
1940
|
-
}
|
|
1941
|
-
if (session && typeof onSessionStart === "function") {
|
|
1942
|
-
onSessionStart(session, renderer);
|
|
1943
|
-
}
|
|
1944
|
-
if (!session && typeof onSessionEnd === "function") {
|
|
1945
|
-
onSessionEnd(renderer);
|
|
1946
|
-
}
|
|
1947
|
-
};
|
|
1948
|
-
applyState(snapshotFromXrManager(xrManager));
|
|
1949
|
-
return xrManager.subscribe(applyState);
|
|
1950
2824
|
}
|
|
1951
|
-
var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
|
|
1952
2825
|
|
|
1953
2826
|
// src/professional-animated-scene-renderer.js
|
|
1954
2827
|
function nowMs2() {
|
|
@@ -3054,7 +3927,7 @@ function scale(a, scalar) {
|
|
|
3054
3927
|
function dot(a, b) {
|
|
3055
3928
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
3056
3929
|
}
|
|
3057
|
-
function
|
|
3930
|
+
function cross2(a, b) {
|
|
3058
3931
|
return [
|
|
3059
3932
|
a[1] * b[2] - a[2] * b[1],
|
|
3060
3933
|
a[2] * b[0] - a[0] * b[2],
|
|
@@ -3675,7 +4548,7 @@ function createMeshTriangleRecords(meshes, gpuMaterialSource = null) {
|
|
|
3675
4548
|
const v0 = readVector(mesh.positions, a, 3, [0, 0, 0]);
|
|
3676
4549
|
const v1 = readVector(mesh.positions, b, 3, [0, 0, 0]);
|
|
3677
4550
|
const v2 = readVector(mesh.positions, c, 3, [0, 0, 0]);
|
|
3678
|
-
const faceNormal = normalize(
|
|
4551
|
+
const faceNormal = normalize(cross2(subtract(v1, v0), subtract(v2, v0)), [0, 1, 0]);
|
|
3679
4552
|
const n0 = mesh.normals ? normalize(readVector(mesh.normals, a, 3, faceNormal), faceNormal) : faceNormal;
|
|
3680
4553
|
const n1 = mesh.normals ? normalize(readVector(mesh.normals, b, 3, faceNormal), faceNormal) : faceNormal;
|
|
3681
4554
|
const n2 = mesh.normals ? normalize(readVector(mesh.normals, c, 3, faceNormal), faceNormal) : faceNormal;
|
|
@@ -4716,9 +5589,9 @@ function resolveEnvironmentPortalMode(value, hasPortals) {
|
|
|
4716
5589
|
}
|
|
4717
5590
|
function orthogonalPortalTangent(normal) {
|
|
4718
5591
|
if (Math.abs(normal[1]) < 0.92) {
|
|
4719
|
-
return normalize(
|
|
5592
|
+
return normalize(cross2([0, 1, 0], normal), [1, 0, 0]);
|
|
4720
5593
|
}
|
|
4721
|
-
return normalize(
|
|
5594
|
+
return normalize(cross2([1, 0, 0], normal), [0, 0, 1]);
|
|
4722
5595
|
}
|
|
4723
5596
|
function resolvePortalTangent(value, normal) {
|
|
4724
5597
|
const fallback = orthogonalPortalTangent(normal);
|
|
@@ -4750,7 +5623,7 @@ function normalizeEnvironmentPortal(portal, index) {
|
|
|
4750
5623
|
const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
|
|
4751
5624
|
const normal = asUnitVec3(portal.normal, [0, 0, 1]);
|
|
4752
5625
|
const tangent = resolvePortalTangent(portal.tangent, normal);
|
|
4753
|
-
const bitangent = normalize(
|
|
5626
|
+
const bitangent = normalize(cross2(normal, tangent), [0, 1, 0]);
|
|
4754
5627
|
const width = readPortalExtent(
|
|
4755
5628
|
`environmentPortals[${index}].width`,
|
|
4756
5629
|
portal.width,
|
|
@@ -4828,8 +5701,8 @@ function resolveCamera(input, width, height) {
|
|
|
4828
5701
|
const target = asVec3(camera.target, DEFAULT_CAMERA2.target);
|
|
4829
5702
|
const upInput = normalize(asVec3(camera.up, DEFAULT_CAMERA2.up), DEFAULT_CAMERA2.up);
|
|
4830
5703
|
const forward = normalize(subtract(target, position), [0, 0, -1]);
|
|
4831
|
-
const right = normalize(
|
|
4832
|
-
const up = normalize(
|
|
5704
|
+
const right = normalize(cross2(forward, upInput), [1, 0, 0]);
|
|
5705
|
+
const up = normalize(cross2(right, forward), [0, 1, 0]);
|
|
4833
5706
|
const fovYDegrees = clamp2(
|
|
4834
5707
|
readFiniteNumber("camera.fovYDegrees", camera.fovYDegrees ?? camera.fov, DEFAULT_CAMERA2.fovYDegrees),
|
|
4835
5708
|
10,
|
|
@@ -5198,8 +6071,8 @@ function supportsWavefrontPathTracingCompute(options = {}) {
|
|
|
5198
6071
|
// src/wavefront-sampling.js
|
|
5199
6072
|
function buildOrthonormalBasis(normal) {
|
|
5200
6073
|
const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
|
|
5201
|
-
const tangent = normalize(
|
|
5202
|
-
const bitangent = normalize(
|
|
6074
|
+
const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
|
|
6075
|
+
const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
|
|
5203
6076
|
return { tangent, bitangent };
|
|
5204
6077
|
}
|
|
5205
6078
|
function localToWorld(local, normal) {
|
|
@@ -11428,7 +12301,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11428
12301
|
const triangleIndex = readNonNegativeInteger2("triangleIndex", options.triangleIndex, 0);
|
|
11429
12302
|
const edge1 = subtract(triangle.v1, triangle.v0);
|
|
11430
12303
|
const edge2 = subtract(triangle.v2, triangle.v0);
|
|
11431
|
-
const pvec =
|
|
12304
|
+
const pvec = cross2(ray.direction, edge2);
|
|
11432
12305
|
const determinant = dot(edge1, pvec);
|
|
11433
12306
|
if (Math.abs(determinant) < 1e-7) {
|
|
11434
12307
|
return null;
|
|
@@ -11439,7 +12312,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11439
12312
|
if (u < 0 || u > 1) {
|
|
11440
12313
|
return null;
|
|
11441
12314
|
}
|
|
11442
|
-
const qvec =
|
|
12315
|
+
const qvec = cross2(tvec, edge1);
|
|
11443
12316
|
const v = dot(ray.direction, qvec) * invDet;
|
|
11444
12317
|
if (v < 0 || u + v > 1) {
|
|
11445
12318
|
return null;
|
|
@@ -11448,7 +12321,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11448
12321
|
if (distance <= 1e-3 || distance > maxDistance) {
|
|
11449
12322
|
return null;
|
|
11450
12323
|
}
|
|
11451
|
-
const geometric = normalize(
|
|
12324
|
+
const geometric = normalize(cross2(edge1, edge2), [0, 1, 0]);
|
|
11452
12325
|
const frontFace = dot(ray.direction, geometric) < 0;
|
|
11453
12326
|
const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
|
|
11454
12327
|
const w = 1 - u - v;
|
|
@@ -11542,7 +12415,7 @@ function triangleAreaForLightSampling(triangle) {
|
|
|
11542
12415
|
}
|
|
11543
12416
|
const edge1 = subtract(triangle.v1, triangle.v0);
|
|
11544
12417
|
const edge2 = subtract(triangle.v2, triangle.v0);
|
|
11545
|
-
return Math.max(Math.hypot(...
|
|
12418
|
+
return Math.max(Math.hypot(...cross2(edge1, edge2)) * 0.5, 1e-6);
|
|
11546
12419
|
}
|
|
11547
12420
|
function emissiveTriangleWeight(triangle) {
|
|
11548
12421
|
if (!triangle) {
|
|
@@ -13417,10 +14290,12 @@ function createRayTracingRenderPlan(options = {}) {
|
|
|
13417
14290
|
bucketFeedbackGameFrameTime,
|
|
13418
14291
|
bucketFeedbackGameViewport,
|
|
13419
14292
|
createAnimatedSceneRenderer,
|
|
14293
|
+
createCanonicalWavefrontMeshInputs,
|
|
13420
14294
|
createDefaultWavefrontSceneObjects,
|
|
13421
14295
|
createFeedbackGameDiagnosticSnapshot,
|
|
13422
14296
|
createGpuRenderer,
|
|
13423
14297
|
createMediumStack,
|
|
14298
|
+
createNativeSceneRenderer,
|
|
13424
14299
|
createProfessionalAnimatedSceneRenderer,
|
|
13425
14300
|
createRayTracingRenderPlan,
|
|
13426
14301
|
createRendererDebugHooks,
|