import blenderproc as bproc import os import bpy import xml.etree.ElementTree as ET import json import argparse from mathutils import Matrix, Vector from pathlib import Path def bpy_cleanup_mesh(obj): assert obj.type == 'MESH' # remove duplicate vertices bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.remove_doubles(threshold=1e-06) bpy.ops.object.mode_set(mode='OBJECT') # disable auto-smoothing # obj.data.use_auto_smooth = False # split edges with an angle above 70 degrees (1.22 radians) m = obj.modifiers.new("EdgeSplit", "EDGE_SPLIT") m.split_angle = 1.22173 bpy.ops.object.modifier_apply(modifier="EdgeSplit") # move every face an epsilon in the direction of its normal, to reduce clipping artifacts m = obj.modifiers.new("Displace", "DISPLACE") m.strength = 0.00001 bpy.ops.object.modifier_apply(modifier="Displace") def parse_origin(element): """Parse the tag to extract translation and rotation.""" translation = [0.0, 0.0, 0.0] rotation = [0.0, 0.0, 0.0] if element is not None: if 'xyz' in element.attrib: translation = [float(x) for x in element.attrib['xyz'].split()] if 'rpy' in element.attrib: rotation = [float(r) for r in element.attrib['rpy'].split()] return translation, rotation def read_joints_and_meshes(urdf_path): """Parse the URDF file to extract joint and mesh information.""" joints = {} tree = ET.parse(urdf_path) root = tree.getroot() link_to_meshes = {} for link in root.findall('link'): link_name = link.get('name') visuals = link.findall('visual') mesh_data = [] for visual in visuals: geometry = visual.find('geometry') origin = visual.find('origin') translation, rotation = parse_origin(origin) if geometry is not None: mesh = geometry.find('mesh') if mesh is not None: mesh_path = mesh.get('filename') if mesh_path: mesh_data.append({ 'path': mesh_path, 'translation': translation, 'rotation': rotation }) if mesh_data: link_to_meshes[link_name] = mesh_data continuous_info = [] for joint in root.findall('joint'): joint_name = joint.get('name') joint_info = { 'type': joint.get('type'), 'axis': None, 'limit': None, 'child': [], 'parent': None, 'origin_translation': [0.0, 0.0, 0.0], 'origin_rotation': [0.0, 0.0, 0.0], 'mesh_data': [] } origin = joint.find('origin') joint_info['origin_translation'], joint_info['origin_rotation'] = parse_origin(origin) axis = joint.find('axis') if axis is not None: joint_info['axis'] = [float(i) if i != 'None' else 0.0 for i in axis.get('xyz').split()] limit = joint.find('limit') if limit is not None: joint_info['limit'] = { 'lower': float(limit.get('lower', 0)), 'upper': float(limit.get('upper', 0)) } parent = joint.find('parent') if parent is not None: joint_info['parent'] = parent.get('link') # TODO: Find all childs child = joint.find('child') parent = joint.find('parent') if child is not None: child_link = child.get('link') joint_info['child'].append(child_link) for child_name in joint_info['child']: joint_info['mesh_data'] += link_to_meshes.get(child_name, []) joints[joint_name] = joint_info return joints def remove_materials_without_textures(): """Remove materials without texture maps and delete the corresponding faces.""" for obj in bpy.data.objects: if obj.type == 'MESH': bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') # Get materials without textures materials_to_remove = [] check_materials = False for i, material in enumerate(obj.data.materials): if material and has_texture_map(material): check_materials = True if check_materials: for i, material in enumerate(obj.data.materials): if not material or not has_texture_map(material): materials_to_remove.append(i) # Remove faces using those materials bpy.ops.mesh.select_all(action='DESELECT') for mat_index in materials_to_remove: obj.active_material_index = mat_index bpy.ops.object.material_slot_select() bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') # Remove unused material slots for mat_index in sorted(materials_to_remove, reverse=True): obj.active_material_index = mat_index bpy.ops.object.material_slot_remove() for obj in bpy.data.objects: if obj.type == 'MESH': # Check geometry and normals bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') # Check and assign materials for face in obj.data.polygons: if face.material_index >= len(obj.material_slots): print(f"Face {face.index} has no valid material.") face.material_index = 0 # Assign default material # Check UV mapping if not obj.data.uv_layers: print("UV mapping missing. Generating UVs...") bpy.ops.object.mode_set(mode='EDIT') bpy.ops.uv.smart_project() bpy.ops.object.mode_set(mode='OBJECT') def has_texture_map(material): """Check if a material has a texture map (image texture).""" if material.use_nodes: for node in material.node_tree.nodes: if node.type == 'TEX_IMAGE' and node.image: return True return False def normalize_scene(center, scale): bpy.ops.transform.translate(value=center) bpy.ops.object.transform_apply(location=True, rotation=False, scale=False) bpy.ops.object.select_all(action='SELECT') bpy.ops.transform.resize(value=(scale, scale, scale)) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) def process_state(joints, mesh_dir, state_idx, num_states, joint_type, joint_list): max_bounds = [-float('inf')] * 3 min_bounds = [float('inf')] * 3 objs = [] """Process a single state, applying transformations to each mesh.""" for joint_name, joint_info in joints.items(): for mesh_entry in joint_info['mesh_data']: mesh_path = os.path.join(mesh_dir, mesh_entry['path']) if not os.path.exists(mesh_path): print(f"Mesh not found: {mesh_path}") continue objs += bproc.loader.load_obj(mesh_path) for obj in objs: obj_b = obj.blender_obj bpy_cleanup_mesh(obj_b) for obj in objs: obj_b = obj.blender_obj for i in range(3): max_bounds[i] = max(max_bounds[i], obj_b.bound_box[0][i]) min_bounds[i] = min(min_bounds[i], obj_b.bound_box[0][i]) for mat in bpy.data.materials: mat.use_backface_culling = True # Calculate joint-specific transformation for joint_name, joint_info in joints.items(): if joint_name == joint_list[0]: if joint_type == 'revolute': lower = joint_info['limit']['lower'] if joint_info['limit'] else -1.57079 upper = joint_info['limit']['upper'] if joint_info['limit'] else 1.57079 angle = lower + (upper - lower) * (state_idx / (num_states - 1)) origin_translation = [joint_info['origin_translation'][i] for i in range(3)] origin_translation = Matrix.Translation(origin_translation) joint_rotation = Matrix.Rotation(angle, 4, joint_info['axis']) joint_transformation = origin_translation @ joint_rotation @ origin_translation.inverted() elif joint_type == 'prismatic': lower = joint_info['limit']['lower'] if joint_info['limit'] else 0 upper = joint_info['limit']['upper'] if joint_info['limit'] else 0.5 translation = lower + (upper - lower) * (state_idx / (num_states - 1)) origin_translation = [joint_info['origin_translation'][i] for i in range(3)] origin_translation = Matrix.Translation(origin_translation) joint_translation = Matrix.Translation([joint_info['axis'][i] * translation for i in range(3)]) joint_transformation = origin_translation @ joint_translation @ origin_translation.inverted() # Apply joint-specific transformation obj_idx = 0 for joint_name, joint_info in joints.items(): for mesh_entry in joint_info['mesh_data']: mesh_path = os.path.join(mesh_dir, mesh_entry['path']) if not os.path.exists(mesh_path): print(f"Mesh not found: {mesh_path}") continue if joint_name in joint_list: objs[obj_idx].blender_obj.matrix_world @= joint_transformation obj_idx += 1 def export_state(output_path): """Export the current Blender scene as a GLB file.""" bpy.ops.export_scene.gltf(filepath=output_path, export_format='GLB', use_selection=False) print(f"Exported: {output_path}") def process_states(mesh_dir, urdf_path, output_dir, num_states=6, joint_type="revolute", joint_list=[], joint_idx=0): """Process all states and export them as GLB files.""" Path(output_dir).mkdir(parents=True, exist_ok=True) joints = read_joints_and_meshes(urdf_path) for state_idx in range(num_states): # Clear scene bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) # Process and export the state process_state(joints, mesh_dir, state_idx, num_states, joint_type, joint_list) export_state(f'{output_dir}/{state_idx:02d}.glb') if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--data_list', type=str, default='configs/partnet.json') parser.add_argument('--input_dir', type=str, default='datasets/PartNet_raw') parser.add_argument('--output_dir', type=str, default='datasets/PartNet') parser.add_argument('--start_idx', type=int, default=0) parser.add_argument('--end_idx', type=int, default=1) parser.add_argument('--num_states', type=int, default=6) args = parser.parse_args() with open(args.data_list) as f: data_info = json.load(f) model_ids = data_info['total_obj_ids'] for model_id in model_ids[args.start_idx:min(args.end_idx, len(model_ids))]: mesh_dir = f"{args.input_dir}/{model_id}" urdf_path = f"{args.output_dir}/{model_id}/mobility.urdf" joints_info_path = f"{args.output_dir}/{model_id}/joints.json" output_dir = f"{args.output_dir}/{model_id}/gt_mesh" joint_info_output_path = f"{args.output_dir}/{model_id}/joint_info.json" with open(joints_info_path, "r") as f: joints_info = json.load(f) joint_type = "revolute" if model_id in data_info['revolute']['obj_ids'] else "prismatic" joints = read_joints_and_meshes(urdf_path) target_joint = joints_info['joints'][0] # Default to only choose the first joint as the target joint # Output joint info for evaluation joint_info = [] for joint_name, joint_cfg in joints.items(): if joint_name == target_joint: current_joint_info = {} current_joint_info['type'] = joint_type current_joint_info['range'] = [joint_cfg['limit']['lower'], joint_cfg['limit']['upper']] current_joint_info['axis'] = {} current_joint_info['axis']['origin'] = joint_cfg['origin_translation'] current_joint_info['axis']['direction'] = joint_cfg['axis'] joint_info.append(current_joint_info) with open(joint_info_output_path, "w") as f: json.dump(joint_info, f, indent=4) print(f"Processing {model_id} with joint type {joint_type}") joint_list = [target_joint] joint_list += joints_info['descendants'][target_joint] process_states(mesh_dir, urdf_path, output_dir, args.num_states, joint_type, joint_list, 0)