[Blender] Implemented tesselation for CAD-model, retopology and optimisation tesselation's result, assigning physical properties with assigned material
This commit is contained in:
parent
9fa936cfba
commit
839ce36c70
19 changed files with 891 additions and 351 deletions
|
@ -1,2 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__version__ = "0.1"
|
'''
|
||||||
|
DESCRIPTION.
|
||||||
|
Tesselate and import FreeCAD parts as meshes to Bledner scene.
|
||||||
|
For asset creation pipeline.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Original code by (C) 2019 yorikvanhavre <yorik@uncreated.net>
|
|
||||||
# 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 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
|
|
|
@ -11,9 +11,21 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# 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 time
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
|
import Part, Mesh, MeshPart
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import xml
|
import xml
|
||||||
|
@ -21,174 +33,173 @@ import sys
|
||||||
import xml.sax
|
import xml.sax
|
||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import bpy
|
import bpy
|
||||||
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
|
from import_fcstd.import_hierarchy import (hierarchy,
|
||||||
from import_fcstd.handler import FreeCAD_xml_handler
|
placement)
|
||||||
from import_fcstd.import_hierarchy import import_hierarchy
|
from import_fcstd.import_materials import (assign_materials,
|
||||||
from import_fcstd.import_materials import import_materials
|
assign_black)
|
||||||
from import_fcstd.is_object_solid import is_object_solid
|
from import_fcstd.is_object_solid import is_object_solid
|
||||||
|
from utils.object_transforms import apply_transforms
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
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,
|
def obj_importer(filename,
|
||||||
linear_deflection,
|
linear_deflection,
|
||||||
angular_deflection,
|
angular_deflection,
|
||||||
update=False,
|
update=False,
|
||||||
placement=True,
|
scene_placement=True,
|
||||||
skiphidden=True,
|
skiphidden=True,
|
||||||
scale=0.001,
|
scale=0.001,
|
||||||
select=True,
|
select=True,
|
||||||
report=None):
|
nonsolid_property = 'Robossembler_NonSolid'):
|
||||||
|
|
||||||
"""Reads a FreeCAD .FCStd file and creates Blender objects"""
|
''' 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()
|
|
||||||
|
|
||||||
doc = FreeCAD.open(filename)
|
doc = FreeCAD.open(filename)
|
||||||
docname = doc.Name
|
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
|
if not update:
|
||||||
import Part, Mesh, MeshPart
|
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:
|
hierarchy_collection = bpy.data.collections.new(hierarchy_col_name)
|
||||||
if not isinstance(e.Curve,(Part.Line,Part.LineSegment)):
|
bpy.context.scene.collection.children.link(hierarchy_collection)
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
parts_collection = bpy.data.collections.new("Import Parts")
|
|
||||||
bpy.context.scene.collection.children.link(parts_collection)
|
|
||||||
|
|
||||||
# collect all materials
|
# collect all materials
|
||||||
fem_mats = []
|
fem_mats = []
|
||||||
for fem_mat in doc.Objects:
|
for fem_mat in doc.Objects:
|
||||||
if fem_mat.isDerivedFrom("App::MaterialObjectPython"):
|
if fem_mat.isDerivedFrom('App::MaterialObjectPython'):
|
||||||
fem_mats.append(fem_mat)
|
fem_mats.append(fem_mat)
|
||||||
|
bobjs = []
|
||||||
|
bobjs_for_render = []
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
# logger.debug("Importing",obj.Label)
|
bobj = None
|
||||||
if skiphidden:
|
if skiphidden:
|
||||||
if obj.Name in guidata:
|
if not obj.Visibility:
|
||||||
if "Visibility" in guidata[obj.Name]:
|
continue
|
||||||
if guidata[obj.Name]["Visibility"] == False:
|
|
||||||
# logger.debug(obj.Label,"is invisible. Skipping.")
|
|
||||||
# TODO add parent visibility check
|
|
||||||
continue
|
|
||||||
|
|
||||||
# process simple parts only
|
if obj.isDerivedFrom('PartDesign::CoordinateSystem'):
|
||||||
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 update:
|
if update:
|
||||||
# locate existing object (mesh with same name)
|
# locate existing object
|
||||||
for o in bpy.data.objects:
|
for o in bpy.data.objects:
|
||||||
if o.data.name == obj.Name:
|
if o.name == obj.Label:
|
||||||
bobj = o
|
bobj = o
|
||||||
logger.debug('Replacing existing %s', obj.Label)
|
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:
|
else:
|
||||||
# create new object
|
bobj = bpy.data.objects.new(obj.Label, None)
|
||||||
bobj = bpy.data.objects.new(obj.Label, bmesh)
|
bobj.empty_display_type = 'ARROWS'
|
||||||
if placement:
|
bobj.empty_display_size = round(random.uniform(0.05, 0.15), 3)
|
||||||
bobj.location = placement.Base.multiply(scale)
|
bobj.show_in_front = True
|
||||||
m = bobj.rotation_mode
|
lcs_collection.objects.link(bobj)
|
||||||
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)
|
|
||||||
|
|
||||||
# one material for the whole object
|
elif obj.isDerivedFrom('Part::Feature'):
|
||||||
for fem_mat in fem_mats:
|
# filter for nonsolids
|
||||||
for ref in fem_mat.References:
|
if is_object_solid(obj) or hasattr(obj, nonsolid_property):
|
||||||
if ref[0].Label == bobj.name:
|
verts = []
|
||||||
import_materials(bobj, fem_mat)
|
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
|
# construct assembly hierarchy
|
||||||
obj_parent = obj.getParentGeoFeatureGroup()
|
hierarchy_objs = hierarchy(bobj, obj, scale)
|
||||||
if obj_parent:
|
for hierarchy_obj in hierarchy_objs:
|
||||||
import_hierarchy(obj, bobj, scale)
|
hierarchy_collection.objects.link(hierarchy_obj)
|
||||||
|
|
||||||
if select:
|
# one material for the whole object
|
||||||
bpy.context.view_layer.objects.active = bobj
|
if bobj.type == 'MESH':
|
||||||
bobj.select_set(True)
|
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)
|
FreeCAD.closeDocument(docname)
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# update do not dork
|
# update do not dork
|
||||||
|
logger.info('Imported %s objects without errors', len(bobjs))
|
||||||
logger.info("Import freecad scene finished without errors")
|
return bobjs_for_render
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
#
|
#
|
||||||
#DESCRIPTION.
|
'''
|
||||||
# Import from json exported FreeCAD's asm4 coordinates as Blender's empty object.
|
DESCRIPTION.
|
||||||
__version__ = "0.1"
|
Import from json exported FreeCAD's asm4 coordinates as Blender's empty object.
|
||||||
|
'''
|
||||||
|
__version__ = '0.2'
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -23,19 +25,20 @@ logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
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:
|
with open(path_json) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
pivot_name = data['label']
|
lcs_name = data['label']
|
||||||
pivot_parent_name = data['parent_label']
|
lcs_parent_name = data['parent_label']
|
||||||
pivot_pose = data['placement']
|
lcs_pose = data['placement']
|
||||||
loc = tuple(pivot_pose['position'].values())
|
loc = tuple(lcs_pose['position'].values())
|
||||||
fori = tuple(pivot_pose['orientation'].values())
|
fori = tuple(lcs_pose['orientation'].values())
|
||||||
bori = (fori[3],)+fori[:3]
|
bori = (fori[3],)+fori[:3]
|
||||||
|
|
||||||
if not bpy.data.collections.get('Import LCS'):
|
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.scene.collection.children.link(lcs_collection)
|
||||||
bpy.context.view_layer.active_layer_collection = \
|
bpy.context.view_layer.active_layer_collection = \
|
||||||
bpy.context.view_layer.layer_collection.children['Import LCS']
|
bpy.context.view_layer.layer_collection.children['Import LCS']
|
||||||
|
@ -45,16 +48,17 @@ def empty_importer(path_json):
|
||||||
bpy.ops.object.empty_add(
|
bpy.ops.object.empty_add(
|
||||||
type='ARROWS', radius=0.1, align='WORLD',
|
type='ARROWS', radius=0.1, align='WORLD',
|
||||||
location=(0, 0, 0), rotation=(0, 0, 0))
|
location=(0, 0, 0), rotation=(0, 0, 0))
|
||||||
pivot_obj = bpy.context.active_object # or bpy.context.object
|
lcs_obj = bpy.context.active_object # or bpy.context.object
|
||||||
pivot_obj.name = pivot_name
|
lcs_obj.name = lcs_name
|
||||||
pivot_obj.rotation_mode = 'QUATERNION'
|
lcs_obj.rotation_mode = 'QUATERNION'
|
||||||
pivot_obj.location = loc
|
lcs_obj.location = loc
|
||||||
pivot_obj.rotation_quaternion = bori
|
lcs_obj.rotation_quaternion = bori
|
||||||
pivot_obj.rotation_mode = 'XYZ'
|
lcs_obj.rotation_mode = 'XYZ'
|
||||||
pivot_obj.show_in_front = True
|
lcs_obj.show_in_front = True
|
||||||
|
|
||||||
if pivot_parent_name:
|
if lcs_parent_name:
|
||||||
pivot_obj.parent = bpy.data.objects[pivot_parent_name]
|
lcs_obj.parent = bpy.data.objects[lcs_parent_name]
|
||||||
|
|
||||||
f.close()
|
f.close()
|
||||||
logger.info('Point %s imported without errors', pivot_name)
|
logger.info('Point %s imported without errors', lcs_name)
|
||||||
|
return lcs_obj
|
||||||
|
|
|
@ -10,8 +10,11 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# 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 logging
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
@ -19,42 +22,39 @@ logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def import_hierarchy(fc_obj, b_obj, scale):
|
def placement(bobj, obj, scale):
|
||||||
"""FreeCAD object, Blender object, scene 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
|
obj_child_name = None
|
||||||
|
parents = []
|
||||||
while obj_parent:
|
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]
|
empty = bpy.data.objects[obj_parent.Label]
|
||||||
else:
|
else:
|
||||||
bpy.ops.object.empty_add(
|
empty = bpy.data.objects.new(obj_parent.Label, None)
|
||||||
type='CUBE', radius=0.01, align='WORLD',
|
empty.empty_display_type = 'CUBE'
|
||||||
location=(0, 0, 0), rotation=(0, 0, 0))
|
empty.empty_display_size = 0.01
|
||||||
empty = bpy.data.objects['Empty']
|
placement(empty, obj_parent, scale)
|
||||||
empty.name = obj_parent.Label
|
parents.append(empty)
|
||||||
placement = obj_parent.Placement
|
if not bobj.parent:
|
||||||
empty.location = placement.Base.multiply(scale)
|
bobj.parent = empty
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
bpy.data.objects[obj_child_name].parent = empty
|
bpy.data.objects[obj_child_name].parent = empty
|
||||||
obj_child_name = obj_parent.Label
|
obj_child_name = obj_parent.Label
|
||||||
obj_parent = obj_parent.getParentGeoFeatureGroup()
|
obj_parent = obj_parent.getParentGeoFeatureGroup()
|
||||||
empty.select_set(False)
|
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
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
|
__version__ = '0.2'
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -20,11 +21,12 @@ logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def import_materials(bobj, fem_mat):
|
def assign_materials(bobj, fem_mat):
|
||||||
""" Build Blender Shader from FreeCAD's FEM material """
|
''' Build Blender shader from FreeCAD's FEM material '''
|
||||||
fem_mat_name = fem_mat.Material['Name']
|
fem_mat_name = fem_mat.Material['Name']
|
||||||
|
|
||||||
if fem_mat_name in bpy.data.materials:
|
if fem_mat_name in bpy.data.materials:
|
||||||
|
# prepare for reimport
|
||||||
if len(bobj.material_slots) < 1:
|
if len(bobj.material_slots) < 1:
|
||||||
bobj.data.materials.append(bpy.data.materials[fem_mat_name])
|
bobj.data.materials.append(bpy.data.materials[fem_mat_name])
|
||||||
else:
|
else:
|
||||||
|
@ -73,6 +75,43 @@ def import_materials(bobj, fem_mat):
|
||||||
principled.roughness = rg
|
principled.roughness = rg
|
||||||
principled.emission_color = e_col
|
principled.emission_color = e_col
|
||||||
principled.alpha = alpha
|
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)
|
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
|
||||||
|
|
|
@ -10,14 +10,16 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
#
|
'''
|
||||||
#DESCRIPTION.
|
DESCRIPTION.
|
||||||
# Simple FreeCAD's object test for manifold mawater-tight surface.
|
Simple FreeCAD's object test for manifold mawater-tight surface.
|
||||||
|
'''
|
||||||
|
__version__ = '0.2'
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
|
|
||||||
|
|
||||||
def is_object_solid(obj):
|
def is_object_solid(obj):
|
||||||
"""If obj is solid return True"""
|
'''If obj is solid return True'''
|
||||||
if not isinstance(obj, FreeCAD.DocumentObject):
|
if not isinstance(obj, FreeCAD.DocumentObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
158
cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py
Normal file
158
cg/blender/import_fcstd/restruct_hierarchy_by_lcs.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# -*- 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.
|
||||||
|
'''
|
||||||
|
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
|
|
@ -1,62 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
'''
|
||||||
DESCRIPTION.
|
DESCRIPTION.
|
||||||
Basic mesh processing for asset pipeline.
|
Mesh processing for asset creation pipeline.
|
||||||
Early WIP!
|
Setup and prepare highpoly objects.
|
||||||
"""
|
Create and prepare lowpoly_objects.
|
||||||
__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
|
|
||||||
|
|
72
cg/blender/remesh/highpoly_setup.py
Normal file
72
cg/blender/remesh/highpoly_setup.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# -*- 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.
|
||||||
|
'''
|
||||||
|
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!')
|
86
cg/blender/remesh/lowpoly_setup.py
Normal file
86
cg/blender/remesh/lowpoly_setup.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- 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.
|
||||||
|
'''
|
||||||
|
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
|
|
@ -10,13 +10,13 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
__version__ = "0.1"
|
__version__ = '0.1'
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
def cleanup_orphan_data():
|
def cleanup_orphan_data():
|
||||||
"""Removes all data without users"""
|
'''Removes all data without users'''
|
||||||
for block in bpy.data.meshes:
|
for block in bpy.data.meshes:
|
||||||
if block.users == 0:
|
if block.users == 0:
|
||||||
bpy.data.meshes.remove(block)
|
bpy.data.meshes.remove(block)
|
||||||
|
|
161
cg/blender/utils/generative_modifiers.py
Normal file
161
cg/blender/utils/generative_modifiers.py
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# -*- 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.
|
||||||
|
'''
|
||||||
|
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
|
40
cg/blender/utils/object_converter.py
Normal file
40
cg/blender/utils/object_converter.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# -*- 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.
|
||||||
|
'''
|
||||||
|
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
|
33
cg/blender/utils/object_relations.py
Normal file
33
cg/blender/utils/object_relations.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- 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.
|
||||||
|
__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
|
|
@ -1,10 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# original idea from https://github.com/machin3io/MACHIN3tools
|
|
||||||
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
|
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
|
||||||
#
|
#
|
||||||
# -*- 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
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
# 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
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
__version__ = "0.2"
|
__version__ = '0.3'
|
||||||
|
|
||||||
from mathutils import Matrix, Vector, Quaternion
|
from mathutils import Matrix, Vector, Quaternion
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
def apply_transforms(obj, location=False, rotation=False, scale=False):
|
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):
|
def get_loc_matrix(location):
|
||||||
return Matrix.Translation(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]
|
scale_martix[i][i] = scale[i]
|
||||||
return scale_martix
|
return scale_martix
|
||||||
|
|
||||||
|
|
||||||
if location and rotation and scale:
|
if location and rotation and scale:
|
||||||
loc, rot, sca = obj.matrix_world.decompose()
|
loc, rot, sca = obj.matrix_world.decompose()
|
||||||
mesh_martix = get_loc_matrix(loc) @ get_rot_matrix(rot) @ get_sca_matrix(sca)
|
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
|
obj.matrix_world = apply_matrix
|
||||||
else:
|
else:
|
||||||
if location:
|
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:
|
if rotation:
|
||||||
loc, rot, sca = obj.matrix_world.decompose()
|
loc, rot, sca = obj.matrix_world.decompose()
|
||||||
mesh_martix = get_rot_matrix(rot)
|
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.matrix_world = apply_matrix
|
||||||
|
|
||||||
obj.rotation_mode = 'XYZ'
|
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
|
|
@ -10,7 +10,7 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
__version__ = "0.1"
|
__version__ = '0.1'
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -20,7 +20,7 @@ logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def remove_collections(collection_name=None):
|
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:
|
if collection_name:
|
||||||
collection = bpy.data.collections.get(collection_name)
|
collection = bpy.data.collections.get(collection_name)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
__version__ = "0.1"
|
__version__ = '0.1'
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
def shiny_to_rough(shininess):
|
def shiny_to_rough(shininess):
|
||||||
""" convert shiny to roughness """
|
''' convert shiny to roughness '''
|
||||||
a, b = -1.0, 2.0
|
a, b = -1.0, 2.0
|
||||||
c = (shininess / 100.0) - 1.0
|
c = (shininess / 100.0) - 1.0
|
||||||
D = math.pow(b,2) - (4 * a * c)
|
D = math.pow(b,2) - (4 * a * c)
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
'''
|
||||||
DESCRIPTION.
|
DESCRIPTION.
|
||||||
Convert and setup FreeCAD solid objects to 3d assets mesh files.
|
Convert and setup FreeCAD solid objects to 3d assets mesh files.
|
||||||
Support Blender compiled as a Python Module only!
|
Support Blender compiled as a Python Module only!
|
||||||
"""
|
'''
|
||||||
__version__ = "0.3"
|
__version__ = '0.4'
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('../blender/')
|
sys.path.append('../blender/')
|
||||||
from import_fcstd.import_cad_objects import obj_importer
|
from import_fcstd.import_cad_objects import obj_importer
|
||||||
from import_fcstd.import_coordinate_point import empty_importer
|
from import_fcstd.restruct_hierarchy_by_lcs import restruct_hierarchy
|
||||||
from utils.apply_transforms import apply_transforms
|
from import_fcstd.import_coordinate_point import lcs_json_importer
|
||||||
from utils.remove_collections import remove_collections
|
from utils.remove_collections import remove_collections
|
||||||
from utils.cleanup_orphan_data import cleanup_orphan_data
|
from utils.cleanup_orphan_data import cleanup_orphan_data
|
||||||
from utils.sdf_mesh_selector import sdf_mesh_selector
|
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.dae import export_dae
|
||||||
from export.collision import export_col_stl
|
from export.collision import export_col_stl
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -25,6 +27,37 @@ import mathutils
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
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,
|
def freecad_asset_pipeline(fcstd_path,
|
||||||
linear_deflection,
|
linear_deflection,
|
||||||
|
@ -33,28 +66,45 @@ def freecad_asset_pipeline(fcstd_path,
|
||||||
json_path=None,
|
json_path=None,
|
||||||
blend_path=None,
|
blend_path=None,
|
||||||
sdf_path=None):
|
sdf_path=None):
|
||||||
""" Setup FreeCAD scene to CG asset """
|
''' Setup FreeCAD scene to CG asset '''
|
||||||
|
|
||||||
# prepare blend file
|
# prepare blend file
|
||||||
remove_collections()
|
remove_collections()
|
||||||
cleanup_orphan_data()
|
cleanup_orphan_data()
|
||||||
|
|
||||||
# import objects
|
# 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
|
# import lcs
|
||||||
if json_path is None:
|
if not bpy.data.collections.get(lcs_col_name):
|
||||||
json_path = os.path.dirname(fcstd_path)
|
if not bpy.data.collections[lcs_col_name].objects:
|
||||||
for f in os.listdir(os.path.dirname(fcstd_path)):
|
if json_path is None:
|
||||||
if f.endswith('.json'):
|
json_path = os.path.dirname(fcstd_path)
|
||||||
json_file = os.path.join(json_path, f)
|
for f in os.listdir(os.path.dirname(fcstd_path)):
|
||||||
empty_importer(json_file)
|
if f.endswith('.json'):
|
||||||
|
json_file = os.path.join(json_path, f)
|
||||||
|
lcs_json_importer(json_file)
|
||||||
|
|
||||||
# sdf setup WIP
|
# sdf setup WIP
|
||||||
if sdf_path is not None:
|
if sdf_path is not None:
|
||||||
sdf_mesh_selector(sdf_path)
|
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
|
# 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
|
# save blender scene
|
||||||
if blend_path is not None:
|
if blend_path is not None:
|
||||||
|
@ -102,4 +152,4 @@ if __name__ == '__main__':
|
||||||
args.blend_path,
|
args.blend_path,
|
||||||
args.sdf_path)
|
args.sdf_path)
|
||||||
|
|
||||||
logger.info("Assets setup finished without errors")
|
logger.info('Assets setup completed!')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue