Tool for convert FreeCAD sub-assemblies to URDF-world according to config

This commit is contained in:
brothermechanic 2023-10-10 16:32:29 +00:00 committed by Igor Brylyov
parent 03bc34539c
commit 184ac7df88
12 changed files with 327 additions and 119 deletions

4
asp/mocks/urdf/asm.urdf Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<robossembler
name="{name}">
</robossembler>

13
asp/mocks/urdf/joint.urdf Normal file
View file

@ -0,0 +1,13 @@
<joint
name="{joint_name}"
type="{joint_type}">
<origin
xyz="{joint_location}"
rpy="{joint_rotation}" />
<parent
link="{parent_part}" />
<child
link="{child_part}" />
<axis
xyz="0 0 1" />
</joint>

31
asp/mocks/urdf/link.urdf Normal file
View file

@ -0,0 +1,31 @@
<link
name="{part_name}">
<inertial>
<origin
xyz="0 0 0"
rpy="0 0 0" />
<mass
value="{mass}" />
<inertia
ixx="0.0"
ixy="0.0"
ixz="0.0"
iyy="0.0"
iyz="0.0"
izz="0.0" />
</inertial>
<visual>
<geometry>
<mesh
filename="{visual_file}"
scale="1.0 1.0 1.0" />
</geometry>
</visual>
<collision>
<geometry>
<mesh
filename="{collision_file}"
scale="1.0 1.0 1.0" />
</geometry>
</collision>
</link>

View file

@ -1,7 +1,52 @@
# -*- coding: utf-8 -*-
"""
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# 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

View file

@ -1,34 +1,64 @@
# -*- coding: utf-8 -*-
"""
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# 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

View file

@ -1,34 +1,41 @@
# -*- coding: utf-8 -*-
"""
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# 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

View file

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Original code by (C) 2019 yorikvanhavre <yorik@uncreated.net>
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# 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('Lost root LCS object!')
logger.info('Root Inlet LCS object already exists!')
else:
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

View file

@ -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))

View file

@ -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]

View file

@ -131,20 +131,31 @@ 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]
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!')
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_locator)
parenting(root_lcs, main_locators[0])
retree_by_lcs(lcs_objects, root_lcs)
lcs_constrainting(lcs_objects, root_lcs)
@ -154,5 +165,8 @@ def restruct_hierarchy():
# remove unused for now collection
bpy.data.collections.remove(bpy.data.collections[hierarchy_col_name])
logger.info('Restructuring pipeline finished!')
return lcs_objects
return logger.info('Restructuring pipeline finished!')
else:
return logger.info('Lost root LCS object!')
else:
return logger.info('Restructuring pipeline canceled!')

View file

@ -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))

View file

@ -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__':