@forgeax/engine-fbx 0.1.2
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/LICENSE +202 -0
- package/README.md +196 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/asset-runtime-fixture.d.ts +9 -0
- package/dist/__tests__/asset-runtime-fixture.d.ts.map +1 -0
- package/dist/__tests__/blendshape-import.integration.test.d.ts +2 -0
- package/dist/__tests__/blendshape-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/blendshape-import.unit.test.d.ts +2 -0
- package/dist/__tests__/blendshape-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fbx-importer.test.d.ts +2 -0
- package/dist/__tests__/fbx-importer.test.d.ts.map +1 -0
- package/dist/__tests__/fbx-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/fbx-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-material-slots.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-material-slots.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-mesh-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-mesh-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-texture-path.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-texture-path.unit.test.d.ts.map +1 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fbx-importer.d.ts +18 -0
- package/dist/fbx-importer.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +9546 -0
- package/dist/index.mjs.map +1 -0
- package/dist/parse-animation-clip.d.ts +35 -0
- package/dist/parse-animation-clip.d.ts.map +1 -0
- package/dist/parse-material.d.ts +30 -0
- package/dist/parse-material.d.ts.map +1 -0
- package/dist/parse-mesh.d.ts +21 -0
- package/dist/parse-mesh.d.ts.map +1 -0
- package/dist/parse-scene.d.ts +28 -0
- package/dist/parse-scene.d.ts.map +1 -0
- package/dist/parse-skeleton.d.ts +15 -0
- package/dist/parse-skeleton.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +20 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/parse-texture.d.ts +15 -0
- package/dist/parse-texture.d.ts.map +1 -0
- package/dist/resolve-texture-path.d.ts +25 -0
- package/dist/resolve-texture-path.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +38 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/package.json +75 -0
- package/pkg/fbx-wasm.mjs +2 -0
- package/pkg/fbx-wasm.wasm +0 -0
- package/scripts/build-wasm.mjs +85 -0
- package/scripts/content-key.mjs +88 -0
- package/scripts/ensure-wasm.mjs +28 -0
- package/scripts/fetch-ufbx.mjs +42 -0
- package/scripts/fetch-wasm.mjs +91 -0
- package/scripts/parity-diff.mjs +127 -0
- package/scripts/test-wasm.mjs +91 -0
- package/src/__tests__/asset-runtime-fixture.ts +251 -0
- package/src/__tests__/blendshape-import.integration.test.ts +96 -0
- package/src/__tests__/blendshape-import.unit.test.ts +83 -0
- package/src/__tests__/fbx-importer.test.ts +200 -0
- package/src/__tests__/fbx-local-artifacts.test.ts +39 -0
- package/src/__tests__/index.test.ts +167 -0
- package/src/__tests__/mesh-material-slots.unit.test.ts +125 -0
- package/src/__tests__/parse-mesh-multi-uv.test.ts +187 -0
- package/src/__tests__/pick-e2e.integration.test.ts +247 -0
- package/src/__tests__/resolve-texture-path.unit.test.ts +50 -0
- package/src/errors.ts +112 -0
- package/src/fbx-importer.ts +372 -0
- package/src/index.ts +215 -0
- package/src/native/bridge.c +1060 -0
- package/src/parse-animation-clip.ts +271 -0
- package/src/parse-material.ts +140 -0
- package/src/parse-mesh.ts +223 -0
- package/src/parse-scene.ts +75 -0
- package/src/parse-skeleton.ts +46 -0
- package/src/parse-skin.ts +71 -0
- package/src/parse-texture.ts +36 -0
- package/src/resolve-texture-path.ts +152 -0
- package/src/to-asset-pack.ts +813 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* bridge.c — ufbx → JSON POD bridge for WebAssembly.
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* Copyright (c) forgeax engine contributors.
|
|
6
|
+
*
|
|
7
|
+
* Emits the engine FBX POD JSON schema consumed by the parse-*.ts /
|
|
8
|
+
* to-asset-pack.ts bridge layer (single ufbx WASM parser, no native addon).
|
|
9
|
+
*
|
|
10
|
+
* Compiled with Emscripten: emcc -O3 ufbx.c bridge.c -o fbx-wasm.mjs
|
|
11
|
+
*
|
|
12
|
+
* Exported functions (called from JS):
|
|
13
|
+
* parseFbxWasm(ptr, size) — parse FBX bytes, store result JSON internally
|
|
14
|
+
* getResultPtr() — pointer to result JSON string
|
|
15
|
+
* getResultLen() — byte length of result JSON string
|
|
16
|
+
* freeResult() — free the result buffer
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
#include "ufbx.h"
|
|
20
|
+
#include <stdlib.h>
|
|
21
|
+
#include <string.h>
|
|
22
|
+
#include <stdio.h>
|
|
23
|
+
#include <math.h>
|
|
24
|
+
|
|
25
|
+
/* ── Dynamic string buffer ─────────────────────────────────────────── */
|
|
26
|
+
|
|
27
|
+
typedef struct {
|
|
28
|
+
char *data;
|
|
29
|
+
size_t len;
|
|
30
|
+
size_t cap;
|
|
31
|
+
} Buf;
|
|
32
|
+
|
|
33
|
+
static void buf_init(Buf *b) {
|
|
34
|
+
b->cap = 4096;
|
|
35
|
+
b->data = (char *)malloc(b->cap);
|
|
36
|
+
b->len = 0;
|
|
37
|
+
b->data[0] = '\0';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static void buf_ensure(Buf *b, size_t extra) {
|
|
41
|
+
while (b->len + extra + 1 > b->cap) {
|
|
42
|
+
b->cap *= 2;
|
|
43
|
+
b->data = (char *)realloc(b->data, b->cap);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static void buf_str(Buf *b, const char *s) {
|
|
48
|
+
size_t n = strlen(s);
|
|
49
|
+
buf_ensure(b, n);
|
|
50
|
+
memcpy(b->data + b->len, s, n);
|
|
51
|
+
b->len += n;
|
|
52
|
+
b->data[b->len] = '\0';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static void buf_char(Buf *b, char c) {
|
|
56
|
+
buf_ensure(b, 1);
|
|
57
|
+
b->data[b->len++] = c;
|
|
58
|
+
b->data[b->len] = '\0';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static void buf_double(Buf *b, double v) {
|
|
62
|
+
if (!isfinite(v)) v = 0.0;
|
|
63
|
+
char tmp[64];
|
|
64
|
+
snprintf(tmp, sizeof(tmp), "%.17g", v);
|
|
65
|
+
buf_str(b, tmp);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static void buf_int(Buf *b, int v) {
|
|
69
|
+
char tmp[32];
|
|
70
|
+
snprintf(tmp, sizeof(tmp), "%d", v);
|
|
71
|
+
buf_str(b, tmp);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static void buf_size(Buf *b, size_t v) {
|
|
75
|
+
char tmp[32];
|
|
76
|
+
snprintf(tmp, sizeof(tmp), "%zu", v);
|
|
77
|
+
buf_str(b, tmp);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static void buf_quoted(Buf *b, const char *s) {
|
|
81
|
+
buf_char(b, '"');
|
|
82
|
+
for (const char *p = s; *p; p++) {
|
|
83
|
+
if (*p == '"') buf_str(b, "\\\"");
|
|
84
|
+
else if (*p == '\\') buf_str(b, "\\\\");
|
|
85
|
+
else if (*p == '\n') buf_str(b, "\\n");
|
|
86
|
+
else if (*p == '\r') buf_str(b, "\\r");
|
|
87
|
+
else if (*p == '\t') buf_str(b, "\\t");
|
|
88
|
+
else buf_char(b, *p);
|
|
89
|
+
}
|
|
90
|
+
buf_char(b, '"');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ── Mesh writing ──────────────────────────────────────────────────── */
|
|
94
|
+
|
|
95
|
+
static void write_mesh(Buf *b, ufbx_mesh *mesh, int source_index) {
|
|
96
|
+
buf_char(b, '{');
|
|
97
|
+
|
|
98
|
+
/* name */
|
|
99
|
+
buf_str(b, "\"name\":");
|
|
100
|
+
const char *name = mesh->element.name.data;
|
|
101
|
+
if (!name || name[0] == '\0') {
|
|
102
|
+
ufbx_node *node = mesh->instances.count > 0 ? mesh->instances.data[0] : NULL;
|
|
103
|
+
name = node ? node->name.data : "";
|
|
104
|
+
}
|
|
105
|
+
buf_quoted(b, name ? name : "");
|
|
106
|
+
buf_char(b, ',');
|
|
107
|
+
|
|
108
|
+
/* Triangulate and collect vertex data */
|
|
109
|
+
size_t max_tris = 0;
|
|
110
|
+
for (size_t fi = 0; fi < mesh->num_faces; fi++) {
|
|
111
|
+
ufbx_face face = mesh->faces.data[fi];
|
|
112
|
+
if (face.num_indices >= 3) max_tris += face.num_indices - 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
size_t num_tri_indices = max_tris * 3;
|
|
116
|
+
uint32_t *tri_indices = (uint32_t *)calloc(num_tri_indices, sizeof(uint32_t));
|
|
117
|
+
|
|
118
|
+
float *positions = (float *)calloc(mesh->num_indices * 3, sizeof(float));
|
|
119
|
+
float *normals = (float *)calloc(mesh->num_indices * 3, sizeof(float));
|
|
120
|
+
|
|
121
|
+
for (size_t i = 0; i < mesh->num_indices; i++) {
|
|
122
|
+
uint32_t vi = mesh->vertex_position.indices.data[i];
|
|
123
|
+
ufbx_vec3 p = mesh->vertex_position.values.data[vi];
|
|
124
|
+
positions[i * 3 + 0] = (float)p.x;
|
|
125
|
+
positions[i * 3 + 1] = (float)p.y;
|
|
126
|
+
positions[i * 3 + 2] = (float)p.z;
|
|
127
|
+
|
|
128
|
+
if (mesh->vertex_normal.exists) {
|
|
129
|
+
uint32_t ni = mesh->vertex_normal.indices.data[i];
|
|
130
|
+
ufbx_vec3 n = mesh->vertex_normal.values.data[ni];
|
|
131
|
+
normals[i * 3 + 0] = (float)n.x;
|
|
132
|
+
normals[i * 3 + 1] = (float)n.y;
|
|
133
|
+
normals[i * 3 + 2] = (float)n.z;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Re-triangulate all faces into tri_indices */
|
|
138
|
+
size_t max_face_tris = mesh->max_face_triangles;
|
|
139
|
+
size_t temp_size = max_face_tris * 3;
|
|
140
|
+
if (temp_size < 64) temp_size = 64;
|
|
141
|
+
uint32_t *temp = (uint32_t *)malloc(temp_size * sizeof(uint32_t));
|
|
142
|
+
size_t tri_idx = 0;
|
|
143
|
+
for (size_t fi = 0; fi < mesh->num_faces; fi++) {
|
|
144
|
+
ufbx_face face = mesh->faces.data[fi];
|
|
145
|
+
size_t nt = ufbx_triangulate_face(temp, temp_size, mesh, face);
|
|
146
|
+
for (size_t ti = 0; ti < nt * 3 && tri_idx < num_tri_indices; ti++) {
|
|
147
|
+
tri_indices[tri_idx++] = temp[ti];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
free(temp);
|
|
151
|
+
num_tri_indices = tri_idx;
|
|
152
|
+
|
|
153
|
+
/* vertices (positions as flat array) */
|
|
154
|
+
buf_str(b, "\"vertices\":[");
|
|
155
|
+
for (size_t i = 0; i < mesh->num_indices * 3; i++) {
|
|
156
|
+
if (i > 0) buf_char(b, ',');
|
|
157
|
+
buf_double(b, positions[i]);
|
|
158
|
+
}
|
|
159
|
+
buf_str(b, "],");
|
|
160
|
+
|
|
161
|
+
/* indices (triangulated) */
|
|
162
|
+
buf_str(b, "\"indices\":[");
|
|
163
|
+
for (size_t i = 0; i < num_tri_indices; i++) {
|
|
164
|
+
if (i > 0) buf_char(b, ',');
|
|
165
|
+
buf_int(b, (int)tri_indices[i]);
|
|
166
|
+
}
|
|
167
|
+
buf_str(b, "],");
|
|
168
|
+
|
|
169
|
+
/* attributes */
|
|
170
|
+
buf_str(b, "\"attributes\":{");
|
|
171
|
+
|
|
172
|
+
if (mesh->vertex_normal.exists) {
|
|
173
|
+
buf_str(b, "\"NORMAL\":[");
|
|
174
|
+
for (size_t i = 0; i < mesh->num_indices * 3; i++) {
|
|
175
|
+
if (i > 0) buf_char(b, ',');
|
|
176
|
+
buf_double(b, normals[i]);
|
|
177
|
+
}
|
|
178
|
+
buf_char(b, ']');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* UV layers */
|
|
182
|
+
for (size_t layer = 0; layer < mesh->uv_sets.count; layer++) {
|
|
183
|
+
ufbx_uv_set *uv_set = &mesh->uv_sets.data[layer];
|
|
184
|
+
if (mesh->vertex_normal.exists || layer > 0) buf_char(b, ',');
|
|
185
|
+
|
|
186
|
+
char key[32];
|
|
187
|
+
snprintf(key, sizeof(key), "TEXCOORD_%zu", layer);
|
|
188
|
+
buf_char(b, '"'); buf_str(b, key); buf_str(b, "\":[");
|
|
189
|
+
for (size_t i = 0; i < mesh->num_indices; i++) {
|
|
190
|
+
uint32_t ui = uv_set->vertex_uv.indices.data[i];
|
|
191
|
+
ufbx_vec2 uv = uv_set->vertex_uv.values.data[ui];
|
|
192
|
+
if (i > 0) buf_char(b, ',');
|
|
193
|
+
buf_double(b, uv.x); buf_char(b, ','); buf_double(b, uv.y);
|
|
194
|
+
}
|
|
195
|
+
buf_char(b, ']');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
buf_str(b, "}");
|
|
199
|
+
|
|
200
|
+
/* BlendShape channels are the FBX producer's morph source of truth.
|
|
201
|
+
* Keep one dense target per ordinary channel; parse-mesh applies the
|
|
202
|
+
* shared target/attribute budgets and rejects in-between keyframes. */
|
|
203
|
+
size_t morph_count = 0;
|
|
204
|
+
for (size_t di = 0; di < mesh->blend_deformers.count; di++) {
|
|
205
|
+
ufbx_blend_deformer *blend = mesh->blend_deformers.data[di];
|
|
206
|
+
morph_count += blend->channels.count;
|
|
207
|
+
}
|
|
208
|
+
if (morph_count > 0) {
|
|
209
|
+
buf_str(b, ",\"morphTargets\":[");
|
|
210
|
+
size_t target_index = 0;
|
|
211
|
+
for (size_t di = 0; di < mesh->blend_deformers.count; di++) {
|
|
212
|
+
ufbx_blend_deformer *blend = mesh->blend_deformers.data[di];
|
|
213
|
+
for (size_t ci = 0; ci < blend->channels.count; ci++) {
|
|
214
|
+
ufbx_blend_channel *channel = blend->channels.data[ci];
|
|
215
|
+
if (target_index++ > 0) buf_char(b, ',');
|
|
216
|
+
ufbx_blend_shape *shape = channel->target_shape;
|
|
217
|
+
if (shape == NULL && channel->keyframes.count == 1) {
|
|
218
|
+
shape = channel->keyframes.data[0].shape;
|
|
219
|
+
}
|
|
220
|
+
buf_char(b, '{');
|
|
221
|
+
buf_str(b, "\"position\":[");
|
|
222
|
+
for (size_t vi = 0; vi < mesh->num_indices; vi++) {
|
|
223
|
+
if (vi > 0) buf_char(b, ',');
|
|
224
|
+
uint32_t cp = mesh->vertex_position.indices.data[vi];
|
|
225
|
+
ufbx_vec3 value = shape ? ufbx_get_blend_shape_vertex_offset(shape, cp) : (ufbx_vec3){0,0,0};
|
|
226
|
+
buf_double(b, value.x); buf_char(b, ',');
|
|
227
|
+
buf_double(b, value.y); buf_char(b, ',');
|
|
228
|
+
buf_double(b, value.z);
|
|
229
|
+
}
|
|
230
|
+
buf_char(b, ']');
|
|
231
|
+
if (shape != NULL && shape->normal_offsets.count > 0) {
|
|
232
|
+
buf_str(b, ",\"normal\":[");
|
|
233
|
+
for (size_t vi = 0; vi < mesh->num_indices; vi++) {
|
|
234
|
+
if (vi > 0) buf_char(b, ',');
|
|
235
|
+
uint32_t cp = mesh->vertex_position.indices.data[vi];
|
|
236
|
+
uint32_t oi = ufbx_get_blend_shape_offset_index(shape, cp);
|
|
237
|
+
ufbx_vec3 value = (ufbx_vec3){0,0,0};
|
|
238
|
+
if (oi != UFBX_NO_INDEX && oi < shape->normal_offsets.count) {
|
|
239
|
+
value = shape->normal_offsets.data[oi];
|
|
240
|
+
}
|
|
241
|
+
buf_double(b, value.x); buf_char(b, ',');
|
|
242
|
+
buf_double(b, value.y); buf_char(b, ',');
|
|
243
|
+
buf_double(b, value.z);
|
|
244
|
+
}
|
|
245
|
+
buf_char(b, ']');
|
|
246
|
+
}
|
|
247
|
+
buf_char(b, '}');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
buf_str(b, "],\"morphWeights\":[");
|
|
251
|
+
target_index = 0;
|
|
252
|
+
for (size_t di = 0; di < mesh->blend_deformers.count; di++) {
|
|
253
|
+
ufbx_blend_deformer *blend = mesh->blend_deformers.data[di];
|
|
254
|
+
for (size_t ci = 0; ci < blend->channels.count; ci++) {
|
|
255
|
+
if (target_index++ > 0) buf_char(b, ',');
|
|
256
|
+
buf_double(b, blend->channels.data[ci]->weight);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
buf_char(b, ']');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
buf_char(b, ',');
|
|
263
|
+
|
|
264
|
+
/* polygonCount, sourceIndex, materialIndex */
|
|
265
|
+
buf_str(b, "\"polygonCount\":"); buf_size(b, mesh->num_faces); buf_char(b, ',');
|
|
266
|
+
buf_str(b, "\"sourceIndex\":"); buf_int(b, source_index); buf_char(b, ',');
|
|
267
|
+
buf_str(b, "\"materialIndex\":");
|
|
268
|
+
if (mesh->materials.count > 0) buf_int(b, 0);
|
|
269
|
+
else buf_int(b, -1);
|
|
270
|
+
|
|
271
|
+
buf_char(b, '}');
|
|
272
|
+
|
|
273
|
+
free(positions);
|
|
274
|
+
free(normals);
|
|
275
|
+
free(tri_indices);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ── System-node filter ────────────────────────────────────────────── */
|
|
279
|
+
|
|
280
|
+
/* FBX files carry built-in system objects the Autodesk SDK never surfaces in
|
|
281
|
+
* the node tree: the seven "Producer <view>" viewport cameras and the
|
|
282
|
+
* "Camera Switcher". ufbx exposes them as ordinary nodes, so we filter them by
|
|
283
|
+
* name to match the SDK ScenePod/animation baseline (M0 probe: 93 -> 85 nodes).
|
|
284
|
+
* Authored cameras/lights/bones/empties are kept. */
|
|
285
|
+
static int is_system_node(const ufbx_node *node) {
|
|
286
|
+
const char *name = node->name.data;
|
|
287
|
+
if (!name) return 0;
|
|
288
|
+
if (strncmp(name, "Producer ", 9) == 0) return 1;
|
|
289
|
+
if (strcmp(name, "Camera Switcher") == 0) return 1;
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* True for nodes that must not appear in the emitted ScenePod / animation. */
|
|
294
|
+
static int skip_node(const ufbx_node *node) {
|
|
295
|
+
return node->is_root || is_system_node(node);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* ── Scene nodes writing ───────────────────────────────────────────── */
|
|
299
|
+
|
|
300
|
+
/* Emit ScenePod nodes in DFS pre-order from the root's children, matching the
|
|
301
|
+
* SDK binding's WalkNode traversal so entity indices + children[] references
|
|
302
|
+
* line up field-for-field. `order` maps each emitted node -> its flat index;
|
|
303
|
+
* two passes (assign indices, then emit) keep children[] forward-referenceable.
|
|
304
|
+
*
|
|
305
|
+
* A DFS that descends into skipped (system) nodes' subtrees would still hide
|
|
306
|
+
* the system node itself but keep its descendants; the SDK's system cameras are
|
|
307
|
+
* childless leaves, so simply skipping them is equivalent. */
|
|
308
|
+
|
|
309
|
+
static void dfs_assign(ufbx_node *node, ufbx_node **order, int *count) {
|
|
310
|
+
if (!skip_node(node)) order[(*count)++] = node;
|
|
311
|
+
for (size_t i = 0; i < node->children.count; i++) {
|
|
312
|
+
dfs_assign(node->children.data[i], order, count);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
static int index_of(ufbx_node **order, int count, const ufbx_node *node) {
|
|
317
|
+
for (int i = 0; i < count; i++) {
|
|
318
|
+
if (order[i] == node) return i;
|
|
319
|
+
}
|
|
320
|
+
return -1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
static void write_nodes(Buf *b, ufbx_scene *scene) {
|
|
324
|
+
buf_str(b, "\"nodes\":[");
|
|
325
|
+
|
|
326
|
+
/* Build the DFS pre-order emit list (root's subtrees, root excluded). */
|
|
327
|
+
ufbx_node **order = (ufbx_node **)malloc(scene->nodes.count * sizeof(ufbx_node *));
|
|
328
|
+
int count = 0;
|
|
329
|
+
ufbx_node *root = scene->root_node;
|
|
330
|
+
if (root) {
|
|
331
|
+
for (size_t i = 0; i < root->children.count; i++) {
|
|
332
|
+
dfs_assign(root->children.data[i], order, &count);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (int n = 0; n < count; n++) {
|
|
337
|
+
ufbx_node *node = order[n];
|
|
338
|
+
if (n > 0) buf_char(b, ',');
|
|
339
|
+
|
|
340
|
+
buf_char(b, '{');
|
|
341
|
+
|
|
342
|
+
/* name */
|
|
343
|
+
buf_str(b, "\"name\":");
|
|
344
|
+
buf_quoted(b, node->name.data ? node->name.data : "");
|
|
345
|
+
buf_char(b, ',');
|
|
346
|
+
|
|
347
|
+
/* transform (local) */
|
|
348
|
+
ufbx_transform xform = node->local_transform;
|
|
349
|
+
buf_str(b, "\"transform\":{");
|
|
350
|
+
buf_str(b, "\"translation\":[");
|
|
351
|
+
buf_double(b, xform.translation.x); buf_char(b, ',');
|
|
352
|
+
buf_double(b, xform.translation.y); buf_char(b, ',');
|
|
353
|
+
buf_double(b, xform.translation.z);
|
|
354
|
+
buf_str(b, "],\"rotation\":[");
|
|
355
|
+
buf_double(b, xform.rotation.x); buf_char(b, ',');
|
|
356
|
+
buf_double(b, xform.rotation.y); buf_char(b, ',');
|
|
357
|
+
buf_double(b, xform.rotation.z); buf_char(b, ',');
|
|
358
|
+
buf_double(b, xform.rotation.w);
|
|
359
|
+
buf_str(b, "],\"scale\":[");
|
|
360
|
+
buf_double(b, xform.scale.x); buf_char(b, ',');
|
|
361
|
+
buf_double(b, xform.scale.y); buf_char(b, ',');
|
|
362
|
+
buf_double(b, xform.scale.z);
|
|
363
|
+
buf_str(b, "]},");
|
|
364
|
+
|
|
365
|
+
/* meshIndex */
|
|
366
|
+
buf_str(b, "\"meshIndex\":");
|
|
367
|
+
if (node->mesh) {
|
|
368
|
+
int idx = -1;
|
|
369
|
+
for (size_t mi = 0; mi < scene->meshes.count; mi++) {
|
|
370
|
+
if (scene->meshes.data[mi] == node->mesh) { idx = (int)mi; break; }
|
|
371
|
+
}
|
|
372
|
+
buf_int(b, idx);
|
|
373
|
+
} else {
|
|
374
|
+
buf_int(b, -1);
|
|
375
|
+
}
|
|
376
|
+
buf_char(b, ',');
|
|
377
|
+
|
|
378
|
+
/* children (indices into the emitted DFS list) */
|
|
379
|
+
buf_str(b, "\"children\":[");
|
|
380
|
+
int first_child = 1;
|
|
381
|
+
for (size_t ci = 0; ci < node->children.count; ci++) {
|
|
382
|
+
ufbx_node *child = node->children.data[ci];
|
|
383
|
+
if (skip_node(child)) continue;
|
|
384
|
+
if (!first_child) buf_char(b, ',');
|
|
385
|
+
first_child = 0;
|
|
386
|
+
buf_int(b, index_of(order, count, child));
|
|
387
|
+
}
|
|
388
|
+
buf_char(b, ']');
|
|
389
|
+
|
|
390
|
+
buf_char(b, '}');
|
|
391
|
+
}
|
|
392
|
+
buf_char(b, ']');
|
|
393
|
+
free(order);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* ── Materials writing ─────────────────────────────────────────────── */
|
|
397
|
+
|
|
398
|
+
static int texture_index(ufbx_scene *scene, const ufbx_texture *texture) {
|
|
399
|
+
if (!texture) return -1;
|
|
400
|
+
for (size_t i = 0; i < scene->textures.count; i++) {
|
|
401
|
+
if (scene->textures.data[i] == texture) return (int)i;
|
|
402
|
+
}
|
|
403
|
+
return -1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
static int map_texture_index(ufbx_scene *scene, const ufbx_material_map *map) {
|
|
407
|
+
if (!map || !map->texture_enabled || !map->texture) return -1;
|
|
408
|
+
return texture_index(scene, map->texture);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
static void write_texture_binding(
|
|
412
|
+
Buf *b,
|
|
413
|
+
ufbx_scene *scene,
|
|
414
|
+
const char *slot,
|
|
415
|
+
const ufbx_material_map *map,
|
|
416
|
+
int *first
|
|
417
|
+
) {
|
|
418
|
+
int index = map_texture_index(scene, map);
|
|
419
|
+
if (index < 0) return;
|
|
420
|
+
if (!*first) buf_char(b, ',');
|
|
421
|
+
*first = 0;
|
|
422
|
+
buf_str(b, "{\"slot\":");
|
|
423
|
+
buf_quoted(b, slot);
|
|
424
|
+
buf_str(b, ",\"textureIndex\":");
|
|
425
|
+
buf_int(b, index);
|
|
426
|
+
buf_char(b, '}');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static void write_texture_bindings(Buf *b, ufbx_scene *scene, ufbx_material *mat) {
|
|
430
|
+
const ufbx_material_map *base_color =
|
|
431
|
+
map_texture_index(scene, &mat->pbr.base_color) >= 0 ? &mat->pbr.base_color : &mat->fbx.diffuse_color;
|
|
432
|
+
const ufbx_material_map *normal =
|
|
433
|
+
map_texture_index(scene, &mat->pbr.normal_map) >= 0 ? &mat->pbr.normal_map : &mat->fbx.normal_map;
|
|
434
|
+
const ufbx_material_map *specular =
|
|
435
|
+
map_texture_index(scene, &mat->pbr.specular_color) >= 0 ? &mat->pbr.specular_color : &mat->fbx.specular_color;
|
|
436
|
+
const ufbx_material_map *emissive =
|
|
437
|
+
map_texture_index(scene, &mat->pbr.emission_color) >= 0 ? &mat->pbr.emission_color : &mat->fbx.emission_color;
|
|
438
|
+
const ufbx_material_map *occlusion = &mat->pbr.ambient_occlusion;
|
|
439
|
+
const ufbx_material_map *metalness = &mat->pbr.metalness;
|
|
440
|
+
const ufbx_material_map *roughness = &mat->pbr.roughness;
|
|
441
|
+
int has_any =
|
|
442
|
+
map_texture_index(scene, base_color) >= 0 ||
|
|
443
|
+
map_texture_index(scene, normal) >= 0 ||
|
|
444
|
+
map_texture_index(scene, specular) >= 0 ||
|
|
445
|
+
map_texture_index(scene, emissive) >= 0 ||
|
|
446
|
+
map_texture_index(scene, occlusion) >= 0 ||
|
|
447
|
+
(map_texture_index(scene, metalness) >= 0 && map_texture_index(scene, roughness) >= 0 &&
|
|
448
|
+
map_texture_index(scene, metalness) == map_texture_index(scene, roughness));
|
|
449
|
+
if (!has_any) return;
|
|
450
|
+
|
|
451
|
+
buf_str(b, ",\"textureBindings\":[");
|
|
452
|
+
int first = 1;
|
|
453
|
+
write_texture_binding(b, scene, "baseColorTexture", base_color, &first);
|
|
454
|
+
write_texture_binding(b, scene, "normalTexture", normal, &first);
|
|
455
|
+
write_texture_binding(b, scene, "specularTintTexture", specular, &first);
|
|
456
|
+
write_texture_binding(b, scene, "emissiveTexture", emissive, &first);
|
|
457
|
+
write_texture_binding(b, scene, "occlusionTexture", occlusion, &first);
|
|
458
|
+
if (map_texture_index(scene, metalness) >= 0 &&
|
|
459
|
+
map_texture_index(scene, metalness) == map_texture_index(scene, roughness)) {
|
|
460
|
+
write_texture_binding(b, scene, "metallicRoughnessTexture", metalness, &first);
|
|
461
|
+
}
|
|
462
|
+
buf_char(b, ']');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
static const char *texture_type_name(ufbx_texture_type type) {
|
|
466
|
+
switch (type) {
|
|
467
|
+
case UFBX_TEXTURE_FILE: return "file";
|
|
468
|
+
case UFBX_TEXTURE_LAYERED: return "layered";
|
|
469
|
+
case UFBX_TEXTURE_PROCEDURAL: return "procedural";
|
|
470
|
+
case UFBX_TEXTURE_SHADER: return "shader";
|
|
471
|
+
default: return "unknown";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static void write_textures(Buf *b, ufbx_scene *scene) {
|
|
476
|
+
if (scene->textures.count == 0) return;
|
|
477
|
+
buf_str(b, ",\"textures\":[");
|
|
478
|
+
for (size_t i = 0; i < scene->textures.count; i++) {
|
|
479
|
+
ufbx_texture *texture = scene->textures.data[i];
|
|
480
|
+
if (i > 0) buf_char(b, ',');
|
|
481
|
+
buf_str(b, "{\"name\":");
|
|
482
|
+
buf_quoted(b, texture->name.data ? texture->name.data : "");
|
|
483
|
+
buf_str(b, ",\"type\":");
|
|
484
|
+
buf_quoted(b, texture_type_name(texture->type));
|
|
485
|
+
buf_str(b, ",\"filePath\":");
|
|
486
|
+
if (texture->relative_filename.data && texture->relative_filename.data[0] != '\0') {
|
|
487
|
+
buf_quoted(b, texture->relative_filename.data);
|
|
488
|
+
} else if (texture->filename.data && texture->filename.data[0] != '\0') {
|
|
489
|
+
buf_quoted(b, texture->filename.data);
|
|
490
|
+
} else {
|
|
491
|
+
buf_quoted(b, "");
|
|
492
|
+
}
|
|
493
|
+
buf_str(b, ",\"relativeFilePath\":");
|
|
494
|
+
buf_quoted(b, texture->relative_filename.data ? texture->relative_filename.data : "");
|
|
495
|
+
buf_str(b, ",\"absoluteFilePath\":");
|
|
496
|
+
buf_quoted(b, texture->absolute_filename.data ? texture->absolute_filename.data : "");
|
|
497
|
+
buf_str(b, ",\"sourceIndex\":");
|
|
498
|
+
buf_size(b, i);
|
|
499
|
+
buf_str(b, ",\"embeddedBytes\":[");
|
|
500
|
+
const unsigned char *content = (const unsigned char *)texture->content.data;
|
|
501
|
+
for (size_t byte = 0; content && byte < texture->content.size; byte++) {
|
|
502
|
+
if (byte > 0) buf_char(b, ',');
|
|
503
|
+
buf_int(b, content[byte]);
|
|
504
|
+
}
|
|
505
|
+
buf_str(b, "]}");
|
|
506
|
+
}
|
|
507
|
+
buf_char(b, ']');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
static void write_materials(Buf *b, ufbx_scene *scene) {
|
|
511
|
+
if (scene->materials.count == 0) return;
|
|
512
|
+
|
|
513
|
+
buf_str(b, ",\"materials\":[");
|
|
514
|
+
for (size_t i = 0; i < scene->materials.count; i++) {
|
|
515
|
+
ufbx_material *mat = scene->materials.data[i];
|
|
516
|
+
if (i > 0) buf_char(b, ',');
|
|
517
|
+
buf_char(b, '{');
|
|
518
|
+
|
|
519
|
+
buf_str(b, "\"name\":");
|
|
520
|
+
buf_quoted(b, mat->name.data ? mat->name.data : "");
|
|
521
|
+
buf_char(b, ',');
|
|
522
|
+
|
|
523
|
+
/* Material kind detection (KB §1.7 / appendix B-1, PBR-first).
|
|
524
|
+
*
|
|
525
|
+
* shader_type alone under-classifies: a material whose shader_type is
|
|
526
|
+
* UFBX_SHADER_UNKNOWN (e.g. legacy 3ds Max "Standard") still fills
|
|
527
|
+
* material->fbx.* / material->pbr.* with valid values, so keying only
|
|
528
|
+
* on the shader_type whitelist wrongly drops it to "fallback" grey.
|
|
529
|
+
*
|
|
530
|
+
* Classification (matches the SDK binding's StingrayPBS > Phong >
|
|
531
|
+
* Lambert > fallback priority):
|
|
532
|
+
* - explicit PBR shader graphs (Stingray/glTF/3ds-Max-PBR/OSL/Arnold/
|
|
533
|
+
* OpenPBR) or a material with the PBR feature enabled -> stingray-pbs
|
|
534
|
+
* (read pbr.* maps)
|
|
535
|
+
* - otherwise a material that has specular (Phong lighting) -> phong
|
|
536
|
+
* (read fbx.* maps: diffuse + specular_exponent + specular_color)
|
|
537
|
+
* - otherwise a material with diffuse -> lambert (read fbx.diffuse)
|
|
538
|
+
* - otherwise -> fallback */
|
|
539
|
+
int is_pbr_shader =
|
|
540
|
+
mat->shader_type == UFBX_SHADER_SHADERFX_GRAPH ||
|
|
541
|
+
mat->shader_type == UFBX_SHADER_GLTF_MATERIAL ||
|
|
542
|
+
mat->shader_type == UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH ||
|
|
543
|
+
mat->shader_type == UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS ||
|
|
544
|
+
mat->shader_type == UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL ||
|
|
545
|
+
mat->shader_type == UFBX_SHADER_OSL_STANDARD_SURFACE ||
|
|
546
|
+
mat->shader_type == UFBX_SHADER_ARNOLD_STANDARD_SURFACE ||
|
|
547
|
+
mat->shader_type == UFBX_SHADER_OPENPBR_MATERIAL;
|
|
548
|
+
|
|
549
|
+
int is_phong =
|
|
550
|
+
mat->shader_type == UFBX_SHADER_FBX_PHONG ||
|
|
551
|
+
mat->shader_type == UFBX_SHADER_BLENDER_PHONG ||
|
|
552
|
+
mat->features.specular.enabled;
|
|
553
|
+
|
|
554
|
+
int has_diffuse =
|
|
555
|
+
mat->features.diffuse.enabled || mat->fbx.diffuse_color.has_value;
|
|
556
|
+
|
|
557
|
+
if (is_pbr_shader || mat->features.pbr.enabled) {
|
|
558
|
+
|
|
559
|
+
ufbx_material_map bc = mat->pbr.base_color;
|
|
560
|
+
buf_str(b, "\"kind\":\"stingray-pbs\",");
|
|
561
|
+
buf_str(b, "\"stingrayProps\":{");
|
|
562
|
+
buf_str(b, "\"baseColor\":[");
|
|
563
|
+
buf_double(b, bc.value_vec4.x); buf_char(b, ',');
|
|
564
|
+
buf_double(b, bc.value_vec4.y); buf_char(b, ',');
|
|
565
|
+
buf_double(b, bc.value_vec4.z);
|
|
566
|
+
buf_str(b, "],");
|
|
567
|
+
buf_str(b, "\"metallic\":");
|
|
568
|
+
buf_double(b, mat->pbr.metalness.value_real);
|
|
569
|
+
buf_str(b, ",\"roughness\":");
|
|
570
|
+
buf_double(b, mat->pbr.roughness.value_real);
|
|
571
|
+
buf_char(b, '}');
|
|
572
|
+
|
|
573
|
+
} else if (is_phong) {
|
|
574
|
+
buf_str(b, "\"kind\":\"phong\",");
|
|
575
|
+
buf_str(b, "\"diffuse\":[");
|
|
576
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.x); buf_char(b, ',');
|
|
577
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.y); buf_char(b, ',');
|
|
578
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.z);
|
|
579
|
+
buf_str(b, "],");
|
|
580
|
+
buf_str(b, "\"shininess\":");
|
|
581
|
+
buf_double(b, mat->fbx.specular_exponent.value_real);
|
|
582
|
+
buf_str(b, ",\"specular\":[");
|
|
583
|
+
buf_double(b, mat->fbx.specular_color.value_vec3.x); buf_char(b, ',');
|
|
584
|
+
buf_double(b, mat->fbx.specular_color.value_vec3.y); buf_char(b, ',');
|
|
585
|
+
buf_double(b, mat->fbx.specular_color.value_vec3.z);
|
|
586
|
+
buf_char(b, ']');
|
|
587
|
+
|
|
588
|
+
} else if (has_diffuse || mat->shader_type == UFBX_SHADER_FBX_LAMBERT) {
|
|
589
|
+
buf_str(b, "\"kind\":\"lambert\",");
|
|
590
|
+
buf_str(b, "\"diffuse\":[");
|
|
591
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.x); buf_char(b, ',');
|
|
592
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.y); buf_char(b, ',');
|
|
593
|
+
buf_double(b, mat->fbx.diffuse_color.value_vec3.z);
|
|
594
|
+
buf_char(b, ']');
|
|
595
|
+
} else {
|
|
596
|
+
buf_str(b, "\"kind\":\"fallback\"");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
write_texture_bindings(b, scene, mat);
|
|
600
|
+
|
|
601
|
+
buf_str(b, ",\"sourceIndex\":");
|
|
602
|
+
buf_size(b, i);
|
|
603
|
+
|
|
604
|
+
buf_char(b, '}');
|
|
605
|
+
}
|
|
606
|
+
buf_char(b, ']');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* ── Skeleton writing ──────────────────────────────────────────────── */
|
|
610
|
+
|
|
611
|
+
static void write_skeleton(Buf *b, ufbx_scene *scene) {
|
|
612
|
+
/* Find the first skin deformer with clusters */
|
|
613
|
+
ufbx_skin_deformer *skin = NULL;
|
|
614
|
+
for (size_t i = 0; i < scene->skin_deformers.count; i++) {
|
|
615
|
+
if (scene->skin_deformers.data[i]->clusters.count > 0) {
|
|
616
|
+
skin = scene->skin_deformers.data[i];
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!skin) return;
|
|
621
|
+
|
|
622
|
+
size_t cluster_count = skin->clusters.count;
|
|
623
|
+
|
|
624
|
+
buf_str(b, ",\"skeletons\":[{");
|
|
625
|
+
buf_str(b, "\"jointCount\":"); buf_size(b, cluster_count); buf_char(b, ',');
|
|
626
|
+
|
|
627
|
+
/* Inverse bind matrices: for each cluster, compute IBM */
|
|
628
|
+
buf_str(b, "\"inverseBindMatrices\":[");
|
|
629
|
+
for (size_t c = 0; c < cluster_count; c++) {
|
|
630
|
+
ufbx_skin_cluster *cluster = skin->clusters.data[c];
|
|
631
|
+
ufbx_matrix ibm = cluster->geometry_to_bone;
|
|
632
|
+
|
|
633
|
+
if (c > 0) buf_char(b, ',');
|
|
634
|
+
/* 4x4 column-major (matching glTF convention) */
|
|
635
|
+
buf_double(b, ibm.m00); buf_char(b, ',');
|
|
636
|
+
buf_double(b, ibm.m10); buf_char(b, ',');
|
|
637
|
+
buf_double(b, ibm.m20); buf_char(b, ',');
|
|
638
|
+
buf_double(b, 0.0); buf_char(b, ',');
|
|
639
|
+
buf_double(b, ibm.m01); buf_char(b, ',');
|
|
640
|
+
buf_double(b, ibm.m11); buf_char(b, ',');
|
|
641
|
+
buf_double(b, ibm.m21); buf_char(b, ',');
|
|
642
|
+
buf_double(b, 0.0); buf_char(b, ',');
|
|
643
|
+
buf_double(b, ibm.m02); buf_char(b, ',');
|
|
644
|
+
buf_double(b, ibm.m12); buf_char(b, ',');
|
|
645
|
+
buf_double(b, ibm.m22); buf_char(b, ',');
|
|
646
|
+
buf_double(b, 0.0); buf_char(b, ',');
|
|
647
|
+
buf_double(b, ibm.m03); buf_char(b, ',');
|
|
648
|
+
buf_double(b, ibm.m13); buf_char(b, ',');
|
|
649
|
+
buf_double(b, ibm.m23); buf_char(b, ',');
|
|
650
|
+
buf_double(b, 1.0);
|
|
651
|
+
}
|
|
652
|
+
buf_str(b, "],");
|
|
653
|
+
|
|
654
|
+
/* Joint paths (bone names) */
|
|
655
|
+
buf_str(b, "\"jointPaths\":[");
|
|
656
|
+
for (size_t c = 0; c < cluster_count; c++) {
|
|
657
|
+
ufbx_skin_cluster *cluster = skin->clusters.data[c];
|
|
658
|
+
if (c > 0) buf_char(b, ',');
|
|
659
|
+
const char *name = cluster->bone_node ?
|
|
660
|
+
cluster->bone_node->name.data : "";
|
|
661
|
+
buf_quoted(b, name ? name : "");
|
|
662
|
+
}
|
|
663
|
+
buf_str(b, "]");
|
|
664
|
+
|
|
665
|
+
buf_str(b, "}]");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* ── Skin writing ──────────────────────────────────────────────────── */
|
|
669
|
+
|
|
670
|
+
/* Emit one influence object (top-4 joints by weight, normalized) for skin
|
|
671
|
+
* control point `cp` (an index into skin->vertices). An out-of-range cp emits a
|
|
672
|
+
* zeroed influence so a corner referencing a control point the deformer never
|
|
673
|
+
* weighted still produces a well-formed entry. */
|
|
674
|
+
static void write_skin_influence(Buf *b, const ufbx_skin_deformer *skin, size_t cp) {
|
|
675
|
+
buf_str(b, "{\"jointIndices\":[");
|
|
676
|
+
|
|
677
|
+
/* Collect all weights for this control point */
|
|
678
|
+
typedef struct { int joint; double weight; } wp;
|
|
679
|
+
wp pairs[64];
|
|
680
|
+
int pair_count = 0;
|
|
681
|
+
|
|
682
|
+
if (cp < skin->vertices.count) {
|
|
683
|
+
ufbx_skin_vertex sv = skin->vertices.data[cp];
|
|
684
|
+
for (size_t wi = 0; wi < sv.num_weights && wi < 64; wi++) {
|
|
685
|
+
ufbx_skin_weight w = skin->weights.data[sv.weight_begin + wi];
|
|
686
|
+
if (pair_count < 64) {
|
|
687
|
+
pairs[pair_count].joint = (int)w.cluster_index;
|
|
688
|
+
pairs[pair_count].weight = w.weight;
|
|
689
|
+
pair_count++;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* Sort by weight descending (simple bubble for <=64 elements) */
|
|
695
|
+
for (int a = 0; a < pair_count - 1; a++) {
|
|
696
|
+
for (int bb = a + 1; bb < pair_count; bb++) {
|
|
697
|
+
if (pairs[bb].weight > pairs[a].weight) {
|
|
698
|
+
wp tmp = pairs[a]; pairs[a] = pairs[bb]; pairs[bb] = tmp;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* Take top 4, normalize */
|
|
704
|
+
int top = pair_count < 4 ? pair_count : 4;
|
|
705
|
+
double sum = 0;
|
|
706
|
+
for (int k = 0; k < top; k++) sum += pairs[k].weight;
|
|
707
|
+
|
|
708
|
+
for (int k = 0; k < 4; k++) {
|
|
709
|
+
if (k > 0) buf_char(b, ',');
|
|
710
|
+
buf_int(b, k < top ? pairs[k].joint : 0);
|
|
711
|
+
}
|
|
712
|
+
buf_str(b, "],\"jointWeights\":[");
|
|
713
|
+
for (int k = 0; k < 4; k++) {
|
|
714
|
+
if (k > 0) buf_char(b, ',');
|
|
715
|
+
buf_double(b, (k < top && sum > 0) ? pairs[k].weight / sum : 0.0);
|
|
716
|
+
}
|
|
717
|
+
buf_str(b, "]}");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
static void write_skin(Buf *b, ufbx_scene *scene) {
|
|
721
|
+
ufbx_skin_deformer *skin = NULL;
|
|
722
|
+
ufbx_mesh *skinned_mesh = NULL;
|
|
723
|
+
|
|
724
|
+
for (size_t i = 0; i < scene->skin_deformers.count; i++) {
|
|
725
|
+
ufbx_skin_deformer *sd = scene->skin_deformers.data[i];
|
|
726
|
+
if (sd->clusters.count == 0) continue;
|
|
727
|
+
skin = sd;
|
|
728
|
+
/* Find the mesh this deformer is attached to */
|
|
729
|
+
for (size_t mi = 0; mi < scene->meshes.count; mi++) {
|
|
730
|
+
ufbx_mesh *m = scene->meshes.data[mi];
|
|
731
|
+
for (size_t di = 0; di < m->skin_deformers.count; di++) {
|
|
732
|
+
if (m->skin_deformers.data[di] == sd) { skinned_mesh = m; break; }
|
|
733
|
+
}
|
|
734
|
+
if (skinned_mesh) break;
|
|
735
|
+
}
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
if (!skin || !skinned_mesh) return;
|
|
739
|
+
|
|
740
|
+
/* write_mesh de-indexes geometry to one vertex per polygon corner (positions
|
|
741
|
+
* sized mesh->num_indices), while ufbx stores skin influences per control
|
|
742
|
+
* point (skin->vertices, indexed by control-point index). Emit influences in
|
|
743
|
+
* the same corner order the mesh uses: for each corner, look up its control
|
|
744
|
+
* point via vertex_position.indices and expand that control point's weights.
|
|
745
|
+
* This keeps influences.length == the emitted mesh vertex count, which
|
|
746
|
+
* to-asset-pack.ts requires (influences.length === vc) to write the skinned
|
|
747
|
+
* 18-float vertex layout. A control-point-length array (1605) against a
|
|
748
|
+
* corner-length mesh (9618 for humanoid) makes that check fail, silently
|
|
749
|
+
* drops skinIndex/skinWeight, and the render-system then fail-fasts with
|
|
750
|
+
* material-skin-attr-missing. Mirrors the per-vertex contract glTF skins
|
|
751
|
+
* already satisfy. */
|
|
752
|
+
size_t corner_count = skinned_mesh->num_indices;
|
|
753
|
+
size_t cluster_count = skin->clusters.count;
|
|
754
|
+
|
|
755
|
+
buf_str(b, ",\"skins\":[{");
|
|
756
|
+
buf_str(b, "\"meshSourceIndex\":0,");
|
|
757
|
+
|
|
758
|
+
/* Joint paths */
|
|
759
|
+
buf_str(b, "\"jointPaths\":[");
|
|
760
|
+
for (size_t c = 0; c < cluster_count; c++) {
|
|
761
|
+
if (c > 0) buf_char(b, ',');
|
|
762
|
+
const char *name = skin->clusters.data[c]->bone_node ?
|
|
763
|
+
skin->clusters.data[c]->bone_node->name.data : "";
|
|
764
|
+
buf_quoted(b, name ? name : "");
|
|
765
|
+
}
|
|
766
|
+
buf_str(b, "],");
|
|
767
|
+
|
|
768
|
+
buf_str(b, "\"vertexCount\":"); buf_size(b, corner_count); buf_char(b, ',');
|
|
769
|
+
|
|
770
|
+
/* Per-corner influences (top 4, normalized), expanded from control points. */
|
|
771
|
+
buf_str(b, "\"influences\":[");
|
|
772
|
+
for (size_t corner = 0; corner < corner_count; corner++) {
|
|
773
|
+
if (corner > 0) buf_char(b, ',');
|
|
774
|
+
uint32_t cp = skinned_mesh->vertex_position.indices.data[corner];
|
|
775
|
+
write_skin_influence(b, skin, cp);
|
|
776
|
+
}
|
|
777
|
+
buf_char(b, ']');
|
|
778
|
+
|
|
779
|
+
buf_str(b, "}]");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* ── Node path helper ──────────────────────────────────────────────── */
|
|
783
|
+
|
|
784
|
+
static void build_node_path(char *out, size_t out_size, ufbx_node *node) {
|
|
785
|
+
/* Build "Root/Parent/Child" from bottom up, then reverse segments. */
|
|
786
|
+
const char *segments[128];
|
|
787
|
+
int depth = 0;
|
|
788
|
+
ufbx_node *cur = node;
|
|
789
|
+
while (cur && !cur->is_root && depth < 128) {
|
|
790
|
+
segments[depth++] = cur->name.data ? cur->name.data : "";
|
|
791
|
+
cur = cur->parent;
|
|
792
|
+
}
|
|
793
|
+
size_t pos = 0;
|
|
794
|
+
for (int i = depth - 1; i >= 0; i--) {
|
|
795
|
+
if (i < depth - 1 && pos < out_size - 1) out[pos++] = '/';
|
|
796
|
+
const char *s = segments[i];
|
|
797
|
+
while (*s && pos < out_size - 1) out[pos++] = *s++;
|
|
798
|
+
}
|
|
799
|
+
out[pos] = '\0';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* ── Animation writing ─────────────────────────────────────────────── */
|
|
803
|
+
|
|
804
|
+
/* Does an anim_value carry at least one keyframe on any of its curves? */
|
|
805
|
+
static int anim_value_has_keys(const ufbx_anim_value *av) {
|
|
806
|
+
if (!av) return 0;
|
|
807
|
+
for (int i = 0; i < 3; i++) {
|
|
808
|
+
const ufbx_anim_curve *cv = av->curves[i];
|
|
809
|
+
if (cv && cv->keyframes.count > 0) return 1;
|
|
810
|
+
}
|
|
811
|
+
return 0;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/* Append every keyframe time (seconds) from an anim_value's curves. */
|
|
815
|
+
static size_t append_key_times(const ufbx_anim_value *av, double *times,
|
|
816
|
+
size_t n, size_t cap) {
|
|
817
|
+
if (!av) return n;
|
|
818
|
+
for (int i = 0; i < 3; i++) {
|
|
819
|
+
const ufbx_anim_curve *cv = av->curves[i];
|
|
820
|
+
if (!cv) continue;
|
|
821
|
+
for (size_t k = 0; k < cv->keyframes.count && n < cap; k++) {
|
|
822
|
+
times[n++] = cv->keyframes.data[k].time;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return n;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* Sort + dedupe an unordered times array in place; returns deduped count. */
|
|
829
|
+
static int cmp_double(const void *a, const void *b) {
|
|
830
|
+
double x = *(const double *)a, y = *(const double *)b;
|
|
831
|
+
return (x < y) ? -1 : (x > y) ? 1 : 0;
|
|
832
|
+
}
|
|
833
|
+
static size_t sort_unique(double *times, size_t n) {
|
|
834
|
+
if (n == 0) return 0;
|
|
835
|
+
qsort(times, n, sizeof(double), cmp_double);
|
|
836
|
+
size_t m = 1;
|
|
837
|
+
for (size_t i = 1; i < n; i++) {
|
|
838
|
+
if (times[i] > times[m - 1] + 1e-9) times[m++] = times[i];
|
|
839
|
+
}
|
|
840
|
+
return m;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* An anim_stack is empty (produces no clip) when it has zero duration, no
|
|
844
|
+
* layers, or no layer carries any animated property (KB §4.3). */
|
|
845
|
+
static int anim_stack_is_empty(const ufbx_anim_stack *stack) {
|
|
846
|
+
if (stack->time_begin == stack->time_end) return 1;
|
|
847
|
+
if (stack->layers.count == 0) return 1;
|
|
848
|
+
for (size_t i = 0; i < stack->layers.count; i++) {
|
|
849
|
+
if (stack->layers.data[i]->anim_props.count > 0) return 0;
|
|
850
|
+
}
|
|
851
|
+
return 1;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* Locate the anim_value bound to a node's Lcl property on a layer, if any. */
|
|
855
|
+
static ufbx_anim_value *find_lcl_value(const ufbx_anim_layer *layer,
|
|
856
|
+
const ufbx_node *node, const char *prop) {
|
|
857
|
+
ufbx_anim_prop *ap = ufbx_find_anim_prop(layer, &node->element, prop);
|
|
858
|
+
return (ap && anim_value_has_keys(ap->anim_value)) ? ap->anim_value : NULL;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* Emit one animation channel: keyTimes = union of the node's animated
|
|
862
|
+
* properties' key times, keyValues = ufbx_evaluate_transform sampled at each
|
|
863
|
+
* key time and decomposed to the requested TRS component. Matches the SDK
|
|
864
|
+
* binding (EvaluateLocalTransform + quaternion sign canonicalization). */
|
|
865
|
+
static void write_channel(Buf *b, ufbx_anim *anim, ufbx_node *node,
|
|
866
|
+
const char *node_path, const char *property,
|
|
867
|
+
const double *times, size_t nt, double time_begin) {
|
|
868
|
+
buf_str(b, "{\"targetNode\":");
|
|
869
|
+
buf_quoted(b, node_path);
|
|
870
|
+
buf_str(b, ",\"property\":\"");
|
|
871
|
+
buf_str(b, property);
|
|
872
|
+
buf_str(b, "\",\"keyTimes\":[");
|
|
873
|
+
for (size_t k = 0; k < nt; k++) {
|
|
874
|
+
if (k > 0) buf_char(b, ',');
|
|
875
|
+
buf_double(b, times[k] - time_begin);
|
|
876
|
+
}
|
|
877
|
+
buf_str(b, "],\"keyValues\":[");
|
|
878
|
+
|
|
879
|
+
int is_rot = strcmp(property, "rotation") == 0;
|
|
880
|
+
int is_trans = strcmp(property, "translation") == 0;
|
|
881
|
+
double pqx = 0, pqy = 0, pqz = 0, pqw = 1;
|
|
882
|
+
|
|
883
|
+
for (size_t k = 0; k < nt; k++) {
|
|
884
|
+
ufbx_transform tr = ufbx_evaluate_transform(anim, node, times[k]);
|
|
885
|
+
if (k > 0) buf_char(b, ',');
|
|
886
|
+
if (is_rot) {
|
|
887
|
+
double cx = tr.rotation.x, cy = tr.rotation.y,
|
|
888
|
+
cz = tr.rotation.z, cw = tr.rotation.w;
|
|
889
|
+
/* Canonicalize sign for short-arc continuity across keys. */
|
|
890
|
+
if (cx * pqx + cy * pqy + cz * pqz + cw * pqw < 0) {
|
|
891
|
+
cx = -cx; cy = -cy; cz = -cz; cw = -cw;
|
|
892
|
+
}
|
|
893
|
+
pqx = cx; pqy = cy; pqz = cz; pqw = cw;
|
|
894
|
+
buf_double(b, cx); buf_char(b, ',');
|
|
895
|
+
buf_double(b, cy); buf_char(b, ',');
|
|
896
|
+
buf_double(b, cz); buf_char(b, ',');
|
|
897
|
+
buf_double(b, cw);
|
|
898
|
+
} else {
|
|
899
|
+
ufbx_vec3 v = is_trans ? tr.translation : tr.scale;
|
|
900
|
+
buf_double(b, v.x); buf_char(b, ',');
|
|
901
|
+
buf_double(b, v.y); buf_char(b, ',');
|
|
902
|
+
buf_double(b, v.z);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
buf_str(b, "]}");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
static void write_animation(Buf *b, ufbx_scene *scene) {
|
|
909
|
+
if (scene->anim_stacks.count == 0) return;
|
|
910
|
+
|
|
911
|
+
/* Buffer for union key times; humanoid clips have <= a few hundred keys. */
|
|
912
|
+
enum { MAX_KEYS = 8192 };
|
|
913
|
+
double *times = (double *)malloc(MAX_KEYS * sizeof(double));
|
|
914
|
+
|
|
915
|
+
buf_str(b, ",\"clips\":[");
|
|
916
|
+
int first_clip = 1;
|
|
917
|
+
for (size_t si = 0; si < scene->anim_stacks.count; si++) {
|
|
918
|
+
ufbx_anim_stack *stack = scene->anim_stacks.data[si];
|
|
919
|
+
|
|
920
|
+
/* Empty-take filter: no clip for zero-duration / no-layer / no-prop
|
|
921
|
+
* stacks (KB §4.3), and no assigning a fake 1.0 duration. */
|
|
922
|
+
if (anim_stack_is_empty(stack)) continue;
|
|
923
|
+
ufbx_anim_layer *layer = stack->layers.data[0];
|
|
924
|
+
|
|
925
|
+
double duration = stack->time_end - stack->time_begin;
|
|
926
|
+
|
|
927
|
+
if (!first_clip) buf_char(b, ',');
|
|
928
|
+
first_clip = 0;
|
|
929
|
+
|
|
930
|
+
buf_char(b, '{');
|
|
931
|
+
buf_str(b, "\"name\":");
|
|
932
|
+
buf_quoted(b, stack->name.data ? stack->name.data : "");
|
|
933
|
+
buf_str(b, ",\"duration\":"); buf_double(b, duration);
|
|
934
|
+
buf_str(b, ",\"channels\":[");
|
|
935
|
+
|
|
936
|
+
int first_channel = 1;
|
|
937
|
+
for (size_t ni = 0; ni < scene->nodes.count; ni++) {
|
|
938
|
+
ufbx_node *node = scene->nodes.data[ni];
|
|
939
|
+
if (skip_node(node)) continue;
|
|
940
|
+
|
|
941
|
+
ufbx_anim_value *tv = find_lcl_value(layer, node, UFBX_Lcl_Translation);
|
|
942
|
+
ufbx_anim_value *rv = find_lcl_value(layer, node, UFBX_Lcl_Rotation);
|
|
943
|
+
ufbx_anim_value *sv = find_lcl_value(layer, node, UFBX_Lcl_Scaling);
|
|
944
|
+
if (!tv && !rv && !sv) continue;
|
|
945
|
+
|
|
946
|
+
char node_path[1024];
|
|
947
|
+
build_node_path(node_path, sizeof(node_path), node);
|
|
948
|
+
|
|
949
|
+
/* One shared timeline per node = union of every animated property's
|
|
950
|
+
* key times, so a single evaluate feeds each channel (SDK parity). */
|
|
951
|
+
size_t nraw = 0;
|
|
952
|
+
nraw = append_key_times(tv, times, nraw, MAX_KEYS);
|
|
953
|
+
nraw = append_key_times(rv, times, nraw, MAX_KEYS);
|
|
954
|
+
nraw = append_key_times(sv, times, nraw, MAX_KEYS);
|
|
955
|
+
size_t nt = sort_unique(times, nraw);
|
|
956
|
+
|
|
957
|
+
/* A property gets a channel only if it owns curves (no static
|
|
958
|
+
* slots), mirroring glTF / the SDK binding. */
|
|
959
|
+
if (tv) {
|
|
960
|
+
if (!first_channel) buf_char(b, ','); first_channel = 0;
|
|
961
|
+
write_channel(b, stack->anim, node, node_path, "translation",
|
|
962
|
+
times, nt, stack->time_begin);
|
|
963
|
+
}
|
|
964
|
+
if (rv) {
|
|
965
|
+
if (!first_channel) buf_char(b, ','); first_channel = 0;
|
|
966
|
+
write_channel(b, stack->anim, node, node_path, "rotation",
|
|
967
|
+
times, nt, stack->time_begin);
|
|
968
|
+
}
|
|
969
|
+
if (sv) {
|
|
970
|
+
if (!first_channel) buf_char(b, ','); first_channel = 0;
|
|
971
|
+
write_channel(b, stack->anim, node, node_path, "scale",
|
|
972
|
+
times, nt, stack->time_begin);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
buf_str(b, "]}");
|
|
977
|
+
}
|
|
978
|
+
buf_char(b, ']');
|
|
979
|
+
free(times);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* ── Main entry point ──────────────────────────────────────────────── */
|
|
983
|
+
|
|
984
|
+
static char *g_result = NULL;
|
|
985
|
+
static size_t g_result_len = 0;
|
|
986
|
+
|
|
987
|
+
__attribute__((used))
|
|
988
|
+
void parseFbxWasm(const void *data, size_t size) {
|
|
989
|
+
/* Free previous result */
|
|
990
|
+
if (g_result) { free(g_result); g_result = NULL; g_result_len = 0; }
|
|
991
|
+
|
|
992
|
+
/* Load FBX from memory via ufbx */
|
|
993
|
+
ufbx_load_opts opts = { 0 };
|
|
994
|
+
opts.target_axes = ufbx_axes_right_handed_y_up;
|
|
995
|
+
opts.target_unit_meters = 1.0;
|
|
996
|
+
opts.space_conversion = UFBX_SPACE_CONVERSION_TRANSFORM_ROOT;
|
|
997
|
+
|
|
998
|
+
ufbx_error error;
|
|
999
|
+
ufbx_scene *scene = ufbx_load_memory(data, size, &opts, &error);
|
|
1000
|
+
|
|
1001
|
+
if (!scene) {
|
|
1002
|
+
/* Return error JSON */
|
|
1003
|
+
Buf b; buf_init(&b);
|
|
1004
|
+
buf_str(&b, "{\"error\":{\"code\":\"fbx-parse-failed\",\"message\":");
|
|
1005
|
+
buf_quoted(&b, error.description.data ? error.description.data : "unknown error");
|
|
1006
|
+
buf_str(&b, "}}");
|
|
1007
|
+
g_result = b.data;
|
|
1008
|
+
g_result_len = b.len;
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
Buf b;
|
|
1013
|
+
buf_init(&b);
|
|
1014
|
+
buf_char(&b, '{');
|
|
1015
|
+
|
|
1016
|
+
/* Meshes */
|
|
1017
|
+
buf_str(&b, "\"meshes\":[");
|
|
1018
|
+
for (size_t i = 0; i < scene->meshes.count; i++) {
|
|
1019
|
+
if (i > 0) buf_char(&b, ',');
|
|
1020
|
+
write_mesh(&b, scene->meshes.data[i], (int)i);
|
|
1021
|
+
}
|
|
1022
|
+
buf_str(&b, "],");
|
|
1023
|
+
|
|
1024
|
+
/* Nodes */
|
|
1025
|
+
write_nodes(&b, scene);
|
|
1026
|
+
|
|
1027
|
+
/* Textures must be emitted before materials so the JSON remains easy to
|
|
1028
|
+
inspect; material bindings refer to these stable scene texture indices. */
|
|
1029
|
+
write_textures(&b, scene);
|
|
1030
|
+
|
|
1031
|
+
/* Materials */
|
|
1032
|
+
write_materials(&b, scene);
|
|
1033
|
+
|
|
1034
|
+
/* Skeleton */
|
|
1035
|
+
write_skeleton(&b, scene);
|
|
1036
|
+
|
|
1037
|
+
/* Skin */
|
|
1038
|
+
write_skin(&b, scene);
|
|
1039
|
+
|
|
1040
|
+
/* Animation */
|
|
1041
|
+
write_animation(&b, scene);
|
|
1042
|
+
|
|
1043
|
+
buf_char(&b, '}');
|
|
1044
|
+
|
|
1045
|
+
g_result = b.data;
|
|
1046
|
+
g_result_len = b.len;
|
|
1047
|
+
|
|
1048
|
+
ufbx_free_scene(scene);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
__attribute__((used))
|
|
1052
|
+
const char *getResultPtr(void) { return g_result; }
|
|
1053
|
+
|
|
1054
|
+
__attribute__((used))
|
|
1055
|
+
size_t getResultLen(void) { return g_result_len; }
|
|
1056
|
+
|
|
1057
|
+
__attribute__((used))
|
|
1058
|
+
void freeResult(void) {
|
|
1059
|
+
if (g_result) { free(g_result); g_result = NULL; g_result_len = 0; }
|
|
1060
|
+
}
|