From 184ac7df8823dc6c188bd9185386c58aeb5e2bb0 Mon Sep 17 00:00:00 2001 From: brothermechanic Date: Tue, 10 Oct 2023 16:32:29 +0000 Subject: [PATCH] Tool for convert FreeCAD sub-assemblies to URDF-world according to config --- asp/mocks/urdf/asm.urdf | 4 + asp/mocks/urdf/joint.urdf | 13 +++ asp/mocks/urdf/link.urdf | 31 ++++++ cg/blender/export/__init__.py | 55 ++++++++++- cg/blender/export/dae.py | 78 ++++++++++----- cg/blender/export/stl.py | 55 ++++++----- cg/blender/import_cad/build_blender_scene.py | 35 ++++--- cg/blender/processing/highpoly_setup.py | 13 +-- cg/blender/processing/lowpoly_setup.py | 4 +- .../processing/restruct_hierarchy_by_lcs.py | 50 ++++++---- cg/blender/processing/uv_setup.py | 9 +- cg/pipeline/cg_pipeline.py | 99 ++++++++++++++----- 12 files changed, 327 insertions(+), 119 deletions(-) create mode 100644 asp/mocks/urdf/asm.urdf create mode 100644 asp/mocks/urdf/joint.urdf create mode 100644 asp/mocks/urdf/link.urdf diff --git a/asp/mocks/urdf/asm.urdf b/asp/mocks/urdf/asm.urdf new file mode 100644 index 0000000..b8d901a --- /dev/null +++ b/asp/mocks/urdf/asm.urdf @@ -0,0 +1,4 @@ + + + diff --git a/asp/mocks/urdf/joint.urdf b/asp/mocks/urdf/joint.urdf new file mode 100644 index 0000000..611aa12 --- /dev/null +++ b/asp/mocks/urdf/joint.urdf @@ -0,0 +1,13 @@ + + + + + + diff --git a/asp/mocks/urdf/link.urdf b/asp/mocks/urdf/link.urdf new file mode 100644 index 0000000..b20131f --- /dev/null +++ b/asp/mocks/urdf/link.urdf @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/cg/blender/export/__init__.py b/cg/blender/export/__init__.py index a3bda60..c5c9e01 100644 --- a/cg/blender/export/__init__.py +++ b/cg/blender/export/__init__.py @@ -1,7 +1,52 @@ # -*- coding: utf-8 -*- -""" +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' DESCRIPTION. -Blender export modules. -Modules exports all objests in scene. -You can set export path and subdir. -""" +Decorator for export functions. +''' +import logging +import os +import bpy +import mathutils + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def export_decorator(func): + + def wrapper(**kwargs): + # add defaults + kwargs.setdefault('path', '//') + kwargs.setdefault('subdir', '') + + obj = bpy.data.objects.get(kwargs['obj_name']) + # deselect all but just one object and make it active + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(state=True) + bpy.context.view_layer.objects.active = obj + # clean hierarchy and transforms + obj.parent = None + obj.matrix_world = mathutils.Matrix() + # construct path + filename = bpy.context.active_object.name + filepath = os.path.join(kwargs['path'], + kwargs['subdir']).replace('\\', '/') + if not os.path.isdir(filepath): + os.makedirs(filepath) + # store path + kwargs['outpath'] = os.path.join(filepath, filename) + # return export function + return func(**kwargs) + + return wrapper diff --git a/cg/blender/export/dae.py b/cg/blender/export/dae.py index b85a2b0..42d2a95 100644 --- a/cg/blender/export/dae.py +++ b/cg/blender/export/dae.py @@ -1,34 +1,64 @@ # -*- coding: utf-8 -*- -""" +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' DESCRIPTION. Collada mesh exporter. Exports all objects in scene. You can set export path and subdir. -""" -__version__ = "0.1" +''' +__version__ = "0.2" -import logging -import sys import bpy -import os - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +from blender.export import export_decorator -def export_dae(path, subdir=""): - """ Collada mesh exporter. Exports all objects in scene. """ - for ob in bpy.context.scene.objects: - # deselect all but just one object and make it active - bpy.ops.object.select_all(action='DESELECT') - ob.select_set(state=True) - bpy.context.view_layer.objects.active = ob - filename = bpy.context.active_object.name - # export dae - dae_path = os.path.join(path, subdir).replace('\\', '/') - if not os.path.isdir(dae_path): - os.makedirs(dae_path) - outpath = os.path.join(dae_path, filename) - logger.debug('vizual:', outpath) +@export_decorator +def export_dae(**kwargs): + outpath = ('{}.dae'.format(kwargs['outpath'])) - bpy.ops.wm.collada_export(filepath=outpath, check_existing=False, apply_modifiers=True, export_mesh_type=0, export_mesh_type_selection='view', export_global_forward_selection='Y', export_global_up_selection='Z', apply_global_orientation=False, selected=True, include_children=False, include_armatures=False, include_shapekeys=False, deform_bones_only=False, include_animations=False, include_all_actions=True, export_animation_type_selection='sample', sampling_rate=1, keep_smooth_curves=False, keep_keyframes=False, keep_flat_curves=False, active_uv_only=False, use_texture_copies=True, triangulate=True, use_object_instantiation=True, use_blender_profile=True, sort_by_name=False, export_object_transformation_type=0, export_object_transformation_type_selection='matrix', export_animation_transformation_type=0, export_animation_transformation_type_selection='matrix', open_sim=False, limit_precision=False, keep_bind_info=False) + bpy.ops.wm.collada_export( + filepath=outpath, + check_existing=False, + apply_modifiers=True, + export_mesh_type=0, + export_mesh_type_selection='view', + export_global_forward_selection='Y', + export_global_up_selection='Z', + apply_global_orientation=False, + selected=True, + include_children=False, + include_armatures=False, + include_shapekeys=False, + deform_bones_only=False, + include_animations=False, + include_all_actions=True, + export_animation_type_selection='sample', + sampling_rate=1, + keep_smooth_curves=False, + keep_keyframes=False, + keep_flat_curves=False, + active_uv_only=False, + use_texture_copies=True, + triangulate=True, + use_object_instantiation=True, + use_blender_profile=True, + sort_by_name=False, + export_object_transformation_type=0, + export_object_transformation_type_selection='matrix', + export_animation_transformation_type=0, + export_animation_transformation_type_selection='matrix', + open_sim=False, + limit_precision=False, + keep_bind_info=False) + + return outpath diff --git a/cg/blender/export/stl.py b/cg/blender/export/stl.py index fd5d5cd..9b3d916 100644 --- a/cg/blender/export/stl.py +++ b/cg/blender/export/stl.py @@ -1,34 +1,41 @@ # -*- coding: utf-8 -*- -""" +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' DESCRIPTION. STL mesh exporter. Exports all objects in scene. You can set export path and subdir. -""" -__version__ = "0.1" +''' +__version__ = "0.2" -import logging -import sys import bpy -import os - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +from blender.export import export_decorator -def export_stl(path, subdir=""): - """ STL mesh exporter. Exports all objects in scene. """ - for ob in bpy.context.scene.objects: - # deselect all but just one object and make it active - bpy.ops.object.select_all(action='DESELECT') - ob.select_set(state=True) - bpy.context.view_layer.objects.active = ob - filename = bpy.context.active_object.name - # export stl - stl_path = os.path.join(path, subdir).replace('\\', '/') - if not os.path.isdir(stl_path): - os.makedirs(stl_path) - outpath = os.path.join(stl_path, filename+'.stl') - logger.debug('collision:', outpath) +@export_decorator +def export_stl(**kwargs): + outpath = ('{}.stl'.format(kwargs['outpath'])) - bpy.ops.export_mesh.stl(filepath=outpath, check_existing=False, filter_glob='*.stl', use_selection=True, global_scale=1.0, use_scene_unit=False, ascii=False, use_mesh_modifiers=True, batch_mode='OFF', axis_forward='Y', axis_up='Z') + bpy.ops.export_mesh.stl(filepath=outpath, + check_existing=False, + filter_glob='*.stl', + use_selection=True, + global_scale=1000, + use_scene_unit=False, + ascii=False, + use_mesh_modifiers=True, + batch_mode='OFF', + axis_forward='Y', + axis_up='Z') + + return outpath diff --git a/cg/blender/import_cad/build_blender_scene.py b/cg/blender/import_cad/build_blender_scene.py index db060db..f8ee0cc 100644 --- a/cg/blender/import_cad/build_blender_scene.py +++ b/cg/blender/import_cad/build_blender_scene.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre # Copyright (C) 2023 Ilia Kurochkin # # This program is free software; you can redistribute it and/or modify @@ -21,6 +20,7 @@ DESCRIPTION. ''' __version__ = '0.1' +import collections import logging import random import bpy @@ -66,8 +66,7 @@ def json_to_blend(js_data): fc_file = list(js_data.keys())[0] - bobjs = [] - bobjs_for_render = [] + imported_objects = collections.defaultdict(list) for js_obj in js_data[fc_file]: bobj = None @@ -78,6 +77,7 @@ def json_to_blend(js_data): bobj.empty_display_size = round(random.uniform(0.05, 0.15), 3) bobj.show_in_front = True lcs_collection.objects.link(bobj) + imported_objects['objs_lcs'].append(bobj.name) elif js_data[fc_file][js_obj]['type'] == 'PART': if js_data[fc_file][js_obj].get('mesh'): @@ -107,24 +107,26 @@ def json_to_blend(js_data): scene_scale) for hierarchy_obj in hierarchy_objs: hierarchy_collection.objects.link(hierarchy_obj) + imported_objects['objs_hierarchy'].append(hierarchy_obj.name) # one material for the whole object if bobj.type == 'MESH': if js_data[fc_file][js_obj].get('material'): fem_mat = js_data[fc_file][js_obj]['material'] assign_materials(bobj, fem_mat) - bobjs_for_render.append(bobj) + imported_objects['objs_foreground'].append(bobj.name) else: assign_black(bobj) - - bobjs.append(bobj) + imported_objects['objs_background'].append(bobj.name) # losted root lcs inlet workaround - lcs_objects = lcs_collection.objects - if lcs_objects: - root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)] + if imported_objects['objs_lcs']: + root_lcs = None + for obj_name in imported_objects['objs_lcs']: + if obj_name.endswith(root): + root_lcs = bpy.data.objects[obj_name] + break if root_lcs: - root_lcs = root_lcs[0] root_inlet_name = '{}{}'.format(root_lcs.name.split(root)[0], inlet) if not bpy.data.objects.get(root_inlet_name): root_inlet = bpy.data.objects.new(root_inlet_name, None) @@ -135,10 +137,17 @@ def json_to_blend(js_data): root_inlet.rotation_euler = root_lcs.rotation_euler root_inlet.parent = root_lcs.parent lcs_collection.objects.link(root_inlet) + imported_objects['objs_lcs'].append(root_inlet.name) + logger.info('Root Inlet LCS object created!') + else: + logger.info('Root Inlet LCS object already exists!') else: - logger.info('Lost root LCS object!') + logger.info('Lost Root LCS object!') + else: + logger.info('No LCS objects found!') # TODO # update do not dork - logger.info('Generated %s objects without errors', len(bobjs)) - return bobjs_for_render + logger.info('Generated %s objects without errors', + len(sum(list(imported_objects.values()), []))) + return imported_objects diff --git a/cg/blender/processing/highpoly_setup.py b/cg/blender/processing/highpoly_setup.py index 0f3361c..ed9dc82 100644 --- a/cg/blender/processing/highpoly_setup.py +++ b/cg/blender/processing/highpoly_setup.py @@ -24,15 +24,16 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def setup_meshes(bobjs, cleanup=False, sharpness=False, shading=False): +def setup_meshes(obj_names, cleanup=False, sharpness=False, shading=False): ''' Setup raw meshes list after importing ''' logger.info('Hightpoly meshes setup launched...') - for bobj in bobjs: - if not bobj.type == 'MESH': + for obj_name in obj_names: + obj = bpy.data.objects[obj_name] + if not obj.type == 'MESH': continue bpy.ops.object.select_all(action='DESELECT') - bobj.select_set(state=True) - bpy.context.view_layer.objects.active = bobj + obj.select_set(state=True) + bpy.context.view_layer.objects.active = obj if cleanup: # remove doubles @@ -68,4 +69,4 @@ def setup_meshes(bobjs, cleanup=False, sharpness=False, shading=False): bpy.context.object.modifiers['triangulate'].keep_custom_normals = 1 bpy.context.object.modifiers['triangulate'].show_expanded = 0 - return logger.info('Hightpoly meshes setup finished!') + return logger.info('Setup of %s hightpoly meshes is finished!', len(obj_names)) diff --git a/cg/blender/processing/lowpoly_setup.py b/cg/blender/processing/lowpoly_setup.py index e75e4b8..2e7ae83 100644 --- a/cg/blender/processing/lowpoly_setup.py +++ b/cg/blender/processing/lowpoly_setup.py @@ -135,6 +135,6 @@ def parts_to_shells(hightpoly_part_names): bpy.ops.mesh.select_mode(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') - logger.info('Lowpoly shells created successfully!') + logger.info('Generation of %s lowpoly shells is finished!', len(lowpoly_col.objects)) - return lowpoly_col.objects + return [obj.name for obj in lowpoly_col.objects] diff --git a/cg/blender/processing/restruct_hierarchy_by_lcs.py b/cg/blender/processing/restruct_hierarchy_by_lcs.py index 1074668..88742d4 100644 --- a/cg/blender/processing/restruct_hierarchy_by_lcs.py +++ b/cg/blender/processing/restruct_hierarchy_by_lcs.py @@ -131,28 +131,42 @@ def lcs_collections(root_lcs, lcs_objects): return root_lcs.children -def restruct_hierarchy(): +def restruct_hierarchy(lcs_names): ''' Execute restructurisation. ''' - lcs_objects = bpy.data.collections[lcs_col_name].objects + #lcs_objects = bpy.data.collections[lcs_col_name].objects + lcs_objects = [] + root_lcs = None + if lcs_names: + for obj_name in lcs_names: + if obj_name.endswith(root): + root_lcs = bpy.data.objects[obj_name] + lcs_objects.append(bpy.data.objects[obj_name]) - main_locator = [obj for obj in bpy.data.objects if not obj.parent][0] - root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)][0] - lcs_objects = [lcs for lcs in lcs_objects if lcs != root_lcs] - root_locator = root_lcs.parent - unparenting(root_lcs) - round_transforms(root_lcs) - unparenting(root_locator) - parenting(root_lcs, root_locator) - parenting(root_lcs, main_locator) + main_locators = [obj for obj in bpy.data.objects if not obj.parent] + if len(main_locators) > 1: + logger.info('Scene has several main (root) locators! ' + 'This may cause an error!') - retree_by_lcs(lcs_objects, root_lcs) - lcs_constrainting(lcs_objects, root_lcs) + if root_lcs: + lcs_objects = [lcs for lcs in lcs_objects if lcs != root_lcs] + root_locator = root_lcs.parent + unparenting(root_lcs) + round_transforms(root_lcs) + unparenting(root_locator) + parenting(root_lcs, root_locator) + parenting(root_lcs, main_locators[0]) - lcs_collections(root_lcs, lcs_objects) + retree_by_lcs(lcs_objects, root_lcs) + lcs_constrainting(lcs_objects, root_lcs) - # remove unused for now collection - bpy.data.collections.remove(bpy.data.collections[hierarchy_col_name]) + lcs_collections(root_lcs, lcs_objects) - logger.info('Restructuring pipeline finished!') - return lcs_objects + # remove unused for now collection + bpy.data.collections.remove(bpy.data.collections[hierarchy_col_name]) + + return logger.info('Restructuring pipeline finished!') + else: + return logger.info('Lost root LCS object!') + else: + return logger.info('Restructuring pipeline canceled!') diff --git a/cg/blender/processing/uv_setup.py b/cg/blender/processing/uv_setup.py index 40989a9..ccb720d 100644 --- a/cg/blender/processing/uv_setup.py +++ b/cg/blender/processing/uv_setup.py @@ -24,9 +24,10 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def uv_unwrap(objs, angle_limit=30): +def uv_unwrap(obj_names, angle_limit=30): ''' UV unwrapping and UV packing processing ''' - for obj in objs: + for obj_name in obj_names: + obj = bpy.data.objects[obj_name] obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') @@ -60,5 +61,5 @@ def uv_unwrap(objs, angle_limit=30): bpy.ops.object.mode_set(mode='OBJECT') obj.select_set(False) - logger.info('UV unwrapping and UV packing processing done successfully!') - return objs + return logger.info('UV setup of %s lowpoly meshes is finished!', len(obj_names)) + diff --git a/cg/pipeline/cg_pipeline.py b/cg/pipeline/cg_pipeline.py index 3a50296..942a117 100644 --- a/cg/pipeline/cg_pipeline.py +++ b/cg/pipeline/cg_pipeline.py @@ -6,9 +6,11 @@ Convert and setup FreeCAD solid objects to 3d assets. Support Blender compiled as a Python Module only! ''' __version__ = '0.6' +import collections import json import logging import os +from itertools import zip_longest from blender.utils.remove_collections import remove_collections from blender.utils.cleanup_orphan_data import cleanup_orphan_data @@ -18,10 +20,10 @@ from blender.processing.restruct_hierarchy_by_lcs import restruct_hierarchy from blender.processing.highpoly_setup import setup_meshes from blender.processing.lowpoly_setup import parts_to_shells from blender.processing.uv_setup import uv_unwrap +from blender.export.dae import export_dae +from blender.export.stl import export_stl import bpy import mathutils -# from export.dae import export_dae -# from export.collision import export_col_stl logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -64,12 +66,15 @@ render = '_render' def cg_pipeline(**kwargs): ''' CG asset creation pipeline ''' + blend_path = kwargs.pop('blend_path', None) + mesh_export_path = kwargs.pop('mesh_export_path', None) + # prepare blend file remove_collections() cleanup_orphan_data() # convert FreeCAD scene to Blender scene - objs_for_render = json_to_blend( + imported_objects = json_to_blend( json.loads( cmd_proc(freecadcmd, fcstd_data_script, @@ -80,36 +85,84 @@ def cg_pipeline(**kwargs): ) # restructuring hierarchy by lcs points - lcs_objects = restruct_hierarchy() + if imported_objects['objs_lcs']: + restruct_hierarchy(imported_objects['objs_lcs']) # save blender scene - if kwargs['blend_path'] is not None: - if not os.path.isdir(os.path.dirname(kwargs['blend_path'])): - os.makedirs(os.path.dirname(kwargs['blend_path'])) - bpy.ops.wm.save_as_mainfile(filepath=kwargs['blend_path']) + if blend_path is not None: + if not os.path.isdir(os.path.dirname(blend_path)): + os.makedirs(os.path.dirname(blend_path)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path) # prepare highpoly - setup_meshes(objs_for_render, sharpness=True, shading=True) + if imported_objects['objs_foreground']: + setup_meshes(imported_objects['objs_foreground'], sharpness=True, shading=True) + else: + setup_meshes(imported_objects['objs_background'], sharpness=True, shading=True) + + # TODO Part's names from LCS names? + part_names = [lcs_name.split(inlet)[0] + for lcs_name in imported_objects['objs_lcs'] + if lcs_name.endswith(inlet)] + # prepare lowpoly - part_names = [p.name.split(inlet)[0] for p in lcs_objects if p.name.endswith(inlet)] lowpoly_objs = parts_to_shells(part_names) uv_unwrap(lowpoly_objs) # save blender scene - if kwargs['blend_path'] is not None: - if not os.path.isdir(os.path.dirname(kwargs['blend_path'])): - os.makedirs(os.path.dirname(kwargs['blend_path'])) - bpy.ops.wm.save_as_mainfile(filepath=kwargs['blend_path']) + if blend_path is not None: + if not os.path.isdir(os.path.dirname(blend_path)): + os.makedirs(os.path.dirname(blend_path)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path) - # export all objects - if kwargs['mesh_export_path'] is not None: - obs = bpy.context.selected_objects - for ob in obs: - ob.matrix_world = mathutils.Matrix() - for ob in obs: - ob.select_set(state=True) - export_dae(kwargs['mesh_export_path']) - export_col_stl(kwargs['mesh_export_path']) + # export object meshes and urdf + to_urdf = collections.defaultdict(list) + + if lowpoly_objs: + export_objs = lowpoly_objs + else: + export_objs = sum([imported_objects['objs_foreground'], + imported_objects['objs_background']], []) + + link = {} + for export_obj in export_objs: + link_prop = {} + if mesh_export_path is not None: + link_prop['visual'] = export_dae( + obj_name=export_obj, path=mesh_export_path, subdir='visual') + link_prop['collision'] = export_stl( + obj_name=export_obj, path=mesh_export_path, subdir='collision') + + link[export_obj] = link_prop + + to_urdf['links'].append(link) + + config = {'sequences': [['cube1', 'cube2', 'cube3', 'cube4'], ['cube2', 'cube1', 'cube4', 'cube3']]} + if config: + for sequence in config['sequences']: + joint = {} + for pair in zip_longest(sequence[0::2], sequence[1::2]): + joint_prop = {} + if pair[1]: + joint_prop['type'] = 'fixed' + location = list(bpy.data.objects.get(pair[1]).location) + rotation = list(bpy.data.objects.get(pair[0]).rotation_euler) + # origin + # round location values + for idx, axis in enumerate(location): + location[idx] = round(axis, 5) + joint_prop['location'] = location + joint_prop['rotation'] = rotation + # parent + joint_prop['parent'] = pair[0] + # child + joint_prop['child'] = pair[1] + + joint['_'.join(pair)] = joint_prop + + to_urdf['sequence'].append(joint) + + print(json.dumps(to_urdf, indent=4)) if __name__ == '__main__':