Skip to content

Commit

Permalink
A number of additions to functionality:
Browse files Browse the repository at this point in the history
* The "Scale" option to export dialog.
* Added "Visible Only" option to export dialog.
* Collection Instances can now be exported (handles recursion etc.)
* Added material re-ordering (available on collection exporter only)
  • Loading branch information
cmbasnett committed Dec 9, 2024
1 parent d6c0186 commit 4b73bf4
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 43 deletions.
50 changes: 24 additions & 26 deletions io_scene_ase/builder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable, Optional, List, Tuple
from typing import Iterable, Optional, List, Tuple, cast

from bpy.types import Object, Context, Material, Mesh

Expand All @@ -8,6 +8,8 @@
import math
from mathutils import Matrix, Vector

from .dfs import DfsObject

SMOOTHING_GROUP_MAX = 32

class ASEBuildError(Exception):
Expand All @@ -24,40 +26,29 @@ def __init__(self):
self.has_vertex_colors = False
self.vertex_color_attribute = ''
self.should_invert_normals = False
self.should_export_visible_only = True
self.scale = 1.0


def get_object_matrix(obj: Object, asset_instance: Optional[Object] = None) -> Matrix:
if asset_instance is not None:
return asset_instance.matrix_world @ Matrix().Translation(asset_instance.instance_collection.instance_offset) @ obj.matrix_local
return obj.matrix_world


def get_mesh_objects(objects: Iterable[Object]) -> List[Tuple[Object, Optional[Object]]]:
mesh_objects = []
for obj in objects:
if obj.type == 'MESH':
mesh_objects.append((obj, None))
elif obj.instance_collection:
for instance_object in obj.instance_collection.all_objects:
if instance_object.type == 'MESH':
mesh_objects.append((instance_object, obj))
return mesh_objects


def build_ase(context: Context, options: ASEBuildOptions, objects: Iterable[Object]) -> ASE:
def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[DfsObject]) -> ASE:
ase = ASE()

main_geometry_object = None
mesh_objects = get_mesh_objects(objects)

context.window_manager.progress_begin(0, len(mesh_objects))
dfs_objects = list(dfs_objects)

context.window_manager.progress_begin(0, len(dfs_objects))

ase.materials = options.materials

for object_index, (obj, asset_instance) in enumerate(mesh_objects):
max_uv_layers = 0
for dfs_object in dfs_objects:
mesh_data = cast(Mesh, dfs_object.obj.data)
max_uv_layers = max(max_uv_layers, len(mesh_data.uv_layers))

matrix_world = get_object_matrix(obj, asset_instance)
matrix_world = options.transform @ matrix_world
for object_index, dfs_object in enumerate(dfs_objects):
obj = dfs_object.obj
matrix_world = dfs_object.matrix_world

# Save the active color name for vertex color export.
active_color_name = obj.data.color_attributes.active_color_name
Expand Down Expand Up @@ -98,7 +89,7 @@ def build_ase(context: Context, options: ASEBuildOptions, objects: Iterable[Obje
del bm
raise ASEBuildError(f'Collision mesh \'{obj.name}\' is not convex')

vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ matrix_world
vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ Matrix.Scale(options.scale, 4) @ matrix_world

for vertex_index, vertex in enumerate(mesh_data.vertices):
geometry_object.vertices.append(vertex_transform @ vertex.co)
Expand Down Expand Up @@ -184,6 +175,13 @@ def build_ase(context: Context, options: ASEBuildOptions, objects: Iterable[Obje
u, v = uv_layer_data[loop_index].uv
uv_layer.texture_vertices.append((u, v, 0.0))

# Add zeroed texture vertices for any missing UV layers.
for i in range(len(geometry_object.uv_layers), max_uv_layers):
uv_layer = ASEUVLayer()
for _ in mesh_data.loops:
uv_layer.texture_vertices.append((0.0, 0.0, 0.0))
geometry_object.uv_layers.append(uv_layer)

# Texture Faces
for loop_triangle in mesh_data.loop_triangles:
geometry_object.texture_vertex_faces.append(
Expand Down
154 changes: 154 additions & 0 deletions io_scene_ase/dfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
'''
Depth-first object iterator functions for Blender collections and view layers.
These functions are used to iterate over objects in a collection or view layer in a depth-first manner, including
instances. This is useful for exporters that need to traverse the object hierarchy in a predictable order.
'''

from typing import Optional, Set, Iterable, List

from bpy.types import Collection, Object, ViewLayer, LayerCollection
from mathutils import Matrix


class DfsObject:
'''
Represents an object in a depth-first search.
'''
def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix):
self.obj = obj
self.instance_objects = instance_objects
self.matrix_world = matrix_world

@property
def is_visible(self) -> bool:
'''
Check if the object is visible.
@return: True if the object is visible, False otherwise.
'''
if self.instance_objects:
return self.instance_objects[-1].visible_get()
return self.obj.visible_get()

@property
def is_selected(self) -> bool:
'''
Check if the object is selected.
@return: True if the object is selected, False otherwise.
'''
if self.instance_objects:
return self.instance_objects[-1].select_get()
return self.obj.select_get()



def _dfs_object_children(obj: Object, collection: Collection) -> Iterable[Object]:
'''
Construct a list of objects in hierarchy order from `collection.objects`, only keeping those that are in the
collection.
@param obj: The object to start the search from.
@param collection: The collection to search in.
@return: An iterable of objects in hierarchy order.
'''
yield obj
for child in obj.children:
if child.name in collection.objects:
yield from _dfs_object_children(child, collection)


def dfs_objects_in_collection(collection: Collection) -> Iterable[Object]:
'''
Returns a depth-first iterator over all objects in a collection, only keeping those that are directly in the
collection.
@param collection: The collection to search in.
@return: An iterable of objects in hierarchy order.
'''
objects_hierarchy = []
for obj in collection.objects:
if obj.parent is None or obj.parent not in set(collection.objects):
objects_hierarchy.append(obj)
for obj in objects_hierarchy:
yield from _dfs_object_children(obj, collection)


def dfs_collection_objects(collection: Collection, visible_only: bool = False) -> Iterable[DfsObject]:
'''
Depth-first search of objects in a collection, including recursing into instances.
@param collection: The collection to search in.
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
'''
yield from _dfs_collection_objects_recursive(collection)


def _dfs_collection_objects_recursive(
collection: Collection,
instance_objects: Optional[List[Object]] = None,
matrix_world: Matrix = Matrix.Identity(4),
visited: Optional[Set[Object]]=None
) -> Iterable[DfsObject]:
'''
Depth-first search of objects in a collection, including recursing into instances.
This is a recursive function.
@param collection: The collection to search in.
@param instance_objects: The running hierarchy of instance objects.
@param matrix_world: The world matrix of the current object.
@param visited: A set of visited object-instance pairs.
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
'''

# We want to also yield the top-level instance object so that callers can inspect the selection status etc.
if visited is None:
visited = set()

if instance_objects is None:
instance_objects = list()

# First, yield all objects in child collections.
for child in collection.children:
yield from _dfs_collection_objects_recursive(child, instance_objects, matrix_world.copy(), visited)

# Then, evaluate all objects in this collection.
for obj in dfs_objects_in_collection(collection):
visited_pair = (obj, instance_objects[-1] if instance_objects else None)
if visited_pair in visited:
continue
# If this an instance, we need to recurse into it.
if obj.instance_collection is not None:
# Calculate the instance transform.
instance_offset_matrix = Matrix.Translation(-obj.instance_collection.instance_offset)
# Recurse into the instance collection.
yield from _dfs_collection_objects_recursive(obj.instance_collection,
instance_objects + [obj],
matrix_world @ (obj.matrix_world @ instance_offset_matrix),
visited)
else:
# Object is not an instance, yield it.
yield DfsObject(obj, instance_objects, matrix_world @ obj.matrix_world)
visited.add(visited_pair)


def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
'''
Depth-first iterator over all objects in a view layer, including recursing into instances.
@param view_layer: The view layer to inspect.
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
'''
def layer_collection_objects_recursive(layer_collection: LayerCollection):
for child in layer_collection.children:
yield from layer_collection_objects_recursive(child)
# Iterate only the top-level objects in this collection first.
yield from _dfs_collection_objects_recursive(layer_collection.collection)

yield from layer_collection_objects_recursive(view_layer.layer_collection)


def _is_dfs_object_visible(obj: Object, instance_objects: List[Object]) -> bool:
'''
Check if a DFS object is visible.
@param obj: The object.
@param instance_objects: The instance objects.
@return: True if the object is visible, False otherwise.
'''
if instance_objects:
return instance_objects[-1].visible_get()
return obj.visible_get()
Loading

0 comments on commit 4b73bf4

Please sign in to comment.