I have the following rectangular mesh which kind of simulates the structure of a room (no window or doors at the moment), so I have four objects a floor, a ceiling, walls and a light. This has a specific floor area which is related to the area of the floor plane.
Now I would like to apply a given number of extrusions randomly to the mesh so that to create variations of the room but keeping the same floor area and without the created "corridor" being smaller than a specific area size so that I do not have long and slim extrusions. Any ideas how to do that efficiently with a script.
Thanks.
The code I used to generate the rectangle and define the objects is the following (it is a bit messy):
import warnings
import math
from math import radians, fabs, acos
import bpy
import bmesh
import mathutils
import numpy as np
import random
def post_process_objects():
bpy.ops.object.select_all(action='DESELECT') # Deselect all objects
for o in ("Wall", "Ceiling", "Floor"):
obj = bpy.context.scene.objects.get(o)
bpy.context.view_layer.objects.active = obj
if obj: obj.select_set(True)
# Get into edit mode
bpy.ops.object.mode_set(mode="EDIT")
# Make sure normals are pointing inwards
bpy.ops.mesh.select_all(action='SELECT') # select all faces from the selected/active objects
bpy.ops.mesh.normals_make_consistent(inside=True) # flip normals
# # Subdivide faces
# bpy.ops.mesh.subdivide(number_cuts=12)
# Return back to object mode
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action='DESELECT') # Deselect all objects
def get_median_face_pose(face: bmesh.types.BMFace, matrix_world: mathutils.Matrix):
# calculate the median position of the current face
median_pose = face.calc_center_median().to_4d()
median_pose[3] = 1.0
median_pose = matrix_world @ median_pose
return median_pose
def check_face_angle(face: bmesh.types.BMFace, matrix_world: mathutils.Matrix, up_vector: mathutils.Vector, cmp_angle: float):
# calculate the normal
normal_face = face.normal.to_4d()
normal_face[3] = 0.0
normal_face = (matrix_world @ normal_face).to_3d()
# compare the normal to the current up_vec
return acos(normal_face @ up_vector) < cmp_angle
def check_face_with(face: bmesh.types.BMFace, matrix_world: mathutils.Matrix, height_value: float, cmp_height: float, up_vector: mathutils.Vector, cmp_angle: float):
median_pose = get_median_face_pose(face, matrix_world)
# compare that pose to the current height_band
if fabs(median_pose[2] - height_value) < cmp_height:
return check_face_angle(face, matrix_world, up_vector, cmp_angle)
return False
def split_at_height_value(bm: bmesh.types.BMesh, height_value: float, compare_height: float, up_vector: mathutils.Vector, cmp_angle: float, matrix_world: mathutils.Matrix):
# deselect all faces
counter = 0
for f in bm.faces:
if check_face_with(f, matrix_world, height_value, compare_height, up_vector, cmp_angle):
counter += 1
f.select = True
print("Selected {} polygons as floor".format(counter))
return counter
def extract_plane_from_room(obj: bpy.types.Object, used_split_height: float, up_vec: mathutils.Vector, new_name_for_obj: str):
compare_height = 0.15
compare_angle = math.radians(7.5)
obj.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bm = bmesh.from_edit_mesh(mesh)
bm.faces.ensure_lookup_table()
# split the floor at the wall height
counter = split_at_height_value(bm, used_split_height, compare_height, mathutils.Vector(up_vec), compare_angle, obj.matrix_world)
# if any faces are selected split them up
if counter:
bpy.ops.mesh.separate(type='SELECTED')
bpy.ops.object.mode_set(mode='OBJECT')
bm.free()
mesh.update()
cur_selected_objects = bpy.context.selected_objects
if cur_selected_objects:
if len(cur_selected_objects) == 2:
cur_selected_objects = [o for o in cur_selected_objects
if o != bpy.context.view_layer.objects.active]
cur_selected_objects[0].name = new_name_for_obj
cur_created_obj = cur_selected_objects[0]
else:
raise Exception("There is more than one selection after splitting, this should not happen!")
else:
raise Exception("No floor object was constructed!")
bpy.ops.object.select_all(action='DESELECT')
return True, cur_created_obj
else:
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
return False, None
wall_obj = None
floor_obj = None
ceiling_obj = None
used_floor_area = 25
fac_from_square_room = 0.3
corridor_width = 0.9
wall_height = 2.8
# calculate the squared room length for the base room
squared_room_length = np.sqrt(used_floor_area)
# create a new plane and rename it to Wall
bpy.ops.mesh.primitive_plane_add()
wall_obj: bpy.types.Object = bpy.context.object
wall_obj.name = "Wall"
# calculate the side length of the base room, for that the `fac_from_square_room` is used
room_length_x = fac_from_square_room * random.uniform(-1, 1) * squared_room_length + squared_room_length
# make sure that the floor area is still used
room_length_y = used_floor_area / room_length_x
# change the plane to this size
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.transform.resize(value=(room_length_x * 0.5, room_length_y * 0.5, 1))
bpy.ops.object.mode_set(mode='OBJECT')
mesh = wall_obj.data
# create walls based on the outer shell
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.normals_make_consistent(inside=False)
bm = bmesh.from_edit_mesh(mesh)
bm.edges.ensure_lookup_table()
# select all edges
boundary_edges = [e for e in bm.edges if e.is_boundary]
for e in boundary_edges:
e.select = True
# extrude all boundary edges to create the walls
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={"value": (0, 0, wall_height)})
bpy.ops.object.mode_set(mode='OBJECT')
bm.free()
mesh.update()
for used_split_height in [(0, "Floor", [0, 0, -1]), (wall_height, "Ceiling", [0, 0, 1])]:
created, created_obj = extract_plane_from_room(wall_obj, used_split_height[0], used_split_height[2], used_split_height[1])
# save the result accordingly
if created and created_obj is not None:
if "Floor" == used_split_height[1]:
floor_obj = created_obj
elif "Ceiling" == used_split_height[1]:
ceiling_obj = created_obj
post_process_objects()
And this are some examples of what I would like to get as an output: