Skip to content

Commit 6a461df

Browse files
authored
Improve mesh remeshing to preserve coordinate system and color information (#602)
- Detect and preserve original COLLADA coordinate system (Y_UP/Z_UP) by reading up_axis from input DAE file and applying appropriate transforms during export - Extract color information from both vertex colors and materials, enabling proper color preservation across multiple remesh operations - Improve color quantization from 0.02 to 0.01 steps for better color accuracy - Add debug logging for color collection and material creation This ensures that remeshed meshes maintain the same orientation and visual appearance as the original, even when remeshing is applied multiple times consecutively.
1 parent 901aa9e commit 6a461df

File tree

1 file changed

+103
-12
lines changed

1 file changed

+103
-12
lines changed

skrobot/utils/_blender_remesh_core.py

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,32 @@ def remesh_and_bake_file(input_path, output_path, voxel_size=0.002, export_forma
3131
Export format ('DAE' or 'STL'). Default is 'DAE'.
3232
"""
3333
import math
34+
import xml.etree.ElementTree as ET
3435

3536
input_path = Path(input_path)
3637
output_path = Path(output_path)
3738

39+
# Detect original coordinate system from DAE file
40+
original_up_axis = 'Y_UP' # default
41+
if export_format.upper() == 'DAE' and input_path.suffix.lower() == '.dae':
42+
try:
43+
tree = ET.parse(input_path)
44+
root = tree.getroot()
45+
# Find up_axis element (handle namespace)
46+
ns = {'collada': 'http://www.collada.org/2005/11/COLLADASchema'}
47+
up_axis_elem = root.find('.//collada:up_axis', ns)
48+
if up_axis_elem is None:
49+
up_axis_elem = root.find('.//up_axis')
50+
if up_axis_elem is not None and up_axis_elem.text:
51+
original_up_axis = up_axis_elem.text.strip()
52+
print(f"Detected original coordinate system: {original_up_axis}")
53+
except Exception as e:
54+
print(f"Warning: Could not detect coordinate system: {e}")
55+
3856
clear_scene()
3957

40-
# Import with fix_orientation to maintain Blender's Z-up coordinate system
58+
# Import with fix_orientation to ensure proper handling in Blender
59+
# Blender uses Z-up, so this converts Y-up to Z-up properly
4160
bpy.ops.wm.collada_import(
4261
filepath=str(input_path),
4362
fix_orientation=True,
@@ -53,31 +72,73 @@ def remesh_and_bake_file(input_path, output_path, voxel_size=0.002, export_forma
5372

5473
# Build a map of face center positions to colors
5574
face_color_data = []
75+
objects_with_vertex_colors = 0
76+
objects_with_materials = 0
5677

5778
for obj in source_objects:
5879
world_matrix = obj.matrix_world
5980

60-
# Get vertex colors if available
61-
if obj.data.vertex_colors:
81+
# Try to get colors from vertex colors first, then from materials
82+
has_vertex_colors = obj.data.vertex_colors and len(obj.data.vertex_colors) > 0
83+
84+
if has_vertex_colors:
85+
objects_with_vertex_colors += 1
6286
vc = obj.data.vertex_colors.active.data
6387

6488
for poly in obj.data.polygons:
65-
# Get average color for this face
89+
# Get average color for this face from vertex colors
6690
color_sum = [0.0, 0.0, 0.0, 0.0]
6791
for loop_index in poly.loop_indices:
6892
for i in range(4):
6993
color_sum[i] += vc[loop_index].color[i]
70-
7194
avg_color = tuple(c / len(poly.loop_indices) for c in color_sum)
95+
face_color = avg_color[:3]
96+
97+
# Get face center in world space
98+
face_center = world_matrix @ poly.center
99+
100+
face_color_data.append({
101+
'center': face_center.copy(),
102+
'color': face_color
103+
})
104+
elif obj.data.materials:
105+
objects_with_materials += 1
106+
107+
for poly in obj.data.polygons:
108+
# Get color from material
109+
face_color = (0.5, 0.5, 0.5) # default fallback
110+
111+
if poly.material_index < len(obj.data.materials):
112+
mat = obj.data.materials[poly.material_index]
113+
if mat and mat.use_nodes:
114+
# Get base color from Principled BSDF
115+
bsdf = mat.node_tree.nodes.get("Principled BSDF")
116+
if bsdf and 'Base Color' in bsdf.inputs:
117+
base_color = bsdf.inputs['Base Color'].default_value
118+
face_color = (base_color[0], base_color[1], base_color[2])
72119

73120
# Get face center in world space
74121
face_center = world_matrix @ poly.center
75122

76123
face_color_data.append({
77124
'center': face_center.copy(),
78-
'color': avg_color[:3]
125+
'color': face_color
79126
})
80127

128+
msg = f"Color collection: {objects_with_vertex_colors} objects with vertex colors"
129+
msg += f'{objects_with_materials} with materials'
130+
msg += f"Collected {len(face_color_data)} face color samples"
131+
print(msg)
132+
133+
# Print color statistics
134+
if face_color_data:
135+
unique_colors = set(data['color'] for data in face_color_data)
136+
print(f"Unique colors in source: {len(unique_colors)}")
137+
# Sample some colors
138+
sample_colors = list(unique_colors)[:5]
139+
for i, c in enumerate(sample_colors):
140+
print(f" Sample {i + 1}: ({c[0]:.3f}, {c[1]:.3f}, {c[2]:.3f})")
141+
81142
# --- 2. JOIN ALL OBJECTS ---
82143
bpy.ops.object.select_all(action='DESELECT')
83144
for obj in source_objects:
@@ -121,7 +182,8 @@ def remesh_and_bake_file(input_path, output_path, voxel_size=0.002, export_forma
121182
nearest_color = face_color_data[nearest_index]['color']
122183

123184
# Round color to reduce number of materials
124-
rounded_color = tuple(round(c * 50) / 50 for c in nearest_color)
185+
# Use finer quantization (0.01 steps) to preserve more color detail
186+
rounded_color = tuple(round(c * 100) / 100 for c in nearest_color)
125187

126188
# Create material if it doesn't exist
127189
if rounded_color not in color_to_material:
@@ -137,18 +199,28 @@ def remesh_and_bake_file(input_path, output_path, voxel_size=0.002, export_forma
137199
# Assign material to face
138200
poly.material_index = color_to_material[rounded_color]
139201

202+
print(f"Created {len(color_to_material)} materials from {len(face_color_data)} source colors")
203+
print("Material color samples:")
204+
for i, (color, mat_idx) in enumerate(list(color_to_material.items())[:5]):
205+
print(f" Material {mat_idx}: ({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})")
206+
140207
# Apply smooth shading
141208
if joined_obj.data.polygons:
142209
joined_obj.data.polygons.foreach_set('use_smooth', [True] * len(joined_obj.data.polygons))
143210

144211
# --- 5. EXPORT ---
145-
# Convert from Blender Z-up to Collada Y-up by rotating X-axis -90 degrees
146-
joined_obj.rotation_euler[0] -= math.pi / 2
147-
148-
# Apply transformation to vertices
212+
# Convert back to original coordinate system if needed
213+
if export_format.upper() == 'DAE' and original_up_axis == 'Y_UP':
214+
# Convert from Blender Z-up back to Y-up
215+
# Rotate -90 degrees around X axis
216+
joined_obj.rotation_euler[0] -= math.pi / 2
217+
bpy.context.view_layer.objects.active = joined_obj
218+
joined_obj.select_set(True)
219+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
220+
221+
# Ensure object is selected for export
149222
bpy.context.view_layer.objects.active = joined_obj
150223
joined_obj.select_set(True)
151-
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
152224

153225
if export_format.upper() == 'DAE':
154226
bpy.ops.wm.collada_export(
@@ -162,6 +234,25 @@ def remesh_and_bake_file(input_path, output_path, voxel_size=0.002, export_forma
162234
use_object_instantiation=False,
163235
sort_by_name=False
164236
)
237+
238+
# Fix coordinate system in exported DAE file to match original
239+
# Use text replacement to preserve all XML formatting and content
240+
try:
241+
with open(output_path, 'r', encoding='utf-8') as f:
242+
content = f.read()
243+
244+
# Replace Z_UP with original coordinate system
245+
content = content.replace('<up_axis>Z_UP</up_axis>', f'<up_axis>{original_up_axis}</up_axis>')
246+
# Also handle namespace prefix variations
247+
content = content.replace(':up_axis>Z_UP</', f':up_axis>{original_up_axis}</')
248+
249+
with open(output_path, 'w', encoding='utf-8') as f:
250+
f.write(content)
251+
252+
print(f"Restored coordinate system to: {original_up_axis}")
253+
except Exception as e:
254+
print(f"Warning: Could not fix coordinate system in output: {e}")
255+
165256
elif export_format.upper() == 'STL':
166257
bpy.ops.export_mesh.stl(
167258
filepath=str(output_path),

0 commit comments

Comments
 (0)