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