3
$\begingroup$

I want to add several spheres (rocks) to my scene to create this image:

enter image description here

To achieve this look, I have to pack the spheres. And I want to do it in python. That's my plan:

  1. Define the radius of the circle; the number of spheres to pack
  2. Add a sphere at a random location inside the sphere. Add this object to a list as well
  3. add the next sphere; check if the sphere is intersecting with spheres in the list. If not, add the new sphere...

How can I add an object to a list after creating it to loop over this object-list afterwards.

My code so far:

import bpy
import random
import math

radius = 50
ptnum = 80

curserPos = bpy.context.scene.cursor.location

spheres = []

for i in range(ptnum):
    r = random.random() * radius
    alpha = random.random() * 2 * math.pi

    x = curserPos[0] + math.sqrt(r) * math.cos(alpha)
    y = curserPos[1] + math.sqrt(r) * math.sin(alpha)

    bpy.ops.mesh.primitive_ico_sphere_add(location=(x,y,0))

enter image description here

$\endgroup$
2
  • $\begingroup$ Do the spheres need to decrease in size as they sit farther from the center? In the example there seems to be a horizontal strip of thicker spheres as well. Are these requirements? $\endgroup$
    – Dazotaro
    Apr 17, 2019 at 16:20
  • $\begingroup$ Yes, but that could be solved later. Therefore you just have to fit the absolute y value in a new range. $\endgroup$ Apr 17, 2019 at 16:33

1 Answer 1

2
$\begingroup$

Creating an empty list, and adding objects to it

# Create empty list
lobjects = []
# adding an object to it
lobjects.append(rock_object)

If you create more objects, you keep appending them to lobjects.

Creating objects

If you already have a template rock object that you want to use to create all of the other rocks:

  1. Duplicating the mesh data of the template rock object

    original_data = rock_object.data new_rock_obj = bpy.data.objects.new('CopiedMeshRock', original_data.copy())

  2. Linking the original object's mesh data

    new_rock_obj = bpy.data.objects.new('LinkedMeshRock', original_data)

Distribution of the rocks:

I think you could start by creating a grid, so an object is placed within each cell of this grid (I am using spheres for this example)

enter image description here

Then if you distort this grid, using any (radial?) distortion function, you can change the size of each cell of the grid, and fit a sphere at the centers with a radius less or equal to the smallest side of the cell.

enter image description here

You would only do this for cells that fall within the desired target pattern (a circle in your case).

I have used the following code to generate those images. If you set $a_x = a_y = 0.0$ you get the first image (no distortion); if you set $a_x$ and $a_y$ to positive values you get barrell distortion.

Blender

import bpy          # bpy.data.filepath (path to the current blend file)
import mathutils    # mathutils.Vector

class ExitError(Exception):
    pass

def addSphere(center, radius):
    '''
    Add a sphere at the given location, with the given radius

    @param center (mathutils.Vector) 3D location of the center of the sphere
    @param radius   (float) Radius of the sphere
    #return (bpy.types.object) newly created object
    '''
    bpy.ops.mesh.primitive_uv_sphere_add(radius = radius, location = center)

    return bpy.context.active_object

def distortPoint(p, ax, ay):
    '''
    Distort point p

    @param p    (mathutils.Vector) 2D point to be distorted
    @param ax   (float) Distortion coefficient in X
    @param ay   (float) Distortion coefficient in Y
    @return (mathutils.Vector) Distorted 2D point
    '''
    px = p.x * ( 1.0 - ax * p.length * p.length)
    py = p.y * ( 1.0 - ay * p.length * p.length)

    return mathutils.Vector((px, py))

def isPointWithinCircle(p, center, radius):
    '''
    Is point p within circle

    @param p        (mathutils.Vector) 2D point
    @param center   (mathutils.Vector) Center of circle
    @param radius   (float) radius
    @return         (boolean)
    '''
    return (p - center).length < radius


def main():
    #fireworks.testDrawCubes(5, mathutils.Vector((2, 1, 0)))
    #fireworklib.drawCubes(2)
    scene = bpy.data.scenes.get('Scene')
    if not scene:
        raise ExitError('Could not retrieve scene')

    # Dimensions of the grid
    nrows = 50
    ncols = 50
    # Original size of each cell of the grid
    width = 0.8
    height = 0.8
    center = mathutils.Vector(( (nrows / 2.0) * width, (ncols / 2.0) * height ))
    # Normalized grid cell size
    nwidth = 1.0 / nrows
    nheight = 1.0 / ncols
    nwidth_half = nwidth / 2.0
    nheight_half = nheight / 2.0
    # Center of the normalized grid
    ncenter = mathutils.Vector(( (nrows / 2.0) * nwidth, (ncols / 2.0) * nheight ))
    # distortion coefficients
    ax = 0.9
    ay = 1.5
    # Circle: Center at center, and with radius
    radius = width * nrows / 3.0

    # List of created objects
    lobjects = []

    for i in range(nrows):
        for j in range(ncols):
            # Calculate center of the cell
            pc = mathutils.Vector(( i * nwidth + nwidth_half, j * nheight + nheight_half)) - ncenter
            # distort it
            dpc = distortPoint(pc, ax, ay) + ncenter
            # offset by center
            unnormalized_center = mathutils.Vector((dpc.x * width / nwidth, dpc.y * height / nheight))

            # only proceed if the center of the distorted cell falls within the circle
            if isPointWithinCircle(unnormalized_center, center, radius):
                p0 = mathutils.Vector((    i * nwidth,     j * nheight)) - ncenter
                p1 = mathutils.Vector((    i * nwidth, (j+1) * nheight)) - ncenter
                p2 = mathutils.Vector(((i+1) * nwidth,     j * nheight)) - ncenter
                p3 = mathutils.Vector(((i+1) * nwidth, (j+1) * nheight)) - ncenter

                # compute distorted points
                dpc = distortPoint(pc, ax, ay) + ncenter
                d0 = distortPoint(p0, ax, ay)
                d1 = distortPoint(p1, ax, ay)
                d2 = distortPoint(p2, ax, ay)
                d3 = distortPoint(p3, ax, ay)

                # approximate distorted bounding box
                dwidth = (d1 - d0).length * width / nwidth
                dheight = (d2 - d0).length * height / nheight
                # smallest side of the bounding box approximation
                dmin = min(dwidth, dheight) / 2.0

                # sphere's radio cannot be larger than smallest side of the approximate bounding box
                r = dmin

                lobjects.append(addSphere(mathutils.Vector((dpc.x * width / nwidth, dpc.y * height / nheight, 0.0)), r))

if __name__ == '__main__':
    try:
        main()
    except ExitError as e:
        print('Failed: {}'.format(e))

Note:* The example above creates a new sphere for each cell of the grid, which in this case is very inefficient: we could just reuse the same mesh and have the subsequent spheres link to it.

Another idea, if you use objects that are not spherical, is to:

  1. give the objects (rocks) a small value to their location Z coordinate
  2. enable rigid body dynamics
  3. add a plane right below the given Z coordinate
  4. Animate the scene so the objects fall by that small z, colliding with the plane and with each other, naturally randomizing them (without having to do that from code).

You could add rigid body dynamics for an object with

sphere = addSphere(mathutils.Vector((dpc.x * width / nwidth, dpc.y * height / nheight, 0.0)), r)
scene.rigidbody_world.collection.objects.link(sphere)                    

# add to the list of objects
lobjects.append(sphere)

Note: make sure that the target scene has rigid body world enabled:

if not scene.rigidbody_world:
    bpy.ops.rigidbody.world_add()

if not scene.rigidbody_world.collection:
    scene.rigidbody_world.collection = bpy.data.collections.new('RigidBodyWorld')
$\endgroup$

You must log in to answer this question.

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