@plasius/gpu-renderer 0.2.45 → 0.2.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +71 -0
- package/dist/index.cjs +2580 -1705
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2578 -1705
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/canonical-wavefront-meshes.js +226 -0
- package/src/index.d.ts +64 -0
- package/src/index.js +2 -0
- package/src/native-scene-renderer.js +550 -0
- package/src/native-scene-shader.js +178 -0
- package/src/renderer-webgpu-runtime.js +24 -19
- package/src/wavefront-adaptive-byte-constants.js +28 -0
- package/src/wavefront-adaptive-metadata.js +185 -0
- package/src/wavefront-adaptive-shader.js +28 -0
package/dist/index.js
CHANGED
|
@@ -1,1861 +1,2732 @@
|
|
|
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/canonical-wavefront-meshes.js
|
|
1217
|
+
var STANDARD_TEXTURE_BINDINGS = Object.freeze([
|
|
1218
|
+
["baseColor", "baseColorTexture"],
|
|
1219
|
+
["metallicRoughness", "metallicRoughnessTexture"],
|
|
1220
|
+
["normal", "normalTexture"],
|
|
1221
|
+
["occlusion", "occlusionTexture"],
|
|
1222
|
+
["emissive", "emissiveTexture"]
|
|
1223
|
+
]);
|
|
1224
|
+
var EXTENSION_TEXTURE_BINDINGS = Object.freeze([
|
|
1225
|
+
["clearcoat", "clearcoat", "clearcoatTexture"],
|
|
1226
|
+
["clearcoatRoughness", "clearcoat", "clearcoatRoughnessTexture"],
|
|
1227
|
+
["clearcoatNormal", "clearcoat", "clearcoatNormalTexture"],
|
|
1228
|
+
["transmission", "transmission", "transmissionTexture"],
|
|
1229
|
+
["sheenColor", "sheen", "sheenColorTexture"],
|
|
1230
|
+
["sheenRoughness", "sheen", "sheenRoughnessTexture"]
|
|
1231
|
+
]);
|
|
1232
|
+
function requireArray(value, label) {
|
|
1233
|
+
if (!Array.isArray(value)) {
|
|
1234
|
+
throw new TypeError(`${label} must be an array.`);
|
|
1235
|
+
}
|
|
1236
|
+
return value;
|
|
616
1237
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1238
|
+
function requireMap(value, label) {
|
|
1239
|
+
if (!value || typeof value !== "object" || typeof value.get !== "function") {
|
|
1240
|
+
throw new TypeError(`${label} must provide a ReadonlyMap-compatible get method.`);
|
|
1241
|
+
}
|
|
1242
|
+
return value;
|
|
1243
|
+
}
|
|
1244
|
+
function textureSample(binding, textures, label, extra = {}) {
|
|
1245
|
+
if (binding === void 0 || binding === null) {
|
|
1246
|
+
return void 0;
|
|
1247
|
+
}
|
|
1248
|
+
const sample = textures.get(binding.textureId);
|
|
1249
|
+
if (!sample) {
|
|
1250
|
+
throw new Error(`Missing decoded texture sample ${binding.textureId} for ${label}.`);
|
|
1251
|
+
}
|
|
1252
|
+
const width = Number(sample.width);
|
|
1253
|
+
const height = Number(sample.height);
|
|
1254
|
+
const data = sample.data;
|
|
1255
|
+
const dataLength = data && typeof data.length === "number" ? data.length : -1;
|
|
1256
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
1257
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} has invalid dimensions.`);
|
|
1258
|
+
}
|
|
1259
|
+
if (width > 4096 || height > 4096 || width * height > 4096 * 4096) {
|
|
1260
|
+
throw new RangeError(`Decoded texture sample ${binding.textureId} exceeds the 4096-pixel resource limit.`);
|
|
1261
|
+
}
|
|
1262
|
+
if (!(Array.isArray(data) || ArrayBuffer.isView(data)) || dataLength < width * height * 4) {
|
|
1263
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} must contain width \xD7 height \xD7 4 RGBA values.`);
|
|
1264
|
+
}
|
|
1265
|
+
return Object.freeze({
|
|
1266
|
+
...sample,
|
|
1267
|
+
...binding.texCoordSet === void 0 ? {} : { texCoord: binding.texCoordSet },
|
|
1268
|
+
...extra
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
function cloneMaterialExtensions(material, textures) {
|
|
1272
|
+
const extensions = { ...material.extensions ?? {} };
|
|
1273
|
+
if (material.workflow === "unlit") {
|
|
1274
|
+
const unlitKey = "KHR_materials_unlit";
|
|
1275
|
+
const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
|
|
1276
|
+
if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
|
|
1277
|
+
throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
|
|
643
1278
|
}
|
|
644
|
-
|
|
645
|
-
|
|
1279
|
+
extensions[unlitKey] = { ...unlitValue, enabled: true };
|
|
1280
|
+
}
|
|
1281
|
+
for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
|
|
1282
|
+
const binding = material.textures?.[bindingName];
|
|
1283
|
+
if (!binding) {
|
|
1284
|
+
continue;
|
|
646
1285
|
}
|
|
647
|
-
|
|
648
|
-
|
|
1286
|
+
const extensionKey = `KHR_materials_${extensionName}`;
|
|
1287
|
+
const extensionValue = extensions[extensionKey] ?? extensions[extensionName] ?? {};
|
|
1288
|
+
if (typeof extensionValue !== "object" || extensionValue === null || Array.isArray(extensionValue)) {
|
|
1289
|
+
throw new TypeError(`Canonical material extension ${extensionKey} must be an object.`);
|
|
649
1290
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
|
1291
|
+
extensions[extensionKey] = {
|
|
1292
|
+
...extensionValue,
|
|
1293
|
+
[textureKey]: textureSample(binding, textures, `${extensionKey}.${textureKey}`)
|
|
666
1294
|
};
|
|
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
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return entry;
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
function resolveTimelineBeat(timeline, loopTimeMs) {
|
|
679
|
-
if (!timeline.length) {
|
|
680
|
-
return null;
|
|
681
1295
|
}
|
|
682
|
-
return
|
|
1296
|
+
return extensions;
|
|
683
1297
|
}
|
|
684
|
-
function
|
|
685
|
-
if (!
|
|
686
|
-
return
|
|
1298
|
+
function mapMaterial(material, materialRefId, textures) {
|
|
1299
|
+
if (!material) {
|
|
1300
|
+
return { materialRefId };
|
|
687
1301
|
}
|
|
688
|
-
if (
|
|
689
|
-
|
|
1302
|
+
if (material.alphaMode === "mask") {
|
|
1303
|
+
throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
|
|
690
1304
|
}
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
function resolveTimelineForward(entry) {
|
|
694
|
-
if (!entry) {
|
|
695
|
-
return [0, 0, -1];
|
|
1305
|
+
if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
|
|
1306
|
+
throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
|
|
696
1307
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1308
|
+
const mapped = {
|
|
1309
|
+
materialRefId,
|
|
1310
|
+
...material.alphaMode === "blend" ? { materialKind: "transparent" } : {},
|
|
1311
|
+
baseColor: material.baseColorFactor,
|
|
1312
|
+
emission: material.emissiveFactor ? [...material.emissiveFactor, 1] : [0, 0, 0, 1],
|
|
1313
|
+
roughness: material.roughnessFactor,
|
|
1314
|
+
metallic: material.metallicFactor,
|
|
1315
|
+
opacity: material.alphaMode === "opaque" ? 1 : material.baseColorFactor?.[3],
|
|
1316
|
+
clearcoat: material.clearcoatFactor,
|
|
1317
|
+
clearcoatRoughness: material.clearcoatRoughnessFactor,
|
|
1318
|
+
transmission: material.transmissionFactor,
|
|
1319
|
+
sheenColor: material.sheenColorFactor,
|
|
1320
|
+
extensions: cloneMaterialExtensions(material, textures)
|
|
1321
|
+
};
|
|
1322
|
+
for (const [bindingName, inputName] of STANDARD_TEXTURE_BINDINGS) {
|
|
1323
|
+
const binding = material.textures?.[bindingName];
|
|
1324
|
+
if (!binding) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const extra = bindingName === "normal" && material.normalScale !== void 0 ? { scale: material.normalScale } : bindingName === "occlusion" && material.occlusionStrength !== void 0 ? { strength: material.occlusionStrength } : {};
|
|
1328
|
+
mapped[inputName] = textureSample(binding, textures, inputName, extra);
|
|
706
1329
|
}
|
|
707
|
-
return
|
|
1330
|
+
return mapped;
|
|
708
1331
|
}
|
|
709
|
-
function
|
|
710
|
-
if (typeof
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1332
|
+
function createCanonicalWavefrontMeshInputs({ document, geometry, textures = /* @__PURE__ */ new Map() } = {}) {
|
|
1333
|
+
if (!document || typeof document !== "object") {
|
|
1334
|
+
throw new TypeError("Canonical model document is required.");
|
|
1335
|
+
}
|
|
1336
|
+
const meshes = requireArray(document.meshes, "Canonical model meshes");
|
|
1337
|
+
const materials = requireArray(document.materials, "Canonical model materials");
|
|
1338
|
+
const geometryInputs = requireArray(geometry, "Renderer-ready geometry");
|
|
1339
|
+
const textureSamples = requireMap(textures, "Decoded texture samples");
|
|
1340
|
+
const geometryByPrimitiveId = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const entry of geometryInputs) {
|
|
1342
|
+
if (!entry || typeof entry.primitiveId !== "string" || entry.primitiveId.length === 0) {
|
|
1343
|
+
throw new TypeError("Renderer-ready geometry requires a non-empty primitiveId.");
|
|
714
1344
|
}
|
|
715
|
-
|
|
1345
|
+
if (geometryByPrimitiveId.has(entry.primitiveId)) {
|
|
1346
|
+
throw new Error(`Duplicate renderer-ready geometry for primitive ${entry.primitiveId}.`);
|
|
1347
|
+
}
|
|
1348
|
+
geometryByPrimitiveId.set(entry.primitiveId, entry);
|
|
716
1349
|
}
|
|
717
|
-
|
|
718
|
-
|
|
1350
|
+
const materialsById = /* @__PURE__ */ new Map();
|
|
1351
|
+
const materialRefIds = /* @__PURE__ */ new Map();
|
|
1352
|
+
materials.forEach((material, index) => {
|
|
1353
|
+
if (!material || typeof material.id !== "string" || material.id.length === 0) {
|
|
1354
|
+
throw new TypeError("Canonical materials require a non-empty id.");
|
|
1355
|
+
}
|
|
1356
|
+
if (materialsById.has(material.id)) {
|
|
1357
|
+
throw new Error(`Duplicate canonical material ${material.id}.`);
|
|
1358
|
+
}
|
|
1359
|
+
materialsById.set(material.id, material);
|
|
1360
|
+
materialRefIds.set(material.id, index + 1);
|
|
1361
|
+
});
|
|
1362
|
+
const output = [];
|
|
1363
|
+
const consumedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1364
|
+
const canonicalPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1365
|
+
for (const mesh of meshes) {
|
|
1366
|
+
if (!mesh || typeof mesh.id !== "string") {
|
|
1367
|
+
throw new TypeError("Canonical meshes require a string id.");
|
|
1368
|
+
}
|
|
1369
|
+
for (const primitive of requireArray(mesh.primitives, `Primitives for mesh ${mesh.id}`)) {
|
|
1370
|
+
if (!primitive || typeof primitive.id !== "string" || primitive.id.length === 0) {
|
|
1371
|
+
throw new TypeError(`Canonical mesh ${mesh.id} contains a primitive without an id.`);
|
|
1372
|
+
}
|
|
1373
|
+
if (canonicalPrimitiveIds.has(primitive.id)) {
|
|
1374
|
+
throw new Error(`Duplicate canonical primitive ${primitive.id}.`);
|
|
1375
|
+
}
|
|
1376
|
+
canonicalPrimitiveIds.add(primitive.id);
|
|
1377
|
+
if (primitive.topology !== "triangles") {
|
|
1378
|
+
throw new Error(`Unsupported canonical primitive topology: ${primitive.topology}.`);
|
|
1379
|
+
}
|
|
1380
|
+
const source = geometryByPrimitiveId.get(primitive.id);
|
|
1381
|
+
if (!source) {
|
|
1382
|
+
throw new Error(`Missing renderer-ready geometry for primitive ${primitive.id}.`);
|
|
1383
|
+
}
|
|
1384
|
+
consumedPrimitiveIds.add(primitive.id);
|
|
1385
|
+
let material = null;
|
|
1386
|
+
let materialRefId = 0;
|
|
1387
|
+
if (primitive.materialId !== void 0) {
|
|
1388
|
+
material = materialsById.get(primitive.materialId);
|
|
1389
|
+
if (!material) {
|
|
1390
|
+
throw new Error(`Missing canonical material ${primitive.materialId} for primitive ${primitive.id}.`);
|
|
1391
|
+
}
|
|
1392
|
+
materialRefId = materialRefIds.get(primitive.materialId);
|
|
1393
|
+
}
|
|
1394
|
+
output.push(Object.freeze({
|
|
1395
|
+
id: output.length + 1,
|
|
1396
|
+
positions: source.positions,
|
|
1397
|
+
...source.indices === void 0 ? {} : { indices: source.indices },
|
|
1398
|
+
...source.normals === void 0 ? {} : { normals: source.normals },
|
|
1399
|
+
...(source.texcoords ?? source.uvs) === void 0 ? {} : { uvs: source.texcoords ?? source.uvs },
|
|
1400
|
+
...mapMaterial(material, materialRefId, textureSamples)
|
|
1401
|
+
}));
|
|
1402
|
+
}
|
|
719
1403
|
}
|
|
720
|
-
|
|
1404
|
+
for (const primitiveId of geometryByPrimitiveId.keys()) {
|
|
1405
|
+
if (!consumedPrimitiveIds.has(primitiveId)) {
|
|
1406
|
+
throw new Error(`Renderer-ready geometry references unknown primitive ${primitiveId}.`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return Object.freeze(output);
|
|
721
1410
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
1411
|
+
|
|
1412
|
+
// src/animated-scene-renderer.js
|
|
1413
|
+
import { resolveCameraRigFrame } from "@plasius/gpu-camera";
|
|
1414
|
+
|
|
1415
|
+
// src/animated-gltf-model.js
|
|
1416
|
+
var GLB_MAGIC = 1179937895;
|
|
1417
|
+
var GLB_VERSION = 2;
|
|
1418
|
+
var GLB_JSON_CHUNK = 1313821514;
|
|
1419
|
+
var GLB_BIN_CHUNK = 5130562;
|
|
1420
|
+
var COMPONENT_READERS = Object.freeze({
|
|
1421
|
+
5120: { size: 1, read: (view, offset) => view.getInt8(offset), normalizedDivisor: 127 },
|
|
1422
|
+
5121: { size: 1, read: (view, offset) => view.getUint8(offset), normalizedDivisor: 255 },
|
|
1423
|
+
5122: { size: 2, read: (view, offset) => view.getInt16(offset, true), normalizedDivisor: 32767 },
|
|
1424
|
+
5123: { size: 2, read: (view, offset) => view.getUint16(offset, true), normalizedDivisor: 65535 },
|
|
1425
|
+
5125: { size: 4, read: (view, offset) => view.getUint32(offset, true), normalizedDivisor: 4294967295 },
|
|
1426
|
+
5126: { size: 4, read: (view, offset) => view.getFloat32(offset, true), normalizedDivisor: 1 }
|
|
1427
|
+
});
|
|
1428
|
+
var ACCESSOR_COMPONENTS = Object.freeze({
|
|
1429
|
+
SCALAR: 1,
|
|
1430
|
+
VEC2: 2,
|
|
1431
|
+
VEC3: 3,
|
|
1432
|
+
VEC4: 4,
|
|
1433
|
+
MAT4: 16
|
|
1434
|
+
});
|
|
1435
|
+
function identityMatrix() {
|
|
1436
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
735
1437
|
}
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
ctx.fillRect(-scale2 * 0.36, -scale2 * 0.035, scale2 * 0.72, scale2 * 0.07);
|
|
742
|
-
}
|
|
743
|
-
function drawProp(ctx, prop, cameraPosition, width, height) {
|
|
744
|
-
const projected = project(prop.position, cameraPosition, width, height);
|
|
745
|
-
const { x, y, scale: scale2 } = projected;
|
|
746
|
-
ctx.save();
|
|
747
|
-
ctx.translate(x, y);
|
|
748
|
-
ctx.lineWidth = Math.max(1, scale2 * 0.04);
|
|
749
|
-
if (prop.kind === "crop-row") {
|
|
750
|
-
ctx.fillStyle = "#8b633f";
|
|
751
|
-
ctx.fillRect(-scale2 * 1.05, -scale2 * 0.08, scale2 * 2.1, scale2 * 0.16);
|
|
752
|
-
ctx.fillStyle = "#6f4a30";
|
|
753
|
-
ctx.fillRect(-scale2 * 0.92, -scale2 * 0.035, scale2 * 1.84, scale2 * 0.07);
|
|
754
|
-
ctx.strokeStyle = "#4f6f32";
|
|
755
|
-
ctx.beginPath();
|
|
756
|
-
ctx.moveTo(-scale2 * 0.8, 0);
|
|
757
|
-
ctx.lineTo(scale2 * 0.8, 0);
|
|
758
|
-
ctx.stroke();
|
|
759
|
-
ctx.fillStyle = "#6f9f43";
|
|
760
|
-
for (let i = -3; i <= 3; i += 1) {
|
|
761
|
-
ctx.fillRect(i * scale2 * 0.22, -scale2 * 0.18, scale2 * 0.06, scale2 * 0.2);
|
|
762
|
-
}
|
|
763
|
-
} else if (prop.kind === "fence-segment") {
|
|
764
|
-
ctx.strokeStyle = "#896b4d";
|
|
765
|
-
ctx.beginPath();
|
|
766
|
-
ctx.moveTo(-scale2 * 0.5, 0);
|
|
767
|
-
ctx.lineTo(scale2 * 0.5, 0);
|
|
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();
|
|
1438
|
+
function multiplyMatrix(a, b) {
|
|
1439
|
+
const out = new Array(16).fill(0);
|
|
1440
|
+
for (let column = 0; column < 4; column += 1) {
|
|
1441
|
+
for (let row = 0; row < 4; row += 1) {
|
|
1442
|
+
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];
|
|
812
1443
|
}
|
|
813
1444
|
}
|
|
814
|
-
|
|
815
|
-
return projected;
|
|
1445
|
+
return out;
|
|
816
1446
|
}
|
|
817
|
-
function
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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();
|
|
857
|
-
}
|
|
858
|
-
ctx.strokeStyle = "#6b4935";
|
|
859
|
-
ctx.beginPath();
|
|
860
|
-
ctx.moveTo(-scale2 * 0.24, -scale2 * 0.76);
|
|
861
|
-
ctx.lineTo(-scale2 * (0.46 + stride * 0.04), -scale2 * (0.5 + stride * 0.06));
|
|
862
|
-
ctx.moveTo(scale2 * 0.24, -scale2 * 0.76);
|
|
863
|
-
ctx.lineTo(scale2 * (0.46 - stride * 0.04), -scale2 * (0.5 - stride * 0.06));
|
|
864
|
-
ctx.stroke();
|
|
865
|
-
ctx.fillStyle = "#d6c09a";
|
|
866
|
-
ctx.fillRect(-scale2 * 0.16, -scale2 * 0.86, scale2 * 0.32, scale2 * 0.18);
|
|
867
|
-
ctx.restore();
|
|
868
|
-
return projected;
|
|
1447
|
+
function transformPoint(matrix, point) {
|
|
1448
|
+
const [x, y, z] = point;
|
|
1449
|
+
return [
|
|
1450
|
+
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
1451
|
+
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
1452
|
+
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
|
|
1453
|
+
];
|
|
869
1454
|
}
|
|
870
|
-
function
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
const
|
|
1455
|
+
function composeMatrix(translation = [0, 0, 0], rotation = [0, 0, 0, 1], scale2 = [1, 1, 1]) {
|
|
1456
|
+
const [x, y, z, w] = normalizeQuaternion(rotation);
|
|
1457
|
+
const [sx, sy, sz] = scale2;
|
|
1458
|
+
const x2 = x + x;
|
|
1459
|
+
const y2 = y + y;
|
|
1460
|
+
const z2 = z + z;
|
|
1461
|
+
const xx = x * x2;
|
|
1462
|
+
const xy = x * y2;
|
|
1463
|
+
const xz = x * z2;
|
|
1464
|
+
const yy = y * y2;
|
|
1465
|
+
const yz = y * z2;
|
|
1466
|
+
const zz = z * z2;
|
|
1467
|
+
const wx = w * x2;
|
|
1468
|
+
const wy = w * y2;
|
|
1469
|
+
const wz = w * z2;
|
|
876
1470
|
return [
|
|
877
|
-
|
|
878
|
-
|
|
1471
|
+
(1 - (yy + zz)) * sx,
|
|
1472
|
+
(xy + wz) * sx,
|
|
1473
|
+
(xz - wy) * sx,
|
|
1474
|
+
0,
|
|
1475
|
+
(xy - wz) * sy,
|
|
1476
|
+
(1 - (xx + zz)) * sy,
|
|
1477
|
+
(yz + wx) * sy,
|
|
1478
|
+
0,
|
|
1479
|
+
(xz + wy) * sz,
|
|
1480
|
+
(yz - wx) * sz,
|
|
1481
|
+
(1 - (xx + yy)) * sz,
|
|
1482
|
+
0,
|
|
1483
|
+
translation[0] ?? 0,
|
|
1484
|
+
translation[1] ?? 0,
|
|
1485
|
+
translation[2] ?? 0,
|
|
1486
|
+
1
|
|
879
1487
|
];
|
|
880
1488
|
}
|
|
881
|
-
function
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1489
|
+
function normalizeQuaternion(quaternion = [0, 0, 0, 1]) {
|
|
1490
|
+
const length = Math.hypot(quaternion[0] ?? 0, quaternion[1] ?? 0, quaternion[2] ?? 0, quaternion[3] ?? 1) || 1;
|
|
1491
|
+
return [
|
|
1492
|
+
(quaternion[0] ?? 0) / length,
|
|
1493
|
+
(quaternion[1] ?? 0) / length,
|
|
1494
|
+
(quaternion[2] ?? 0) / length,
|
|
1495
|
+
(quaternion[3] ?? 1) / length
|
|
1496
|
+
];
|
|
1497
|
+
}
|
|
1498
|
+
function lerp(a, b, t) {
|
|
1499
|
+
return a + (b - a) * t;
|
|
1500
|
+
}
|
|
1501
|
+
function lerpArray(a, b, t) {
|
|
1502
|
+
return a.map((value, index) => lerp(value, b[index] ?? value, t));
|
|
1503
|
+
}
|
|
1504
|
+
function nlerpQuaternion(a, b, t) {
|
|
1505
|
+
let end = b;
|
|
1506
|
+
const dot2 = a.reduce((total, value, index) => total + value * (b[index] ?? 0), 0);
|
|
1507
|
+
if (dot2 < 0) {
|
|
1508
|
+
end = b.map((value) => -value);
|
|
886
1509
|
}
|
|
887
|
-
|
|
888
|
-
|
|
1510
|
+
return normalizeQuaternion(lerpArray(a, end, t));
|
|
1511
|
+
}
|
|
1512
|
+
function parseGlb(buffer) {
|
|
1513
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
1514
|
+
return null;
|
|
889
1515
|
}
|
|
890
|
-
|
|
891
|
-
|
|
1516
|
+
const view = new DataView(buffer);
|
|
1517
|
+
if (view.byteLength < 20 || view.getUint32(0, true) !== GLB_MAGIC || view.getUint32(4, true) !== GLB_VERSION) {
|
|
1518
|
+
return null;
|
|
892
1519
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const base = triangleIndex * 3;
|
|
904
|
-
const a = sample.vertices[sample.indices[base]];
|
|
905
|
-
const b = sample.vertices[sample.indices[base + 1]];
|
|
906
|
-
const c = sample.vertices[sample.indices[base + 2]];
|
|
907
|
-
if (!a || !b || !c) {
|
|
908
|
-
continue;
|
|
1520
|
+
let offset = 12;
|
|
1521
|
+
let json = null;
|
|
1522
|
+
let binary = null;
|
|
1523
|
+
while (offset + 8 <= view.byteLength) {
|
|
1524
|
+
const length = view.getUint32(offset, true);
|
|
1525
|
+
const type = view.getUint32(offset + 4, true);
|
|
1526
|
+
const start = offset + 8;
|
|
1527
|
+
const end = start + length;
|
|
1528
|
+
if (end > view.byteLength) {
|
|
1529
|
+
return null;
|
|
909
1530
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
triangles.sort((left, right) => left.depth - right.depth);
|
|
919
|
-
ctx.save();
|
|
920
|
-
drawGroundShadow(ctx, projection.scale);
|
|
921
|
-
for (const triangle of triangles) {
|
|
922
|
-
const [ax, ay] = pointForModelVertex(triangle.a, projection, sample);
|
|
923
|
-
const [bx, by] = pointForModelVertex(triangle.b, projection, sample);
|
|
924
|
-
const [cx, cy] = pointForModelVertex(triangle.c, projection, sample);
|
|
925
|
-
ctx.fillStyle = triangle.fill;
|
|
926
|
-
ctx.beginPath();
|
|
927
|
-
ctx.moveTo(ax, ay);
|
|
928
|
-
ctx.lineTo(bx, by);
|
|
929
|
-
ctx.lineTo(cx, cy);
|
|
930
|
-
ctx.closePath?.();
|
|
931
|
-
ctx.fill();
|
|
1531
|
+
if (type === GLB_JSON_CHUNK) {
|
|
1532
|
+
const text = new TextDecoder().decode(new Uint8Array(buffer, start, length)).trim();
|
|
1533
|
+
json = JSON.parse(text);
|
|
1534
|
+
} else if (type === GLB_BIN_CHUNK) {
|
|
1535
|
+
binary = new Uint8Array(buffer, start, length);
|
|
1536
|
+
}
|
|
1537
|
+
offset = end;
|
|
932
1538
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
};
|
|
1539
|
+
if (!json || !binary) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
return { json, binary };
|
|
938
1543
|
}
|
|
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
|
-
});
|
|
1544
|
+
function readAccessor(document, accessorIndex) {
|
|
1545
|
+
const accessor = document.json.accessors?.[accessorIndex];
|
|
1546
|
+
if (!accessor) {
|
|
1547
|
+
return [];
|
|
968
1548
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1549
|
+
const bufferView = document.json.bufferViews?.[accessor.bufferView];
|
|
1550
|
+
const component = COMPONENT_READERS[accessor.componentType];
|
|
1551
|
+
const componentCount = ACCESSOR_COMPONENTS[accessor.type] ?? 1;
|
|
1552
|
+
if (!bufferView || !component) {
|
|
1553
|
+
return [];
|
|
1554
|
+
}
|
|
1555
|
+
const view = new DataView(
|
|
1556
|
+
document.binary.buffer,
|
|
1557
|
+
document.binary.byteOffset + (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0),
|
|
1558
|
+
bufferView.byteLength ?? 0
|
|
1559
|
+
);
|
|
1560
|
+
const stride = bufferView.byteStride ?? component.size * componentCount;
|
|
1561
|
+
const values = [];
|
|
1562
|
+
for (let index = 0; index < accessor.count; index += 1) {
|
|
1563
|
+
const item = [];
|
|
1564
|
+
const baseOffset = index * stride;
|
|
1565
|
+
for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
|
|
1566
|
+
const raw = component.read(view, baseOffset + componentIndex * component.size);
|
|
1567
|
+
item.push(accessor.normalized && accessor.componentType !== 5126 ? raw / component.normalizedDivisor : raw);
|
|
1568
|
+
}
|
|
1569
|
+
values.push(componentCount === 1 ? item[0] : item);
|
|
1570
|
+
}
|
|
1571
|
+
return values;
|
|
1572
|
+
}
|
|
1573
|
+
function readMatrices(document, accessorIndex) {
|
|
1574
|
+
return readAccessor(document, accessorIndex).map((matrix) => Array.from(matrix));
|
|
1575
|
+
}
|
|
1576
|
+
function textureImage(document, textureIndex) {
|
|
1577
|
+
if (typeof textureIndex !== "number") {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const texture = document.json.textures?.[textureIndex];
|
|
1581
|
+
return typeof texture?.source === "number" ? document.json.images?.[texture.source] ?? null : null;
|
|
1582
|
+
}
|
|
1583
|
+
function imageHasBuffer(document, image) {
|
|
1584
|
+
if (!image) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof image.uri === "string" && image.uri.trim().length > 0) {
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
return typeof image.bufferView === "number" && Boolean(document.json.bufferViews?.[image.bufferView]);
|
|
1591
|
+
}
|
|
1592
|
+
function extractMaterialTextureMetadata(document, primitive) {
|
|
1593
|
+
const material = document.json.materials?.[primitive?.material];
|
|
1594
|
+
const baseColorTextureIndex = material?.pbrMetallicRoughness?.baseColorTexture?.index;
|
|
1595
|
+
const normalTextureIndex = material?.normalTexture?.index;
|
|
1596
|
+
const missingTextureReferences = [];
|
|
1597
|
+
if (typeof baseColorTextureIndex === "number" && !imageHasBuffer(document, textureImage(document, baseColorTextureIndex))) {
|
|
1598
|
+
missingTextureReferences.push("baseColorTexture");
|
|
1599
|
+
}
|
|
1600
|
+
if (typeof normalTextureIndex === "number" && !imageHasBuffer(document, textureImage(document, normalTextureIndex))) {
|
|
1601
|
+
missingTextureReferences.push("normalTexture");
|
|
1602
|
+
}
|
|
1603
|
+
return Object.freeze({
|
|
1604
|
+
materialCount: document.json.materials?.length ?? 0,
|
|
1605
|
+
textureCount: document.json.textures?.length ?? 0,
|
|
1606
|
+
imageCount: document.json.images?.length ?? 0,
|
|
1607
|
+
embeddedImageCount: (document.json.images ?? []).filter((image) => typeof image.bufferView === "number").length,
|
|
1608
|
+
baseColorTextureCount: typeof baseColorTextureIndex === "number" ? 1 : 0,
|
|
1609
|
+
normalTextureCount: typeof normalTextureIndex === "number" ? 1 : 0,
|
|
1610
|
+
hasBaseColorTexture: typeof baseColorTextureIndex === "number",
|
|
1611
|
+
hasNormalTexture: typeof normalTextureIndex === "number",
|
|
1612
|
+
missingTextureReferences: Object.freeze(missingTextureReferences),
|
|
1613
|
+
materialName: material?.name ?? ""
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
function nodeLocalMatrix(node, override = {}) {
|
|
1617
|
+
if (node.matrix && !override.translation && !override.rotation && !override.scale) {
|
|
1618
|
+
return [...node.matrix];
|
|
1619
|
+
}
|
|
1620
|
+
return composeMatrix(
|
|
1621
|
+
override.translation ?? node.translation ?? [0, 0, 0],
|
|
1622
|
+
override.rotation ?? node.rotation ?? [0, 0, 0, 1],
|
|
1623
|
+
override.scale ?? node.scale ?? [1, 1, 1]
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
function computeWorldMatrices(nodes, overridesByName = /* @__PURE__ */ new Map()) {
|
|
1627
|
+
const parents = /* @__PURE__ */ new Set();
|
|
1628
|
+
for (const node of nodes) {
|
|
1629
|
+
for (const child of node.children ?? []) {
|
|
1630
|
+
parents.add(child);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const roots = nodes.map((_, index) => index).filter((index) => !parents.has(index));
|
|
1634
|
+
const worldMatrices = nodes.map(() => identityMatrix());
|
|
1635
|
+
function visit(nodeIndex, parentMatrix) {
|
|
1636
|
+
const node = nodes[nodeIndex];
|
|
1637
|
+
if (!node) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const worldMatrix = multiplyMatrix(parentMatrix, nodeLocalMatrix(node, overridesByName.get(node.name) ?? {}));
|
|
1641
|
+
worldMatrices[nodeIndex] = worldMatrix;
|
|
1642
|
+
for (const child of node.children ?? []) {
|
|
1643
|
+
visit(child, worldMatrix);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
for (const root of roots) {
|
|
1647
|
+
visit(root, identityMatrix());
|
|
1648
|
+
}
|
|
1649
|
+
return worldMatrices;
|
|
1650
|
+
}
|
|
1651
|
+
function normalizeWeights(weights) {
|
|
1652
|
+
const total = weights.reduce((sum, value) => sum + Math.max(0, value), 0);
|
|
1653
|
+
if (total <= 0) {
|
|
1654
|
+
return [1, 0, 0, 0];
|
|
1655
|
+
}
|
|
1656
|
+
return weights.map((value) => Math.max(0, value) / total);
|
|
1657
|
+
}
|
|
1658
|
+
function deriveBounds(vertices) {
|
|
1659
|
+
const min = [Infinity, Infinity, Infinity];
|
|
1660
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
1661
|
+
for (const vertex of vertices) {
|
|
1662
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
1663
|
+
min[axis] = Math.min(min[axis], vertex[axis]);
|
|
1664
|
+
max[axis] = Math.max(max[axis], vertex[axis]);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (!Number.isFinite(min[0])) {
|
|
1668
|
+
return { min: [0, 0, 0], max: [0, 1, 0], size: [1, 1, 1] };
|
|
999
1669
|
}
|
|
1000
1670
|
return {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
modelRenderable,
|
|
1005
|
-
fallbackProxyActive,
|
|
1006
|
-
skinnedVertexCount,
|
|
1007
|
-
skinnedTriangleCount,
|
|
1008
|
-
skinnedJointCount,
|
|
1009
|
-
skinnedAnimatedNodeCount,
|
|
1010
|
-
skinnedClipCount,
|
|
1011
|
-
activeClipRenderable,
|
|
1012
|
-
propGroundAnchors
|
|
1671
|
+
min,
|
|
1672
|
+
max,
|
|
1673
|
+
size: [max[0] - min[0], max[1] - min[1], max[2] - min[2]]
|
|
1013
1674
|
};
|
|
1014
1675
|
}
|
|
1015
|
-
function
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
options.clipAssets ?? options.animationAdventure?.clipAssets ?? []
|
|
1020
|
-
);
|
|
1021
|
-
} catch {
|
|
1676
|
+
function sampleChannel(channel, sampler, document, timeSeconds) {
|
|
1677
|
+
const input = readAccessor(document, sampler.input);
|
|
1678
|
+
const output = readAccessor(document, sampler.output);
|
|
1679
|
+
if (!input.length || !output.length) {
|
|
1022
1680
|
return null;
|
|
1023
1681
|
}
|
|
1682
|
+
const duration = input.at(-1) || 0;
|
|
1683
|
+
const sampleTime = duration > 0 ? timeSeconds % duration : 0;
|
|
1684
|
+
let frame = 0;
|
|
1685
|
+
while (frame < input.length - 2 && sampleTime > input[frame + 1]) {
|
|
1686
|
+
frame += 1;
|
|
1687
|
+
}
|
|
1688
|
+
const start = input[frame] ?? 0;
|
|
1689
|
+
const end = input[frame + 1] ?? start;
|
|
1690
|
+
const t = end > start ? (sampleTime - start) / (end - start) : 0;
|
|
1691
|
+
const startValue = Array.isArray(output[frame]) ? output[frame] : [output[frame] ?? 0];
|
|
1692
|
+
const endValue = Array.isArray(output[frame + 1]) ? output[frame + 1] : startValue;
|
|
1693
|
+
return channel.target.path === "rotation" ? nlerpQuaternion(startValue, endValue, t) : lerpArray(startValue, endValue, t);
|
|
1024
1694
|
}
|
|
1025
|
-
function
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
if (!ctx) {
|
|
1029
|
-
throw new Error("Animated scene renderer requires a 2D canvas context.");
|
|
1695
|
+
function rootTranslationDistance(clipPayload) {
|
|
1696
|
+
if (!clipPayload) {
|
|
1697
|
+
return 0;
|
|
1030
1698
|
}
|
|
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;
|
|
1699
|
+
let distance = 0;
|
|
1700
|
+
for (const channel of clipPayload.animation.channels ?? []) {
|
|
1701
|
+
if (channel.target?.path !== "translation") {
|
|
1702
|
+
continue;
|
|
1119
1703
|
}
|
|
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));
|
|
1704
|
+
const nodeName = clipPayload.document.json.nodes?.[channel.target.node]?.name ?? "";
|
|
1705
|
+
if (!/(^|:)Hips$/u.test(nodeName) && nodeName !== "mixamorigHips") {
|
|
1706
|
+
continue;
|
|
1176
1707
|
}
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
};
|
|
1182
|
-
cameraPosition = [...cameraRigFrame.transform.position];
|
|
1183
|
-
pendingCameraControl = null;
|
|
1184
|
-
activeCameraControl = false;
|
|
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;
|
|
1232
|
-
}
|
|
1233
|
-
function tick(timestamp) {
|
|
1234
|
-
if (!running) {
|
|
1235
|
-
return;
|
|
1708
|
+
const sampler = clipPayload.animation.samplers?.[channel.sampler];
|
|
1709
|
+
const output = sampler ? readAccessor(clipPayload.document, sampler.output) : [];
|
|
1710
|
+
if (output.length < 2) {
|
|
1711
|
+
continue;
|
|
1236
1712
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1713
|
+
const start = Array.isArray(output[0]) ? output[0] : [0, 0, 0];
|
|
1714
|
+
const end = Array.isArray(output.at(-1)) ? output.at(-1) : start;
|
|
1715
|
+
distance = Math.max(distance, Math.hypot((end[0] ?? 0) - (start[0] ?? 0), (end[2] ?? 0) - (start[2] ?? 0)));
|
|
1716
|
+
}
|
|
1717
|
+
return distance;
|
|
1718
|
+
}
|
|
1719
|
+
function createClipPayload(clipRef) {
|
|
1720
|
+
const document = parseGlb(clipRef?.asset);
|
|
1721
|
+
const animation = document?.json.animations?.[0];
|
|
1722
|
+
if (!document || !animation) {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
const nodeNames = /* @__PURE__ */ new Set();
|
|
1726
|
+
let durationSeconds = 0;
|
|
1727
|
+
for (const channel of animation.channels ?? []) {
|
|
1728
|
+
const nodeName = document.json.nodes?.[channel.target?.node]?.name;
|
|
1729
|
+
if (nodeName) {
|
|
1730
|
+
nodeNames.add(nodeName);
|
|
1240
1731
|
}
|
|
1732
|
+
const sampler = animation.samplers?.[channel.sampler];
|
|
1733
|
+
const input = sampler ? readAccessor(document, sampler.input) : [];
|
|
1734
|
+
durationSeconds = Math.max(durationSeconds, input.at(-1) ?? 0);
|
|
1241
1735
|
}
|
|
1242
1736
|
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
|
-
|
|
1737
|
+
id: clipRef.id,
|
|
1738
|
+
document,
|
|
1739
|
+
animation,
|
|
1740
|
+
durationSeconds,
|
|
1741
|
+
nodeNames
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function sampleClipOverrides(clipPayload, timeSeconds) {
|
|
1745
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1746
|
+
if (!clipPayload) {
|
|
1747
|
+
return overrides;
|
|
1748
|
+
}
|
|
1749
|
+
for (const channel of clipPayload.animation.channels ?? []) {
|
|
1750
|
+
const targetNode = clipPayload.document.json.nodes?.[channel.target?.node];
|
|
1751
|
+
const sampler = clipPayload.animation.samplers?.[channel.sampler];
|
|
1752
|
+
if (!targetNode?.name || !sampler) {
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
const value = sampleChannel(channel, sampler, clipPayload.document, timeSeconds);
|
|
1756
|
+
if (!value) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
const existing = overrides.get(targetNode.name) ?? {};
|
|
1760
|
+
overrides.set(targetNode.name, {
|
|
1761
|
+
...existing,
|
|
1762
|
+
[channel.target.path]: value
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
return overrides;
|
|
1766
|
+
}
|
|
1767
|
+
function createAnimatedGltfModel(modelAsset, clipRefs = []) {
|
|
1768
|
+
const document = parseGlb(modelAsset);
|
|
1769
|
+
if (!document) {
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
const nodes = document.json.nodes ?? [];
|
|
1773
|
+
const meshNodeIndex = nodes.findIndex((node) => node.mesh !== void 0 && node.skin !== void 0);
|
|
1774
|
+
const meshNode = nodes[meshNodeIndex];
|
|
1775
|
+
const mesh = document.json.meshes?.[meshNode?.mesh];
|
|
1776
|
+
const primitive = mesh?.primitives?.find((candidate) => candidate.attributes?.POSITION !== void 0);
|
|
1777
|
+
const skin = document.json.skins?.[meshNode?.skin];
|
|
1778
|
+
if (!meshNode || !primitive || !skin) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const positions = readAccessor(document, primitive.attributes.POSITION);
|
|
1782
|
+
const normals = primitive.attributes.NORMAL === void 0 ? [] : readAccessor(document, primitive.attributes.NORMAL);
|
|
1783
|
+
const texcoords = primitive.attributes.TEXCOORD_0 === void 0 ? [] : readAccessor(document, primitive.attributes.TEXCOORD_0);
|
|
1784
|
+
const joints = primitive.attributes.JOINTS_0 === void 0 ? positions.map(() => [0, 0, 0, 0]) : readAccessor(document, primitive.attributes.JOINTS_0);
|
|
1785
|
+
const weights = primitive.attributes.WEIGHTS_0 === void 0 ? positions.map(() => [1, 0, 0, 0]) : readAccessor(document, primitive.attributes.WEIGHTS_0).map(normalizeWeights);
|
|
1786
|
+
const indices = primitive.indices === void 0 ? positions.map((_, index) => index) : readAccessor(document, primitive.indices);
|
|
1787
|
+
const inverseBindMatrices = skin.inverseBindMatrices === void 0 ? skin.joints.map(() => identityMatrix()) : readMatrices(document, skin.inverseBindMatrices);
|
|
1788
|
+
const clips = clipRefs.map(createClipPayload).filter(Boolean);
|
|
1789
|
+
const bindWorldMatrices = computeWorldMatrices(nodes);
|
|
1790
|
+
const bindVertices = skinVertices({
|
|
1791
|
+
positions,
|
|
1792
|
+
joints,
|
|
1793
|
+
weights,
|
|
1794
|
+
skin,
|
|
1795
|
+
inverseBindMatrices,
|
|
1796
|
+
worldMatrices: bindWorldMatrices
|
|
1797
|
+
});
|
|
1798
|
+
const bounds = deriveBounds(bindVertices);
|
|
1799
|
+
const materialTextureMetadata = extractMaterialTextureMetadata(document, primitive);
|
|
1800
|
+
const clipRootMotionDistances = new Map(clips.map((clip) => [clip.id, rootTranslationDistance(clip)]));
|
|
1801
|
+
return {
|
|
1802
|
+
name: mesh.name ?? meshNode.name ?? "skinned-gltf-model",
|
|
1803
|
+
vertexCount: positions.length,
|
|
1804
|
+
triangleCount: Math.floor(indices.length / 3),
|
|
1805
|
+
jointCount: skin.joints.length,
|
|
1806
|
+
animatedNodeCount: new Set(clips.flatMap((clip) => [...clip.nodeNames])).size,
|
|
1807
|
+
clipCount: clips.length,
|
|
1808
|
+
materialTextureMetadata,
|
|
1809
|
+
textureCount: materialTextureMetadata.textureCount,
|
|
1810
|
+
materialCount: materialTextureMetadata.materialCount,
|
|
1811
|
+
hasBaseColorTexture: materialTextureMetadata.hasBaseColorTexture,
|
|
1812
|
+
hasNormalTexture: materialTextureMetadata.hasNormalTexture,
|
|
1813
|
+
hasUv: texcoords.length === positions.length,
|
|
1814
|
+
hasNormals: normals.length === positions.length,
|
|
1815
|
+
professionalRenderable: materialTextureMetadata.hasBaseColorTexture && materialTextureMetadata.hasNormalTexture && materialTextureMetadata.missingTextureReferences.length === 0 && texcoords.length === positions.length && normals.length === positions.length && skin.joints.length > 0,
|
|
1816
|
+
clips,
|
|
1817
|
+
sample(activeClipId, clipTimeMs) {
|
|
1818
|
+
const clip = clips.find((candidate) => candidate.id === activeClipId) ?? clips[0] ?? null;
|
|
1819
|
+
const overrides = sampleClipOverrides(clip, Math.max(0, clipTimeMs) / 1e3);
|
|
1820
|
+
const worldMatrices = computeWorldMatrices(nodes, overrides);
|
|
1821
|
+
const vertices = skinVertices({
|
|
1822
|
+
positions,
|
|
1823
|
+
joints,
|
|
1824
|
+
weights,
|
|
1825
|
+
skin,
|
|
1826
|
+
inverseBindMatrices,
|
|
1827
|
+
worldMatrices
|
|
1828
|
+
});
|
|
1271
1829
|
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)
|
|
1830
|
+
vertices,
|
|
1831
|
+
indices,
|
|
1832
|
+
bounds: deriveBounds(vertices),
|
|
1833
|
+
bindBounds: bounds,
|
|
1834
|
+
activeClipRenderable: Boolean(clip),
|
|
1835
|
+
activeClipDurationSeconds: clip?.durationSeconds ?? 0,
|
|
1836
|
+
activeClipRootTranslationDistance: clipRootMotionDistances.get(clip?.id) ?? 0
|
|
1303
1837
|
};
|
|
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
1838
|
}
|
|
1317
1839
|
};
|
|
1318
1840
|
}
|
|
1841
|
+
function skinVertices({ positions, joints, weights, skin, inverseBindMatrices, worldMatrices }) {
|
|
1842
|
+
return positions.map((position, vertexIndex) => {
|
|
1843
|
+
const jointIndices = joints[vertexIndex] ?? [0, 0, 0, 0];
|
|
1844
|
+
const jointWeights = weights[vertexIndex] ?? [1, 0, 0, 0];
|
|
1845
|
+
const skinned = [0, 0, 0];
|
|
1846
|
+
for (let influence = 0; influence < 4; influence += 1) {
|
|
1847
|
+
const weight = jointWeights[influence] ?? 0;
|
|
1848
|
+
if (weight <= 0) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const jointNodeIndex = skin.joints[jointIndices[influence] ?? 0] ?? skin.joints[0];
|
|
1852
|
+
const jointMatrix = multiplyMatrix(worldMatrices[jointNodeIndex] ?? identityMatrix(), inverseBindMatrices[jointIndices[influence] ?? 0] ?? identityMatrix());
|
|
1853
|
+
const transformed = transformPoint(jointMatrix, position);
|
|
1854
|
+
skinned[0] += transformed[0] * weight;
|
|
1855
|
+
skinned[1] += transformed[1] * weight;
|
|
1856
|
+
skinned[2] += transformed[2] * weight;
|
|
1857
|
+
}
|
|
1858
|
+
return skinned;
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1319
1861
|
|
|
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
|
-
|
|
1862
|
+
// src/animated-scene-renderer.js
|
|
1863
|
+
var DEFAULT_CAMERA = Object.freeze({
|
|
1864
|
+
mode: "lagged-follow",
|
|
1865
|
+
viewMode: "spectator",
|
|
1866
|
+
cubicBezier: [0.22, 0.61, 0.36, 1],
|
|
1867
|
+
lagMs: 240,
|
|
1868
|
+
lookAheadMs: 320,
|
|
1869
|
+
offset: [0, 2.4, 5.5],
|
|
1870
|
+
constraints: Object.freeze({
|
|
1871
|
+
maxDistance: 10,
|
|
1872
|
+
firstPersonHeadOffset: 0.05
|
|
1873
|
+
}),
|
|
1874
|
+
headLook: Object.freeze({
|
|
1875
|
+
enabled: true,
|
|
1876
|
+
returnMs: 240
|
|
1877
|
+
})
|
|
1878
|
+
});
|
|
1879
|
+
function nowMs() {
|
|
1880
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
1881
|
+
}
|
|
1882
|
+
function clamp012(value) {
|
|
1883
|
+
return Math.min(1, Math.max(0, value));
|
|
1884
|
+
}
|
|
1885
|
+
function lerp2(a, b, t) {
|
|
1886
|
+
return a + (b - a) * t;
|
|
1887
|
+
}
|
|
1888
|
+
function lerp3(a, b, t) {
|
|
1889
|
+
return [
|
|
1890
|
+
lerp2(a[0] ?? 0, b[0] ?? 0, t),
|
|
1891
|
+
lerp2(a[1] ?? 0, b[1] ?? 0, t),
|
|
1892
|
+
lerp2(a[2] ?? 0, b[2] ?? 0, t)
|
|
1893
|
+
];
|
|
1894
|
+
}
|
|
1895
|
+
function normalize3(value, fallback = [0, 0, -1]) {
|
|
1896
|
+
if (!Array.isArray(value) || value.length < 3) {
|
|
1897
|
+
return [...fallback];
|
|
1898
|
+
}
|
|
1899
|
+
const vector = [
|
|
1900
|
+
Number.isFinite(Number(value[0])) ? Number(value[0]) : fallback[0],
|
|
1901
|
+
Number.isFinite(Number(value[1])) ? Number(value[1]) : fallback[1],
|
|
1902
|
+
Number.isFinite(Number(value[2])) ? Number(value[2]) : fallback[2]
|
|
1903
|
+
];
|
|
1904
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
1905
|
+
if (length <= 1e-6) {
|
|
1906
|
+
return [...fallback];
|
|
1907
|
+
}
|
|
1908
|
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
1909
|
+
}
|
|
1910
|
+
function clamp(value, min, max) {
|
|
1911
|
+
return Math.min(max, Math.max(min, value));
|
|
1912
|
+
}
|
|
1913
|
+
function bezierY([, y1, , y2], t) {
|
|
1914
|
+
const u = 1 - t;
|
|
1915
|
+
return 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t;
|
|
1916
|
+
}
|
|
1917
|
+
function durationFromBeats(beats) {
|
|
1918
|
+
return beats.reduce((total, beat) => total + Math.max(0, beat.durationMs ?? 0), 0);
|
|
1919
|
+
}
|
|
1920
|
+
function definedCameraOverrides(camera) {
|
|
1921
|
+
if (!camera || typeof camera !== "object") {
|
|
1922
|
+
return {};
|
|
1923
|
+
}
|
|
1924
|
+
return Object.fromEntries(Object.entries(camera).filter(([, value]) => value !== void 0));
|
|
1925
|
+
}
|
|
1926
|
+
function resolveBeat(beats, loopTimeMs) {
|
|
1927
|
+
let cursor = 0;
|
|
1928
|
+
for (const beat of beats) {
|
|
1929
|
+
const durationMs = Math.max(1, beat.durationMs ?? 1);
|
|
1930
|
+
if (loopTimeMs < cursor + durationMs) {
|
|
1931
|
+
return { beat, beatTimeMs: loopTimeMs - cursor, durationMs };
|
|
1932
|
+
}
|
|
1933
|
+
cursor += durationMs;
|
|
1934
|
+
}
|
|
1935
|
+
const fallback = beats.at(-1);
|
|
1936
|
+
return { beat: fallback, beatTimeMs: fallback?.durationMs ?? 0, durationMs: fallback?.durationMs ?? 1 };
|
|
1937
|
+
}
|
|
1938
|
+
function resolveRoutePosition(route, timeMs) {
|
|
1939
|
+
if (!route.length) {
|
|
1940
|
+
return [0, 0, 0];
|
|
1941
|
+
}
|
|
1942
|
+
if (route.length === 1 || timeMs <= (route[0].arriveMs ?? 0)) {
|
|
1943
|
+
return [...route[0].position];
|
|
1944
|
+
}
|
|
1945
|
+
for (let index = 1; index < route.length; index += 1) {
|
|
1946
|
+
const previous = route[index - 1];
|
|
1947
|
+
const next = route[index];
|
|
1948
|
+
const start = previous.arriveMs ?? 0;
|
|
1949
|
+
const end = next.arriveMs ?? start + 1;
|
|
1950
|
+
if (timeMs <= end) {
|
|
1951
|
+
return lerp3(previous.position, next.position, clamp012((timeMs - start) / Math.max(1, end - start)));
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return [...route.at(-1).position];
|
|
1955
|
+
}
|
|
1956
|
+
function resolveRouteForward(route, timeMs) {
|
|
1957
|
+
if (route.length < 2) {
|
|
1958
|
+
return [0, 0, -1];
|
|
1959
|
+
}
|
|
1960
|
+
let previous = route[0];
|
|
1961
|
+
let next = route[1];
|
|
1962
|
+
for (let index = 1; index < route.length; index += 1) {
|
|
1963
|
+
next = route[index];
|
|
1964
|
+
if (timeMs <= (next.arriveMs ?? 0)) {
|
|
1965
|
+
previous = route[index - 1];
|
|
1966
|
+
break;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return normalize3([
|
|
1970
|
+
(next.position?.[0] ?? 0) - (previous.position?.[0] ?? 0),
|
|
1971
|
+
(next.position?.[1] ?? 0) - (previous.position?.[1] ?? 0),
|
|
1972
|
+
(next.position?.[2] ?? 0) - (previous.position?.[2] ?? 0)
|
|
1973
|
+
]);
|
|
1974
|
+
}
|
|
1975
|
+
function distance3(a = [0, 0, 0], b = [0, 0, 0]) {
|
|
1976
|
+
return Math.hypot(
|
|
1977
|
+
(b[0] ?? 0) - (a[0] ?? 0),
|
|
1978
|
+
(b[1] ?? 0) - (a[1] ?? 0),
|
|
1979
|
+
(b[2] ?? 0) - (a[2] ?? 0)
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
function movementRequirementType(requirement, beat) {
|
|
1983
|
+
if (requirement?.type) {
|
|
1984
|
+
return requirement.type;
|
|
1985
|
+
}
|
|
1986
|
+
if (beat?.kind === "locomotion") {
|
|
1987
|
+
return beat?.rootMotion === "in-place" ? "stationary" : "travel";
|
|
1988
|
+
}
|
|
1989
|
+
if (beat?.kind === "action" || beat?.kind === "idle") {
|
|
1990
|
+
return "stationary";
|
|
1991
|
+
}
|
|
1992
|
+
const clipId = String(beat?.clipId ?? "").toLowerCase();
|
|
1993
|
+
if (/(idle|gesture|dig|plant|water|pick|serve|swing|fire|block|hoe)/iu.test(clipId)) {
|
|
1994
|
+
return "stationary";
|
|
1995
|
+
}
|
|
1996
|
+
if (/(walk|run|jump|climb|crawl|locomotion)/iu.test(clipId)) {
|
|
1997
|
+
return clipId.includes("jump") ? "jump" : "travel";
|
|
1998
|
+
}
|
|
1999
|
+
return beat?.pathPointId ? "travel" : "stationary";
|
|
2000
|
+
}
|
|
2001
|
+
function movementDistancePerLoop(profile = {}) {
|
|
2002
|
+
profile ??= {};
|
|
2003
|
+
const rootDistance = Number(profile.rootTranslationDistance ?? profile.rootTranslation?.distance ?? 0);
|
|
2004
|
+
if (Number.isFinite(rootDistance) && rootDistance > 0) {
|
|
2005
|
+
return rootDistance;
|
|
2006
|
+
}
|
|
2007
|
+
const strideLength = Number(profile.strideLength ?? profile.strideLengthMeters ?? 0);
|
|
2008
|
+
if (Number.isFinite(strideLength) && strideLength > 0) {
|
|
2009
|
+
return strideLength;
|
|
2010
|
+
}
|
|
2011
|
+
return 0;
|
|
2012
|
+
}
|
|
2013
|
+
function clipDurationMs(profile = {}, fallbackMs = 1) {
|
|
2014
|
+
profile ??= {};
|
|
2015
|
+
const duration = Number(profile.durationMs ?? 0);
|
|
2016
|
+
return Number.isFinite(duration) && duration > 0 ? duration : Math.max(1, fallbackMs);
|
|
2017
|
+
}
|
|
2018
|
+
function profileAllowsDisplacement(profile = {}, movementType) {
|
|
2019
|
+
profile ??= {};
|
|
2020
|
+
if (movementType === "stationary") {
|
|
2021
|
+
return profile.worldDisplacementAllowed !== true;
|
|
2022
|
+
}
|
|
2023
|
+
return profile.worldDisplacementAllowed === true || profile.motionMode === "root-authored" || profile.motionMode === "calibrated-in-place" || profile.motionMode === "jump";
|
|
2024
|
+
}
|
|
2025
|
+
function resolveClipProfiles(clipAssets = []) {
|
|
2026
|
+
return new Map(clipAssets.map((clip) => [clip.id, clip.movementProfile ?? clip.profile ?? null]));
|
|
2027
|
+
}
|
|
2028
|
+
function buildBeatTimeline(beats, route, clipProfiles) {
|
|
2029
|
+
const routePoints = new Map(route.map((point) => [point.id, point]));
|
|
2030
|
+
let cursorMs = 0;
|
|
2031
|
+
let routeCursor = 0;
|
|
2032
|
+
let currentPosition = route[0]?.position ? [...route[0].position] : [0, 0, 0];
|
|
2033
|
+
return beats.map((beat) => {
|
|
2034
|
+
const requirement = beat.movementRequirement ?? beat.movement ?? null;
|
|
2035
|
+
const movementType = movementRequirementType(requirement, beat);
|
|
2036
|
+
const movesThroughWorld = movementType === "travel" || movementType === "jump" || movementType === "root-authored";
|
|
2037
|
+
const explicitTargetPoint = beat.pathPointId ? routePoints.get(beat.pathPointId) : null;
|
|
2038
|
+
const inferredTargetPoint = movesThroughWorld ? route[Math.min(route.length - 1, routeCursor + 1)] : null;
|
|
2039
|
+
const targetPoint = explicitTargetPoint ?? inferredTargetPoint ?? null;
|
|
2040
|
+
const targetPosition = targetPoint?.position ? [...targetPoint.position] : [...currentPosition];
|
|
2041
|
+
const startPosition = [...currentPosition];
|
|
2042
|
+
const endPosition = movesThroughWorld ? targetPosition : [...currentPosition];
|
|
2043
|
+
const segmentDistance = Number(requirement?.distance ?? distance3(startPosition, endPosition));
|
|
2044
|
+
const profile = clipProfiles.get(beat.clipId) ?? null;
|
|
2045
|
+
const distancePerLoop = movementDistancePerLoop(profile);
|
|
2046
|
+
const loopCount = movesThroughWorld && distancePerLoop > 0 ? Math.max(1, Math.ceil(segmentDistance / distancePerLoop)) : 1;
|
|
2047
|
+
const derivedDurationMs = movesThroughWorld && distancePerLoop > 0 ? loopCount * clipDurationMs(profile, beat.durationMs) : Math.max(1, beat.durationMs ?? 1);
|
|
2048
|
+
const durationMs = Math.max(1, beat.validatedDurationMs ?? requirement?.validatedDurationMs ?? derivedDurationMs);
|
|
2049
|
+
const expectedSpeed = Number(profile?.expectedSpeed ?? (durationMs > 0 ? segmentDistance / (durationMs / 1e3) : 0));
|
|
2050
|
+
const actualSpeed = durationMs > 0 ? segmentDistance / (durationMs / 1e3) : 0;
|
|
2051
|
+
const warnings = [];
|
|
2052
|
+
if (movesThroughWorld && !profileAllowsDisplacement(profile, movementType)) {
|
|
2053
|
+
warnings.push(`clip '${beat.clipId}' does not allow ${movementType} displacement`);
|
|
2054
|
+
}
|
|
2055
|
+
if (movesThroughWorld && distancePerLoop <= 0 && profile?.motionMode !== "root-authored") {
|
|
2056
|
+
warnings.push(`clip '${beat.clipId}' has no root or calibrated stride distance`);
|
|
2057
|
+
}
|
|
2058
|
+
if (movementType === "stationary" && profile?.worldDisplacementAllowed === true) {
|
|
2059
|
+
warnings.push(`stationary beat '${beat.id}' uses a displacement-capable clip`);
|
|
2060
|
+
}
|
|
2061
|
+
const entry = {
|
|
2062
|
+
beat,
|
|
2063
|
+
startMs: cursorMs,
|
|
2064
|
+
endMs: cursorMs + durationMs,
|
|
2065
|
+
durationMs,
|
|
2066
|
+
startPosition,
|
|
2067
|
+
endPosition,
|
|
2068
|
+
movementType,
|
|
2069
|
+
movementProfile: profile,
|
|
2070
|
+
movementDistance: segmentDistance,
|
|
2071
|
+
distancePerLoop,
|
|
2072
|
+
loopCount,
|
|
2073
|
+
expectedSpeed,
|
|
2074
|
+
actualSpeed,
|
|
2075
|
+
footSlideWarning: Number(profile?.footSlideTolerance ?? 0) < 0 ? "invalid-foot-slide-tolerance" : null,
|
|
2076
|
+
warnings
|
|
2077
|
+
};
|
|
2078
|
+
cursorMs = entry.endMs;
|
|
2079
|
+
currentPosition = [...endPosition];
|
|
2080
|
+
if (targetPoint) {
|
|
2081
|
+
const nextRouteIndex = route.findIndex((point) => point.id === targetPoint.id);
|
|
2082
|
+
if (nextRouteIndex >= 0) {
|
|
2083
|
+
routeCursor = nextRouteIndex;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return entry;
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
function resolveTimelineBeat(timeline, loopTimeMs) {
|
|
2090
|
+
if (!timeline.length) {
|
|
2091
|
+
return null;
|
|
1372
2092
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
2093
|
+
return timeline.find((entry) => loopTimeMs < entry.endMs) ?? timeline.at(-1);
|
|
2094
|
+
}
|
|
2095
|
+
function resolveTimelinePosition(entry, beatTimeMs) {
|
|
2096
|
+
if (!entry) {
|
|
2097
|
+
return [0, 0, 0];
|
|
1375
2098
|
}
|
|
1376
|
-
|
|
2099
|
+
if (entry.movementType !== "travel" && entry.movementType !== "jump" && entry.movementType !== "root-authored") {
|
|
2100
|
+
return [...entry.startPosition];
|
|
2101
|
+
}
|
|
2102
|
+
return lerp3(entry.startPosition, entry.endPosition, clamp012(beatTimeMs / Math.max(1, entry.durationMs)));
|
|
1377
2103
|
}
|
|
1378
|
-
function
|
|
1379
|
-
if (!
|
|
1380
|
-
|
|
2104
|
+
function resolveTimelineForward(entry) {
|
|
2105
|
+
if (!entry) {
|
|
2106
|
+
return [0, 0, -1];
|
|
1381
2107
|
}
|
|
1382
|
-
return
|
|
2108
|
+
return normalize3([
|
|
2109
|
+
(entry.endPosition[0] ?? 0) - (entry.startPosition[0] ?? 0),
|
|
2110
|
+
(entry.endPosition[1] ?? 0) - (entry.startPosition[1] ?? 0),
|
|
2111
|
+
(entry.endPosition[2] ?? 0) - (entry.startPosition[2] ?? 0)
|
|
2112
|
+
]);
|
|
1383
2113
|
}
|
|
1384
|
-
function
|
|
1385
|
-
if (
|
|
1386
|
-
|
|
2114
|
+
function resolveCameraViewMode(camera) {
|
|
2115
|
+
if (camera?.mode === "lagged-follow") {
|
|
2116
|
+
return camera.viewMode ?? "spectator";
|
|
1387
2117
|
}
|
|
1388
|
-
return
|
|
2118
|
+
return camera?.viewMode ?? camera?.mode ?? "spectator";
|
|
1389
2119
|
}
|
|
1390
|
-
function
|
|
1391
|
-
if (typeof
|
|
1392
|
-
|
|
2120
|
+
function resolveCanvas2(canvas) {
|
|
2121
|
+
if (typeof canvas === "string") {
|
|
2122
|
+
const found = globalThis.document?.querySelector?.(canvas);
|
|
2123
|
+
if (!found) {
|
|
2124
|
+
throw new Error(`Animated scene canvas '${canvas}' was not found.`);
|
|
2125
|
+
}
|
|
2126
|
+
return found;
|
|
1393
2127
|
}
|
|
1394
|
-
|
|
2128
|
+
if (!canvas) {
|
|
2129
|
+
throw new Error("Animated scene renderer requires a canvas.");
|
|
2130
|
+
}
|
|
2131
|
+
return canvas;
|
|
1395
2132
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
2133
|
+
function project(position, cameraPosition, width, height) {
|
|
2134
|
+
const x = position[0] - cameraPosition[0];
|
|
2135
|
+
const depth = Math.max(0.2, cameraPosition[2] - (position[2] ?? 0));
|
|
2136
|
+
const scale2 = clamp(34 + 150 / (depth + 1.2), 34, 104);
|
|
2137
|
+
const groundY = height * 0.82 - depth * height * 0.045 - (position[1] ?? 0) * scale2;
|
|
2138
|
+
return {
|
|
2139
|
+
x: width * 0.5 + x * scale2,
|
|
2140
|
+
y: groundY,
|
|
2141
|
+
groundY,
|
|
2142
|
+
scale: scale2,
|
|
2143
|
+
depth,
|
|
2144
|
+
visible: groundY > -height * 0.2 && groundY < height * 1.18
|
|
2145
|
+
};
|
|
1400
2146
|
}
|
|
1401
|
-
function
|
|
1402
|
-
return
|
|
2147
|
+
function propIdentity(prop, index) {
|
|
2148
|
+
return prop.id ?? `${prop.kind}-${index}`;
|
|
1403
2149
|
}
|
|
1404
|
-
function
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
2150
|
+
function drawGroundShadow(ctx, scale2) {
|
|
2151
|
+
ctx.fillStyle = "rgba(44, 38, 28, 0.18)";
|
|
2152
|
+
ctx.fillRect(-scale2 * 0.36, -scale2 * 0.035, scale2 * 0.72, scale2 * 0.07);
|
|
2153
|
+
}
|
|
2154
|
+
function drawProp(ctx, prop, cameraPosition, width, height) {
|
|
2155
|
+
const projected = project(prop.position, cameraPosition, width, height);
|
|
2156
|
+
const { x, y, scale: scale2 } = projected;
|
|
2157
|
+
ctx.save();
|
|
2158
|
+
ctx.translate(x, y);
|
|
2159
|
+
ctx.lineWidth = Math.max(1, scale2 * 0.04);
|
|
2160
|
+
if (prop.kind === "crop-row") {
|
|
2161
|
+
ctx.fillStyle = "#8b633f";
|
|
2162
|
+
ctx.fillRect(-scale2 * 1.05, -scale2 * 0.08, scale2 * 2.1, scale2 * 0.16);
|
|
2163
|
+
ctx.fillStyle = "#6f4a30";
|
|
2164
|
+
ctx.fillRect(-scale2 * 0.92, -scale2 * 0.035, scale2 * 1.84, scale2 * 0.07);
|
|
2165
|
+
ctx.strokeStyle = "#4f6f32";
|
|
2166
|
+
ctx.beginPath();
|
|
2167
|
+
ctx.moveTo(-scale2 * 0.8, 0);
|
|
2168
|
+
ctx.lineTo(scale2 * 0.8, 0);
|
|
2169
|
+
ctx.stroke();
|
|
2170
|
+
ctx.fillStyle = "#6f9f43";
|
|
2171
|
+
for (let i = -3; i <= 3; i += 1) {
|
|
2172
|
+
ctx.fillRect(i * scale2 * 0.22, -scale2 * 0.18, scale2 * 0.06, scale2 * 0.2);
|
|
1421
2173
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2174
|
+
} else if (prop.kind === "fence-segment") {
|
|
2175
|
+
ctx.strokeStyle = "#896b4d";
|
|
2176
|
+
ctx.beginPath();
|
|
2177
|
+
ctx.moveTo(-scale2 * 0.5, 0);
|
|
2178
|
+
ctx.lineTo(scale2 * 0.5, 0);
|
|
2179
|
+
ctx.moveTo(-scale2 * 0.35, -scale2 * 0.25);
|
|
2180
|
+
ctx.lineTo(-scale2 * 0.35, scale2 * 0.1);
|
|
2181
|
+
ctx.moveTo(scale2 * 0.35, -scale2 * 0.25);
|
|
2182
|
+
ctx.lineTo(scale2 * 0.35, scale2 * 0.1);
|
|
2183
|
+
ctx.stroke();
|
|
2184
|
+
} else if (prop.kind === "cart") {
|
|
2185
|
+
drawGroundShadow(ctx, scale2);
|
|
2186
|
+
ctx.fillStyle = "#7b5238";
|
|
2187
|
+
ctx.fillRect(-scale2 * 0.52, -scale2 * 0.38, scale2 * 1.04, scale2 * 0.36);
|
|
2188
|
+
ctx.fillStyle = "#a4774c";
|
|
2189
|
+
ctx.fillRect(-scale2 * 0.44, -scale2 * 0.32, scale2 * 0.88, scale2 * 0.08);
|
|
2190
|
+
ctx.fillStyle = "#25201c";
|
|
2191
|
+
ctx.beginPath();
|
|
2192
|
+
ctx.arc(-scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
2193
|
+
ctx.arc(scale2 * 0.32, scale2 * 0.02, scale2 * 0.1, 0, Math.PI * 2);
|
|
2194
|
+
ctx.fill();
|
|
2195
|
+
} else if (prop.kind === "small-tree") {
|
|
2196
|
+
drawGroundShadow(ctx, scale2);
|
|
2197
|
+
ctx.fillStyle = "#5b3b24";
|
|
2198
|
+
ctx.fillRect(-scale2 * 0.07, -scale2 * 0.56, scale2 * 0.14, scale2 * 0.56);
|
|
2199
|
+
ctx.fillStyle = "#2f6d3b";
|
|
2200
|
+
ctx.beginPath();
|
|
2201
|
+
ctx.arc(-scale2 * 0.16, -scale2 * 0.72, scale2 * 0.25, 0, Math.PI * 2);
|
|
2202
|
+
ctx.fill();
|
|
2203
|
+
ctx.fillStyle = "#3f8746";
|
|
2204
|
+
ctx.beginPath();
|
|
2205
|
+
ctx.arc(scale2 * 0.13, -scale2 * 0.76, scale2 * 0.28, 0, Math.PI * 2);
|
|
2206
|
+
ctx.fill();
|
|
2207
|
+
ctx.fillStyle = "#4f9a50";
|
|
2208
|
+
ctx.beginPath();
|
|
2209
|
+
ctx.arc(0, -scale2 * 0.98, scale2 * 0.26, 0, Math.PI * 2);
|
|
2210
|
+
ctx.fill();
|
|
2211
|
+
} else {
|
|
2212
|
+
drawGroundShadow(ctx, scale2);
|
|
2213
|
+
ctx.fillStyle = prop.kind === "crate" ? "#9a6a3d" : "#d0c39f";
|
|
2214
|
+
ctx.fillRect(-scale2 * 0.2, -scale2 * 0.32, scale2 * 0.4, scale2 * 0.32);
|
|
2215
|
+
if (prop.kind === "crate") {
|
|
2216
|
+
ctx.strokeStyle = "#6e4829";
|
|
2217
|
+
ctx.beginPath();
|
|
2218
|
+
ctx.moveTo(-scale2 * 0.2, -scale2 * 0.16);
|
|
2219
|
+
ctx.lineTo(scale2 * 0.2, -scale2 * 0.16);
|
|
2220
|
+
ctx.moveTo(0, -scale2 * 0.32);
|
|
2221
|
+
ctx.lineTo(0, 0);
|
|
2222
|
+
ctx.stroke();
|
|
1429
2223
|
}
|
|
1430
2224
|
}
|
|
1431
|
-
|
|
2225
|
+
ctx.restore();
|
|
2226
|
+
return projected;
|
|
1432
2227
|
}
|
|
1433
|
-
function
|
|
1434
|
-
|
|
1435
|
-
|
|
2228
|
+
function drawCharacter(ctx, characterPosition, cameraPosition, width, height, gaitPhase, headLook) {
|
|
2229
|
+
const projected = project(characterPosition, cameraPosition, width, height);
|
|
2230
|
+
const { x, y, scale: scale2 } = projected;
|
|
2231
|
+
const stride = Math.sin(gaitPhase * Math.PI * 2);
|
|
2232
|
+
const headYaw = (headLook?.yaw ?? 0) * (headLook?.weight ?? 0);
|
|
2233
|
+
ctx.save();
|
|
2234
|
+
ctx.translate(x, y);
|
|
2235
|
+
ctx.lineWidth = Math.max(2, scale2 * 0.05);
|
|
2236
|
+
ctx.lineCap = "round";
|
|
2237
|
+
drawGroundShadow(ctx, scale2);
|
|
2238
|
+
ctx.strokeStyle = "#4c2f25";
|
|
2239
|
+
ctx.beginPath();
|
|
2240
|
+
ctx.moveTo(-scale2 * 0.12, -scale2 * 0.34);
|
|
2241
|
+
ctx.lineTo(-scale2 * (0.18 + stride * 0.04), -scale2 * 0.02);
|
|
2242
|
+
ctx.moveTo(scale2 * 0.12, -scale2 * 0.34);
|
|
2243
|
+
ctx.lineTo(scale2 * (0.18 - stride * 0.04), -scale2 * 0.02);
|
|
2244
|
+
ctx.stroke();
|
|
2245
|
+
ctx.fillStyle = "#6f8f62";
|
|
2246
|
+
ctx.beginPath();
|
|
2247
|
+
ctx.moveTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
2248
|
+
ctx.lineTo(scale2 * 0.3, -scale2 * 0.86);
|
|
2249
|
+
ctx.lineTo(scale2 * 0.42, -scale2 * 0.24);
|
|
2250
|
+
ctx.lineTo(-scale2 * 0.42, -scale2 * 0.24);
|
|
2251
|
+
ctx.lineTo(-scale2 * 0.3, -scale2 * 0.86);
|
|
2252
|
+
ctx.fill();
|
|
2253
|
+
ctx.fillStyle = "#d8b18c";
|
|
2254
|
+
ctx.beginPath();
|
|
2255
|
+
ctx.arc(0, -scale2 * 1.16, scale2 * 0.18, 0, Math.PI * 2);
|
|
2256
|
+
ctx.fill();
|
|
2257
|
+
ctx.fillStyle = "#5a3a2b";
|
|
2258
|
+
ctx.fillRect(-scale2 * 0.2, -scale2 * 1.28, scale2 * 0.4, scale2 * 0.16);
|
|
2259
|
+
ctx.fillRect(-scale2 * 0.23, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
2260
|
+
ctx.fillRect(scale2 * 0.13, -scale2 * 1.14, scale2 * 0.1, scale2 * 0.28);
|
|
2261
|
+
if (headLook?.status === "active" || headLook?.status === "returning") {
|
|
2262
|
+
ctx.strokeStyle = "rgba(255, 246, 210, 0.74)";
|
|
2263
|
+
ctx.lineWidth = Math.max(1, scale2 * 0.025);
|
|
2264
|
+
ctx.beginPath();
|
|
2265
|
+
ctx.moveTo(0, -scale2 * 1.16);
|
|
2266
|
+
ctx.lineTo(Math.sin(headYaw) * scale2 * 0.32, -scale2 * 1.16 - scale2 * 0.08);
|
|
2267
|
+
ctx.stroke();
|
|
1436
2268
|
}
|
|
1437
|
-
|
|
2269
|
+
ctx.strokeStyle = "#6b4935";
|
|
2270
|
+
ctx.beginPath();
|
|
2271
|
+
ctx.moveTo(-scale2 * 0.24, -scale2 * 0.76);
|
|
2272
|
+
ctx.lineTo(-scale2 * (0.46 + stride * 0.04), -scale2 * (0.5 + stride * 0.06));
|
|
2273
|
+
ctx.moveTo(scale2 * 0.24, -scale2 * 0.76);
|
|
2274
|
+
ctx.lineTo(scale2 * (0.46 - stride * 0.04), -scale2 * (0.5 - stride * 0.06));
|
|
2275
|
+
ctx.stroke();
|
|
2276
|
+
ctx.fillStyle = "#d6c09a";
|
|
2277
|
+
ctx.fillRect(-scale2 * 0.16, -scale2 * 0.86, scale2 * 0.32, scale2 * 0.18);
|
|
2278
|
+
ctx.restore();
|
|
2279
|
+
return projected;
|
|
1438
2280
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2281
|
+
function pointForModelVertex(vertex, projection, sample) {
|
|
2282
|
+
const bounds = sample.bounds;
|
|
2283
|
+
const height = Math.max(0.1, bounds.size[1] || sample.bindBounds.size[1] || 1);
|
|
2284
|
+
const modelScale = projection.scale * 1.3 / height;
|
|
2285
|
+
const centerX = (bounds.min[0] + bounds.max[0]) * 0.5;
|
|
2286
|
+
const centerZ = (bounds.min[2] + bounds.max[2]) * 0.5;
|
|
2287
|
+
return [
|
|
2288
|
+
projection.x + (vertex[0] - centerX) * modelScale,
|
|
2289
|
+
projection.groundY - (vertex[1] - bounds.min[1]) * modelScale + (vertex[2] - centerZ) * modelScale * 0.16
|
|
2290
|
+
];
|
|
1444
2291
|
}
|
|
1445
|
-
function
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
} = options;
|
|
1451
|
-
if (typeof getTargetFrameTimeMs === "function") {
|
|
1452
|
-
const resolved = getTargetFrameTimeMs(event);
|
|
1453
|
-
return readPositiveNumber("getTargetFrameTimeMs()", resolved);
|
|
2292
|
+
function modelFillForHeight(vertex, sample) {
|
|
2293
|
+
const height = Math.max(0.1, sample.bounds.size[1] || 1);
|
|
2294
|
+
const t = (vertex[1] - sample.bounds.min[1]) / height;
|
|
2295
|
+
if (t > 0.82) {
|
|
2296
|
+
return "#6d4a36";
|
|
1454
2297
|
}
|
|
1455
|
-
if (
|
|
1456
|
-
return
|
|
2298
|
+
if (t > 0.58) {
|
|
2299
|
+
return "#d7b28f";
|
|
1457
2300
|
}
|
|
1458
|
-
if (
|
|
1459
|
-
return
|
|
2301
|
+
if (t > 0.25) {
|
|
2302
|
+
return "#6f8f62";
|
|
1460
2303
|
}
|
|
1461
|
-
return
|
|
2304
|
+
return "#5e4636";
|
|
1462
2305
|
}
|
|
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
|
-
);
|
|
2306
|
+
function drawSkinnedGltfCharacter(ctx, model, snapshot, width, height) {
|
|
2307
|
+
const projection = project(snapshot.characterPosition, snapshot.cameraPosition, width, height);
|
|
2308
|
+
const sample = model.sample(snapshot.activeClipId, snapshot.clipTimeMs);
|
|
2309
|
+
const maxTriangles = 12e3;
|
|
2310
|
+
const triangleCount = Math.floor(sample.indices.length / 3);
|
|
2311
|
+
const stride = Math.max(1, Math.ceil(triangleCount / maxTriangles));
|
|
2312
|
+
const triangles = [];
|
|
2313
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += stride) {
|
|
2314
|
+
const base = triangleIndex * 3;
|
|
2315
|
+
const a = sample.vertices[sample.indices[base]];
|
|
2316
|
+
const b = sample.vertices[sample.indices[base + 1]];
|
|
2317
|
+
const c = sample.vertices[sample.indices[base + 2]];
|
|
2318
|
+
if (!a || !b || !c) {
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
triangles.push({
|
|
2322
|
+
a,
|
|
2323
|
+
b,
|
|
2324
|
+
c,
|
|
2325
|
+
depth: (a[2] + b[2] + c[2]) / 3,
|
|
2326
|
+
fill: modelFillForHeight(a, sample)
|
|
2327
|
+
});
|
|
1489
2328
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
2329
|
+
triangles.sort((left, right) => left.depth - right.depth);
|
|
2330
|
+
ctx.save();
|
|
2331
|
+
drawGroundShadow(ctx, projection.scale);
|
|
2332
|
+
for (const triangle of triangles) {
|
|
2333
|
+
const [ax, ay] = pointForModelVertex(triangle.a, projection, sample);
|
|
2334
|
+
const [bx, by] = pointForModelVertex(triangle.b, projection, sample);
|
|
2335
|
+
const [cx, cy] = pointForModelVertex(triangle.c, projection, sample);
|
|
2336
|
+
ctx.fillStyle = triangle.fill;
|
|
2337
|
+
ctx.beginPath();
|
|
2338
|
+
ctx.moveTo(ax, ay);
|
|
2339
|
+
ctx.lineTo(bx, by);
|
|
2340
|
+
ctx.lineTo(cx, cy);
|
|
2341
|
+
ctx.closePath?.();
|
|
2342
|
+
ctx.fill();
|
|
1492
2343
|
}
|
|
1493
|
-
|
|
1494
|
-
targetFrameTimeMs: fixedTargetFrameTimeMs,
|
|
1495
|
-
targetFrameRate: fixedTargetFrameRate,
|
|
1496
|
-
getTargetFrameTimeMs
|
|
1497
|
-
};
|
|
2344
|
+
ctx.restore();
|
|
1498
2345
|
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
|
-
}
|
|
2346
|
+
projection,
|
|
2347
|
+
sample
|
|
1527
2348
|
};
|
|
1528
2349
|
}
|
|
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.");
|
|
2350
|
+
function renderScene(canvas, ctx, snapshot, props, characterModel) {
|
|
2351
|
+
const width = canvas.width || 1;
|
|
2352
|
+
const height = canvas.height || 1;
|
|
2353
|
+
ctx.clearRect(0, 0, width, height);
|
|
2354
|
+
ctx.fillStyle = "#d8e8d0";
|
|
2355
|
+
ctx.fillRect(0, 0, width, height);
|
|
2356
|
+
ctx.fillStyle = "#9fc47a";
|
|
2357
|
+
ctx.fillRect(0, height * 0.52, width, height * 0.48);
|
|
2358
|
+
ctx.strokeStyle = "#caa66d";
|
|
2359
|
+
ctx.lineWidth = Math.max(2, width * 6e-3);
|
|
2360
|
+
ctx.beginPath();
|
|
2361
|
+
ctx.moveTo(width * 0.35, height);
|
|
2362
|
+
ctx.quadraticCurveTo(width * 0.53, height * 0.58, width * 0.62, 0);
|
|
2363
|
+
ctx.stroke();
|
|
2364
|
+
const projectedProps = props.map((prop, index) => ({
|
|
2365
|
+
prop,
|
|
2366
|
+
id: propIdentity(prop, index),
|
|
2367
|
+
projected: project(prop.position, snapshot.cameraPosition, width, height)
|
|
2368
|
+
})).sort((a, b) => b.projected.depth - a.projected.depth);
|
|
2369
|
+
const propGroundAnchors = [];
|
|
2370
|
+
for (const { prop, id } of projectedProps) {
|
|
2371
|
+
const projected = drawProp(ctx, prop, snapshot.cameraPosition, width, height);
|
|
2372
|
+
propGroundAnchors.push({
|
|
2373
|
+
id,
|
|
2374
|
+
kind: prop.kind,
|
|
2375
|
+
groundY: projected.groundY,
|
|
2376
|
+
depth: projected.depth,
|
|
2377
|
+
visible: projected.visible
|
|
2378
|
+
});
|
|
1560
2379
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2380
|
+
let characterProjection;
|
|
2381
|
+
let modelRenderable = false;
|
|
2382
|
+
let fallbackProxyActive = true;
|
|
2383
|
+
let skinnedVertexCount = 0;
|
|
2384
|
+
let skinnedTriangleCount = 0;
|
|
2385
|
+
let skinnedJointCount = 0;
|
|
2386
|
+
let skinnedAnimatedNodeCount = 0;
|
|
2387
|
+
let skinnedClipCount = 0;
|
|
2388
|
+
let activeClipRenderable = false;
|
|
2389
|
+
if (characterModel) {
|
|
2390
|
+
const drawn = drawSkinnedGltfCharacter(ctx, characterModel, snapshot, width, height);
|
|
2391
|
+
characterProjection = drawn.projection;
|
|
2392
|
+
modelRenderable = true;
|
|
2393
|
+
fallbackProxyActive = false;
|
|
2394
|
+
skinnedVertexCount = characterModel.vertexCount;
|
|
2395
|
+
skinnedTriangleCount = characterModel.triangleCount;
|
|
2396
|
+
skinnedJointCount = characterModel.jointCount;
|
|
2397
|
+
skinnedAnimatedNodeCount = characterModel.animatedNodeCount;
|
|
2398
|
+
skinnedClipCount = characterModel.clipCount;
|
|
2399
|
+
activeClipRenderable = drawn.sample.activeClipRenderable;
|
|
2400
|
+
} else {
|
|
2401
|
+
characterProjection = drawCharacter(
|
|
2402
|
+
ctx,
|
|
2403
|
+
snapshot.characterPosition,
|
|
2404
|
+
snapshot.cameraPosition,
|
|
2405
|
+
width,
|
|
2406
|
+
height,
|
|
2407
|
+
snapshot.clipTimeMs / 900,
|
|
2408
|
+
snapshot.headLook
|
|
2409
|
+
);
|
|
1566
2410
|
}
|
|
1567
|
-
context.configure({
|
|
1568
|
-
device,
|
|
1569
|
-
format,
|
|
1570
|
-
alphaMode
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
function createRenderPassDescriptor(view, clearColor) {
|
|
1574
2411
|
return {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
]
|
|
2412
|
+
characterGroundY: characterProjection.groundY,
|
|
2413
|
+
characterVisible: characterProjection.visible,
|
|
2414
|
+
modelLoaded: Boolean(characterModel),
|
|
2415
|
+
modelRenderable,
|
|
2416
|
+
fallbackProxyActive,
|
|
2417
|
+
skinnedVertexCount,
|
|
2418
|
+
skinnedTriangleCount,
|
|
2419
|
+
skinnedJointCount,
|
|
2420
|
+
skinnedAnimatedNodeCount,
|
|
2421
|
+
skinnedClipCount,
|
|
2422
|
+
activeClipRenderable,
|
|
2423
|
+
propGroundAnchors
|
|
1588
2424
|
};
|
|
1589
2425
|
}
|
|
1590
|
-
function
|
|
2426
|
+
function resolveCharacterModel(options) {
|
|
1591
2427
|
try {
|
|
1592
|
-
|
|
1593
|
-
|
|
2428
|
+
return createAnimatedGltfModel(
|
|
2429
|
+
options.modelAsset ?? options.animationAdventure?.modelAsset,
|
|
2430
|
+
options.clipAssets ?? options.animationAdventure?.clipAssets ?? []
|
|
2431
|
+
);
|
|
1594
2432
|
} catch {
|
|
1595
|
-
return
|
|
2433
|
+
return null;
|
|
1596
2434
|
}
|
|
1597
2435
|
}
|
|
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.");
|
|
2436
|
+
function createAnimatedSceneRenderer(options = {}) {
|
|
2437
|
+
const canvas = resolveCanvas2(options.canvas);
|
|
2438
|
+
const ctx = canvas.getContext?.("2d");
|
|
2439
|
+
if (!ctx) {
|
|
2440
|
+
throw new Error("Animated scene renderer requires a 2D canvas context.");
|
|
1625
2441
|
}
|
|
1626
|
-
const
|
|
1627
|
-
|
|
2442
|
+
const route = [...options.route ?? options.animationAdventure?.route ?? []];
|
|
2443
|
+
const beats = [...options.beats ?? options.animationAdventure?.beats ?? []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
2444
|
+
const props = [...options.props ?? options.animationAdventure?.props ?? []];
|
|
2445
|
+
const clipAssets = options.clipAssets ?? options.animationAdventure?.clipAssets ?? [];
|
|
2446
|
+
const clipProfiles = resolveClipProfiles(clipAssets);
|
|
2447
|
+
const beatTimeline = buildBeatTimeline(beats, route, clipProfiles);
|
|
2448
|
+
const characterModel = resolveCharacterModel(options);
|
|
2449
|
+
let camera = {
|
|
2450
|
+
...DEFAULT_CAMERA,
|
|
2451
|
+
...definedCameraOverrides(options.camera ?? options.animationAdventure?.camera)
|
|
2452
|
+
};
|
|
2453
|
+
const loopDurationMs = Math.max(beatTimeline.at(-1)?.endMs ?? durationFromBeats(beats), 1);
|
|
2454
|
+
const movementWarnings = beatTimeline.flatMap((entry) => entry.warnings);
|
|
2455
|
+
const requestFrame = options.requestAnimationFrame ?? globalThis.requestAnimationFrame?.bind(globalThis);
|
|
2456
|
+
const cancelFrame = options.cancelAnimationFrame ?? globalThis.cancelAnimationFrame?.bind(globalThis);
|
|
2457
|
+
let frameHandle;
|
|
1628
2458
|
let running = false;
|
|
1629
|
-
let destroyed = false;
|
|
1630
2459
|
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
|
-
}
|
|
2460
|
+
let startedAtMs;
|
|
2461
|
+
let lastTimestamp;
|
|
2462
|
+
let pendingCameraControl = null;
|
|
2463
|
+
let activeCameraControl = false;
|
|
2464
|
+
let headLookWeight = 0;
|
|
2465
|
+
let cameraPosition = route[0] ? [
|
|
2466
|
+
route[0].position[0] + (camera.offset?.[0] ?? 0),
|
|
2467
|
+
route[0].position[1] + (camera.offset?.[1] ?? 0),
|
|
2468
|
+
route[0].position[2] + (camera.offset?.[2] ?? 0)
|
|
2469
|
+
] : [0, 2.4, 5.5];
|
|
2470
|
+
let cameraRigFrame = resolveCameraRigFrame({
|
|
2471
|
+
viewMode: "editor",
|
|
2472
|
+
anchors: {
|
|
2473
|
+
target: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
2474
|
+
head: route[0]?.position ? [route[0].position[0], route[0].position[1] + 1.65, route[0].position[2]] : [0, 1.65, 0],
|
|
2475
|
+
forward: [0, 0, -1]
|
|
2476
|
+
},
|
|
2477
|
+
camera: {
|
|
2478
|
+
id: "animation-adventure",
|
|
2479
|
+
transform: {
|
|
2480
|
+
position: [...cameraPosition],
|
|
2481
|
+
target: route[0]?.position ? [...route[0].position] : [0, 0, 0]
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
constraints: camera.constraints
|
|
2485
|
+
});
|
|
2486
|
+
let snapshot = {
|
|
2487
|
+
frame,
|
|
2488
|
+
running,
|
|
2489
|
+
activeClipId: beats[0]?.clipId ?? "",
|
|
2490
|
+
activeBeatId: beats[0]?.id ?? "",
|
|
2491
|
+
activeMovementMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
2492
|
+
blendProgress: 0,
|
|
2493
|
+
clipTimeMs: 0,
|
|
2494
|
+
characterPosition: route[0]?.position ? [...route[0].position] : [0, 0, 0],
|
|
2495
|
+
cameraPosition: [...cameraPosition],
|
|
2496
|
+
cameraViewMode: resolveCameraViewMode(camera),
|
|
2497
|
+
cameraTransform: cameraRigFrame.transform,
|
|
2498
|
+
targetDistance: cameraRigFrame.targetDistance,
|
|
2499
|
+
headLook: cameraRigFrame.headLook,
|
|
2500
|
+
characterGroundY: 0,
|
|
2501
|
+
characterVisible: false,
|
|
2502
|
+
modelLoaded: Boolean(characterModel),
|
|
2503
|
+
modelRenderable: false,
|
|
2504
|
+
fallbackProxyActive: true,
|
|
2505
|
+
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
2506
|
+
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
2507
|
+
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
2508
|
+
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
2509
|
+
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
2510
|
+
activeClipRenderable: false,
|
|
2511
|
+
propGroundAnchors: [],
|
|
2512
|
+
movementValidation: {
|
|
2513
|
+
status: movementWarnings.length ? "warning" : "passed",
|
|
2514
|
+
warnings: movementWarnings,
|
|
2515
|
+
activeBeatId: beats[0]?.id ?? "",
|
|
2516
|
+
activeClipId: beats[0]?.clipId ?? "",
|
|
2517
|
+
motionMode: beatTimeline[0]?.movementType ?? "stationary",
|
|
2518
|
+
rootMotionSource: beatTimeline[0]?.movementProfile?.motionMode ?? "none",
|
|
2519
|
+
expectedSpeed: beatTimeline[0]?.expectedSpeed ?? 0,
|
|
2520
|
+
actualSpeed: beatTimeline[0]?.actualSpeed ?? 0,
|
|
2521
|
+
movementDistance: beatTimeline[0]?.movementDistance ?? 0,
|
|
2522
|
+
loopCount: beatTimeline[0]?.loopCount ?? 1,
|
|
2523
|
+
footSlideWarning: beatTimeline[0]?.footSlideWarning ?? null
|
|
2524
|
+
},
|
|
2525
|
+
frameState: "initialized"
|
|
1733
2526
|
};
|
|
1734
|
-
|
|
1735
|
-
if (
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
if (running) {
|
|
1739
|
-
return false;
|
|
2527
|
+
function renderOnce(timestamp = nowMs()) {
|
|
2528
|
+
if (startedAtMs === void 0) {
|
|
2529
|
+
startedAtMs = timestamp;
|
|
1740
2530
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2531
|
+
const elapsedMs = Math.max(0, timestamp - startedAtMs);
|
|
2532
|
+
const loopTimeMs = elapsedMs % loopDurationMs;
|
|
2533
|
+
const frameTimeMs = lastTimestamp === void 0 ? 16.67 : Math.max(0, timestamp - lastTimestamp);
|
|
2534
|
+
lastTimestamp = timestamp;
|
|
2535
|
+
frame += 1;
|
|
2536
|
+
const timelineEntry = resolveTimelineBeat(beatTimeline, loopTimeMs);
|
|
2537
|
+
const fallbackBeat = resolveBeat(beats, loopTimeMs);
|
|
2538
|
+
const beat = timelineEntry?.beat ?? fallbackBeat.beat;
|
|
2539
|
+
const beatTimeMs = timelineEntry ? loopTimeMs - timelineEntry.startMs : fallbackBeat.beatTimeMs;
|
|
2540
|
+
const durationMs = timelineEntry?.durationMs ?? fallbackBeat.durationMs;
|
|
2541
|
+
const blend = beat?.blend ?? { inMs: 0, outMs: 0 };
|
|
2542
|
+
const blendIn = blend.inMs > 0 ? clamp012(beatTimeMs / blend.inMs) : 1;
|
|
2543
|
+
const blendOut = blend.outMs > 0 ? clamp012((durationMs - beatTimeMs) / blend.outMs) : 1;
|
|
2544
|
+
const blendProgress = clamp012(Math.min(blendIn, blendOut));
|
|
2545
|
+
const characterPosition = timelineEntry ? resolveTimelinePosition(timelineEntry, beatTimeMs) : resolveRoutePosition(route, loopTimeMs);
|
|
2546
|
+
const characterForward = timelineEntry ? resolveTimelineForward(timelineEntry) : resolveRouteForward(route, loopTimeMs);
|
|
2547
|
+
const headAnchor = camera.headBoneAvailable === false ? void 0 : [
|
|
2548
|
+
characterPosition[0],
|
|
2549
|
+
characterPosition[1] + (camera.headHeight ?? 1.65),
|
|
2550
|
+
characterPosition[2]
|
|
2551
|
+
];
|
|
2552
|
+
const lookAheadTimeMs = (loopTimeMs + camera.lookAheadMs) % loopDurationMs;
|
|
2553
|
+
const lookAheadEntry = resolveTimelineBeat(beatTimeline, lookAheadTimeMs);
|
|
2554
|
+
const lookAheadPosition = lookAheadEntry ? resolveTimelinePosition(lookAheadEntry, lookAheadTimeMs - lookAheadEntry.startMs) : resolveRoutePosition(route, lookAheadTimeMs);
|
|
2555
|
+
const desiredCamera = [
|
|
2556
|
+
lookAheadPosition[0] + (camera.offset?.[0] ?? 0),
|
|
2557
|
+
lookAheadPosition[1] + (camera.offset?.[1] ?? 0),
|
|
2558
|
+
lookAheadPosition[2] + (camera.offset?.[2] ?? 0)
|
|
2559
|
+
];
|
|
2560
|
+
const smoothing = bezierY(camera.cubicBezier, clamp012(frameTimeMs / Math.max(1, camera.lagMs)));
|
|
2561
|
+
cameraPosition = lerp3(cameraPosition, desiredCamera, smoothing);
|
|
2562
|
+
const cameraViewMode = resolveCameraViewMode(camera);
|
|
2563
|
+
const rigViewMode = cameraViewMode === "spectator" ? "editor" : cameraViewMode;
|
|
2564
|
+
cameraRigFrame = resolveCameraRigFrame({
|
|
2565
|
+
viewMode: rigViewMode,
|
|
2566
|
+
anchors: {
|
|
2567
|
+
target: characterPosition,
|
|
2568
|
+
...headAnchor ? { head: headAnchor } : {},
|
|
2569
|
+
forward: characterForward
|
|
2570
|
+
},
|
|
2571
|
+
camera: {
|
|
2572
|
+
id: "animation-adventure",
|
|
2573
|
+
transform: {
|
|
2574
|
+
position: [...cameraPosition],
|
|
2575
|
+
target: lookAheadPosition
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
constraints: camera.constraints,
|
|
2579
|
+
control: pendingCameraControl,
|
|
2580
|
+
activeControl: activeCameraControl
|
|
2581
|
+
});
|
|
2582
|
+
if (activeCameraControl && cameraRigFrame.headLook.status === "active") {
|
|
2583
|
+
headLookWeight = cameraRigFrame.headLook.weight;
|
|
1744
2584
|
} else {
|
|
1745
|
-
|
|
2585
|
+
const returnMs = Math.max(1, camera.headLook?.returnMs ?? DEFAULT_CAMERA.headLook.returnMs);
|
|
2586
|
+
headLookWeight = lerp2(headLookWeight, 0, clamp012(frameTimeMs / returnMs));
|
|
1746
2587
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2588
|
+
const headLook = {
|
|
2589
|
+
...cameraRigFrame.headLook,
|
|
2590
|
+
status: cameraRigFrame.headLook.status === "unavailable" ? "unavailable" : headLookWeight > 1e-3 && !activeCameraControl ? "returning" : cameraRigFrame.headLook.status,
|
|
2591
|
+
weight: headLookWeight
|
|
2592
|
+
};
|
|
2593
|
+
cameraPosition = [...cameraRigFrame.transform.position];
|
|
2594
|
+
pendingCameraControl = null;
|
|
2595
|
+
activeCameraControl = false;
|
|
2596
|
+
snapshot = {
|
|
2597
|
+
frame,
|
|
2598
|
+
running,
|
|
2599
|
+
activeClipId: beat?.clipId ?? "",
|
|
2600
|
+
activeBeatId: beat?.id ?? "",
|
|
2601
|
+
activeMovementMode: timelineEntry?.movementType ?? "stationary",
|
|
2602
|
+
blendProgress,
|
|
2603
|
+
clipTimeMs: beatTimeMs,
|
|
2604
|
+
characterPosition,
|
|
2605
|
+
cameraPosition: [...cameraPosition],
|
|
2606
|
+
cameraViewMode,
|
|
2607
|
+
cameraTransform: cameraRigFrame.transform,
|
|
2608
|
+
targetDistance: cameraRigFrame.targetDistance,
|
|
2609
|
+
headLook,
|
|
2610
|
+
characterGroundY: 0,
|
|
2611
|
+
characterVisible: false,
|
|
2612
|
+
modelLoaded: Boolean(characterModel),
|
|
2613
|
+
modelRenderable: false,
|
|
2614
|
+
fallbackProxyActive: true,
|
|
2615
|
+
skinnedVertexCount: characterModel?.vertexCount ?? 0,
|
|
2616
|
+
skinnedTriangleCount: characterModel?.triangleCount ?? 0,
|
|
2617
|
+
skinnedJointCount: characterModel?.jointCount ?? 0,
|
|
2618
|
+
skinnedAnimatedNodeCount: characterModel?.animatedNodeCount ?? 0,
|
|
2619
|
+
skinnedClipCount: characterModel?.clipCount ?? 0,
|
|
2620
|
+
activeClipRenderable: false,
|
|
2621
|
+
propGroundAnchors: [],
|
|
2622
|
+
movementValidation: {
|
|
2623
|
+
status: movementWarnings.length ? "warning" : "passed",
|
|
2624
|
+
warnings: [...movementWarnings],
|
|
2625
|
+
activeBeatId: beat?.id ?? "",
|
|
2626
|
+
activeClipId: beat?.clipId ?? "",
|
|
2627
|
+
motionMode: timelineEntry?.movementType ?? "stationary",
|
|
2628
|
+
rootMotionSource: timelineEntry?.movementProfile?.motionMode ?? "none",
|
|
2629
|
+
expectedSpeed: timelineEntry?.expectedSpeed ?? 0,
|
|
2630
|
+
actualSpeed: timelineEntry?.actualSpeed ?? 0,
|
|
2631
|
+
movementDistance: timelineEntry?.movementDistance ?? 0,
|
|
2632
|
+
loopCount: timelineEntry?.loopCount ?? 1,
|
|
2633
|
+
footSlideWarning: timelineEntry?.footSlideWarning ?? null
|
|
2634
|
+
},
|
|
2635
|
+
frameState: running ? "running" : "rendered-once"
|
|
2636
|
+
};
|
|
2637
|
+
const visibility = renderScene(canvas, ctx, snapshot, props, characterModel);
|
|
2638
|
+
snapshot = {
|
|
2639
|
+
...snapshot,
|
|
2640
|
+
...visibility
|
|
2641
|
+
};
|
|
2642
|
+
return snapshot;
|
|
2643
|
+
}
|
|
2644
|
+
function tick(timestamp) {
|
|
1750
2645
|
if (!running) {
|
|
1751
|
-
return
|
|
1752
|
-
}
|
|
1753
|
-
running = false;
|
|
1754
|
-
if (rafId !== null && typeof cancelAnimationFrame === "function") {
|
|
1755
|
-
cancelAnimationFrame(rafId);
|
|
2646
|
+
return;
|
|
1756
2647
|
}
|
|
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`;
|
|
2648
|
+
renderOnce(timestamp);
|
|
2649
|
+
if (running && requestFrame) {
|
|
2650
|
+
frameHandle = requestFrame(tick);
|
|
1768
2651
|
}
|
|
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();
|
|
2652
|
+
}
|
|
2653
|
+
return {
|
|
2654
|
+
start() {
|
|
2655
|
+
if (running) {
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
running = true;
|
|
2659
|
+
snapshot = { ...snapshot, running: true, frameState: "running" };
|
|
2660
|
+
if (requestFrame) {
|
|
2661
|
+
frameHandle = requestFrame(tick);
|
|
2662
|
+
} else {
|
|
2663
|
+
renderOnce();
|
|
1806
2664
|
}
|
|
1807
|
-
detachXrBinding = bindRendererToXrManager(renderer, xrManager, bindOptions);
|
|
1808
|
-
return detachXrBinding;
|
|
1809
2665
|
},
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2666
|
+
resize(width, height, devicePixelRatio = 1) {
|
|
2667
|
+
const ratio = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
2668
|
+
canvas.width = Math.max(1, Math.floor(width * ratio));
|
|
2669
|
+
canvas.height = Math.max(1, Math.floor(height * ratio));
|
|
2670
|
+
if (canvas.style) {
|
|
2671
|
+
canvas.style.width = `${width}px`;
|
|
2672
|
+
canvas.style.height = `${height}px`;
|
|
1816
2673
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
2674
|
+
const visibility = renderScene(canvas, ctx, snapshot, props, characterModel);
|
|
2675
|
+
snapshot = {
|
|
2676
|
+
...snapshot,
|
|
2677
|
+
...visibility
|
|
2678
|
+
};
|
|
2679
|
+
},
|
|
2680
|
+
renderOnce,
|
|
2681
|
+
getSnapshot() {
|
|
2682
|
+
return {
|
|
2683
|
+
...snapshot,
|
|
2684
|
+
characterPosition: [...snapshot.characterPosition],
|
|
2685
|
+
cameraPosition: [...snapshot.cameraPosition],
|
|
2686
|
+
cameraTransform: {
|
|
2687
|
+
position: [...snapshot.cameraTransform.position],
|
|
2688
|
+
target: [...snapshot.cameraTransform.target],
|
|
2689
|
+
up: [...snapshot.cameraTransform.up]
|
|
2690
|
+
},
|
|
2691
|
+
headLook: {
|
|
2692
|
+
...snapshot.headLook,
|
|
2693
|
+
target: [...snapshot.headLook.target]
|
|
2694
|
+
},
|
|
2695
|
+
movementValidation: {
|
|
2696
|
+
...snapshot.movementValidation,
|
|
2697
|
+
warnings: [...snapshot.movementValidation?.warnings ?? []]
|
|
2698
|
+
},
|
|
2699
|
+
propGroundAnchors: snapshot.propGroundAnchors.map((anchor) => ({ ...anchor }))
|
|
2700
|
+
};
|
|
2701
|
+
},
|
|
2702
|
+
setCamera(nextCamera = {}) {
|
|
2703
|
+
camera = { ...camera, ...nextCamera };
|
|
2704
|
+
snapshot = {
|
|
2705
|
+
...snapshot,
|
|
2706
|
+
cameraViewMode: resolveCameraViewMode(camera)
|
|
2707
|
+
};
|
|
2708
|
+
},
|
|
2709
|
+
setCameraViewMode(viewMode) {
|
|
2710
|
+
camera = { ...camera, viewMode };
|
|
2711
|
+
snapshot = {
|
|
2712
|
+
...snapshot,
|
|
2713
|
+
cameraViewMode: resolveCameraViewMode(camera)
|
|
2714
|
+
};
|
|
2715
|
+
},
|
|
2716
|
+
applyCameraControl(control, options2 = {}) {
|
|
2717
|
+
pendingCameraControl = control;
|
|
2718
|
+
activeCameraControl = options2.activeControl !== false;
|
|
2719
|
+
},
|
|
2720
|
+
destroy() {
|
|
2721
|
+
running = false;
|
|
2722
|
+
if (frameHandle !== void 0 && cancelFrame) {
|
|
2723
|
+
cancelFrame(frameHandle);
|
|
1819
2724
|
}
|
|
2725
|
+
frameHandle = void 0;
|
|
2726
|
+
snapshot = { ...snapshot, running: false, frameState: "destroyed" };
|
|
1820
2727
|
}
|
|
1821
2728
|
};
|
|
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
2729
|
}
|
|
1858
|
-
var defaultRendererClearColor = DEFAULT_CLEAR_COLOR;
|
|
1859
2730
|
|
|
1860
2731
|
// src/professional-animated-scene-renderer.js
|
|
1861
2732
|
function nowMs2() {
|
|
@@ -2965,7 +3836,7 @@ function scale(a, scalar) {
|
|
|
2965
3836
|
function dot(a, b) {
|
|
2966
3837
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
2967
3838
|
}
|
|
2968
|
-
function
|
|
3839
|
+
function cross2(a, b) {
|
|
2969
3840
|
return [
|
|
2970
3841
|
a[1] * b[2] - a[2] * b[1],
|
|
2971
3842
|
a[2] * b[0] - a[0] * b[2],
|
|
@@ -3586,7 +4457,7 @@ function createMeshTriangleRecords(meshes, gpuMaterialSource = null) {
|
|
|
3586
4457
|
const v0 = readVector(mesh.positions, a, 3, [0, 0, 0]);
|
|
3587
4458
|
const v1 = readVector(mesh.positions, b, 3, [0, 0, 0]);
|
|
3588
4459
|
const v2 = readVector(mesh.positions, c, 3, [0, 0, 0]);
|
|
3589
|
-
const faceNormal = normalize(
|
|
4460
|
+
const faceNormal = normalize(cross2(subtract(v1, v0), subtract(v2, v0)), [0, 1, 0]);
|
|
3590
4461
|
const n0 = mesh.normals ? normalize(readVector(mesh.normals, a, 3, faceNormal), faceNormal) : faceNormal;
|
|
3591
4462
|
const n1 = mesh.normals ? normalize(readVector(mesh.normals, b, 3, faceNormal), faceNormal) : faceNormal;
|
|
3592
4463
|
const n2 = mesh.normals ? normalize(readVector(mesh.normals, c, 3, faceNormal), faceNormal) : faceNormal;
|
|
@@ -4627,9 +5498,9 @@ function resolveEnvironmentPortalMode(value, hasPortals) {
|
|
|
4627
5498
|
}
|
|
4628
5499
|
function orthogonalPortalTangent(normal) {
|
|
4629
5500
|
if (Math.abs(normal[1]) < 0.92) {
|
|
4630
|
-
return normalize(
|
|
5501
|
+
return normalize(cross2([0, 1, 0], normal), [1, 0, 0]);
|
|
4631
5502
|
}
|
|
4632
|
-
return normalize(
|
|
5503
|
+
return normalize(cross2([1, 0, 0], normal), [0, 0, 1]);
|
|
4633
5504
|
}
|
|
4634
5505
|
function resolvePortalTangent(value, normal) {
|
|
4635
5506
|
const fallback = orthogonalPortalTangent(normal);
|
|
@@ -4661,7 +5532,7 @@ function normalizeEnvironmentPortal(portal, index) {
|
|
|
4661
5532
|
const position = asVec3(portal.position ?? portal.center, [0, 0, 0]);
|
|
4662
5533
|
const normal = asUnitVec3(portal.normal, [0, 0, 1]);
|
|
4663
5534
|
const tangent = resolvePortalTangent(portal.tangent, normal);
|
|
4664
|
-
const bitangent = normalize(
|
|
5535
|
+
const bitangent = normalize(cross2(normal, tangent), [0, 1, 0]);
|
|
4665
5536
|
const width = readPortalExtent(
|
|
4666
5537
|
`environmentPortals[${index}].width`,
|
|
4667
5538
|
portal.width,
|
|
@@ -4739,8 +5610,8 @@ function resolveCamera(input, width, height) {
|
|
|
4739
5610
|
const target = asVec3(camera.target, DEFAULT_CAMERA2.target);
|
|
4740
5611
|
const upInput = normalize(asVec3(camera.up, DEFAULT_CAMERA2.up), DEFAULT_CAMERA2.up);
|
|
4741
5612
|
const forward = normalize(subtract(target, position), [0, 0, -1]);
|
|
4742
|
-
const right = normalize(
|
|
4743
|
-
const up = normalize(
|
|
5613
|
+
const right = normalize(cross2(forward, upInput), [1, 0, 0]);
|
|
5614
|
+
const up = normalize(cross2(right, forward), [0, 1, 0]);
|
|
4744
5615
|
const fovYDegrees = clamp2(
|
|
4745
5616
|
readFiniteNumber("camera.fovYDegrees", camera.fovYDegrees ?? camera.fov, DEFAULT_CAMERA2.fovYDegrees),
|
|
4746
5617
|
10,
|
|
@@ -5109,8 +5980,8 @@ function supportsWavefrontPathTracingCompute(options = {}) {
|
|
|
5109
5980
|
// src/wavefront-sampling.js
|
|
5110
5981
|
function buildOrthonormalBasis(normal) {
|
|
5111
5982
|
const tangentFallback = Math.abs(normal[1]) < 0.999 ? [0, 1, 0] : [1, 0, 0];
|
|
5112
|
-
const tangent = normalize(
|
|
5113
|
-
const bitangent = normalize(
|
|
5983
|
+
const tangent = normalize(cross2(tangentFallback, normal), [1, 0, 0]);
|
|
5984
|
+
const bitangent = normalize(cross2(normal, tangent), [0, 0, 1]);
|
|
5114
5985
|
return { tangent, bitangent };
|
|
5115
5986
|
}
|
|
5116
5987
|
function localToWorld(local, normal) {
|
|
@@ -11339,7 +12210,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11339
12210
|
const triangleIndex = readNonNegativeInteger2("triangleIndex", options.triangleIndex, 0);
|
|
11340
12211
|
const edge1 = subtract(triangle.v1, triangle.v0);
|
|
11341
12212
|
const edge2 = subtract(triangle.v2, triangle.v0);
|
|
11342
|
-
const pvec =
|
|
12213
|
+
const pvec = cross2(ray.direction, edge2);
|
|
11343
12214
|
const determinant = dot(edge1, pvec);
|
|
11344
12215
|
if (Math.abs(determinant) < 1e-7) {
|
|
11345
12216
|
return null;
|
|
@@ -11350,7 +12221,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11350
12221
|
if (u < 0 || u > 1) {
|
|
11351
12222
|
return null;
|
|
11352
12223
|
}
|
|
11353
|
-
const qvec =
|
|
12224
|
+
const qvec = cross2(tvec, edge1);
|
|
11354
12225
|
const v = dot(ray.direction, qvec) * invDet;
|
|
11355
12226
|
if (v < 0 || u + v > 1) {
|
|
11356
12227
|
return null;
|
|
@@ -11359,7 +12230,7 @@ function intersectWavefrontReferenceTriangle(ray, triangle, options = {}) {
|
|
|
11359
12230
|
if (distance <= 1e-3 || distance > maxDistance) {
|
|
11360
12231
|
return null;
|
|
11361
12232
|
}
|
|
11362
|
-
const geometric = normalize(
|
|
12233
|
+
const geometric = normalize(cross2(edge1, edge2), [0, 1, 0]);
|
|
11363
12234
|
const frontFace = dot(ray.direction, geometric) < 0;
|
|
11364
12235
|
const orientedGeometric = frontFace ? geometric : scale(geometric, -1);
|
|
11365
12236
|
const w = 1 - u - v;
|
|
@@ -11453,7 +12324,7 @@ function triangleAreaForLightSampling(triangle) {
|
|
|
11453
12324
|
}
|
|
11454
12325
|
const edge1 = subtract(triangle.v1, triangle.v0);
|
|
11455
12326
|
const edge2 = subtract(triangle.v2, triangle.v0);
|
|
11456
|
-
return Math.max(Math.hypot(...
|
|
12327
|
+
return Math.max(Math.hypot(...cross2(edge1, edge2)) * 0.5, 1e-6);
|
|
11457
12328
|
}
|
|
11458
12329
|
function emissiveTriangleWeight(triangle) {
|
|
11459
12330
|
if (!triangle) {
|
|
@@ -13327,10 +14198,12 @@ export {
|
|
|
13327
14198
|
bucketFeedbackGameFrameTime,
|
|
13328
14199
|
bucketFeedbackGameViewport,
|
|
13329
14200
|
createAnimatedSceneRenderer,
|
|
14201
|
+
createCanonicalWavefrontMeshInputs,
|
|
13330
14202
|
createDefaultWavefrontSceneObjects,
|
|
13331
14203
|
createFeedbackGameDiagnosticSnapshot,
|
|
13332
14204
|
createGpuRenderer,
|
|
13333
14205
|
createMediumStack,
|
|
14206
|
+
createNativeSceneRenderer,
|
|
13334
14207
|
createProfessionalAnimatedSceneRenderer,
|
|
13335
14208
|
createRayTracingRenderPlan,
|
|
13336
14209
|
createRendererDebugHooks,
|