4
$\begingroup$

I have a number of different objects (e.g. cube, cylinder, monkey, furniture, etc) which I would like to distribute randomly over a plane/surface (the floor in this case) without having any intersections between the objects themselves as well as the wall. The objects there is no problem to be rotated so that they fit the "room" structure.

enter image description here

From a quick view I noticed people suggesting using the particles operator or the randomize transform. Would any of these alternatives work? I would like to do this in a python script.

Testing sample/example:

This is the file with furniture that I would like to position over the floor plane in random positions:


Update:

It seems that using the randomize transform I can move the objects around the scene:

import bpy,random
from math import *

for obj in bpy.context.selected_objects:
    bpy.ops.object.randomize_transform(random_seed =random.randint(0,100), loc=(2, 2, 0), scale=(1, 1, 1))

enter image description here

The problem now is how to specify that I want the objects to be only within the borders of the floor (or the wall) object. I was thinking to specify loc= to the limits of the the bounding box of the floor but this will not handle the extruded parts. So there is the possibility that the moved object will be outside or intersecting to the wall. Moreover, from what I've noticed the randomize transform does not handle intersections thus this is also something that I would need to address.


Update 2:

Based on the script from @james_t below as well as from this answer I've created the following solution:

import bpy,random
from math import *
from mathutils.bvhtree import BVHTree

import bmesh
import numpy as np
from mathutils import Vector

def numpy_apply_transforms(ob, co):
    m = np.array(ob.matrix_world)    
    mat = m[:3, :3].T 
    loc = m[:3, 3]
    return co @ mat + loc

def are_inside(points, bool_name, boundary='inside'):
    """
    input: 
        points
        - a list of vectors (can also be tuples/lists)
        bm
        - a manifold bmesh with verts and (edge/faces) for which the 
          normals are calculated already. (add bm.normal_update() otherwise)
    returns:
        a list
        - a mask lists with True if the point is inside the bmesh, False otherwise
    """

    
    rpoints = []
    addp = rpoints.append
    
    target_object = bpy.context.object
    bool_object   = bpy.data.objects[bool_name]

    #create mesh data
    bm = bmesh.new()
    bm.from_mesh(bool_object.data)
    bmesh.ops.transform(bm, matrix=bool_object.matrix_world, verts=bm.verts) #local to global coord
    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()
    bm.faces.ensure_lookup_table()
    bvh = BVHTree.FromBMesh(bm, epsilon=0.0001)
    
    points = numpy_apply_transforms(target_object, points) #local to global coord

    # return points on polygons
    for point in points:
        fco, normal, _, _ = bvh.find_nearest(point)
        p2 = fco - Vector(point)
        v = p2.dot(normal)
        if boundary == 'outside':
            addp(not v < 0.0) # addp(v >= 0.0) ?
        else:    
            addp(v < 0.0)

    return rpoints

obj_list = ['IKEA_bed_LEIRVIK', 'IKEA_chair_MARKUS']

#print(bpy.data.objects['Wall'].dimensions.x)
#print(bpy.data.objects['Wall'].dimensions.y)

for obj in obj_list:    
    obj = bpy.context.scene.objects.get(obj)
    bpy.context.view_layer.objects.active = obj
    if obj: obj.select_set(True)
    
    maxIter = 5
    placed = False

    print("Object: {}".format(obj.name))
    print( "origLocation: {}".format(obj.location) )
    
    while(not placed):
        print("Iteration: {}".format(maxIter))
        
        if maxIter < 0:
            print("gave up!!!")
            break
        
        # set object back to original location on each loop, to try again from that loc:
        obj.location.x = bpy.data.objects['Floor'].location.x
        obj.location.y = bpy.data.objects['Floor'].location.y
        
        print( "startingLocation: {}".format(obj.location) )
        
        # put object in new location
        bpy.ops.object.randomize_transform( random_seed =random.randint(0,100), use_loc=True, loc=(bpy.data.objects['Floor'].dimensions.x/2, bpy.data.objects['Wall'].dimensions.y/2, 0.0), scale=(1, 1, 1))
        
        print( "newLocation: {}".format(obj.location) )
        
        # Check if object is out of wall boundary
        rvalAry = are_inside(obj.bound_box, "Wall")
        print(rvalAry)
        
        if all(rvalAry):
            
            # Check if object is colliding with other objects
            for ob in obj_list:
                ob = bpy.context.scene.objects.get(ob)
                if obj == ob:
                    continue
                
                rvalAry_ = are_inside(obj.bound_box, ob.name, 'outside')
                print(rvalAry_)
                
                if not any(rvalAry_):
                    print("Object placed!!!")
                    placed = True
            
        maxIter -= 1
    bpy.ops.object.select_all(action='DESELECT') # Deselect all objects

This seems to work perfectly for putting the objects within the wall boundaries in first place. However, for some reason it still gives a fail while the objects are within the wall boundary and not colliding to each other (for simplicity I just considered only two objects and set the maxIter=5 this should be enough to have an example showing up) which I do not understand why. What am I missing?


Update 3:

I've managed to solve the object overlapping with the following function:

def intersection_check(target_name, bool_name):
    
    target_object = bpy.data.objects[target_name]
    bool_object   = bpy.data.objects[bool_name]
    
    #create mesh data
    bm = bmesh.new()
    bm.from_mesh(target_object.data)
    bmesh.ops.transform(bm, matrix=target_object.matrix_world, verts=bm.verts) #local to global coord
    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()
    bm.faces.ensure_lookup_table()
    bvh = BVHTree.FromBMesh(bm, epsilon=0.0001)
    
    #create mesh data
    bm1 = bmesh.new()
    bm1.from_mesh(bool_object.data)
    bmesh.ops.transform(bm1, matrix=bool_object.matrix_world, verts=bm1.verts) #local to global coord
    bm1.verts.ensure_lookup_table()
    bm1.edges.ensure_lookup_table()
    bm1.faces.ensure_lookup_table()
    bvh1 = BVHTree.FromBMesh(bm1, epsilon=0.0001)
    
    if bvh.overlap( bvh1 ):
        return True
    else:
        return False

and then replacing the corresponding part from the previous update code snippet with the following:

            # Check if object is colliding with other objects
            rvalAry_ = []
            for ob in obj_list:
                ob = bpy.context.scene.objects.get(ob)
                if obj == ob:
                    continue
                
#                rvalAry_ = are_inside(obj.bound_box, ob.name, 'outside')
                rvalAry_.append( intersection_check(obj.name, ob.name) )
                print(rvalAry_)
                
            if not any(rvalAry_):
                print("Object placed!!!")
                placed = True

so far it seems to work ok.

$\endgroup$
7
  • $\begingroup$ And also adhere to the rules of feng shui ? Joking aside, this is pretty broad. Noticed people suggesting? please add something more substantial, links? $\endgroup$
    – batFINGER
    Feb 18 at 16:29
  • $\begingroup$ Apologies for not linking the other threads that I found. Some of the links that I have shortlisted are the following blender.stackexchange.com/questions/3322/… blender.stackexchange.com/questions/33132/… blender.stackexchange.com/questions/75754/… stackoverflow.com/questions/60885338/… $\endgroup$
    – ttsesm
    Feb 18 at 22:43
  • 1
    $\begingroup$ Is there any idea how I could check for intersections? For setting random location I could iterate over my objects and set it based on the dimensions of my "Wall" object: x = bpy.data.objects['Wall'].dimensions.x y = bpy.data.objects['Wall'].dimensions.y z = bpy.data.objects['Wall'].dimensions.z obj.location = (random.randrange(x),random.randrange(y) , random.randrange(z)) but then my concern is how to deal with the intersections. $\endgroup$
    – ttsesm
    Feb 21 at 20:21
  • $\begingroup$ There is also the bpy.ops.object.randomize_transform() operator function but I couldn't figure how it works. $\endgroup$
    – ttsesm
    Feb 22 at 11:00
  • $\begingroup$ Yup. After a little search and thinking perhaps blender.stackexchange.com/questions/31693/… offers a clue the ray_cast() methods let's you know if a point on the mesh is inside or outside, in terms of confirming whether two pieces of furniture overlap? $\endgroup$
    – james_t
    Feb 25 at 21:36
4
$\begingroup$

Here is perhaps a solution, using the closest_point_on_mesh operation. I just enhanced to test the extreme vertices (bound_box) on the object being placed to be sure that it is placed completely within the floor bounds. I tested with on odd shaped floor (not just a rectangle plane). Note that this solution assumes that the floor has many vertices (subdivide if not) so that the locality test (closest_point_on_mesh()) is accurate. One problem I could not solve was that

# set object back to original location on each loop, to try again from that loc:
    obj.location = origLocation

was being ignored so I changed it to = floor.location.[xy] in the following code:

for obj in bpy.context.selected_objects:
    maxIter = 50
    placed = False
    #origLocation = obj.location
    #print( obj.location )
    print( "obj.dimensions=" )
    print( obj.dimensions )
    origLocation = obj.location
    print( "origLocation" )
    print( origLocation )
    while ( not placed ):
        print( maxIter )
        # set object back to original location on each loop, to try again from that loc:
        obj.location.x = floor.location.x
        obj.location.y = floor.location.y
        print( "obj.location=" )
        print( obj.location )
        bpy.ops.object.randomize_transform( random_seed =random.randint(0,100), use_loc=True, loc=(0.5, 0.5, 0.0), scale=(1, 1, 1))
        # check all boundary points:
        locatn = obj.location
        print( "new location=" )
        print( locatn )
        placed = True  # if any bounds are outside the floor plan, then reject (set to false)
        for bb in obj.bound_box:
            locatn.x = obj.location.x + bb[0]
            locatn.y = obj.location.y + bb[1]
            rvalAry = floor.closest_point_on_mesh( locatn, distance=0.1 )
            if ( not rvalAry[0] ): 
                placed = False
        maxIter -= 1
        if ( (maxIter < 0) or (placed) ) :
            placed = True
            if ( maxIter < 0 ):
                print( "gave up" )
                obj.location = origLocation
            else:                
                print( "placed" )
                print( obj.location )        
    print( obj.location )
$\endgroup$
4
  • $\begingroup$ Thanks @james_t, it seems interesting. I will test ASAP and give some feedback. Ideally skipping through your code I was thinking that possibly I could combine it with this solution blender.stackexchange.com/a/150047/106722 so that to address any possible overlapping at the same time. $\endgroup$
    – ttsesm
    Feb 24 at 22:52
  • $\begingroup$ @james-t I tested your script but it is not always working. There are times that it is still putting part of the objects outside the wall object and it considers that it is inside. I've created an updated version in my initial question which works nicely for putting the objects within the wall boundaries but it fails when I have to check the collision with the other objects. $\endgroup$
    – ttsesm
    Feb 25 at 17:56
  • $\begingroup$ Yes I did notice it fails sometimes and in fact I had to make "distance=0.1" as =0 was a problem. I thought perhaps it was because of the granularity of the floor mesh. It failed a lot when I had just a simple four-vertex plane, worked better as I sub-divided, and better still dependent on the spacing of the vectors. It would be nice if there was a closest_FACE_on_mesh() function; or one could calculate that if I weren't such a "lazy good enough" programmer, so to speak! $\endgroup$
    – james_t
    Feb 25 at 18:17
  • $\begingroup$ I realized after posting that one should also build an array of placed objects and test against those also. Good catch! $\endgroup$
    – james_t
    Feb 25 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.