Simulating cell division in Blender
Published: Jun 2020Using Blender's rigid body physics and python scripting, we can create a very basic simulation of cell divison:
Each cell consists of a rigid body, which interacts with other rigid bodies in the simulation, and a metaball parented to the rigid body, which is what actually gets rendered on screen. When a cell duplicates, the rigid bodies of each cell push each other apart:
To begin, we'll set up a Cell class:
class Cell:
def __init__(self, rigid_body, metaball):
# initiate cell with a random division time
self.divisionTime = random.randint(60, 120)
# each cell is composed of two objects, a rigid body that is part of the simulation
# and a metaball that is rendered
self.rigid_body = rigid_body
self.metaball = metaball
The basic idea is simple: each cell has an internal counter which gets decremented each frame, and if the counter equals zero, the cell divides (creates a duplicate of itself):
def decrementTime(self):
if (self.divisionTime == 0):
self.divisionTime = random.randint(60, 120)
self.duplicate()
else:
self.divisionTime -= 1;
When a cell duplicates, we copy the rigid body and metaball from a prototypical cell object (which we just hide in another layer in Blender, not part of the simulation.) These objects are placed at the location of the cell which just duplicated:
def duplicate(self):
# get current cell position
matrix = self.rigid_body.matrix_world.copy()
print(self.rigid_body)
print(matrix.translation)
position = matrix.translation
# copy rigid body
rb = deep_copy(rb_orig)
rb.matrix_world = matrix
# ensure that the new rigid body object is in the rigid body collection
ensure_single_collection(rb, rigid_bodies)
# set rigid body position by translating the vertices in edit mode
rb.select_set(True)
bpy.context.view_layer.objects.active = rb
bpy.ops.object.editmode_toggle()
bpy.ops.transform.translate(value = position)
bpy.ops.object.editmode_toggle()
rb.select_set(False)
bpy.context.view_layer.objects.active = None
#copy metaball
mb = deep_copy(mb_orig)
# ensure that the new metaball is in the metaball collection
ensure_single_collection(mb, metaballs)
# position and parent the metaball to the rigid body
mb.location = position
mb.parent = rb
mb.matrix_parent_inverse = rb.matrix_world.inverted()
# create new cell object
new_cell = Cell(rb, mb)
# add to array of cells in simulation
cells.append(new_cell)
Some helper functions used in the function above for collection management and copying objects:
def ensure_single_collection(object, collection):
if object.name not in collection.objects:
collection.objects.link(object)
for c in bpy.data.collections:
if c is not collection and object.name in c.objects:
c.unlink(object)
if object.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(object)
def deep_copy(obj):
new_obj = obj.copy()
new_obj.data = obj.data.copy()
return new_obj
Since the Eevee render engine doesn't play well with frame handlers (at least on my machine), the render is handled by a simple loop:
for frame in range(scene.frame_start, scene.frame_end + 1):
add_cells(scene)
bpy.context.scene.render.filepath = filepath + str(frame).zfill(4)
scene.frame_set(frame)
bpy.ops.render.render(write_still=True)
if frame == scene.frame_end:
bpy.context.scene.render.filepath = filepath #reset filepath
The function add_cells() decrements counter for each cell in our cell array, and handles clearing objects when we reach the end of the range of rendered frames:
def add_cells(scene):
if scene.frame_current == scene.frame_end:
bpy.ops.screen.animation_cancel()
remove_all_objects_from_collection(rigid_bodies)
remove_all_objects_from_collection(metaballs)
remove_unused_data_blocks()
else:
for cell in cells:
cell.decrementTime()
def remove_unused_data_blocks():
for block in bpy.data.meshes:
if block.users == 0:
bpy.data.meshes.remove(block)
def remove_all_objects_from_collection(collection):
objects_to_delete = collection.objects
for o in objects_to_delete:
bpy.data.objects.remove(o, do_unlink=True)
(This approach could definitely be improved – for example, the end condition should probably be handled inside the render loop itself.)
The full script is as follows:
import bpy, math, random
scene = bpy.context.scene
bpy.context.scene.frame_set(1)
rigid_bodies = bpy.data.collections['Rigid Bodies']
metaballs = bpy.data.collections['Metaballs']
class Cell:
def __init__(self, rigid_body, metaball):
# initiate cell with a random division time
self.divisionTime = random.randint(60, 120)
# each cell is composed of two objects, a rigid body that is part of the simulation
# and a metaball that is rendered
self.rigid_body = rigid_body
self.metaball = metaball
def decrementTime(self):
if (self.divisionTime == 0):
self.divisionTime = random.randint(60, 120)
self.duplicate()
else:
self.divisionTime -= 1;
def duplicate(self):
# get current cell position
matrix = self.rigid_body.matrix_world.copy()
print(self.rigid_body)
print(matrix.translation)
position = matrix.translation
# copy rigid body
rb = deep_copy(rb_orig)
rb.matrix_world = matrix
# ensure that the new rigid body object is in the rigid body collection
ensure_single_collection(rb, rigid_bodies)
# set rigid body position by translating the vertices in edit mode
rb.select_set(True)
bpy.context.view_layer.objects.active = rb
bpy.ops.object.editmode_toggle()
bpy.ops.transform.translate(value = position)
bpy.ops.object.editmode_toggle()
rb.select_set(False)
bpy.context.view_layer.objects.active = None
#copy metaball
mb = deep_copy(mb_orig)
# ensure that the new metaball is in the metaball collection
ensure_single_collection(mb, metaballs)
# position and parent the metaball to the rigid body
mb.location = position
mb.parent = rb
mb.matrix_parent_inverse = rb.matrix_world.inverted()
# create new cell object
new_cell = Cell(rb, mb)
# add to array of cells in simulation
cells.append(new_cell)
def ensure_single_collection(object, collection):
if object.name not in collection.objects:
collection.objects.link(object)
for c in bpy.data.collections:
if c is not collection and object.name in c.objects:
c.unlink(object)
if object.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(object)
def deep_copy(obj):
new_obj = obj.copy()
new_obj.data = obj.data.copy()
return new_obj
def add_cells(scene):
if scene.frame_current == scene.frame_end:
bpy.ops.screen.animation_cancel()
remove_all_objects_from_collection(rigid_bodies)
remove_all_objects_from_collection(metaballs)
remove_unused_data_blocks()
else:
for cell in cells:
cell.decrementTime()
def remove_unused_data_blocks():
for block in bpy.data.meshes:
if block.users == 0:
bpy.data.meshes.remove(block)
def remove_all_objects_from_collection(collection):
objects_to_delete = collection.objects
for o in objects_to_delete:
bpy.data.objects.remove(o, do_unlink=True)
remove_all_objects_from_collection(rigid_bodies)
remove_all_objects_from_collection(metaballs)
remove_unused_data_blocks()
cells = []
rb_orig = bpy.data.objects["Rigid Body"]
mb_orig = bpy.data.objects["Mball"]
first_cell = Cell(rb_orig, mb_orig)
first_cell.duplicate()
cells[0].divisionTime = 1
filepath = bpy.context.scene.render.filepath = "/Users/sean/Desktop/cell_division/test-frames/"
for frame in range(scene.frame_start, scene.frame_end + 1):
add_cells(scene)
bpy.context.scene.render.filepath = filepath + str(frame).zfill(4)
scene.frame_set(frame)
bpy.ops.render.render(write_still=True)
if frame == scene.frame_end:
bpy.context.scene.render.filepath = filepath #reset filepath
A couple things to note:
1) In order to get accurate positions for the rigid bodies, the Steps per Second and Solver Iterations both need to be turned way up from the defaults (I arbitrarily set 200 for each).
2) In order to keep the cells clustered together, I added an attractive force field pulling the rigid bodies towards the center.
Download the blender file if you'd like to take a look around or use it in one of your projects.