-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathblender_npc_anim.py
More file actions
114 lines (101 loc) · 3.86 KB
/
Copy pathblender_npc_anim.py
File metadata and controls
114 lines (101 loc) · 3.86 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
"""Headless Blender: build a MULTI-MATERIAL vertex-animated NPC mesh from baked
per-frame positions and export it to FBX with blend shapes + a looping clip and
EMBEDDED textures. Unity overlays this on the static NPCPOSE__ placement (same
pattern as the animated MORPH objects).
blender --background --python blender_npc_anim.py -- <data.npz> <mats.json> <out.fbx>
data.npz: verts (F,V,3) per-frame positions (file scale, pre-rotated x,-z,y),
faces (T,3), uvs (V,2), face_mat (T,) material index per face, fps.
mats.json: {"tex_dir": "...", "mats": [{"name": "...", "tex": "head01.png",
"two": false}, ...]} (index = material slot, matches face_mat)
"""
import bpy
import os
import sys
import json
import numpy as np
import addon_utils
try:
addon_utils.enable("io_scene_fbx")
except Exception:
pass
argv = sys.argv[sys.argv.index("--") + 1:]
npz_path, mats_path, fbx_path = argv[0], argv[1], argv[2]
d = np.load(npz_path)
verts = d["verts"].astype(float) # (F, V, 3)
faces = d["faces"].astype(int) # (T, 3)
uvs = d["uvs"].astype(float) if "uvs" in d.files else None
face_mat = d["face_mat"].astype(int) if "face_mat" in d.files else None
fps = int(d["fps"]) if "fps" in d.files else 30
F, V, _ = verts.shape
meta = json.load(open(mats_path))
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("rose_npc")
obj = bpy.data.objects.new("rose_npc", mesh)
bpy.context.collection.objects.link(obj)
mesh.from_pydata([tuple(v) for v in verts[0]], [], [tuple(f) for f in faces])
mesh.update()
# UV0 (flip V to match the glTF/Blender convention, same as the static path)
if uvs is not None and len(uvs) == V:
uvl = mesh.uv_layers.new(name="UV0")
for loop in mesh.loops:
vi = loop.vertex_index
uvl.data[loop.index].uv = (float(uvs[vi][0]), 1.0 - float(uvs[vi][1]))
# materials with embedded base-colour textures
tex_dir = meta.get("tex_dir", "")
for m in meta["mats"]:
mat = bpy.data.materials.new(m["name"])
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if m.get("tex"):
tp = os.path.join(tex_dir, m["tex"])
if os.path.exists(tp):
img = bpy.data.images.load(tp)
tex = mat.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = img
if bsdf is not None:
mat.node_tree.links.new(bsdf.inputs["Base Color"], tex.outputs["Color"])
if m.get("two"):
mat.use_backface_culling = False
mesh.materials.append(mat)
if face_mat is not None and len(face_mat) == len(mesh.polygons):
for i, poly in enumerate(mesh.polygons):
poly.material_index = int(face_mat[i])
# basis + per-frame shape keys
obj.shape_key_add(name="Basis")
for f in range(F):
sk = obj.shape_key_add(name="f%03d" % f)
co = verts[f]
for vi in range(V):
sk.data[vi].co = (float(co[vi][0]), float(co[vi][1]), float(co[vi][2]))
# cross-fade each key 1.0 at its frame, 0.0 at neighbours -> smooth loop
scene = bpy.context.scene
scene.render.fps = max(1, fps)
scene.frame_start = 0
scene.frame_end = max(1, F - 1)
kb = mesh.shape_keys.key_blocks
for f in range(F):
sk = kb["f%03d" % f]
for tf in (f - 1, f, f + 1):
if 0 <= tf <= F - 1:
sk.value = 1.0 if tf == f else 0.0
sk.keyframe_insert("value", frame=tf)
ad = mesh.shape_keys.animation_data
if ad and ad.action:
ad.action.name = "RoseIdle"
bpy.ops.export_scene.fbx(
filepath=fbx_path,
use_selection=False,
apply_unit_scale=True,
object_types={'MESH'},
use_mesh_modifiers=False,
add_leaf_bones=False,
bake_anim=True,
bake_anim_use_all_actions=False,
bake_anim_use_nla_strips=False,
path_mode='COPY',
embed_textures=True,
axis_forward='-Z',
axis_up='Y',
)
print("[blender] NPC anim FBX written: %s (%d frames, %d verts, %d mats)"
% (fbx_path, F, V, len(meta["mats"])))