From 78e31ea49cce1ab9864bbcdccb97339e650c09b5 Mon Sep 17 00:00:00 2001 From: brothermechanic Date: Tue, 12 Sep 2023 09:07:30 +0000 Subject: [PATCH] [Blender 3.6] UV Packer --- cg/blender/import_cad/build_blender_scene.py | 8 +- cg/blender/{remesh => processing}/README.md | 0 cg/blender/{remesh => processing}/__init__.py | 0 .../{remesh => processing}/highpoly_setup.py | 3 +- cg/blender/processing/lowpoly_setup.py | 140 +++++++++++++ .../restruct_hierarchy_by_lcs.py | 8 +- cg/blender/processing/uv_setup.py | 64 ++++++ cg/blender/remesh/lowpoly_setup.py | 86 -------- cg/blender/utils/blender_render_settings.py | 1 + cg/blender/utils/mesh_tools.py | 83 ++++++++ cg/blender/utils/object_converter.py | 2 +- cg/freecad/utils/freecad_cmd.py | 56 ------ ...rt_freecad_scene.py => freecad_to_json.py} | 96 ++++++--- cg/pipeline/README.md | 2 +- cg/pipeline/cg_pipeline.py | 187 ++++++++++++++++++ cg/pipeline/freecad_to_asset.py | 165 ---------------- cg/utils/cmd_proc.py | 26 +++ cg/utils/custom_parser.py | 55 ++++++ 18 files changed, 635 insertions(+), 347 deletions(-) rename cg/blender/{remesh => processing}/README.md (100%) rename cg/blender/{remesh => processing}/__init__.py (100%) rename cg/blender/{remesh => processing}/highpoly_setup.py (97%) create mode 100644 cg/blender/processing/lowpoly_setup.py rename cg/blender/{import_cad => processing}/restruct_hierarchy_by_lcs.py (97%) create mode 100644 cg/blender/processing/uv_setup.py delete mode 100644 cg/blender/remesh/lowpoly_setup.py create mode 100644 cg/blender/utils/mesh_tools.py delete mode 100644 cg/freecad/utils/freecad_cmd.py rename cg/freecad/utils/{export_freecad_scene.py => freecad_to_json.py} (66%) create mode 100644 cg/pipeline/cg_pipeline.py delete mode 100644 cg/pipeline/freecad_to_asset.py create mode 100644 cg/utils/cmd_proc.py create mode 100644 cg/utils/custom_parser.py diff --git a/cg/blender/import_cad/build_blender_scene.py b/cg/blender/import_cad/build_blender_scene.py index 9359056..db060db 100644 --- a/cg/blender/import_cad/build_blender_scene.py +++ b/cg/blender/import_cad/build_blender_scene.py @@ -35,10 +35,10 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) # COLLECTIONS NAMIG CONVENTION -part_col_name = 'Part' +part_col_name = 'Parts' lcs_col_name = 'LCS' hierarchy_col_name = 'Hierarchy' -lowpoly_col_name = 'Lowpoly Parts' +lowpoly_col_name = 'Lowpoly' # LCS POINT'S SUFFIXES CONVENTION inlet = '_in' outlet = '_out' @@ -91,8 +91,6 @@ def json_to_blend(js_data): bmesh.update() bobj = bpy.data.objects.new(js_obj, bmesh) part_collection.objects.link(bobj) - else: - logger.info('%s has not mesh data!', js_obj) if bobj: fc_placement(bobj, @@ -142,5 +140,5 @@ def json_to_blend(js_data): # TODO # update do not dork - logger.info('Imported %s objects without errors', len(bobjs)) + logger.info('Generated %s objects without errors', len(bobjs)) return bobjs_for_render diff --git a/cg/blender/remesh/README.md b/cg/blender/processing/README.md similarity index 100% rename from cg/blender/remesh/README.md rename to cg/blender/processing/README.md diff --git a/cg/blender/remesh/__init__.py b/cg/blender/processing/__init__.py similarity index 100% rename from cg/blender/remesh/__init__.py rename to cg/blender/processing/__init__.py diff --git a/cg/blender/remesh/highpoly_setup.py b/cg/blender/processing/highpoly_setup.py similarity index 97% rename from cg/blender/remesh/highpoly_setup.py rename to cg/blender/processing/highpoly_setup.py index 048904a..0f3361c 100644 --- a/cg/blender/remesh/highpoly_setup.py +++ b/cg/blender/processing/highpoly_setup.py @@ -17,7 +17,6 @@ Basic mesh processing for asset pipeline. __version__ = '0.2' import logging -import sys import bpy import math @@ -51,7 +50,7 @@ def setup_meshes(bobjs, cleanup=False, sharpness=False, shading=False): 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.edges_select_sharp(sharpness=math.radians(12)) bpy.ops.mesh.mark_sharp() bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project() diff --git a/cg/blender/processing/lowpoly_setup.py b/cg/blender/processing/lowpoly_setup.py new file mode 100644 index 0000000..e75e4b8 --- /dev/null +++ b/cg/blender/processing/lowpoly_setup.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Create lowpoly shells from parts collections. +''' +__version__ = '0.1' + +import logging +import bpy + +from blender.utils.generative_modifiers import shell_remesher +from blender.utils.object_converter import mesh_to_mesh +from blender.utils.object_relations import parenting +from blender.utils.mesh_tools import select_peaks, select_stratched_edges + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Parts' +lcs_col_name = 'LCS' +hierarchy_col_name = 'Hierarchy' +lowpoly_col_name = 'Lowpoly' +# 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, mesh_to_mesh(lowpoly_obj)) + + # fix non_manifold shape + for lowpoly_obj in lowpoly_col.objects: + bpy.ops.object.select_all(action='DESELECT') + lowpoly_obj.select_set(state=True) + bpy.context.view_layer.objects.active = lowpoly_obj + bpy.ops.object.mode_set(mode='EDIT') + # pass 1 + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + select_peaks(lowpoly_obj.data) + bpy.ops.mesh.select_non_manifold() + bpy.ops.mesh.dissolve_mode(use_verts=True, use_boundary_tear=False) + bpy.ops.mesh.delete(type='VERT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + # pass 2 + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + select_peaks(lowpoly_obj.data) + bpy.ops.mesh.select_non_manifold() + bpy.ops.mesh.dissolve_mode(use_verts=True, use_boundary_tear=False) + bpy.ops.mesh.delete(type='VERT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + # pass 3 + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + select_peaks(lowpoly_obj.data) + bpy.ops.mesh.select_non_manifold() + bpy.ops.mesh.dissolve_mode(use_verts=True, use_boundary_tear=False) + bpy.ops.mesh.delete(type='VERT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + # pass 4 + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + select_peaks(lowpoly_obj.data) + bpy.ops.mesh.select_non_manifold() + bpy.ops.mesh.dissolve_mode(use_verts=True, use_boundary_tear=False) + bpy.ops.mesh.delete(type='VERT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + # pass 5 + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(type='EDGE') + select_stratched_edges(lowpoly_obj.data) + bpy.ops.mesh.dissolve_mode(use_verts=True) + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + bpy.ops.mesh.normals_make_consistent() + # final + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(type='FACE') + bpy.ops.object.mode_set(mode='OBJECT') + + logger.info('Lowpoly shells created successfully!') + + return lowpoly_col.objects diff --git a/cg/blender/import_cad/restruct_hierarchy_by_lcs.py b/cg/blender/processing/restruct_hierarchy_by_lcs.py similarity index 97% rename from cg/blender/import_cad/restruct_hierarchy_by_lcs.py rename to cg/blender/processing/restruct_hierarchy_by_lcs.py index eb5e819..1074668 100644 --- a/cg/blender/import_cad/restruct_hierarchy_by_lcs.py +++ b/cg/blender/processing/restruct_hierarchy_by_lcs.py @@ -31,10 +31,10 @@ 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' +parts_col_name = 'Parts' +lcs_col_name = 'LCS' +hierarchy_col_name = 'Hierarchy' +lowpoly_col_name = 'Lowpoly' # LCS POINT'S SUFFIXES CONVENTION inlet = '_in' outlet = '_out' diff --git a/cg/blender/processing/uv_setup.py b/cg/blender/processing/uv_setup.py new file mode 100644 index 0000000..40989a9 --- /dev/null +++ b/cg/blender/processing/uv_setup.py @@ -0,0 +1,64 @@ +# coding: utf-8 +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +UV unwrapping and UV packing processing. +''' +__version__ = '0.1' + +import logging +import math +import bpy + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def uv_unwrap(objs, angle_limit=30): + ''' UV unwrapping and UV packing processing ''' + for obj in objs: + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + # unwrapping + bpy.ops.uv.smart_project(angle_limit=math.radians(angle_limit)) + + # packing + bpy.ops.uv.pack_islands(udim_source='CLOSEST_UDIM', + rotate=True, + rotate_method='ANY', + scale=True, + merge_overlap=False, + margin_method='ADD', + margin=(1 / 256), + pin=False, + pin_method='LOCKED', + shape_method='CONCAVE') + bpy.ops.uv.pack_islands(udim_source='CLOSEST_UDIM', + rotate=True, + rotate_method='ANY', + scale=True, + merge_overlap=False, + margin_method='ADD', + margin=(1 / 256), + pin=False, + pin_method='LOCKED', + shape_method='CONCAVE') + + bpy.ops.object.mode_set(mode='OBJECT') + obj.select_set(False) + + logger.info('UV unwrapping and UV packing processing done successfully!') + return objs diff --git a/cg/blender/remesh/lowpoly_setup.py b/cg/blender/remesh/lowpoly_setup.py deleted file mode 100644 index f3951f7..0000000 --- a/cg/blender/remesh/lowpoly_setup.py +++ /dev/null @@ -1,86 +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. -''' -DESCRIPTION. -Create lowpoly shells from parts collections. -''' -__version__ = '0.1' - -import logging -import sys -import bpy -import math - -from blender.utils.generative_modifiers import shell_remesher -from blender.utils.object_converter import convert_mesh_to_mesh -from blender.utils.object_relations import parenting - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -# COLLECTIONS NAMIG CONVENTION -parts_col_name = 'Import Parts' -lcs_col_name = 'Import LCS' -hierarchy_col_name = 'Import Hierarchy' -lowpoly_col_name = 'Lowpoly Parts' -# LCS POINT'S SUFFIXES CONVENTION -inlet = '_in' -outlet = '_out' -root = '_root' -# CG ASSETS SUFFIXES CONVENTION -hightpoly = '_hp' -lowpoly = '_lp' -render = '_render' - - -def parts_to_shells(hightpoly_part_names): - ''' Create lowpoly shells from parts collections. ''' - logger.info('Lowpoly shells creation launched...') - - lowpoly_col = bpy.data.collections.new(lowpoly_col_name) - bpy.context.scene.collection.children.link(lowpoly_col) - - for part_name in hightpoly_part_names: - # generate lowpoly objects from part collections - lowpoly_name = ('{}{}'.format(part_name, lowpoly)) - lowpoly_mesh = bpy.data.meshes.new(lowpoly_name) - lowpoly_obj = bpy.data.objects.new(lowpoly_name, lowpoly_mesh) - bpy.context.view_layer.update() - part_inlet = bpy.data.objects.get('{}{}'.format(part_name, inlet)) - lowpoly_obj.matrix_world = part_inlet.matrix_world.copy() - parenting(part_inlet, lowpoly_obj) - lowpoly_col.objects.link(lowpoly_obj) - - shell_remesher(lowpoly_obj, 'remesh_nodes', 'robossembler') - part_col = bpy.data.collections[('{}{}'.format(part_name, hightpoly))] - lowpoly_obj.modifiers['remesh_nodes']['Input_0'] = part_col - - remesh_voxel = lowpoly_obj.modifiers.new('remesh_voxel', type='REMESH') - remesh_voxel.mode = 'VOXEL' - remesh_voxel.voxel_size = 0.001 - - remesh_sharp = lowpoly_obj.modifiers.new('remesh_sharp', type='REMESH') - remesh_sharp.mode = 'SHARP' - remesh_sharp.octree_depth = 7 - - decimate = lowpoly_obj.modifiers.new('decimate', type='DECIMATE') - decimate.decimate_type = 'COLLAPSE' - decimate.ratio = 0.1 - - # apply all modifiers to mesh - parenting(part_inlet, convert_mesh_to_mesh(lowpoly_obj)) - - logger.info('Lowpoly shells created successfully!') - - return lowpoly_col.objects diff --git a/cg/blender/utils/blender_render_settings.py b/cg/blender/utils/blender_render_settings.py index e5727be..56a072d 100644 --- a/cg/blender/utils/blender_render_settings.py +++ b/cg/blender/utils/blender_render_settings.py @@ -2,6 +2,7 @@ """ DESCRIPTION. This script setup render settings for reduce rendertime! +DEPRECATED """ __version__ = "0.1" diff --git a/cg/blender/utils/mesh_tools.py b/cg/blender/utils/mesh_tools.py new file mode 100644 index 0000000..9cffcb7 --- /dev/null +++ b/cg/blender/utils/mesh_tools.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2023 Ilia Kurochkin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +''' +DESCRIPTION. +Various mesh tools for Edit Mode. +''' +__version__ = '0.1' + +import bmesh +from math import radians + + +def select_peaks(me, peak_limit_angle=60, peak_accuracy_angle=10): + ''' Select sharp vertices that stand alone. ''' + bm = bmesh.from_edit_mesh(me) + + def is_sharp(vert, eps=radians(peak_limit_angle)): + sharps = [] + face_before = None + for face in vert.link_faces: + if face_before: + face_angle = face.normal.angle(face_before.normal) + if face_angle > radians(peak_accuracy_angle): + angle = vert.normal.angle(face.normal) + if angle > eps: + sharps.append(angle) + face_before = face + return ( + (len(sharps) + 1) == len(vert.link_faces) + or (len(sharps) + 2) == len(vert.link_faces) + ) + + def non_single(vert): + for edge in vert.link_edges: + if edge.other_vert(vert).select: + return False + return True + + for v in bm.verts: + v.select_set( + is_sharp(v) + ) + + for v in bm.verts: + if v.select: + v.select_set( + non_single(v) + ) + + bmesh.update_edit_mesh(me) + return me + + +def select_zero_faces(me): + ''' Select very small faces. ''' + bm = bmesh.from_edit_mesh(me) + [f.select_set(True) for f in bm.faces if f.calc_area() < 1e-7] + bmesh.update_edit_mesh(me) + return me + + +def select_stratched_edges(me, edge_length_limit=0.002): + ''' Select very stratched edges of small faces. ''' + bm = bmesh.from_edit_mesh(me) + faces_stratched = [f for f in bm.faces if f.calc_area() < 1e-6] + for face in faces_stratched: + edges_lengths = {e: e.calc_length() for e in face.edges} + edge_max_length = max(edges_lengths.values()) + if edge_max_length > edge_length_limit: + edge_max = [k for k, v in edges_lengths.items() if v == edge_max_length][0] + edge_max.select_set(True) + bmesh.update_edit_mesh(me) + return me diff --git a/cg/blender/utils/object_converter.py b/cg/blender/utils/object_converter.py index fdf4a10..ab1ca76 100644 --- a/cg/blender/utils/object_converter.py +++ b/cg/blender/utils/object_converter.py @@ -19,7 +19,7 @@ __version__ = '0.1' import bpy -def convert_mesh_to_mesh(obj): +def mesh_to_mesh(obj): ''' Convert all deformers and modifiers of object to it's mesh. ''' if obj and obj.type == 'MESH': deg = bpy.context.evaluated_depsgraph_get() diff --git a/cg/freecad/utils/freecad_cmd.py b/cg/freecad/utils/freecad_cmd.py deleted file mode 100644 index c8d2bae..0000000 --- a/cg/freecad/utils/freecad_cmd.py +++ /dev/null @@ -1,56 +0,0 @@ -# coding: utf-8 -#!/usr/bin/env python -# 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 subprocess -import json - - -def freecad_proc(*args, **kwargs): - command = [ - args[0], - args[1], - - # general property - kwargs['filename'], - kwargs['tesselation_method'], - kwargs['linear_deflection'], - kwargs['angular_deflection'], - kwargs['fem_size'], - kwargs['skiphidden'], - kwargs['nonsolid_property'], - ] - - proc = subprocess.run(command, - check=True, - stdout=subprocess.PIPE, - encoding='utf-8') - - return json.loads(proc.stdout.split('FreeCAD ')[0]) - - -kwargs = {} -kwargs['filename'] = '/' -kwargs['tesselation_method'] = 'Standard' -kwargs['linear_deflection'] = '0.1' -kwargs['angular_deflection'] = '30.0' -kwargs['fem_size'] = '10.0' -kwargs['skiphidden'] = 'True' -kwargs['nonsolid_property'] = 'Robossembler_NonSolid' - -js_data = freecad_proc( - 'freecadcmd', - '//cg/freecad/utils/export_freecad_scene.py', - **kwargs) - -print(js_data) diff --git a/cg/freecad/utils/export_freecad_scene.py b/cg/freecad/utils/freecad_to_json.py similarity index 66% rename from cg/freecad/utils/export_freecad_scene.py rename to cg/freecad/utils/freecad_to_json.py index 8f41a31..d48a38d 100644 --- a/cg/freecad/utils/export_freecad_scene.py +++ b/cg/freecad/utils/freecad_to_json.py @@ -24,26 +24,20 @@ import Mesh import MeshPart import logging import math -import sys from freecad.utils.is_object_solid import is_object_solid +from utils.custom_parser import CustomArgumentParser logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def freecad_to_json(filename, - tesselation_method='Standard', - linear_deflection=0.1, - angular_deflection=30.0, - fem_size=50.0, - skiphidden=True, - nonsolid_property='Robossembler_NonSolid'): +def freecad_to_json(**kwargs): ''' Reads a FreeCAD .FCStd file and return json assembly. ''' scene = {} js_objs = {} - doc = FreeCAD.open(filename) + doc = FreeCAD.open(kwargs['fcstd_path']) docname = doc.Name # collect all materials @@ -55,7 +49,7 @@ def freecad_to_json(filename, for obj in doc.Objects: js_obj = {} - if skiphidden: + if kwargs['skiphidden']: if not obj.Visibility: continue @@ -66,26 +60,26 @@ def freecad_to_json(filename, js_obj['type'] = 'PART' # filter for nonsolids - if is_object_solid(obj) or hasattr(obj, nonsolid_property): + if is_object_solid(obj) or hasattr(obj, kwargs['property_forse_nonsolid']): # create mesh from shape shape = obj.Shape shape = obj.Shape.copy() shape.Placement = obj.Placement.inverse().multiply(shape.Placement) meshfromshape = doc.addObject('Mesh::Feature', 'Mesh') - if tesselation_method == 'Standard': + if kwargs['tesselation_method'] == 'Standard': meshfromshape.Mesh = MeshPart.meshFromShape( Shape=shape, - LinearDeflection=linear_deflection, - AngularDeflection=math.radians(angular_deflection), + LinearDeflection=kwargs['linear_deflection'], + AngularDeflection=math.radians(kwargs['angular_deflection']), Relative=False) - elif tesselation_method == 'FEM': + elif kwargs['tesselation_method'] == 'FEM': meshfromshape.Mesh = MeshPart.meshFromShape( Shape=shape, - MaxLength=fem_size) + MaxLength=kwargs['fem_size']) else: raise TypeError('Wrong tesselation method! ' 'Standard and FEM methods are supported only!') - break + t = meshfromshape.Mesh.Topology verts = [[v.x, v.y, v.z] for v in t[0]] faces = t[1] @@ -128,18 +122,66 @@ def freecad_to_json(filename, FreeCAD.closeDocument(docname) - scene[filename] = js_objs + scene[kwargs['fcstd_path']] = js_objs - logger.info('Stored %s objects without errors', len(js_objs)) + logger.info('Passed %s objects without errors', len(js_objs)) print(json.dumps(scene)) -args = sys.argv[2:] -for arg in args[2:5]: - args[args.index(arg)] = float(arg) -for arg in args[5:6]: - args[args.index(arg)] = bool(arg) -#for num, item in enumerate(args): -# print(num, type(item)) -freecad_to_json(*args) +# to run script via FreeCADCmd +parser = CustomArgumentParser() +parser.add_argument( + '--fcstd_path', + type=str, + help='Path to source FreeCAD scene', + required=True +) +parser.add_argument( + '--tesselation_method', + type=str, + help='Select tesselation method: Standard or FEM.', + default='Standard', + required=False +) +parser.add_argument( + '--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( + '--fem_size', + type=float, + help='For FEM method only! Finite element size in mm', + default=50.0, + required=False +) +parser.add_argument( + '--skiphidden', + type=bool, + help='Skip processing for hidden FreeCAD objects', + default=True, + required=False +) +parser.add_argument( + '--property_forse_nonsolid', + type=str, + help='FreeCAD property to enable processing for nonsolid objects', + default='Robossembler_NonSolid', + required=False +) + +kwargs = vars(parser.parse_known_args()[0]) + +freecad_to_json(**kwargs) + +logger.info('FreeCAD scene passed!') diff --git a/cg/pipeline/README.md b/cg/pipeline/README.md index 9485cbc..316da5f 100644 --- a/cg/pipeline/README.md +++ b/cg/pipeline/README.md @@ -1,4 +1,4 @@ -### freecad_to_asset.py +### cg_pipeline.py Пакетное производство 3д ассетов из объектов CAD сцены. diff --git a/cg/pipeline/cg_pipeline.py b/cg/pipeline/cg_pipeline.py new file mode 100644 index 0000000..3a50296 --- /dev/null +++ b/cg/pipeline/cg_pipeline.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python +''' +DESCRIPTION. +Convert and setup FreeCAD solid objects to 3d assets. +Support Blender compiled as a Python Module only! +''' +__version__ = '0.6' +import json +import logging +import os + +from blender.utils.remove_collections import remove_collections +from blender.utils.cleanup_orphan_data import cleanup_orphan_data +from utils.cmd_proc import cmd_proc +from blender.import_cad.build_blender_scene import json_to_blend +from blender.processing.restruct_hierarchy_by_lcs import restruct_hierarchy +from blender.processing.highpoly_setup import setup_meshes +from blender.processing.lowpoly_setup import parts_to_shells +from blender.processing.uv_setup import uv_unwrap +import bpy +import mathutils +# from export.dae import export_dae +# from export.collision import export_col_stl + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +''' +IMPORT COLLECTIONS NAMIG CONVENTION: +Parts - collection for mesh objects +LCS - collection for location points +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) +''' + +# ENV +freecadcmd = 'freecadcmd' +fcstd_data_script = 'freecad_to_json.py' +# COLLECTIONS NAMIG CONVENTION +parts_col_name = 'Parts' +lcs_col_name = 'LCS' +hierarchy_col_name = 'Hierarchy' +lowpoly_col_name = 'Lowpoly' +# LCS POINT'S SUFFIXES CONVENTION +inlet = '_in' +outlet = '_out' +root = '_root' +# CG ASSETS SUFFIXES CONVENTION +hightpoly = '_hp' +lowpoly = '_lp' +render = '_render' + + +def cg_pipeline(**kwargs): + ''' CG asset creation pipeline ''' + + # prepare blend file + remove_collections() + cleanup_orphan_data() + + # convert FreeCAD scene to Blender scene + objs_for_render = json_to_blend( + json.loads( + cmd_proc(freecadcmd, + fcstd_data_script, + '--', + **kwargs + ).split('FreeCAD ')[0] + ) + ) + + # restructuring hierarchy by lcs points + lcs_objects = restruct_hierarchy() + + # save blender scene + if kwargs['blend_path'] is not None: + if not os.path.isdir(os.path.dirname(kwargs['blend_path'])): + os.makedirs(os.path.dirname(kwargs['blend_path'])) + bpy.ops.wm.save_as_mainfile(filepath=kwargs['blend_path']) + + # prepare highpoly + setup_meshes(objs_for_render, sharpness=True, shading=True) + # prepare lowpoly + part_names = [p.name.split(inlet)[0] for p in lcs_objects if p.name.endswith(inlet)] + lowpoly_objs = parts_to_shells(part_names) + uv_unwrap(lowpoly_objs) + + # save blender scene + if kwargs['blend_path'] is not None: + if not os.path.isdir(os.path.dirname(kwargs['blend_path'])): + os.makedirs(os.path.dirname(kwargs['blend_path'])) + bpy.ops.wm.save_as_mainfile(filepath=kwargs['blend_path']) + + # export all objects + if kwargs['mesh_export_path'] is not None: + obs = bpy.context.selected_objects + for ob in obs: + ob.matrix_world = mathutils.Matrix() + for ob in obs: + ob.select_set(state=True) + export_dae(kwargs['mesh_export_path']) + export_col_stl(kwargs['mesh_export_path']) + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description='Convert and setup FreeCAD solid objects to 3d assets mesh files.' + ) + parser.add_argument( + '--fcstd_path', + type=str, + help='Path to source FreeCAD scene', + required=True + ) + parser.add_argument( + '--tesselation_method', + type=str, + help='Select tesselation method: Standard or FEM.', + default='Standard', + required=False + ) + parser.add_argument( + '--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( + '--fem_size', + type=float, + help='For FEM method only! Finite element size in mm', + default=50.0, + required=False + ) + parser.add_argument( + '--skiphidden', + type=bool, + help='Skip processing for hidden FreeCAD objects', + default=True, + required=False + ) + parser.add_argument( + '--property_forse_nonsolid', + type=str, + help='FreeCAD property to enable processing for nonsolid objects', + default='Robossembler_NonSolid', + required=False + ) + parser.add_argument( + '--mesh_export_path', + type=str, help='Path for export meshes', + required=False + ) + parser.add_argument( + '--blend_path', + type=str, + help='Path for export blend assembly file', + required=False + ) + + args = parser.parse_args() + + kwargs = {key: getattr(args, key) for key in dir(args) if not key.startswith('_')} + + cg_pipeline(**kwargs) + + logger.info('CG Pipeline Completed!') diff --git a/cg/pipeline/freecad_to_asset.py b/cg/pipeline/freecad_to_asset.py deleted file mode 100644 index 81f3ba2..0000000 --- a/cg/pipeline/freecad_to_asset.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -''' -DESCRIPTION. -Convert and setup FreeCAD solid objects to 3d assets mesh files. -Support Blender compiled as a Python Module only! -''' -__version__ = '0.5' - -import logging -import os -import sys -sys.path.append('../') -from freecad.utils.export_freecad_scene import freecad_to_json - -from blender.import_cad.build_blender_scene import json_to_blend -from blender.import_cad.restruct_hierarchy_by_lcs import restruct_hierarchy -from blender.import_cad.import_coordinate_point import lcs_json_importer -from blender.utils.remove_collections import remove_collections -from blender.utils.cleanup_orphan_data import cleanup_orphan_data -from blender.utils.sdf_mesh_selector import sdf_mesh_selector -from blender.remesh.highpoly_setup import setup_meshes -from blender.remesh.lowpoly_setup import parts_to_shells - -from export.dae import export_dae -from export.collision import export_col_stl -import bpy -import mathutils - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -''' -IMPORT COLLECTIONS NAMIG CONVENTION: -Import Parts - collection for mesh objects -Import LCS - collection for location points -Import Hierarchy - collection for hierarchy locators - -LCS POINT'S SUFFIXES CONVENTION: -'_in' - inlet suffix -'_out' - outlet suffix -'_root' - root suffix - -CG ASSETS SUFFIXES CONVENTION: -'_hp' - hightpoly asset (reference baking source) -'_lp' - lowpoly asset (prepared for game engines) -'_render' - root suffix (prepared for render engines) -''' - - -# COLLECTIONS NAMIG CONVENTION -parts_col_name = 'Import Parts' -lcs_col_name = 'Import LCS' -hierarchy_col_name = 'Import Hierarchy' -lowpoly_col_name = 'Lowpoly Parts' -# LCS POINT'S SUFFIXES CONVENTION -inlet = '_in' -outlet = '_out' -root = '_root' -# CG ASSETS SUFFIXES CONVENTION -hightpoly = '_hp' -lowpoly = '_lp' -render = '_render' - - -def freecad_asset_pipeline(fcstd_path, - tesselation_method, - linear_deflection, - angular_deflection, - fem_size, - mesh_export_path=None, - json_path=None, - blend_path=None, - sdf_path=None): - ''' Setup FreeCAD scene to CG asset ''' - - # prepare blend file - remove_collections() - cleanup_orphan_data() - - ## convert FreeCAD scene to Blender scene - # TODO - objs_for_render = json_to_blend(freecad_to_json(**kwargs)) - - # restructuring hierarchy by lcs points - lcs_objects = restruct_hierarchy() - - # import lcs - if not bpy.data.collections.get(lcs_col_name): - if not bpy.data.collections[lcs_col_name].objects: - if json_path is None: - json_path = os.path.dirname(fcstd_path) - for f in os.listdir(os.path.dirname(fcstd_path)): - if f.endswith('.json'): - json_file = os.path.join(json_path, f) - lcs_json_importer(json_file) - - # sdf setup WIP - if sdf_path is not None: - sdf_mesh_selector(sdf_path) - - # save blender scene - if blend_path is not None: - if not os.path.isdir(os.path.dirname(blend_path)): - os.makedirs(os.path.dirname(blend_path)) - bpy.ops.wm.save_as_mainfile(filepath=blend_path) - - # retopo - setup_meshes(objs_for_render, sharpness=True, shading=True) - - part_names = [p.name.split(inlet)[0] for p in lcs_objects if p.name.endswith(inlet)] - parts_to_shells(part_names) - - # save blender scene - if blend_path is not None: - 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) - - # export all objects - if mesh_export_path is not None: - obs = bpy.context.selected_objects - for ob in obs: - ob.matrix_world = mathutils.Matrix() - for ob in obs: - ob.select_set(state=True) - export_dae(mesh_export_path) - export_col_stl(mesh_export_path) - - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser( - description='Convert and setup FreeCAD solid objects to 3d assets mesh files.') - parser.add_argument( - '--fcstd_path', type=str, help='Path to source FreeCAD scene', required=True) - parser.add_argument( - '--tesselation_method', type=str, help='Select tesselation method: Standard or FEM.', default='Standard', required=False) - parser.add_argument( - '--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( - '--fem_size', type=float, help='For FEM method only! Finite element size in mm', default=1.0, required=False) - parser.add_argument( - '--mesh_export_path', type=str, help='Path for export meshes', required=False) - parser.add_argument( - '--json_path', type=str, help='Path to DIR with coordinate points jsons', required=False) - parser.add_argument( - '--blend_path', type=str, help='Path for export blend assembly file', required=False) - parser.add_argument( - '--sdf_path', type=str, help='Path to source SDF assembly file', required=False) - args = parser.parse_args() - - freecad_asset_pipeline(args.fcstd_path, - args.tesselation_method, - args.linear_deflection, - args.angular_deflection, - args.fem_size, - args.mesh_export_path, - args.json_path, - args.blend_path, - args.sdf_path) - - logger.info('Assets setup completed!') diff --git a/cg/utils/cmd_proc.py b/cg/utils/cmd_proc.py new file mode 100644 index 0000000..cfc7379 --- /dev/null +++ b/cg/utils/cmd_proc.py @@ -0,0 +1,26 @@ +# 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 subprocess + + +def cmd_proc(*args, **kwargs): + command = list(args) + for akey, aval in kwargs.items(): + command.append(f'--{akey}') + command.append(str(aval)) + + return subprocess.run(command, + check=True, + stdout=subprocess.PIPE, + encoding='utf-8').stdout diff --git a/cg/utils/custom_parser.py b/cg/utils/custom_parser.py new file mode 100644 index 0000000..cdba040 --- /dev/null +++ b/cg/utils/custom_parser.py @@ -0,0 +1,55 @@ +# coding: utf-8 +# https://blender.stackexchange.com/a/134596 + +import argparse +import sys + + +class CustomArgumentParser(argparse.ArgumentParser): + """ + This class is identical to its superclass, except for the parse_args + method (see docstring). It resolves the ambiguity generated when calling + Blender from the CLI with a python script, and both Blender and the script + have arguments. E.g., the following call will make Blender crash because + it will try to process the script's -a and -b flags: + >>> blender --python my_script.py -a 1 -b 2 + + To bypass this issue this class uses the fact that Blender will ignore all + arguments given after a double-dash ('--'). The approach is that all + arguments before '--' go to Blender, arguments after go to the script. + The following calls work fine: + >>> blender --python my_script.py -- -a 1 -b 2 + >>> blender --python my_script.py -- + """ + + @staticmethod + def _get_argv_after_doubledash(): + """ + Given the sys.argv as a list of strings, this method returns the + sublist right after the '--' element (if present, otherwise returns + an empty list). + """ + try: + idx = sys.argv.index("--") + return sys.argv[idx+1:] # the list after '--' + except ValueError as e: # '--' not in the list: + return None + + # overrides superclass + def parse_args(self, args=None, namespace=None): + """ + This method is expected to behave identically as in the superclass, + except that the sys.argv list will be pre-processed using + _get_argv_after_doubledash before. See the docstring of the class for + usage examples and details. + """ + return super().parse_args( + args=args or self._get_argv_after_doubledash(), + namespace=namespace + ) + + def parse_known_args(self, args=None, namespace=None): + return super().parse_known_args( + args=args or self._get_argv_after_doubledash(), + namespace=namespace + )