lecodes-assets 0.2.1 → 2.4.10

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.
Files changed (97) hide show
  1. package/README.md +203 -26
  2. package/dist/cli.js +17948 -11911
  3. package/dist/index.js +17525 -11479
  4. package/package.json +3 -1
  5. package/src/cli-kit/command.ts +100 -0
  6. package/src/cli-kit/help.ts +72 -0
  7. package/src/cli-kit/index.ts +4 -0
  8. package/src/cli-kit/parse.ts +72 -0
  9. package/src/cli-kit/run.ts +52 -0
  10. package/src/cli.ts +24 -1039
  11. package/src/commands/avatar.ts +77 -0
  12. package/src/commands/bvh.ts +26 -0
  13. package/src/commands/convert.ts +156 -0
  14. package/src/commands/doctor.ts +67 -0
  15. package/src/commands/geometry.ts +45 -0
  16. package/src/commands/index.ts +24 -0
  17. package/src/commands/marks.ts +38 -0
  18. package/src/commands/mirror.ts +35 -0
  19. package/src/commands/probe.ts +43 -0
  20. package/src/commands/retarget/donor.ts +63 -0
  21. package/src/commands/retarget/index.ts +169 -0
  22. package/src/commands/retarget/map.ts +81 -0
  23. package/src/commands/retarget/report.ts +77 -0
  24. package/src/commands/scene.ts +103 -0
  25. package/src/commands/shared.ts +121 -0
  26. package/src/commands/sky.ts +35 -0
  27. package/src/commands/terrainPack.ts +40 -0
  28. package/src/commands/unpack.ts +43 -0
  29. package/src/commands/unreal.ts +109 -0
  30. package/src/commands/unwrap.ts +56 -0
  31. package/src/doctor/cardTrim.ts +286 -0
  32. package/src/doctor/doctor.ts +94 -30
  33. package/src/doctor/merge/accessors.ts +30 -0
  34. package/src/doctor/merge/atlasLayout.ts +94 -0
  35. package/src/doctor/merge/atlasPixels.ts +114 -0
  36. package/src/doctor/merge/bindSpace.ts +59 -0
  37. package/src/doctor/merge/classify.ts +136 -0
  38. package/src/doctor/merge/geometry.ts +167 -0
  39. package/src/doctor/merge/index.ts +98 -0
  40. package/src/doctor/merge/matrix.ts +60 -0
  41. package/src/doctor/merge/plan.ts +88 -0
  42. package/src/doctor/merge/textures.ts +60 -0
  43. package/src/doctor/merge/types.ts +77 -0
  44. package/src/doctor/transcodeKtx2.ts +179 -28
  45. package/src/doctor/validateGlb.ts +2 -2
  46. package/src/doctor/vertexAo.ts +321 -0
  47. package/src/fbx/read.ts +2 -0
  48. package/src/fbx/toDocument.ts +227 -209
  49. package/src/geometry.ts +222 -0
  50. package/src/index.ts +2 -0
  51. package/src/retarget/heading.ts +238 -171
  52. package/src/retarget/humanoid.ts +572 -561
  53. package/src/retarget/retarget.ts +2375 -2272
  54. package/src/retarget/rig.ts +4 -1
  55. package/src/retarget/writeMarks.ts +134 -132
  56. package/src/sky/cmgen.ts +3 -3
  57. package/src/sky/index.ts +57 -8
  58. package/src/terrainPack.ts +61 -22
  59. package/src/unity/colliderFit.ts +287 -0
  60. package/src/unity/fbxModel.ts +194 -0
  61. package/src/unity/foliage.ts +63 -0
  62. package/src/unity/gameObjects.ts +81 -0
  63. package/src/unity/guid.ts +104 -0
  64. package/src/unity/image.ts +58 -0
  65. package/src/unity/kindGlb.ts +364 -0
  66. package/src/unity/lecodesLevel.ts +381 -0
  67. package/src/unity/levelTerrain.ts +116 -0
  68. package/src/unity/matFile.ts +179 -0
  69. package/src/unity/materialSlots.ts +53 -0
  70. package/src/unity/materialWriter.ts +306 -0
  71. package/src/unity/materials.ts +34 -429
  72. package/src/unity/math.ts +82 -0
  73. package/src/unity/prefabTree.ts +285 -0
  74. package/src/unity/prefabs.ts +10 -36
  75. package/src/unity/propertyPath.ts +43 -0
  76. package/src/unity/scene.ts +325 -304
  77. package/src/unity/serialized.ts +242 -0
  78. package/src/unity/terrain.ts +221 -0
  79. package/src/unity/triBudget.ts +77 -0
  80. package/src/unity/triplanar.ts +122 -0
  81. package/src/unity/unityMesh.ts +151 -0
  82. package/src/unity/unpack.ts +24 -5
  83. package/src/unity/yaml.ts +131 -0
  84. package/src/unreal/index.ts +6 -5
  85. package/src/unreal/materialGraph.ts +224 -0
  86. package/src/unreal/materials.ts +117 -9
  87. package/src/unreal/package.ts +30 -12
  88. package/src/unreal/resolve.ts +6 -4
  89. package/src/unreal/sound.ts +128 -0
  90. package/src/unreal/texture.ts +70 -3
  91. package/src/unreal/toDocument.ts +7 -0
  92. package/src/unreal/versions.ts +8 -0
  93. package/src/unwrap/index.ts +167 -0
  94. package/src/unwrap/lightmapUv.ts +761 -0
  95. package/src/wasm/fbxReader.js +1 -1
  96. package/src/wasm/lightmapUv.js +1 -1
  97. package/src/doctor/lightmapUv.ts +0 -452
@@ -0,0 +1,321 @@
1
+ // VERTEX AMBIENT OCCLUSION FOR VEGETATION — the sky's visibility from every vertex of a plant, baked into
2
+ // COLOR_0.r at import (what SpeedTree bakes for its trees, and the one thing that makes a crown read as
3
+ // a volume instead of a heap of green cards: the leaves deep inside take less sky than the ones on the
4
+ // outside, and the trunk under the crown stands in its shade).
5
+ //
6
+ // The convention the vegetation tier reads (creator-gl materials/src/foliage.mat):
7
+ // COLOR_0 = (ao, cavity, 1, wind) ao = sky visibility 0..1 (cosine-weighted upper hemisphere, WORLD up)
8
+ // cavity = the bark's short-range occlusion 0..1 (the hemisphere of its own normal,
9
+ // hits within `cavity` metres: limb joints, the trunk under a limb); cards 1
10
+ // wind = the per-vertex wind weight — the pack's own (COLOR_0.a as it came)
11
+ // or, for a mesh without one, the height over the plant squared
12
+ // An exact white (1, 1, 1, 1) is what gltfio hands a mesh WITHOUT a colour set, and the shader takes that
13
+ // as "no data" (ao 1, height weight) — a vertex whose ao AND wind both round to 1 lands there too, and it
14
+ // is exactly the vertex nothing changes for.
15
+ //
16
+ // Occluders = every triangle of the asset's level-0 meshes (`_LOD<n>` n ≥ 1 are copies of the same
17
+ // plant), cards counted opaque — a coarse hit for an alpha-cutout leaf, which over-darkens a little and
18
+ // is what the shader's strength knob is for. A BVH over the triangles, `rays` cosine-weighted rays per
19
+ // vertex from a fixed sequence rotated per vertex, rays start `bias` metres off the surface.
20
+
21
+ import type { Document, Primitive } from "@gltf-transform/core"
22
+
23
+ type Tri = { ax: number, ay: number, az: number, bx: number, by: number, bz: number, cx: number, cy: number, cz: number }
24
+ type BvhNode = { min: [number, number, number], max: [number, number, number], left: number, right: number, first: number, count: number }
25
+
26
+ const buildBvh = (tris: Tri[]): { nodes: BvhNode[], order: number[] } => {
27
+ const order = tris.map((_, i) => i)
28
+ const nodes: BvhNode[] = []
29
+ const cx = (t: Tri): number => (t.ax + t.bx + t.cx) / 3, cy = (t: Tri): number => (t.ay + t.by + t.cy) / 3, cz = (t: Tri): number => (t.az + t.bz + t.cz) / 3
30
+ const build = (first: number, count: number): number => {
31
+ const min: [number, number, number] = [ Infinity, Infinity, Infinity ], max: [number, number, number] = [ -Infinity, -Infinity, -Infinity ]
32
+ for (let i = first; i < first + count; i++) {
33
+ const t = tris[order[i]]
34
+ min[0] = Math.min(min[0], t.ax, t.bx, t.cx); min[1] = Math.min(min[1], t.ay, t.by, t.cy); min[2] = Math.min(min[2], t.az, t.bz, t.cz)
35
+ max[0] = Math.max(max[0], t.ax, t.bx, t.cx); max[1] = Math.max(max[1], t.ay, t.by, t.cy); max[2] = Math.max(max[2], t.az, t.bz, t.cz)
36
+ }
37
+ const id = nodes.length
38
+ nodes.push({ min, max, left: -1, right: -1, first, count })
39
+ if (count <= 8) return id
40
+ const ext = [ max[0] - min[0], max[1] - min[1], max[2] - min[2] ]
41
+ const axis = ext[0] >= ext[1] && ext[0] >= ext[2] ? 0 : ext[1] >= ext[2] ? 1 : 2
42
+ const key = axis === 0 ? cx : axis === 1 ? cy : cz
43
+ const slice = order.slice(first, first + count).sort((a, b) => key(tris[a]) - key(tris[b]))
44
+ for (let i = 0; i < count; i++) order[first + i] = slice[i]
45
+ const half = count >> 1
46
+ const n = nodes[id]
47
+ n.count = 0
48
+ n.left = build(first, half)
49
+ n.right = build(first + half, count - half)
50
+ return id
51
+ }
52
+ if (tris.length) build(0, tris.length)
53
+ return { nodes, order }
54
+ }
55
+
56
+ /** Möller–Trumbore, both sides: the hit distance in (tmin, tmax), or -1. */
57
+ const triDist = (t: Tri, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tmin: number, tmax: number): number => {
58
+ const e1x = t.bx - t.ax, e1y = t.by - t.ay, e1z = t.bz - t.az
59
+ const e2x = t.cx - t.ax, e2y = t.cy - t.ay, e2z = t.cz - t.az
60
+ const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x
61
+ const det = e1x * px + e1y * py + e1z * pz
62
+ if (Math.abs(det) < 1e-9) return -1
63
+ const inv = 1 / det
64
+ const tx = ox - t.ax, ty = oy - t.ay, tz = oz - t.az
65
+ const u = (tx * px + ty * py + tz * pz) * inv
66
+ if (u < 0 || u > 1) return -1
67
+ const qx = ty * e1z - tz * e1y, qy = tz * e1x - tx * e1z, qz = tx * e1y - ty * e1x
68
+ const v = (dx * qx + dy * qy + dz * qz) * inv
69
+ if (v < 0 || u + v > 1) return -1
70
+ const d = (e2x * qx + e2y * qy + e2z * qz) * inv
71
+ return d > tmin && d < tmax ? d : -1
72
+ }
73
+ const hitTri = (t: Tri, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tmin: number, tmax: number): boolean =>
74
+ triDist(t, ox, oy, oz, dx, dy, dz, tmin, tmax) >= 0
75
+
76
+ const hitBox = (n: BvhNode, ox: number, oy: number, oz: number, ix: number, iy: number, iz: number, tmax: number): boolean => {
77
+ let t0 = 0, t1 = tmax
78
+ const a = (n.min[0] - ox) * ix, b = (n.max[0] - ox) * ix; t0 = Math.max(t0, Math.min(a, b)); t1 = Math.min(t1, Math.max(a, b))
79
+ const c = (n.min[1] - oy) * iy, d = (n.max[1] - oy) * iy; t0 = Math.max(t0, Math.min(c, d)); t1 = Math.min(t1, Math.max(c, d))
80
+ const e = (n.min[2] - oz) * iz, f = (n.max[2] - oz) * iz; t0 = Math.max(t0, Math.min(e, f)); t1 = Math.min(t1, Math.max(e, f))
81
+ return t1 >= t0
82
+ }
83
+
84
+ const occluded = (bvh: { nodes: BvhNode[], order: number[] }, tris: Tri[], ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tmin: number, tmax: number): boolean => {
85
+ if (!bvh.nodes.length) return false
86
+ const ix = 1 / (dx || 1e-12), iy = 1 / (dy || 1e-12), iz = 1 / (dz || 1e-12)
87
+ const stack = [ 0 ]
88
+ while (stack.length) {
89
+ const n = bvh.nodes[stack.pop()!]
90
+ if (!hitBox(n, ox, oy, oz, ix, iy, iz, tmax)) continue
91
+ if (n.left < 0) {
92
+ for (let i = n.first; i < n.first + n.count; i++) if (hitTri(tris[bvh.order[i]], ox, oy, oz, dx, dy, dz, tmin, tmax)) return true
93
+ } else { stack.push(n.left, n.right) }
94
+ }
95
+ return false
96
+ }
97
+
98
+ /** The nearest hit along the ray in (tmin, tmax), or Infinity. */
99
+ const nearestHit = (bvh: { nodes: BvhNode[], order: number[] }, tris: Tri[], ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tmin: number, tmax: number): number => {
100
+ let best = Infinity
101
+ if (!bvh.nodes.length) return best
102
+ const ix = 1 / (dx || 1e-12), iy = 1 / (dy || 1e-12), iz = 1 / (dz || 1e-12)
103
+ const stack = [ 0 ]
104
+ while (stack.length) {
105
+ const n = bvh.nodes[stack.pop()!]
106
+ if (!hitBox(n, ox, oy, oz, ix, iy, iz, Math.min(tmax, best))) continue
107
+ if (n.left < 0) {
108
+ for (let i = n.first; i < n.first + n.count; i++) {
109
+ const d = triDist(tris[bvh.order[i]], ox, oy, oz, dx, dy, dz, tmin, Math.min(tmax, best))
110
+ if (d >= 0 && d < best) best = d
111
+ }
112
+ } else { stack.push(n.left, n.right) }
113
+ }
114
+ return best
115
+ }
116
+
117
+ export type VertexAoOptions = {
118
+ /** Rays per vertex (default 48). */
119
+ rays?: number
120
+ /** Metres a ray starts off its vertex (default 0.02). */
121
+ bias?: number
122
+ /** Metres a ray travels (default: the asset's extent). */
123
+ maxDistance?: number
124
+ /** CAVITY (junction) occlusion for the OPAQUE primitives — the bark: `cavityRays` cosine-weighted rays in the
125
+ * hemisphere of the vertex' own normal, a hit within `cavity` metres counts by (1 - distance / cavity) → COLOR_0.g,
126
+ * 1 = open. The base of a limb and the trunk around it darken, the joint reads as grown, not laid on. Leaf cards
127
+ * keep 1 (their own neighbours would darken every crown). Default 0.8 m, 0 = off. */
128
+ cavity?: number
129
+ /** Rays per vertex for the cavity term (default 24). */
130
+ cavityRays?: number
131
+ /** CANOPY NORMALS: how far the normals of the alpha-MASK primitives (the leaf cards) are bent toward the
132
+ * direction from the crown's centre — 0 keeps the cards' own (each card lit as a flat plane: a heap of
133
+ * cut-outs), 1 makes the crown shade like one sphere. Default 0.6 — what a tree tool bakes. */
134
+ canopy?: number
135
+ }
136
+
137
+ const LOD_RE = /_LOD([1-9]\d*)$/i
138
+
139
+ /** World-space triangles of every level-0 mesh node. */
140
+ const collectOccluders = (doc: Document): { tris: Tri[], min: [number, number, number], max: [number, number, number] } => {
141
+ const tris: Tri[] = []
142
+ const min: [number, number, number] = [ Infinity, Infinity, Infinity ], max: [number, number, number] = [ -Infinity, -Infinity, -Infinity ]
143
+ for (const node of doc.getRoot().listNodes()) {
144
+ const mesh = node.getMesh()
145
+ if (!mesh || LOD_RE.test(node.getName())) continue
146
+ const m = node.getWorldMatrix()
147
+ for (const p of mesh.listPrimitives()) {
148
+ const pos = p.getAttribute("POSITION"), idx = p.getIndices()
149
+ if (!pos || p.getMode() !== 4) continue
150
+ const n = idx ? idx.getCount() : pos.getCount()
151
+ const v: number[] = []
152
+ const at = (k: number): [number, number, number] => {
153
+ pos.getElement(idx ? idx.getScalar(k) : k, v)
154
+ const x = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12], y = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13], z = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14]
155
+ min[0] = Math.min(min[0], x); min[1] = Math.min(min[1], y); min[2] = Math.min(min[2], z)
156
+ max[0] = Math.max(max[0], x); max[1] = Math.max(max[1], y); max[2] = Math.max(max[2], z)
157
+ return [ x, y, z ]
158
+ }
159
+ for (let k = 0; k + 2 < n; k += 3) {
160
+ const a = at(k), b = at(k + 1), c = at(k + 2)
161
+ tris.push({ ax: a[0], ay: a[1], az: a[2], bx: b[0], by: b[1], bz: b[2], cx: c[0], cy: c[1], cz: c[2] })
162
+ }
163
+ }
164
+ }
165
+ return { tris, min, max }
166
+ }
167
+
168
+ /** Cosine-weighted directions on the upper hemisphere (world +Y), a fixed Fibonacci set. */
169
+ const skyRays = (count: number): [number, number, number][] => {
170
+ const out: [number, number, number][] = []
171
+ const golden = Math.PI * (3 - Math.sqrt(5))
172
+ for (let i = 0; i < count; i++) {
173
+ const u = (i + 0.5) / count // cosine-weighted: y = sqrt(1 - u) puts more rays near the zenith
174
+ const y = Math.sqrt(1 - u), r = Math.sqrt(u), phi = i * golden
175
+ out.push([ r * Math.cos(phi), y, r * Math.sin(phi) ])
176
+ }
177
+ return out
178
+ }
179
+
180
+ /**
181
+ * Bake the sky visibility of every vertex of the document's meshes into COLOR_0 as (ao, 1, 1, wind) — see the
182
+ * header. Returns the number of vertices written and the mean ao (a plant reads ~0.5–0.7).
183
+ */
184
+ export const bakeVertexAo = (doc: Document, opts: VertexAoOptions = {}): { vertices: number, meanAo: number } => {
185
+ const { tris, min, max } = collectOccluders(doc)
186
+ if (!tris.length) return { vertices: 0, meanAo: 1 }
187
+ const bvh = buildBvh(tris)
188
+ const rays = skyRays(opts.rays ?? 48)
189
+ const bias = opts.bias ?? 0.02
190
+ const reach = opts.maxDistance ?? Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2])
191
+ const cavReach = Math.max(opts.cavity ?? 0.8, 0)
192
+ const cavRays = skyRays(opts.cavityRays ?? 24)
193
+ const height = Math.max(max[1] - min[1], 0.05)
194
+ let vertices = 0, sum = 0
195
+ const done = new Set<Primitive>()
196
+ // the crown: the centre of every leaf card's vertices (world), what the canopy normals point away from
197
+ const canopy = Math.min(Math.max(opts.canopy ?? 0.6, 0), 1)
198
+ const crown = [ 0, 0, 0 ]
199
+ let crownN = 0
200
+ if (canopy > 0) {
201
+ for (const node of doc.getRoot().listNodes()) {
202
+ const mesh = node.getMesh()
203
+ if (!mesh || LOD_RE.test(node.getName())) continue
204
+ const m = node.getWorldMatrix()
205
+ for (const p of mesh.listPrimitives()) {
206
+ if (p.getMaterial()?.getAlphaMode() !== "MASK") continue
207
+ const pos = p.getAttribute("POSITION"); if (!pos) continue
208
+ const v: number[] = []
209
+ for (let i = 0; i < pos.getCount(); i++) { pos.getElement(i, v); crown[0] += m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12]; crown[1] += m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13]; crown[2] += m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14]; crownN++ }
210
+ }
211
+ }
212
+ if (crownN) { crown[0] /= crownN; crown[1] /= crownN; crown[2] /= crownN }
213
+ }
214
+ for (const node of doc.getRoot().listNodes()) {
215
+ const mesh = node.getMesh()
216
+ if (!mesh) continue
217
+ const m = node.getWorldMatrix()
218
+ for (const p of mesh.listPrimitives()) {
219
+ if (done.has(p)) continue
220
+ done.add(p)
221
+ const pos = p.getAttribute("POSITION")
222
+ if (!pos) continue
223
+ const n = pos.getCount()
224
+ // canopy normals on the leaf cards: the card's normal blended toward "away from the crown's centre" (in the
225
+ // node's frame: the crown centre pulled back through the inverse world matrix, rotation-only for the normal)
226
+ const nrm = p.getAttribute("NORMAL")
227
+ if (canopy > 0 && crownN && nrm && p.getMaterial()?.getAlphaMode() === "MASK") {
228
+ const inv = invert4(m)
229
+ const cl = inv ? [ inv[0] * crown[0] + inv[4] * crown[1] + inv[8] * crown[2] + inv[12], inv[1] * crown[0] + inv[5] * crown[1] + inv[9] * crown[2] + inv[13], inv[2] * crown[0] + inv[6] * crown[1] + inv[10] * crown[2] + inv[14] ] : null
230
+ if (cl) {
231
+ const out = new Float32Array(new ArrayBuffer(n * 12)), v: number[] = [], q: number[] = []
232
+ for (let i = 0; i < n; i++) {
233
+ pos.getElement(i, v); nrm.getElement(i, q)
234
+ let dx = v[0] - cl[0], dy = v[1] - cl[1], dz = v[2] - cl[2]
235
+ const l = Math.hypot(dx, dy, dz) || 1
236
+ dx /= l; dy /= l; dz /= l
237
+ // a card facing the wrong way is flipped into the hemisphere of its radial first (double-sided cards
238
+ // have no side of their own), then blended
239
+ const flip = q[0] * dx + q[1] * dy + q[2] * dz < 0 ? -1 : 1
240
+ let nx = q[0] * flip * (1 - canopy) + dx * canopy, ny = q[1] * flip * (1 - canopy) + dy * canopy, nz = q[2] * flip * (1 - canopy) + dz * canopy
241
+ const nl = Math.hypot(nx, ny, nz) || 1
242
+ out[i * 3] = nx / nl; out[i * 3 + 1] = ny / nl; out[i * 3 + 2] = nz / nl
243
+ }
244
+ p.setAttribute("NORMAL", doc.createAccessor().setType("VEC3").setArray(out).setBuffer(doc.getRoot().listBuffers()[0] ?? doc.createBuffer()))
245
+ if (nrm.listParents().length <= 1) nrm.dispose()
246
+ }
247
+ }
248
+ const old = p.getAttribute("COLOR_0")
249
+ const oldEl = old ? old.getElementSize() : 0
250
+ const out = new Float32Array(new ArrayBuffer(n * 16))
251
+ const v: number[] = [], c: number[] = [], q: number[] = []
252
+ // the cavity term: the bark (opaque) with a normal, rays in the hemisphere of the vertex' world normal
253
+ const cavNrm = cavReach > 0 && p.getMaterial()?.getAlphaMode() !== "MASK" ? p.getAttribute("NORMAL") : null
254
+ for (let i = 0; i < n; i++) {
255
+ pos.getElement(i, v)
256
+ const x = m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12], y = m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13], z = m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14]
257
+ // a per-vertex rotation of the fixed set about +Y, so neighbours never share a banding pattern
258
+ const rot = ((x * 12.9898 + z * 78.233 + y * 37.719) % 1 + 1) % 1 * Math.PI * 2, cr = Math.cos(rot), sr = Math.sin(rot)
259
+ let open = 0
260
+ for (const d of rays) {
261
+ const dx = d[0] * cr - d[2] * sr, dz = d[0] * sr + d[2] * cr
262
+ if (!occluded(bvh, tris, x + dx * bias, y + d[1] * bias, z + dz * bias, dx, d[1], dz, 0, reach)) open++
263
+ }
264
+ const ao = open / rays.length
265
+ let cav = 1
266
+ if (cavNrm) {
267
+ nrmOf(cavNrm, i, m, q)
268
+ // a tangent frame about the normal: the +Y ray set turned into it, and the per-vertex rotation kept
269
+ const ax = Math.abs(q[1]) < 0.9 ? 0 : 1, ay = ax ? 0 : 1
270
+ let tx = ay * q[2] - 0, ty = 0 - ax * q[2], tz = ax * q[1] - ay * q[0] // cross(n, a) for a = (ax, ay, 0)
271
+ const tl = Math.hypot(tx, ty, tz) || 1; tx /= tl; ty /= tl; tz /= tl
272
+ const bx = q[1] * tz - q[2] * ty, by = q[2] * tx - q[0] * tz, bz = q[0] * ty - q[1] * tx // cross(n, t)
273
+ let occ = 0
274
+ for (const d of cavRays) {
275
+ const rx = d[0] * cr - d[2] * sr, rz = d[0] * sr + d[2] * cr
276
+ const dx = tx * rx + q[0] * d[1] + bx * rz, dy = ty * rx + q[1] * d[1] + by * rz, dz = tz * rx + q[2] * d[1] + bz * rz
277
+ // the origin is lifted off the vertex' OWN surface along its normal only, and a hit at distance 0 counts: a limb's
278
+ // base vertex lies ON the trunk, and the rays into the trunk meet its surface right there - that IS the joint
279
+ const h = nearestHit(bvh, tris, x + q[0] * bias, y + q[1] * bias, z + q[2] * bias, dx, dy, dz, -1e-6, cavReach)
280
+ if (h < cavReach) occ += 1 - h / cavReach
281
+ }
282
+ cav = 1 - occ / cavRays.length
283
+ }
284
+ // the wind weight: the pack's own (its alpha, as it came) or the height over the plant, squared
285
+ let wind: number
286
+ if (old && oldEl === 4) { old.getElement(i, c); wind = c[3] } else { const h = Math.min(Math.max((y - min[1]) / height, 0), 1); wind = h * h }
287
+ out[i * 4] = ao; out[i * 4 + 1] = cav; out[i * 4 + 2] = 1; out[i * 4 + 3] = wind
288
+ sum += ao; vertices++
289
+ }
290
+ const acc = doc.createAccessor().setType("VEC4").setArray(out).setBuffer(doc.getRoot().listBuffers()[0] ?? doc.createBuffer())
291
+ p.setAttribute("COLOR_0", acc)
292
+ if (old && old.listParents().length <= 1) old.dispose()
293
+ }
294
+ }
295
+ return { vertices, meanAo: vertices ? sum / vertices : 1 }
296
+ }
297
+
298
+ /** The vertex' normal in world space (the node's rotation part, then normalised), into `q`. */
299
+ const nrmOf = (nrm: { getElement(i: number, out: number[]): number[] }, i: number, m: ArrayLike<number>, q: number[]): void => {
300
+ nrm.getElement(i, q)
301
+ const x = m[0] * q[0] + m[4] * q[1] + m[8] * q[2], y = m[1] * q[0] + m[5] * q[1] + m[9] * q[2], z = m[2] * q[0] + m[6] * q[1] + m[10] * q[2]
302
+ const l = Math.hypot(x, y, z) || 1
303
+ q[0] = x / l; q[1] = y / l; q[2] = z / l
304
+ }
305
+
306
+ /** 4×4 column-major inverse (null when singular). */
307
+ const invert4 = (m: ArrayLike<number>): number[] | null => {
308
+ const a = Array.from(m), r = new Array<number>(16)
309
+ const [ a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, a30, a31, a32, a33 ] = a
310
+ const b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11
311
+ const b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30
312
+ const b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32
313
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06
314
+ if (Math.abs(det) < 1e-12) return null
315
+ det = 1 / det
316
+ r[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; r[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; r[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; r[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det
317
+ r[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; r[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; r[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; r[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det
318
+ r[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; r[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; r[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; r[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det
319
+ r[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; r[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; r[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; r[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det
320
+ return r
321
+ }
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
  }