diff --git a/cg/freecad/utils/README.md b/cg/freecad/utils/README.md index a87b714..fa505b2 100644 --- a/cg/freecad/utils/README.md +++ b/cg/freecad/utils/README.md @@ -1,20 +1,9 @@ -## cg.freecad.utils +## FreeCAD's tools for publishing .FCStd scene. -Общие инструменты для работы в среде FreeCAD. -### freecad_to_json.py - -Сценарий производит: -- анализ FCStd сцены FreeCAD на наличие: - - твердых тел - - твердотельных поверхностей - - точек координат LCS - - иерархии - - скрытых объектов - - FEM материалов -- тесселяцию твердых тел по заданным параметрам -- формирование описания сцены в виде JSON конфигурации - -### is_object_solid.py - -Проверка, является ли объект твердотельным. +```python +import sys +sys.path.append('/path/to/freecad_exporters') +import freecad_exporters +freecad_exporters.publish_project_database() +``` diff --git a/cg/freecad/utils/freecad_exporters.py b/cg/freecad/utils/freecad_exporters.py index 926bd8a..b20232b 100644 --- a/cg/freecad/utils/freecad_exporters.py +++ b/cg/freecad/utils/freecad_exporters.py @@ -48,15 +48,15 @@ logging.basicConfig(level=logging.INFO) def export_assembly_trees(doc, clones_dic=None) -> list: ''' Read FreeCAD .FCStd hierarchy and store it to assembly JSON config files. ''' # determine root locators - lcs_root_points = [] + default_origins = [] for obj in doc.Objects: if obj.isDerivedFrom('PartDesign::CoordinateSystem'): if (hasattr(obj, 'Robossembler_DefaultOrigin') and getattr(obj, 'Robossembler_DefaultOrigin')): - lcs_root_points.append(obj) - if len(lcs_root_points) > 1: + default_origins.append(obj) + if len(default_origins) > 1: root_locators = [] - for lcs in lcs_root_points: + for lcs in default_origins: if hasattr(lcs, 'Robossembler_RootLocator'): root_locators.append(getattr(lcs, 'Robossembler_DefaultOrigin')) else: @@ -68,23 +68,23 @@ def export_assembly_trees(doc, clones_dic=None) -> list: root_locators = [ root for root in doc.Objects if not root.InList - if root .isDerivedFrom('App::Part')] + if root.isDerivedFrom('App::Part')] - config_files = [] + tree_files = [] for root_locator in root_locators: dict_tree = {} freecad_tools.hierarchy_tree(root_locator, dict_tree, clones_dic) # write file - main_file_dir = os.path.dirname(doc.FileName) - assembly_tree_path = os.path.join(main_file_dir, f'{root_locator.Label}.json') + project_dir = os.path.dirname(doc.FileName) + assembly_tree_path = os.path.join(project_dir, root_locator.Label + '_tree_version.json') with open(assembly_tree_path, 'w', encoding='utf-8') as json_file: json.dump(dict_tree, json_file, ensure_ascii=False, indent=4) logger.info('Assembly tree saved successfully to %s!', assembly_tree_path) - config_files.append(assembly_tree_path) - logger.info('Saved %s assembly trees!', len(config_files)) + tree_files.append(assembly_tree_path) + logger.info('Saved %s assembly trees!', len(tree_files)) - return config_files + return tree_files def export_parts_database( @@ -94,9 +94,7 @@ def export_parts_database( Collect parts database and export as JSON config file [ { - 'type': '', 'name': '', - 'attributes': [], 'part_path': '', 'material_path': '' }, @@ -104,13 +102,19 @@ def export_parts_database( ] ''' # path directory - main_file_dir = os.path.dirname(doc.FileName) - parts_dir = os.path.join(main_file_dir, 'parts') - if os.path.exists(parts_dir): - shutil.rmtree(parts_dir) - os.makedirs(parts_dir) + project_dir = os.path.dirname(doc.FileName) + materials_dir = os.path.join(project_dir, 'parts', 'materials') + if os.path.exists(materials_dir): + shutil.rmtree(materials_dir) + os.makedirs(materials_dir) else: - os.makedirs(parts_dir) + os.makedirs(materials_dir) + objects_dir = os.path.join(project_dir, 'parts', 'objects') + if os.path.exists(objects_dir): + shutil.rmtree(objects_dir) + os.makedirs(objects_dir) + else: + os.makedirs(objects_dir) # collect all materials fem_mats = [fem_mat for fem_mat in doc.Objects @@ -140,7 +144,11 @@ def export_parts_database( logger.warning('Part has non solid shape! Please check %s', obj.Label) continue - db_obj = {'type': 'PART'} + db_obj = { + 'name': '', + 'part_path': '', + 'material_path': '' + } db_obj['name'] = obj.Label if clones_dic: @@ -154,7 +162,7 @@ def export_parts_database( for attr in robossembler_attrs: db_obj['attributes'].append({attr: getattr(obj, attr)}) - part_path = os.path.join(parts_dir, db_obj['name'] + '.stl') + part_path = os.path.join(objects_dir, db_obj['name'] + '.stl') if os.path.exists(part_path): # this is clone continue @@ -167,7 +175,7 @@ def export_parts_database( # export to stl files #Mesh.export([mesh_from_shape], part_path, tolerance=linear_deflection) mesh_from_shape.Mesh.write(part_path) - db_obj['part_path'] = os.path.relpath(part_path, main_file_dir) + db_obj['part_path'] = os.path.relpath(part_path, project_dir) logger.info('Part %s exported to stl file %s.', obj.Label, part_path) # find linked material path @@ -176,15 +184,20 @@ def export_parts_database( for ref in fem_mat.References if ref[0].Label == obj.Label] if fem_mat_name: - for material_path in material_paths: - if fem_mat_name[0] in material_path: - db_obj['material_path'] = material_path + for source_path in material_paths: + if fem_mat_name[0] not in source_path: + continue + material_path = os.path.join( + materials_dir, os.path.basename(source_path)) + if not os.path.exists(material_path): + shutil.copy2(source_path, material_path) + db_obj['material_path'] = os.path.relpath(material_path, project_dir) # append to database parts_db.append(db_obj) logger.info('Passed %s parts without errors', len(parts_db)) - parts_db_path = os.path.join(main_file_dir, f'{FreeCAD.ActiveDocument.Label}.FCStd.json') + parts_db_path = os.path.join(project_dir, FreeCAD.ActiveDocument.Label + '_parts_data.json') with open(parts_db_path, 'w', encoding='utf-8') as json_file: json.dump(parts_db, json_file, ensure_ascii=False, indent=4) logger.info('Parts Database exported successfully to %s!', parts_db_path) @@ -218,9 +231,8 @@ def publish_project_database(doc=FreeCAD.getDocument(FreeCAD.ActiveDocument.Labe #doc.saveAs(u"//") clones_dic = freecad_tools.collect_clones(doc) - - export_assembly_trees(doc, clones_dic) - export_parts_database(doc, clones_dic, **tesselation_params) + tree_files = export_assembly_trees(doc, clones_dic) + parts_data = export_parts_database(doc, clones_dic, **tesselation_params) logger.info('FreeCAD document %s published!', doc.Label) diff --git a/cg/freecad/utils/freecad_tools.py b/cg/freecad/utils/freecad_tools.py index ccdc5c9..530e9c0 100644 --- a/cg/freecad/utils/freecad_tools.py +++ b/cg/freecad/utils/freecad_tools.py @@ -244,8 +244,7 @@ def hierarchy_tree(obj, dict_tree, clones_dic=None) -> dict: 'type': '', 'name': '', 'base_name': '', - 'loc_xyz': [], - 'rot_xyzw': [], + 'pose': [{'loc_xyz': []}, {'rot_xyzw': []}], 'attributes': [], 'children': [ { @@ -261,6 +260,7 @@ def hierarchy_tree(obj, dict_tree, clones_dic=None) -> dict: ] } ''' + # collect type if obj.isDerivedFrom('Part::Feature'): if obj.isDerivedFrom('PartDesign::CoordinateSystem'): @@ -274,22 +274,25 @@ def hierarchy_tree(obj, dict_tree, clones_dic=None) -> dict: # collect name dict_tree['name'] = obj.Label # collect base_name + dict_tree['base_name'] = '' if clones_dic: if obj.isDerivedFrom('Part::Feature'): for k, v in clones_dic.items(): if obj.Label in v: dict_tree['base_name'] = k # collect transforms - dict_tree['loc_xyz'] = list(obj.Placement.Base) - dict_tree['rot_xyzw'] = list(obj.Placement.Rotation.Q) + dict_tree['pose'] = [ + {'loc_xyz': list(obj.Placement.Base)}, + {'rot_xyzw': list(obj.Placement.Rotation.Q)} + ] # collect attributes + dict_tree['attributes'] = [] robossembler_attrs = [attr for attr in dir(obj) if 'Robossembler' in attr] if robossembler_attrs: - dict_tree['attributes'] = [] for attr in robossembler_attrs: dict_tree['attributes'].append({attr: getattr(obj, attr)}) - # collect children - if obj.OutList: + # collect children for LOCATOR only + if obj.OutList and obj.isDerivedFrom('App::Part'): dict_tree['children'] = [] for child in obj.OutList: # skip hidden objects diff --git a/rcg_pipeline/LICENSE b/rcg_pipeline/LICENSE new file mode 100644 index 0000000..6fb6a01 --- /dev/null +++ b/rcg_pipeline/LICENSE @@ -0,0 +1,157 @@ +# GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the +terms and conditions of version 3 of the GNU General Public License, +supplemented by the additional permissions listed below. + +## 0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the +GNU General Public License. + +"The Library" refers to a covered work governed by this License, other +than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + +The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +## 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +## 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + +- a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or +- b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +## 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a +header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + +- a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the object code with a copy of the GNU GPL and this + license document. + +## 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + +- a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. +- c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. +- d) Do one of the following: + - 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + - 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. +- e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +## 5. Combined Libraries. + +You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + +- a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. +- b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + +## 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License "or any later version" applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, you +may choose any version of the GNU Lesser General Public License ever +published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/rcg_pipeline/Makefile b/rcg_pipeline/Makefile new file mode 100644 index 0000000..0e844dc --- /dev/null +++ b/rcg_pipeline/Makefile @@ -0,0 +1,31 @@ +.FORCE: + +BLUE=\033[0;34m +BLACK=\033[0;30m + +help: + @echo "$(BLUE) make test - run all unit tests" + @echo " make coverage - run unit tests and coverage report" + @echo " make dist - build dist files" + @echo " make upload - upload to PyPI" + @echo " make clean - remove dist and docs build files" + @echo " make help - this message$(BLACK)" + +test: + pytest + +coverage: + coverage run --omit=\*/test_\* -m unittest + coverage report + +dist: .FORCE + #$(MAKE) test + python -m build + ls -lh dist + +upload: .FORCE + twine upload dist/* + +clean: .FORCE + -rm -r *.egg-info + -rm -r dist build diff --git a/rcg_pipeline/README.md b/rcg_pipeline/README.md new file mode 100644 index 0000000..ffdc24e --- /dev/null +++ b/rcg_pipeline/README.md @@ -0,0 +1,37 @@ +## Robossembler CG Pipeline + +Алгоритмы запуска технологии компьютерной графики. + +Пакетное производство 3д ассетов из базы данных тесселированных объектов САПР (parts) их сборочной иерархии. + +Поддерживается работа поверх Blender в качестве модуля! + +Этапы алгоритма: + +* генерация Blender сцены, +* исправоение parts объектов, +* генерация и назначение CG материала parts объектам, +* перестроение иерархии сборок на основе данных LCS объектов, +* группировка parts объектов объектов в составные RENDER ассеты, +* генерация, и развертка монолитных VISUAL ассетов из RENDER ассетов, +* запекание поверхности и материала RENDER ассетоа в текстуры VISUAL ассетов, +* назначение материалов и тестур VISUAL ассетам, +* генерация COLLISION ассетов из VISUAL ассетов, +- экспорт всех типов ассетов в требуемые форматы базы данных. + +Пример запуска:: +```python +# nixGL /nix/store/gd3shnza1i50zn8zs04fa729ribr88m9-python3-3.11.8/bin/python3 + +import sys +sys.path.append('/nix/store/ip49yhfl2l8gphylj4i43kdpq5qnq93i-bpy-4.1.0/lib/python3.11/site-packages') +sys.path.append('/media/disk/robossembler/project/collab/150-cg-render-assets/rcg_pipeline') +sys.path.append('/nix/store/x1rqfn240xn6m6p6077gxfqxdxxj1cmc-python3.11-numpy-1.26.4/lib/python3.11/site-packages') +import rcg_pipeline +project_dir = '/path/to/' +parts_tree_path = '/path/to/_tree_version.json' +libs_data_path = '/path/to/_libs_data.json' + +rcg_pipeline.libs.generate_libs_database(project_dir) +rcg_pipeline.render_asset.build_render_asset(parts_tree_path, libs_data_path, project_dir) +``` diff --git a/rcg_pipeline/pyproject.toml b/rcg_pipeline/pyproject.toml new file mode 100644 index 0000000..408f737 --- /dev/null +++ b/rcg_pipeline/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools >= 69.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["rcg_pipeline"] + +[project] +name = "rcg_pipeline" +version = "1.0.0" +description = "Robossembler CG Pipeline" +readme = "README.md" +license = {file = "LICENSE"} +authors = [{name = "Ilia Kurochkin", email = "brothermechanic@yandex.com"}] +maintainers = [{name = "Igor Brylyov", email = "movefasta@dezcom.org"}] +requires-python = ">= 3.10" +dependencies = ["numpy >= 1.26.4"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Operating System :: Linux", +] +keywords = ["cg", "robossembler", "blender", "engineering", "robotics", "gamedev"] + +[project.urls] +Homepage = "https://robossembler.org/" +Documentation = "https://robossembler.org/docs/technologies/cad-cg-pipeline" +Repository = "https://gitlab.com/robossembler/framework.git" +Issues = "https://gitlab.com/robossembler/framework/-/issues" diff --git a/rcg_pipeline/rcg_pipeline/__init__.py b/rcg_pipeline/rcg_pipeline/__init__.py new file mode 100644 index 0000000..15972fb --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/__init__.py @@ -0,0 +1,67 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2021-2024 Robossembler LLC +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This file is part of Robossembler Framework +# project repo: https://gitlab.com/robossembler/framework +# +# Robossembler Framework 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Computer Graphics Pipeline for Robossembler Framework. +Batch production of 3d assets from the database +of tessellated CAD objects (parts) and their assembling hierarchy. +''' + +__title__ = 'Robossembler CG Pipeline' +__version__ = '1.0.0' +__author__ = 'Ilia Kurochkin' +__email__ = 'brothermechanic@yandex.com' +__copyright__ = 'Copyright (C) 2021-2024 Robossembler LLC' +__url__ = ['https://robossembler.org'] +__license__ = 'GPL-3' +#__all__ = ['libs', 'render_asset', 'rcg_full_pipeline'] + +import logging +import os + +from . import libs +from . import render_asset + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def rcg_full_pipeline(project_dir): + ''' + ''' + # 1 generate libs + libs_data_path = libs.generate_libs_database(project_dir) + + # 2 build render assets + parts_tree_names = [ + data for data in os.listdir(project_dir) + if data.endswith('tree_version.json')] + for parts_tree_name in parts_tree_names: + parts_tree_path = os.path.join(project_dir, parts_tree_name) + render_asset.build_render_asset(parts_tree_path, libs_data_path, project_dir) + return True diff --git a/rcg_pipeline/rcg_pipeline/export/__init__.py b/rcg_pipeline/rcg_pipeline/export/__init__.py new file mode 100644 index 0000000..8f79be7 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/__init__.py @@ -0,0 +1,77 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Decorator for export functions. +''' + +import os +import bpy +import mathutils + + +def export_decorator(func): + + def wrapper(**kwargs): + bpy.ops.object.select_all(action='DESELECT') + # add defaults + kwargs.setdefault('global_scale', 1000) + kwargs.setdefault('axis_forward', 'Y') + kwargs.setdefault('axis_up', 'Z') + kwargs.setdefault('file_dir', '//') + kwargs.setdefault('sub_dir', '') + kwargs.setdefault('reset_transforms', False) + + if kwargs['reset_transforms']: + obj = bpy.data.objects.get(kwargs['obj_name']) + obj.name = kwargs['obj_name'] + '_orig' + obj_data = obj.data.copy() + obj_tmp = obj.copy() + obj_tmp.data = obj_data + obj_tmp.name = kwargs['obj_name'] + bpy.context.collection.objects.link(obj_tmp) + obj_tmp.parent = None + obj_tmp.matrix_world = mathutils.Matrix() + + 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 + file_dir = os.path.join(kwargs['file_dir'], kwargs['sub_dir']) + os.makedirs(file_dir, exist_ok=True) + kwargs['outpath'] = os.path.join(file_dir, kwargs['obj_name']) + # return export function + file_path = func(**kwargs) + + #cleanup temporary object + if kwargs['reset_transforms']: + bpy.data.objects.remove(obj, do_unlink=True) + obj = bpy.data.objects.get(kwargs['obj_name'] + '_orig') + obj.name = kwargs['obj_name'] + + return file_path + + return wrapper diff --git a/rcg_pipeline/rcg_pipeline/export/dae.py b/rcg_pipeline/rcg_pipeline/export/dae.py new file mode 100644 index 0000000..18ac50e --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/dae.py @@ -0,0 +1,75 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Collada object exporter. +''' + +__version__ = '0.3' + +import bpy +from . import export_decorator + + +@export_decorator +def export(**kwargs): + file_path = ('{}.dae'.format(kwargs['outpath'])) + + bpy.ops.wm.collada_export( + filepath=file_path, + check_existing=False, + apply_modifiers=True, + export_mesh_type=0, + export_mesh_type_selection='view', + export_global_forward_selection=kwargs['axis_forward'], + export_global_up_selection=kwargs['axis_up'], + 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 file_path diff --git a/rcg_pipeline/rcg_pipeline/export/fbx.py b/rcg_pipeline/rcg_pipeline/export/fbx.py new file mode 100644 index 0000000..41848f6 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/fbx.py @@ -0,0 +1,84 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +FBX object exporter. +''' + +__version__ = "0.3" + +import bpy +from . import export_decorator + + +@export_decorator +def export(**kwargs): + file_path = ('{}.fbx'.format(kwargs['outpath'])) + + bpy.ops.export_scene.fbx( + filepath=file_path, + check_existing=False, + filter_glob="*.fbx", + use_selection=True, + use_visible=False, + use_active_collection=False, + global_scale=1, + apply_unit_scale=True, + apply_scale_options='FBX_SCALE_NONE', + use_space_transform=True, + bake_space_transform=False, + object_types={'MESH'}, + use_mesh_modifiers=True, + use_mesh_modifiers_render=True, + mesh_smooth_type='FACE', + colors_type='SRGB', + use_subsurf=False, + use_mesh_edges=False, + use_tspace=False, + use_triangles=True, + use_custom_props=False, + add_leaf_bones=True, + primary_bone_axis='Y', + secondary_bone_axis='X', + use_armature_deform_only=False, + armature_nodetype='NULL', + bake_anim=False, + bake_anim_use_all_bones=True, + bake_anim_use_nla_strips=True, + bake_anim_use_all_actions=True, + bake_anim_force_startend_keying=True, + bake_anim_step=1, + bake_anim_simplify_factor=1, + path_mode='AUTO', + embed_textures=False, + batch_mode='OFF', + use_batch_own_dir=True, + use_metadata=True, + # '-Z' + axis_forward=kwargs['axis_forward'], + # 'Y' + axis_up=kwargs['axis_up']) + + return file_path diff --git a/rcg_pipeline/rcg_pipeline/export/glb.py b/rcg_pipeline/rcg_pipeline/export/glb.py new file mode 100644 index 0000000..5a525f9 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/glb.py @@ -0,0 +1,83 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +glTF object exporter. +''' + +__version__ = "0.3" + +import bpy +from . import export_decorator + + +@export_decorator +def export(**kwargs): + file_path = ('{}.glb'.format(kwargs['outpath'])) + + bpy.ops.export_scene.gltf( + filepath=file_path, + check_existing=False, + gltf_export_id="", + export_use_gltfpack=False, + export_format='GLB', + export_copyright="glTF object exporter", + export_image_format='AUTO', + export_image_add_webp=False, + export_image_webp_fallback=False, + export_texture_dir="", + export_jpeg_quality=75, + export_image_quality=75, + export_keep_originals=False, + export_texcoords=True, + export_normals=True, + # no custom + export_draco_mesh_compression_enable=False, + export_tangents=False, + export_materials='EXPORT', + export_unused_images=False, + export_unused_textures=False, + export_attributes=False, + # custom + use_selection=True, + use_visible=False, + use_renderable=False, + use_active_collection_with_nested=True, + use_active_collection=False, + use_active_scene=False, + export_extras=False, + # no custom + export_yup=True, + export_apply=False, + # custom + export_animations=False, + # custom + export_morph=False, + export_hierarchy_flatten_bones=False, + export_original_specular=False, + will_save_settings=False, + export_hierarchy_full_collections=False) + + return file_path diff --git a/rcg_pipeline/rcg_pipeline/export/ply.py b/rcg_pipeline/rcg_pipeline/export/ply.py new file mode 100644 index 0000000..aeca279 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/ply.py @@ -0,0 +1,55 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +PLY object exporter. +''' + +__version__ = "0.3" + +import bpy +from . import export_decorator + + +@export_decorator +def export(**kwargs): + file_path = ('{}.ply'.format(kwargs['outpath'])) + + bpy.ops.wm.ply_export( + filepath=file_path, + check_existing=False, + forward_axis=kwargs['axis_forward'], + up_axis=kwargs['axis_up'], + global_scale=kwargs['global_scale'], + apply_modifiers=True, + export_selected_objects=True, + export_uv=True, + export_normals=True, + export_colors='SRGB', + export_attributes=True, + export_triangulated_mesh=True, + ascii_format=True) + + return file_path diff --git a/rcg_pipeline/rcg_pipeline/export/stl.py b/rcg_pipeline/rcg_pipeline/export/stl.py new file mode 100644 index 0000000..93b549c --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/export/stl.py @@ -0,0 +1,52 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +STL object exporter. +''' + +__version__ = '0.3' + +import bpy +from . import export_decorator + + +@export_decorator +def export(**kwargs): + file_path = ('{}.stl'.format(kwargs['outpath'])) + + bpy.ops.wm.stl_export( + filepath=file_path, + check_existing=False, + ascii_format=False, + use_batch=False, + export_selected_objects=True, + global_scale=kwargs['global_scale'], + use_scene_unit=False, + forward_axis=kwargs['axis_forward'], + up_axis=kwargs['axis_up'], + apply_modifiers=True) + + return file_path diff --git a/rcg_pipeline/rcg_pipeline/libs.py b/rcg_pipeline/rcg_pipeline/libs.py new file mode 100644 index 0000000..fa432ef --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/libs.py @@ -0,0 +1,208 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2021-2024 Robossembler LLC +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This file is part of Robossembler Framework +# project repo: https://gitlab.com/robossembler/framework +# +# Robossembler Framework 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Generate CG libs database for Robossembler Framework. +''' + +__version__ = '1.0' + +import logging +import json +import math +import os +import shutil + +import bpy + +from .utils.collection_tools import remove_collections_with_objects +from .utils.cleanup_orphan_data import cleanup_orphan_data +from .utils.object_transforms import apply_transforms +from .material_generators import material_generator, black_material +from .utils.object_converter import mesh_to_mesh +from .export import dae, fbx, glb, ply + +logger = logging.getLogger(__name__) + + +def generate_libs_database(project_dir): + ''' Generate Blender LIBS database from PARTs database. ''' + parts_data_name = [ + data for data in os.listdir(project_dir) + if data.endswith('_parts_data.json')] + if len(parts_data_name) != 1: + raise Exception( + 'No database! Only single supported! Check %s directory' % project_dir) + parts_data_path = os.path.join(project_dir, parts_data_name[0]) + + data_name = parts_data_name[0].split('_parts_data.json')[0] + + with open(parts_data_path, encoding='utf-8') as data: + parts_data = json.load(data) + + libs_data = [] + + # material libs + materials_dir = os.path.join(project_dir, 'libs', 'materials') + if os.path.exists(materials_dir): + shutil.rmtree(materials_dir) + os.makedirs(materials_dir) + else: + os.makedirs(materials_dir) + + for part in parts_data: + bpy.ops.wm.read_homefile() + obj = bpy.data.objects['Cube'] + if not part.get('material_path'): + continue + material_name = os.path.splitext(os.path.basename(part['material_path']))[0] + # do not regenerate exists material + material_in_libs = False + for lib in libs_data: + if lib['type'] == 'MATERIAL' and lib['name'] == material_name: + material_in_libs = True + break + if material_in_libs: + continue + # generate material + mat = material_generator(obj, os.path.join(project_dir, part['material_path'])) + bpy.data.materials[mat.name].use_fake_user = True + remove_collections_with_objects() + cleanup_orphan_data() + bpy.data.materials.remove(bpy.data.materials['Dots Stroke']) + bpy.ops.wm.previews_ensure() + + img = bpy.data.images.new(mat.name, *mat.preview.image_size, alpha=True) + img.pixels.foreach_set(mat.preview.image_pixels_float) + img_path = os.path.join(materials_dir, mat.name + '.png') + img.save(filepath=img_path) + + blend_path = os.path.join(materials_dir, mat.name + '.blend') + logger.info('Material stored as libs %s to %s.', mat.name, materials_dir) + bpy.ops.wm.save_as_mainfile(filepath=blend_path, compress=True) + + libs_data.append( + { + 'type': mat.rna_type.name.upper(), + 'name': mat.name, + 'path': os.path.relpath(blend_path, project_dir), + 'thumbnail': os.path.relpath(img_path, project_dir), + } + ) + + # object libs + objects_dir = os.path.join(project_dir, 'libs', 'objects') + if os.path.exists(objects_dir): + shutil.rmtree(objects_dir) + os.makedirs(objects_dir) + else: + os.makedirs(objects_dir) + + for part in parts_data: + bpy.ops.wm.read_homefile() + remove_collections_with_objects() + cleanup_orphan_data() + obj_collection = bpy.data.collections.new(part['name']) + bpy.context.scene.collection.children.link(obj_collection) + # set active to collection + active_collection = bpy.context.view_layer.layer_collection.children[part['name']] + bpy.context.view_layer.active_layer_collection = active_collection + # import stl file + bpy.ops.wm.stl_import( + filepath=os.path.join(project_dir, part['part_path']), + global_scale=0.001, + use_facet_normal=False, + forward_axis='Y', + up_axis='Z') + obj = bpy.data.objects.get(part['name']) + if not obj: + raise Exception('STL Import Fail for %s' % part['name']) + apply_transforms(obj, scale=True) + # setup material + if part.get('material_path'): + material_name = os.path.splitext(os.path.basename(part['material_path']))[0] + for lib in libs_data: + if lib['type'] == 'MATERIAL' and lib['name'] == material_name: + lib_path = os.path.join(project_dir, lib['path']) + bpy.ops.wm.link( + filepath=lib_path, + directory=os.path.join(lib_path, 'Material'), + filename=lib['name'], + relative_path=True, + do_reuse_local_id=True) + obj.data.materials.append(bpy.data.materials[lib['name']]) + else: + black_material(obj) + + with bpy.context.temp_override(selected_editable_objects=[obj]): + bpy.ops.object.shade_smooth_by_angle(angle=math.radians(12)) + obj.modifiers.new( + type='WEIGHTED_NORMAL', name='weighted_normal').keep_sharp = True + obj = mesh_to_mesh(obj) + + obj.asset_mark() + obj.asset_generate_preview() + img = bpy.data.images.new(part['name'], *obj.preview.image_size, alpha=True) + img.pixels.foreach_set(obj.preview.image_pixels_float) + img_path = os.path.join(objects_dir, part['name'] + '.png') + img.save(filepath=img_path) + obj.asset_clear() + + blend_path = os.path.join(objects_dir, part['name'] + '.blend') + logger.info('Object stored as libs %s to %s.', part['name'], objects_dir) + bpy.ops.wm.save_as_mainfile(filepath=blend_path, compress=True) + + libs_data.append( + { + 'type': obj.rna_type.name.upper(), + 'name': part['name'], + 'path': os.path.relpath(blend_path, project_dir), + 'dae': os.path.relpath( + dae.export(obj_name=part['name'], file_dir=objects_dir), + project_dir), + 'fbx': os.path.relpath( + fbx.export(obj_name=part['name'], file_dir=objects_dir, + axis_forward='-Z', axis_up='Y'), + project_dir), + 'glb': os.path.relpath( + glb.export(obj_name=part['name'], file_dir=objects_dir), + project_dir), + 'ply': os.path.relpath( + ply.export(obj_name=part['name'], file_dir=objects_dir), + project_dir), + 'thumbnail': os.path.relpath(img_path, project_dir), + } + ) + + # write db file + libs_data_path = os.path.join(project_dir, data_name + '_libs_data.json') + with open(libs_data_path, 'w', encoding='utf-8') as libs_data_file: + json.dump(libs_data, libs_data_file, ensure_ascii=False, indent=4) + logger.info('Database saved successfully to %s!', libs_data_path) + + return libs_data_path diff --git a/rcg_pipeline/rcg_pipeline/material_generators.py b/rcg_pipeline/rcg_pipeline/material_generators.py new file mode 100644 index 0000000..9f05cd2 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/material_generators.py @@ -0,0 +1,165 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2021-2024 Robossembler LLC +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This file is part of Robossembler Framework +# project repo: https://gitlab.com/robossembler/framework +# +# Robossembler Framework 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Blender material generation functions. +''' + +__version__ = '1.0' + +import configparser +import logging +import os + +import bpy +from bpy_extras.node_shader_utils import PrincipledBSDFWrapper + +from .utils.shininess_to_roughness import shiny_to_rough + +logger = logging.getLogger(__name__) + + +def black_material(obj): + ''' Generate absolute black body shader ''' + mat_name = 'Robossembler_Black_Body' + + if mat_name in bpy.data.materials: + # assign material to object + if len(obj.material_slots) < 1: + obj.data.materials.append(bpy.data.materials[mat_name]) + else: + obj.material_slots[0].material = bpy.data.materials[mat_name] + else: + bmat = bpy.data.materials.new(name=mat_name) + bmat.use_nodes = True + bmat.diffuse_color = (0, 0, 0, 1) + principled = bmat.node_tree.nodes['Principled BSDF'] + principled.inputs['Base Color'].default_value = (0, 0, 0, 1) + principled.inputs['Specular IOR Level'].default_value = 0.0 + principled.inputs['Roughness'].default_value = 1.0 + # assign material to object + if len(obj.material_slots) < 1: + obj.data.materials.append(bmat) + else: + obj.material_slots[0].material = bmat + + logger.debug('Material was assigned to object: %s -> %s', bmat.name, obj.name) + return bmat + + +def material_generator(obj, material_path): + ''' Generate shader from FreeCAD's FEM material ''' + material = configparser.ConfigParser() + with open(material_path, encoding='utf-8') as data: + material.read_file(data) + mat_name = os.path.splitext(os.path.basename(material_path))[0] + if mat_name != material['General']['Name']: + logger.warning( + 'Material %s is not supported! ' + 'For Robossembler CG Pipeline material should have ' + 'a same Name and FCMat filename!', + mat_name) + return black_material(obj) + + if mat_name in bpy.data.materials: + # assign material to object + if len(obj.material_slots) < 1: + obj.data.materials.append(bpy.data.materials[mat_name]) + else: + obj.material_slots[0].material = bpy.data.materials[mat_name] + + logger.debug('Material was assigned to object: %s -> %s', mat_name, obj.name) + return obj + + if 'Rendering' not in material: + logger.warning( + 'Material %s is not supported! ' + 'For Robossembler CG Pipeline material should have ' + 'Rendering category with shading parameters!', + mat_name) + return black_material(obj) + + rendering = material['Rendering'] + + if rendering.get('DiffuseColor'): + d_col_str = rendering['DiffuseColor'] + d_col4 = tuple(map(float, d_col_str[1:-1].split(', '))) + d_col = d_col4[:-1] + else: + logger.warning( + 'DiffuseColor not found for %s %s.', mat_name, obj.name) + d_col = (1.0, 0.0, 0.0) + if rendering.get('Father'): + if rendering['Father'] == 'Metal': + me = 1 + else: + me = 0 + else: + me = 0 + if rendering.get('Shininess'): + shiny = float(rendering['Shininess']) + if shiny == 0: + rg = 0.5 + else: + rg = shiny_to_rough(shiny) + else: + rg = 0.5 + if rendering.get('EmissiveColor'): + e_col_str = rendering['EmissiveColor'] + e_col4 = tuple(map(float, e_col_str[1:-1].split(', '))) + e_col = e_col4[:-1] + else: + e_col = (0.0, 0.0, 0.0) + if rendering.get('Transparency'): + tr_str = rendering['Transparency'] + alpha = 1.0 - float(tr_str) + else: + alpha = 1.0 + + bmat = bpy.data.materials.new(name=mat_name) + bmat.use_nodes = True + principled = PrincipledBSDFWrapper(bmat, is_readonly=False) + principled.base_color = d_col + principled.metallic = me + principled.roughness = rg + principled.emission_color = e_col + principled.alpha = alpha + bevel = bmat.node_tree.nodes.new(type="ShaderNodeBevel") + bevel.location = -300, -300 + bevel.samples = 32 + bevel.inputs[0].default_value = 0.001 + principled_node = bmat.node_tree.nodes["Principled BSDF"] + bmat.node_tree.links.new(bevel.outputs['Normal'], principled_node.inputs['Normal']) + # assign material to object + if len(obj.material_slots) < 1: + obj.data.materials.append(bmat) + else: + obj.material_slots[0].material = bmat + + logger.debug('Material was assigned to object: %s -> %s', bmat.name, obj.name) + return bmat diff --git a/rcg_pipeline/rcg_pipeline/render_asset.py b/rcg_pipeline/rcg_pipeline/render_asset.py new file mode 100644 index 0000000..f0d0806 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/render_asset.py @@ -0,0 +1,238 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2021-2024 Robossembler LLC +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This file is part of Robossembler Framework +# project repo: https://gitlab.com/robossembler/framework +# +# Robossembler Framework 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Generate render asset from assembly tree and CG libs database. +''' + +__version__ = '1.0' + +import logging +import json +import os +import random +import shutil + +import bpy +from mathutils import Vector, Matrix + +from .utils.collection_tools import remove_collections_with_objects +from .utils.cleanup_orphan_data import cleanup_orphan_data +from .utils.object_relations import unparenting, parenting +from .utils.object_transforms import round_transforms + +logger = logging.getLogger(__name__) + + +def recursive_layer_collection(layer_coll, coll_name): + found = None + if layer_coll.name == coll_name: + return layer_coll + for layer in layer_coll.children: + found = recursive_layer_collection(layer, coll_name) + if found: + return found + return False + + +def assembly_builder(item, libs_data, libs_data_dir, collection=None, parent=None): + ''' + ''' + if not collection: + collection = bpy.context.scene.collection + item_obj = None + if item['type'] == 'LOCATOR': + item_obj = bpy.data.objects.new(item['name'], None) + item_obj.empty_display_type = 'CUBE' + item_obj.empty_display_size = 0.01 + collection.objects.link(item_obj) + elif item['type'] == 'LCS': + item_obj = bpy.data.objects.new(item['name'], None) + item_obj.empty_display_type = 'ARROWS' + item_obj.empty_display_size = round(random.uniform(0.05, 0.15), 3) + item_obj.show_in_front = True + collection.objects.link(item_obj) + elif item['type'] == 'PART': + # link clones + if item.get('base_name'): + item_data = [ + data for data in libs_data + if data['type'] == 'OBJECT' + if data['name'] == item['base_name']] + if not item_data: + logger.error('No %s in database!', item['name']) + return False + # if there is local base_named object in scene -> rename it + local_obj = [ + loc for loc in bpy.data.objects + if loc.name == item['base_name'] + if not loc.library] + if local_obj: + local_obj[0].name += '_loc' + item_file_path = os.path.join(libs_data_dir, item_data[0]['path']) + # TODO already linked + bpy.ops.wm.link( + filepath=item_file_path, + directory=os.path.join(item_file_path, 'Collection'), + filename=item['base_name'], + relative_path=True, + do_reuse_local_id=True, + active_collection=True, + ) + item_obj = bpy.data.objects[item['base_name']] + item_obj.name = item['name'] + # rename local back + if local_obj: + local_obj[0].name = item['base_name'] + # link unical + else: + item_data = [ + data for data in libs_data + if data['type'] == 'OBJECT' + if data['name'] == item['name']] + if not item_data: + logger.error('No %s in database!', item['name']) + return False + item_file_path = os.path.join(libs_data_dir, item_data[0]['path']) + bpy.ops.wm.link( + filepath=item_file_path, + directory=os.path.join(item_file_path, 'Collection'), + filename=item['name'], + relative_path=True, + do_reuse_local_id=True, + active_collection=True, + ) + item_obj = bpy.data.objects[item['name']] + item_obj.empty_display_type = 'PLAIN_AXES' + item_obj.empty_display_size = 0.01 + else: + logger.error('Unknown object type %s of %s', item['type'], item['name']) + return False + item_obj.location = Vector(item['pose'][0]['loc_xyz']) * 0.001 + item_obj.rotation_mode = 'QUATERNION' + item_obj.rotation_quaternion = [ + item['pose'][1]['rot_xyzw'][3]] + item['pose'][1]['rot_xyzw'][:3] + + if item.get('attributes'): + for attr in item['attributes']: + item_obj[list(attr)[0]] = attr[list(attr)[0]] + + item_obj.parent = parent + + if item.get('children'): + for child_item in item.get('children'): + assembly_builder( + child_item, libs_data, libs_data_dir, collection, parent=item_obj) + + return True + + +def assembly_rebuilder(): + ''' Restructure assembling hierarchy. ''' + default_origin = [ + lcs for lcs in bpy.data.objects + if lcs.type == 'EMPTY' + if lcs.get('Robossembler_SocketFlow') == 'inlet' + if lcs.get('Robossembler_DefaultOrigin')] + if not default_origin: + raise Exception('Default Origin not found!') + if len(default_origin) > 1: + raise Exception('Several Default Origins do not supported!') + default_origin = default_origin[0] + + root_locator = [ + obj for obj in bpy.data.objects + if not obj.parent + if not obj.library] + for i in root_locator: + print(i.name) + if len(root_locator) > 1: + raise Exception('Render asset should consist of only one hierarchy!') + root_locator = root_locator[0] + + # retree_by lcs + for lcs in bpy.data.objects: + if lcs.type == 'EMPTY': + if lcs.get('Robossembler_SocketFlow'): + unparenting(lcs) + round_transforms(lcs) + + parenting(default_origin, root_locator) + + for lcs in bpy.data.objects: + if lcs.type == 'EMPTY': + if lcs.get('Robossembler_SocketFlow'): + if lcs != default_origin: + parenting(default_origin, lcs) + + default_origin.matrix_world = Matrix() + + logger.info('Restructuring assembling hierarchy finished!') + + return default_origin + + +def build_render_asset(parts_tree_path, libs_data_path, project_dir): + ''' + ''' + # start from stratch + bpy.ops.wm.read_homefile() + remove_collections_with_objects() + cleanup_orphan_data() + + with open(parts_tree_path, encoding='utf-8') as data: + parts_tree_item = json.load(data) + with open(libs_data_path, encoding='utf-8') as data: + libs_data = json.load(data) + + # create redner collection + render_collection = bpy.data.collections.new(parts_tree_item['name']) + bpy.context.scene.collection.children.link(render_collection) + active_collection = recursive_layer_collection( + bpy.context.view_layer.layer_collection, + render_collection.name) + bpy.context.view_layer.active_layer_collection = active_collection + + # build original hierarchy + assembly_builder(parts_tree_item, libs_data, project_dir, render_collection) + # rebuild to LCS hierarchy + assembly_rebuilder() + + # render assets dir + render_assets_dir = os.path.join(project_dir, 'assets', 'render') + if os.path.exists(render_assets_dir): + shutil.rmtree(render_assets_dir) + os.makedirs(render_assets_dir) + else: + os.makedirs(render_assets_dir) + + blend_path = os.path.join(render_assets_dir, parts_tree_item['name'] + '.blend') + bpy.ops.wm.save_as_mainfile(filepath=blend_path) + logger.info('Render asset %s generated!', parts_tree_item['name']) + + return blend_path diff --git a/rcg_pipeline/rcg_pipeline/utils/__init__.py b/rcg_pipeline/rcg_pipeline/utils/__init__.py new file mode 100644 index 0000000..062386f --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/__init__.py @@ -0,0 +1,28 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Blender utils. +''' diff --git a/rcg_pipeline/rcg_pipeline/utils/cleanup_orphan_data.py b/rcg_pipeline/rcg_pipeline/utils/cleanup_orphan_data.py new file mode 100644 index 0000000..c9555c9 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/cleanup_orphan_data.py @@ -0,0 +1,67 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Blender scene from stratch. +''' + +__version__ = '0.1' + +import bpy + + +def cleanup_orphan_data(): + '''Removes all data without users''' + for block in bpy.data.meshes: + if block.users == 0: + bpy.data.meshes.remove(block) + + for block in bpy.data.materials: + if block.users == 0: + bpy.data.materials.remove(block) + + for block in bpy.data.textures: + if block.users == 0: + bpy.data.textures.remove(block) + + for block in bpy.data.images: + if block.users == 0: + bpy.data.images.remove(block) + + for block in bpy.data.collections: + if block.users == 0: + bpy.data.collections.remove(block) + + for block in bpy.data.cameras: + if block.users == 0: + bpy.data.cameras.remove(block) + + for block in bpy.data.lights: + if block.users == 0: + bpy.data.lights.remove(block) + + for block in bpy.data.scenes: + if block.users == 0: + bpy.data.scenes.remove(block) diff --git a/rcg_pipeline/rcg_pipeline/utils/collection_tools.py b/rcg_pipeline/rcg_pipeline/utils/collection_tools.py new file mode 100644 index 0000000..702579e --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/collection_tools.py @@ -0,0 +1,92 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Various collection tools. +''' + +__version__ = '0.1' + +import bpy +from collections import defaultdict + + +def copy_objects(from_col, to_col, linked, double_lut): + '''Function copying objects from collection to collection.''' + for obj in from_col.objects: + double = obj.copy() + if not linked and obj.data: + double.data = double.data.copy() + to_col.objects.link(double) + double_lut[obj] = double + return True + + +def copy_collections_recursive(collection, suffix='copy', linked=False): + '''Function recursive copying collection.''' + double_lut = defaultdict(lambda: None) + parent = [p for p in (bpy.data.collections[:] + [bpy.context.scene.collection]) + if collection.name in p.children.keys()][0] + + def _copy(parent, collection, suffix, linked=False): + '''Function copying collection.''' + clone_collection = bpy.data.collections.new( + '_'.join((collection.name, suffix)) + ) + copy_objects(collection, clone_collection, linked, double_lut) + + for _collection in collection.children: + _copy(clone_collection, _collection, suffix, linked) + + parent.children.link(clone_collection) + + _copy(parent, collection, suffix, linked) + for obj, double in tuple(double_lut.items()): + parent = double_lut[obj.parent] + if parent: + double.parent = parent + return '_'.join((collection.name, suffix)) + + +def unlink_from_collections(obj): + ''' Unlinking object from all collections. ''' + for col in bpy.data.collections: + if obj.name in col.objects: + col.objects.unlink(obj) + return obj + + +def remove_collections_with_objects(collection=None): + '''Removes all collection (or given) with objects from scene ''' + if collection: + for obj in collection.objects: + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.collections.remove(collection) + else: + for col in bpy.data.collections: + for obj in col.objects: + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.collections.remove(col) + return True diff --git a/rcg_pipeline/rcg_pipeline/utils/generative_modifiers.py b/rcg_pipeline/rcg_pipeline/utils/generative_modifiers.py new file mode 100644 index 0000000..df9073d --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/generative_modifiers.py @@ -0,0 +1,173 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Basic mesh processing for asset pipeline. +DEPRECATED +''' + +__version__ = '0.1' + +import bpy + + +def shell_remesher(lowpoly_obj, mod_name='shell_mod', tree_name='shell_tree'): + ''' Conctruct geometry nodes modifier. ''' + + modifier = lowpoly_obj.modifiers.new(mod_name, type='NODES') + tree = bpy.data.node_groups.new(name=tree_name, type='GeometryNodeTree') + modifier.node_group = tree + + group_input = tree.nodes.new(type='NodeGroupInput') + + collection_info = tree.nodes.new(type='GeometryNodeCollectionInfo') + collection_info.location = (300, 0) + collection_info.transform_space = 'RELATIVE' + + tree.interface.new_socket(name='Collection', in_out='INPUT', + socket_type='NodeSocketCollection') + tree.links.new(group_input.outputs['Collection'], + collection_info.inputs['Collection']) + + realize_instances = tree.nodes.new(type='GeometryNodeRealizeInstances') + realize_instances.location = (600, 0) + + tree.links.new(collection_info.outputs[0], + realize_instances.inputs['Geometry']) + + mesh_to_volume = tree.nodes.new(type='GeometryNodeMeshToVolume') + mesh_to_volume.location = (900, 0) + mesh_to_volume.resolution_mode = 'VOXEL_SIZE' + mesh_to_volume.inputs['Density'].default_value = 10.0 + mesh_to_volume.inputs['Voxel Size'].default_value = 0.005 + #mesh_to_volume.inputs['Exterior Band Width'].default_value = 0.005 + + tree.links.new(realize_instances.outputs['Geometry'], + mesh_to_volume.inputs['Mesh']) + + volume_to_mesh = tree.nodes.new(type='GeometryNodeVolumeToMesh') + volume_to_mesh.location = (1200, 0) + + tree.links.new(mesh_to_volume.outputs['Volume'], + volume_to_mesh.inputs['Volume']) + + extrude_mesh = tree.nodes.new(type='GeometryNodeExtrudeMesh') + extrude_mesh.location = (1500, 0) + extrude_mesh.inputs['Offset Scale'].default_value = 0.001 + extrude_mesh.inputs['Individual'].default_value = False + + tree.links.new(volume_to_mesh.outputs['Mesh'], + extrude_mesh.inputs['Mesh']) + + # 1 pass + mesh_to_volume = tree.nodes.new(type='GeometryNodeMeshToVolume') + mesh_to_volume.location = (1800, 0) + mesh_to_volume.resolution_mode = 'VOXEL_SIZE' + mesh_to_volume.inputs['Density'].default_value = 1.0 + mesh_to_volume.inputs['Voxel Size'].default_value = 0.003 + #mesh_to_volume.inputs['Exterior Band Width'].default_value = 0.003 + + tree.links.new(extrude_mesh.outputs['Mesh'], + mesh_to_volume.inputs['Mesh']) + + volume_to_mesh = tree.nodes.new(type='GeometryNodeVolumeToMesh') + volume_to_mesh.location = (2100, 0) + + tree.links.new(mesh_to_volume.outputs['Volume'], + volume_to_mesh.inputs['Volume']) + + set_position_01 = tree.nodes.new(type='GeometryNodeSetPosition') + set_position_01.location = (2400, -300) + + tree.links.new(volume_to_mesh.outputs['Mesh'], + set_position_01.inputs['Geometry']) + + # 2 pass + mesh_to_volume = tree.nodes.new(type='GeometryNodeMeshToVolume') + mesh_to_volume.location = (2700, 0) + mesh_to_volume.resolution_mode = 'VOXEL_SIZE' + mesh_to_volume.inputs['Density'].default_value = 1.0 + mesh_to_volume.inputs['Voxel Size'].default_value = 0.001 + #mesh_to_volume.inputs['Exterior Band Width'].default_value = 0.001 + + tree.links.new(set_position_01.outputs['Geometry'], + mesh_to_volume.inputs['Mesh']) + + volume_to_mesh = tree.nodes.new(type='GeometryNodeVolumeToMesh') + volume_to_mesh.location = (3000, 0) + + tree.links.new(mesh_to_volume.outputs['Volume'], + volume_to_mesh.inputs['Volume']) + + set_position_02 = tree.nodes.new(type='GeometryNodeSetPosition') + set_position_02.location = (3300, -300) + + tree.links.new(volume_to_mesh.outputs['Mesh'], + set_position_02.inputs['Geometry']) + + # 3 pass + mesh_to_volume = tree.nodes.new(type='GeometryNodeMeshToVolume') + mesh_to_volume.location = (3600, 0) + mesh_to_volume.resolution_mode = 'VOXEL_SIZE' + mesh_to_volume.inputs['Density'].default_value = 1.0 + mesh_to_volume.inputs['Voxel Size'].default_value = 0.0005 + #mesh_to_volume.inputs['Exterior Band Width'].default_value = 0.0001 + + tree.links.new(set_position_02.outputs['Geometry'], + mesh_to_volume.inputs['Mesh']) + + volume_to_mesh = tree.nodes.new(type='GeometryNodeVolumeToMesh') + volume_to_mesh.location = (3900, 0) + + tree.links.new(mesh_to_volume.outputs['Volume'], + volume_to_mesh.inputs['Volume']) + + set_position_03 = tree.nodes.new(type='GeometryNodeSetPosition') + set_position_03.location = (4200, -300) + + tree.links.new(volume_to_mesh.outputs['Mesh'], + set_position_03.inputs['Geometry']) + + group_output = tree.nodes.new(type='NodeGroupOutput') + group_output.location = (4500, 0) + + tree.interface.new_socket(name='Geometry', in_out='OUTPUT', + socket_type='NodeSocketGeometry') + tree.links.new(set_position_03.outputs['Geometry'], + group_output.inputs['Geometry']) + + geometry_proximity = tree.nodes.new(type='GeometryNodeProximity') + geometry_proximity.location = (1200, -1000) + + tree.links.new(realize_instances.outputs['Geometry'], + geometry_proximity.inputs['Target']) + tree.links.new(geometry_proximity.outputs['Position'], + set_position_01.inputs['Position']) + tree.links.new(geometry_proximity.outputs['Position'], + set_position_02.inputs['Position']) + tree.links.new(geometry_proximity.outputs['Position'], + set_position_03.inputs['Position']) + + return modifier diff --git a/rcg_pipeline/rcg_pipeline/utils/mesh_tools.py b/rcg_pipeline/rcg_pipeline/utils/mesh_tools.py new file mode 100644 index 0000000..6d8e075 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/mesh_tools.py @@ -0,0 +1,118 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Various mesh tools for Edit Mode. +''' + +__version__ = '0.1' + +import math +import bpy +import bmesh + + +def select_peaks(me, peak_limit_angle=60, peak_accuracy_angle=10): + ''' Select sharp vertices that stand alone. ''' + bm = bmesh.from_edit_mesh(me) + + def is_sharp(vert, eps=math.radians(peak_limit_angle)): + sharps = [] + face_before = None + for face in vert.link_faces: + if face_before: + face_angle = face.normal.angle(face_before.normal) + if face_angle > math.radians(peak_accuracy_angle): + angle = vert.normal.angle(face.normal) + if angle > eps: + sharps.append(angle) + face_before = face + return ( + (len(sharps) + 1) == len(vert.link_faces) + or (len(sharps) + 2) == len(vert.link_faces) + ) + + def non_single(vert): + for edge in vert.link_edges: + if edge.other_vert(vert).select: + return False + return True + + for v in bm.verts: + v.select_set( + is_sharp(v) + ) + + for v in bm.verts: + if v.select: + v.select_set( + non_single(v) + ) + + bmesh.update_edit_mesh(me) + return me + + +def select_zero_faces(me): + ''' Select very small faces. ''' + bm = bmesh.from_edit_mesh(me) + for myface in bm.faces: + if myface.calc_area() < 1e-7: + myface.select_set(True) + bmesh.update_edit_mesh(me) + return me + + +def select_stratched_edges(me, edge_length_limit=0.002): + ''' Select very stratched edges of small faces. ''' + bm = bmesh.from_edit_mesh(me) + faces_stratched = [f for f in bm.faces if f.calc_area() < 1e-6] + for face in faces_stratched: + edges_lengths = {e: e.calc_length() for e in face.edges} + edge_max_length = max(edges_lengths.values()) + if edge_max_length > edge_length_limit: + edge_max = [k for k, v in edges_lengths.items() if v == edge_max_length][0] + edge_max.select_set(True) + bmesh.update_edit_mesh(me) + return me + + +def collect_less_volume_objs(objs: list, min_volume): + ''' Separate selection for less volume objects. ''' + less_volume_objs = [] + for obj in objs: + bpy.ops.object.select_all(action='DESELECT') + if obj.type != 'MESH': + continue + # requed for bmesh + obj.hide_set(False) + obj.select_set(state=True) + if obj.type == 'MESH': + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + if bm.calc_volume() < min_volume: + less_volume_objs.append(obj) + bpy.ops.object.mode_set(mode='OBJECT') + return less_volume_objs diff --git a/rcg_pipeline/rcg_pipeline/utils/object_converter.py b/rcg_pipeline/rcg_pipeline/utils/object_converter.py new file mode 100644 index 0000000..61a30d1 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/object_converter.py @@ -0,0 +1,53 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Convert all deformers and modifiers of object to it's mesh. +''' + +__version__ = '0.1' + +import bpy + + +def mesh_to_mesh(obj): + ''' Convert all deformers and modifiers of object to it's mesh. ''' + if obj and obj.type == 'MESH': + deg = bpy.context.evaluated_depsgraph_get() + eval_mesh = obj.evaluated_get(deg).data.copy() + + orig_name = obj.name + obj.name = '{}_temp'.format(orig_name) + converted_obj = bpy.data.objects.new(orig_name, eval_mesh) + converted_obj.matrix_world = obj.matrix_world + + bpy.context.view_layer.update() + converted_obj.matrix_world = obj.matrix_world.copy() + + obj.users_collection[0].objects.link(converted_obj) + + bpy.data.objects.remove(obj, do_unlink=True) + + return converted_obj diff --git a/rcg_pipeline/rcg_pipeline/utils/object_relations.py b/rcg_pipeline/rcg_pipeline/utils/object_relations.py new file mode 100644 index 0000000..06b45e8 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/object_relations.py @@ -0,0 +1,50 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Blender object relations functions. +''' + +__version__ = '0.1' + +import bpy + + +def parenting(parent, child): + ''' Parenting child object to parent object. ''' + child.parent = parent + child.matrix_parent_inverse = parent.matrix_world.inverted() + return child + + +def unparenting(child): + ''' Unarenting child object from parent object. ''' + # update database + bpy.context.view_layer.update() + + world_matrix = child.matrix_world.copy() + child.parent = None + child.matrix_world = world_matrix + return child diff --git a/rcg_pipeline/rcg_pipeline/utils/object_transforms.py b/rcg_pipeline/rcg_pipeline/utils/object_transforms.py new file mode 100644 index 0000000..f2119dc --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/object_transforms.py @@ -0,0 +1,88 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Blender object transforms functions. +''' + +__version__ = '0.3' + +from mathutils import Matrix, Vector, Quaternion +import bpy + + +def apply_transforms(obj, location=False, rotation=False, scale=False): + ''' bake local object transforms ''' + # original idea from https://github.com/machin3io/MACHIN3tools + # update database + bpy.context.view_layer.update() + + def get_loc_matrix(location): + return Matrix.Translation(location) + + def get_rot_matrix(rotation): + return rotation.to_matrix().to_4x4() + + def get_sca_matrix(scale): + scene_scale_martix = Matrix() + for i in range(3): + scene_scale_martix[i][i] = scale[i] + return scene_scale_martix + + if location and rotation and scale: + loc, rot, sca = obj.matrix_world.decompose() + mesh_martix = get_loc_matrix(loc) @ get_rot_matrix( + rot) @ get_sca_matrix(sca) + obj.data.transform(mesh_martix) + apply_matrix = get_loc_matrix(Vector.Fill(3, 0)) @ get_rot_matrix(Quaternion()) @ get_sca_matrix(Vector.Fill(3, 1)) + obj.matrix_world = apply_matrix + else: + if location: + raise Exception( + 'Location only applies with all transformations (rotate and scale) together!' + ) + if rotation: + loc, rot, sca = obj.matrix_world.decompose() + mesh_martix = get_rot_matrix(rot) + obj.data.transform(mesh_martix) + apply_matrix = get_loc_matrix(loc) @ get_rot_matrix(Quaternion()) @ get_sca_matrix(sca) + obj.matrix_world = apply_matrix + + if scale: + loc, rot, sca = obj.matrix_world.decompose() + mesh_martix = get_sca_matrix(sca) + obj.data.transform(mesh_martix) + apply_matrix = get_loc_matrix(loc) @ get_rot_matrix(rot) @ get_sca_matrix(Vector.Fill(3, 1)) + obj.matrix_world = apply_matrix + + obj.rotation_mode = 'XYZ' + return obj + + +def round_transforms(obj): + ''' Geting location of object and round it. ''' + for idx, axis in enumerate(obj.location[:]): + obj.location[idx] = round(axis, 5) + return obj.location diff --git a/rcg_pipeline/rcg_pipeline/utils/shininess_to_roughness.py b/rcg_pipeline/rcg_pipeline/utils/shininess_to_roughness.py new file mode 100644 index 0000000..2bfa837 --- /dev/null +++ b/rcg_pipeline/rcg_pipeline/utils/shininess_to_roughness.py @@ -0,0 +1,42 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# original alg from https://github.com/assimp/assimp/issues/4573 +# Copyright (C) 2024 Ilia Kurochkin +# +# Created by Ilia Kurochkin (brothermechanic) +# contact: brothermechanic@yandex.com +# +# This 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 . +# +# ***** END GPL LICENSE BLOCK ***** +# +# coding: utf-8 +''' +DESCRIPTION. +Math function. +''' + +__version__ = '0.1' + +import math + + +def shiny_to_rough(shininess): + ''' convert shiny to roughness ''' + a, b = -1.0, 2.0 + c = (shininess / 100.0) - 1.0 + D = math.pow(b, 2) - (4 * a * c) + roughness = (-b + math.sqrt(D)) / (2 * a) + return max(roughness, 0)