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.
- package/README.md +203 -26
- package/dist/cli.js +17948 -11911
- package/dist/index.js +17525 -11479
- 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 +67 -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 +103 -0
- package/src/commands/shared.ts +121 -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 +286 -0
- package/src/doctor/doctor.ts +94 -30
- 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 +179 -28
- package/src/doctor/validateGlb.ts +2 -2
- package/src/doctor/vertexAo.ts +321 -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 +81 -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 +381 -0
- package/src/unity/levelTerrain.ts +116 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +306 -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 +325 -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/unity/scene.ts
CHANGED
|
@@ -1,363 +1,384 @@
|
|
|
1
|
-
// Unity scene (`.unity`, text-serialised
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Unity scene (`.unity`, text-serialised) → a level description in LeCodes' frame: every placed
|
|
2
|
+
// prefab instance as a KIND (a distinct fully-expanded GameObject tree: meshes + materials + LOD
|
|
3
|
+
// levels + colliders, relative to its root) plus one placement per instance, the terrain (heights,
|
|
4
|
+
// splat layers) with its painted trees and detail meshes as more placements, and the lights.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the prefab's internal offsets live in the prefab file, which the big packs ship binary-serialised,
|
|
11
|
-
// so a child parented deep inside a prefab lands at the prefab's root (counted in `approximate`).
|
|
6
|
+
// Kinds are keyed by CONTENT, not by prefab: two instances of one prefab share a kind unless the
|
|
7
|
+
// scene overrode something inside one of them (a moved child, another material) — then that one gets
|
|
8
|
+
// a variant kind. Nested prefabs, variants and modifications are resolved by prefabTree.ts; the
|
|
9
|
+
// scene's own loose mesh objects become kinds too.
|
|
12
10
|
//
|
|
13
|
-
// Frame:
|
|
14
|
-
//
|
|
15
|
-
// UnityGLTF convention. `handedness: "z"` mirrors Z instead (for content authored in Unity itself),
|
|
16
|
-
// `"none"` keeps Unity's numbers.
|
|
11
|
+
// Frame: every transform is conjugated by the X mirror (math.ts) — placements AND the part matrices
|
|
12
|
+
// inside kinds.
|
|
17
13
|
|
|
18
|
-
import {
|
|
19
|
-
import { basename
|
|
20
|
-
import {
|
|
14
|
+
import { readFileSync } from "node:fs"
|
|
15
|
+
import { basename } from "node:path"
|
|
16
|
+
import { comp, enabled, goActive, goName, localTRS, lodLevels, type UComp, type UGO } from "./gameObjects"
|
|
17
|
+
import { resolverFor } from "./guid"
|
|
18
|
+
import { I4, decompose, mirrorX, mmul, r6, trsMatrix, type Mat4 } from "./math"
|
|
19
|
+
import { PrefabLoader, newStats } from "./prefabTree"
|
|
20
|
+
import { readUnityTerrain, type UnityTerrain } from "./terrain"
|
|
21
|
+
import { color, isMap, num, ref, vec3, type Quat, type Vec3 } from "./yaml"
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
export type Handedness = "x" | "none"
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
type Line = { indent: number, text: string }
|
|
25
|
+
// ---- output types -------------------------------------------------------------------------------------
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
open += (nx.match(/\{/g) ?? []).length - (nx.match(/\}/g) ?? []).length
|
|
39
|
-
}
|
|
40
|
-
const indent = l.length - l.trimStart().length
|
|
41
|
-
out.push({ indent, text: l.trim() })
|
|
42
|
-
}
|
|
43
|
-
return out
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const scalar = (s: string): Y => {
|
|
47
|
-
s = s.trim()
|
|
48
|
-
if (s === "" || s === "~" || s === "null") return ""
|
|
49
|
-
if (s === "[]") return []
|
|
50
|
-
if (s === "{}") return {}
|
|
51
|
-
if (s.startsWith("{") && s.endsWith("}")) {
|
|
52
|
-
const obj: { [k: string]: Y } = {}
|
|
53
|
-
for (const part of s.slice(1, -1).split(",")) {
|
|
54
|
-
const c = part.indexOf(":")
|
|
55
|
-
if (c < 0) continue
|
|
56
|
-
obj[part.slice(0, c).trim()] = scalar(part.slice(c + 1))
|
|
57
|
-
}
|
|
58
|
-
return obj
|
|
59
|
-
}
|
|
60
|
-
if (s.startsWith("[") && s.endsWith("]")) return s.slice(1, -1).split(",").map((x) => scalar(x))
|
|
61
|
-
if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith("\"") && s.endsWith("\""))) return s.slice(1, -1).replace(/''/g, "'")
|
|
62
|
-
return s
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const parseUnityYaml = (text: string): { [k: string]: Y } => {
|
|
66
|
-
const lines = splitLines(text)
|
|
67
|
-
let pos = 0
|
|
68
|
-
const parseValue = (minIndent: number): Y => {
|
|
69
|
-
if (pos >= lines.length || lines[pos].indent < minIndent) return ""
|
|
70
|
-
return lines[pos].text.startsWith("- ") || lines[pos].text === "-" ? parseSeq(lines[pos].indent) : parseMap(lines[pos].indent)
|
|
71
|
-
}
|
|
72
|
-
const parseMap = (indent: number): { [k: string]: Y } => {
|
|
73
|
-
const obj: { [k: string]: Y } = {}
|
|
74
|
-
while (pos < lines.length && lines[pos].indent === indent && !lines[pos].text.startsWith("- ")) {
|
|
75
|
-
const t = lines[pos].text
|
|
76
|
-
const c = t.search(/:( |$)/)
|
|
77
|
-
if (c < 0) { pos++; continue }
|
|
78
|
-
const key = t.slice(0, c).trim()
|
|
79
|
-
const rest = t.slice(c + 1).trim()
|
|
80
|
-
pos++
|
|
81
|
-
if (rest === "") obj[key] = pos < lines.length && lines[pos].indent > indent ? parseValue(indent + 1) : (pos < lines.length && lines[pos].indent === indent && lines[pos].text.startsWith("- ") ? parseSeq(indent) : "")
|
|
82
|
-
else obj[key] = scalar(rest)
|
|
83
|
-
}
|
|
84
|
-
return obj
|
|
85
|
-
}
|
|
86
|
-
const parseSeq = (indent: number): Y[] => {
|
|
87
|
-
const arr: Y[] = []
|
|
88
|
-
while (pos < lines.length && lines[pos].indent === indent && (lines[pos].text.startsWith("- ") || lines[pos].text === "-")) {
|
|
89
|
-
const rest = lines[pos].text.slice(1).trim()
|
|
90
|
-
if (rest === "") { pos++; arr.push(parseValue(indent + 1)); continue }
|
|
91
|
-
// a flow map / flow list / plain scalar item (`- {fileID: 0, guid: …}`) is one value, not a key
|
|
92
|
-
if (rest.startsWith("{") || rest.startsWith("[") || rest.search(/:( |$)/) < 0) { pos++; arr.push(scalar(rest)); continue }
|
|
93
|
-
// the item's first key shares the dash line; the rest of its keys sit two deeper
|
|
94
|
-
lines[pos] = { indent: indent + 2, text: rest }
|
|
95
|
-
arr.push(parseValue(indent + 2))
|
|
96
|
-
}
|
|
97
|
-
return arr
|
|
98
|
-
}
|
|
99
|
-
return parseMap(lines[0]?.indent ?? 0)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---- the scene -----------------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
type Ref = { fileID: string, guid?: string }
|
|
105
|
-
const ref = (v: Y | undefined): Ref => (typeof v === "object" && !Array.isArray(v) && v ? { fileID: String(v.fileID ?? "0"), guid: typeof v.guid === "string" ? v.guid : undefined } : { fileID: "0" })
|
|
106
|
-
const num = (v: Y | undefined, d = 0): number => { const n = typeof v === "string" ? Number(v) : NaN; return Number.isFinite(n) ? n : d }
|
|
107
|
-
const str = (v: Y | undefined): string => (typeof v === "string" ? v : "")
|
|
108
|
-
const vec3 = (v: Y | undefined, d: [number, number, number]): [number, number, number] => {
|
|
109
|
-
const o = typeof v === "object" && !Array.isArray(v) && v ? v : {}
|
|
110
|
-
return [ num(o.x, d[0]), num(o.y, d[1]), num(o.z, d[2]) ]
|
|
27
|
+
export type KindPart = {
|
|
28
|
+
/** GameObject path inside the kind (`Spruce_3/Spruce_3_LOD1`). */
|
|
29
|
+
path: string
|
|
30
|
+
/** Kind-local transform, LeCodes frame (column-major). */
|
|
31
|
+
matrix: Mat4
|
|
32
|
+
mesh: { guid: string, fileID: string }
|
|
33
|
+
/** `.mat` GUIDs, one per submesh (Unity's order). */
|
|
34
|
+
materials: string[]
|
|
35
|
+
/** LODGroup level (0 when the renderer is in no LODGroup). */
|
|
36
|
+
lod: number
|
|
37
|
+
castShadows: boolean
|
|
111
38
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const st: Vec3 = [ p.s[0] * l.t[0], p.s[1] * l.t[1], p.s[2] * l.t[2] ]
|
|
135
|
-
const rt = qrot(p.r, st)
|
|
136
|
-
return { t: [ p.t[0] + rt[0], p.t[1] + rt[1], p.t[2] + rt[2] ], r: qnorm(qmul(p.r, l.r)), s: [ p.s[0] * l.s[0], p.s[1] * l.s[1], p.s[2] * l.s[2] ] }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export type Handedness = "x" | "z" | "none"
|
|
140
|
-
const toLeCodes = (w: TRS, h: Handedness): TRS => {
|
|
141
|
-
if (h === "x") return { t: [ -w.t[0], w.t[1], w.t[2] ], r: [ w.r[0], -w.r[1], -w.r[2], w.r[3] ], s: w.s }
|
|
142
|
-
if (h === "z") return { t: [ w.t[0], w.t[1], -w.t[2] ], r: [ -w.r[0], -w.r[1], w.r[2], w.r[3] ], s: w.s }
|
|
143
|
-
return w
|
|
39
|
+
export type KindCollider = {
|
|
40
|
+
path: string
|
|
41
|
+
matrix: Mat4
|
|
42
|
+
trigger: boolean
|
|
43
|
+
} & (
|
|
44
|
+
| { shape: "box", center: Vec3, size: Vec3 }
|
|
45
|
+
| { shape: "sphere", center: Vec3, radius: number }
|
|
46
|
+
| { shape: "capsule", center: Vec3, radius: number, height: number, direction: number }
|
|
47
|
+
| { shape: "mesh", convex: boolean, mesh: { guid: string, fileID: string } | null }
|
|
48
|
+
)
|
|
49
|
+
export type KindLight = { path: string, matrix: Mat4, type: number, color: [number, number, number, number], intensity: number, range: number, area?: LightArea, unit?: string }
|
|
50
|
+
export type SceneKind = {
|
|
51
|
+
key: string
|
|
52
|
+
/** File-safe unique name (`P_Bush_3`, `P_Bush_3_v2`). */
|
|
53
|
+
name: string
|
|
54
|
+
prefab: string | null
|
|
55
|
+
prefabPath: string | null
|
|
56
|
+
parts: KindPart[]
|
|
57
|
+
colliders: KindCollider[]
|
|
58
|
+
lights: KindLight[]
|
|
59
|
+
particles: number
|
|
60
|
+
count: number
|
|
144
61
|
}
|
|
145
|
-
|
|
146
62
|
export type SceneInstance = {
|
|
63
|
+
kind: string
|
|
147
64
|
name: string
|
|
148
|
-
/**
|
|
149
|
-
prefab: string | null
|
|
150
|
-
/** `Static/Roads` — the scene's grouping GameObjects above the instance */
|
|
65
|
+
/** `Vegetation/Grass` — the scene groups above the instance; `Terrain/Trees`, `Terrain/Details`. */
|
|
151
66
|
group: string
|
|
152
67
|
position: Vec3
|
|
153
68
|
rotation: Quat
|
|
154
69
|
scale: Vec3
|
|
155
|
-
/** true when a parent sat inside a binary prefab — placed at that prefab's root, not its inner child */
|
|
156
|
-
approximate?: boolean
|
|
157
70
|
}
|
|
158
|
-
|
|
71
|
+
/** An AREA light's emitter (Unity rectangle / disc lights, HDRP rectangle / tube / disc): `width` runs along the light's
|
|
72
|
+
* local X (`along`), `height` along its local Y (`up`; 0 for a tube), and it emits along `direction` (local +Z). The
|
|
73
|
+
* lightmap bake takes such a lamp as a rectangle (`Light.point({ bakeArea })`) - a fixture lights from its length, not
|
|
74
|
+
* from a point. */
|
|
75
|
+
export type LightArea = { shape: "rect" | "tube" | "disc", width: number, height: number }
|
|
76
|
+
export type SceneLight = { name: string, type: "directional" | "point" | "spot", position: Vec3, direction: Vec3, color: [number, number, number], intensity: number, range: number, shadows: boolean,
|
|
77
|
+
area?: LightArea, along?: Vec3, up?: Vec3,
|
|
78
|
+
/** HDRP's unit of `intensity`: lumen | candela | lux | nits | ev100 (absent = the pipeline's unitless number). */
|
|
79
|
+
unit?: string }
|
|
80
|
+
export type SceneCamera = { name: string, position: Vec3, forward: Vec3, fov: number }
|
|
81
|
+
export type SceneTerrainInfo = {
|
|
82
|
+
name: string
|
|
83
|
+
dataPath: string
|
|
84
|
+
/** LeCodes node origin (sample (0,0) — the mirrored grid's corner). */
|
|
85
|
+
position: Vec3
|
|
86
|
+
resolution: number
|
|
87
|
+
size: Vec3
|
|
88
|
+
layers: { name: string, diffuse: string | null, normal: string | null, tileSize: [number, number], coverage: number }[]
|
|
89
|
+
}
|
|
159
90
|
export type SceneImport = {
|
|
160
91
|
source: string
|
|
161
92
|
frame: string
|
|
162
|
-
|
|
93
|
+
kinds: Record<string, SceneKind>
|
|
163
94
|
instances: SceneInstance[]
|
|
164
|
-
|
|
95
|
+
lights: SceneLight[]
|
|
96
|
+
cameras: SceneCamera[]
|
|
97
|
+
terrain: SceneTerrainInfo | null
|
|
98
|
+
stats: { prefabInstances: number, sceneMeshes: number, trees: number, details: number, inactive: number, unresolvedMods: number, guessedTransforms: number, missingPrefabs: number, binaryPrefabs: string[], variants: number, particles: number, prefabLights: number }
|
|
165
99
|
}
|
|
100
|
+
/** The terrain arrays that do not belong in JSON (the `.terrain` writer reads them). */
|
|
101
|
+
export type SceneImportFull = SceneImport & { terrainData: UnityTerrain | null }
|
|
166
102
|
|
|
167
103
|
export type ImportSceneOptions = {
|
|
168
|
-
/** `unity-index.txt` for GUID → path (default: found walking up from the scene
|
|
104
|
+
/** `unity-index.txt` for GUID → path (default: found walking up from the scene, else the Unity
|
|
105
|
+
* project's `.meta` sidecars). */
|
|
169
106
|
indexFile?: string | null
|
|
170
107
|
handedness?: Handedness
|
|
171
|
-
/** keep only instances whose
|
|
108
|
+
/** keep only instances whose kind name / group matches */
|
|
172
109
|
filter?: RegExp
|
|
110
|
+
/** …and the opposite: DROP every instance whose kind name / group / node name matches. For props a
|
|
111
|
+
* pack authors inside the geometry they decorate — the Modular Shooting Range sinks its wall signs
|
|
112
|
+
* ~3.5 cm into the concrete and lets Unity's decal ordering sort it out, so they z-fight here. */
|
|
113
|
+
drop?: RegExp
|
|
114
|
+
/** DROP the placements standing at these points (lecodes frame, as the scene file prints them; within 10 cm) —
|
|
115
|
+
* for the one instance of a kind the game moves while its twins stay: the shooting range's five lane targets
|
|
116
|
+
* are `SM_CardboardTarget_01` like the sixth on a shelf, so no name tells them apart, their place does. */
|
|
117
|
+
dropAt?: [number, number, number][]
|
|
173
118
|
/** Unity-space X/Z rectangle [x0, z0, x1, z1] — instances outside are dropped */
|
|
174
119
|
region?: [number, number, number, number]
|
|
175
120
|
/** only instances under a group whose path contains this */
|
|
176
121
|
group?: string
|
|
177
122
|
includeInactive?: boolean
|
|
123
|
+
/** Skip the terrain's painted trees / detail meshes. */
|
|
124
|
+
noTrees?: boolean
|
|
125
|
+
noDetails?: boolean
|
|
178
126
|
}
|
|
179
127
|
|
|
180
|
-
|
|
128
|
+
// ---- component readers -----------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/** A collider component as a kind's collider (`M` = the owner's kind-local matrix, mirrored), or null for any other component. */
|
|
131
|
+
const readCollider = (c: UComp, path: string, M: Mat4): KindCollider | null => {
|
|
132
|
+
const base = { path, matrix: M, trigger: num(c.body.m_IsTrigger, 0) !== 0 }
|
|
133
|
+
switch (c.cls) {
|
|
134
|
+
case "BoxCollider": return { ...base, shape: "box", center: vec3(c.body.m_Center), size: vec3(c.body.m_Size, [ 1, 1, 1 ]) }
|
|
135
|
+
case "SphereCollider": return { ...base, shape: "sphere", center: vec3(c.body.m_Center), radius: num(c.body.m_Radius, 0.5) }
|
|
136
|
+
case "CapsuleCollider": return { ...base, shape: "capsule", center: vec3(c.body.m_Center), radius: num(c.body.m_Radius, 0.5), height: num(c.body.m_Height, 2), direction: num(c.body.m_Direction, 1) }
|
|
137
|
+
case "MeshCollider": {
|
|
138
|
+
const mesh = ref(c.body.m_Mesh)
|
|
139
|
+
return { ...base, shape: "mesh", convex: num(c.body.m_Convex, 0) !== 0, mesh: mesh.guid ? { guid: mesh.guid, fileID: mesh.fileID } : null }
|
|
140
|
+
}
|
|
141
|
+
default: return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The emitter of an area light, or undefined for a punctual one. HDRP keeps it in the HDAdditionalLightData script next to
|
|
146
|
+
* the Light (m_PointlightHDType 1 = area; m_AreaLightShape 0 rectangle, 1 tube, 2 disc; m_ShapeWidth / m_ShapeHeight);
|
|
147
|
+
* the built-in and URP pipelines in the Light itself (m_Type 3 rectangle, 4 disc; m_AreaSize). */
|
|
148
|
+
const HD_UNITS = [ "lumen", "candela", "lux", "nits", "ev100" ]
|
|
149
|
+
const readArea = (comps: UComp[], light: UComp): { area?: LightArea, unit?: string } => {
|
|
150
|
+
const hd = comps.find((c) => c.cls === "MonoBehaviour" && c.body.m_AreaLightShape !== undefined && c.body.m_ShapeWidth !== undefined)
|
|
151
|
+
if (hd) {
|
|
152
|
+
const unit = HD_UNITS[num(hd.body.m_LightUnit, -1)]
|
|
153
|
+
if (num(hd.body.m_PointlightHDType, 0) !== 1) return { unit }
|
|
154
|
+
const shape = ([ "rect", "tube", "disc" ] as const)[num(hd.body.m_AreaLightShape, 0)] ?? "rect"
|
|
155
|
+
const width = num(hd.body.m_ShapeWidth, 0.5)
|
|
156
|
+
return { unit, area: { shape, width, height: shape === "tube" ? 0 : shape === "disc" ? width : num(hd.body.m_ShapeHeight, 0.5) } }
|
|
157
|
+
}
|
|
158
|
+
const type = num(light.body.m_Type, 2)
|
|
159
|
+
if (type !== 3 && type !== 4) return {}
|
|
160
|
+
const size = light.body.m_AreaSize
|
|
161
|
+
const width = isMap(size) ? num(size.x, 1) : 1, height = isMap(size) ? num(size.y, 1) : 1
|
|
162
|
+
return { area: { shape: type === 4 ? "disc" : "rect", width, height: type === 4 ? width : height } }
|
|
163
|
+
}
|
|
164
|
+
const axisOf = (M: Mat4, col: number): Vec3 => {
|
|
165
|
+
const f = M.slice(col * 4, col * 4 + 3)
|
|
166
|
+
const l = Math.hypot(...f) || 1
|
|
167
|
+
return f.map((v) => r6(v / l)) as Vec3
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The unit +Z of a (mirrored) world matrix: where a light shines and a camera looks. */
|
|
171
|
+
const forwardOf = (M: Mat4): Vec3 => {
|
|
172
|
+
const f = M.slice(8, 11)
|
|
173
|
+
const l = Math.hypot(...f) || 1
|
|
174
|
+
return f.map((v) => r6(v / l)) as Vec3
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const readLight = (name: string, light: UComp, M: Mat4, comps: UComp[] = []): SceneLight => {
|
|
178
|
+
const type = num(light.body.m_Type, 2)
|
|
179
|
+
const c = color(light.body.m_Color)
|
|
180
|
+
return {
|
|
181
|
+
name, type: type === 1 ? "directional" : type === 0 ? "spot" : "point", position: decompose(M).t.map(r6) as Vec3, direction: forwardOf(M),
|
|
182
|
+
color: [ c[0], c[1], c[2] ], intensity: num(light.body.m_Intensity, 1), range: num(light.body.m_Range, 10),
|
|
183
|
+
shadows: isMap(light.body.m_Shadows) ? num(light.body.m_Shadows.m_Type, 0) !== 0 : false,
|
|
184
|
+
...readArea(comps, light), along: axisOf(M, 0), up: axisOf(M, 1),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const readCamera = (name: string, cam: UComp, M: Mat4): SceneCamera =>
|
|
189
|
+
({ name, position: [ M[12], M[13], M[14] ].map(r6) as Vec3, forward: forwardOf(M), fov: num(cam.body["field of view"], 60) })
|
|
190
|
+
|
|
191
|
+
export const importUnityScene = async (scenePath: string, opts: ImportSceneOptions = {}): Promise<SceneImportFull> => {
|
|
181
192
|
const text = readFileSync(scenePath, "utf8")
|
|
182
193
|
if (!text.startsWith("%YAML")) throw new Error(`${basename(scenePath)} is not text-serialised (Unity: Edit ▸ Project Settings ▸ Editor ▸ Asset Serialization = Force Text)`)
|
|
183
194
|
const handedness = opts.handedness ?? "x"
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
const mirror = (m: Mat4): Mat4 => (handedness === "x" ? mirrorX(m) : m)
|
|
196
|
+
const resolver = resolverFor(scenePath, opts.indexFile)
|
|
197
|
+
const tstats = newStats()
|
|
198
|
+
const loader = new PrefabLoader(resolver, tstats)
|
|
199
|
+
// the terrain first: its tree / detail prototypes join the model preload
|
|
200
|
+
const tdGuid = /m_TerrainData: \{fileID: -?\d+, guid: ([0-9a-f]{32})/.exec(text)?.[1]
|
|
201
|
+
const tdPath = tdGuid ? resolver.resolve(tdGuid) : null
|
|
202
|
+
const tdLocal = tdPath ? readUnityTerrain(tdPath, [ 0, 0, 0 ], resolver) : null
|
|
203
|
+
await loader.preload([ text ], tdLocal ? [ ...new Set([ ...tdLocal.trees, ...tdLocal.details ].map((p) => p.prefab)) ] : [])
|
|
204
|
+
const tree = loader.buildText(text, null)
|
|
189
205
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const s = starts[i]
|
|
198
|
-
const chunk = text.slice(s.end, i + 1 < starts.length ? starts[i + 1].idx : text.length)
|
|
199
|
-
const body = parseUnityYaml(chunk)
|
|
200
|
-
const cls = Object.keys(body)[0] ?? ""
|
|
201
|
-
objs.set(s.id, { type: s.type, id: s.id, stripped: s.stripped, cls, body: (body[cls] as { [k: string]: Y }) ?? {} })
|
|
202
|
-
}
|
|
206
|
+
const kinds: Record<string, SceneKind> = {}
|
|
207
|
+
const nameUse = new Map<string, number>()
|
|
208
|
+
const instances: SceneInstance[] = []
|
|
209
|
+
const lights: SceneLight[] = []
|
|
210
|
+
const cameras: SceneCamera[] = []
|
|
211
|
+
const stats: SceneImport["stats"] = { prefabInstances: 0, sceneMeshes: 0, trees: 0, details: 0, inactive: 0, unresolvedMods: 0, guessedTransforms: 0, missingPrefabs: 0, binaryPrefabs: [], variants: 0, particles: 0, prefabLights: 0 }
|
|
212
|
+
const prefabName = (guid: string | null): string | null => { const p = guid ? resolver.resolve(guid) : null; return p ? basename(p).replace(/\.(prefab|fbx|obj|dae|blend)$/i, "") : null }
|
|
203
213
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
const mt = /^m_Local(Position|Rotation|Scale)\.([xyzw])$/.exec(path)
|
|
235
|
-
if (mt) {
|
|
236
|
-
if (rootTarget === null) rootTarget = target
|
|
237
|
-
if (target !== rootTarget) continue
|
|
238
|
-
const k = "xyzw".indexOf(mt[2])
|
|
239
|
-
if (mt[1] === "Position") local.t[k] = num(value, local.t[k])
|
|
240
|
-
else if (mt[1] === "Rotation") local.r[k] = num(value, local.r[k])
|
|
241
|
-
else local.s[k] = num(value, local.s[k])
|
|
242
|
-
} else if (path === "m_Name" && name === null) name = value
|
|
243
|
-
else if (path === "m_IsActive" && value === "0") active = false
|
|
214
|
+
// ---- a prefab instance → a kind (+ its nested prefab instances as placements of their own) ----
|
|
215
|
+
// Kinds stop at nested prefab roots: a composite (a cliff wall built from cliff-rock prefabs, a
|
|
216
|
+
// campfire around a fire prefab) becomes placements of its LEAF prefabs, so a moved rock inside one
|
|
217
|
+
// wall is one more placement, not one more GLB carrying twelve rocks.
|
|
218
|
+
const lodSuffix = (n: string): number => { const m = /_LOD(\d+)$/i.exec(n); return m ? +m[1] : 0 }
|
|
219
|
+
const kindOf = (root: UGO, prefab: string | null, fallbackName: string, nested: { g: UGO, local: Mat4 }[]): SceneKind | null => {
|
|
220
|
+
const parts: KindPart[] = [], colliders: KindCollider[] = [], klights: KindLight[] = []
|
|
221
|
+
let particles = 0
|
|
222
|
+
const lods = lodLevels([ root ])
|
|
223
|
+
const walk = (g: UGO, parentM: Mat4, path: string, top: boolean): void => {
|
|
224
|
+
if (!goActive(g) && !opts.includeInactive) return
|
|
225
|
+
const l = localTRS(g)
|
|
226
|
+
const m = top ? I4() : mmul(parentM, trsMatrix(l.t, l.r, l.s))
|
|
227
|
+
if (!top && g.prefab) { nested.push({ g, local: m }); return }
|
|
228
|
+
const p = top ? "" : (path ? `${path}/` : "") + goName(g)
|
|
229
|
+
const M = mirror(m)
|
|
230
|
+
const mf = comp(g, "MeshFilter"), mr = comp(g, "MeshRenderer")
|
|
231
|
+
if (mf && mr && enabled(mr)) {
|
|
232
|
+
const mesh = ref(mf.body.m_Mesh)
|
|
233
|
+
if (mesh.guid && mesh.guid !== "0000000000000000e000000000000000") {
|
|
234
|
+
const mats = Array.isArray(mr.body.m_Materials) ? mr.body.m_Materials.map((x) => ref(x).guid ?? "") : []
|
|
235
|
+
parts.push({ path: p, matrix: M, mesh: { guid: mesh.guid, fileID: mesh.fileID }, materials: mats, lod: lods.get(mr) ?? lodSuffix(goName(g)), castShadows: num(mr.body.m_CastShadows, 1) !== 0 })
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const c of g.comps) {
|
|
239
|
+
if (!enabled(c)) continue
|
|
240
|
+
const collider = readCollider(c, p, M)
|
|
241
|
+
if (collider) colliders.push(collider)
|
|
242
|
+
else if (c.cls === "Light") klights.push({ path: p, matrix: M, type: num(c.body.m_Type, 2), color: color(c.body.m_Color), intensity: num(c.body.m_Intensity, 1), range: num(c.body.m_Range, 10), ...readArea(g.comps, c) })
|
|
243
|
+
else if (c.cls === "ParticleSystem") particles++
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
prefabInstances.set(o.id, { parent: ref(mod.m_TransformParent).fileID, local, guid: ref(o.body.m_SourcePrefab).guid, name, active })
|
|
245
|
+
for (const ch of g.children) walk(ch, m, p, false)
|
|
247
246
|
}
|
|
247
|
+
walk(root, I4(), "", true)
|
|
248
|
+
stats.particles += particles
|
|
249
|
+
stats.prefabLights += klights.length
|
|
250
|
+
if (!parts.length && !colliders.length) return null
|
|
251
|
+
const key = JSON.stringify([ prefab, parts.map((x) => [ x.path, x.matrix.map(r6), x.mesh, x.materials, x.lod, x.castShadows ]), colliders.map((c) => ({ ...c, matrix: c.matrix.map(r6) })) ])
|
|
252
|
+
let k = kinds[key]
|
|
253
|
+
if (!k) {
|
|
254
|
+
const base = (prefabName(prefab) ?? fallbackName).replace(/\s*\(\d+\)$/, "").replace(/[^\w.-]+/g, "_")
|
|
255
|
+
const n = (nameUse.get(base) ?? 0) + 1
|
|
256
|
+
nameUse.set(base, n)
|
|
257
|
+
if (n > 1) stats.variants++
|
|
258
|
+
k = kinds[key] = { key, name: n > 1 ? `${base}_v${n}` : base, prefab, prefabPath: prefab ? resolver.resolve(prefab) : null, parts, colliders, lights: klights, particles, count: 0 }
|
|
259
|
+
}
|
|
260
|
+
return k
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const resolveT = (id: string, depth = 0): { w: TRS, approx: boolean, path: string[] } => {
|
|
255
|
-
if (id === "0" || depth > 64) return ROOT
|
|
256
|
-
const hit = worldT.get(id); if (hit) return hit
|
|
257
|
-
const t = transforms.get(id)
|
|
258
|
-
let out = ROOT
|
|
259
|
-
if (t) {
|
|
260
|
-
if (t.prefabInstance) { const p = resolvePI(t.prefabInstance, depth + 1); out = { w: p.w, approx: true, path: p.path } }
|
|
261
|
-
else {
|
|
262
|
-
const par = resolveT(t.father, depth + 1)
|
|
263
|
-
const go = gameObjects.get(t.gameObject)
|
|
264
|
-
out = { w: compose(par.w, t.local), approx: par.approx, path: go?.name ? [ ...par.path, go.name ] : par.path }
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
worldT.set(id, out)
|
|
268
|
-
return out
|
|
263
|
+
// ---- placements ----
|
|
264
|
+
const worldOf = (m: Mat4): { position: Vec3, rotation: Quat, scale: Vec3 } => {
|
|
265
|
+
const d = decompose(mirror(m))
|
|
266
|
+
return { position: d.t.map(r6) as Vec3, rotation: d.r.map(r6) as Quat, scale: d.s.map(r6) as Vec3 }
|
|
269
267
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
268
|
+
const inRegion = (m: Mat4): boolean => !opts.region || (m[12] >= opts.region[0] && m[12] <= opts.region[2] && m[14] >= opts.region[1] && m[14] <= opts.region[3])
|
|
269
|
+
const place = (k: SceneKind, name: string, group: string, m: Mat4): boolean => {
|
|
270
|
+
if (opts.group && !group.includes(opts.group)) return false
|
|
271
|
+
if (opts.filter && !(opts.filter.test(k.name) || opts.filter.test(group) || opts.filter.test(name))) return false
|
|
272
|
+
if (opts.drop && (opts.drop.test(k.name) || opts.drop.test(group) || opts.drop.test(name))) return false
|
|
273
|
+
if (!inRegion(m)) return false
|
|
274
|
+
const w = worldOf(m)
|
|
275
|
+
if (opts.dropAt?.some((p) => Math.hypot(p[0] - w.position[0], p[1] - w.position[1], p[2] - w.position[2]) < 0.1)) return false
|
|
276
|
+
k.count++
|
|
277
|
+
instances.push({ kind: k.name, name, group, ...w })
|
|
278
|
+
return true
|
|
281
279
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
/** A prefab instance at `world` (its root's own transform included) — itself and every nested one. */
|
|
281
|
+
const placePrefab = (root: UGO, world: Mat4, groups: string[], fallbackName: string): number => {
|
|
282
|
+
const nested: { g: UGO, local: Mat4 }[] = []
|
|
283
|
+
const k = kindOf(root, root.prefab ?? null, fallbackName, nested)
|
|
284
|
+
const name = goName(root) || fallbackName
|
|
285
|
+
let n = k && place(k, name, groups.join("/"), world) ? 1 : 0
|
|
286
|
+
for (const x of nested) n += placePrefab(x.g, mmul(world, x.local), [ ...groups, name ], goName(x.g))
|
|
287
|
+
return n
|
|
285
288
|
}
|
|
286
289
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
290
|
+
let terrainInfo: SceneTerrainInfo | null = null
|
|
291
|
+
let terrainOrigin: Vec3 = [ 0, 0, 0 ]
|
|
292
|
+
const visit = (g: UGO, parentM: Mat4, groups: string[]): void => {
|
|
293
|
+
if (!goActive(g) && !opts.includeInactive) { stats.inactive++; return }
|
|
294
|
+
const l = localTRS(g)
|
|
295
|
+
const m = mmul(parentM, trsMatrix(l.t, l.r, l.s))
|
|
296
|
+
const name = goName(g)
|
|
297
|
+
if (g.prefab) {
|
|
298
|
+
stats.prefabInstances++
|
|
299
|
+
placePrefab(g, m, groups, name)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
// the scene's own objects
|
|
303
|
+
const light = comp(g, "Light")
|
|
304
|
+
if (light && enabled(light)) lights.push(readLight(name, light, mirror(m), g.comps))
|
|
305
|
+
const cam = comp(g, "Camera")
|
|
306
|
+
if (cam && enabled(cam)) cameras.push(readCamera(name, cam, mirror(m)))
|
|
307
|
+
const terrain = comp(g, "Terrain")
|
|
308
|
+
if (terrain && enabled(terrain) && tdLocal && !terrainInfo) {
|
|
309
|
+
const td = tdLocal
|
|
310
|
+
terrainOrigin = [ m[12], m[13], m[14] ]
|
|
311
|
+
const origin: Vec3 = handedness === "x" ? [ -(m[12] + td.size[0]), m[13], m[14] ] : [ m[12], m[13], m[14] ]
|
|
312
|
+
terrainInfo = {
|
|
313
|
+
name: td.name, dataPath: tdPath!, position: origin.map(r6) as Vec3, resolution: td.resolution, size: td.size,
|
|
314
|
+
layers: td.layers.map((ly, i) => { let s = 0; const w = td.weights[i]; if (w) for (let k = 0; k < w.length; k++) s += w[k]; return { name: ly.name, diffuse: ly.diffuse, normal: ly.normal, tileSize: ly.tileSize, coverage: w ? r6(s / w.length / 255) : 0 } }),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (comp(g, "MeshFilter") && comp(g, "MeshRenderer") && !terrain) {
|
|
318
|
+
// a loose mesh object: its own kind, children visited separately
|
|
319
|
+
const k = kindOf({ ...g, children: [] }, null, name, [])
|
|
320
|
+
if (k && place(k, name, groups.join("/"), m)) stats.sceneMeshes++
|
|
321
|
+
}
|
|
322
|
+
for (const ch of g.children) visit(ch, m, [ ...groups, name ])
|
|
298
323
|
}
|
|
324
|
+
for (const r of tree.roots) visit(r, I4(), [])
|
|
299
325
|
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (opts.filter && !(opts.filter.test(name) || opts.filter.test(group) || (prefabPath !== null && opts.filter.test(prefabPath)))) return
|
|
308
|
-
if (!inRegion(res.w)) return
|
|
309
|
-
const w = toLeCodes(res.w, handedness)
|
|
310
|
-
const inst: SceneInstance = { name, prefab, group, position: w.t.map(r6) as Vec3, rotation: w.r.map(r6) as Quat, scale: w.s.map(r6) as Vec3 }
|
|
311
|
-
if (res.approx) { inst.approximate = true; stats.approximate++ }
|
|
312
|
-
instances.push(inst)
|
|
313
|
-
}
|
|
314
|
-
for (const [ id, p ] of prefabInstances) {
|
|
315
|
-
stats.prefabInstances++
|
|
316
|
-
if (!p.active && !opts.includeInactive) { stats.inactive++; continue }
|
|
317
|
-
const guid = p.guid ?? "?"
|
|
318
|
-
const path = guidPath.get(guid) ?? null
|
|
319
|
-
if (!prefabs[guid]) {
|
|
320
|
-
const name = prefabName(p.guid)
|
|
321
|
-
if (!path) stats.unresolvedPrefabs++
|
|
322
|
-
const fbx = path ? guessFbx(path, name) : null
|
|
323
|
-
prefabs[guid] = { path, name, fbx, node: fbx ? name : null, count: 0 }
|
|
326
|
+
// ---- terrain trees + details: placements of their prefabs ----
|
|
327
|
+
const td = terrainInfo ? tdLocal : null
|
|
328
|
+
if (td) {
|
|
329
|
+
const protos = new Map<string, UGO | null>()
|
|
330
|
+
const protoRoot = (guid: string): UGO | null => {
|
|
331
|
+
if (!protos.has(guid)) protos.set(guid, loader.prefab(guid)?.roots[0] ?? null)
|
|
332
|
+
return protos.get(guid)!
|
|
324
333
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (!
|
|
339
|
-
prefabs[key].count++
|
|
340
|
-
push(go.name, key, resolveT(tId), fbx)
|
|
334
|
+
const emit = (list: UnityTerrain["trees"], group: string): number => {
|
|
335
|
+
let n = 0
|
|
336
|
+
for (const p of list) {
|
|
337
|
+
const root = protoRoot(p.prefab)
|
|
338
|
+
if (!root) continue
|
|
339
|
+
root.prefab ??= p.prefab
|
|
340
|
+
const h = p.rotation / 2
|
|
341
|
+
const pos: Vec3 = [ terrainOrigin[0] + p.position[0], terrainOrigin[1] + p.position[1], terrainOrigin[2] + p.position[2] ]
|
|
342
|
+
n += placePrefab(root, trsMatrix(pos, [ 0, Math.sin(h), 0, Math.cos(h) ], [ p.widthScale, p.heightScale, p.widthScale ]), [ group ], prefabName(p.prefab) ?? p.prefab)
|
|
343
|
+
}
|
|
344
|
+
return n
|
|
345
|
+
}
|
|
346
|
+
if (!opts.noTrees) stats.trees = emit(td.trees, "Terrain/Trees")
|
|
347
|
+
if (!opts.noDetails) stats.details = emit(td.details, "Terrain/Details")
|
|
341
348
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
for (const
|
|
345
|
-
|
|
349
|
+
|
|
350
|
+
// kinds nothing uses (filtered out) are dropped
|
|
351
|
+
for (const k of Object.keys(kinds)) if (!kinds[k].count) delete kinds[k]
|
|
352
|
+
stats.unresolvedMods = tstats.unresolvedMods
|
|
353
|
+
stats.guessedTransforms = tstats.guessedTransforms
|
|
354
|
+
stats.missingPrefabs = tstats.missingPrefabs.size
|
|
355
|
+
stats.binaryPrefabs = [ ...tstats.binaryPrefabs ]
|
|
346
356
|
|
|
347
357
|
return {
|
|
348
358
|
source: basename(scenePath),
|
|
349
|
-
frame: handedness === "none" ? "unity (left-handed, untouched)" :
|
|
350
|
-
|
|
359
|
+
frame: handedness === "none" ? "unity (left-handed, untouched)" : "lecodes: Unity mirrored on X (position −x, quaternion (x, −y, −z, w)), metres, Y up",
|
|
360
|
+
kinds, instances, lights, cameras, terrain: terrainInfo, terrainData: terrainInfo ? tdLocal : null, stats,
|
|
351
361
|
}
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
/** The prefab table as printable lines, most used first. */
|
|
364
|
+
/** The import as printable lines: kinds most used first, then what was skipped. */
|
|
357
365
|
export const describeSceneImport = (r: SceneImport, max = 20): string[] => {
|
|
358
|
-
const rows = Object.
|
|
359
|
-
const
|
|
360
|
-
|
|
366
|
+
const rows = Object.values(r.kinds).sort((a, b) => b.count - a.count)
|
|
367
|
+
const s = r.stats
|
|
368
|
+
const lines = [ `${r.instances.length} placements of ${rows.length} kind(s) — ${s.prefabInstances} prefab instances, ${s.trees} terrain trees, ${s.details} detail meshes, ${s.sceneMeshes} scene meshes${s.variants ? ` · ${s.variants} override variant(s)` : ""}${s.inactive ? ` · ${s.inactive} inactive skipped` : ""}` ]
|
|
369
|
+
for (const k of rows.slice(0, max)) lines.push(` ${String(k.count).padStart(5)} × ${k.name.padEnd(34)} ${k.parts.length} mesh part(s)${k.colliders.length ? `, ${k.colliders.length} collider(s)` : ""}${k.parts.some((p) => p.lod > 0) ? `, LOD 0–${Math.max(...k.parts.map((p) => p.lod))}` : ""}`)
|
|
361
370
|
if (rows.length > max) lines.push(` … ${rows.length - max} more`)
|
|
371
|
+
if (r.terrain) lines.push(` terrain ${r.terrain.name}: ${r.terrain.resolution}² samples, ${r.terrain.size[0]}×${r.terrain.size[2]} m, layers ${r.terrain.layers.map((l) => `${l.name} ${(l.coverage * 100).toFixed(1)}%`).join(", ")}`)
|
|
372
|
+
if (r.lights.length) lines.push(` lights: ${r.lights.map((l) => `${l.area ? `${l.area.shape} ${+l.area.width.toFixed(2)}${l.area.height ? ` x ${+l.area.height.toFixed(2)}` : ""} m` : l.type} ${l.name}`).join(", ")}`)
|
|
373
|
+
const areaKinds = Object.values(r.kinds).filter((k) => k.lights.some((l) => l.area))
|
|
374
|
+
if (areaKinds.length) lines.push(` area lights in prefabs: ${areaKinds.map((k) => `${k.name} x${k.count} (${k.lights.filter((l) => l.area).map((l) => `${l.area!.shape} ${+l.area!.width.toFixed(2)} x ${+l.area!.height.toFixed(2)} m`).join(", ")})`).join(", ")} - the lightmap bake takes them as rectangles: Light.point({ bakeArea })`)
|
|
375
|
+
const notes: string[] = []
|
|
376
|
+
if (s.unresolvedMods) notes.push(`${s.unresolvedMods} prefab modification(s) with no target`)
|
|
377
|
+
if (s.guessedTransforms) notes.push(`${s.guessedTransforms} unaddressable child transform(s) matched by position`)
|
|
378
|
+
if (s.missingPrefabs) notes.push(`${s.missingPrefabs} prefab GUID(s) not found`)
|
|
379
|
+
if (s.binaryPrefabs.length) notes.push(`${s.binaryPrefabs.length} binary-serialised prefab(s) skipped`)
|
|
380
|
+
if (s.particles) notes.push(`${s.particles} particle system(s) not converted`)
|
|
381
|
+
if (s.prefabLights) notes.push(`${s.prefabLights} light(s) inside prefabs not converted`)
|
|
382
|
+
if (notes.length) lines.push(` ℹ ${notes.join(" · ")}`)
|
|
362
383
|
return lines
|
|
363
384
|
}
|