Skip to content

Commit 51c5ef9

Browse files
committed
Updated DFS scene traversal to handle more cases
This fixes a number of bugs related to BSP building and copy-paste functionality. This DFS implementation was lifted from our PSK/PSA addon.
1 parent 74de976 commit 51c5ef9

File tree

4 files changed

+171
-17
lines changed

4 files changed

+171
-17
lines changed

bdk_addon/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
importlib.reload(constants)
77
importlib.reload(property_group_helpers)
88
importlib.reload(node_helpers)
9+
importlib.reload(dfs)
910

1011
importlib.reload(actor_properties)
1112

@@ -91,6 +92,7 @@
9192
else:
9293
from . import data as bdk_data
9394
from . import helpers as bdk_helpers
95+
from . import dfs as dfs
9496

9597
from .actor import properties as actor_properties
9698

bdk_addon/bsp/operators.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from bmesh.types import BMFace
33
from mathutils import Vector, Matrix
44

5+
from ..dfs import dfs_view_layer_objects
6+
57
from .builder import ensure_bdk_brush_uv_node_tree, create_bsp_brush_polygon, apply_level_to_brush_mapping, \
68
ensure_bdk_level_visibility_modifier
7-
from ..helpers import should_show_bdk_developer_extras, dfs_view_layer_objects, humanize_time
9+
from ..helpers import should_show_bdk_developer_extras, humanize_time
810
from .data import bsp_optimization_items, ORIGIN_ATTRIBUTE_NAME, TEXTURE_U_ATTRIBUTE_NAME, TEXTURE_V_ATTRIBUTE_NAME, \
911
POLY_FLAGS_ATTRIBUTE_NAME, BRUSH_INDEX_ATTRIBUTE_NAME, BRUSH_POLYGON_INDEX_ATTRIBUTE_NAME, \
1012
MATERIAL_INDEX_ATTRIBUTE_NAME, READ_ONLY_ATTRIBUTE_NAME, bsp_surface_attributes
@@ -756,9 +758,10 @@ def brush_object_filter(obj: Object, instance_objects: list[Object]):
756758
elif not obj.visible_get():
757759
return False
758760
return True
761+
762+
assert context.view_layer is not None
759763

760-
brush_objects = [(obj, instance_objects, matrix_world) for (obj, instance_objects, matrix_world) in
761-
dfs_view_layer_objects(context.view_layer) if brush_object_filter(obj, instance_objects)]
764+
brush_objects = [x for x in dfs_view_layer_objects(context.view_layer) if brush_object_filter(x.obj, x.instance_objects)]
762765

763766
# This is a list of the materials used for the brushes. It is populated as we iterate over the brush objects.
764767
# We then use this at the end to create the materials for the level object.
@@ -774,8 +777,10 @@ def _get_or_add_material(material: Material | None) -> int:
774777
# Add the brushes to the level object.
775778
level_object.bdk.level.brushes.clear()
776779

777-
for brush_index, (brush_object, instance_collections, _) in enumerate(brush_objects):
778-
is_instanced_brush = len(instance_collections) > 0
780+
for brush_index, dfs_object in enumerate(brush_objects):
781+
brush_object = dfs_object.obj
782+
instance_objects = dfs_object.instance_objects
783+
is_instanced_brush = len(instance_objects) > 0
779784
if is_instanced_brush:
780785
# Skip brushes that are instanced, as we cannot change their texturing.
781786
continue
@@ -791,10 +796,13 @@ def _get_or_add_material(material: Material | None) -> int:
791796
instanced_brush_indices = []
792797

793798
brushes: list[Brush] = []
794-
for brush_index, (brush_object, asset_instances, matrix_world) in enumerate(brush_objects):
799+
for brush_index, dfs_object in enumerate(brush_objects):
795800

796-
if asset_instances:
801+
if dfs_object.instance_objects:
797802
instanced_brush_indices.append(brush_index)
803+
804+
brush_object = dfs_object.obj
805+
matrix_world = dfs_object.matrix_world
798806

799807
# Create a new Poly object for each face of the brush.
800808
polys = []

bdk_addon/dfs.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
Depth-first object iterator functions for Blender collections and view layers.
3+
4+
These functions are used to iterate over objects in a collection or view layer in a depth-first manner, including
5+
instances. This is useful for exporters that need to traverse the object hierarchy in a predictable order.
6+
"""
7+
8+
from typing import Optional, Set, Iterable, List
9+
10+
from bpy.types import Collection, Object, ViewLayer, LayerCollection
11+
from mathutils import Matrix
12+
13+
14+
class DfsObject:
15+
"""
16+
Represents an object in a depth-first search.
17+
"""
18+
def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix):
19+
self.obj = obj
20+
self.instance_objects = instance_objects
21+
self.matrix_world = matrix_world
22+
23+
@property
24+
def is_visible(self) -> bool:
25+
"""
26+
Check if the object is visible.
27+
28+
@return: True if the object is visible, False otherwise.
29+
"""
30+
if self.instance_objects:
31+
return self.instance_objects[-1].visible_get()
32+
return self.obj.visible_get()
33+
34+
@property
35+
def is_selected(self) -> bool:
36+
"""
37+
Check if the object is selected.
38+
@return: True if the object is selected, False otherwise.
39+
"""
40+
if self.instance_objects:
41+
return self.instance_objects[-1].select_get()
42+
return self.obj.select_get()
43+
44+
45+
def _dfs_object_children(obj: Object, collection: Collection) -> Iterable[Object]:
46+
"""
47+
Construct a list of objects in hierarchy order from `collection.objects`, only keeping those that are in the
48+
collection.
49+
50+
@param obj: The object to start the search from.
51+
@param collection: The collection to search in.
52+
@return: An iterable of objects in hierarchy order.
53+
"""
54+
yield obj
55+
for child in obj.children:
56+
if child.name in collection.objects:
57+
yield from _dfs_object_children(child, collection)
58+
59+
60+
def dfs_objects_in_collection(collection: Collection) -> Iterable[Object]:
61+
"""
62+
Returns a depth-first iterator over all objects in a collection, only keeping those that are directly in the
63+
collection.
64+
65+
@param collection: The collection to search in.
66+
@return: An iterable of objects in hierarchy order.
67+
"""
68+
objects_hierarchy = []
69+
for obj in collection.objects:
70+
if obj.parent is None or obj.parent not in set(collection.objects):
71+
objects_hierarchy.append(obj)
72+
for obj in objects_hierarchy:
73+
yield from _dfs_object_children(obj, collection)
74+
75+
76+
def dfs_collection_objects(collection: Collection) -> Iterable[DfsObject]:
77+
"""
78+
Depth-first search of objects in a collection, including recursing into instances.
79+
80+
@param collection: The collection to search in.
81+
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
82+
"""
83+
yield from _dfs_collection_objects_recursive(collection)
84+
85+
86+
def _dfs_collection_objects_recursive(
87+
collection: Collection,
88+
instance_objects: Optional[List[Object]] = None,
89+
matrix_world: Matrix = Matrix.Identity(4),
90+
visited: Optional[Set[Object]]=None
91+
) -> Iterable[DfsObject]:
92+
"""
93+
Depth-first search of objects in a collection, including recursing into instances.
94+
This is a recursive function.
95+
96+
@param collection: The collection to search in.
97+
@param instance_objects: The running hierarchy of instance objects.
98+
@param matrix_world: The world matrix of the current object.
99+
@param visited: A set of visited object-instance pairs.
100+
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
101+
"""
102+
103+
# We want to also yield the top-level instance object so that callers can inspect the selection status etc.
104+
if visited is None:
105+
visited = set()
106+
107+
if instance_objects is None:
108+
instance_objects = list()
109+
110+
# First, yield all objects in child collections.
111+
for child in collection.children:
112+
yield from _dfs_collection_objects_recursive(child, instance_objects, matrix_world.copy(), visited)
113+
114+
# Then, evaluate all objects in this collection.
115+
for obj in dfs_objects_in_collection(collection):
116+
visited_pair = (obj, instance_objects[-1] if instance_objects else None)
117+
if visited_pair in visited:
118+
continue
119+
# If this an instance, we need to recurse into it.
120+
if obj.instance_collection is not None:
121+
# Calculate the instance transform.
122+
instance_offset_matrix = Matrix.Translation(-obj.instance_collection.instance_offset)
123+
# Recurse into the instance collection.
124+
yield from _dfs_collection_objects_recursive(obj.instance_collection,
125+
instance_objects + [obj],
126+
matrix_world @ (obj.matrix_world @ instance_offset_matrix),
127+
visited)
128+
else:
129+
# Object is not an instance, yield it.
130+
yield DfsObject(obj, instance_objects, matrix_world @ obj.matrix_world)
131+
visited.add(visited_pair)
132+
133+
134+
def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
135+
"""
136+
Depth-first iterator over all objects in a view layer, including recursing into instances.
137+
138+
@param view_layer: The view layer to inspect.
139+
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
140+
"""
141+
visited = set()
142+
def layer_collection_objects_recursive(layer_collection: LayerCollection):
143+
for child in layer_collection.children:
144+
yield from layer_collection_objects_recursive(child)
145+
# Iterate only the top-level objects in this collection first.
146+
yield from _dfs_collection_objects_recursive(layer_collection.collection, visited=visited)
147+
148+
yield from layer_collection_objects_recursive(view_layer.layer_collection)

bdk_addon/t3d/operators.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from pathlib import Path
2323
from .importer import import_t3d
2424
from .writer import T3DWriter
25-
from ..helpers import dfs_view_layer_objects, sanitize_name_for_unreal, humanize_size
25+
from ..helpers import sanitize_name_for_unreal, humanize_size
26+
from ..dfs import dfs_view_layer_objects
2627

2728

2829
class BDK_OT_t3d_import_from_clipboard(Operator):
@@ -40,6 +41,7 @@ def poll(cls, context: Context):
4041
return True
4142

4243
def execute(self, context: Context):
44+
assert context.window_manager is not None
4345
try:
4446
import_t3d(context.window_manager, context.window_manager.clipboard, context)
4547
self.report({'INFO'}, f'Imported actors from clipboard')
@@ -407,19 +409,13 @@ def poll(cls, context: Context):
407409

408410
def execute(self, context: Context):
409411
# Use the depth-first iterator to get all the objects in the view layer.
412+
assert context.view_layer is not None
410413
dfs_objects = list(dfs_view_layer_objects(context.view_layer))
411414

412415
# Filter only the selected objects.
413416
selected_objects: list[tuple[Object, Matrix]] = list()
414-
for obj, instance_objects, matrix_world in dfs_objects:
415-
if instance_objects:
416-
if instance_objects[0].select_get():
417-
selected_objects.append((obj, matrix_world))
418-
else:
419-
if obj.select_get():
420-
selected_objects.append((obj, matrix_world))
421-
422-
# selected_objects = list(filter(lambda obj: obj[0].select_get() or (obj[1] is not None and obj[1].select_get()), dfs_objects))
417+
for dfs_object in filter(lambda x: x.is_selected, dfs_objects):
418+
selected_objects.append((dfs_object.obj, dfs_object.matrix_world))
423419

424420
# Start a progress bar.
425421
wm = context.window_manager

0 commit comments

Comments
 (0)