-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathexport_fbx.py
More file actions
234 lines (205 loc) · 9.67 KB
/
Copy pathexport_fbx.py
File metadata and controls
234 lines (205 loc) · 9.67 KB
1
2
3
4
5
6
7
8
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
76
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""Build a distributable FBX bundle for a zone.
Pipeline:
1. export_map.export() -> .glb (+ material manifest) [reuses the glTF path]
2. Blender (headless) -> .fbx with named material slots
3. deduped textures -> Textures/<tex>.png
4. materials.json -> material name -> texture + alpha mode + 2-sided
5. Unity + UE editor scripts + README copied in
6. zip the whole folder -> exports/<zone>_fbx.zip (one top folder inside)
Usage: python export_fbx.py JPT01-1
"""
from __future__ import annotations
import os
import sys
import json
import shutil
import zipfile
import subprocess
from PIL import Image
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import export_map
import export_anim
import export_effects
import export_npcs
import export_npc_models
import export_npc_posed
import export_npc_vat
import export_npc_unity
BLENDER = os.environ.get("BLENDER_EXE",
r"C:/Program Files/Blender Foundation/Blender 5.0/blender.exe")
TEMPLATES = os.path.join(_HERE, "fbx_templates")
def _sanitize(s):
return "".join(c if (c.isalnum() or c in "_-") else "_" for c in s)[:48]
def build(key, out_root=None):
out_root = out_root or os.path.join(_HERE, "exports")
bundle = os.path.join(out_root, "%s_fbx" % key)
if os.path.isdir(bundle):
shutil.rmtree(bundle)
os.makedirs(os.path.join(bundle, "Textures"))
os.makedirs(os.path.join(bundle, "Editor")) # Unity special folder: editor scripts
os.makedirs(os.path.join(bundle, "Shaders")) # runtime shader (URP)
os.makedirs(os.path.join(bundle, "UE5"))
# 1. glTF (intermediate) + material info
glb = os.path.join(out_root, "%s.glb" % key)
print("[fbx] building glb…")
stats = export_map.export(key, glb)
minfo = stats["material_info"]
# 2. Blender glb -> fbx
fbx = os.path.join(bundle, "%s.fbx" % key)
print("[fbx] converting to FBX via Blender…")
if not os.path.exists(BLENDER):
raise FileNotFoundError("Blender not found at %s (set BLENDER_EXE)" % BLENDER)
subprocess.run([BLENDER, "--background", "--python",
os.path.join(_HERE, "blender_glb_to_fbx.py"), "--", glb, fbx],
check=True)
if not os.path.exists(fbx):
raise RuntimeError("Blender did not produce the FBX")
# 3. deduped textures + rewrite manifest texture names
print("[fbx] writing textures…")
texmap = {} # (src.lower, alpha) -> filename
used = {} # filename -> source key (to disambiguate name clashes)
for m in minfo:
src = m.get("texture_src")
if not src:
m["texture"] = None
m.pop("texture_src", None)
continue
keyt = (src.lower(), bool(m["alpha"]))
if keyt not in texmap:
base = _sanitize(os.path.splitext(os.path.basename(src))[0])
suf = "a" if m["alpha"] else "rgb"
fn = "%s_%s.png" % (base, suf)
n = 1
while fn in used and used[fn] != keyt: # different source, same name
fn = "%s_%s_%d.png" % (base, suf, n); n += 1
used[fn] = keyt
try:
im = Image.open(src); im.load()
im = im.convert("RGBA" if m["alpha"] else "RGB")
im.save(os.path.join(bundle, "Textures", fn), "PNG")
except Exception as e:
print(" tex fail %s (%s)" % (src, e))
texmap[keyt] = fn
m["texture"] = texmap[keyt]
m.pop("texture_src", None)
# 4. manifest
with open(os.path.join(bundle, "materials.json"), "w") as f:
json.dump({"zone": key, "fbx": "%s.fbx" % key, "materials": minfo}, f, indent=1)
# 5. editor scripts + shader + readme
shutil.copy2(os.path.join(TEMPLATES, "AssignRoseMaterials.cs"),
os.path.join(bundle, "Editor", "AssignRoseMaterials.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RoseAnimatedObjects.cs"),
os.path.join(bundle, "Editor", "RoseAnimatedObjects.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RoseEffects.cs"),
os.path.join(bundle, "Editor", "RoseEffects.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RoseNPCs.cs"),
os.path.join(bundle, "Editor", "RoseNPCs.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RosePlayerSetup.cs"),
os.path.join(bundle, "Editor", "RosePlayerSetup.cs"))
shutil.copy2(os.path.join(TEMPLATES, "ROSE_URP_Lit.shader"),
os.path.join(bundle, "Shaders", "ROSE_URP_Lit.shader"))
shutil.copy2(os.path.join(TEMPLATES, "ROSE_Skybox.shader"),
os.path.join(bundle, "Shaders", "ROSE_Skybox.shader"))
shutil.copy2(os.path.join(TEMPLATES, "ROSE_Water.shader"),
os.path.join(bundle, "Shaders", "ROSE_Water.shader"))
shutil.copy2(os.path.join(TEMPLATES, "RoseAnimationSpeed.cs"), # runtime (not in Editor/)
os.path.join(bundle, "RoseAnimationSpeed.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RoseFlyCamera.cs"), # runtime fly camera
os.path.join(bundle, "RoseFlyCamera.cs"))
shutil.copy2(os.path.join(TEMPLATES, "RoseCubePlayer.cs"), # runtime cube player
os.path.join(bundle, "RoseCubePlayer.cs"))
shutil.copy2(os.path.join(TEMPLATES, "assign_rose_materials_ue.py"),
os.path.join(bundle, "UE5", "assign_rose_materials_ue.py"))
shutil.copy2(os.path.join(TEMPLATES, "import_rose_map_ue.py"),
os.path.join(bundle, "UE5", "import_rose_map_ue.py"))
shutil.copy2(os.path.join(TEMPLATES, "import_npcs_ue.py"),
os.path.join(bundle, "UE5", "import_npcs_ue.py"))
shutil.copy2(os.path.join(TEMPLATES, "import_npcs_vat_ue.py"),
os.path.join(bundle, "UE5", "import_npcs_vat_ue.py"))
shutil.copy2(os.path.join(TEMPLATES, "README.txt"), os.path.join(bundle, "README.txt"))
# 5b. animated MORPH objects (waving banners, streaming water) -> Animations/
anim_stats = None
try:
print("[fbx] baking animated objects…")
export_anim.build(key, out_root=bundle) # creates <bundle>/<key>_anim
src_anim = os.path.join(bundle, "%s_anim" % key)
dst_anim = os.path.join(bundle, "Animations")
if os.path.isdir(src_anim):
if os.path.isdir(dst_anim):
shutil.rmtree(dst_anim)
shutil.move(src_anim, dst_anim)
anim_stats = {"fbx": len([f for f in os.listdir(dst_anim) if f.lower().endswith(".fbx")])}
except Exception as e:
print(" [fbx] animation bake skipped: %s" % e)
# 5c. data-driven particle effects (object-attached EFTs + standalone) ->
# Effects/effects.json + Effects/Textures/, built by RoseEffects.cs.
fx_stats = None
try:
print("[fbx] exporting effects…")
fx_stats = export_effects.build(key, bundle)
except Exception as e:
print(" [fbx] effects export skipped: %s" % e)
# 5d. NPC (MOB) + monster-spawn (REGEN) placements -> NPCs/npcs.json, used by
# the UE5 importer (import_rose_map_ue.py) to drop markers on the map.
npc_stats = None
try:
print("[fbx] exporting NPC/monster placements…")
npc_stats = export_npcs.build(key, bundle)
except Exception as e:
print(" [fbx] NPC export skipped: %s" % e)
# 5e. animated skeletal NPC/monster meshes -> NPCs/Models/<id>.fbx (Blender).
npc_model_stats = None
try:
print("[fbx] building animated NPC/monster meshes…")
npc_model_stats = export_npc_models.build(key, bundle)
except Exception as e:
print(" [fbx] NPC models skipped: %s" % e)
# 5f. posed-static NPC glb (1:1 with the map pipeline) -> npcs_posed.glb
try:
print("[fbx] building posed NPC glb…")
export_npc_posed.build(key, os.path.join(bundle, "npcs_posed.glb"))
except Exception as e:
print(" [fbx] posed NPCs skipped: %s" % e)
# 5g. VAT animated NPC crowd (static meshes + WPO idle anim) -> npcs_vat.glb + VAT/
try:
print("[fbx] building VAT NPC crowd…")
export_npc_vat.build(key, bundle)
except Exception as e:
print(" [fbx] VAT NPCs skipped: %s" % e)
# 5h. Unity NPC crowd: posed-static FBX (1:1) + per-character blend-shape idle
try:
print("[fbx] building Unity NPC crowd (blend-shape idle)…")
export_npc_unity.build(key, bundle)
except Exception as e:
print(" [fbx] Unity NPCs skipped: %s" % e)
# cleanup the intermediate glb + its sidecar (bundle is FBX-only)
for p in (glb, glb + ".materials.json"):
if os.path.exists(p):
os.remove(p)
# 6. zip (one top folder inside)
zip_path = os.path.join(out_root, "%s_fbx.zip" % key)
if os.path.exists(zip_path):
os.remove(zip_path)
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
for root, _dirs, files in os.walk(bundle):
for fn in files:
full = os.path.join(root, fn)
arc = os.path.relpath(full, out_root)
z.write(full, arc)
return {
"bundle": bundle, "zip": zip_path,
"fbx_bytes": os.path.getsize(fbx),
"textures": len(os.listdir(os.path.join(bundle, "Textures"))),
"materials": len(minfo),
"animations": anim_stats["fbx"] if anim_stats else 0,
"effects": fx_stats if fx_stats else 0,
"npcs": npc_stats if npc_stats else 0,
"npc_models": npc_model_stats if npc_model_stats else 0,
"zip_bytes": os.path.getsize(zip_path),
}
if __name__ == "__main__":
k = sys.argv[1] if len(sys.argv) > 1 else "JPT01-1"
print("building FBX bundle for %s" % k)
print(json.dumps(build(k), indent=2))