From 839ce36c70e0a48d82c32c5b6fe17c5634c8ee07 Mon Sep 17 00:00:00 2001 From: brothermechanic Date: Sat, 24 Jun 2023 13:54:49 +0000 Subject: [PATCH] [Blender] Implemented tesselation for CAD-model, retopology and optimisation tesselation's result, assigning physical properties with assigned material --- cg/blender/import_fcstd/__init__.py | 7 +- cg/blender/import_fcstd/handler.py | 74 ----- cg/blender/import_fcstd/import_cad_objects.py | 269 +++++++++--------- .../import_fcstd/import_coordinate_point.py | 44 +-- cg/blender/import_fcstd/import_hierarchy.py | 60 ++-- cg/blender/import_fcstd/import_materials.py | 45 ++- cg/blender/import_fcstd/is_object_solid.py | 10 +- .../import_fcstd/restruct_hierarchy_by_lcs.py | 158 ++++++++++ cg/blender/remesh/__init__.py | 65 +---- cg/blender/remesh/highpoly_setup.py | 72 +++++ cg/blender/remesh/lowpoly_setup.py | 86 ++++++ cg/blender/utils/cleanup_orphan_data.py | 4 +- cg/blender/utils/generative_modifiers.py | 161 +++++++++++ cg/blender/utils/object_converter.py | 40 +++ cg/blender/utils/object_relations.py | 33 +++ ...ply_transforms.py => object_transforms.py} | 24 +- cg/blender/utils/remove_collections.py | 4 +- cg/blender/utils/shininess_to_roughness.py | 4 +- cg/pipeline/freecad_to_asset.py | 82 ++++-- 19 files changed, 891 insertions(+), 351 deletions(-) delete mode 100644 cg/blender/import_fcstd/handler.py create mode 100644 cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py create mode 100644 cg/blender/remesh/highpoly_setup.py create mode 100644 cg/blender/remesh/lowpoly_setup.py create mode 100644 cg/blender/utils/generative_modifiers.py create mode 100644 cg/blender/utils/object_converter.py create mode 100644 cg/blender/utils/object_relations.py rename cg/blender/utils/{apply_transforms.py => object_transforms.py} (80%) diff --git a/cg/blender/import_fcstd/__init__.py b/cg/blender/import_fcstd/__init__.py index b64814f..f4bced8 100644 --- a/cg/blender/import_fcstd/__init__.py +++ b/cg/blender/import_fcstd/__init__.py @@ -1,2 +1,7 @@ # -*- coding: utf-8 -*- -__version__ = "0.1" +''' +DESCRIPTION. +Tesselate and import FreeCAD parts as meshes to Bledner scene. +For asset creation pipeline. +''' + diff --git a/cg/blender/import_fcstd/handler.py b/cg/blender/import_fcstd/handler.py deleted file mode 100644 index 8a15406..0000000 --- a/cg/blender/import_fcstd/handler.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# Original code by (C) 2019 yorikvanhavre -# Copyright (C) 2023 Ilia Kurochkin -# -# This program is free software; you can redistribute it and/or modify -# 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. - -import xml.sax - - -class FreeCAD_xml_handler(xml.sax.ContentHandler): - - """A XML handler to process the FreeCAD GUI xml data""" - - # this creates a dictionary where each key is a FC object name, - # and each value is a dictionary of property:value pairs - - def __init__(self): - - self.guidata = {} - self.current = None - self.properties = {} - self.currentprop = None - self.currentval = None - - # Call when an element starts - - def startElement(self, tag, attributes): - - if tag == "ViewProvider": - self.current = attributes["name"] - elif tag == "Property": - name = attributes["name"] - if name in ["Visibility","ShapeColor","Transparency","DiffuseColor"]: - self.currentprop = name - elif tag == "Bool": - if attributes["value"] == "true": - self.currentval = True - else: - self.currentval = False - elif tag == "PropertyColor": - c = int(attributes["value"]) - r = float((c>>24)&0xFF)/255.0 - g = float((c>>16)&0xFF)/255.0 - b = float((c>>8)&0xFF)/255.0 - self.currentval = (r,g,b) - elif tag == "Integer": - self.currentval = int(attributes["value"]) - elif tag == "Float": - self.currentval = float(attributes["value"]) - elif tag == "ColorList": - self.currentval = attributes["file"] - - # Call when an elements ends - - def endElement(self, tag): - - if tag == "ViewProvider": - if self.current and self.properties: - self.guidata[self.current] = self.properties - self.current = None - self.properties = {} - elif tag == "Property": - if self.currentprop and (self.currentval != None): - self.properties[self.currentprop] = self.currentval - self.currentprop = None - self.currentval = None diff --git a/cg/blender/import_fcstd/import_cad_objects.py b/cg/blender/import_fcstd/import_cad_objects.py index ab88c1d..c997f8c 100644 --- a/cg/blender/import_fcstd/import_cad_objects.py +++ b/cg/blender/import_fcstd/import_cad_objects.py @@ -11,9 +11,21 @@ # 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. -__version__ = "0.2" +''' +DESCRIPTION. +Main module: +- Reads a FreeCAD .FCStd file. +- Set tesselation parts to mesh. +- Inport meshes in Blender scene. +- Setup hierarchy. +- Setup materials. +- Setup LCS points. +- Apply FreeCAD to Bledner scene transforms. +''' +__version__ = '0.3' import time import FreeCAD +import Part, Mesh, MeshPart import logging import math import xml @@ -21,174 +33,173 @@ import sys import xml.sax import zipfile import os +import random import bpy -from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -from import_fcstd.handler import FreeCAD_xml_handler -from import_fcstd.import_hierarchy import import_hierarchy -from import_fcstd.import_materials import import_materials +from import_fcstd.import_hierarchy import (hierarchy, + placement) +from import_fcstd.import_materials import (assign_materials, + assign_black) from import_fcstd.is_object_solid import is_object_solid - +from utils.object_transforms import apply_transforms logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Import Parts' +lcs_col_name = 'Import LCS' +hierarchy_col_name = 'Import Hierarchy' +lowpoly_col_name = 'Lowpoly Parts' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' + + def obj_importer(filename, linear_deflection, angular_deflection, update=False, - placement=True, + scene_placement=True, skiphidden=True, scale=0.001, select=True, - report=None): + nonsolid_property = 'Robossembler_NonSolid'): - """Reads a FreeCAD .FCStd file and creates Blender objects""" - - guidata = {} - zdoc = zipfile.ZipFile(filename) - if zdoc: - if "GuiDocument.xml" in zdoc.namelist(): - gf = zdoc.open("GuiDocument.xml") - guidata = gf.read() - gf.close() - Handler = FreeCAD_xml_handler() - xml.sax.parseString(guidata, Handler) - guidata = Handler.guidata - for key,properties in guidata.items(): - # open each diffusecolor files and retrieve values - # first 4 bytes are the array length, then each group of 4 bytes is abgr - if "DiffuseColor" in properties: - #logger.debug ("opening:",guidata[key]["DiffuseColor"]) - df = zdoc.open(guidata[key]["DiffuseColor"]) - buf = df.read() - #logger.debug (buf," length ",len(buf)) - df.close() - cols = [] - for i in range(1,int(len(buf)/4)): - cols.append((buf[i*4+3],buf[i*4+2],buf[i*4+1],buf[i*4])) - guidata[key]["DiffuseColor"] = cols - zdoc.close() + ''' Reads a FreeCAD .FCStd file and creates Blender objects ''' doc = FreeCAD.open(filename) docname = doc.Name - if not doc: - logger.debug("Unable to open the given FreeCAD file") - if report: - report({'ERROR'},"Unable to open the given FreeCAD file") - return {'CANCELLED'} - # import some FreeCAD modules needed below. After "import FreeCAD" these modules become available - import Part, Mesh, MeshPart + if not update: + parts_collection = bpy.data.collections.new(parts_col_name) + bpy.context.scene.collection.children.link(parts_collection) - def hascurves(shape): + lcs_collection = bpy.data.collections.new(lcs_col_name) + bpy.context.scene.collection.children.link(lcs_collection) - for e in shape.Edges: - if not isinstance(e.Curve,(Part.Line,Part.LineSegment)): - return True - return False - - parts_collection = bpy.data.collections.new("Import Parts") - bpy.context.scene.collection.children.link(parts_collection) + hierarchy_collection = bpy.data.collections.new(hierarchy_col_name) + bpy.context.scene.collection.children.link(hierarchy_collection) # collect all materials fem_mats = [] for fem_mat in doc.Objects: - if fem_mat.isDerivedFrom("App::MaterialObjectPython"): + if fem_mat.isDerivedFrom('App::MaterialObjectPython'): fem_mats.append(fem_mat) - + bobjs = [] + bobjs_for_render = [] for obj in doc.Objects: - # logger.debug("Importing",obj.Label) + bobj = None if skiphidden: - if obj.Name in guidata: - if "Visibility" in guidata[obj.Name]: - if guidata[obj.Name]["Visibility"] == False: - # logger.debug(obj.Label,"is invisible. Skipping.") - # TODO add parent visibility check - continue + if not obj.Visibility: + continue - # process simple parts only - if not obj.isDerivedFrom("Part::Feature"): - logger.debug('%s is not simple part', obj.Label) - continue - - # process solids only - if not is_object_solid(obj): - logger.debug('%s is not solid', obj.Label) - continue - - verts = [] - edges = [] - faces = [] - - # create mesh from shape - shape = obj.Shape - if placement: - placement = obj.Placement - shape = obj.Shape.copy() - shape.Placement = placement.inverse().multiply(shape.Placement) - meshfromshape = doc.addObject("Mesh::Feature","Mesh") - meshfromshape.Mesh = MeshPart.meshFromShape( - Shape=shape, - LinearDeflection=linear_deflection, - AngularDeflection=math.radians(angular_deflection), - Relative=False) - - t = meshfromshape.Mesh.Topology - verts = [[v.x,v.y,v.z] for v in t[0]] - faces = t[1] - - if verts and faces: - # create or update object with mesh and material data - bobj = None - bmat = None + if obj.isDerivedFrom('PartDesign::CoordinateSystem'): if update: - # locate existing object (mesh with same name) + # locate existing object for o in bpy.data.objects: - if o.data.name == obj.Name: + if o.name == obj.Label: bobj = o logger.debug('Replacing existing %s', obj.Label) - bmesh = bpy.data.meshes.new(name=obj.Name) - bmesh.from_pydata(verts, edges, faces) - bmesh.update() - if bobj: - logger.debug('Updating the mesh of existing %s', obj.Label) - # update only the mesh of existing object. Don't touch materials - bobj.data = bmesh else: - # create new object - bobj = bpy.data.objects.new(obj.Label, bmesh) - if placement: - bobj.location = placement.Base.multiply(scale) - m = bobj.rotation_mode - bobj.rotation_mode = 'QUATERNION' - if placement.Rotation.Angle: - # FreeCAD Quaternion is XYZW while Blender is WXYZ - q = (placement.Rotation.Q[3],)+placement.Rotation.Q[:3] - bobj.rotation_quaternion = (q) - bobj.rotation_mode = m - bobj.scale = (scale, scale, scale) + bobj = bpy.data.objects.new(obj.Label, None) + bobj.empty_display_type = 'ARROWS' + bobj.empty_display_size = round(random.uniform(0.05, 0.15), 3) + bobj.show_in_front = True + lcs_collection.objects.link(bobj) - # one material for the whole object - for fem_mat in fem_mats: - for ref in fem_mat.References: - if ref[0].Label == bobj.name: - import_materials(bobj, fem_mat) + elif obj.isDerivedFrom('Part::Feature'): + # filter for nonsolids + if is_object_solid(obj) or hasattr(obj, nonsolid_property): + verts = [] + edges = [] + faces = [] + # create mesh from shape + shape = obj.Shape + if scene_placement: + shape = obj.Shape.copy() + shape.Placement = obj.Placement.inverse().multiply(shape.Placement) + meshfromshape = doc.addObject('Mesh::Feature','Mesh') + meshfromshape.Mesh = MeshPart.meshFromShape( + Shape=shape, + LinearDeflection=linear_deflection, + AngularDeflection=math.radians(angular_deflection), + Relative=False) + t = meshfromshape.Mesh.Topology + verts = [[v.x,v.y,v.z] for v in t[0]] + faces = t[1] - parts_collection.objects.link(bobj) + if verts and faces: + # create or update object with mesh and material data + bmesh = bpy.data.meshes.new(name=obj.Label) + bmesh.from_pydata(verts, edges, faces) + bmesh.update() + if update: + # locate existing object + for o in bpy.data.objects: + if o.name == obj.Label: + bobj = o + bobj.data = bmesh + logger.debug('Replacing existing %s', obj.Label) + else: + bobj = bpy.data.objects.new(obj.Label, bmesh) + parts_collection.objects.link(bobj) + + # skip for other object's types + else: + continue + + if bobj and scene_placement and not update: + placement(bobj, obj, scale) + if bobj.type == 'MESH': + bobj.scale = (scale, scale, scale) + apply_transforms(bobj, scale=True) # construct assembly hierarchy - obj_parent = obj.getParentGeoFeatureGroup() - if obj_parent: - import_hierarchy(obj, bobj, scale) + hierarchy_objs = hierarchy(bobj, obj, scale) + for hierarchy_obj in hierarchy_objs: + hierarchy_collection.objects.link(hierarchy_obj) - if select: - bpy.context.view_layer.objects.active = bobj - bobj.select_set(True) + # one material for the whole object + if bobj.type == 'MESH': + for fem_mat in fem_mats: + for ref in fem_mat.References: + if ref[0].Label == bobj.name: + assign_materials(bobj, fem_mat) + bobjs_for_render.append(bobj) + continue + # looks like this is hidden internal object + if not bobj.material_slots: + assign_black(bobj) + + # optional select object after importing + if bobj and select: + bpy.context.view_layer.objects.active = bobj + bobj.select_set(True) + + bobjs.append(bobj) + + # losted root lcs inlet workaround + lcs_objects = lcs_collection.objects + root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)][0] + root_inlet_name = ('{}{}'.format(root_lcs.name.split(root)[0], inlet)) + if not bpy.data.objects.get(root_inlet_name): + root_inlet = bpy.data.objects.new(root_inlet_name, None) + root_inlet.empty_display_type = 'ARROWS' + root_inlet.empty_display_size = 0.1 + root_inlet.show_in_front = True + root_inlet.location = root_lcs.location + root_inlet.rotation_euler = root_lcs.rotation_euler + root_inlet.parent = root_lcs.parent + lcs_collection.objects.link(root_inlet) FreeCAD.closeDocument(docname) # TODO # update do not dork - - logger.info("Import freecad scene finished without errors") - return {'FINISHED'} + logger.info('Imported %s objects without errors', len(bobjs)) + return bobjs_for_render diff --git a/cg/blender/import_fcstd/import_coordinate_point.py b/cg/blender/import_fcstd/import_coordinate_point.py index c683ff9..463180a 100644 --- a/cg/blender/import_fcstd/import_coordinate_point.py +++ b/cg/blender/import_fcstd/import_coordinate_point.py @@ -11,9 +11,11 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -#DESCRIPTION. -# Import from json exported FreeCAD's asm4 coordinates as Blender's empty object. -__version__ = "0.1" +''' +DESCRIPTION. +Import from json exported FreeCAD's asm4 coordinates as Blender's empty object. +''' +__version__ = '0.2' import logging import bpy @@ -23,19 +25,20 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def empty_importer(path_json): +def lcs_json_importer(path_json): + ''' Import json LCS as Bledner's Empty object. ''' with open(path_json) as f: data = json.load(f) - pivot_name = data['label'] - pivot_parent_name = data['parent_label'] - pivot_pose = data['placement'] - loc = tuple(pivot_pose['position'].values()) - fori = tuple(pivot_pose['orientation'].values()) + lcs_name = data['label'] + lcs_parent_name = data['parent_label'] + lcs_pose = data['placement'] + loc = tuple(lcs_pose['position'].values()) + fori = tuple(lcs_pose['orientation'].values()) bori = (fori[3],)+fori[:3] if not bpy.data.collections.get('Import LCS'): - lcs_collection = bpy.data.collections.new("Import LCS") + lcs_collection = bpy.data.collections.new('Import LCS') bpy.context.scene.collection.children.link(lcs_collection) bpy.context.view_layer.active_layer_collection = \ bpy.context.view_layer.layer_collection.children['Import LCS'] @@ -45,16 +48,17 @@ def empty_importer(path_json): bpy.ops.object.empty_add( type='ARROWS', radius=0.1, align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0)) - pivot_obj = bpy.context.active_object # or bpy.context.object - pivot_obj.name = pivot_name - pivot_obj.rotation_mode = 'QUATERNION' - pivot_obj.location = loc - pivot_obj.rotation_quaternion = bori - pivot_obj.rotation_mode = 'XYZ' - pivot_obj.show_in_front = True + lcs_obj = bpy.context.active_object # or bpy.context.object + lcs_obj.name = lcs_name + lcs_obj.rotation_mode = 'QUATERNION' + lcs_obj.location = loc + lcs_obj.rotation_quaternion = bori + lcs_obj.rotation_mode = 'XYZ' + lcs_obj.show_in_front = True - if pivot_parent_name: - pivot_obj.parent = bpy.data.objects[pivot_parent_name] + if lcs_parent_name: + lcs_obj.parent = bpy.data.objects[lcs_parent_name] f.close() - logger.info('Point %s imported without errors', pivot_name) + logger.info('Point %s imported without errors', lcs_name) + return lcs_obj diff --git a/cg/blender/import_fcstd/import_hierarchy.py b/cg/blender/import_fcstd/import_hierarchy.py index ab1e9d7..1879773 100644 --- a/cg/blender/import_fcstd/import_hierarchy.py +++ b/cg/blender/import_fcstd/import_hierarchy.py @@ -10,8 +10,11 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# DESCRIPTION. -# Collecting all parents and reconstruct this hierarhy in bledner. +''' +DESCRIPTION. +Collecting all parents and reconstruct this hierarhy in bledner. +''' +__version__ = '0.2' import logging import bpy @@ -19,42 +22,39 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def import_hierarchy(fc_obj, b_obj, scale): - """FreeCAD object, Blender object, scene scale""" +def placement(bobj, obj, scale): + ''' blender object, freecad object, scale factor ''' + bobj.location = obj.Placement.Base.multiply(scale) + m = bobj.rotation_mode + bobj.rotation_mode = 'QUATERNION' + if obj.Placement.Rotation.Angle: + # FreeCAD Quaternion is XYZW while Blender is WXYZ + q = (obj.Placement.Rotation.Q[3],)+obj.Placement.Rotation.Q[:3] + bobj.rotation_quaternion = (q) + bobj.rotation_mode = m + return bobj - if not bpy.data.collections.get('Import Hierarchy'): - hierarchy_collection = bpy.data.collections.new("Import Hierarchy") - bpy.context.scene.collection.children.link(hierarchy_collection) - bpy.context.view_layer.active_layer_collection = \ - bpy.context.view_layer.layer_collection.children['Import Hierarchy'] - else: - hierarchy_collection = bpy.data.collections['Import Hierarchy'] - obj_parent = fc_obj.getParentGeoFeatureGroup() +def hierarchy(bobj, obj, scale): + ''' blender object, freecad object, scale factor ''' + obj_parent = obj.getParentGeoFeatureGroup() obj_child_name = None + parents = [] while obj_parent: - if hierarchy_collection.objects.get(obj_parent.Label): + if bpy.data.objects.get(obj_parent.Label): empty = bpy.data.objects[obj_parent.Label] else: - bpy.ops.object.empty_add( - type='CUBE', radius=0.01, align='WORLD', - location=(0, 0, 0), rotation=(0, 0, 0)) - empty = bpy.data.objects['Empty'] - empty.name = obj_parent.Label - placement = obj_parent.Placement - empty.location = placement.Base.multiply(scale) - rm = empty.rotation_mode - if placement.Rotation.Angle: - empty.rotation_mode = 'QUATERNION' - q = (placement.Rotation.Q[3],)+placement.Rotation.Q[:3] - empty.rotation_quaternion = (q) - empty.rotation_mode = rm - if not b_obj.parent: - b_obj.parent = empty + empty = bpy.data.objects.new(obj_parent.Label, None) + empty.empty_display_type = 'CUBE' + empty.empty_display_size = 0.01 + placement(empty, obj_parent, scale) + parents.append(empty) + if not bobj.parent: + bobj.parent = empty else: bpy.data.objects[obj_child_name].parent = empty obj_child_name = obj_parent.Label obj_parent = obj_parent.getParentGeoFeatureGroup() empty.select_set(False) - logger.debug('Add parent %s to object %s', empty.name, b_obj.name) - + logger.debug('Add parent %s to object %s', empty.name, bobj.name) + return parents diff --git a/cg/blender/import_fcstd/import_materials.py b/cg/blender/import_fcstd/import_materials.py index 7f2e50b..08d6c48 100644 --- a/cg/blender/import_fcstd/import_materials.py +++ b/cg/blender/import_fcstd/import_materials.py @@ -10,6 +10,7 @@ # 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. +__version__ = '0.2' import logging import sys import bpy @@ -20,11 +21,12 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def import_materials(bobj, fem_mat): - """ Build Blender Shader from FreeCAD's FEM material """ +def assign_materials(bobj, fem_mat): + ''' Build Blender shader from FreeCAD's FEM material ''' fem_mat_name = fem_mat.Material['Name'] if fem_mat_name in bpy.data.materials: + # prepare for reimport if len(bobj.material_slots) < 1: bobj.data.materials.append(bpy.data.materials[fem_mat_name]) else: @@ -73,6 +75,43 @@ def import_materials(bobj, fem_mat): principled.roughness = rg principled.emission_color = e_col principled.alpha = alpha - bobj.data.materials.append(bmat) + # prepare for reimport + if len(bobj.material_slots) < 1: + bobj.data.materials.append(bmat) + else: + bobj.material_slots[0].material = bmat logger.debug('Assign %s to object %s', fem_mat_name, bobj.name) + return bobj + + +def assign_black(bobj): + ''' Set absolute black Blender shader ''' + fem_mat_name = 'black_mat' + + if fem_mat_name in bpy.data.materials: + # prepare for reimport + if len(bobj.material_slots) < 1: + bobj.data.materials.append(bpy.data.materials[fem_mat_name]) + else: + bobj.material_slots[0].material = bpy.data.materials[fem_mat_name] + else: + bmat = bpy.data.materials.new(name=fem_mat_name) + bmat.use_nodes = True + bmat.diffuse_color = (0, 0, 0, 1) + bmat.node_tree.nodes.remove(bmat.node_tree.nodes['Principled BSDF']) + emission = bmat.node_tree.nodes.new(type='ShaderNodeEmission') + emission.location = 0, 300 + emission.inputs['Color'].default_value = (0, 0, 0, 1) + emission.inputs['Strength'].default_value = 0 + bmat.node_tree.links.new( + emission.outputs['Emission'], + bmat.node_tree.nodes['Material Output'].inputs['Surface']) + # prepare for reimport + if len(bobj.material_slots) < 1: + bobj.data.materials.append(bmat) + else: + bobj.material_slots[0].material = bmat + + logger.debug('Assign %s to object %s', fem_mat_name, bobj.name) + return bobj diff --git a/cg/blender/import_fcstd/is_object_solid.py b/cg/blender/import_fcstd/is_object_solid.py index 10ee07a..7158ef2 100644 --- a/cg/blender/import_fcstd/is_object_solid.py +++ b/cg/blender/import_fcstd/is_object_solid.py @@ -10,14 +10,16 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# -#DESCRIPTION. -# Simple FreeCAD's object test for manifold mawater-tight surface. +''' +DESCRIPTION. +Simple FreeCAD's object test for manifold mawater-tight surface. +''' +__version__ = '0.2' import FreeCAD def is_object_solid(obj): - """If obj is solid return True""" + '''If obj is solid return True''' if not isinstance(obj, FreeCAD.DocumentObject): return False diff --git a/cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py b/cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py new file mode 100644 index 0000000..d641c75 --- /dev/null +++ b/cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Reorganization and restructuring of assembly structure +based on LCS point objects. +''' +__version__ = '0.1' +import logging +import math + +import bpy +from mathutils import Matrix + +from utils.object_relations import (parenting, + unparenting) +from utils.object_transforms import round_transforms + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Import Parts' +lcs_col_name = 'Import LCS' +hierarchy_col_name = 'Import Hierarchy' +lowpoly_col_name = 'Lowpoly Parts' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' + + +def retree_by_lcs(lcs_objects, root_lcs): + ''' Organizing project structure based on LCS. ''' + for lcs in lcs_objects: + locator = lcs.parent + if lcs.name.endswith(inlet): + unparenting(lcs) + round_transforms(lcs) + if locator.parent: + unparenting(locator) + parenting(lcs, locator) + parenting(root_lcs, lcs) + for lcs in lcs_objects: + if lcs.name.endswith(outlet): + unparenting(lcs) + round_transforms(lcs) + parenting( + lcs_objects[lcs_objects.index( + bpy.data.objects[ + '{}{}'.format(lcs.name.split(outlet)[0], inlet)])], + lcs) + + root_lcs.matrix_world = Matrix() + return lcs_objects + + +def closest_lcs(lcs_objects): + ''' Finding closest outlet to inlet LCS. ''' + target_dists = {} + for target in lcs_objects: + if target.name.endswith(inlet): + dists = {} + for lcs in lcs_objects: + if lcs.name.endswith(outlet): + dist = math.dist( + target.matrix_world.translation, + lcs.matrix_world.translation) + dists[lcs.name] = dist + min_dist = min(dists.values()) + if min_dist < 0.01: + min_lcs = [k for k, v in dists.items() if v == min_dist][0] + target_dists[target.name] = min_lcs + return target_dists + + +def lcs_constrainting(lcs_objects, root_lcs): + ''' Placing inlet right on outlet LCS. ''' + closests = closest_lcs(lcs_objects) + for lcs in lcs_objects: + if lcs.name in closests: + constraint = lcs.constraints.new(type='COPY_TRANSFORMS') + constraint.target = bpy.data.objects[closests[lcs.name]] + if lcs.name.endswith(outlet): + constraint = lcs.constraints.new(type='COPY_TRANSFORMS') + constraint.target = root_lcs + constraint.enabled = False + for lcs in lcs_objects: + if len(lcs.constraints) == 0: + constraint = lcs.constraints.new(type='COPY_TRANSFORMS') + constraint.target = root_lcs + constraint.enabled = False + return lcs_objects + + +def unlink_from_col(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 lcs_collections(root_lcs, lcs_objects): + ''' Create LCS based hierarchy. ''' + for lcs in root_lcs.children: + lcs_col = bpy.data.collections.new( + '{}{}'.format(lcs.name.split(inlet)[0], hightpoly)) + bpy.data.collections[parts_col_name].children.link(lcs_col) + for obj in lcs.children_recursive: + unlink_from_col(obj) + lcs_col.objects.link(obj) + if lcs not in lcs_objects: + unlink_from_col(lcs) + lcs_col.objects.link(lcs) + return root_lcs.children + + +def restruct_hierarchy(): + ''' Execute restructurisation. ''' + + lcs_objects = bpy.data.collections[lcs_col_name].objects + + main_locator = [obj for obj in bpy.data.objects if not obj.parent][0] + root_lcs = [lcs for lcs in lcs_objects if lcs.name.endswith(root)][0] + lcs_objects = [lcs for lcs in lcs_objects if lcs != root_lcs] + root_locator = root_lcs.parent + unparenting(root_lcs) + round_transforms(root_lcs) + unparenting(root_locator) + parenting(root_lcs, root_locator) + parenting(root_lcs, main_locator) + + retree_by_lcs(lcs_objects, root_lcs) + lcs_constrainting(lcs_objects, root_lcs) + + lcs_collections(root_lcs, lcs_objects) + + # remove unused for now collection + bpy.data.collections.remove(bpy.data.collections[hierarchy_col_name]) + + logger.info('Restructuring pipeline finished!') + return lcs_objects diff --git a/cg/blender/remesh/__init__.py b/cg/blender/remesh/__init__.py index a68c0b0..96a5469 100644 --- a/cg/blender/remesh/__init__.py +++ b/cg/blender/remesh/__init__.py @@ -1,62 +1,7 @@ # -*- coding: utf-8 -*- -""" +''' DESCRIPTION. -Basic mesh processing for asset pipeline. -Early WIP! -""" -__version__ = "0.1" - -import logging -import sys -import bpy -import math -from utils.apply_transforms import apply_transforms - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -def asset_setup(transforms=True, sharpness=True, shading=True): - """ asset setup pipeline """ - for ob in bpy.context.scene.objects: - if not ob.type == 'MESH': - continue - bpy.ops.object.select_all(action='DESELECT') - ob.select_set(state=True) - bpy.context.view_layer.objects.active = ob - # apply scale - apply_transforms(ob, location=False, rotation=False, scale=True) - - if transforms: - # remove doubles - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.remove_doubles(threshold = 0.00001) - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.mesh.select_mode(type = 'FACE') - bpy.ops.mesh.select_interior_faces() - bpy.ops.mesh.delete(type='FACE') - bpy.ops.object.mode_set(mode='OBJECT') - - if sharpness: - # set shaps and unwrap - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.mesh.select_mode(type='EDGE') - bpy.ops.mesh.edges_select_sharp( sharpness = math.radians(10) ) - bpy.ops.mesh.mark_sharp() - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.smart_project() - bpy.ops.object.mode_set(mode='OBJECT') - - if shading: - #fix shading - bpy.ops.object.shade_smooth() - bpy.context.view_layer.objects.active.data.use_auto_smooth = 1 - bpy.context.view_layer.objects.active.modifiers.new(type='DECIMATE', name='decimate') - bpy.context.view_layer.objects.active.modifiers["decimate"].decimate_type = "DISSOLVE" - bpy.context.view_layer.objects.active.modifiers["decimate"].angle_limit = 0.00872665 - bpy.context.object.modifiers["decimate"].show_expanded = 0 - bpy.context.view_layer.objects.active.modifiers.new(type='TRIANGULATE', name='triangulate') - bpy.context.object.modifiers["triangulate"].keep_custom_normals = 1 - bpy.context.object.modifiers["triangulate"].show_expanded = 0 +Mesh processing for asset creation pipeline. +Setup and prepare highpoly objects. +Create and prepare lowpoly_objects. +''' diff --git a/cg/blender/remesh/highpoly_setup.py b/cg/blender/remesh/highpoly_setup.py new file mode 100644 index 0000000..048904a --- /dev/null +++ b/cg/blender/remesh/highpoly_setup.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Basic mesh processing for asset pipeline. +''' +__version__ = '0.2' + +import logging +import sys +import bpy +import math + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def setup_meshes(bobjs, cleanup=False, sharpness=False, shading=False): + ''' Setup raw meshes list after importing ''' + logger.info('Hightpoly meshes setup launched...') + for bobj in bobjs: + if not bobj.type == 'MESH': + continue + bpy.ops.object.select_all(action='DESELECT') + bobj.select_set(state=True) + bpy.context.view_layer.objects.active = bobj + + if cleanup: + # remove doubles + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(threshold=0.00001) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(type='FACE') + bpy.ops.mesh.select_interior_faces() + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + if sharpness: + # set shaps and unwrap + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(type='EDGE') + bpy.ops.mesh.edges_select_sharp( sharpness=math.radians(12) ) + bpy.ops.mesh.mark_sharp() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.smart_project() + bpy.ops.object.mode_set(mode='OBJECT') + + if shading: + # fix shading + bpy.ops.object.shade_smooth() + bpy.context.view_layer.objects.active.data.use_auto_smooth = 1 + bpy.context.view_layer.objects.active.modifiers.new(type='DECIMATE', name='decimate') + bpy.context.view_layer.objects.active.modifiers['decimate'].decimate_type = 'DISSOLVE' + bpy.context.view_layer.objects.active.modifiers['decimate'].angle_limit = 0.00872665 + bpy.context.object.modifiers['decimate'].show_expanded = 0 + bpy.context.view_layer.objects.active.modifiers.new(type='TRIANGULATE', name='triangulate') + bpy.context.object.modifiers['triangulate'].keep_custom_normals = 1 + bpy.context.object.modifiers['triangulate'].show_expanded = 0 + + return logger.info('Hightpoly meshes setup finished!') diff --git a/cg/blender/remesh/lowpoly_setup.py b/cg/blender/remesh/lowpoly_setup.py new file mode 100644 index 0000000..df9e5b5 --- /dev/null +++ b/cg/blender/remesh/lowpoly_setup.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Create lowpoly shells from parts collections. +''' +__version__ = '0.1' + +import logging +import sys +import bpy +import math + +from utils.generative_modifiers import shell_remesher +from utils.object_converter import convert_mesh_to_mesh +from utils.object_relations import parenting + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Import Parts' +lcs_col_name = 'Import LCS' +hierarchy_col_name = 'Import Hierarchy' +lowpoly_col_name = 'Lowpoly Parts' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' + + +def parts_to_shells(hightpoly_part_names): + ''' Create lowpoly shells from parts collections. ''' + logger.info('Lowpoly shells creation launched...') + + lowpoly_col = bpy.data.collections.new(lowpoly_col_name) + bpy.context.scene.collection.children.link(lowpoly_col) + + for part_name in hightpoly_part_names: + # generate lowpoly objects from part collections + lowpoly_name = ('{}{}'.format(part_name, lowpoly)) + lowpoly_mesh = bpy.data.meshes.new(lowpoly_name) + lowpoly_obj = bpy.data.objects.new(lowpoly_name, lowpoly_mesh) + bpy.context.view_layer.update() + part_inlet = bpy.data.objects.get('{}{}'.format(part_name, inlet)) + lowpoly_obj.matrix_world = part_inlet.matrix_world.copy() + parenting(part_inlet, lowpoly_obj) + lowpoly_col.objects.link(lowpoly_obj) + + shell_remesher(lowpoly_obj, 'remesh_nodes', 'robossembler') + part_col = bpy.data.collections[('{}{}'.format(part_name, hightpoly))] + lowpoly_obj.modifiers['remesh_nodes']['Input_0'] = part_col + + remesh_voxel = lowpoly_obj.modifiers.new('remesh_voxel', type='REMESH') + remesh_voxel.mode = 'VOXEL' + remesh_voxel.voxel_size = 0.001 + + remesh_sharp = lowpoly_obj.modifiers.new('remesh_sharp', type='REMESH') + remesh_sharp.mode = 'SHARP' + remesh_sharp.octree_depth = 7 + + decimate = lowpoly_obj.modifiers.new('decimate', type='DECIMATE') + decimate.decimate_type = 'COLLAPSE' + decimate.ratio = 0.1 + + # apply all modifiers to mesh + parenting(part_inlet, convert_mesh_to_mesh(lowpoly_obj)) + + logger.info('Lowpoly shells created successfully!') + + return lowpoly_col.objects diff --git a/cg/blender/utils/cleanup_orphan_data.py b/cg/blender/utils/cleanup_orphan_data.py index 4b83c61..1dce092 100644 --- a/cg/blender/utils/cleanup_orphan_data.py +++ b/cg/blender/utils/cleanup_orphan_data.py @@ -10,13 +10,13 @@ # 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. -__version__ = "0.1" +__version__ = '0.1' import bpy def cleanup_orphan_data(): - """Removes all data without users""" + '''Removes all data without users''' for block in bpy.data.meshes: if block.users == 0: bpy.data.meshes.remove(block) diff --git a/cg/blender/utils/generative_modifiers.py b/cg/blender/utils/generative_modifiers.py new file mode 100644 index 0000000..a7103a2 --- /dev/null +++ b/cg/blender/utils/generative_modifiers.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Basic mesh processing for asset pipeline. +''' +__version__ = '0.1' + +import logging +import bpy + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +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.inputs.new('NodeSocketCollection', 'Collection') + 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.outputs.new('NodeSocketGeometry', 'Geometry') + 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/cg/blender/utils/object_converter.py b/cg/blender/utils/object_converter.py new file mode 100644 index 0000000..fdf4a10 --- /dev/null +++ b/cg/blender/utils/object_converter.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Convert all deformers and modifiers of object to it's mesh. +''' +__version__ = '0.1' + +import bpy + + +def convert_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/cg/blender/utils/object_relations.py b/cg/blender/utils/object_relations.py new file mode 100644 index 0000000..ab647a9 --- /dev/null +++ b/cg/blender/utils/object_relations.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +__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/cg/blender/utils/apply_transforms.py b/cg/blender/utils/object_transforms.py similarity index 80% rename from cg/blender/utils/apply_transforms.py rename to cg/blender/utils/object_transforms.py index 2d1f554..0b85c10 100644 --- a/cg/blender/utils/apply_transforms.py +++ b/cg/blender/utils/object_transforms.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -# original idea from https://github.com/machin3io/MACHIN3tools # Copyright (C) 2023 Ilia Kurochkin # -# -*- coding: utf-8 -*- -# Copyright 2023 by brothermechanic. All Rights Reserved. -# Based on https://github.com/machin3io/MACHIN3tools/blob/master/operators/apply.py # 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 @@ -14,13 +10,18 @@ # 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. -__version__ = "0.2" +__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 """ + ''' 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) @@ -33,7 +34,6 @@ def apply_transforms(obj, location=False, rotation=False, scale=False): scale_martix[i][i] = scale[i] return 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) @@ -42,7 +42,7 @@ def apply_transforms(obj, location=False, rotation=False, scale=False): obj.matrix_world = apply_matrix else: if location: - raise Exception("Location only applies with all transformations (rotate and scale) together!") + 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) @@ -58,3 +58,11 @@ def apply_transforms(obj, location=False, rotation=False, scale=False): 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/cg/blender/utils/remove_collections.py b/cg/blender/utils/remove_collections.py index 710dfa1..88b993c 100644 --- a/cg/blender/utils/remove_collections.py +++ b/cg/blender/utils/remove_collections.py @@ -10,7 +10,7 @@ # 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. -__version__ = "0.1" +__version__ = '0.1' import logging import bpy @@ -20,7 +20,7 @@ logging.basicConfig(level=logging.INFO) def remove_collections(collection_name=None): - """Removes all all collection or collection_name only""" + '''Removes all all collection or collection_name only''' if collection_name: collection = bpy.data.collections.get(collection_name) try: diff --git a/cg/blender/utils/shininess_to_roughness.py b/cg/blender/utils/shininess_to_roughness.py index 6f35a8e..e284886 100644 --- a/cg/blender/utils/shininess_to_roughness.py +++ b/cg/blender/utils/shininess_to_roughness.py @@ -11,13 +11,13 @@ # 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. -__version__ = "0.1" +__version__ = '0.1' import math def shiny_to_rough(shininess): - """ convert shiny to roughness """ + ''' convert shiny to roughness ''' a, b = -1.0, 2.0 c = (shininess / 100.0) - 1.0 D = math.pow(b,2) - (4 * a * c) diff --git a/cg/pipeline/freecad_to_asset.py b/cg/pipeline/freecad_to_asset.py index 5c860e7..d55e484 100644 --- a/cg/pipeline/freecad_to_asset.py +++ b/cg/pipeline/freecad_to_asset.py @@ -1,22 +1,24 @@ # -*- coding: utf-8 -*- -""" +''' DESCRIPTION. Convert and setup FreeCAD solid objects to 3d assets mesh files. Support Blender compiled as a Python Module only! -""" -__version__ = "0.3" +''' +__version__ = '0.4' import logging import os import sys sys.path.append('../blender/') from import_fcstd.import_cad_objects import obj_importer -from import_fcstd.import_coordinate_point import empty_importer -from utils.apply_transforms import apply_transforms +from import_fcstd.restruct_hierarchy_by_lcs import restruct_hierarchy +from import_fcstd.import_coordinate_point import lcs_json_importer from utils.remove_collections import remove_collections from utils.cleanup_orphan_data import cleanup_orphan_data from utils.sdf_mesh_selector import sdf_mesh_selector -from remesh import asset_setup +from remesh.highpoly_setup import setup_meshes +from remesh.lowpoly_setup import parts_to_shells + from export.dae import export_dae from export.collision import export_col_stl import bpy @@ -25,6 +27,37 @@ import mathutils logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) +''' +IMPORT COLLECTIONS NAMIG CONVENTION: +Import Parts - collection for mesh objects +Import LCS - collection for location points +Import Hierarchy - collection for hierarchy locators + +LCS POINT'S SUFFIXES CONVENTION: +'_in' - inlet suffix +'_out' - outlet suffix +'_root' - root suffix + +CG ASSETS SUFFIXES CONVENTION: +'_hp' - hightpoly asset (reference baking source) +'_lp' - lowpoly asset (prepared for game engines) +'_render' - root suffix (prepared for render engines) +''' + + +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Import Parts' +lcs_col_name = 'Import LCS' +hierarchy_col_name = 'Import Hierarchy' +lowpoly_col_name = 'Lowpoly Parts' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' def freecad_asset_pipeline(fcstd_path, linear_deflection, @@ -33,28 +66,45 @@ def freecad_asset_pipeline(fcstd_path, json_path=None, blend_path=None, sdf_path=None): - """ Setup FreeCAD scene to CG asset """ + ''' Setup FreeCAD scene to CG asset ''' + # prepare blend file remove_collections() cleanup_orphan_data() # import objects - obj_importer(fcstd_path, linear_deflection, angular_deflection) + objs_for_render = obj_importer(fcstd_path, + linear_deflection, + angular_deflection) + + # restructuring hierarchy by lcs points + lcs_objects = restruct_hierarchy() # import lcs - if json_path is None: - json_path = os.path.dirname(fcstd_path) - for f in os.listdir(os.path.dirname(fcstd_path)): - if f.endswith('.json'): - json_file = os.path.join(json_path, f) - empty_importer(json_file) + if not bpy.data.collections.get(lcs_col_name): + if not bpy.data.collections[lcs_col_name].objects: + if json_path is None: + json_path = os.path.dirname(fcstd_path) + for f in os.listdir(os.path.dirname(fcstd_path)): + if f.endswith('.json'): + json_file = os.path.join(json_path, f) + lcs_json_importer(json_file) # sdf setup WIP if sdf_path is not None: sdf_mesh_selector(sdf_path) + # save blender scene + if blend_path is not None: + if not os.path.isdir(os.path.dirname(blend_path)): + os.makedirs(os.path.dirname(blend_path)) + bpy.ops.wm.save_as_mainfile(filepath=blend_path) + # retopo - asset_setup() + setup_meshes(objs_for_render, sharpness=True, shading=True) + + part_names = [p.name.split(inlet)[0] for p in lcs_objects if p.name.endswith(inlet)] + parts_to_shells(part_names) # save blender scene if blend_path is not None: @@ -102,4 +152,4 @@ if __name__ == '__main__': args.blend_path, args.sdf_path) - logger.info("Assets setup finished without errors") + logger.info('Assets setup completed!')