Blender Scene Compose & Export addon
This commit is contained in:
parent
021e5862ff
commit
c64bbf4a70
7 changed files with 455 additions and 0 deletions
|
@ -21,3 +21,9 @@
|
|||
- __Предикат стабильной осуществимости__. Верен для последовательности сборки, когда сборка на каждом из этапов приходит к стабильному состоянию.
|
||||
- __Предикат степеней свободы__. Формируется на основе уже сгенерированных графов/графа сборки. В каких степенях свободы возможно перемещать деталь.
|
||||
|
||||
# Генерация сцен
|
||||
|
||||
TODO: составить описание
|
||||
|
||||
[пример файла описания сцены](docs/scene_generator)
|
||||
|
||||
|
|
139
cg/blender/scripts/addons/Robossembler/__init__.py
Normal file
139
cg/blender/scripts/addons/Robossembler/__init__.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>
|
||||
# and write to the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Fifth Floor, Boston, MA 02110-1301, USA..
|
||||
#
|
||||
# The Original Code is Copyright (C) 2023 by Kurochkin Ilia ###
|
||||
# All rights reserved.
|
||||
#
|
||||
# Contact: brothermechanic@yandex.com ###
|
||||
# Information: https://gitlab.com/robossembler ###
|
||||
#
|
||||
# The Original Code is: all of this file.
|
||||
#
|
||||
# ***** END GPL LICENSE BLOCK *****
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import bpy
|
||||
|
||||
from bpy.types import (
|
||||
Panel,
|
||||
Operator,
|
||||
PropertyGroup)
|
||||
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
EnumProperty,
|
||||
PointerProperty)
|
||||
|
||||
from .scene_to_json import export_scene_conf
|
||||
|
||||
bl_info = {
|
||||
'name': 'Robossembler Tools',
|
||||
'author': 'brothermechanic@gmail.com',
|
||||
'version': (0, 1),
|
||||
'blender': (3, 6, 1),
|
||||
'location': '3D View > Toolbox',
|
||||
'description': 'Robossembler pipeline tools',
|
||||
'warning': '',
|
||||
'wiki_url': '',
|
||||
'tracker_url': 'https://gitlab.com/robossembler',
|
||||
'category': 'Robossembler',
|
||||
}
|
||||
|
||||
|
||||
class addon_Properties(PropertyGroup):
|
||||
|
||||
engine: EnumProperty(
|
||||
name='Physics Engine',
|
||||
description='Selest Target Engine',
|
||||
items=[('BULLET', 'Bullet', ''),
|
||||
('ODE', 'O D E', ''),
|
||||
('SIMBODY', 'Simbody', ''),
|
||||
('OPENSIM', 'OpenSim', '')
|
||||
]
|
||||
)
|
||||
|
||||
conf_file_path: StringProperty(
|
||||
name='Config File Path',
|
||||
description='Input/output config file path',
|
||||
default='',
|
||||
maxlen=1023,
|
||||
subtype='FILE_PATH'
|
||||
)
|
||||
|
||||
|
||||
class RobossemblerPanel(Panel):
|
||||
'''Doc'''
|
||||
bl_label = 'Robossembler Tools'
|
||||
bl_idname = 'ROBOSSEMBLER_PT_PANEL'
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'Robossembler'
|
||||
|
||||
def draw(self, context):
|
||||
prop = context.scene.robossembler_properties
|
||||
|
||||
layout = self.layout
|
||||
layout.prop(prop, 'conf_file_path')
|
||||
layout.prop(prop, 'engine')
|
||||
|
||||
col = layout.column()
|
||||
col.alert = True
|
||||
col.scale_y = 2.0
|
||||
col.operator('export.scene_config',
|
||||
icon='WORLD_DATA',
|
||||
text='Export Scene Config')
|
||||
|
||||
|
||||
class RobossemblerOperator(Operator):
|
||||
'''Tooltip'''
|
||||
bl_idname = 'export.scene_config'
|
||||
bl_label = ''
|
||||
bl_description = 'Export scene config'
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
prop = context.scene.robossembler_properties
|
||||
|
||||
conf_file_path = os.path.realpath(bpy.path.abspath((prop.conf_file_path)))
|
||||
physics_engine = prop.engine
|
||||
export_scene_conf(context, conf_file_path, physics_engine)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
classes = (
|
||||
RobossemblerPanel,
|
||||
RobossemblerOperator,
|
||||
addon_Properties)
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.Scene.robossembler_properties = PointerProperty(type=addon_Properties)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
del bpy.types.Scene.robossembler_properties
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
register()
|
30
cg/blender/scripts/addons/Robossembler/model_md5.py
Normal file
30
cg/blender/scripts/addons/Robossembler/model_md5.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# coding: utf-8
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
||||
def md5(file_path):
|
||||
''' Generate md5 hash. '''
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
|
||||
def create_file_md5(file_path):
|
||||
''' Get md5_file_hash of file and store to md5 file. '''
|
||||
md5_file_path = f'{file_path}.md5'
|
||||
md5_file_hash = md5(file_path)
|
||||
# create md5_file
|
||||
if not os.path.isfile(md5_file_path):
|
||||
with open(md5_file_path, 'w', encoding='utf-8') as md5_file:
|
||||
md5_file.write(md5_file_hash)
|
||||
else:
|
||||
with open(md5_file_path, 'r', encoding='utf-8') as md5_file:
|
||||
md5_file_stored = md5_file.read()
|
||||
# check md5_file
|
||||
assert md5_file_hash == md5_file_stored, (
|
||||
"Mosel's md5 don't matched with stored %s.md5 file", file_path)
|
||||
return md5_file_hash
|
18
cg/blender/scripts/addons/Robossembler/model_name.py
Normal file
18
cg/blender/scripts/addons/Robossembler/model_name.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# coding: utf-8
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_sdf_urdf_model_name(model_path):
|
||||
mytree = ET.parse(model_path)
|
||||
myroot = mytree.getroot()
|
||||
for elem in myroot.iter():
|
||||
if elem.tag in ('link', 'joint', 'material', 'visual', 'collision'):
|
||||
continue
|
||||
if not elem.attrib.get('name'):
|
||||
continue
|
||||
return elem.attrib.get('name')
|
||||
return logger.warning('Model %s do not have a name!', model_path)
|
12
cg/blender/scripts/addons/Robossembler/model_paths.py
Normal file
12
cg/blender/scripts/addons/Robossembler/model_paths.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# coding: utf-8
|
||||
|
||||
import glob
|
||||
|
||||
|
||||
def get_urdf_sdf_model_paths(models_dir):
|
||||
''' Collect models paths. '''
|
||||
model_paths = []
|
||||
for file_name in glob.glob(f'{models_dir}/**', recursive=True):
|
||||
if file_name.endswith('.urdf') or file_name.endswith('.sdf'):
|
||||
model_paths.append(file_name.replace('\\', '/'))
|
||||
return model_paths
|
189
cg/blender/scripts/addons/Robossembler/scene_to_json.py
Normal file
189
cg/blender/scripts/addons/Robossembler/scene_to_json.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>
|
||||
# and write to the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Fifth Floor, Boston, MA 02110-1301, USA..
|
||||
#
|
||||
# The Original Code is Copyright (C) 2023 by Kurochkin Ilia ###
|
||||
# All rights reserved.
|
||||
#
|
||||
# Contact: brothermechanic@yandex.com ###
|
||||
# Information: https://gitlab.com/robossembler ###
|
||||
#
|
||||
# The Original Code is: all of this file.
|
||||
#
|
||||
# ***** END GPL LICENSE BLOCK *****
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
DESCRIPTION.
|
||||
Collect all root objects in blender scene and export as json scene configuration.
|
||||
'''
|
||||
__version__ = '0.1'
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
|
||||
from .model_paths import get_urdf_sdf_model_paths
|
||||
from .model_name import get_sdf_urdf_model_name
|
||||
from .model_md5 import create_file_md5
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def export_scene_conf(context, scene_config_path, physics_engine):
|
||||
''' Export scene config to json. '''
|
||||
# open conf file
|
||||
if os.path.isfile(scene_config_path):
|
||||
with open(scene_config_path, 'r', encoding='utf-8') as conf_file:
|
||||
world_dic = json.load(conf_file)
|
||||
else:
|
||||
world_dic = collections.defaultdict(list)
|
||||
models_dir = os.path.join(os.path.dirname(scene_config_path), 'models')
|
||||
model_paths = get_urdf_sdf_model_paths(models_dir)
|
||||
for model_path in model_paths:
|
||||
model_dic = {}
|
||||
# 1
|
||||
model_dic['name'] = get_sdf_urdf_model_name(model_path)
|
||||
# 2
|
||||
model_dic['id'] = create_file_md5(model_path)
|
||||
# 3
|
||||
relative_model_path = model_path.split(
|
||||
os.path.dirname(scene_config_path))[1][1:]
|
||||
model_dic['path'] = relative_model_path
|
||||
world_dic['models'].append(model_dic)
|
||||
|
||||
# collect all non parented objs
|
||||
roots = [obj for obj in context.scene.objects if not obj.parent]
|
||||
|
||||
# collect instances
|
||||
for instance in roots:
|
||||
instance_dic = {}
|
||||
# 1
|
||||
instance_dic['model_name'] = ''
|
||||
if instance.get('model/name'):
|
||||
instance_dic['model_name'] = instance['model/name']
|
||||
# 2
|
||||
instance_dic['model_id'] = ''
|
||||
for item in world_dic['models']:
|
||||
if item['name'] != instance_dic['model_name']:
|
||||
continue
|
||||
instance_dic['model_id'] = item['id']
|
||||
# 2
|
||||
instance_dic['id'] = ''
|
||||
# 3
|
||||
instance_dic['pose'] = {}
|
||||
instance_dic['pose']['x'] = round(instance.location[0], 3)
|
||||
instance_dic['pose']['y'] = round(instance.location[1], 3)
|
||||
instance_dic['pose']['z'] = round(instance.location[2], 3)
|
||||
instance_dic['pose']['roll'] = round(math.degrees(instance.rotation_euler[0]), 3)
|
||||
instance_dic['pose']['pitch'] = round(math.degrees(instance.rotation_euler[1]), 3)
|
||||
instance_dic['pose']['yaw'] = round(math.degrees(instance.rotation_euler[2]), 3)
|
||||
# 4
|
||||
if instance.scale[0] != sum(instance.scale[:]) / 3:
|
||||
logger.warning('Asset instance {} is not uniformly scaled!'.format(
|
||||
instance.name))
|
||||
instance_dic['scale'] = round(instance.scale[0], 3)
|
||||
# 5
|
||||
# if assembly
|
||||
if instance_dic['model_name']:
|
||||
instance_dic['type'] = 'asset'
|
||||
links = instance.children_recursive
|
||||
links.insert(0, instance)
|
||||
instance_dic['link_names'] = []
|
||||
instance_dic['links'] = {}
|
||||
transforms = 0.0
|
||||
for link in links:
|
||||
child = {}
|
||||
if link.type != 'ARMATURE':
|
||||
continue
|
||||
bone = link.pose.bones[0]
|
||||
rotation_mode = bone.rotation_mode
|
||||
bone.rotation_mode = 'XYZ'
|
||||
child['pose'] = {}
|
||||
child['pose']['x'] = round(bone.location[0], 6)
|
||||
child['pose']['y'] = round(bone.location[1], 6)
|
||||
child['pose']['z'] = round(bone.location[2], 6)
|
||||
child['pose']['roll'] = round(math.degrees(bone.rotation_euler[0]), 6)
|
||||
child['pose']['pitch'] = round(math.degrees(bone.rotation_euler[1]), 6)
|
||||
child['pose']['yaw'] = round(math.degrees(bone.rotation_euler[2]), 6)
|
||||
bone.rotation_mode = rotation_mode
|
||||
if bone.scale[0] != sum(bone.scale[:]) / 3:
|
||||
logger.warning(
|
||||
'Link {} of asset {} is not uniformly scaled!'.format(
|
||||
link.name, asset_dic['name']))
|
||||
child['scale'] = round(bone.scale[0], 3)
|
||||
transforms += round(sum(
|
||||
[child['pose']['x'], child['pose']['y'], child['pose']['z'],
|
||||
child['pose']['roll'], child['pose']['pitch'], child['pose']['yaw']],
|
||||
(child['scale'] - 1.0)),
|
||||
6)
|
||||
if '.' in link.name:
|
||||
link_name = link.name.rpartition('.')[0]
|
||||
else:
|
||||
link_name = link.name
|
||||
|
||||
instance_dic['link_names'].append(link_name)
|
||||
instance_dic['links'][link_name] = child
|
||||
|
||||
if transforms == 0.0:
|
||||
instance_dic.pop('link_names')
|
||||
instance_dic.pop('links')
|
||||
|
||||
# 6
|
||||
instance_dic['parent'] = 'world'
|
||||
# if light
|
||||
if instance.type == 'LIGHT':
|
||||
instance_dic['type'] = instance.type.lower()
|
||||
if instance.data.type == 'POINT':
|
||||
instance_dic['light_type'] = 'point'
|
||||
elif instance.data.type == 'SUN':
|
||||
instance_dic['light_type'] = 'directional'
|
||||
elif instance.data.type == 'SPOT':
|
||||
instance_dic['light_type'] = 'spot'
|
||||
instance_dic['spot_angle'] = round(
|
||||
math.degrees(instance.data.spot_size), 1)
|
||||
else:
|
||||
logger.warning(
|
||||
'Unsupported light type {} on instance {}!'.format(
|
||||
instance.data.type, instance.name))
|
||||
instance_dic['intencity'] = instance.data.energy
|
||||
instance_dic['diffuse'] = list(instance.data.color)
|
||||
# if camera
|
||||
if instance.type == 'CAMERA':
|
||||
instance_dic['type'] = instance.type.lower()
|
||||
instance_dic['focal_length'] = instance.data.lens
|
||||
instance_dic['sensor_size'] = [instance.data.sensor_width,
|
||||
instance.data.sensor_height]
|
||||
|
||||
world_dic['instances'].append(instance_dic)
|
||||
|
||||
# select physics engine
|
||||
world_dic['physics'] = {}
|
||||
world_dic['physics']['engine_name'] = physics_engine
|
||||
world_dic['physics']['gravity'] = {}
|
||||
world_dic['physics']['gravity']['x'] = round(context.scene.gravity[0], 3)
|
||||
world_dic['physics']['gravity']['y'] = round(context.scene.gravity[1], 3)
|
||||
world_dic['physics']['gravity']['z'] = round(context.scene.gravity[2], 3)
|
||||
|
||||
# write conf file
|
||||
with open(scene_config_path, 'w', encoding='utf-8') as world_conf_file:
|
||||
json.dump(world_dic, world_conf_file, ensure_ascii=False, indent=4)
|
||||
logger.info('Scene configuration in json file was completed successfully!')
|
||||
return scene_config_path
|
61
docs/scene_generator.md
Normal file
61
docs/scene_generator.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
## Пример файла описания сцены из Blender
|
||||
|
||||
TODO: рассписать потребность в основных элементах, что и для чего
|
||||
|
||||
```json
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"name": "robo-arm",
|
||||
"id": "629b29d7-fe15-428b-9014-6c3dde045af8",
|
||||
"model_path": "../model.urdf"
|
||||
}
|
||||
],
|
||||
"instances": [
|
||||
// измненная URDF модель
|
||||
{
|
||||
"id": "0e29084f-1190-45d0-bd59-8f4ce2591gb1",
|
||||
"name": "robo-arm-1",
|
||||
//assetId указывает на родительский ассет
|
||||
"assetId": "629b29d7-fe15-428b-9014-6c3dde045af8",
|
||||
"pose": {
|
||||
"x": 12.0,
|
||||
"y": 1.0,
|
||||
"z": 4.0,
|
||||
"roll": 1.0,
|
||||
"pitch": 4.0,
|
||||
"yaw": 5.0
|
||||
},
|
||||
//если изменено внутренее состояние URDF модели
|
||||
"tags": [
|
||||
"<joint name=\"robotiq_85_right_finger_joint\" type=\"fixed\"><parent link=\"robotiq_85_right_knuckle_link\"/><child link=\"robotiq_85_right_finger_link\"/><origin rpy=\"0 0 0\" xyz=\"-0.03152616 0.0 -0.00376347\"/></joint>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0e27984f-8890-4d90-bd59-8f4ce29920f9",
|
||||
"name": "robo-arm-2",
|
||||
//assetId указывает на родительский ассет
|
||||
"assetId": "629b29d7-fe15-428b-9014-6c3dde045af8",
|
||||
//если дефолтные позиции модели
|
||||
"pose": null,
|
||||
//если не изменено внутренее состояние URDF модели
|
||||
"tags": null
|
||||
},
|
||||
{
|
||||
"type": "LIGHT",
|
||||
"light_type": "SPOT",
|
||||
"power": 10.0,
|
||||
"spot_angle": 45.0,
|
||||
"name": null,
|
||||
"pose": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0,
|
||||
"roll": 0.0,
|
||||
"pitch": 0.0,
|
||||
"yaw": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue