2
$\begingroup$

I'm trying to place an object at a random place on a plane. So far, I found out how to get the vertices locations in real world.

import bpy

obj = bpy.context.active_object
vert = obj.data.vertices

vertex_0 = obj.matrix_world @ vert[0].co
vertex_1 = obj.matrix_world @ vert[1].co
vertex_2 = obj.matrix_world @ vert[2].co
vertex_3 = obj.matrix_world @ vert[3].co

print(vertex_0, vertex_1)

But now, I'm stuck. How can I give an object coordinates somewhere between those vertices?

I can imagine how this would work in 2 dimensions, but with a rotated plane in 3D? My head explodes.

EDIT:

Another idea was to work in local space. Parent an object to the plane and work from there. But then will the child object be scaled with the parent object. And that is not what I want. So I used the "Child of" constraint and disabled the scale like this:

import bpy

obj = bpy.context.object

# put the location on a vertex of the plane
obj.location = (-1,1,0)

# give the constraint to the selected object
obj.constraints.new('CHILD_OF')
obj.constraints["Child Of"].use_scale_x = False
obj.constraints["Child Of"].use_scale_y = False
obj.constraints["Child Of"].use_scale_z = False
obj.constraints["Child Of"].target = bpy.context.scene.objects['Plane']

But after clicking the "Clear Inverse" Button in the constraint, the objects location is not on the vertex of the plane how it should be (when the plane is scaled before the script is loaded). Instead, the origin of the object will be on the expected vertex, when the Scale X, Y and Z in the Constraint is enabled again. (Scale is disabled in the script).
So: How can I put an object on another object and having a child relationship without scaling but with the right location?

$\endgroup$
4
  • 1
    $\begingroup$ Hello, what kind of mesh will you be using this on ? You can't for sure expect 4 points to be on the same plane. Try using only 3 coordinates. See stackoverflow.com/questions/47410054/… $\endgroup$ – Gorgious May 5 at 9:42
  • $\begingroup$ The mesh is a plane. Isn't there an easy way to place an object randomly on it? Looks like a persistent task, but I couldn't find something about it. $\endgroup$ – Andi May 5 at 10:27
  • $\begingroup$ So is it a face (as in title) or a plane? Do you mean any quad, or a square? I assume the quad always has 4 vertices, as that's what you show in your code. But are those vertices on the same plane, i.e. can you look at the face at such angle, that it looks like a line? Here's an extreme example of a quad which doesn't have all vertices on a single plane: i.imgur.com/w0ehd8w.png $\endgroup$ – Markus von Broady May 5 at 11:47
  • $\begingroup$ It's a normal plane you can add from the "Shift A" menu. I asked for a face in the hope to find the very best solution to work on every imaginable object. Sorry for the confusion. $\endgroup$ – Andi May 5 at 12:16
2
$\begingroup$

Use the local space.

The default plane in blender has local coordinates where x and y range from -1 to 1 and z is zero.

>>> me = C.object.data
>>> for v in me.vertices:
...     v.co[:]
...     
(-1.0, -1.0, 0.0)
(1.0, -1.0, 0.0)
(-1.0, 1.0, 0.0)
(1.0, 1.0, 0.0)

With this arrangement can consider only the x, y coordinates effectively making it 2D within the plane z=0.

Hence any point with x and y randomly chosen from that range will be on the plane.

Simple test script, with an object that is the default plane "Plane" with any arbitrary global transform, and it will add an empty on its surface.

Multiplying this local point by the objects matrix world will ensure it is at a global location on the surface of planes current position.

import bpy
from mathutils import Vector, Matrix                    
from random import uniform
context = bpy.context
scene = context.scene

plane = scene.objects.get("Plane")
if plane:
    vec = Vector((uniform(-1, 1), uniform(-1, 1), 0))
    bpy.ops.object.empty_add(
            location = plane.matrix_world @ vec,
            ) 

Will re-iterate this relies on the plane having its original local or mesh coordinates in range explained above, making it a simple task to place an empty on its surface.

Via Parenting.

Another way to use local coordinates is via parenting. In this case need only set the local location of the empty within range above to have it on the plane. This will have the effect of aligning the axes of empty to plane.

if plane:
    vec = Vector((uniform(-1, 1), uniform(-1, 1), 0))
    bpy.ops.object.empty_add()
    mt = context.object
    mt.parent = plane
    mt.location = vec 

To make the influence of the parent planes scale to empty unity for any subsequent children of the empty,. Does a child object inherit the matrix from the parent?

if plane:
    mw = plane.matrix_world
    _, _, scale = mw.decompose()
    S = Matrix.Diagonal(scale.to_4d())
    vec = Vector((uniform(-1, 1), uniform(-1, 1), 0))
    bpy.ops.object.empty_add()
    mt = context.object
    mt.parent = plane
    mt.matrix_parent_inverse = S.inverted()
    mt.location = S @ vec 

For an arbitrarily un axis aligned plane it is a bit more complex. https://stackoverflow.com/questions/42352622/finding-points-within-a-bounding-box-with-numpy

$\endgroup$
4
  • $\begingroup$ Thank you batFINGER. This is nearly the same way, I made before. The problem now is, that the scaling from the plane will also scale the parented object(s). To avoid this, I've tried to use the "Child of" Constraint and disable the scale XYZ. But there was another problem: How to make without an ops the "Clear inverse" function? I've tried so much with the matrix_world... but this is all a new topic for me. So I hoped for another solution, but maybe it's still better in local space. Can you tell me how to "Clear Inverse" for the Child of Constraint in a good way? $\endgroup$ – Andi May 5 at 12:12
  • $\begingroup$ IIRC have answered that question. blender.stackexchange.com/questions/122586/… $\endgroup$ – batFINGER May 5 at 12:17
  • 1
    $\begingroup$ @Andi added example to remove influence of scale between child placed empty and plane. $\endgroup$ – batFINGER May 5 at 12:39
  • $\begingroup$ Yes, this is what I'm looking for! Thank you very much. A tiny little edit if someone finds this later and is confused as I am : from mathutils import Vector, Matrix. Tried to modify your post, but for an edit do I have to make at least 10 characters (Matrix is not imported at this moment and the word is not long enough for an edit.) $\endgroup$ – Andi May 5 at 12:57
2
$\begingroup$

This solution will work for any face (triangle, quad, ngon) generating points with even distribution. I think the conversion from local to other coordinate system has been explained by batFINGER, so there's no reason to deal with it here:

import bpy, bmesh, random
from mathutils import Vector

num_points = 1000


def point_on_triangle(face):
    a, b, c = map(lambda v: v.co, face.verts)
    a2b = b - a  # vector from a to b
    a2c = c - a  
    height = random.triangular(low=0.0, high=1.0, mode=0.0)
    return a + a2c*height + a2b*(1-height)*random.random()


def main():
    me = bpy.context.active_object.data
    bm = bmesh.from_edit_mesh(me)
    temp_geom = bmesh.ops.duplicate(bm, geom=[bm.select_history.active])
    temp_face = next(el for el in temp_geom['geom'] if isinstance(el, bmesh.types.BMFace))
    triangles = bmesh.ops.triangulate(bm, faces=[temp_face])['faces']
    surfaces = map(lambda t: t.calc_area(), triangles)
    choices = random.choices(population=triangles, weights=surfaces, k=num_points)
    points = map(point_on_triangle, choices)
    temp_verts = list({v for tri in triangles for v in tri.verts})
    
    for point in points:
        bm.verts.new(point)
    bmesh.ops.delete(bm, geom=temp_verts)
    bmesh.update_edit_mesh(me)
    
    
if __name__ == "__main__":
    main()

  1. Duplicate a face and triangulate it.
  2. Randomize a list of triangles (they can repeat) based on their surfaces (a triangle B with twice more surface than triangle A will appear twice more often in the list.
  3. For each position of the list, take the triangle on that position and randomize a point inside it.
  4. Randomizing a point on a triangle: randomize a point on a rectangle made of 2 such triangles, but multiply the vector along "base" (whichever edge is assumed as base doesn't matter) by the distance from the "top" (the vertex not making the "base"), to actually get a point on a triangle and not on the imagined rectangle. For an even distribution, randomize the height on which the point will spawn first, with non-uniform distribution, countering the "stretch" - random.triangular.
  5. Gather a list of temporary vertices, passing them through a set to get rid of duplicates, and remove them.
$\endgroup$
1
  • $\begingroup$ Mind blowing! Thank you Markus von Broady, $\endgroup$ – Andi May 6 at 5:55

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.