This commit is contained in:
2026-04-27 19:21:50 -07:00
parent 98856b3f54
commit a3d5fa846b
16 changed files with 513 additions and 337 deletions

View File

@ -610,19 +610,30 @@ class LeenkxExporter:
return None
def write_bone_matrices(self, scene, action):
# profile_time = time.time()
begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1])
if len(self.bone_tracks) > 0:
for i in range(begin_frame, end_frame + 1):
scene.frame_set(i)
for track in self.bone_tracks:
values, pose_bone = track[0], track[1]
parent = pose_bone.parent
if parent:
values += LeenkxExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix))
else:
values += LeenkxExporter.write_matrix(pose_bone.matrix)
# print('Bone matrices exported in ' + str(time.time() - profile_time))
hidden_states = {}
try:
for obj in list(scene.collection.all_objects):
if obj is not None and obj.type != 'ARMATURE' and not obj.hide_viewport:
hidden_states[obj] = False
obj.hide_viewport = True
for i in range(begin_frame, end_frame + 1):
scene.frame_set(i)
for track in self.bone_tracks:
values, pose_bone = track[0], track[1]
parent = pose_bone.parent
if parent:
values += LeenkxExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix))
else:
values += LeenkxExporter.write_matrix(pose_bone.matrix)
finally:
for obj, was_hidden in hidden_states.items():
try:
obj.hide_viewport = was_hidden
except ReferenceError:
pass
@staticmethod
def has_baked_material(bobject, materials):
@ -849,23 +860,36 @@ class LeenkxExporter:
out_object['tilesheet_ref'] = bobject.lnx_tilesheet
out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action
if len(bobject.vertex_groups) > 0:
if len(bobject.vertex_groups) > 0 and bobject.data is not None and len(bobject.data.vertices) > 0:
out_object['vertex_groups'] = []
_verts = bobject.data.vertices
_num_verts = len(_verts)
_all_co = np.empty(_num_verts * 3, dtype='<f8')
_verts.foreach_get('co', _all_co)
_all_str = _all_co.astype('U32').tolist()
_num_groups = len(bobject.vertex_groups)
_vg_idx = [[] for _ in range(_num_groups)]
for v in _verts:
vi = v.index
for g in v.groups:
gi = g.group
if 0 <= gi < _num_groups:
_vg_idx[gi].append(vi)
for group in bobject.vertex_groups:
verts = []
for v in bobject.data.vertices:
for g in v.groups:
if g.group == group.index:
verts.append(str(v.co.x))
verts.append(str(v.co.y))
verts.append(str(v.co.z))
out_vertex_groups = {
indices = _vg_idx[group.index]
if len(indices) > 0:
_values = []
for vi in indices:
base = vi * 3
_values.append(_all_str[base])
_values.append(_all_str[base + 1])
_values.append(_all_str[base + 2])
else:
_values = []
out_object['vertex_groups'].append({
'name': group.name,
'value': verts
}
out_object['vertex_groups'].append(out_vertex_groups)
'value': _values
})
if len(bobject.lnx_camera_list) > 0:
out_camera_list = []
@ -1114,13 +1138,27 @@ class LeenkxExporter:
_o.select_set(False)
skelobj.select_set(True)
bake_result = bpy.ops.nla.bake(
frame_start=int(action.frame_range[0]),
frame_end=int(action.frame_range[1]),
step=1,
only_selected=False,
visual_keying=True
)
_bake_hidden = {}
try:
for _obj in list(bpy.context.scene.collection.all_objects):
if _obj is not None and _obj.type != 'ARMATURE' and not _obj.hide_viewport:
_bake_hidden[_obj] = False
_obj.hide_viewport = True
bake_result = bpy.ops.nla.bake(
frame_start=int(action.frame_range[0]),
frame_end=int(action.frame_range[1]),
step=1,
only_selected=False,
visual_keying=True
)
finally:
for _obj, _was_hidden in _bake_hidden.items():
try:
_obj.hide_viewport = _was_hidden
except ReferenceError:
pass
action = skelobj.animation_data.action
skelobj.select_set(False)
@ -1225,44 +1263,67 @@ class LeenkxExporter:
else:
group_remap.append(-1)
bone_count_array = np.empty(len(export_mesh.loops), dtype='<i2')
bone_index_array = np.empty(len(export_mesh.loops) * 4, dtype='<i2')
bone_weight_array = np.empty(len(export_mesh.loops) * 4, dtype='<f4')
num_loops = len(export_mesh.loops)
_loop_vidx = np.empty(num_loops, dtype='<i4')
export_mesh.loops.foreach_get('vertex_index', _loop_vidx)
num_verts = len(bobject.data.vertices)
verts = bobject.data.vertices
vert_bone_count = np.zeros(num_verts, dtype='<i2')
vert_bone_index = np.zeros((num_verts, 4), dtype='<i2')
vert_bone_weight = np.zeros((num_verts, 4), dtype='<f4')
_group_remap = group_remap
for vi in range(num_verts):
groups = verts[vi].groups
n = len(groups)
if n == 0:
continue
vertices = bobject.data.vertices
count = 0
for index, l in enumerate(export_mesh.loops):
bone_count = 0
total_weight = 0.0
bone_values = []
for g in vertices[l.vertex_index].groups:
bone_index = group_remap[g.group]
bone_weight = g.weight
if bone_index >= 0: #and bone_weight != 0.0:
bone_values.append((bone_weight, bone_index))
total_weight += bone_weight
bone_count += 1
total_weight = 0.0
for gi in range(n):
g = groups[gi]
bone_index = _group_remap[g.group]
if bone_index >= 0:
bone_values.append((g.weight, bone_index))
total_weight += g.weight
bone_count = len(bone_values)
if bone_count == 0:
continue
if bone_count > 4:
bone_count = 4
bone_values.sort(reverse=True)
bone_values = bone_values[:4]
bone_count_array[index] = bone_count
for bv in bone_values:
bone_weight_array[count] = bv[0]
bone_index_array[count] = bv[1]
count += 1
bone_count = 4
total_weight = sum(bv[0] for bv in bone_values)
if total_weight not in (0.0, 1.0):
normalizer = 1.0 / total_weight
for i in range(bone_count):
bone_weight_array[count - i - 1] *= normalizer
bone_values = [(bv[0] * normalizer, bv[1]) for bv in bone_values]
vert_bone_count[vi] = bone_count
for j in range(bone_count):
vert_bone_weight[vi, j] = bone_values[j][0]
vert_bone_index[vi, j] = bone_values[j][1]
bone_count_array = vert_bone_count[_loop_vidx]
loop_bone_weight = vert_bone_weight[_loop_vidx]
loop_bone_index = vert_bone_index[_loop_vidx]
loop_bone_count = bone_count_array
slot_idx = np.arange(4, dtype='<i2')
active_mask = slot_idx[np.newaxis, :] < loop_bone_count[:, np.newaxis]
bone_weight_array = loop_bone_weight[active_mask].astype('<f4')
bone_index_array = loop_bone_index[active_mask].astype('<i2')
bone_index_array = bone_index_array[:count]
bone_weight_array = bone_weight_array[:count]
bone_weight_array *= 32767
bone_weight_array = np.array(bone_weight_array, dtype='<i2')
bone_weight_array = bone_weight_array.astype('<i2')
oskin['bone_count_array'] = bone_count_array
oskin['bone_index_array'] = bone_index_array
@ -1291,6 +1352,8 @@ class LeenkxExporter:
shape_key_base = bobject.data.shape_keys.key_blocks[0]
count = 0
_base_co = None
_base_nor = None
# Loop through all shape keys
for shape_key in bobject.data.shape_keys.key_blocks[1:]:
@ -1299,7 +1362,13 @@ class LeenkxExporter:
# get vertex data from shape key
if shape_key.mute:
continue
vert_data = self.get_vertex_data_from_shape_key(shape_key_base, shape_key)
vert_data = self.get_vertex_data_from_shape_key(
shape_key_base, shape_key,
_base_co=_base_co, _base_nor=_base_nor)
# Cache base arrays after first call
if _base_co is None:
_base_co = vert_data['_base_co']
_base_nor = vert_data['_base_nor']
vert_pos.append(vert_data['pos'])
vert_nor.append(vert_data['nor'])
names.append(shape_key.name)
@ -1342,37 +1411,39 @@ class LeenkxExporter:
morph_target['morph_target_ref'] = names
morph_target['morph_target_defaults'] = default_values
morph_target['num_morph_targets'] = count
morph_target['morph_scale'] = max - min
morph_target['morph_offset'] = min
morph_target['morph_img_size'] = img_size
morph_target['morph_block_size'] = block_size
morph_target['morph_scale'] = float(max - min)
morph_target['morph_offset'] = float(min)
morph_target['morph_img_size'] = int(img_size)
morph_target['morph_block_size'] = int(block_size)
out_mesh['morph_target'] = morph_target
return
def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data):
def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data, _base_co=None, _base_nor=None):
base_vert_pos = shape_key_base.data.values()
base_vert_nor = shape_key_base.normals_split_get()
vert_pos = shape_key_data.data.values()
vert_nor = shape_key_data.normals_split_get()
num_verts = len(shape_key_data.data)
num_verts = len(vert_pos)
sk_co = np.empty(num_verts * 3, dtype='<f4')
shape_key_data.data.foreach_get('co', sk_co)
sk_co = sk_co.reshape(num_verts, 3)
pos = []
nor = []
if _base_co is None:
_base_co = np.empty(num_verts * 3, dtype='<f4')
shape_key_base.data.foreach_get('co', _base_co)
_base_co = _base_co.reshape(num_verts, 3)
# Loop through all vertices
for i in range(num_verts):
# Vertex position relative to base vertex
pos.append(list(vert_pos[i].co - base_vert_pos[i].co))
temp = []
for j in range(3):
# Vertex normal relative to base vertex
temp.append(vert_nor[j + i * 3] - base_vert_nor[j + i * 3])
nor.append(temp)
pos_delta = sk_co - _base_co
return {'pos': pos, 'nor': nor}
sk_nor_raw = np.array(shape_key_data.normals_split_get(), dtype='<f4')
sk_nor = sk_nor_raw[:num_verts * 3].reshape(num_verts, 3)
if _base_nor is None:
base_nor_raw = np.array(shape_key_base.normals_split_get(), dtype='<f4')
_base_nor = base_nor_raw[:num_verts * 3].reshape(num_verts, 3)
nor_delta = sk_nor - _base_nor
return {'pos': pos_delta, 'nor': nor_delta, '_base_co': _base_co, '_base_nor': _base_nor}
def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir):
# Scale position data between [0, 1] to bake to image
@ -1387,21 +1458,19 @@ class LeenkxExporter:
def write_output_image(self, data, extra_x, img_size, name, output_dir):
# Pad data with zeros to make up for required number of pixels of 2^n format
data = np.pad(data, ((0, 0), (0, extra_x), (0, 0)), 'minimum')
pixel_list = []
data = np.pad(data, ((0, 0), (0, int(extra_x)), (0, 0)), 'minimum')
for y in range(len(data)):
for x in range(len(data[0])):
# assign RGBA
pixel_list.append(data[y, x, 0])
pixel_list.append(data[y, x, 1])
pixel_list.append(data[y, x, 2])
pixel_list.append(1.0)
total_pixels = data.shape[0] * data.shape[1]
rgba = np.ones((total_pixels, 4), dtype=np.float32)
rgba[:, :3] = data.reshape(-1, 3)
pixel_list = (pixel_list + [0] * (img_size * img_size * 4 - len(pixel_list)))
img_total = img_size * img_size
if total_pixels < img_total:
padding = np.zeros((img_total - total_pixels, 4), dtype=np.float32)
rgba = np.concatenate((rgba, padding), axis=0)
image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True)
image.pixels = pixel_list
image.pixels = rgba.ravel().tolist()
output_path = os.path.join(output_dir, name + ".png")
image.save_render(output_path, scene= bpy.context.scene)
bpy.data.images.remove(image)
@ -1579,30 +1648,28 @@ class LeenkxExporter:
# Scale for packed coords
lay0 = uv_layers[t0map]
maxdim_uvlayer = lay0
for v in lay0.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
_uv0_raw = np.empty(len(lay0.data) * 2, dtype='<f4')
lay0.data.foreach_get('uv', _uv0_raw)
_uv0_absmax = float(np.abs(_uv0_raw).max()) if len(_uv0_raw) > 0 else 0.0
if _uv0_absmax > maxdim:
maxdim = _uv0_absmax
if has_tex1:
lay1 = uv_layers[t1map]
for v in lay1.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
maxdim_uvlayer = lay1
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
maxdim_uvlayer = lay1
_uv1_raw = np.empty(len(lay1.data) * 2, dtype='<f4')
lay1.data.foreach_get('uv', _uv1_raw)
_uv1_absmax = float(np.abs(_uv1_raw).max()) if len(_uv1_raw) > 0 else 0.0
if _uv1_absmax > maxdim:
maxdim = _uv1_absmax
maxdim_uvlayer = lay1
if has_morph_target:
morph_data = np.empty(num_verts * 2, dtype='<f4')
lay2 = uv_layers[morph_uv_index]
for v in lay2.data:
if abs(v.uv[0]) > maxdim:
maxdim = abs(v.uv[0])
maxdim_uvlayer = lay2
if abs(v.uv[1]) > maxdim:
maxdim = abs(v.uv[1])
maxdim_uvlayer = lay2
_uv2_raw = np.empty(len(lay2.data) * 2, dtype='<f4')
lay2.data.foreach_get('uv', _uv2_raw)
_uv2_absmax = float(np.abs(_uv2_raw).max()) if len(_uv2_raw) > 0 else 0.0
if _uv2_absmax > maxdim:
maxdim = _uv2_absmax
maxdim_uvlayer = lay2
if maxdim > 1:
o['scale_tex'] = maxdim
invscale_tex = (1 / o['scale_tex']) * 32767
@ -1636,102 +1703,87 @@ class LeenkxExporter:
invscale_pos = (1 / scale_pos) * 32767
verts = export_mesh.vertices
_all_co = np.empty(len(verts) * 3, dtype='<f4')
verts.foreach_get('co', _all_co)
_all_co = _all_co.reshape(-1, 3)
_loop_vidx = np.empty(num_verts, dtype='<i4')
loops.foreach_get('vertex_index', _loop_vidx)
_loop_co = _all_co[_loop_vidx]
_all_normals = np.empty(num_verts * 3, dtype='<f4')
loops.foreach_get('normal', _all_normals)
_all_normals = _all_normals.reshape(-1, 3)
pdata[0::4] = _loop_co[:, 0]
pdata[1::4] = _loop_co[:, 1]
pdata[2::4] = _loop_co[:, 2]
pdata[3::4] = _all_normals[:, 2] * scale_pos
ndata[0::2] = _all_normals[:, 0]
ndata[1::2] = _all_normals[:, 1]
if has_tex:
lay0 = export_mesh.uv_layers[t0map]
lay0.data.foreach_get('uv', t0data)
t0data[1::2] = 1.0 - t0data[1::2]
if has_tex1:
lay1 = export_mesh.uv_layers[t1map]
lay1.data.foreach_get('uv', t1data)
t1data[1::2] = 1.0 - t1data[1::2]
if has_tang:
loops.foreach_get('tangent', tangdata)
if has_morph_target:
lay2 = export_mesh.uv_layers[morph_uv_index]
lay2.data.foreach_get('uv', morph_data)
morph_data[1::2] = 1.0 - morph_data[1::2]
if has_col:
vcol0 = self.get_nth_vertex_colors(export_mesh, 0).data
loop: bpy.types.MeshLoop
for i, loop in enumerate(loops):
v = verts[loop.vertex_index]
co = v.co
normal = loop.normal
tang = loop.tangent
i4 = i * 4
i2 = i * 2
pdata[i4 ] = co[0]
pdata[i4 + 1] = co[1]
pdata[i4 + 2] = co[2]
pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale
ndata[i2 ] = normal[0]
ndata[i2 + 1] = normal[1]
if has_tex:
uv = lay0.data[loop.index].uv
t0data[i2 ] = uv[0]
t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y
if has_tex1:
uv = lay1.data[loop.index].uv
t1data[i2 ] = uv[0]
t1data[i2 + 1] = 1.0 - uv[1]
if has_tang:
i3 = i * 3
tangdata[i3 ] = tang[0]
tangdata[i3 + 1] = tang[1]
tangdata[i3 + 2] = tang[2]
if has_morph_target:
uv = lay2.data[loop.index].uv
morph_data[i2 ] = uv[0]
morph_data[i2 + 1] = 1.0 - uv[1]
if has_col:
col = vcol0[loop.index].color
i3 = i * 3
cdata[i3 ] = col[0]
cdata[i3 + 1] = col[1]
cdata[i3 + 2] = col[2]
_col_raw = np.empty(len(vcol0) * 4, dtype='<f4')
vcol0.foreach_get('color', _col_raw)
_col_raw = _col_raw.reshape(-1, 4)
cdata[0::3] = _col_raw[:, 0]
cdata[1::3] = _col_raw[:, 1]
cdata[2::3] = _col_raw[:, 2]
mats = export_mesh.materials
poly_map = []
for i in range(max(len(mats), 1)):
poly_map.append([])
for poly in export_mesh.polygons:
poly_map[poly.material_index].append(poly)
o['index_arrays'] = []
# map polygon indices to triangle loops
tri_loops = {}
for loop in export_mesh.loop_triangles:
if loop.polygon_index not in tri_loops:
tri_loops[loop.polygon_index] = []
tri_loops[loop.polygon_index].append(loop)
num_tris = len(export_mesh.loop_triangles)
if num_tris > 0:
_tri_loops = np.empty(num_tris * 3, dtype='<i4')
export_mesh.loop_triangles.foreach_get('loops', _tri_loops)
for index, polys in enumerate(poly_map):
tris = 0
for poly in polys:
tris += poly.loop_total - 2
if tris == 0: # No face assigned
continue
prim = np.empty(tris * 3, dtype='<i4')
v_map = np.empty(tris * 3, dtype='<i4')
_tri_polys = np.empty(num_tris, dtype='<i4')
export_mesh.loop_triangles.foreach_get('polygon_index', _tri_polys)
i = 0
for poly in polys:
for loop in tri_loops[poly.index]:
prim[i ] = loops[loop.loops[0]].index
prim[i + 1] = loops[loop.loops[1]].index
prim[i + 2] = loops[loop.loops[2]].index
i += 3
num_polys = len(export_mesh.polygons)
_poly_mat_idx = np.empty(num_polys, dtype='<i4')
export_mesh.polygons.foreach_get('material_index', _poly_mat_idx)
j = 0
for poly in polys:
for loop in tri_loops[poly.index]:
v_map[j ] = loops[loop.loops[0]].vertex_index
v_map[j + 1] = loops[loop.loops[1]].vertex_index
v_map[j + 2] = loops[loop.loops[2]].vertex_index
j += 3
_tri_mat_idx = _poly_mat_idx[_tri_polys]
ia = {'values': prim, 'material': 0, 'vertex_map': v_map}
if len(mats) > 1:
for i in range(len(mats)): # Multi-mat mesh
if mats[i] == mats[index]: # Default material for empty slots
ia['material'] = i
break
o['index_arrays'].append(ia)
_tri_loops_2d = _tri_loops.reshape(-1, 3)
for mat_idx in range(max(len(mats), 1)):
mask = (_tri_mat_idx == mat_idx)
num_mat_tris = int(np.count_nonzero(mask))
if num_mat_tris == 0:
continue
mat_tri_loops = _tri_loops_2d[mask].flatten().astype('<i4')
prim = mat_tri_loops.copy()
v_map = _loop_vidx[mat_tri_loops].astype('<i4')
ia = {'values': prim, 'material': 0, 'vertex_map': v_map}
if len(mats) > 1:
for i in range(len(mats)): # Multi-mat mesh
if mats[i] == mats[mat_idx]: # Default material for empty slots
ia['material'] = i
break
o['index_arrays'].append(ia)
# Pack
pdata *= invscale_pos
@ -1821,7 +1873,6 @@ class LeenkxExporter:
def export_mesh(self, object_ref):
"""Exports a single mesh object."""
# profile_time = time.time()
table = object_ref[1]["objectTable"]
bobject = table[0]
oid = lnx.utils.safestr(object_ref[1]["structName"])
@ -1921,7 +1972,6 @@ class LeenkxExporter:
out_mesh['dynamic_usage'] = bobject.data.lnx_dynamic_usage
self.write_mesh(bobject, fp, out_mesh)
# print('Mesh exported in ' + str(time.time() - profile_time))
if hasattr(bobject, 'evaluated_get'):
bobject_eval.to_mesh_clear()
@ -2680,13 +2730,12 @@ class LeenkxExporter:
}
# Set viewport camera projection
if is_viewport_camera:
proj, is_persp = self.get_viewport_projection_matrix()
if proj is not None:
if is_persp:
self.extract_projection(out_camera, proj, with_planes=False)
else:
self.extract_ortho(out_camera, proj)
proj, is_persp = self.get_viewport_projection_matrix()
if proj is not None:
if is_persp:
self.extract_projection(out_camera, proj, with_planes=False)
else:
self.extract_ortho(out_camera, proj)
self.output['camera_datas'].append(out_camera)
out_object = {