5

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.

ttsesm
  • 409
  • 3
  • 10
  • And also adhere to the rules of feng shui ? Joking aside, this is pretty broad. Noticed people suggesting? please add something more substantial, links? – batFINGER Feb 18 '21 at 16:29
  • Apologies for not linking the other threads that I found. Some of the links that I have shortlisted are the following https://blender.stackexchange.com/questions/3322/randomly-place-and-scale-objects https://blender.stackexchange.com/questions/33132/random-distribution-of-objects-over-a-plane https://blender.stackexchange.com/questions/75754/randomize-location-of-multiple-objects https://stackoverflow.com/questions/60885338/place-objects-randomly-within-a-range-in-blender-python – ttsesm Feb 18 '21 at 22:43
  • 1
    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. – ttsesm Feb 21 '21 at 20:21
  • There is also the bpy.ops.object.randomize_transform() operator function but I couldn't figure how it works. – ttsesm Feb 22 '21 at 11:00
  • Yup. After a little search and thinking perhaps https://blender.stackexchange.com/questions/31693/how-to-find-if-a-point-is-inside-a-mesh/80781#80781 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? – james_t Feb 25 '21 at 21:36
  • Yes I used this solution for finding whether the bounding points of an object is within the wall object boundaries. This worked nicely for that task, then I thought that I could use the same for finding whether two objects are colliding or not but for some reason it doesn't work. Anyways, I created a new function (check update 3) and this seems to work so far. – ttsesm Feb 25 '21 at 23:49
  • Ideally, I believe that the best solution would be to create a kind of lookup table based on the faces of the floor plane. Then randomly put the first object, you mark out the faces from the lookup table that the object covered, then you put the next object in a position of the remaining faces and so on. In this case you might be able to apply a best fit of floor area per object so that you do not have any object left out. Kind of like a tetris. It sounds a bit tricky though and I am not that experienced to apply that straight forward :-(. – ttsesm Feb 26 '21 at 10:40

1 Answers1

4

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 )
batFINGER
  • 84,216
  • 10
  • 108
  • 233
james_t
  • 5,446
  • 8
  • 29
  • 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 https://blender.stackexchange.com/a/150047/106722 so that to address any possible overlapping at the same time. – ttsesm Feb 24 '21 at 22:52
  • @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. – ttsesm Feb 25 '21 at 17:56
  • 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! – james_t Feb 25 '21 at 18:17
  • I realized after posting that one should also build an array of placed objects and test against those also. Good catch! – james_t Feb 25 '21 at 18:19