lecodes-assets 0.2.1 → 2.5.0
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/README.md +203 -26
- package/dist/cli.js +18157 -11441
- package/dist/index.js +16603 -9878
- package/package.json +3 -1
- package/src/cli-kit/command.ts +100 -0
- package/src/cli-kit/help.ts +72 -0
- package/src/cli-kit/index.ts +4 -0
- package/src/cli-kit/parse.ts +72 -0
- package/src/cli-kit/run.ts +52 -0
- package/src/cli.ts +24 -1039
- package/src/commands/avatar.ts +77 -0
- package/src/commands/bvh.ts +26 -0
- package/src/commands/convert.ts +156 -0
- package/src/commands/doctor.ts +79 -0
- package/src/commands/geometry.ts +45 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/marks.ts +38 -0
- package/src/commands/mirror.ts +35 -0
- package/src/commands/probe.ts +43 -0
- package/src/commands/retarget/donor.ts +63 -0
- package/src/commands/retarget/index.ts +169 -0
- package/src/commands/retarget/map.ts +81 -0
- package/src/commands/retarget/report.ts +77 -0
- package/src/commands/scene.ts +108 -0
- package/src/commands/shared.ts +140 -0
- package/src/commands/sky.ts +35 -0
- package/src/commands/terrainPack.ts +40 -0
- package/src/commands/unpack.ts +43 -0
- package/src/commands/unreal.ts +109 -0
- package/src/commands/unwrap.ts +56 -0
- package/src/doctor/cardTrim.ts +288 -0
- package/src/doctor/doctor.ts +94 -30
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/merge/accessors.ts +30 -0
- package/src/doctor/merge/atlasLayout.ts +94 -0
- package/src/doctor/merge/atlasPixels.ts +114 -0
- package/src/doctor/merge/bindSpace.ts +59 -0
- package/src/doctor/merge/classify.ts +136 -0
- package/src/doctor/merge/geometry.ts +167 -0
- package/src/doctor/merge/index.ts +98 -0
- package/src/doctor/merge/matrix.ts +60 -0
- package/src/doctor/merge/plan.ts +88 -0
- package/src/doctor/merge/textures.ts +60 -0
- package/src/doctor/merge/types.ts +77 -0
- package/src/doctor/transcodeKtx2.ts +186 -31
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/validateGlb.ts +2 -2
- package/src/doctor/vertexAo.ts +333 -0
- package/src/fbx/read.ts +2 -0
- package/src/fbx/toDocument.ts +227 -209
- package/src/geometry.ts +222 -0
- package/src/index.ts +2 -0
- package/src/retarget/heading.ts +238 -171
- package/src/retarget/humanoid.ts +572 -561
- package/src/retarget/retarget.ts +2375 -2272
- package/src/retarget/rig.ts +4 -1
- package/src/retarget/writeMarks.ts +134 -132
- package/src/sky/cmgen.ts +3 -3
- package/src/sky/index.ts +57 -8
- package/src/terrainPack.ts +61 -22
- package/src/unity/colliderFit.ts +287 -0
- package/src/unity/fbxModel.ts +194 -0
- package/src/unity/foliage.ts +63 -0
- package/src/unity/gameObjects.ts +90 -0
- package/src/unity/guid.ts +104 -0
- package/src/unity/image.ts +58 -0
- package/src/unity/kindGlb.ts +364 -0
- package/src/unity/lecodesLevel.ts +414 -0
- package/src/unity/levelTerrain.ts +125 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +316 -0
- package/src/unity/materials.ts +34 -429
- package/src/unity/math.ts +82 -0
- package/src/unity/prefabTree.ts +285 -0
- package/src/unity/prefabs.ts +10 -36
- package/src/unity/propertyPath.ts +43 -0
- package/src/unity/scene.ts +339 -304
- package/src/unity/serialized.ts +242 -0
- package/src/unity/terrain.ts +221 -0
- package/src/unity/triBudget.ts +77 -0
- package/src/unity/triplanar.ts +122 -0
- package/src/unity/unityMesh.ts +151 -0
- package/src/unity/unpack.ts +24 -5
- package/src/unity/yaml.ts +131 -0
- package/src/unreal/index.ts +6 -5
- package/src/unreal/materialGraph.ts +224 -0
- package/src/unreal/materials.ts +117 -9
- package/src/unreal/package.ts +30 -12
- package/src/unreal/resolve.ts +6 -4
- package/src/unreal/sound.ts +128 -0
- package/src/unreal/texture.ts +70 -3
- package/src/unreal/toDocument.ts +7 -0
- package/src/unreal/versions.ts +8 -0
- package/src/unwrap/index.ts +167 -0
- package/src/unwrap/lightmapUv.ts +761 -0
- package/src/wasm/fbxReader.js +1 -1
- package/src/wasm/lightmapUv.js +1 -1
- package/src/doctor/lightmapUv.ts +0 -452
package/src/fbx/read.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type FbxPrimitive = {
|
|
|
22
22
|
vertexCount: number
|
|
23
23
|
indexCount: number
|
|
24
24
|
pos: number; nrm: number; uv: number; col: number; joints: number; weights: number; indices: number
|
|
25
|
+
/** UV sets 1..3 (Unity uv2 / uv3 / uv4) — a pack's detail layer lives there; -1 when the mesh has none. */
|
|
26
|
+
uv1: number; uv2: number; uv3: number
|
|
25
27
|
min: [number, number, number]
|
|
26
28
|
max: [number, number, number]
|
|
27
29
|
}
|
package/src/fbx/toDocument.ts
CHANGED
|
@@ -1,209 +1,227 @@
|
|
|
1
|
-
// FbxScene (IR from the ufbx wasm reader) → glTF-Transform Document. Nodes/meshes/skins/materials/
|
|
2
|
-
// textures/animations; the doctor passes and the writer then run on the Document like on any GLB.
|
|
3
|
-
|
|
4
|
-
import { Document, type Node, type Texture, type Material } from "@gltf-transform/core"
|
|
5
|
-
import { type FbxScene, type FbxTexture, f32, u16, u32, bytes } from "./read"
|
|
6
|
-
import { resolveTextureImage, type ResolvedImage } from "../textures/resolveImage"
|
|
7
|
-
|
|
8
|
-
export type ConvertWarning = { level: "warn" | "info", message: string }
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
mat.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
prim.
|
|
143
|
-
if (p.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1
|
+
// FbxScene (IR from the ufbx wasm reader) → glTF-Transform Document. Nodes/meshes/skins/materials/
|
|
2
|
+
// textures/animations; the doctor passes and the writer then run on the Document like on any GLB.
|
|
3
|
+
|
|
4
|
+
import { Document, type Node, type Texture, type Material } from "@gltf-transform/core"
|
|
5
|
+
import { type FbxScene, type FbxTexture, f32, u16, u32, bytes } from "./read"
|
|
6
|
+
import { resolveTextureImage, type ResolvedImage } from "../textures/resolveImage"
|
|
7
|
+
|
|
8
|
+
export type ConvertWarning = { level: "warn" | "info", message: string }
|
|
9
|
+
|
|
10
|
+
/** A COLOR_0 set (RGBA per vertex) that is black on EVERY vertex - a channel nobody authored, never a colour to multiply by. */
|
|
11
|
+
export function deadVertexColor(col: Float32Array): boolean {
|
|
12
|
+
for (let i = 0; i + 3 < col.length; i += 4) if (col[i] > 0 || col[i + 1] > 0 || col[i + 2] > 0) return false
|
|
13
|
+
return col.length > 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ToDocumentOptions = {
|
|
17
|
+
/** Directory of the source .fbx — external texture references are resolved against it. */
|
|
18
|
+
baseDir?: string
|
|
19
|
+
/** Name used for the file's single clip when its stack name is generic (`mixamo.com`, `Take 001`, `Unreal Take`). */
|
|
20
|
+
defaultClipName?: string
|
|
21
|
+
/** Skip meshes/skins/materials/textures (clip-only conversion). */
|
|
22
|
+
animationsOnly?: boolean
|
|
23
|
+
/** Also write the FBX's further UV sets as TEXCOORD_1..3 (off by default: TEXCOORD_1 is the lightmap set in
|
|
24
|
+
* every LeCodes GLB, and a pack's extra sets are shader inputs — the Unity kind builder reads them and
|
|
25
|
+
* drops them again). */
|
|
26
|
+
extraUvSets?: boolean
|
|
27
|
+
warn?: (w: ConvertWarning) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const GENERIC_CLIP_NAMES = new Set([ "", "mixamo.com", "take 001", "take001", "unreal take", "default take", "unnamed", "scene" ])
|
|
31
|
+
export const isGenericClipName = (name: string): boolean => GENERIC_CLIP_NAMES.has(name.trim().toLowerCase())
|
|
32
|
+
|
|
33
|
+
const isIdentityTransform = (t: number[], r: number[], s: number[]): boolean =>
|
|
34
|
+
Math.hypot(t[0], t[1], t[2]) < 1e-9 && Math.abs(r[3] - 1) < 1e-9 && Math.hypot(r[0], r[1], r[2]) < 1e-9 &&
|
|
35
|
+
Math.abs(s[0] - 1) < 1e-9 && Math.abs(s[1] - 1) < 1e-9 && Math.abs(s[2] - 1) < 1e-9
|
|
36
|
+
|
|
37
|
+
/** Make consecutive quaternions hemisphere-consistent so LINEAR (slerp) interpolation never takes the long way. */
|
|
38
|
+
export const fixQuatContinuity = (q: Float32Array): void => {
|
|
39
|
+
for (let i = 4; i < q.length; i += 4) {
|
|
40
|
+
const dot = q[i] * q[i - 4] + q[i + 1] * q[i - 3] + q[i + 2] * q[i - 2] + q[i + 3] * q[i - 1]
|
|
41
|
+
if (dot < 0) { q[i] = -q[i]; q[i + 1] = -q[i + 1]; q[i + 2] = -q[i + 2]; q[i + 3] = -q[i + 3] }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ToDocumentResult = {
|
|
46
|
+
doc: Document
|
|
47
|
+
/** glTF node per FBX node index (null for the ufbx root when it was flattened away). */
|
|
48
|
+
nodes: (Node | null)[]
|
|
49
|
+
clipNames: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const toDocument = async (scene: FbxScene, opts: ToDocumentOptions = {}): Promise<ToDocumentResult> => {
|
|
53
|
+
const warn = opts.warn ?? (() => {})
|
|
54
|
+
const doc = new Document()
|
|
55
|
+
const buffer = doc.createBuffer("buffer")
|
|
56
|
+
const gscene = doc.createScene("Scene")
|
|
57
|
+
|
|
58
|
+
// ---- nodes ----
|
|
59
|
+
const nodes: (Node | null)[] = new Array(scene.nodes.length).fill(null)
|
|
60
|
+
const rootIdx = scene.nodes.findIndex((n) => n.isRoot)
|
|
61
|
+
const root = rootIdx >= 0 ? scene.nodes[rootIdx] : undefined
|
|
62
|
+
let rootNode: Node | null = null
|
|
63
|
+
if (root && !isIdentityTransform(root.t, root.r, root.s)) {
|
|
64
|
+
rootNode = doc.createNode(root.name || "RootNode").setTranslation(root.t).setRotation(root.r).setScale(root.s)
|
|
65
|
+
gscene.addChild(rootNode)
|
|
66
|
+
nodes[rootIdx] = rootNode
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < scene.nodes.length; i++) {
|
|
69
|
+
const n = scene.nodes[i]
|
|
70
|
+
if (n.isRoot) continue
|
|
71
|
+
const g = doc.createNode(n.name).setTranslation(n.t).setRotation(n.r).setScale(n.s)
|
|
72
|
+
nodes[i] = g
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < scene.nodes.length; i++) {
|
|
75
|
+
const n = scene.nodes[i]
|
|
76
|
+
const g = nodes[i]
|
|
77
|
+
if (!g || n.isRoot) continue
|
|
78
|
+
const parent = n.parent >= 0 ? nodes[n.parent] : null
|
|
79
|
+
if (parent) parent.addChild(g)
|
|
80
|
+
else gscene.addChild(g)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!opts.animationsOnly) {
|
|
84
|
+
// ---- textures ----
|
|
85
|
+
const textures: (Texture | null)[] = []
|
|
86
|
+
const byHash = new Map<string, Texture>()
|
|
87
|
+
for (let i = 0; i < scene.textures.length; i++) {
|
|
88
|
+
const t = scene.textures[i]
|
|
89
|
+
const img = await resolveTextureImage(t, t.content >= 0 ? bytes(scene, t.content, t.size) : null, opts.baseDir, warn)
|
|
90
|
+
if (!img) { textures.push(null); continue }
|
|
91
|
+
const key = img.hash
|
|
92
|
+
let tex = byHash.get(key)
|
|
93
|
+
if (!tex) {
|
|
94
|
+
tex = doc.createTexture(textureName(t, img)).setImage(img.data).setMimeType(img.mimeType)
|
|
95
|
+
byHash.set(key, tex)
|
|
96
|
+
}
|
|
97
|
+
textures.push(tex)
|
|
98
|
+
}
|
|
99
|
+
const texAt = (i: number): Texture | null => (i >= 0 && i < textures.length ? textures[i] : null)
|
|
100
|
+
|
|
101
|
+
// ---- materials ----
|
|
102
|
+
const materials: Material[] = scene.materials.map((m) => {
|
|
103
|
+
const mat = doc.createMaterial(m.name)
|
|
104
|
+
mat.setBaseColorFactor([ clamp01(m.baseColor[0]), clamp01(m.baseColor[1]), clamp01(m.baseColor[2]), clamp01(m.baseColor[3]) ])
|
|
105
|
+
const base = texAt(m.baseTex)
|
|
106
|
+
if (base) { mat.setBaseColorTexture(base); mat.setBaseColorFactor([ 1, 1, 1, clamp01(m.baseColor[3]) ]) }
|
|
107
|
+
mat.setMetallicFactor(clamp01(m.metallic))
|
|
108
|
+
mat.setRoughnessFactor(clamp01(m.roughness))
|
|
109
|
+
// glTF packs metallic (B) + roughness (G) into ONE texture; FBX keeps them apart. Only when both
|
|
110
|
+
// point at the same image can we forward it — otherwise the factors above stand in.
|
|
111
|
+
if (m.metallicTex >= 0 && m.metallicTex === m.roughnessTex) {
|
|
112
|
+
const mr = texAt(m.metallicTex); if (mr) { mat.setMetallicRoughnessTexture(mr); mat.setMetallicFactor(1); mat.setRoughnessFactor(1) }
|
|
113
|
+
} else if (m.metallicTex >= 0 || m.roughnessTex >= 0) {
|
|
114
|
+
warn({ level: "info", message: `material '${m.name}': separate metallic/roughness maps are not packed (factors used)` })
|
|
115
|
+
}
|
|
116
|
+
const nrm = texAt(m.normalTex); if (nrm) mat.setNormalTexture(nrm)
|
|
117
|
+
const occ = texAt(m.occlusionTex); if (occ) mat.setOcclusionTexture(occ)
|
|
118
|
+
const em = texAt(m.emissiveTex)
|
|
119
|
+
if (em) { mat.setEmissiveTexture(em); mat.setEmissiveFactor([ 1, 1, 1 ]) }
|
|
120
|
+
else mat.setEmissiveFactor([ clamp01(m.emissive[0]), clamp01(m.emissive[1]), clamp01(m.emissive[2]) ])
|
|
121
|
+
// alpha: an opacity map → cutout (hair cards, leaves); a factor < 1 → blend
|
|
122
|
+
if (m.opacityTex >= 0) {
|
|
123
|
+
mat.setAlphaMode("MASK").setAlphaCutoff(0.5)
|
|
124
|
+
if (!base && texAt(m.opacityTex)) mat.setBaseColorTexture(texAt(m.opacityTex)!)
|
|
125
|
+
} else if (m.opacity < 0.999) {
|
|
126
|
+
mat.setAlphaMode("BLEND")
|
|
127
|
+
const f = mat.getBaseColorFactor(); mat.setBaseColorFactor([ f[0], f[1], f[2], clamp01(m.opacity) ])
|
|
128
|
+
}
|
|
129
|
+
mat.setDoubleSided(m.doubleSided)
|
|
130
|
+
return mat
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// ---- meshes ----
|
|
134
|
+
// An FBX geometry is often unnamed (Maya exports, "wolf" → mesh ""); name it after the node that
|
|
135
|
+
// carries it so probe/doctor/pick-by-name have something to print.
|
|
136
|
+
const meshNodeName: string[] = new Array(scene.meshes.length).fill("")
|
|
137
|
+
for (const n of scene.nodes) if (n.mesh >= 0 && n.mesh < meshNodeName.length && !meshNodeName[n.mesh]) meshNodeName[n.mesh] = n.name
|
|
138
|
+
const meshes = scene.meshes.map((m, mi) => {
|
|
139
|
+
const mesh = doc.createMesh(m.name || meshNodeName[mi])
|
|
140
|
+
for (const p of m.primitives) {
|
|
141
|
+
const prim = doc.createPrimitive()
|
|
142
|
+
prim.setAttribute("POSITION", doc.createAccessor().setType("VEC3").setArray(f32(scene, p.pos, p.vertexCount * 3).slice()).setBuffer(buffer))
|
|
143
|
+
if (p.nrm >= 0) prim.setAttribute("NORMAL", doc.createAccessor().setType("VEC3").setArray(f32(scene, p.nrm, p.vertexCount * 3).slice()).setBuffer(buffer))
|
|
144
|
+
if (p.uv >= 0) prim.setAttribute("TEXCOORD_0", doc.createAccessor().setType("VEC2").setArray(f32(scene, p.uv, p.vertexCount * 2).slice()).setBuffer(buffer))
|
|
145
|
+
if (opts.extraUvSets) for (const [ k, off ] of [ p.uv1, p.uv2, p.uv3 ].entries()) if (off !== undefined && off >= 0) prim.setAttribute(`TEXCOORD_${k + 1}`, doc.createAccessor().setType("VEC2").setArray(f32(scene, off, p.vertexCount * 2).slice()).setBuffer(buffer))
|
|
146
|
+
if (p.col >= 0) {
|
|
147
|
+
const col = f32(scene, p.col, p.vertexCount * 4).slice()
|
|
148
|
+
// An all-black colour set is an UNUSED channel: Unity's Standard shader never reads vertex colour, so a pack
|
|
149
|
+
// ships whatever its DCC left there (the Military FPS hands: zeros on the skin mesh) - while every glTF
|
|
150
|
+
// renderer multiplies it into the base colour, and the mesh comes out black. Dropped, with a note.
|
|
151
|
+
if (deadVertexColor(col)) warn({ level: "info", message: `mesh '${m.name || meshNodeName[mi]}': all-black COLOR_0 dropped (it would multiply the base colour to black)` })
|
|
152
|
+
else prim.setAttribute("COLOR_0", doc.createAccessor().setType("VEC4").setArray(col).setBuffer(buffer))
|
|
153
|
+
}
|
|
154
|
+
if (p.joints >= 0 && p.weights >= 0) {
|
|
155
|
+
prim.setAttribute("JOINTS_0", doc.createAccessor().setType("VEC4").setArray(u16(scene, p.joints, p.vertexCount * 4).slice()).setBuffer(buffer))
|
|
156
|
+
prim.setAttribute("WEIGHTS_0", doc.createAccessor().setType("VEC4").setArray(f32(scene, p.weights, p.vertexCount * 4).slice()).setBuffer(buffer))
|
|
157
|
+
}
|
|
158
|
+
const idx = u32(scene, p.indices, p.indexCount)
|
|
159
|
+
const indices = p.vertexCount <= 65535 ? Uint16Array.from(idx) : idx.slice()
|
|
160
|
+
prim.setIndices(doc.createAccessor().setType("SCALAR").setArray(indices).setBuffer(buffer))
|
|
161
|
+
if (p.material >= 0 && p.material < materials.length) prim.setMaterial(materials[p.material])
|
|
162
|
+
mesh.addPrimitive(prim)
|
|
163
|
+
}
|
|
164
|
+
return mesh
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// ---- skins ----
|
|
168
|
+
const skins = scene.skins.map((s) => {
|
|
169
|
+
const skin = doc.createSkin(s.name)
|
|
170
|
+
const ibm = f32(scene, s.ibm, s.joints.length * 16).slice()
|
|
171
|
+
for (let j = 0; j < s.joints.length; j++) {
|
|
172
|
+
const jn = s.joints[j] >= 0 ? nodes[s.joints[j]] : null
|
|
173
|
+
if (jn) skin.addJoint(jn)
|
|
174
|
+
else {
|
|
175
|
+
// a cluster without a bone: bind it to the first available node so joint indices stay aligned
|
|
176
|
+
const fallback = nodes.find((n) => n) ?? doc.createNode("missing-joint")
|
|
177
|
+
skin.addJoint(fallback)
|
|
178
|
+
warn({ level: "warn", message: `skin '${s.name}': cluster ${j} has no bone node — bound to a placeholder` })
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
skin.setInverseBindMatrices(doc.createAccessor().setType("MAT4").setArray(ibm).setBuffer(buffer))
|
|
182
|
+
return skin
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < scene.nodes.length; i++) {
|
|
186
|
+
const n = scene.nodes[i]
|
|
187
|
+
const g = nodes[i]
|
|
188
|
+
if (!g) continue
|
|
189
|
+
if (n.mesh >= 0 && n.mesh < meshes.length) g.setMesh(meshes[n.mesh])
|
|
190
|
+
if (n.skin >= 0 && n.skin < skins.length) g.setSkin(skins[n.skin])
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- animations ----
|
|
195
|
+
const emitted = scene.animations.filter((a) => a.tracks.length > 0)
|
|
196
|
+
const clipNames: string[] = []
|
|
197
|
+
for (const a of emitted) {
|
|
198
|
+
let name = a.name
|
|
199
|
+
if (emitted.length === 1 && isGenericClipName(name) && opts.defaultClipName) name = opts.defaultClipName
|
|
200
|
+
clipNames.push(name)
|
|
201
|
+
const anim = doc.createAnimation(name)
|
|
202
|
+
for (const tr of a.tracks) {
|
|
203
|
+
const target = nodes[tr.node]
|
|
204
|
+
if (!target) continue
|
|
205
|
+
const times = f32(scene, tr.times, tr.count).slice()
|
|
206
|
+
const comps = tr.path === "rotation" ? 4 : 3
|
|
207
|
+
const values = f32(scene, tr.values, tr.count * comps).slice()
|
|
208
|
+
if (tr.path === "rotation") fixQuatContinuity(values)
|
|
209
|
+
const sampler = doc.createAnimationSampler()
|
|
210
|
+
.setInput(doc.createAccessor().setType("SCALAR").setArray(times).setBuffer(buffer))
|
|
211
|
+
.setOutput(doc.createAccessor().setType(comps === 4 ? "VEC4" : "VEC3").setArray(values).setBuffer(buffer))
|
|
212
|
+
.setInterpolation("LINEAR")
|
|
213
|
+
const channel = doc.createAnimationChannel().setTargetNode(target).setTargetPath(tr.path).setSampler(sampler)
|
|
214
|
+
anim.addSampler(sampler).addChannel(channel)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { doc, nodes, clipNames }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v)
|
|
222
|
+
|
|
223
|
+
const textureName = (t: FbxTexture, img: ResolvedImage): string => {
|
|
224
|
+
const src = t.relative || t.filename || t.absolute || t.name
|
|
225
|
+
const base = src.split(/[\\/]/).pop() ?? t.name
|
|
226
|
+
return (base.replace(/\.[^.]+$/, "") || t.name || "texture") + (img.converted ? "" : "")
|
|
227
|
+
}
|