lecodes-assets 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -5
- package/dist/cli.js +7757 -683
- package/dist/index.js +7866 -675
- package/package.json +13 -3
- package/src/bvh/fromDocument.ts +136 -0
- package/src/bvh/toDocument.ts +202 -0
- package/src/cli.ts +1039 -0
- package/src/doctor/doctor.ts +241 -0
- package/src/doctor/lightmapUv.ts +452 -0
- package/src/doctor/lod.ts +177 -0
- package/src/doctor/normalizeScale.ts +220 -0
- package/src/doctor/reduceGlbBones.ts +218 -0
- package/src/doctor/shading.ts +95 -0
- package/src/doctor/transcodeKtx2.ts +124 -0
- package/src/doctor/transcodeTextures.ts +101 -0
- package/src/doctor/validateGlb.ts +264 -0
- package/src/fbx/convert.ts +327 -0
- package/src/fbx/read.ts +109 -0
- package/src/fbx/toDocument.ts +209 -0
- package/src/filmstrip.ts +263 -0
- package/src/index.ts +17 -0
- package/src/mergeSkins.ts +117 -0
- package/src/probe.ts +167 -0
- package/src/props.ts +137 -0
- package/src/retarget/automap.ts +276 -0
- package/src/retarget/ghost.ts +23 -0
- package/src/retarget/heading.ts +171 -0
- package/src/retarget/humanoid.ts +561 -0
- package/src/retarget/inPlace.ts +130 -0
- package/src/retarget/marks.ts +581 -0
- package/src/retarget/mirror.ts +0 -0
- package/src/retarget/retarget.ts +2272 -0
- package/src/retarget/rig.ts +542 -0
- package/src/retarget/unityAnim.ts +171 -0
- package/src/retarget/writeMarks.ts +132 -0
- package/src/sky/analyze.ts +136 -0
- package/src/sky/cmgen.ts +71 -0
- package/src/sky/index.ts +151 -0
- package/src/terrainPack.ts +169 -0
- package/src/textures/resolveImage.ts +202 -0
- package/src/unity/materials.ts +593 -0
- package/src/unity/png.ts +75 -0
- package/src/unity/prefabs.ts +80 -0
- package/src/unity/scene.ts +363 -0
- package/src/unity/unpack.ts +130 -0
- package/src/unreal/anim.ts +284 -0
- package/src/unreal/clipSource.ts +25 -0
- package/src/unreal/convert.ts +67 -0
- package/src/unreal/convertAsset.ts +81 -0
- package/src/unreal/index.ts +114 -0
- package/src/unreal/list.ts +113 -0
- package/src/unreal/materials.ts +291 -0
- package/src/unreal/mesh.ts +205 -0
- package/src/unreal/oodle.ts +153 -0
- package/src/unreal/package.ts +659 -0
- package/src/unreal/probe.ts +144 -0
- package/src/unreal/resolve.ts +51 -0
- package/src/unreal/rig.ts +149 -0
- package/src/unreal/sequencer.ts +193 -0
- package/src/unreal/skeleton.ts +74 -0
- package/src/unreal/texture.ts +202 -0
- package/src/unreal/toDocument.ts +360 -0
- package/src/unreal/versions.ts +100 -0
- package/src/wasm/fbxReader.js +14 -0
- package/src/wasm/lightmapUv.js +14 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// In-place conversion of a retargeted clip: the root's horizontal travel (and, on request, its
|
|
2
|
+
// turn) is taken OFF the animation — the root stays where and how the model rests, so the character
|
|
3
|
+
// stands on its capsule whatever window of a take the clip is — and handed back as per-key profiles
|
|
4
|
+
// the game commands its capsule and heading with.
|
|
5
|
+
//
|
|
6
|
+
// Runs on the glTF document AFTER `retarget()` — so the retarget saw the real motion (foot contacts,
|
|
7
|
+
// planting, the travel in the report) and only the playback loses it. Root MOTION stays available
|
|
8
|
+
// through the report / `extras.lecodes.travel`; here the extras gain `inPlace: true`.
|
|
9
|
+
|
|
10
|
+
import type { Document } from "@gltf-transform/core"
|
|
11
|
+
|
|
12
|
+
export interface InPlaceResult {
|
|
13
|
+
/** the keyframe times of the root's translation track, seconds */
|
|
14
|
+
times: number[]
|
|
15
|
+
/** the horizontal displacement that was removed, per key: [dx, dz] from the root's rest position */
|
|
16
|
+
travel: [ number, number ][]
|
|
17
|
+
/** the horizontal speed per key, m/s (central differences over the keys) */
|
|
18
|
+
speed: number[]
|
|
19
|
+
/** with `yaw`: the root's heading per key, degrees, cumulative from the first key (+ = a turn to
|
|
20
|
+
* the left, about +Y) — the turn a `rootRotation: 'yaw'` retarget put on the root; empty otherwise */
|
|
21
|
+
yaw: number[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InPlaceOptions {
|
|
25
|
+
/** also flatten the root's ROTATION track to the node's rest rotation and return its yaw profile
|
|
26
|
+
* (a turn-in-place / turn-start clip: the game turns the node by the profile, the clip stays put) */
|
|
27
|
+
yaw?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Q = [ number, number, number, number ]
|
|
31
|
+
const qmul = (a: Q, b: Q): Q => [
|
|
32
|
+
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
33
|
+
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
34
|
+
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
35
|
+
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
|
|
36
|
+
]
|
|
37
|
+
const qinv = (q: Q): Q => [ -q[0], -q[1], -q[2], q[3] ]
|
|
38
|
+
/** heading of the +Z axis after `q`, radians about +Y */
|
|
39
|
+
const headingOf = (q: Q): number => Math.atan2(2 * (q[0] * q[2] + q[3] * q[1]), 1 - 2 * (q[0] * q[0] + q[1] * q[1]))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hold the root's x/z at the node's REST translation for the named animation (and its rotation at
|
|
43
|
+
* the rest rotation with `yaw`). Returns the travel / turn that was removed (null when the animation
|
|
44
|
+
* has no root translation track — nothing to do).
|
|
45
|
+
*/
|
|
46
|
+
export const makeInPlace = (doc: Document, animationName: string, rootNodeName: string, opts: InPlaceOptions = {}): InPlaceResult | null => {
|
|
47
|
+
const anim = doc.getRoot().listAnimations().find((a) => a.getName() === animationName)
|
|
48
|
+
if (!anim) throw new Error(`makeInPlace: no animation "${animationName}"`)
|
|
49
|
+
const channel = anim.listChannels().find((c) => c.getTargetPath() === "translation" && c.getTargetNode()?.getName() === rootNodeName)
|
|
50
|
+
if (!channel) return null
|
|
51
|
+
const node = channel.getTargetNode()!
|
|
52
|
+
const [ rx, , rz ] = node.getTranslation()
|
|
53
|
+
const sampler = channel.getSampler()!
|
|
54
|
+
const input = sampler.getInput()!, output = sampler.getOutput()!
|
|
55
|
+
const times = Array.from(input.getArray() as ArrayLike<number>)
|
|
56
|
+
const values = new Float32Array(output.getArray()! as ArrayLike<number>)
|
|
57
|
+
const travel: [ number, number ][] = []
|
|
58
|
+
for (let k = 0; k < times.length; k++) {
|
|
59
|
+
travel.push([ values[k * 3]! - rx, values[k * 3 + 2]! - rz ])
|
|
60
|
+
values[k * 3] = rx
|
|
61
|
+
values[k * 3 + 2] = rz
|
|
62
|
+
}
|
|
63
|
+
output.setArray(values)
|
|
64
|
+
const speed = times.map((_, k) => {
|
|
65
|
+
const a = Math.max(0, k - 1), b = Math.min(times.length - 1, k + 1)
|
|
66
|
+
const dt = times[b]! - times[a]!
|
|
67
|
+
return dt > 0 ? Math.hypot(travel[b]![0] - travel[a]![0], travel[b]![1] - travel[a]![1]) / dt : 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// the turn: the rotation track relative to the rest rotation is a pure yaw (retarget rootRotation
|
|
71
|
+
// 'yaw'); read it per translation key, then hold the track at rest
|
|
72
|
+
const yaw: number[] = []
|
|
73
|
+
if (opts.yaw) {
|
|
74
|
+
const rc = anim.listChannels().find((c) => c.getTargetPath() === "rotation" && c.getTargetNode() === node)
|
|
75
|
+
if (rc) {
|
|
76
|
+
const rs = rc.getSampler()!
|
|
77
|
+
const rt = Array.from(rs.getInput()!.getArray() as ArrayLike<number>)
|
|
78
|
+
const rv = new Float32Array(rs.getOutput()!.getArray()! as ArrayLike<number>)
|
|
79
|
+
const rest = node.getRotation() as Q
|
|
80
|
+
const at = (t: number): Q => {
|
|
81
|
+
let i = 0
|
|
82
|
+
while (i < rt.length - 2 && rt[i + 1]! <= t) i++
|
|
83
|
+
const t0 = rt[i]!, t1 = rt[i + 1] ?? t0, u = t1 > t0 ? Math.min(1, Math.max(0, (t - t0) / (t1 - t0))) : 0
|
|
84
|
+
const a = Array.from(rv.subarray(i * 4, i * 4 + 4)) as Q, b = rt.length > i + 1 ? Array.from(rv.subarray(i * 4 + 4, i * 4 + 8)) as Q : a
|
|
85
|
+
const s = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0 ? -1 : 1
|
|
86
|
+
const q: Q = [ a[0] + (s * b[0] - a[0]) * u, a[1] + (s * b[1] - a[1]) * u, a[2] + (s * b[2] - a[2]) * u, a[3] + (s * b[3] - a[3]) * u ]
|
|
87
|
+
const n = Math.hypot(...q) || 1
|
|
88
|
+
return [ q[0] / n, q[1] / n, q[2] / n, q[3] / n ]
|
|
89
|
+
}
|
|
90
|
+
let acc = 0, prev = 0
|
|
91
|
+
for (let k = 0; k < times.length; k++) {
|
|
92
|
+
const h = headingOf(qmul(at(times[k]!), qinv(rest)))
|
|
93
|
+
if (k > 0) { let d = h - prev; d = Math.atan2(Math.sin(d), Math.cos(d)); acc += d }
|
|
94
|
+
prev = h
|
|
95
|
+
yaw.push(acc * 180 / Math.PI)
|
|
96
|
+
}
|
|
97
|
+
const base = yaw[0] ?? 0
|
|
98
|
+
for (let k = 0; k < yaw.length; k++) yaw[k] = yaw[k]! - base
|
|
99
|
+
for (let k = 0; k < rt.length; k++) { rv[k * 4] = rest[0]; rv[k * 4 + 1] = rest[1]; rv[k * 4 + 2] = rest[2]; rv[k * 4 + 3] = rest[3] }
|
|
100
|
+
rs.getOutput()!.setArray(rv)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const extras = anim.getExtras() as Record<string, unknown>
|
|
105
|
+
const lecodes = { ...((extras.lecodes as Record<string, unknown>) ?? {}), inPlace: true }
|
|
106
|
+
anim.setExtras({ ...extras, lecodes })
|
|
107
|
+
return { times, travel, speed, yaw }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A per-key series resampled at `hz`, from t = 0 to the last key (linear between keys). */
|
|
111
|
+
const resample = (times: number[], series: number[], hz: number): number[] => {
|
|
112
|
+
const end = times[times.length - 1] ?? 0
|
|
113
|
+
const n = Math.ceil(end * hz)
|
|
114
|
+
const out: number[] = []
|
|
115
|
+
for (let k = 0; k <= n; k++) {
|
|
116
|
+
const t = Math.min(end, k / hz)
|
|
117
|
+
let i = 0
|
|
118
|
+
while (i < times.length - 2 && times[i + 1]! <= t) i++
|
|
119
|
+
const t0 = times[i]!, t1 = times[i + 1] ?? t0
|
|
120
|
+
const u = t1 > t0 ? (t - t0) / (t1 - t0) : 0
|
|
121
|
+
out.push(series[i]! + ((series[i + 1] ?? series[i]!) - series[i]!) * u)
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The removed travel's speed resampled at `hz`, from t = 0 to the last key (linear between keys). */
|
|
127
|
+
export const speedProfile = (r: InPlaceResult, hz = 30): number[] => resample(r.times, r.speed, hz)
|
|
128
|
+
|
|
129
|
+
/** The removed turn resampled at `hz` — degrees since t = 0 (empty unless `makeInPlace` ran with `yaw`). */
|
|
130
|
+
export const yawProfile = (r: InPlaceResult, hz = 30): number[] => (r.yaw.length ? resample(r.times, r.yaw, hz) : [])
|