framework/cg/pipeline/cg_pipeline.py

357 lines
12 KiB
Python
Raw Normal View History

2023-09-12 09:07:30 +00:00
# -*- coding: utf-8 -*-
#!/usr/bin/env python
'''
DESCRIPTION.
2023-12-18 07:48:45 +00:00
CG asset (models) creation pipelines from FreeCAD data.
2023-09-12 09:07:30 +00:00
Support Blender compiled as a Python Module only!
'''
2023-12-18 07:48:45 +00:00
__version__ = '0.8'
2023-09-12 09:07:30 +00:00
import json
import logging
2023-11-13 13:07:33 +00:00
import shutil
2023-09-12 09:07:30 +00:00
import os
from itertools import zip_longest
2023-09-12 09:07:30 +00:00
from blender.utils.cleanup_orphan_data import cleanup_orphan_data
2023-11-13 13:07:33 +00:00
from blender.utils.collection_tools import (copy_collections_recursive,
remove_collections_with_objects)
2023-09-12 09:07:30 +00:00
from utils.cmd_proc import cmd_proc
from blender.import_cad.build_blender_scene import json_to_blend
2023-12-18 07:48:45 +00:00
from blender.processing.restruct_hierarchy import (hierarchy_assembly,
hierarchy_separated_parts,
hierarchy_mono_part)
from blender.processing.render_assets import setup_meshes
2023-10-16 12:21:34 +00:00
from blender.processing.midpoly_setup import hightpoly_collections_to_midpoly
from blender.processing.visual_assets import parts_to_shells
2023-09-12 09:07:30 +00:00
from blender.processing.uv_setup import uv_unwrap
2023-11-13 13:07:33 +00:00
from blender.texturing.bake_submitter import bw_bake
2023-10-26 10:40:04 +03:00
from blender.texturing.composing import compose_baked_textures
from blender.texturing.shading import assign_pbr_material
from blender.export.dae import export_dae
from blender.export.stl import export_stl
2023-11-13 13:07:33 +00:00
from blender.export.fbx import export_fbx
2023-12-18 07:48:45 +00:00
from blender.export.ply import export_ply
2023-09-12 09:07:30 +00:00
import bpy
2023-11-13 13:07:33 +00:00
# TODO Path
freecad_to_json_script = 'freecad_to_json.py'
2023-09-12 09:07:30 +00:00
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
2023-11-13 13:07:33 +00:00
# TODO NIX
freecad_bin = 'freecadcmd'
# TODO NIX BakwWrangler/nodes/node_tree.py:659
blender_bin = 'blender'
# TODO WEBAPP
cg_config = {}
"""
2023-11-13 13:07:33 +00:00
'parts_col_name': 'Parts',
'midpoly_col_name': 'Midpoly',
'visual_col_name': 'Lowpoly',
2023-11-13 13:07:33 +00:00
'lcs_inlet': 'in',
'lcs_outlet': 'out',
'lcs_root': 'root',
'render': 'hp',
2023-11-13 13:07:33 +00:00
'midpoly': 'mp',
'visual': 'lp',
2023-11-13 13:07:33 +00:00
'render': 'render',
}
"""
2023-09-12 09:07:30 +00:00
defined_pipeline_list = ['cad', 'render', 'visual', 'collision', 'baking', 'export']
2023-12-18 07:48:45 +00:00
2023-09-12 09:07:30 +00:00
def cg_pipeline(**kwargs):
2023-12-18 07:48:45 +00:00
''' CG assets (models) creation pipeline '''
# set defaults
# ______________________________
assembly_name = (
kwargs['fcstd_path'].replace('\\', '/').rpartition('/')[2].rpartition('.')[0])
# clear other paths from kwargs (freecad issue)
2023-11-13 13:07:33 +00:00
parts_sequence_path = kwargs.pop('parts_sequence_path', None)
blend_path = kwargs.pop('blend_path', None)
2023-11-13 13:07:33 +00:00
export_path = kwargs.pop('export_path', None)
2023-12-18 07:48:45 +00:00
if kwargs['pipeline_type'] not in defined_pipeline_list:
return logger.error('Pipeline type %s is not defined!', kwargs['pipeline_type'])
2023-11-13 13:07:33 +00:00
# output file management
if not blend_path:
blend_path = kwargs['fcstd_path'].replace('\\', '/').rpartition('/')[0]
if not export_path:
2023-12-18 07:48:45 +00:00
export_path = os.path.join(blend_path, 'models').replace('\\', '/')
2023-11-13 13:07:33 +00:00
os.makedirs(blend_path, exist_ok=True)
2023-12-18 07:48:45 +00:00
#blend_file = os.path.join(blend_path, f'{assembly_name}.blend').replace('\\', '/')
2023-11-13 13:07:33 +00:00
remove_collections_with_objects()
2023-09-12 09:07:30 +00:00
cleanup_orphan_data()
2023-12-18 07:48:45 +00:00
# 1) сonvert FreeCAD scene to Blender scene
# ______________________________
cad_objects = json_to_blend(
2023-09-12 09:07:30 +00:00
json.loads(
2023-11-13 13:07:33 +00:00
cmd_proc(freecad_bin,
freecad_to_json_script,
'--',
**kwargs
).split('FreeCAD ')[0]
)
2023-09-12 09:07:30 +00:00
)
2023-12-18 07:48:45 +00:00
# Save original cad setup as blender scene
if kwargs['pipeline_type'] == 'cad':
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
return blend_file
# 2) prepare render (depend of cad_objects['objs_lcs'] and parts_sequence)
2023-12-18 07:48:45 +00:00
# ______________________________
if parts_sequence_path and os.path.isfile(parts_sequence_path):
with open(parts_sequence_path, 'r', encoding='utf-8') as sequence_file:
parts_sequence = json.load(sequence_file)
else:
parts_sequence = None
lcs_inlets = [
lcs_name for lcs_name in cad_objects['objs_lcs']
if (bpy.data.objects[lcs_name].get('Robossembler_SocketFlow') == 'inlet'
and bpy.data.objects[lcs_name].get('Robossembler_DefaultOrigin'))]
2023-09-12 09:07:30 +00:00
2023-12-18 07:48:45 +00:00
# input cases
# 1 case
if parts_sequence and len(lcs_inlets) > 1:
2023-12-18 07:48:45 +00:00
logger.info('Parts assembling sequence and LCS points found! '
'Launch "hierarchy_assembly" restructuring pipeline.')
part_names = hierarchy_assembly(
cad_objects['objs_lcs'], parts_sequence)
2023-12-18 07:48:45 +00:00
# 2 case
elif parts_sequence and len(lcs_inlets) < 2:
2023-12-18 07:48:45 +00:00
return logger.error('Assembly do not have enough LCS points!')
# 3 case
elif not parts_sequence and lcs_inlets:
2023-12-18 07:48:45 +00:00
logger.info('Parts assembling sequence not found! '
'Launch "hierarchy_separated_parts" restructuring pipeline.')
part_names = hierarchy_separated_parts(
cad_objects['objs_lcs'])
2023-12-18 07:48:45 +00:00
# 4 case
elif not parts_sequence and not lcs_inlets:
2023-12-18 07:48:45 +00:00
logger.info('Parts assembling sequence and LCS points not found! '
'Launch "hierarchy_mono_part" restructuring pipeline.')
part_names = hierarchy_mono_part()
2023-11-13 13:07:33 +00:00
if not part_names:
2023-12-18 07:48:45 +00:00
return logger.error('Can not generate parts!')
2023-10-25 10:54:36 +00:00
# setup renders with materials only
2023-12-18 07:48:45 +00:00
if cad_objects['objs_foreground']:
setup_meshes(cad_objects['objs_foreground'],
2023-11-13 13:07:33 +00:00
sharpness=True, shading=True)
# setup all renders
else:
2023-12-18 07:48:45 +00:00
setup_meshes(cad_objects['objs_background'],
2023-11-13 13:07:33 +00:00
sharpness=True, shading=True)
# Save render setup as blender scene
if kwargs['pipeline_type'] == 'render':
2023-12-18 07:48:45 +00:00
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
logger.info('%s original hightpoly collections ready!', len(part_names))
return blend_file
# 3) prepare midpoly
# ______________________________
tmp_col_name = copy_collections_recursive(
2023-11-13 13:07:33 +00:00
bpy.data.collections[cg_config['parts_col_name']],
suffix=cg_config['midpoly']
2023-10-16 12:21:34 +00:00
)
2023-11-13 13:07:33 +00:00
midpoly_obj_names = hightpoly_collections_to_midpoly(
2023-12-18 07:48:45 +00:00
tmp_col_name, part_names, **cg_config)
# Save midpoly setup as blender scene
if kwargs['pipeline_type'] == 'midpoly':
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
logger.info('%s midpoly objects ready!', len(midpoly_obj_names))
return blend_file
2023-10-16 12:21:34 +00:00
# 4) prepare visual
2023-12-18 07:48:45 +00:00
# ______________________________
visual_obj_names = parts_to_shells(part_names, **cg_config)
uv_unwrap(visual_obj_names)
2023-09-12 09:07:30 +00:00
# Save visual setup as blender scene
if kwargs['pipeline_type'] == 'visual':
2023-12-18 07:48:45 +00:00
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
logger.info('%s visual objects ready!', len(visual_obj_names))
2023-12-18 07:48:45 +00:00
return blend_file
2023-11-13 13:07:33 +00:00
2023-12-18 07:48:45 +00:00
# 5) bake textures
# ______________________________
if kwargs['textures_resolution'] == 0:
logger.info('Baking pipeline has been canceled!')
else:
2023-11-13 13:07:33 +00:00
textures_path = os.path.join(blend_path, 'textures').replace('\\', '/')
bake_paths = bw_bake(visual_obj_names,
2023-11-13 13:07:33 +00:00
textures_path,
kwargs['textures_resolution'],
**cg_config)
# Save baking result
2023-12-18 07:48:45 +00:00
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, 'baking')
).replace('\\', '/')
2023-11-13 13:07:33 +00:00
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
# 5 prepare textures
bpy.ops.wm.quit_blender()
compose_baked_textures(textures_path, bake_paths, kwargs['textures_resolution'])
for bake_path in bake_paths:
shutil.rmtree(bake_path)
bpy.ops.wm.open_mainfile(filepath=blend_file)
assign_pbr_material(visual_obj_names, textures_path)
2023-11-13 13:07:33 +00:00
bpy.ops.file.make_paths_relative()
2023-12-18 07:48:45 +00:00
# Save baking setup as blender scene
if kwargs['pipeline_type'] == 'baking':
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
2023-11-13 13:07:33 +00:00
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
logger.info('%s visual objects baked!', len(visual_obj_names))
2023-12-18 07:48:45 +00:00
return blend_file
2023-11-13 13:07:33 +00:00
2023-12-18 07:48:45 +00:00
# 6 export object meshes
# ______________________________
# TODO asset manager
# Save blender scene
if kwargs['pipeline_type'] == 'export':
blend_file = os.path.join(
blend_path,
'{}_{}.blend'.format(assembly_name, kwargs['pipeline_type'])
).replace('\\', '/')
bpy.ops.wm.save_as_mainfile(filepath=blend_file)
2023-11-13 13:07:33 +00:00
for part_name in part_names:
2023-12-18 07:48:45 +00:00
export_fbx(
obj_name=f'{part_name}_{cg_config["visual"]}',
2023-12-18 07:48:45 +00:00
path=os.path.join(export_path, part_name, 'meshes').replace('\\', '/'))
export_ply(
obj_name=f'{part_name}_{cg_config["midpoly"]}',
path=os.path.join(export_path, part_name, 'meshes').replace('\\', '/'))
export_dae(
2023-11-13 13:07:33 +00:00
obj_name=f'{part_name}_{cg_config["midpoly"]}',
2023-12-18 07:48:45 +00:00
path=os.path.join(export_path, part_name, 'meshes').replace('\\', '/'))
export_stl(
obj_name=f'{part_name}_{cg_config["visual"]}',
2023-12-18 07:48:45 +00:00
path=os.path.join(export_path, part_name, 'meshes').replace('\\', '/'))
logger.info('%s parts exported!', len(part_names))
return blend_file
2023-09-12 09:07:30 +00:00
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.01,
2023-09-12 09:07:30 +00:00
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
)
2023-12-18 07:48:45 +00:00
parser.add_argument(
'--pipeline_type',
type=str,
help='Set pipeline type: "cad", "render","visual", "collision", "baking", "export"',
2023-12-18 07:48:45 +00:00
default='export',
required=False
)
2023-09-12 09:07:30 +00:00
parser.add_argument(
2023-11-13 13:07:33 +00:00
'--parts_sequence_path',
2023-12-18 07:48:45 +00:00
type=str,
help='Path to parts assembling sequence json file.',
2023-11-13 13:07:33 +00:00
required=False
)
parser.add_argument(
'--export_path',
2023-12-18 07:48:45 +00:00
type=str,
help='Path for export assets. If not, fcstd_path will be used instead.',
2023-09-12 09:07:30 +00:00
required=False
)
parser.add_argument(
'--blend_path',
type=str,
2023-11-13 13:07:33 +00:00
help='Path for blender scene. If not, fcstd_path will be used instead.',
required=False
)
parser.add_argument(
'--textures_resolution',
type=int,
help='Set baking texture resolution. Recomended - 4096 pix ',
default=512,
2023-09-12 09:07:30 +00:00
required=False
)
args = parser.parse_args()
2023-11-13 13:07:33 +00:00
cg_kwargs = {key: getattr(args, key) for key in dir(args) if not key.startswith('_')}
2023-09-12 09:07:30 +00:00
2023-11-13 13:07:33 +00:00
cg_pipeline(**cg_kwargs)