[Blender] Доработаны процедуры ретопологии мешей

This commit is contained in:
brothermechanic 2023-05-19 18:35:03 +00:00 committed by Igor Brylyov
parent 45e0d29ea0
commit 6560a4359d
9 changed files with 188 additions and 204 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
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)

View file

@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
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)

View file

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

View file

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

View file

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

View file

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