-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplus.py
More file actions
241 lines (197 loc) · 8.93 KB
/
Copy pathplus.py
File metadata and controls
241 lines (197 loc) · 8.93 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
235
236
237
238
239
240
241
# plus.py
# blender를 이용하여 DECA 모델과 body 모델을 병합
import bpy, sys
import numpy as np
from mathutils import Vector
import os, sys
# ── 인자 처리 ─────────────────────────────────────
argv = sys.argv
argv = argv[argv.index("--") + 1:] if "--" in argv else []
if len(argv) == 3:
deca_path, base_glb, out_glb = argv
hair_path = None
elif len(argv) == 4:
deca_path, base_glb, out_glb, hair_path = argv
else:
print("Usage: blender --background --python plus.py -- <deca.obj/glb> [<hair.glb>] <base.glb> <out.glb>")
sys.exit(1)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DECA_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "DECA"))
# ── 몸통 기준 얼굴 랜드마크 (절대 위치) ────────────
body_left_cheek = Vector((-0.087, -0.079, 1.653))
body_right_cheek = Vector(( 0.0875, -0.079, 1.653))
body_nose = Vector(( 0.0006, -0.099, 1.6025))
body_chin = Vector(( 0.0006, -0.0635, 1.54))
body_back_head = Vector(( 0.0006, 0.0200, 1.608))
# ── 유틸: 임포트된 새 오브젝트 목록 가져오기 ─────────
def import_gltf(filepath):
before = set(bpy.data.objects)
bpy.ops.import_scene.gltf(filepath=filepath)
after = set(bpy.data.objects)
return [o for o in after - before]
def import_obj(filepath):
before = set(bpy.data.objects)
bpy.ops.wm.obj_import(filepath=filepath)
after = set(bpy.data.objects)
return [o for o in after - before]
# ── 씬 초기화 + body 불러오기 ──────────────────────
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.data.orphans_purge(do_recursive=True)
if base_glb.lower().endswith((".glb", ".gltf")):
body_objs = import_gltf(base_glb)
else:
raise RuntimeError(f"지원하지 않는 base 형식: {base_glb}")
# ── DECA 모델 임포트 + 트랜스폼 초기화 ─────────────
if deca_path.lower().endswith(".obj"):
bpy.ops.wm.obj_import(filepath=deca_path)
else:
bpy.ops.import_scene.gltf(filepath=deca_path)
deca_obj = next(o for o in bpy.context.selected_objects if o.type=="MESH")
deca_obj.location = (0,0,0)
deca_obj.rotation_euler = (0,0,0)
bpy.context.view_layer.update()
# ── DECA와 본체 전면(front) 방향 맞추기 ─────────────
import math
from mathutils import Euler
# X축 -90° 돌려 Z-up 맞추고, Z축 180° 돌려 앞뒤 반전 보정
#deca_obj.rotation_euler = Euler((math.radians(90), 0, 0), 'XYZ')
deca_obj.rotation_euler = Euler((0, 0, 0), 'XYZ')
bpy.context.view_layer.update()
# ── 메쉬 평가본 가져오기 ────────────────────────────
deps = bpy.context.evaluated_depsgraph_get()
mesh_eval = deca_obj.evaluated_get(deps).to_mesh()
# ── 월드 좌표 정점 배열 생성 ─────────────────────────
verts = np.array([list(deca_obj.matrix_world @ v.co) for v in mesh_eval.vertices])
# ── DECA 3D 랜드마크 계산 (barycentric interpolation) ─────────────
# clean npy 에는 full_lmk_faces_idx, full_lmk_bary_coords, static_lmk_faces_idx 등이 dict 형태로 들어 있습니다.
lmk_path = os.path.join(DECA_ROOT, "data", "embedding_full_clean.npz")
npz = np.load(lmk_path)
full_faces = npz['full_lmk_faces_idx'][0]
full_barys = npz['full_lmk_bary_coords'][0]
# 평가용 메시 & 월드 좌표 리스트
deps = bpy.context.evaluated_depsgraph_get()
mesh_eval = deca_obj.evaluated_get(deps).to_mesh()
verts3d = [deca_obj.matrix_world @ v.co for v in mesh_eval.vertices]
# face → vertex 인덱스 뽑기
def interp_point(lmk_id):
f_idx = int(full_faces[lmk_id])
w0, w1, w2 = full_barys[lmk_id]
poly = mesh_eval.polygons[f_idx]
i0, i1, i2 = poly.vertices
p0, p1, p2 = verts3d[i0], verts3d[i1], verts3d[i2]
return w0*p0 + w1*p1 + w2*p2
# Landmark ID 기준 (Multi-PIE): 30=코끝, 2=왼광대, 14=오광대
nose_world = Vector(interp_point(30))
L_cheek = Vector(interp_point(2))
R_cheek = Vector(interp_point(14))
# ── 턱(chin) 추출 (Multi-PIE 68pt 기준 8번) ─────────────
chin_world = Vector(interp_point(8))
## ── 코끝→턱 벡터 계산 & 정규화
v_deca = (nose_world - chin_world).normalized()
v_body = (body_nose - body_chin ).normalized()
## ── 두 벡터를 일치시키는 회전(quaternion) 계산
axis = v_deca.cross(v_body)
angle = v_deca.angle(v_body)
from mathutils import Quaternion
rot_q = Quaternion(axis, angle)
## ── DECA 객체에 회전 보정 적용
deca_obj.rotation_euler = rot_q.to_euler('XYZ')
bpy.context.view_layer.update()
# ── bounding-box로 뒤통수 근사 ───────────────────────
ys = verts[:,1]
ymax = ys.max()
xmid = (verts[:,0].min() + verts[:,0].max()) / 2
zmid = (verts[:,2].min() + verts[:,2].max()) / 2
deca_back_head = Vector((xmid, ymax, zmid))
# ── 스케일 계산 ─────────────────────────────────────
body_cheek_dist = (body_right_cheek - body_left_cheek).length
deca_cheek_dist = (R_cheek - L_cheek).length
scale_xy = body_cheek_dist / deca_cheek_dist
body_face_depth = (body_back_head - body_nose).length
deca_face_depth = (deca_back_head - nose_world).length
scale_z = scale_xy if abs(deca_face_depth)<1e-6 else body_face_depth / deca_face_depth
UNIT_CONV = 0.75
deca_obj.scale = (scale_xy* UNIT_CONV, scale_xy* UNIT_CONV, scale_xy* UNIT_CONV,)
bpy.context.view_layer.update()
# ── 스케일 후 코끝 위치 재계산 + 이동 ────────────────
deps = bpy.context.evaluated_depsgraph_get()
mesh_eval = deca_obj.evaluated_get(deps).to_mesh()
verts3d = [deca_obj.matrix_world @ v.co for v in mesh_eval.vertices]
scaled_nose = interp_point(30)
scaled_nose = Vector(scaled_nose)
translation = body_nose - scaled_nose
deca_obj.location = translation
bpy.context.view_layer.update()
# ── 텍스쳐 유효 폴리곤만 남기기 ──────────────────
import bmesh
# 1) 메쉬 & UV 레이어 가져오기
mesh = deca_obj.data
if not mesh.uv_layers.active:
raise RuntimeError("활성 UV 레이어가 없습니다.")
uv_layer = mesh.uv_layers.active.data
# 2) 재질에서 Image 텍스쳐 찾기
img = None
for slot in deca_obj.material_slots:
mat = slot.material
if mat and mat.use_nodes:
for node in mat.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image is not None:
img = node.image
break
if img:
break
if img is None:
raise RuntimeError("Image 텍스쳐 노드를 찾을 수 없습니다.")
# 3) 픽셀 데이터 로드
w, h = img.size
# img.pixels는 [r,g,b,a, r,g,b,a, ...] 형태의 flat list
pixels = np.array(img.pixels[:])
# 4) BMesh로 읽어서, 텍스쳐 없는 폴리곤 수집
bm = bmesh.new()
bm.from_mesh(mesh)
to_delete = []
layer = bm.loops.layers.uv.active
tol = 0.02 # 회색 판정 허용 오차
for face in bm.faces:
keep = False
for loop in face.loops:
u, v = loop[layer].uv
x = min(max(int(u * (w - 1)), 0), w - 1)
y = min(max(int(v * (h - 1)), 0), h - 1)
idx = (y * w + x) * 4
r, g, b, a = pixels[idx:idx+4]
# 1) 투명도 낮거나 2) RGB가 거의 동일한 균일 회색 영역은 스킵
if a < 0.01 or (max(r, g, b) - min(r, g, b) < tol):
continue
# 나머지(컬러가 있는) 픽셀만 있으면 이 폴리곤은 살린다
keep = True
break
if not keep:
to_delete.append(face)
# 5) 삭제
bmesh.ops.delete(bm, geom=to_delete, context='FACES')
bm.to_mesh(mesh)
bm.free()
bpy.context.view_layer.update()
# ────────────────────────────────────────────
hair_objs = []
if hair_path:
hair_objs = import_gltf(hair_path)
# 헤어는 body 좌표계에 맞춰 이미 제작되었다고 하셨으니 0/0/1 유지
for o in hair_objs:
o.location = (0, 0, 0)
o.rotation_euler = (0, 0, 0)
o.scale = (1, 1, 1)
bpy.context.view_layer.update()
# ── ★ 선택만 내보내기
for o in bpy.data.objects:
o.select_set(False)
for o in body_objs + [deca_obj] + hair_objs:
o.select_set(True)
# ── body + DECA 함께 내보내기 ───────────────────────
bpy.ops.export_scene.gltf(
filepath=out_glb,
export_format='GLB',
use_selection=True
)
print("✅ 완전 병합 & 정렬 완료:", out_glb)