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