[Blender 3.6] UV Packer
This commit is contained in:
parent
80e3c58913
commit
78e31ea49c
18 changed files with 635 additions and 347 deletions
7
cg/blender/processing/README.md
Normal file
7
cg/blender/processing/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
## Пакетная обработка всех объектов сцены в Blender
|
||||
|
||||
Совместим как с прикладной версией блендера, так и с модульной (скомпилированой в качестве модуля).
|
||||
|
||||
Подходит для запуска как в качестве модуля, так и внутри blender сцены.
|
||||
|
||||
Производится запекание локальных координат, выделение острых граней, обработка затенения полигонов.
|
7
cg/blender/processing/__init__.py
Normal file
7
cg/blender/processing/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
DESCRIPTION.
|
||||
Mesh processing for asset creation pipeline.
|
||||
Setup and prepare highpoly objects.
|
||||
Create and prepare lowpoly_objects.
|
||||
'''
|
71
cg/blender/processing/highpoly_setup.py
Normal file
71
cg/blender/processing/highpoly_setup.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# -*- 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 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!')
|
140
cg/blender/processing/lowpoly_setup.py
Normal file
140
cg/blender/processing/lowpoly_setup.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
# -*- 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 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
|
158
cg/blender/processing/restruct_hierarchy_by_lcs.py
Normal file
158
cg/blender/processing/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 blender.utils.object_relations import (parenting,
|
||||
unparenting)
|
||||
from blender.utils.object_transforms import round_transforms
|
||||
|
||||
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 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
|
64
cg/blender/processing/uv_setup.py
Normal file
64
cg/blender/processing/uv_setup.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# 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.
|
||||
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
|
Loading…
Add table
Add a link
Reference in a new issue