3
$\begingroup$

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.

enter image description here

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: enter image description here

enter image description here

enter image description here

enter image description here

$\endgroup$
8
  • $\begingroup$ Hello, could you specify where these nodes come from ? $\endgroup$
    – Gorgious
    Commented Jan 11, 2021 at 12:25
  • $\begingroup$ Added the code to recreate the mesh, the nodes are coming from the VI-Suite addon which I use afterwards to apply some lighting simulation, but they are irrelevant with the creation of the room. Once you have the complete mesh you can just feed them to the addon input. $\endgroup$
    – ttsesm
    Commented Jan 11, 2021 at 13:35
  • $\begingroup$ Oh okay thanks, I thought that was generated with the nodes. May be a bit too specialized to be answered in a single question though $\endgroup$
    – Gorgious
    Commented Jan 11, 2021 at 13:42
  • $\begingroup$ Why you say that? In principle I need to figure out how to extrude parts of the wall and transform others so that I create "corridors" but I keep the floor area fixed and if possible to do that in an efficient way. $\endgroup$
    – ttsesm
    Commented Jan 11, 2021 at 13:48
  • $\begingroup$ To clarify somewhat could you please add an example of what some random extruded variations would look like? $\endgroup$
    – batFINGER
    Commented Jan 12, 2021 at 2:55

1 Answer 1

2
$\begingroup$

Bmesh version.

enter image description here

As a proof of concept thought I'd put together a bmesh version.

  • Add a random n x m grid mesh as main room, dissolve all internal geometry and some but not all inline verts.

  • Get the edges. While there is still some area to make up, extrude an edge, or extrude to match area.

  • Check the area remaining.

  • Clean up by removing any cases of extruding neighbouring edge.. prob better to remove when chosen..

Test script

import bpy
import bmesh
from mathutils import Matrix, Vector
from random import random, randint, uniform, choice
from math import pi

# add some BS mesh quickly
bpy.ops.mesh.primitive_plane_add()


bm = bmesh.new()
up = Vector((0, 0, 1))
# make the main room
max_area = 25
height = 2
x_segments = randint(3, 6)
y_segments = randint(3, min((max_area // x_segments, 5)))
bmesh.ops.create_grid(
        bm,
        x_segments=x_segments,
        y_segments=y_segments,
        size=0.5,
        matrix=Matrix.Diagonal(
                (x_segments, y_segments, 1)
            )
        )

verts = [
        v for v in bm.verts 
        if len(v.link_edges) == 4
        or (random() < 0.6
            
        and len(v.link_faces) == 2)]

bmesh.ops.dissolve_verts(
        bm,
        verts = verts,
        )
edges = bm.edges[:]

bm.faces.ensure_lookup_table()
area = bm.faces[0].calc_area()
rem_area = max_area - area
while edges and rem_area > 0:
    # pick an edge
    
    e = edges.pop(randint(0, len(edges) - 1))
    l = e.calc_length()
    s = min((rem_area / l, randint(1, 3))) 
    
    vec = s * up.cross(e.verts[0].co - e.verts[1].co).normalized()
    ret = bmesh.ops.extrude_edge_only(
            bm,
            edges=[e],
            )["geom"]
    
    bmesh.ops.translate(
            bm, 
            verts=[e for e in ret if isinstance(e, bmesh.types.BMVert)],
            vec=vec,
            )
    bmesh.ops.dissolve_edges(
            bm,
            edges=[e for e in bm.edges if len(e.link_faces) > 1],
            )
    area = bm.faces[:].pop().calc_area() 
    rem_area = max_area - area
    #break
    
bmesh.ops.dissolve_verts(
        bm,
        verts = [v for v in bm.verts if
                v.calc_edge_angle() > 0.9 * pi]
        )    
edges = bm.faces[:].pop().edges[:]

face = bmesh.ops.extrude_discrete_faces(
        bm,
        faces=bm.faces,
        )["faces"].pop()

bmesh.ops.translate(
        bm,
        verts=face.verts,
        vec=height * up,
        )
        
bmesh.ops.contextual_create(
        bm,
        geom=edges,
        )
# quick hacky hack to swap into plane mesh added via operator.             
bm.to_mesh(bpy.context.object.data)
bpy.context.object.data.update()

PS have made this one mesh object instead of three. Can see that writing to other meshes using bmesh would be trivial.

Have also done a lazy mesh swap in. Create the object and mesh and add to a collection yada yada.

$\endgroup$
9
  • $\begingroup$ thanks a lot and apologies for the late response. The script seems to work quite nicely. Just some clarification questions, 1) in order the script to work as I understand it needs to be at least one random (e.g. plane, cube, etc) mesh/object in the grid (I think this is what you say in your last sentence). 2) I've noticed that the bottom of the new object, i.e. my "floor" in this case is always missing. 3) do you check somewhere whether the extrude area is not smaller than a threshold so that it is not slim and long $\endgroup$
    – ttsesm
    Commented Jan 15, 2021 at 11:46
  • 1
    $\begingroup$ Fixed the floor. Posted as an example. Used an integer grid to start, cannot have 1 or 2 as a dimension, eg could be 6x3 all edges will be 1 bu long. Some verts are disolved so edges could be in length range 1 to 6 . Then extruded s = min((rem_area / l, randint(1, 3))) either just enough to make total area exactly 25 or at most length 3. (which is a bit jetty like when edge length of 1 is extruded) Once again posted as an example there are a number of other bmesh ops to look at here, eg sub'd an edge without grid fill instead of starting with a grid. Will leave that up to you. $\endgroup$
    – batFINGER
    Commented Jan 15, 2021 at 12:20
  • $\begingroup$ thanks a lot ;-) $\endgroup$
    – ttsesm
    Commented Jan 15, 2021 at 12:50
  • $\begingroup$ it works perfect, I've also managed to separate it to "Wall", "Ceiling", "Floor" by using the extract_plane_from_room() from my initial post. The only problem is that when I apply subdivision the "Ceiling" and "Floor" objects are not affected. Any specific reason for that? I've tried both with bmesh.ops.subdivide_edges() using a bmesh and with bpy.ops.mesh.subdivide() and working in EDIT mode. $\endgroup$
    – ttsesm
    Commented Jan 15, 2021 at 14:41
  • $\begingroup$ Good one, however Once again could do it via bmesh. Make the floor write to a mesh. Write to a mesh as ceiling (transform by height). Then the walls is bridge edge loops of both. Re applying subdivision, might need to quad the ngons first. Made a post re doing this recently. $\endgroup$
    – batFINGER
    Commented Jan 15, 2021 at 14:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .