From 6560a4359d9b1060df7708a44ed34ff24bad4f1a Mon Sep 17 00:00:00 2001 From: brothermechanic Date: Fri, 19 May 2023 18:35:03 +0000 Subject: [PATCH] =?UTF-8?q?[Blender]=20=D0=94=D0=BE=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=BD=D1=8B=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D0=B4=D1=83=D1=80=D1=8B=20=D1=80=D0=B5=D1=82=D0=BE=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=B8=20=D0=BC=D0=B5=D1=88=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cg/blender/import_fcstd/import_cad_objects.py | 155 +++++------------- .../import_fcstd/import_coordinate_point.py | 9 + cg/blender/import_fcstd/import_hierarchy.py | 18 +- cg/blender/import_fcstd/import_materials.py | 78 +++++++++ cg/blender/import_fcstd/materials.py | 78 --------- cg/blender/remesh/__init__.py | 5 +- .../Robossembler_ABS-Dark-Rough.FCMat | 33 ++++ .../Robossembler_ABS-White-Rough.FCMat | 4 +- cg/pipeline/freecad_to_asset.py | 12 +- 9 files changed, 188 insertions(+), 204 deletions(-) create mode 100644 cg/blender/import_fcstd/import_materials.py delete mode 100644 cg/blender/import_fcstd/materials.py create mode 100644 cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-Dark-Rough.FCMat diff --git a/cg/blender/import_fcstd/import_cad_objects.py b/cg/blender/import_fcstd/import_cad_objects.py index 026a610..ab88c1d 100644 --- a/cg/blender/import_fcstd/import_cad_objects.py +++ b/cg/blender/import_fcstd/import_cad_objects.py @@ -15,6 +15,7 @@ __version__ = "0.2" import time import FreeCAD import logging +import math import xml import sys import xml.sax @@ -24,7 +25,7 @@ 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.materials import set_fem_mat +from import_fcstd.import_materials import import_materials from import_fcstd.is_object_solid import is_object_solid logger = logging.getLogger(__name__) @@ -32,7 +33,8 @@ logging.basicConfig(level=logging.INFO) def obj_importer(filename, - tessellation, + linear_deflection, + angular_deflection, update=False, placement=True, skiphidden=True, @@ -42,8 +44,6 @@ def obj_importer(filename, """Reads a FreeCAD .FCStd file and creates Blender objects""" - TRIANGULATE = True # set to True to triangulate all faces (will loose multimaterial info) - guidata = {} zdoc = zipfile.ZipFile(filename) if zdoc: @@ -78,7 +78,7 @@ def obj_importer(filename, return {'CANCELLED'} # import some FreeCAD modules needed below. After "import FreeCAD" these modules become available - import Part + import Part, Mesh, MeshPart def hascurves(shape): @@ -87,8 +87,14 @@ def obj_importer(filename, return True return False - fcstd_collection = bpy.data.collections.new("FreeCAD import") - bpy.context.scene.collection.children.link(fcstd_collection) + parts_collection = bpy.data.collections.new("Import Parts") + bpy.context.scene.collection.children.link(parts_collection) + + # collect all materials + fem_mats = [] + for fem_mat in doc.Objects: + if fem_mat.isDerivedFrom("App::MaterialObjectPython"): + fem_mats.append(fem_mat) for obj in doc.Objects: # logger.debug("Importing",obj.Label) @@ -100,6 +106,12 @@ def obj_importer(filename, # TODO add parent visibility check 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 @@ -107,107 +119,25 @@ def obj_importer(filename, verts = [] edges = [] faces = [] - matindex = [] # face to material relationship - faceedges = [] # a placeholder to store edges that belong to a face - if obj.isDerivedFrom("Part::Feature"): - # !!! - # create mesh from shape - shape = obj.Shape - if placement: - # !!! - placement = obj.Placement - shape = obj.Shape.copy() - shape.Placement = placement.inverse().multiply(shape.Placement) - if shape.Faces: - # !!! - if TRIANGULATE: - # triangulate and make faces - rawdata = shape.tessellate(tessellation) - for v in rawdata[0]: - verts.append([v.x,v.y,v.z]) - for f in rawdata[1]: - faces.append(f) - for face in shape.Faces: - for e in face.Edges: - faceedges.append(e.hashCode()) - else: - # !!! - # write FreeCAD faces as polygons when possible - time_start = time.time() - for face in shape.Faces: - if (len(face.Wires) > 1) or (not isinstance(face.Surface,Part.Plane)) or hascurves(face): - # !!! - # face has holes or is curved, so we need to triangulate it - rawdata = face.tessellate(tessellation) - for v in rawdata[0]: - vl = [v.x,v.y,v.z] - if not vl in verts: - verts.append(vl) - for f in rawdata[1]: - nf = [] - for vi in f: - nv = rawdata[0][vi] - nf.append(verts.index([nv.x,nv.y,nv.z])) - faces.append(nf) - matindex.append(len(rawdata[1])) - else: - # !!! - f = [] - ov = face.OuterWire.OrderedVertexes - for v in ov: - vl = [v.X,v.Y,v.Z] - if not vl in verts: - verts.append(vl) - f.append(verts.index(vl)) - # FreeCAD doesn't care about verts order. Make sure our loop goes clockwise - c = face.CenterOfMass - v1 = ov[0].Point.sub(c) - v2 = ov[1].Point.sub(c) - n = face.normalAt(0,0) - if (v1.cross(v2)).getAngle(n) > 1.57: - f.reverse() # inverting verts order if the direction is couterclockwise - faces.append(f) - matindex.append(1) - for e in face.Edges: - faceedges.append(e.hashCode()) - logger.debug('faces time is %s', (time.time() - time_start)) - for edge in shape.Edges: - # !!! - # Treat remaining edges (that are not in faces) - if not (edge.hashCode() in faceedges): - if hascurves(edge): - dv = edge.discretize(9) # TODO use tessellation value - for i in range(len(dv)-1): - dv1 = [dv[i].x,dv[i].y,dv[i].z] - dv2 = [dv[i+1].x,dv[i+1].y,dv[i+1].z] - if not dv1 in verts: - verts.append(dv1) - if not dv2 in verts: - verts.append(dv2) - edges.append([verts.index(dv1),verts.index(dv2)]) - else: - e = [] - for vert in edge.Vertexes: - # TODO discretize non-linear edges - v = [vert.X,vert.Y,vert.Z] - if not v in verts: - verts.append(v) - e.append(verts.index(v)) - edges.append(e) + # 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) - elif obj.isDerivedFrom("Mesh::Feature"): - # convert freecad mesh to blender mesh - mesh = obj.Mesh - if placement: - placement = obj.Placement - mesh = obj.Mesh.copy() # in meshes, this zeroes the placement - t = mesh.Topology - verts = [[v.x,v.y,v.z] for v in t[0]] - faces = t[1] + t = meshfromshape.Mesh.Topology + verts = [[v.x,v.y,v.z] for v in t[0]] + faces = t[1] - if verts and (faces or edges): - # !!! + if verts and faces: # create or update object with mesh and material data bobj = None bmat = None @@ -225,7 +155,6 @@ def obj_importer(filename, # 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: @@ -239,17 +168,19 @@ def obj_importer(filename, bobj.rotation_mode = m bobj.scale = (scale, scale, scale) - if obj.Name in guidata: - # !!! - # one material for the whole object - for fem_mat in doc.Objects: - set_fem_mat(obj, bobj, fem_mat) + # 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) + parts_collection.objects.link(bobj) + + # construct assembly hierarchy obj_parent = obj.getParentGeoFeatureGroup() if obj_parent: import_hierarchy(obj, bobj, scale) - fcstd_collection.objects.link(bobj) if select: bpy.context.view_layer.objects.active = bobj bobj.select_set(True) diff --git a/cg/blender/import_fcstd/import_coordinate_point.py b/cg/blender/import_fcstd/import_coordinate_point.py index a880e38..c683ff9 100644 --- a/cg/blender/import_fcstd/import_coordinate_point.py +++ b/cg/blender/import_fcstd/import_coordinate_point.py @@ -34,6 +34,14 @@ def empty_importer(path_json): fori = tuple(pivot_pose['orientation'].values()) bori = (fori[3],)+fori[:3] + if not bpy.data.collections.get('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'] + else: + lcs_collection = bpy.data.collections['Import LCS'] + bpy.ops.object.empty_add( type='ARROWS', radius=0.1, align='WORLD', location=(0, 0, 0), rotation=(0, 0, 0)) @@ -43,6 +51,7 @@ def empty_importer(path_json): pivot_obj.location = loc pivot_obj.rotation_quaternion = bori pivot_obj.rotation_mode = 'XYZ' + pivot_obj.show_in_front = True if pivot_parent_name: pivot_obj.parent = bpy.data.objects[pivot_parent_name] diff --git a/cg/blender/import_fcstd/import_hierarchy.py b/cg/blender/import_fcstd/import_hierarchy.py index 5ec54eb..ab1e9d7 100644 --- a/cg/blender/import_fcstd/import_hierarchy.py +++ b/cg/blender/import_fcstd/import_hierarchy.py @@ -21,10 +21,19 @@ logging.basicConfig(level=logging.INFO) def import_hierarchy(fc_obj, b_obj, scale): """FreeCAD object, Blender object, scene scale""" + + 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() obj_child_name = None while obj_parent: - if bpy.context.scene.objects.get(obj_parent.Label): + if hierarchy_collection.objects.get(obj_parent.Label): empty = bpy.data.objects[obj_parent.Label] else: bpy.ops.object.empty_add( @@ -40,11 +49,12 @@ def import_hierarchy(fc_obj, b_obj, scale): q = (placement.Rotation.Q[3],)+placement.Rotation.Q[:3] empty.rotation_quaternion = (q) empty.rotation_mode = rm - if b_obj.parent: - bpy.data.objects[obj_child_name].parent = empty - else: + if not b_obj.parent: b_obj.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) + diff --git a/cg/blender/import_fcstd/import_materials.py b/cg/blender/import_fcstd/import_materials.py new file mode 100644 index 0000000..7f2e50b --- /dev/null +++ b/cg/blender/import_fcstd/import_materials.py @@ -0,0 +1,78 @@ +# -*- 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. +import logging +import sys +import bpy +from bpy_extras.node_shader_utils import PrincipledBSDFWrapper +from utils.shininess_to_roughness import shiny_to_rough + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def import_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: + 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: + if 'DiffuseColor' in fem_mat.Material.keys(): + d_col_str = fem_mat.Material['DiffuseColor'] + d_col4 = tuple( + map(float, d_col_str[1:-1].split(', '))) + d_col = d_col4[:-1] + else: + d_col = (0.5, 0.5, 0.5) + if 'Father' in fem_mat.Material.keys(): + if fem_mat.Material['Father'] == 'Metal': + me = 1 + else: + me = 0 + else: + me = 0 + if 'Shininess' in fem_mat.Material.keys(): + shiny = float(fem_mat.Material['Shininess']) + if shiny == 0: + rg = 0.5 + else: + rg = shiny_to_rough(shiny) + else: + rg = 0.5 + if 'EmissiveColor' in fem_mat.Material.keys(): + e_col_str = fem_mat.Material['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 'Transparency' in fem_mat.Material.keys(): + tr_str = fem_mat.Material['Transparency'] + alpha = 1.0 - float(tr_str) + else: + alpha = 1.0 + + bmat = bpy.data.materials.new(name=fem_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 + bobj.data.materials.append(bmat) + + logger.debug('Assign %s to object %s', fem_mat_name, bobj.name) diff --git a/cg/blender/import_fcstd/materials.py b/cg/blender/import_fcstd/materials.py deleted file mode 100644 index 92defca..0000000 --- a/cg/blender/import_fcstd/materials.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- 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. -import logging -import sys -import bpy -from bpy_extras.node_shader_utils import PrincipledBSDFWrapper -from utils.shininess_to_roughness import shiny_to_rough - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -def set_fem_mat(obj, bobj, fem_mat): - if fem_mat.isDerivedFrom("App::MaterialObjectPython"): - if fem_mat.References[0][0].Name == obj.Name: - fem_mat_name = fem_mat.Material['Name'] - if 'DiffuseColor' in fem_mat.Material.keys(): - d_col_str = fem_mat.Material['DiffuseColor'] - d_col4 = tuple( - map(float, d_col_str[1:-1].split(', '))) - d_col = d_col4[:-1] - else: - d_col = (0.5, 0.5, 0.5) - if 'Father' in fem_mat.Material.keys(): - if fem_mat.Material['Father'] == 'Metal': - me = 1 - else: - me = 0 - else: - me = 0 - if 'Shininess' in fem_mat.Material.keys(): - shiny = float(fem_mat.Material['Shininess']) - if shiny == 0: - rg = 0.5 - else: - rg = shiny_to_rough(shiny) - else: - rg = 0.5 - if 'EmissiveColor' in fem_mat.Material.keys(): - e_col_str = fem_mat.Material['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 'Transparency' in fem_mat.Material.keys(): - tr_str = fem_mat.Material['Transparency'] - alpha = 1.0 - float(tr_str) - else: - alpha = 1.0 - - logger.debug('Assign %s to object %s', fem_mat_name, obj.Label) - - if fem_mat_name in bpy.data.materials: - 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 - 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 - bobj.data.materials.append(bmat) diff --git a/cg/blender/remesh/__init__.py b/cg/blender/remesh/__init__.py index ded2629..a68c0b0 100644 --- a/cg/blender/remesh/__init__.py +++ b/cg/blender/remesh/__init__.py @@ -43,7 +43,7 @@ def asset_setup(transforms=True, sharpness=True, shading=True): 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(30) ) + 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() @@ -57,9 +57,6 @@ def asset_setup(transforms=True, sharpness=True, shading=True): 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='WEIGHTED_NORMAL', name='weightednormal') - bpy.context.view_layer.objects.active.modifiers["weightednormal"].keep_sharp = 1 - bpy.context.object.modifiers["weightednormal"].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 diff --git a/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-Dark-Rough.FCMat b/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-Dark-Rough.FCMat new file mode 100644 index 0000000..49eb02a --- /dev/null +++ b/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-Dark-Rough.FCMat @@ -0,0 +1,33 @@ +; Robossembler_ABS-Dark-Rough +; (c) 2023 brothermechanic (CC-BY 3.0) + +[General] +Name = Robossembler_ABS-Dark-Rough +Description = Generic ABS material for Robossembler project's pipeline. +Father = Thermoplast + +[Mechanical] +Density = 1060 kg/m^3 +PoissonRatio = 0.37 +UltimateTensileStrength = 38.8 MPa +YieldStrength = 44.1 MPa +YoungsModulus = 2300 MPa + +[Thermal] +SpecificHeat = 2050 J/kg/K +ThermalConductivity = 0.158 W/m/K +ThermalExpansionCoefficient = 0.000093 m/m/K + +[Rendering] +DiffuseColor = (0.1, 0.1, 0.1, 1.0) +EmissiveColor = (0.0, 0.0, 0.0, 1.0) +Shininess = 35 +TexturePath = ~/texture.jpg +Transparency = 0.0 + +[VectorRendering] +ViewColor = (0.0, 0.0, 1.0, 1.0) + +[UserDefined] +usernum = 0.0 +userstr = String diff --git a/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-White-Rough.FCMat b/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-White-Rough.FCMat index c8f3124..e84e279 100644 --- a/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-White-Rough.FCMat +++ b/cg/freecad/Robossembler_FEM_Materials/Robossembler_ABS-White-Rough.FCMat @@ -1,8 +1,8 @@ -; Robossembler_ABS-Grey-Rough +; Robossembler_ABS-White-Rough ; (c) 2023 brothermechanic (CC-BY 3.0) [General] -Name = Robossembler_ABS-Grey-Rough +Name = Robossembler_ABS-White-Rough Description = Generic ABS material for Robossembler project's pipeline. Father = Thermoplast diff --git a/cg/pipeline/freecad_to_asset.py b/cg/pipeline/freecad_to_asset.py index 138a0eb..5c860e7 100644 --- a/cg/pipeline/freecad_to_asset.py +++ b/cg/pipeline/freecad_to_asset.py @@ -27,7 +27,8 @@ logging.basicConfig(level=logging.INFO) def freecad_asset_pipeline(fcstd_path, - tessellation, + linear_deflection, + angular_deflection, mesh_export_path=None, json_path=None, blend_path=None, @@ -38,7 +39,7 @@ def freecad_asset_pipeline(fcstd_path, cleanup_orphan_data() # import objects - obj_importer(fcstd_path, tessellation) + obj_importer(fcstd_path, linear_deflection, angular_deflection) # import lcs if json_path is None: @@ -80,7 +81,9 @@ if __name__ == '__main__': parser.add_argument( '--fcstd_path', type=str, help='Path to source FreeCAD scene', required=True) parser.add_argument( - '--tessellation', type=int, help='Tessellation number', default=10, required=False) + '--linear_deflection', type=float, help='Max linear distance error', default=0.1, required=False) + parser.add_argument( + '--angular_deflection', type=float, help='Max angular distance error', default=20.0, required=False) parser.add_argument( '--mesh_export_path', type=str, help='Path for export meshes', required=False) parser.add_argument( @@ -92,7 +95,8 @@ if __name__ == '__main__': args = parser.parse_args() freecad_asset_pipeline(args.fcstd_path, - args.tessellation, + args.linear_deflection, + args.angular_deflection, args.mesh_export_path, args.json_path, args.blend_path,