@babylonjs/loaders 9.26.0 → 9.26.1
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/FBX/fbxConstraintBehavior.d.ts +188 -0
- package/FBX/fbxConstraintBehavior.js +532 -0
- package/FBX/fbxConstraintBehavior.js.map +1 -0
- package/FBX/fbxFileLoader.pure.d.ts +175 -6
- package/FBX/fbxFileLoader.pure.js +1521 -286
- package/FBX/fbxFileLoader.pure.js.map +1 -1
- package/FBX/index.d.ts +4 -1
- package/FBX/index.js +1 -0
- package/FBX/index.js.map +1 -1
- package/FBX/interpreter/animation.d.ts +73 -33
- package/FBX/interpreter/animation.js +620 -223
- package/FBX/interpreter/animation.js.map +1 -1
- package/FBX/interpreter/animationCurve.d.ts +91 -0
- package/FBX/interpreter/animationCurve.js +623 -0
- package/FBX/interpreter/animationCurve.js.map +1 -0
- package/FBX/interpreter/blendShapes.js +9 -18
- package/FBX/interpreter/blendShapes.js.map +1 -1
- package/FBX/interpreter/connections.d.ts +1 -1
- package/FBX/interpreter/connections.js +173 -34
- package/FBX/interpreter/connections.js.map +1 -1
- package/FBX/interpreter/constraints.d.ts +68 -0
- package/FBX/interpreter/constraints.js +143 -0
- package/FBX/interpreter/constraints.js.map +1 -0
- package/FBX/interpreter/fbxInterpreter.d.ts +89 -4
- package/FBX/interpreter/fbxInterpreter.js +474 -122
- package/FBX/interpreter/fbxInterpreter.js.map +1 -1
- package/FBX/interpreter/geometry.d.ts +1 -1
- package/FBX/interpreter/geometry.js +150 -92
- package/FBX/interpreter/geometry.js.map +1 -1
- package/FBX/interpreter/legacyDocument.d.ts +11 -0
- package/FBX/interpreter/legacyDocument.js +425 -0
- package/FBX/interpreter/legacyDocument.js.map +1 -0
- package/FBX/interpreter/materialModel.d.ts +71 -0
- package/FBX/interpreter/materialModel.js +622 -0
- package/FBX/interpreter/materialModel.js.map +1 -0
- package/FBX/interpreter/materials.d.ts +13 -1
- package/FBX/interpreter/materials.js +88 -5
- package/FBX/interpreter/materials.js.map +1 -1
- package/FBX/interpreter/nodeTransform.d.ts +32 -0
- package/FBX/interpreter/nodeTransform.js +54 -0
- package/FBX/interpreter/nodeTransform.js.map +1 -0
- package/FBX/interpreter/nurbs.d.ts +99 -0
- package/FBX/interpreter/nurbs.js +627 -0
- package/FBX/interpreter/nurbs.js.map +1 -0
- package/FBX/interpreter/propertyTemplates.d.ts +23 -0
- package/FBX/interpreter/propertyTemplates.js +109 -2
- package/FBX/interpreter/propertyTemplates.js.map +1 -1
- package/FBX/interpreter/rig.js +16 -0
- package/FBX/interpreter/rig.js.map +1 -1
- package/FBX/interpreter/sceneDiagnostics.js +28 -3
- package/FBX/interpreter/sceneDiagnostics.js.map +1 -1
- package/FBX/interpreter/skeleton.d.ts +4 -18
- package/FBX/interpreter/skeleton.js +16 -94
- package/FBX/interpreter/skeleton.js.map +1 -1
- package/FBX/parsers/fbxAsciiParser.js +119 -9
- package/FBX/parsers/fbxAsciiParser.js.map +1 -1
- package/FBX/parsers/fbxBinaryParser.js +74 -22
- package/FBX/parsers/fbxBinaryParser.js.map +1 -1
- package/FBX/pure.d.ts +1 -0
- package/FBX/pure.js +1 -0
- package/FBX/pure.js.map +1 -1
- package/FBX/types/fbxTypes.d.ts +18 -2
- package/FBX/types/fbxTypes.js +40 -12
- package/FBX/types/fbxTypes.js.map +1 -1
- package/USD/usdCommandProtocol.d.ts +20 -4
- package/USD/usdCommandProtocol.js +25 -3
- package/USD/usdCommandProtocol.js.map +1 -1
- package/USD/usdFileLoader.pure.js +2 -0
- package/USD/usdFileLoader.pure.js.map +1 -1
- package/USD/usdSceneMaterializer.js +430 -92
- package/USD/usdSceneMaterializer.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
+
/**
|
|
3
|
+
* NURBS curves and surfaces, and polyline ("Line") geometry.
|
|
4
|
+
*
|
|
5
|
+
* The basis evaluation and tessellation follow the FBX SDK conventions as implemented by ufbx: knot spans are
|
|
6
|
+
* subdivided uniformly, closed and periodic curves wrap their control points, and surfaces are tessellated into
|
|
7
|
+
* quads (degenerate quads at poles become triangles) with welded positions along the wrapped edges.
|
|
8
|
+
*/
|
|
9
|
+
import { findChildByName, getPropertyValue, cleanFBXName, getNodeArray } from "../types/fbxTypes.js";
|
|
10
|
+
import { getPropertyEntries } from "./propertyTemplates.js";
|
|
11
|
+
const MAX_NURBS_ORDER = 128;
|
|
12
|
+
const DEFAULT_SPAN_SUBDIVISION = 4;
|
|
13
|
+
const MAX_FILE_SPAN_SUBDIVISION = 16;
|
|
14
|
+
const MAX_SPAN_SUBDIVISION = 64;
|
|
15
|
+
function readTopology(form) {
|
|
16
|
+
return form === "Closed" ? "closed" : form === "Periodic" ? "periodic" : "open";
|
|
17
|
+
}
|
|
18
|
+
export function createNurbsBasis(order, form, knots) {
|
|
19
|
+
const basis = { order, topology: readTopology(form), knots, tMin: 0, tMax: 0, spans: new Float64Array(0), valid: false };
|
|
20
|
+
if (order > 1 && order <= MAX_NURBS_ORDER) {
|
|
21
|
+
const degree = order - 1;
|
|
22
|
+
if (knots.length >= 2 * degree + 1) {
|
|
23
|
+
basis.tMin = knots[degree];
|
|
24
|
+
basis.tMax = knots[knots.length - degree - 1];
|
|
25
|
+
const maxSpans = knots.length - 2 * degree;
|
|
26
|
+
const spans = [];
|
|
27
|
+
let prev = -Infinity;
|
|
28
|
+
for (let i = 0; i < maxSpans; i++) {
|
|
29
|
+
const t = knots[degree + i];
|
|
30
|
+
if (t !== prev) {
|
|
31
|
+
spans.push(t);
|
|
32
|
+
prev = t;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
basis.spans = Float64Array.from(spans);
|
|
36
|
+
basis.valid = true;
|
|
37
|
+
for (let i = 1; i < knots.length; i++) {
|
|
38
|
+
if (!(knots[i - 1] <= knots[i])) {
|
|
39
|
+
basis.valid = false;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return basis;
|
|
46
|
+
}
|
|
47
|
+
function nurbsWeight(knots, knot, degree, u) {
|
|
48
|
+
if (knot < 0 || knot >= knots.length || knots.length - knot < degree) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const prevU = knots[knot];
|
|
52
|
+
const nextU = knots[knot + degree];
|
|
53
|
+
if (prevU >= nextU) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (u <= prevU) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (u >= nextU) {
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
return (u - prevU) / (nextU - prevU);
|
|
63
|
+
}
|
|
64
|
+
function nurbsDeriv(knots, knot, degree) {
|
|
65
|
+
if (knot < 0 || knot >= knots.length || knots.length - knot < degree) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const prevU = knots[knot];
|
|
69
|
+
const nextU = knots[knot + degree];
|
|
70
|
+
if (prevU >= nextU) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
return degree / (nextU - prevU);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Evaluates the basis functions (and their derivatives) that are non-zero at `u`.
|
|
77
|
+
* @returns index of the first influencing control point, or -1 when the basis cannot be evaluated
|
|
78
|
+
*/
|
|
79
|
+
export function evaluateNurbsBasis(basis, u, weights, derivatives) {
|
|
80
|
+
if (basis.order <= 1 || !basis.valid) {
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
const degree = basis.order - 1;
|
|
84
|
+
const knots = basis.knots;
|
|
85
|
+
let knot;
|
|
86
|
+
if (u <= basis.tMin) {
|
|
87
|
+
knot = degree;
|
|
88
|
+
u = basis.tMin;
|
|
89
|
+
}
|
|
90
|
+
else if (u >= basis.tMax) {
|
|
91
|
+
knot = knots.length - degree - 2;
|
|
92
|
+
u = basis.tMax;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Knot span [knots[knot], knots[knot + 1]) containing u
|
|
96
|
+
let lo = 0;
|
|
97
|
+
let hi = knots.length - 1;
|
|
98
|
+
while (lo < hi) {
|
|
99
|
+
const mid = (lo + hi) >> 1;
|
|
100
|
+
if (knots[mid + 1] <= u) {
|
|
101
|
+
lo = mid + 1;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
hi = mid;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!(knots[lo] <= u && u < knots[lo + 1])) {
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
knot = lo;
|
|
111
|
+
}
|
|
112
|
+
if (knot < degree) {
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
weights[0] = 1;
|
|
116
|
+
for (let p = 1; p <= degree; p++) {
|
|
117
|
+
let prev = 0;
|
|
118
|
+
let g = 1 - nurbsWeight(knots, knot - p + 1, p, u);
|
|
119
|
+
let dg = 0;
|
|
120
|
+
const last = derivatives !== null && p === degree;
|
|
121
|
+
if (last) {
|
|
122
|
+
dg = nurbsDeriv(knots, knot - p + 1, p);
|
|
123
|
+
}
|
|
124
|
+
for (let i = p; i > 0; i--) {
|
|
125
|
+
const f = nurbsWeight(knots, knot - p + i, p, u);
|
|
126
|
+
const weight = weights[i - 1];
|
|
127
|
+
weights[i] = f * weight + g * prev;
|
|
128
|
+
if (last) {
|
|
129
|
+
const df = nurbsDeriv(knots, knot - p + i, p);
|
|
130
|
+
derivatives[i] = df * weight - dg * prev;
|
|
131
|
+
dg = df;
|
|
132
|
+
}
|
|
133
|
+
prev = weight;
|
|
134
|
+
g = 1 - f;
|
|
135
|
+
}
|
|
136
|
+
weights[0] = g * prev;
|
|
137
|
+
if (last) {
|
|
138
|
+
derivatives[0] = -dg * prev;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return knot - degree;
|
|
142
|
+
}
|
|
143
|
+
/** Evaluates a curve point; `out` receives position (0..2) and derivative (3..5). */
|
|
144
|
+
export function evaluateNurbsCurve(curve, u, out, weights, derivs) {
|
|
145
|
+
const base = evaluateNurbsBasis(curve.basis, u, weights, derivs);
|
|
146
|
+
if (base < 0 || curve.numControlPoints === 0) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const order = curve.basis.order;
|
|
150
|
+
let px = 0, py = 0, pz = 0, pw = 0, dx = 0, dy = 0, dz = 0, dw = 0;
|
|
151
|
+
const cp = curve.controlPoints;
|
|
152
|
+
for (let i = 0; i < order; i++) {
|
|
153
|
+
const ix = ((base + i) % curve.numControlPoints) * 4;
|
|
154
|
+
const w = weights[i] * cp[ix + 3];
|
|
155
|
+
const d = derivs[i] * cp[ix + 3];
|
|
156
|
+
px += cp[ix] * w;
|
|
157
|
+
py += cp[ix + 1] * w;
|
|
158
|
+
pz += cp[ix + 2] * w;
|
|
159
|
+
pw += w;
|
|
160
|
+
dx += cp[ix] * d;
|
|
161
|
+
dy += cp[ix + 1] * d;
|
|
162
|
+
dz += cp[ix + 2] * d;
|
|
163
|
+
dw += d;
|
|
164
|
+
}
|
|
165
|
+
const rcp = pw !== 0 ? 1 / pw : 0;
|
|
166
|
+
out[0] = px * rcp;
|
|
167
|
+
out[1] = py * rcp;
|
|
168
|
+
out[2] = pz * rcp;
|
|
169
|
+
out[3] = (dx - dw * out[0]) * rcp;
|
|
170
|
+
out[4] = (dy - dw * out[1]) * rcp;
|
|
171
|
+
out[5] = (dz - dw * out[2]) * rcp;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
/** Evaluates a surface point; `out` receives position (0..2), du (3..5) and dv (6..8). */
|
|
175
|
+
export function evaluateNurbsSurface(surface, u, v, out, scratch) {
|
|
176
|
+
const baseU = evaluateNurbsBasis(surface.basisU, u, scratch.wu, scratch.du);
|
|
177
|
+
const baseV = evaluateNurbsBasis(surface.basisV, v, scratch.wv, scratch.dv);
|
|
178
|
+
if (baseU < 0 || baseV < 0 || surface.numU === 0 || surface.numV === 0) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const orderU = surface.basisU.order;
|
|
182
|
+
const orderV = surface.basisV.order;
|
|
183
|
+
const cp = surface.controlPoints;
|
|
184
|
+
let px = 0, py = 0, pz = 0, pw = 0;
|
|
185
|
+
let ux = 0, uy = 0, uz = 0, uw = 0;
|
|
186
|
+
let vx = 0, vy = 0, vz = 0, vw = 0;
|
|
187
|
+
for (let vi = 0; vi < orderV; vi++) {
|
|
188
|
+
const vix = (baseV + vi) % surface.numV;
|
|
189
|
+
const weightV = scratch.wv[vi];
|
|
190
|
+
const derivV = scratch.dv[vi];
|
|
191
|
+
for (let ui = 0; ui < orderU; ui++) {
|
|
192
|
+
const uix = (baseU + ui) % surface.numU;
|
|
193
|
+
const weightU = scratch.wu[ui];
|
|
194
|
+
const derivU = scratch.du[ui];
|
|
195
|
+
const ix = (vix * surface.numU + uix) * 4;
|
|
196
|
+
const cw = cp[ix + 3];
|
|
197
|
+
const w = weightU * weightV * cw;
|
|
198
|
+
const wdu = derivU * weightV * cw;
|
|
199
|
+
const wdv = derivV * weightU * cw;
|
|
200
|
+
const x = cp[ix], y = cp[ix + 1], z = cp[ix + 2];
|
|
201
|
+
px += x * w;
|
|
202
|
+
py += y * w;
|
|
203
|
+
pz += z * w;
|
|
204
|
+
pw += w;
|
|
205
|
+
ux += x * wdu;
|
|
206
|
+
uy += y * wdu;
|
|
207
|
+
uz += z * wdu;
|
|
208
|
+
uw += wdu;
|
|
209
|
+
vx += x * wdv;
|
|
210
|
+
vy += y * wdv;
|
|
211
|
+
vz += z * wdv;
|
|
212
|
+
vw += wdv;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const rcp = pw !== 0 ? 1 / pw : 0;
|
|
216
|
+
out[0] = px * rcp;
|
|
217
|
+
out[1] = py * rcp;
|
|
218
|
+
out[2] = pz * rcp;
|
|
219
|
+
out[3] = (ux - uw * out[0]) * rcp;
|
|
220
|
+
out[4] = (uy - uw * out[1]) * rcp;
|
|
221
|
+
out[5] = (uz - uw * out[2]) * rcp;
|
|
222
|
+
out[6] = (vx - vw * out[0]) * rcp;
|
|
223
|
+
out[7] = (vy - vw * out[1]) * rcp;
|
|
224
|
+
out[8] = (vz - vw * out[2]) * rcp;
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Tessellates a curve into a polyline with `subdivision` segments per knot span. Closed and periodic curves
|
|
229
|
+
* end on a copy of their first point.
|
|
230
|
+
*/
|
|
231
|
+
export function tessellateNurbsCurve(curve, subdivision = DEFAULT_SPAN_SUBDIVISION) {
|
|
232
|
+
const basis = curve.basis;
|
|
233
|
+
if (!basis.valid || curve.numControlPoints === 0 || basis.spans.length === 0) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const numSub = Math.max(1, Math.floor(subdivision));
|
|
237
|
+
const numSpans = basis.spans.length;
|
|
238
|
+
const isOpen = basis.topology === "open";
|
|
239
|
+
const numIndices = numSpans + (numSpans - 1) * (numSub - 1);
|
|
240
|
+
const numVertices = numIndices - (isOpen ? 0 : 1);
|
|
241
|
+
const points = new Float64Array(numIndices * 3);
|
|
242
|
+
const weights = new Float64Array(basis.order + 1);
|
|
243
|
+
const derivs = new Float64Array(basis.order + 1);
|
|
244
|
+
const out = new Float64Array(6);
|
|
245
|
+
for (let spanIx = 0; spanIx < numSpans; spanIx++) {
|
|
246
|
+
const numSplits = spanIx + 1 === numSpans ? 1 : numSub;
|
|
247
|
+
for (let subIx = 0; subIx < numSplits; subIx++) {
|
|
248
|
+
const ix = spanIx * numSub + subIx;
|
|
249
|
+
if (ix < numVertices) {
|
|
250
|
+
let u = basis.spans[spanIx];
|
|
251
|
+
if (subIx > 0) {
|
|
252
|
+
const t = subIx / numSub;
|
|
253
|
+
u = u * (1 - t) + t * basis.spans[spanIx + 1];
|
|
254
|
+
}
|
|
255
|
+
if (evaluateNurbsCurve(curve, u, out, weights, derivs)) {
|
|
256
|
+
points[ix * 3] = out[0];
|
|
257
|
+
points[ix * 3 + 1] = out[1];
|
|
258
|
+
points[ix * 3 + 2] = out[2];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
points[ix * 3] = points[0];
|
|
263
|
+
points[ix * 3 + 1] = points[1];
|
|
264
|
+
points[ix * 3 + 2] = points[2];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return points;
|
|
269
|
+
}
|
|
270
|
+
/** Span subdivision to use for a surface direction: an explicit override, else the file's Step, else 4. */
|
|
271
|
+
export function resolveSpanSubdivision(fileStep, override) {
|
|
272
|
+
if (override !== undefined && Number.isFinite(override) && override > 0) {
|
|
273
|
+
return Math.min(MAX_SPAN_SUBDIVISION, Math.max(1, Math.floor(override)));
|
|
274
|
+
}
|
|
275
|
+
if (fileStep > 0) {
|
|
276
|
+
return Math.min(MAX_FILE_SPAN_SUBDIVISION, Math.floor(fileStep));
|
|
277
|
+
}
|
|
278
|
+
return DEFAULT_SPAN_SUBDIVISION;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Tessellates a surface into triangles. Positions on wrapped (closed/periodic) edges and at poles are welded
|
|
282
|
+
* exactly like the FBX SDK does, so degenerate quads collapse into triangles.
|
|
283
|
+
*/
|
|
284
|
+
export function tessellateNurbsSurface(surface, subU, subV) {
|
|
285
|
+
const basisU = surface.basisU;
|
|
286
|
+
const basisV = surface.basisV;
|
|
287
|
+
if (!basisU.valid || !basisV.valid || surface.numU === 0 || surface.numV === 0 || basisU.spans.length < 2 || basisV.spans.length < 2) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const openU = basisU.topology === "open";
|
|
291
|
+
const openV = basisV.topology === "open";
|
|
292
|
+
const spansU = basisU.spans.length;
|
|
293
|
+
const spansV = basisV.spans.length;
|
|
294
|
+
const facesU = (spansU - 1) * subU;
|
|
295
|
+
const facesV = (spansV - 1) * subV;
|
|
296
|
+
const indicesU = spansU + (spansU - 1) * (subU - 1);
|
|
297
|
+
const indicesV = spansV + (spansV - 1) * (subV - 1);
|
|
298
|
+
const numAttribs = indicesU * indicesV;
|
|
299
|
+
const numFaces = facesU * facesV;
|
|
300
|
+
if (numAttribs > 0x3fffffff || numFaces > 0x3fffffff) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
// Unique (welded) positions and per-attribute-vertex data
|
|
304
|
+
const positionIx = new Uint32Array(numAttribs);
|
|
305
|
+
const welded = new Float64Array(numAttribs * 3);
|
|
306
|
+
let numPositions = 0;
|
|
307
|
+
const attribUv = new Float64Array(numAttribs * 2);
|
|
308
|
+
const scratch = {
|
|
309
|
+
wu: new Float64Array(basisU.order + 1),
|
|
310
|
+
wv: new Float64Array(basisV.order + 1),
|
|
311
|
+
du: new Float64Array(basisU.order + 1),
|
|
312
|
+
dv: new Float64Array(basisV.order + 1),
|
|
313
|
+
};
|
|
314
|
+
const out = new Float64Array(9);
|
|
315
|
+
for (let spanV = 0; spanV < spansV; spanV++) {
|
|
316
|
+
const splitsV = spanV + 1 === spansV ? 1 : subV;
|
|
317
|
+
for (let splitV = 0; splitV < splitsV; splitV++) {
|
|
318
|
+
const ixV = spanV * subV + splitV;
|
|
319
|
+
let v = basisV.spans[spanV];
|
|
320
|
+
if (splitV > 0) {
|
|
321
|
+
const t = splitV / splitsV;
|
|
322
|
+
v = v * (1 - t) + t * basisV.spans[spanV + 1];
|
|
323
|
+
}
|
|
324
|
+
const originalV = v;
|
|
325
|
+
if (spanV + 1 === spansV && !openV) {
|
|
326
|
+
v = basisV.spans[0];
|
|
327
|
+
}
|
|
328
|
+
for (let spanU = 0; spanU < spansU; spanU++) {
|
|
329
|
+
const splitsU = spanU + 1 === spansU ? 1 : subU;
|
|
330
|
+
for (let splitU = 0; splitU < splitsU; splitU++) {
|
|
331
|
+
const ixU = spanU * subU + splitU;
|
|
332
|
+
let u = basisU.spans[spanU];
|
|
333
|
+
if (splitU > 0) {
|
|
334
|
+
const t = splitU / splitsU;
|
|
335
|
+
u = u * (1 - t) + t * basisU.spans[spanU + 1];
|
|
336
|
+
}
|
|
337
|
+
const originalU = u;
|
|
338
|
+
if (spanU + 1 === spansU && !openU) {
|
|
339
|
+
u = basisU.spans[0];
|
|
340
|
+
}
|
|
341
|
+
let px = 0, py = 0, pz = 0;
|
|
342
|
+
if (evaluateNurbsSurface(surface, u, v, out, scratch)) {
|
|
343
|
+
px = out[0];
|
|
344
|
+
py = out[1];
|
|
345
|
+
pz = out[2];
|
|
346
|
+
}
|
|
347
|
+
// Positions that wrap around to an already emitted neighbour are welded.
|
|
348
|
+
const neighbors = [];
|
|
349
|
+
if ((spanV === 0 && (spanU > 0 || splitU > 0)) || (spanU === 0 && (spanV > 0 || splitV > 0))) {
|
|
350
|
+
neighbors.push(0);
|
|
351
|
+
}
|
|
352
|
+
if (spanV + 1 === spansV) {
|
|
353
|
+
neighbors.push(ixU);
|
|
354
|
+
if (spanU > 0 || splitU > 0) {
|
|
355
|
+
neighbors.push(ixV * indicesU);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (spanU + 1 === spansU) {
|
|
359
|
+
neighbors.push(ixV * indicesU);
|
|
360
|
+
if (spanV > 0 || splitV > 0) {
|
|
361
|
+
neighbors.push(indicesU - 1);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const ix = ixV * indicesU + ixU;
|
|
365
|
+
let posIx = numPositions;
|
|
366
|
+
for (const nb of neighbors) {
|
|
367
|
+
if (nb >= ix) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const nbPos = positionIx[nb];
|
|
371
|
+
const dx = welded[nbPos * 3] - px;
|
|
372
|
+
const dy = welded[nbPos * 3 + 1] - py;
|
|
373
|
+
const dz = welded[nbPos * 3 + 2] - pz;
|
|
374
|
+
if (dx * dx + dy * dy + dz * dz < 0.0000001) {
|
|
375
|
+
posIx = nbPos;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
positionIx[ix] = posIx;
|
|
380
|
+
if (posIx === numPositions) {
|
|
381
|
+
welded[posIx * 3] = px;
|
|
382
|
+
welded[posIx * 3 + 1] = py;
|
|
383
|
+
welded[posIx * 3 + 2] = pz;
|
|
384
|
+
numPositions++;
|
|
385
|
+
}
|
|
386
|
+
attribUv[ix * 2] = originalU;
|
|
387
|
+
attribUv[ix * 2 + 1] = originalV;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Faces: quads, or triangles where two consecutive corners share a welded position
|
|
393
|
+
const triangles = [];
|
|
394
|
+
const faceCorners = [0, 0, 0, 0];
|
|
395
|
+
const faceVerts = [0, 0, 0, 0];
|
|
396
|
+
for (let faceV = 0; faceV < facesV; faceV++) {
|
|
397
|
+
for (let faceU = 0; faceU < facesU; faceU++) {
|
|
398
|
+
faceCorners[0] = faceV * indicesU + faceU;
|
|
399
|
+
faceCorners[1] = faceV * indicesU + faceU + 1;
|
|
400
|
+
faceCorners[2] = (faceV + 1) * indicesU + faceU + 1;
|
|
401
|
+
faceCorners[3] = (faceV + 1) * indicesU + faceU;
|
|
402
|
+
for (let i = 0; i < 4; i++) {
|
|
403
|
+
faceVerts[i] = positionIx[faceCorners[i]];
|
|
404
|
+
}
|
|
405
|
+
let count = 4;
|
|
406
|
+
for (let prev = 0; prev < 4; prev++) {
|
|
407
|
+
const next = (prev + 1) % 4;
|
|
408
|
+
if (faceVerts[prev] === faceVerts[next]) {
|
|
409
|
+
for (let i = next; i < 3; i++) {
|
|
410
|
+
faceCorners[i] = faceCorners[i + 1];
|
|
411
|
+
faceVerts[i] = faceVerts[i + 1];
|
|
412
|
+
}
|
|
413
|
+
count = 3;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
triangles.push(faceCorners[0], faceCorners[1], faceCorners[2]);
|
|
418
|
+
if (count === 4) {
|
|
419
|
+
triangles.push(faceCorners[0], faceCorners[2], faceCorners[3]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Smooth normals: area-weighted face normals accumulated per welded position
|
|
424
|
+
const posNormals = new Float64Array(numPositions * 3);
|
|
425
|
+
for (let t = 0; t < triangles.length; t += 3) {
|
|
426
|
+
const a = positionIx[triangles[t]] * 3;
|
|
427
|
+
const b = positionIx[triangles[t + 1]] * 3;
|
|
428
|
+
const c = positionIx[triangles[t + 2]] * 3;
|
|
429
|
+
const e1x = welded[b] - welded[a], e1y = welded[b + 1] - welded[a + 1], e1z = welded[b + 2] - welded[a + 2];
|
|
430
|
+
const e2x = welded[c] - welded[a], e2y = welded[c + 1] - welded[a + 1], e2z = welded[c + 2] - welded[a + 2];
|
|
431
|
+
const nx = e1y * e2z - e1z * e2y;
|
|
432
|
+
const ny = e1z * e2x - e1x * e2z;
|
|
433
|
+
const nz = e1x * e2y - e1y * e2x;
|
|
434
|
+
for (const p of [a, b, c]) {
|
|
435
|
+
posNormals[p] += nx;
|
|
436
|
+
posNormals[p + 1] += ny;
|
|
437
|
+
posNormals[p + 2] += nz;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const flip = surface.flipNormals ? -1 : 1;
|
|
441
|
+
for (let p = 0; p < numPositions; p++) {
|
|
442
|
+
const x = posNormals[p * 3], y = posNormals[p * 3 + 1], z = posNormals[p * 3 + 2];
|
|
443
|
+
const len = Math.hypot(x, y, z);
|
|
444
|
+
if (len > 0) {
|
|
445
|
+
posNormals[p * 3] = (x / len) * flip;
|
|
446
|
+
posNormals[p * 3 + 1] = (y / len) * flip;
|
|
447
|
+
posNormals[p * 3 + 2] = (z / len) * flip;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const positions = new Float64Array(numAttribs * 3);
|
|
451
|
+
const normals = new Float64Array(numAttribs * 3);
|
|
452
|
+
for (let ix = 0; ix < numAttribs; ix++) {
|
|
453
|
+
const p = positionIx[ix] * 3;
|
|
454
|
+
positions[ix * 3] = welded[p];
|
|
455
|
+
positions[ix * 3 + 1] = welded[p + 1];
|
|
456
|
+
positions[ix * 3 + 2] = welded[p + 2];
|
|
457
|
+
normals[ix * 3] = posNormals[p];
|
|
458
|
+
normals[ix * 3 + 1] = posNormals[p + 1];
|
|
459
|
+
normals[ix * 3 + 2] = posNormals[p + 2];
|
|
460
|
+
}
|
|
461
|
+
return { positions, indices: Uint32Array.from(triangles), normals, uvs: attribUv };
|
|
462
|
+
}
|
|
463
|
+
function readFloatArray(node, name) {
|
|
464
|
+
const child = findChildByName(node, name);
|
|
465
|
+
if (!child) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const array = getNodeArray(child);
|
|
469
|
+
if (!array) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return array instanceof Float64Array ? array : Float64Array.from(array);
|
|
473
|
+
}
|
|
474
|
+
function readColor(node) {
|
|
475
|
+
const entry = getPropertyEntries(node).find((e) => e.name === "Color");
|
|
476
|
+
if (!entry || typeof entry.values[0] !== "number" || typeof entry.values[1] !== "number" || typeof entry.values[2] !== "number") {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return [entry.values[0], entry.values[1], entry.values[2]];
|
|
480
|
+
}
|
|
481
|
+
/** Reads a NurbsCurve geometry node. */
|
|
482
|
+
export function extractNurbsCurve(node) {
|
|
483
|
+
const orderNode = findChildByName(node, "Order");
|
|
484
|
+
const formNode = findChildByName(node, "Form");
|
|
485
|
+
const order = orderNode ? (getPropertyValue(orderNode, 0) ?? 0) : 0;
|
|
486
|
+
const form = formNode ? getPropertyValue(formNode, 0) : undefined;
|
|
487
|
+
const points = readFloatArray(node, "Points");
|
|
488
|
+
const knots = readFloatArray(node, "KnotVector");
|
|
489
|
+
if (!points || !knots || points.length % 4 !== 0) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
return { basis: createNurbsBasis(order, form, knots), controlPoints: points, numControlPoints: points.length / 4 };
|
|
493
|
+
}
|
|
494
|
+
/** Reads a NurbsSurface geometry node. */
|
|
495
|
+
export function extractNurbsSurface(node) {
|
|
496
|
+
const orderNode = findChildByName(node, "NurbsSurfaceOrder");
|
|
497
|
+
const dimensionsNode = findChildByName(node, "Dimensions");
|
|
498
|
+
const stepNode = findChildByName(node, "Step");
|
|
499
|
+
const formNode = findChildByName(node, "Form");
|
|
500
|
+
const flipNode = findChildByName(node, "FlipNormals");
|
|
501
|
+
const orderU = orderNode ? (getPropertyValue(orderNode, 0) ?? 0) : 0;
|
|
502
|
+
const orderV = orderNode ? (getPropertyValue(orderNode, 1) ?? 0) : 0;
|
|
503
|
+
const numU = dimensionsNode ? (getPropertyValue(dimensionsNode, 0) ?? 0) : 0;
|
|
504
|
+
const numV = dimensionsNode ? (getPropertyValue(dimensionsNode, 1) ?? 0) : 0;
|
|
505
|
+
const stepU = stepNode ? (getPropertyValue(stepNode, 0) ?? 0) : 0;
|
|
506
|
+
const stepV = stepNode ? (getPropertyValue(stepNode, 1) ?? 0) : 0;
|
|
507
|
+
const formU = formNode ? getPropertyValue(formNode, 0) : undefined;
|
|
508
|
+
const formV = formNode ? getPropertyValue(formNode, 1) : undefined;
|
|
509
|
+
const flipValue = flipNode ? getPropertyValue(flipNode, 0) : 0;
|
|
510
|
+
const points = readFloatArray(node, "Points");
|
|
511
|
+
const knotsU = readFloatArray(node, "KnotVectorU");
|
|
512
|
+
const knotsV = readFloatArray(node, "KnotVectorV");
|
|
513
|
+
if (!points || !knotsU || !knotsV || points.length % 4 !== 0 || points.length / 4 !== numU * numV || numU <= 0 || numV <= 0) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
basisU: createNurbsBasis(orderU, formU, knotsU),
|
|
518
|
+
basisV: createNurbsBasis(orderV, formV, knotsV),
|
|
519
|
+
numU,
|
|
520
|
+
numV,
|
|
521
|
+
controlPoints: points,
|
|
522
|
+
flipNormals: flipValue === true || flipValue === 1,
|
|
523
|
+
stepU: typeof stepU === "number" ? stepU : 0,
|
|
524
|
+
stepV: typeof stepV === "number" ? stepV : 0,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** Tessellates a NurbsSurface geometry node into mesh geometry. */
|
|
528
|
+
export function nurbsSurfaceToGeometry(node, geometryId, subdivisionOverride) {
|
|
529
|
+
const name = cleanFBXName(getPropertyValue(node, 1) ?? "NurbsSurface");
|
|
530
|
+
const surface = extractNurbsSurface(node);
|
|
531
|
+
const diagnostics = [];
|
|
532
|
+
if (!surface) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
const subU = resolveSpanSubdivision(surface.stepU, subdivisionOverride);
|
|
536
|
+
const subV = resolveSpanSubdivision(surface.stepV, subdivisionOverride);
|
|
537
|
+
const tess = tessellateNurbsSurface(surface, subU, subV);
|
|
538
|
+
if (!tess) {
|
|
539
|
+
diagnostics.push({ type: "nurbs-invalid", message: `NURBS surface '${name}' has an invalid basis and was skipped.` });
|
|
540
|
+
return {
|
|
541
|
+
id: geometryId,
|
|
542
|
+
name,
|
|
543
|
+
positions: new Float64Array(0),
|
|
544
|
+
indices: new Uint32Array(0),
|
|
545
|
+
normals: null,
|
|
546
|
+
uvs: null,
|
|
547
|
+
uvSets: [],
|
|
548
|
+
colors: null,
|
|
549
|
+
tangents: null,
|
|
550
|
+
binormals: null,
|
|
551
|
+
controlPointIndices: null,
|
|
552
|
+
materialIndices: null,
|
|
553
|
+
diagnostics,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
id: geometryId,
|
|
558
|
+
name,
|
|
559
|
+
positions: tess.positions,
|
|
560
|
+
indices: tess.indices,
|
|
561
|
+
normals: tess.normals,
|
|
562
|
+
uvs: tess.uvs,
|
|
563
|
+
uvSets: [{ name: "map1", data: tess.uvs }],
|
|
564
|
+
colors: null,
|
|
565
|
+
tangents: null,
|
|
566
|
+
binormals: null,
|
|
567
|
+
controlPointIndices: null,
|
|
568
|
+
materialIndices: null,
|
|
569
|
+
diagnostics,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/** Reads a NurbsCurve geometry node into a tessellated polyline. */
|
|
573
|
+
export function extractNurbsCurveGeometry(node, geometryId, subdivision = DEFAULT_SPAN_SUBDIVISION) {
|
|
574
|
+
const name = cleanFBXName(getPropertyValue(node, 1) ?? "NurbsCurve");
|
|
575
|
+
const diagnostics = [];
|
|
576
|
+
const curve = extractNurbsCurve(node);
|
|
577
|
+
const polyline = curve ? tessellateNurbsCurve(curve, subdivision) : null;
|
|
578
|
+
if (!polyline) {
|
|
579
|
+
diagnostics.push({ type: "nurbs-invalid", message: `NURBS curve '${name}' has an invalid basis and was skipped.` });
|
|
580
|
+
}
|
|
581
|
+
return { id: geometryId, name, kind: "nurbsCurve", polylines: polyline ? [polyline] : [], color: readColor(node), diagnostics };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Reads a Line geometry node: Points plus PointsIndex where a negative index (~index) ends a segment, as in
|
|
585
|
+
* polygon vertex indices.
|
|
586
|
+
*/
|
|
587
|
+
export function extractLineGeometry(node, geometryId) {
|
|
588
|
+
const name = cleanFBXName(getPropertyValue(node, 1) ?? "Line");
|
|
589
|
+
const diagnostics = [];
|
|
590
|
+
const points = readFloatArray(node, "Points");
|
|
591
|
+
const indexNode = findChildByName(node, "PointsIndex");
|
|
592
|
+
const rawIndices = indexNode ? getNodeArray(indexNode) : null;
|
|
593
|
+
const polylines = [];
|
|
594
|
+
if (points && points.length % 3 === 0) {
|
|
595
|
+
const numPoints = points.length / 3;
|
|
596
|
+
const indices = rawIndices ? Array.from(rawIndices) : Array.from({ length: numPoints }, (_, i) => i);
|
|
597
|
+
let current = [];
|
|
598
|
+
const flush = () => {
|
|
599
|
+
if (current.length >= 2) {
|
|
600
|
+
polylines.push(Float64Array.from(current));
|
|
601
|
+
}
|
|
602
|
+
else if (current.length === 1) {
|
|
603
|
+
diagnostics.push({ type: "degenerate-polygon", message: `Line '${name}' has a single-point segment that was skipped.` });
|
|
604
|
+
}
|
|
605
|
+
current = [];
|
|
606
|
+
};
|
|
607
|
+
for (let i = 0; i < indices.length; i++) {
|
|
608
|
+
let ix = indices[i];
|
|
609
|
+
const ends = ix < 0;
|
|
610
|
+
if (ends) {
|
|
611
|
+
ix = ~ix;
|
|
612
|
+
}
|
|
613
|
+
if (ix >= 0 && ix < numPoints) {
|
|
614
|
+
current.push(points[ix * 3], points[ix * 3 + 1], points[ix * 3 + 2]);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
diagnostics.push({ type: "layer-index-out-of-bounds", message: `Line '${name}' references point ${ix} outside its ${numPoints} points.`, index: i });
|
|
618
|
+
}
|
|
619
|
+
if (ends) {
|
|
620
|
+
flush();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
flush();
|
|
624
|
+
}
|
|
625
|
+
return { id: geometryId, name, kind: "line", polylines, color: readColor(node), diagnostics };
|
|
626
|
+
}
|
|
627
|
+
//# sourceMappingURL=nurbs.js.map
|