From 8900cebe12fca5c2c12e0825ecf7c256014edfdc Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Tue, 20 Aug 2024 16:17:04 -0400 Subject: [PATCH 1/4] Add coplanar relation to constraint files, WIP implement snapping backend --- .../constraint_language/__init__.py | 1 + .../constraint_language/relations.py | 7 +- .../example_solver/geometry/dof.py | 26 ++++++- .../example_solver/room/decorate.py | 28 ------- infinigen_examples/constraints/home.py | 74 ++++++------------- infinigen_examples/constraints/util.py | 3 + 6 files changed, 53 insertions(+), 86 deletions(-) diff --git a/infinigen/core/constraints/constraint_language/__init__.py b/infinigen/core/constraints/constraint_language/__init__.py index 464fd0a1..e6d445d2 100644 --- a/infinigen/core/constraints/constraint_language/__init__.py +++ b/infinigen/core/constraints/constraint_language/__init__.py @@ -44,6 +44,7 @@ from .relations import ( AnyRelation, ConnectorType, + CoPlanar, CutFrom, GeometryRelation, NegatedRelation, diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index 636f9e96..feb581b6 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -387,9 +387,14 @@ class SupportedBy(Touching): @dataclass(frozen=True) -class StableAgainst(GeometryRelation): +class CoPlanar(GeometryRelation): margin: float = 0 + __repr__ = no_frozenset_repr + + +@dataclass(frozen=True) +class StableAgainst(GeometryRelation): # check_ if False, only check x/z stability, z is allowed to overhand. # typical use is chair-against-table relation check_z: bool = True diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 0faa2a4f..48a0c759 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -287,7 +287,10 @@ def apply_relations_surfacesample( parent_planes.append(parent_plane) parent_objs.append(parent_obj) match relation_state.relation: - case cl.StableAgainst(_child_tags, parent_tags, margin): + case ( + cl.StableAgainst(_child_tags, parent_tags, margin) + | cl.Touching(_child_tags, parent_tags, margin) + ): margins.append(margin) parent_tag_list.append(parent_tags) case cl.SupportedBy(_parent_tags, parent_tags): @@ -403,11 +406,22 @@ def apply_relations_surfacesample( face_mask = tagging.tagged_face_mask( parent_obj, relation_state.relation.parent_tags ) - stability.move_obj_random_pt( - state, obj_name, parent_obj.name, face_mask, parent_plane - ) + match relation_state.relation: + case cl.CoPlanar: + stability.snap_against( + state.trimesh_scene, + obj_name, + parent_obj.name, + obj_plane, + parent_plane, + margin=margin, + ) + case cl.StableAgainst(_, parent_tags, margin): + stability.move_obj_random_pt( + state, obj_name, parent_obj.name, face_mask, parent_plane + ) stability.snap_against( state.trimesh_scene, obj_name, @@ -416,7 +430,11 @@ def apply_relations_surfacesample( parent_plane, margin=margin, ) + case cl.SupportedBy(_, parent_tags): + stability.move_obj_random_pt( + state, obj_name, parent_obj.name, face_mask, parent_plane + ) stability.snap_against( state.trimesh_scene, obj_name, diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py index b77a826a..909c04a4 100644 --- a/infinigen/core/constraints/example_solver/room/decorate.py +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -184,34 +184,6 @@ def import_material(factory_name): (2, "plaster"), (1, "half"), ), - t.Semantics.Office: ( - "weighted_choice", - (2, "none"), - (2, "art"), - (2, "plaster"), - (1, "half"), - ), - t.Semantics.OpenOffice: ( - "weighted_choice", - (2, "none"), - (2, "art"), - (2, "plaster"), - (1, "half"), - ), - t.Semantics.FactoryOffice: ( - "weighted_choice", - (2, "none"), - (2, "art"), - (2, "plaster"), - (1, "half"), - ), - t.Semantics.BreakRoom: ( - "weighted_choice", - (2, "none"), - (2, "art"), - (2, "plaster"), - (1, "half"), - ), } room_wall_alternative_fns = defaultdict( lambda: ("weighted_choice", (2, "none"), (0.5, "half")), room_wall_alternative_fns diff --git a/infinigen_examples/constraints/home.py b/infinigen_examples/constraints/home.py index f03f5103..e71402ef 100644 --- a/infinigen_examples/constraints/home.py +++ b/infinigen_examples/constraints/home.py @@ -708,14 +708,18 @@ def vertical_diff(o, r): deskchair = furniture[seating.OfficeChairFactory].related_to( desks, cu.front_to_front ) - monitors = obj[appliances.MonitorFactory] + desk_monitors = ( + obj[appliances.MonitorFactory] + .related_to(desks, cu.ontop) + .related_to(desks, cu.back_coplanar_back) + ) constraints["desk"] = rooms.all( lambda r: ( desks.related_to(r).all( lambda t: ( deskchair.related_to(r).related_to(t).count().in_range(0, 1) - * monitors.related_to(t, cu.ontop).count().equals(1) + * desk_monitors.related_to(t, cu.ontop).count().equals(1) * (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count() >= 0) * (deskchair.related_to(r).related_to(t).count() == 1) ) @@ -730,15 +734,6 @@ def vertical_diff(o, r): + d.distance(doors.related_to(r)).maximize(weight=0.1) + cl.accessibility_cost(d, furniture.related_to(r)).minimize(weight=3) + cl.accessibility_cost(d, r).minimize(weight=3) - + monitors.related_to(d).mean( - lambda m: ( - cl.accessibility_cost(m, r, dist=2).minimize(weight=3) - + cl.accessibility_cost( - m, obj.related_to(r), dist=0.5 - ).minimize(weight=3) - + m.distance(r, cu.walltags).hinge(0.1, 1e7).minimize(weight=1) - ) - ) + deskchair.distance(rooms, cu.walltags).maximize(weight=1) ) ) @@ -956,49 +951,16 @@ def vertical_diff(o, r): cu.bottom, {Subpart.SupportSurface}, margin=0.001 ) cl.StableAgainst(cu.back, cu.walltags, margin=0.1) - kitchen_sink = obj[Semantics.Sink][table_decorations.SinkFactory].related_to( - countertops, sink_flush_on_counter + kitchen_sink = ( + obj[Semantics.Sink][table_decorations.SinkFactory] + .related_to(countertops, sink_flush_on_counter) + .related_to(countertops, cu.front_coplanar_front) ) + constraints["kitchen_sink"] = kitchens.all( lambda r: ( - # those sinks can be on either type of counter kitchen_sink.related_to(wallcounter.related_to(r)).count().in_range(0, 1) - * kitchen_sink.related_to(island.related_to(r)) - .count() - .in_range(0, 1) # island sinks dont need to be against wall - * countertops.related_to(r).all( - lambda c: ( - kitchen_sink.related_to(c).all( - lambda s: s.distance(c, cu.side).in_range(0.05, 0.2) - ) - ) - ) - ) - ) - - score_terms["kitchen_sink"] = kitchens.mean( - lambda r: ( - countertops.mean( - lambda c: kitchen_sink.related_to(c).mean( - lambda s: ( - (s.volume(dims=2) / c.volume(dims=2)) - .hinge(0.2, 0.4) - .minimize(weight=10) - ) - ) - ) - + island.related_to(r).mean( - lambda isl: ( # sinks on islands must be near to edge and oriented outwards - kitchen_sink.related_to(isl).mean( - lambda s: ( - cl.angle_alignment_cost(s, isl, cu.side).minimize(weight=10) - + cl.distance(s, isl, cu.side) - .hinge(0.05, 0.07) - .minimize(weight=10) - ) - ) - ) - ) + * kitchen_sink.related_to(island.related_to(r)).count().in_range(0, 1) ) ) @@ -1006,8 +968,10 @@ def vertical_diff(o, r): kitchen_appliances_big = kitchen_appliances.related_to( kitchens, cu.on_floor ).related_to(kitchens, cu.against_wall) - microwaves = kitchen_appliances[appliances.MicrowaveFactory].related_to( - wallcounter, cu.on + microwaves = ( + kitchen_appliances[appliances.MicrowaveFactory] + .related_to(wallcounter, cu.on) + .related_to(wallcounter, cu.back_coplanar_back) ) constraints["kitchen_appliance"] = kitchens.all( @@ -1200,7 +1164,11 @@ def freestanding(o, r): ) ) - tvs = obj[appliances.TVFactory].related_to(tvstands, cu.ontop) + tvs = ( + obj[appliances.TVFactory] + .related_to(tvstands, cu.ontop) + .related_to(tvstands, cu.back_coplanar_back) + ) if params["has_tv"]: constraints["tv"] = livingrooms.all( diff --git a/infinigen_examples/constraints/util.py b/infinigen_examples/constraints/util.py index 7d27df08..3e08094a 100644 --- a/infinigen_examples/constraints/util.py +++ b/infinigen_examples/constraints/util.py @@ -69,6 +69,9 @@ hanging = cl.StableAgainst(top, ceilingtags, margin=0.05) side_against_wall = cl.StableAgainst(side, walltags, margin=0.05) +front_coplanar_front = cl.CoPlanar(front, front, margin=0.05) +back_coplanar_back = cl.CoPlanar(back, back, margin=0.05) + ontop = cl.StableAgainst(bottom, top) on = cl.StableAgainst(bottom, {t.Subpart.SupportSurface}) From f5c715c797516a55ab83ba74584ad7dfa05d73d1 Mon Sep 17 00:00:00 2001 From: karhankayan Date: Wed, 28 Aug 2024 21:27:06 -0400 Subject: [PATCH 2/4] check coplanar, add test case --- .../constraint_language/relations.py | 6 ++ .../example_solver/geometry/dof.py | 8 ++ .../example_solver/geometry/stability.py | 45 ++++++++++ .../example_solver/geometry/validity.py | 13 ++- tests/solver/test_stable_against.py | 82 ++++++++++++++++++- 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index feb581b6..223fc803 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -390,11 +390,17 @@ class SupportedBy(Touching): class CoPlanar(GeometryRelation): margin: float = 0 + # rev_normal: if True, align the normals so they face the SAME direction, rather than two planes facing eachother. + # typical use is for sink embedded in countertop + rev_normal: bool = False + __repr__ = no_frozenset_repr @dataclass(frozen=True) class StableAgainst(GeometryRelation): + margin: float = 0 + # check_ if False, only check x/z stability, z is allowed to overhand. # typical use is chair-against-table relation check_z: bool = True diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 48a0c759..e3612722 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -109,6 +109,11 @@ def rotate_object_around_axis(obj, axis, std, angle=None): def check_init_valid( state: state_def.State, name: str, obj_planes: list, assigned_planes: list, margins ): + """ + Check that the plane assignments to the object is valid. First checks that the rotations can be satisfied, then + checks that the translations can be satisfied. Returns a boolean indicating if the assignments are valid, the number + of degrees of freedom remaining, and the translation vector if the assignments are valid. + """ if len(obj_planes) == 0: raise ValueError(f"{check_init_valid.__name__} for {name=} got {obj_planes=}") if len(obj_planes) > 3: @@ -117,6 +122,9 @@ def check_init_valid( ) def get_rot(ind): + """ + Get the rotation axis and angle needed to align the object's plane with the assigned plane. + """ try: a = obj_planes[ind][0] b = assigned_planes[ind][0] diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index c42b28b1..f9f8e152 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -177,6 +177,51 @@ def stable_against( return True +@gin.configurable +def coplanar( + state: state_def.State, + obj_name: str, + relation_state: state_def.RelationState, +): + """ + check that the object's tagged surface is coplanar with the target object's tagged surface translated with margin. + """ + + relation = relation_state.relation + assert isinstance(relation, cl.CoPlanar) + + logger.debug(f"coplanar {obj_name=} {relation_state=}") + a_blender_obj = state.objs[obj_name].obj + b_blender_obj = state.objs[relation_state.target_name].obj + + pa, pb = state.planes.get_rel_state_planes(state, obj_name, relation_state) + + poly_a = state.planes.planerep_to_poly(pa) + poly_b = state.planes.planerep_to_poly(pb) + + normal_a = iu.global_polygon_normal(a_blender_obj, poly_a) + normal_b = iu.global_polygon_normal(b_blender_obj, poly_b) + dot = np.array(normal_a).dot(normal_b) + if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)): + logger.debug(f"coplanar failed, not parallel {dot=}") + return False + + origin_b = iu.global_vertex_coordinates( + b_blender_obj, b_blender_obj.data.vertices[poly_b.vertices[0]] + ) + + for vertex in poly_a.vertices: + vertex_global = iu.global_vertex_coordinates( + a_blender_obj, a_blender_obj.data.vertices[vertex] + ) + distance = iu.distance_to_plane(vertex_global, origin_b, normal_b) + if not np.isclose(distance, relation_state.relation.margin, atol=1e-2): + logger.debug(f"coplanar failed, not close to {distance=}") + return False + + return True + + def snap_against(scene, a, b, a_plane, b_plane, margin=0): """ snap a against b with some margin. diff --git a/infinigen/core/constraints/example_solver/geometry/validity.py b/infinigen/core/constraints/example_solver/geometry/validity.py index a38f8697..e348fcc2 100644 --- a/infinigen/core/constraints/example_solver/geometry/validity.py +++ b/infinigen/core/constraints/example_solver/geometry/validity.py @@ -19,7 +19,10 @@ any_touching, constrain_contact, ) -from infinigen.core.constraints.example_solver.geometry.stability import stable_against +from infinigen.core.constraints.example_solver.geometry.stability import ( + coplanar, + stable_against, +) from infinigen.core.constraints.example_solver.state_def import State from infinigen.core.util import blender as butil @@ -76,6 +79,14 @@ def all_relations_valid(state, name): f"{name} failed relation {i=}/{len(rels)} {relation_state.relation} on {relation_state.target_name}" ) return False + + case cl.CoPlanar(_child_tags, _parent_tags, _margin): + res = coplanar(state, name, relation_state) + if not res: + logger.debug( + f"{name} failed relation {i=}/{len(rels)} {relation_state.relation} on {relation_state.target_name}" + ) + return False case _: raise TypeError(f"Unhandled {relation_state.relation}") diff --git a/tests/solver/test_stable_against.py b/tests/solver/test_stable_against.py index a02cd3e8..7bac0b34 100644 --- a/tests/solver/test_stable_against.py +++ b/tests/solver/test_stable_against.py @@ -52,6 +52,50 @@ def make_scene(loc2): return state_def.State(objs=objs) +def make_scene_coplanar(loc2): + """Create a scene with a table and a cup, and return the state.""" + butil.clear_scene() + objs = {} + + table = butil.spawn_cube(scale=(5, 5, 1), name="table") + cup = butil.spawn_cube(scale=(1, 1, 1), name="cup", location=loc2) + + for o in [table, cup]: + butil.apply_transform(o) + parse_scene.preprocess_obj(o) + tagging.tag_canonical_surfaces(o) + + assert table.scale == Vector((1, 1, 1)) + assert cup.location != Vector((0, 0, 0)) + + bpy.context.view_layer.update() + + objs["table"] = state_def.ObjectState(table) + objs["cup"] = state_def.ObjectState(cup) + objs["cup"].relations.append( + state_def.RelationState( + cl.StableAgainst({t.Subpart.Bottom}, {t.Subpart.Top}), + target_name="table", + child_plane_idx=0, + parent_plane_idx=0, + ) + ) + back = {t.Subpart.Back, -t.Subpart.Top, -t.Subpart.Front} + back_coplanar_back = cl.CoPlanar(back, back, margin=0) + + objs["cup"].relations.append( + state_def.RelationState( + back_coplanar_back, + target_name="table", + child_plane_idx=0, + parent_plane_idx=0, + ) + ) + butil.save_blend("test.blend") + + return state_def.State(objs=objs) + + def test_stable_against(): # too low, intersects ground assert not validity.check_post_move_validity(make_scene((0, 0, 0.5)), "cup") @@ -150,5 +194,41 @@ def test_horizontal_stability(): # butil.save_blend('test.blend') +def test_coplanar(): + # Test case 1: Cup is stable against but not coplanar (should be invalid) + assert not validity.check_post_move_validity(make_scene_coplanar((0, 0, 1)), "cup") + + # Test case 2: Cup is stable against and coplanar with the table (should be valid) + assert validity.check_post_move_validity(make_scene_coplanar((-2, 0, 1)), "cup") + + # Test case 3: Cup is coplanar but not stable against (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((-5.2, 0, 1)), "cup" + ) + + # Test case 4: Cup is neither stable against nor coplanar (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((2, 2, 1.1)), "cup" + ) + + # Test case 5: Cup is at the back edge, stable against and coplanar (should be valid) + assert validity.check_post_move_validity(make_scene_coplanar((-2, 2, 1)), "cup") + + # Test case 6: Cup is slightly off the back edge, not stable against but coplanar (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((-2.1, 2, 1)), "cup" + ) + + # Test case 7: Cup is far from the table (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((10, 10, 10)), "cup" + ) + + # Test case 8: Cup is inside the table, not stable against but coplanar (should be invalid) + assert not validity.check_post_move_validity(make_scene_coplanar((-2, 0, 0)), "cup") + + print("All test cases for coplanar constraint passed successfully.") + + if __name__ == "__main__": - test_horizontal_stability() + test_coplanar() From 2e2e92999eeca4f206a43da1c59b34b46b77e9f4 Mon Sep 17 00:00:00 2001 From: karhankayan Date: Thu, 29 Aug 2024 14:57:53 -0400 Subject: [PATCH 3/4] make dof compatible with coplanarity --- .../example_solver/geometry/dof.py | 40 +++++++++++++++---- .../example_solver/geometry/stability.py | 2 + 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index e3612722..c6692223 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -259,20 +259,30 @@ def apply_relations_surfacesample( state: state_def.State, name: str, ): + """ + Apply the relation constraints to the object. Place it in the scene according to the constraints. + """ + obj_state = state.objs[name] obj_name = obj_state.obj.name + def relation_sort_key(relation_state): + return isinstance(relation_state.relation, cl.CoPlanar) + + obj_state.relations = sorted(obj_state.relations, key=relation_sort_key) + parent_objs = [] parent_planes = [] obj_planes = [] margins = [] parent_tag_list = [] + relations = [] if len(obj_state.relations) == 0: raise ValueError(f"Object {name} has no relations") elif len(obj_state.relations) > 3: raise ValueError( - f"Object {name} has more than 2 relations, not supported. {obj_state.relations=}" + f"Object {name} has more than 3 relations, not supported. {obj_state.relations=}" ) for i, relation_state in enumerate(obj_state.relations): @@ -301,9 +311,15 @@ def apply_relations_surfacesample( ): margins.append(margin) parent_tag_list.append(parent_tags) + relations.append(relation_state.relation) case cl.SupportedBy(_parent_tags, parent_tags): margins.append(0) parent_tag_list.append(parent_tags) + relations.append(relation_state.relation) + case cl.CoPlanar(_child_tags, parent_tags, margin): + margins.append(margin) + parent_tag_list.append(parent_tags) + relations.append(relation_state.relation) case _: raise NotImplementedError @@ -328,6 +344,8 @@ def apply_relations_surfacesample( margin2 = margins[1] obj_plane1 = obj_planes[0] obj_plane2 = obj_planes[1] + relation1 = relations[0] + relation2 = relations[1] parent1_trimesh = state.planes.get_tagged_submesh( state.trimesh_scene, parent_obj1.name, parent_tags1, parent_plane1 @@ -350,9 +368,13 @@ def apply_relations_surfacesample( f"Failed to project {parent1_trimesh=} {plane_normal_1=} for {name=}" ) + if isinstance(relation2, cl.CoPlanar) or isinstance(relation1, cl.CoPlanar): + print("Here comes CoPlanar", obj_name) + butil.save_blend("debug.blend") + if all( [p1_to_p1.buffer(1e-1).contains(Point(pt[0], pt[1])) for pt in projected] - ): + ) and (not isinstance(relation2, cl.CoPlanar)): face_mask = tagging.tagged_face_mask(parent_obj2, parent_tags2) stability.move_obj_random_pt( state, obj_name, parent_obj2.name, face_mask, parent_plane2 @@ -396,7 +418,7 @@ def apply_relations_surfacesample( ) elif dof == 2: - assert len(parent_planes) == 1, (name, len(parent_planes)) + # assert len(parent_planes) == 1, (name, len(parent_planes)) for i, relation_state in enumerate(obj_state.relations): parent_obj = state.objs[relation_state.target_name].obj obj_plane, parent_plane = state.planes.get_rel_state_planes( @@ -467,7 +489,7 @@ def validate_relations_feasible(state: state_def.State, name: str) -> bool: @gin.configurable def try_apply_relation_constraints( - state: state_def.State, name: str, n_try_resolve=10, visualize=False + state: state_def.State, name: str, n_try_resolve=10, visualize=True ): """ name is in objs.name @@ -504,10 +526,14 @@ def try_apply_relation_constraints( return True if visualize: - vis = butil.copy(obj_state.obj) - vis.name = obj_state.obj.name[:30] + "_failure_" + str(retry) + if ( + "monitor" in obj_state.obj.name.lower() + or "tv" in obj_state.obj.name.lower() + ): + vis = butil.copy(obj_state.obj) + vis.name = obj_state.obj.name[:30] + "_failure_" + str(retry) - # butil.save_blend("test.blend") + butil.save_blend("test.blend") logger.debug(f"Exhausted {n_try_resolve=} tries for {name=}") return False diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index f9f8e152..79244db2 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -201,6 +201,8 @@ def coplanar( normal_a = iu.global_polygon_normal(a_blender_obj, poly_a) normal_b = iu.global_polygon_normal(b_blender_obj, poly_b) + if relation.rev_normal: + normal_b = -normal_b dot = np.array(normal_a).dot(normal_b) if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)): logger.debug(f"coplanar failed, not parallel {dot=}") From f0cefc33bdea050ca4e31564340e848c22e454f8 Mon Sep 17 00:00:00 2001 From: karhankayan Date: Sat, 7 Sep 2024 20:02:59 -0400 Subject: [PATCH 4/4] global vertex, normal moved to butil. coplanar works --- .../constraint_language/relations.py | 2 + .../constraints/constraint_language/util.py | 27 +++---- .../core/constraints/evaluator/indoor_util.py | 35 +++++---- .../evaluator/node_impl/trimesh_geometry.py | 7 +- .../example_solver/geometry/dof.py | 71 ++++++++++++------- .../example_solver/geometry/planes.py | 40 ++++------- .../example_solver/geometry/stability.py | 40 ++++++----- infinigen/core/tagging.py | 8 +-- infinigen/core/util/blender.py | 14 ++++ infinigen_examples/constraints/util.py | 4 +- 10 files changed, 126 insertions(+), 122 deletions(-) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index 223fc803..f6cf9c5e 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -405,6 +405,8 @@ class StableAgainst(GeometryRelation): # typical use is chair-against-table relation check_z: bool = True + rev_normal: bool = False + __repr__ = no_frozenset_repr diff --git a/infinigen/core/constraints/constraint_language/util.py b/infinigen/core/constraints/constraint_language/util.py index 390893d9..a7293303 100644 --- a/infinigen/core/constraints/constraint_language/util.py +++ b/infinigen/core/constraints/constraint_language/util.py @@ -213,30 +213,21 @@ def delete_obj(scene, a, delete_blender=True): scene.delete_geometry(obj_name + "_mesh") -def global_vertex_coordinates(obj, local_vertex) -> Vector: - return obj.matrix_world @ local_vertex.co - - -def global_polygon_normal(obj, polygon): - loc, rot, scale = obj.matrix_world.decompose() - rot = rot.to_matrix() - normal = rot @ polygon.normal - return normal / np.linalg.norm(normal) - - def is_planar(obj, tolerance=1e-6): if len(obj.data.polygons) != 1: return False polygon = obj.data.polygons[0] - global_normal = global_polygon_normal(obj, polygon) + global_normal = butil.global_polygon_normal(obj, polygon) # Take the first vertex as a reference point on the plane - ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon.vertices[0]]) + ref_vertex = butil.global_vertex_coordinates( + obj, obj.data.vertices[polygon.vertices[0]] + ) # Check if all vertices lie on the plane defined by the reference vertex and the global normal for vertex in obj.data.vertices: - distance = (global_vertex_coordinates(obj, vertex) - ref_vertex).dot( + distance = (butil.global_vertex_coordinates(obj, vertex) - ref_vertex).dot( global_normal ) if not math.isclose(distance, 0, abs_tol=tolerance): @@ -253,8 +244,12 @@ def planes_parallel(plane_obj_a, plane_obj_b, tolerance=1e-6): # if not is_planar(plane_obj_a) or not is_planar(plane_obj_b): # raise ValueError("One or both objects are not planar") - global_normal_a = global_polygon_normal(plane_obj_a, plane_obj_a.data.polygons[0]) - global_normal_b = global_polygon_normal(plane_obj_b, plane_obj_b.data.polygons[0]) + global_normal_a = butil.global_polygon_normal( + plane_obj_a, plane_obj_a.data.polygons[0] + ) + global_normal_b = butil.global_polygon_normal( + plane_obj_b, plane_obj_b.data.polygons[0] + ) dot_product = global_normal_a.dot(global_normal_b) diff --git a/infinigen/core/constraints/evaluator/indoor_util.py b/infinigen/core/constraints/evaluator/indoor_util.py index df5a2a70..aeef60dd 100644 --- a/infinigen/core/constraints/evaluator/indoor_util.py +++ b/infinigen/core/constraints/evaluator/indoor_util.py @@ -12,6 +12,8 @@ import trimesh from shapely import LineString, Point +from infinigen.core.util import blender as butil + def meshes_from_names(scene, names): if isinstance(names, str): @@ -172,30 +174,21 @@ def delete_obj(a, scene=None): scene.delete_geometry(obj_name + "_mesh") -def global_vertex_coordinates(obj, local_vertex): - return obj.matrix_world @ local_vertex.co - - -def global_polygon_normal(obj, polygon): - loc, rot, scale = obj.matrix_world.decompose() - rot = rot.to_matrix() - normal = rot @ polygon.normal - return normal / np.linalg.norm(normal) - - def is_planar(obj, tolerance=1e-6): if len(obj.data.polygons) != 1: return False polygon = obj.data.polygons[0] - global_normal = global_polygon_normal(obj, polygon) + global_normal = butil.global_polygon_normal(obj, polygon) # Take the first vertex as a reference point on the plane - ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon.vertices[0]]) + ref_vertex = butil.global_vertex_coordinates( + obj, obj.data.vertices[polygon.vertices[0]] + ) # Check if all vertices lie on the plane defined by the reference vertex and the global normal for vertex in obj.data.vertices: - distance = (global_vertex_coordinates(obj, vertex) - ref_vertex).dot( + distance = (butil.global_vertex_coordinates(obj, vertex) - ref_vertex).dot( global_normal ) if not math.isclose(distance, 0, abs_tol=tolerance): @@ -212,8 +205,12 @@ def planes_parallel(plane_obj_a, plane_obj_b, tolerance=1e-6): # if not is_planar(plane_obj_a) or not is_planar(plane_obj_b): # raise ValueError("One or both objects are not planar") - global_normal_a = global_polygon_normal(plane_obj_a, plane_obj_a.data.polygons[0]) - global_normal_b = global_polygon_normal(plane_obj_b, plane_obj_b.data.polygons[0]) + global_normal_a = butil.global_polygon_normal( + plane_obj_a, plane_obj_a.data.polygons[0] + ) + global_normal_b = butil.global_polygon_normal( + plane_obj_b, plane_obj_b.data.polygons[0] + ) dot_product = global_normal_a.dot(global_normal_b) @@ -230,12 +227,12 @@ def distance_to_plane(point, plane_point, plane_normal): def is_within_margin_from_plane(obj, obj_b, margin, tol=1e-6): """Check if all vertices of an object are within a given margin from a plane.""" polygon_b = obj_b.data.polygons[0] - plane_point_b = global_vertex_coordinates( + plane_point_b = butil.global_vertex_coordinates( obj_b, obj_b.data.vertices[polygon_b.vertices[0]] ) - plane_normal_b = global_polygon_normal(obj_b, polygon_b) + plane_normal_b = butil.global_polygon_normal(obj_b, polygon_b) for vertex in obj.data.vertices: - global_vertex = global_vertex_coordinates(obj, vertex) + global_vertex = butil.global_vertex_coordinates(obj, vertex) distance = distance_to_plane(global_vertex, plane_point_b, plane_normal_b) if not math.isclose(distance, margin, abs_tol=tol): return False diff --git a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py index aaf03f75..de1a2c3d 100644 --- a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py +++ b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py @@ -31,10 +31,9 @@ from infinigen.core import tags as t from infinigen.core.constraints.example_solver import state_def from infinigen.core.constraints.example_solver.geometry.parse_scene import add_to_scene +from infinigen.core.util import blender as butil from infinigen.core.util.logging import lazydebug -# from infinigen.core.util import blender as butil - # import fcl @@ -85,10 +84,10 @@ def get_axis(state: state_def.State, obj: bpy.types.Object, tag=t.Subpart.Front) a_front_plane = a_front_planes[0] a_front_plane_ind = a_front_plane[1] a_poly = obj.data.polygons[a_front_plane_ind] - front_plane_pt = iu.global_vertex_coordinates( + front_plane_pt = butil.global_vertex_coordinates( obj, obj.data.vertices[a_poly.vertices[0]] ) - front_plane_normal = iu.global_polygon_normal(obj, a_poly) + front_plane_normal = butil.global_polygon_normal(obj, a_poly) return front_plane_pt, front_plane_normal diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index c6692223..25e2584c 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -107,7 +107,12 @@ def rotate_object_around_axis(obj, axis, std, angle=None): def check_init_valid( - state: state_def.State, name: str, obj_planes: list, assigned_planes: list, margins + state: state_def.State, + name: str, + obj_planes: list, + assigned_planes: list, + margins: list, + rev_normals: list[bool], ): """ Check that the plane assignments to the object is valid. First checks that the rotations can be satisfied, then @@ -133,6 +138,7 @@ def get_rot(ind): a_plane = obj_planes[ind] b_plane = assigned_planes[ind] + rev_normal = rev_normals[ind] a_obj = bpy.data.objects[a] b_obj = bpy.data.objects[b] @@ -140,8 +146,8 @@ def get_rot(ind): a_poly = a_obj.data.polygons[a_poly_index] b_poly_index = b_plane[1] b_poly = b_obj.data.polygons[b_poly_index] - plane_normal_a = iu.global_polygon_normal(a_obj, a_poly) - plane_normal_b = iu.global_polygon_normal(b_obj, b_poly) + plane_normal_a = butil.global_polygon_normal(a_obj, a_poly) + plane_normal_b = butil.global_polygon_normal(b_obj, b_poly, rev_normal) plane_normal_b = -plane_normal_b rotation_axis = np.cross(plane_normal_a, plane_normal_b) @@ -200,6 +206,7 @@ def is_rotation_allowed(rotation_axis, reference_normal): a_obj_name, a_poly_index = obj_planes[i] b_obj_name, b_poly_index = assigned_planes[i] margin = margins[i] + rev_normal = rev_normals[i] a_obj = bpy.data.objects[a_obj_name] b_obj = bpy.data.objects[b_obj_name] @@ -208,13 +215,13 @@ def is_rotation_allowed(rotation_axis, reference_normal): b_poly = b_obj.data.polygons[b_poly_index] # Get global coordinates and normals - plane_point_a = iu.global_vertex_coordinates( + plane_point_a = butil.global_vertex_coordinates( a_obj, a_obj.data.vertices[a_poly.vertices[0]] ) - plane_point_b = iu.global_vertex_coordinates( + plane_point_b = butil.global_vertex_coordinates( b_obj, b_obj.data.vertices[b_poly.vertices[0]] ) - plane_normal_b = iu.global_polygon_normal(b_obj, b_poly) + plane_normal_b = butil.global_polygon_normal(b_obj, b_poly, rev_normal) plane_point_b += plane_normal_b * margin # Append to the matrix A and vector b for Ax = c @@ -277,6 +284,7 @@ def relation_sort_key(relation_state): margins = [] parent_tag_list = [] relations = [] + rev_normals = [] if len(obj_state.relations) == 0: raise ValueError(f"Object {name} has no relations") @@ -305,25 +313,34 @@ def relation_sort_key(relation_state): parent_planes.append(parent_plane) parent_objs.append(parent_obj) match relation_state.relation: - case ( - cl.StableAgainst(_child_tags, parent_tags, margin) - | cl.Touching(_child_tags, parent_tags, margin) + case cl.StableAgainst( + _child_tags, parent_tags, margin, _check_z, rev_normal ): margins.append(margin) parent_tag_list.append(parent_tags) relations.append(relation_state.relation) + rev_normals.append(rev_normal) case cl.SupportedBy(_parent_tags, parent_tags): margins.append(0) parent_tag_list.append(parent_tags) relations.append(relation_state.relation) - case cl.CoPlanar(_child_tags, parent_tags, margin): + rev_normals.append(False) + case cl.CoPlanar(_child_tags, parent_tags, margin, rev_normal): + margins.append(margin) + parent_tag_list.append(parent_tags) + relations.append(relation_state.relation) + rev_normals.append(rev_normal) + case cl.Touching(_child_tags, parent_tags, margin): margins.append(margin) parent_tag_list.append(parent_tags) relations.append(relation_state.relation) + rev_normals.append(False) case _: raise NotImplementedError - valid, dof, T = check_init_valid(state, name, obj_planes, parent_planes, margins) + valid, dof, T = check_init_valid( + state, name, obj_planes, parent_planes, margins, rev_normals + ) if not valid: rels = [(rels.relation, rels.target_name) for rels in obj_state.relations] logger.warning(f"Init was invalid for {name=} {rels=}") @@ -344,8 +361,9 @@ def relation_sort_key(relation_state): margin2 = margins[1] obj_plane1 = obj_planes[0] obj_plane2 = obj_planes[1] - relation1 = relations[0] relation2 = relations[1] + rev_normal1 = rev_normals[0] + rev_normal2 = rev_normals[1] parent1_trimesh = state.planes.get_tagged_submesh( state.trimesh_scene, parent_obj1.name, parent_tags1, parent_plane1 @@ -356,7 +374,7 @@ def relation_sort_key(relation_state): parent1_poly_index = parent_plane1[1] parent1_poly = parent_obj1.data.polygons[parent1_poly_index] - plane_normal_1 = iu.global_polygon_normal(parent_obj1, parent1_poly) + plane_normal_1 = butil.global_polygon_normal(parent_obj1, parent1_poly) pts = parent2_trimesh.vertices projected = project(pts, plane_normal_1) p1_to_p1 = trimesh.path.polygons.projected( @@ -368,10 +386,6 @@ def relation_sort_key(relation_state): f"Failed to project {parent1_trimesh=} {plane_normal_1=} for {name=}" ) - if isinstance(relation2, cl.CoPlanar) or isinstance(relation1, cl.CoPlanar): - print("Here comes CoPlanar", obj_name) - butil.save_blend("debug.blend") - if all( [p1_to_p1.buffer(1e-1).contains(Point(pt[0], pt[1])) for pt in projected] ) and (not isinstance(relation2, cl.CoPlanar)): @@ -386,6 +400,7 @@ def relation_sort_key(relation_state): obj_plane2, parent_plane2, margin=margin2, + rev_normal=rev_normal2, ) stability.snap_against( state.trimesh_scene, @@ -394,6 +409,7 @@ def relation_sort_key(relation_state): obj_plane1, parent_plane1, margin=margin1, + rev_normal=rev_normal1, ) else: face_mask = tagging.tagged_face_mask(parent_obj1, parent_tags1) @@ -407,6 +423,7 @@ def relation_sort_key(relation_state): obj_plane1, parent_plane1, margin=margin1, + rev_normal=rev_normal1, ) stability.snap_against( state.trimesh_scene, @@ -415,6 +432,7 @@ def relation_sort_key(relation_state): obj_plane2, parent_plane2, margin=margin2, + rev_normal=rev_normal2, ) elif dof == 2: @@ -446,9 +464,10 @@ def relation_sort_key(relation_state): obj_plane, parent_plane, margin=margin, + rev_normal=relation_state.relation.rev_normal, ) - case cl.StableAgainst(_, parent_tags, margin): + case cl.StableAgainst(_, parent_tags, margin, _check_z, rev_normal): stability.move_obj_random_pt( state, obj_name, parent_obj.name, face_mask, parent_plane ) @@ -459,6 +478,7 @@ def relation_sort_key(relation_state): obj_plane, parent_plane, margin=margin, + rev_normal=rev_normal, ) case cl.SupportedBy(_, parent_tags): @@ -472,6 +492,7 @@ def relation_sort_key(relation_state): obj_plane, parent_plane, margin=0, + rev_normal=False, ) case _: raise NotImplementedError @@ -489,7 +510,7 @@ def validate_relations_feasible(state: state_def.State, name: str) -> bool: @gin.configurable def try_apply_relation_constraints( - state: state_def.State, name: str, n_try_resolve=10, visualize=True + state: state_def.State, name: str, n_try_resolve=10, visualize=False ): """ name is in objs.name @@ -518,6 +539,7 @@ def try_apply_relation_constraints( if visualize: vis = butil.copy(obj_state.obj) vis.name = obj_state.obj.name[:30] + "_noneplanes_" + str(retry) + butil.save_blend("test.blend") return False if validity.check_post_move_validity(state, name): @@ -526,14 +548,9 @@ def try_apply_relation_constraints( return True if visualize: - if ( - "monitor" in obj_state.obj.name.lower() - or "tv" in obj_state.obj.name.lower() - ): - vis = butil.copy(obj_state.obj) - vis.name = obj_state.obj.name[:30] + "_failure_" + str(retry) - - butil.save_blend("test.blend") + vis = butil.copy(obj_state.obj) + vis.name = obj_state.obj.name[:30] + "_failure_" + str(retry) + butil.save_blend("test.blend") logger.debug(f"Exhausted {n_try_resolve=} tries for {name=}") return False diff --git a/infinigen/core/constraints/example_solver/geometry/planes.py b/infinigen/core/constraints/example_solver/geometry/planes.py index e01870e9..0c9b3a60 100644 --- a/infinigen/core/constraints/example_solver/geometry/planes.py +++ b/infinigen/core/constraints/example_solver/geometry/planes.py @@ -23,22 +23,6 @@ logger = logging.getLogger(__name__) -def global_vertex_coordinates(obj, local_vertex): - return obj.matrix_world @ local_vertex.co - - -def global_polygon_normal(obj, polygon): - loc, rot, scale = obj.matrix_world.decompose() - rot = rot.to_matrix() - normal = rot @ polygon.normal - try: - return normal / np.linalg.norm(normal) - except ZeroDivisionError: - raise ZeroDivisionError( - f"Zero division error in global_polygon_normal for {obj.name=}, {polygon.index=}, {normal=}" - ) - - class Planes: def __init__(self): self._mesh_hashes = {} # Dictionary to store mesh hashes for each object @@ -97,10 +81,10 @@ def compute_all_planes_fast(self, obj, face_mask, tolerance=1e-4): # Cache computations vertex_cache = { - v.index: global_vertex_coordinates(obj, v) for v in obj.data.vertices + v.index: butil.global_vertex_coordinates(obj, v) for v in obj.data.vertices } normal_cache = { - p.index: global_polygon_normal(obj, p) + p.index: butil.global_polygon_normal(obj, p) for p in obj.data.polygons if face_mask[p.index] } @@ -136,17 +120,17 @@ def get_all_planes_deprecated( for polygon in obj.data.polygons: if not face_mask[polygon.index]: continue - vertex = global_vertex_coordinates( + vertex = butil.global_vertex_coordinates( obj, obj.data.vertices[polygon.vertices[0]] ) - normal = global_polygon_normal(obj, polygon) + normal = butil.global_polygon_normal(obj, polygon) belongs_to_existing_plane = False for name, polygon2_index in unique_planes: polygon2 = obj.data.polygons[polygon2_index] - plane_vertex = global_vertex_coordinates( + plane_vertex = butil.global_vertex_coordinates( obj, obj.data.vertices[polygon2.vertices[0]] ) - plane_normal = global_polygon_normal(obj, polygon2) + plane_normal = butil.global_polygon_normal(obj, polygon2) if np.allclose( np.cross(normal, plane_normal), 0, rtol=tolerance ) and np.allclose( @@ -291,10 +275,10 @@ def tagged_plane_mask( current_hash = self.calculate_mesh_hash(obj) # Calculate current mesh hash face_mask_hash = self.hash_face_mask(face_mask) # Calculate hash for face_mask ref_poly = self.planerep_to_poly(plane) - ref_vertex = global_vertex_coordinates( + ref_vertex = butil.global_vertex_coordinates( obj, obj.data.vertices[ref_poly.vertices[0]] ) - ref_normal = global_polygon_normal(obj, ref_poly) + ref_normal = butil.global_polygon_normal(obj, ref_poly) plane_hash = self.hash_plane( ref_normal, ref_vertex, hash_tolerance ) # Calculate hash for plane @@ -334,19 +318,19 @@ def _compute_tagged_plane_mask(self, obj, face_mask, plane, tolerance): """ plane_mask = np.zeros(len(obj.data.polygons), dtype=bool) ref_poly = self.planerep_to_poly(plane) - ref_vertex = global_vertex_coordinates( + ref_vertex = butil.global_vertex_coordinates( obj, obj.data.vertices[ref_poly.vertices[0]] ) - ref_normal = global_polygon_normal(obj, ref_poly) + ref_normal = butil.global_polygon_normal(obj, ref_poly) for candidate_polygon in obj.data.polygons: if not face_mask[candidate_polygon.index]: continue - candidate_vertex = global_vertex_coordinates( + candidate_vertex = butil.global_vertex_coordinates( obj, obj.data.vertices[candidate_polygon.vertices[0]] ) - candidate_normal = global_polygon_normal(obj, candidate_polygon) + candidate_normal = butil.global_polygon_normal(obj, candidate_polygon) diff_vec = ref_vertex - candidate_vertex if not np.isclose(np.linalg.norm(diff_vec), 0): diff_vec /= np.linalg.norm(diff_vec) diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index 79244db2..5d76c0af 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -102,7 +102,7 @@ def stable_against( relation = relation_state.relation assert isinstance(relation, cl.StableAgainst) - logger.debug(f"stable against {obj_name=} {relation_state=}") + logger.debug(f"stable against {obj_name=} {relation_state=} {relation.rev_normal=}") a_blender_obj = state.objs[obj_name].obj b_blender_obj = state.objs[relation_state.target_name].obj sa = state.objs[obj_name] @@ -113,14 +113,16 @@ def stable_against( poly_a = state.planes.planerep_to_poly(pa) poly_b = state.planes.planerep_to_poly(pb) - normal_a = iu.global_polygon_normal(a_blender_obj, poly_a) - normal_b = iu.global_polygon_normal(b_blender_obj, poly_b) + normal_a = butil.global_polygon_normal(a_blender_obj, poly_a) + normal_b = butil.global_polygon_normal( + b_blender_obj, poly_b, rev_normal=relation.rev_normal + ) dot = np.array(normal_a).dot(normal_b) if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)): logger.debug(f"stable against failed, not parallel {dot=}") return False - origin_b = iu.global_vertex_coordinates( + origin_b = butil.global_vertex_coordinates( b_blender_obj, b_blender_obj.data.vertices[poly_b.vertices[0]] ) @@ -166,7 +168,7 @@ def stable_against( return False for vertex in poly_a.vertices: - vertex_global = iu.global_vertex_coordinates( + vertex_global = butil.global_vertex_coordinates( a_blender_obj, a_blender_obj.data.vertices[vertex] ) distance = iu.distance_to_plane(vertex_global, origin_b, normal_b) @@ -199,21 +201,21 @@ def coplanar( poly_a = state.planes.planerep_to_poly(pa) poly_b = state.planes.planerep_to_poly(pb) - normal_a = iu.global_polygon_normal(a_blender_obj, poly_a) - normal_b = iu.global_polygon_normal(b_blender_obj, poly_b) - if relation.rev_normal: - normal_b = -normal_b + normal_a = butil.global_polygon_normal(a_blender_obj, poly_a) + normal_b = butil.global_polygon_normal( + b_blender_obj, poly_b, rev_normal=relation.rev_normal + ) dot = np.array(normal_a).dot(normal_b) if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)): logger.debug(f"coplanar failed, not parallel {dot=}") return False - origin_b = iu.global_vertex_coordinates( + origin_b = butil.global_vertex_coordinates( b_blender_obj, b_blender_obj.data.vertices[poly_b.vertices[0]] ) for vertex in poly_a.vertices: - vertex_global = iu.global_vertex_coordinates( + vertex_global = butil.global_vertex_coordinates( a_blender_obj, a_blender_obj.data.vertices[vertex] ) distance = iu.distance_to_plane(vertex_global, origin_b, normal_b) @@ -224,11 +226,11 @@ def coplanar( return True -def snap_against(scene, a, b, a_plane, b_plane, margin=0): +def snap_against(scene, a, b, a_plane, b_plane, margin=0, rev_normal=False): """ snap a against b with some margin. """ - logging.debug("snap_against", a, b, a_plane, b_plane, margin) + logging.debug("snap_against", a, b, a_plane, b_plane, margin, rev_normal) a_obj = bpy.data.objects[a] b_obj = bpy.data.objects[b] @@ -237,14 +239,14 @@ def snap_against(scene, a, b, a_plane, b_plane, margin=0): a_poly = a_obj.data.polygons[a_poly_index] b_poly_index = b_plane[1] b_poly = b_obj.data.polygons[b_poly_index] - plane_point_a = iu.global_vertex_coordinates( + plane_point_a = butil.global_vertex_coordinates( a_obj, a_obj.data.vertices[a_poly.vertices[0]] ) - plane_normal_a = iu.global_polygon_normal(a_obj, a_poly) - plane_point_b = iu.global_vertex_coordinates( + plane_normal_a = butil.global_polygon_normal(a_obj, a_poly) + plane_point_b = butil.global_vertex_coordinates( b_obj, b_obj.data.vertices[b_poly.vertices[0]] ) - plane_normal_b = iu.global_polygon_normal(b_obj, b_poly) + plane_normal_b = butil.global_polygon_normal(b_obj, b_poly, rev_normal) plane_normal_b = -plane_normal_b norm_mag_a = np.linalg.norm(plane_normal_a) @@ -267,10 +269,10 @@ def snap_against(scene, a, b, a_plane, b_plane, margin=0): a_obj = bpy.data.objects[a] a_poly = a_obj.data.polygons[a_poly_index] # Recalculate vertex_a and normal_a after rotation - plane_point_a = iu.global_vertex_coordinates( + plane_point_a = butil.global_vertex_coordinates( a_obj, a_obj.data.vertices[a_poly.vertices[0]] ) - plane_normal_a = iu.global_polygon_normal(a_obj, a_poly) + plane_normal_a = butil.global_polygon_normal(a_obj, a_poly) distance = (plane_point_a - plane_point_b).dot(plane_normal_b) diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py index 37079f9f..c660869e 100644 --- a/infinigen/core/tagging.py +++ b/infinigen/core/tagging.py @@ -463,12 +463,6 @@ def tag_support_surfaces(obj, angle_threshold=0.1): angle_threshold (float): The cosine of the maximum angle deviation from +z to be considered a support surface. """ - def global_polygon_normal(obj, polygon): - loc, rot, scale = obj.matrix_world.decompose() - rot = rot.to_matrix() - normal = rot @ polygon.normal - return normal / np.linalg.norm(normal) - def process_mesh(mesh_obj): up_vector = Vector((0, 0, 1)) @@ -476,7 +470,7 @@ def process_mesh(mesh_obj): support_mask = np.zeros(n_poly, dtype=bool) for poly in mesh_obj.data.polygons: - global_normal = global_polygon_normal(mesh_obj, poly) + global_normal = butil.global_polygon_normal(mesh_obj, poly) if global_normal.dot(up_vector) > 1 - angle_threshold: support_mask[poly.index] = True diff --git a/infinigen/core/util/blender.py b/infinigen/core/util/blender.py index 41b9ad4e..20912b2f 100644 --- a/infinigen/core/util/blender.py +++ b/infinigen/core/util/blender.py @@ -17,6 +17,7 @@ import mathutils import numpy as np import trimesh +from mathutils import Vector from tqdm import tqdm from infinigen.core.nodes.node_info import DATATYPE_DIMS, DATATYPE_FIELDS @@ -1011,3 +1012,16 @@ def purge_empty_materials(obj): continue bpy.context.object.active_material_index = i bpy.ops.object.material_slot_remove() + + +def global_polygon_normal(obj, polygon, rev_normal=False): + loc, rot, scale = obj.matrix_world.decompose() + rot = rot.to_matrix() + normal = rot @ polygon.normal + if rev_normal: + normal = -normal + return normal / np.linalg.norm(normal) + + +def global_vertex_coordinates(obj, local_vertex) -> Vector: + return obj.matrix_world @ local_vertex.co diff --git a/infinigen_examples/constraints/util.py b/infinigen_examples/constraints/util.py index 3e08094a..d7202c63 100644 --- a/infinigen_examples/constraints/util.py +++ b/infinigen_examples/constraints/util.py @@ -69,8 +69,8 @@ hanging = cl.StableAgainst(top, ceilingtags, margin=0.05) side_against_wall = cl.StableAgainst(side, walltags, margin=0.05) -front_coplanar_front = cl.CoPlanar(front, front, margin=0.05) -back_coplanar_back = cl.CoPlanar(back, back, margin=0.05) +front_coplanar_front = cl.CoPlanar(front, front, margin=0.05, rev_normal=True) +back_coplanar_back = cl.CoPlanar(back, back, margin=0.05, rev_normal=True) ontop = cl.StableAgainst(bottom, top) on = cl.StableAgainst(bottom, {t.Subpart.SupportSurface})