Bmesh version.
Pyramid
Scaling a tetra particle on a single vert point cloud, such that each time it doubles in size, it uses the next level pyramid as a particle
Make one tetrahedron from coordinates. I have skinned it lazilly with convex hull
Now for each level, its pretty much from any vertex corner of the first tetrahedron, make 3 copies and move to the other end of each linked edge.
As the level increases by one the number of unit tetra edges that make up the offset doubles.
So pretty much make a unit mesh, for each level copy and distribute mesh 3 times, write to mesh.
Pointcloud
As well as creating a skinned unit form tetrahedrons, can simply seed the pyramid with a single vertex mesh, at the tetra origin. Dupliverts or particle systems can then instance our units.
Four units, a level 0, 1, 2 and 3 pyramid. Three point clouds at level 0, 4, and 8
Script to make the 4 units and 3 point clouds.
import bpy
import bmesh
from mathutils import Vector
context = bpy.context
scene = context.scene
levels = 12
sublevels = 4
def new_coll(name, parent=None, obs=()):
c = bpy.data.collections.new(name)
if parent:
parent.children.link(c)
for o in obs:
c.objects.link(o)
o.hide_set(True)
return c
coords = [Vector(c) for c in (
(0, 0, 0),
(1, 0, 0),
(.5, 3**.5 / 2, 0),
(.5, 1 / 3 * 3**.5 / 2, ((3**.5 / 2)**2 - (1 / 3 * 3**.5 / 2)**2)**.5))]
o = sum(coords, Vector()) / 4
def pyramid(me, levels, origin=o, scale=(1, 1, 1)):
bm = bmesh.new()
bm.from_mesh(me)
for l in range(1, levels + 1):
lvrts = len(me.vertices)
for vec in coords[1:]:
bm.from_mesh(me)
bmesh.ops.translate(bm,
vec=2 ** (l - 1) * vec,
verts=bm.verts[-lvrts:],
)
bm.to_mesh(me)
# me.update()
# to change origin to centroid.
x = sum([v.co for v in bm.verts], Vector()) / len(bm.verts)
bmesh.ops.translate(bm,
vec=-x,
verts=bm.verts,
)
bmesh.ops.scale(bm,
vec=scale,
verts=bm.verts)
bm.to_mesh(me)
bm.free()
return me
bm = bmesh.new()
# make a tetra
bmesh.ops.convex_hull(bm,
input=[bm.verts.new(c - o) for c in coords]
)
me = bpy.data.meshes.new("Tetra")
bm.to_mesh(me)
ob = bpy.data.objects.new("0Tetra", me)
trap_coll = new_coll("Trapinski", scene.collection)
units = [ob]
for level in range(1, sublevels):
units.append(bpy.data.objects.new(f"{level}Pyramid",
pyramid(me.copy(), level, origin=o / 4, scale=(2 ** -(level), ) * 3)))
unit_coll = new_coll("Units", trap_coll, units)
bm.clear()
frames = []
for level in range(0, levels, sublevels): # levels):
mef = bpy.data.meshes.new(f"Frame{level}")
bm.verts.new(o)
bm.to_mesh(mef)
pyramid(mef, level)
ob = bpy.data.objects.new(f"Frame{level}", mef)
ob.scale = (2 ** -level,) * 3
frames.append(ob)
ob.instance_type = 'VERTS'
frame_coll = new_coll("Frames", trap_coll, frames)
Animating
Using a method similar to @TLousky with a frame change handler. continuing from script above
Swaps pointcloud every 40 frames and unit parented to that pointcloud ever 10 frames.
def show(units, frames):
def handler(scene):
f = scene.frame_current
uidx = (f % 40) // 10
idx = (f // 40) % len(frames.objects)
#uidx = 0
#uidx = scene.get("prop", 0)
unit = units.objects[uidx]
for frame in frames.objects:
frame.hide_set(True)
frame = frames.objects[idx]
frame.hide_set(False)
for u in units.objects:
u.scale = (1, 1, 1)
u.parent = None
unit.parent = frame
frame.hide_set(False)
return handler
bpy.app.handlers.frame_change_pre.clear()
bpy.app.handlers.frame_change_pre.append(show(unit_coll, frame_coll))
Particle System
Another way of instancing is via a particle system.
Here I've added a simple particle system to single vertex frame 0 and shown result using each of 4 units as particle object
Can stack a number of particle systems per pointcloud objects, staggering a start and end frame and life time.
Using particles to grow the tree.
Growing the pyramid like a tree
AFAIC this is a tree structure. It doesn't radiate out from its centre, rather it grows from the selected base.
Stepping through the first few frames. The top vertex is vertex group "level0" and particle system "level0" of the point cloud, the tetra particle grows (particle scale 0 to 1) in 5 frames. (given a long life time or they start vanishing (cool effect actually), whereapon the next level is used, and so on
Here I've made one tetra, and one point cloud. Each level of the cloud is assigned a vertex group, and for each vertex group used as density for a staggered frame start particle system. The particle is keyframed to scale from 0 to 1 over tpf
frames. For a level
levels point cloud.
import bpy
import bmesh
from mathutils import Vector
context = bpy.context
scene = context.scene
def new_coll(name, parent=None, obs=()):
c = bpy.data.collections.new(name)
if parent:
parent.children.link(c)
for o in obs:
c.objects.link(o)
#o.hide_set(True)
return c
coords = [Vector(c) for c in (
(0, 0, 0),
(1, 0, 0),
(.5, 3**.5 / 2, 0),
(.5, 1 / 3 * 3**.5 / 2, ((3**.5 / 2)**2 - (1 / 3 * 3**.5 / 2)**2)**.5))]
o = coords[3]
def pyramid(me, levels, origin=None, scale=(1, 1, 1)):
bm = bmesh.new()
bm.from_mesh(me)
for l in range(1, levels + 1):
lvrts = len(me.vertices)
for vec in coords[1:]:
bm.from_mesh(me)
bmesh.ops.translate(bm,
vec=2 ** (l - 1) * vec,
verts=bm.verts[-lvrts:],
)
bm.to_mesh(me)
# me.update()
# to change origin to centroid.
if origin is not None:
x = Vector(o)
else:
x = sum([v.co for v in bm.verts], Vector()) / len(bm.verts)
bmesh.ops.translate(bm,
vec=-x,
verts=bm.verts,
)
bmesh.ops.scale(bm,
vec=scale,
verts=bm.verts)
bm.to_mesh(me)
bm.free()
return me
bm = bmesh.new()
# make a tetra
bmesh.ops.convex_hull(bm,
input=[bm.verts.new(c - o) for c in coords]
)
me = bpy.data.meshes.new("Tetra")
bm.to_mesh(me)
ob = bpy.data.objects.new("0Tetra", me)
# parameters ##########################
fpt = 5
level = 6
#######################################
trap_coll = new_coll("Trapinski", scene.collection)
units = [ob]
unit_coll = new_coll("Units", trap_coll, units)
bm.clear()
frames = []
ob.hide_set(True)
mef = bpy.data.meshes.new(f"Frame{level}")
bm.verts.new()
bm.to_mesh(mef)
pyramid(mef, level)
f = bpy.data.objects.new(f"Frame{level}", mef)
frames.append(f)
bm.clear()
bm.from_mesh(mef)
verts = sorted(bm.verts, key=lambda v: v.co.z)
lev = 0
while verts:
v = verts.pop()
print(v.co)
vlevel = [v.index]
while verts:
if abs(v.co.z - verts[-1].co.z) < 1e-6:
vlevel.append(verts.pop().index)
else:
break
vg = f.vertex_groups.new(name = f"level{0}")
vg.add(vlevel, 1, 'REPLACE')
ps = f.modifiers.new(f"level{lev}", 'PARTICLE_SYSTEM')
psys = ps.particle_system
psys.settings.count = len(vlevel)
psys.settings.frame_start = lev * fpt + 1
psys.settings.frame_end = lev * fpt + 1
psys.settings.lifetime = 1000
psys.settings.render_type = 'OBJECT'
psys.settings.instance_object = ob
psys.settings.emit_from = 'VERT'
psys.settings.use_emit_random = False
psys.settings.physics_type = 'NO'
psys.settings.particle_size = 0
psys.settings.keyframe_insert("particle_size", frame=lev * fpt + 1)
psys.settings.particle_size = 1
psys.settings.keyframe_insert("particle_size", frame=lev * fpt + fpt + 1)
psys.settings.use_global_instance = True
psys.settings.use_rotation_instance = True
psys.settings.use_scale_instance = True
psys.settings.rotation_mode = 'NONE'
psys.vertex_group_density = vg.name
lev += 1
frame_coll = new_coll("Frames", trap_coll, frames)
Note could use a certain level staggered to be the next level, ie use the animation created again offset to the bottom three corners.
Collection Instance
Lastly, can make a collection instance from the duplivert frame / unit pair.
The collection instance of frame4 dupliverting simple tetra
This instance can be used as a particle.
Scaling
The code above is set up to have the units unit size, and the frames scaled such that the whole is unit size. The pyramid will double in size for each level, and gets large very quickly. It is not splitting edges.
Adding a parent empty to double unit in size on every level change. Looking from above in viewport, till it gets so large we are inside it looking thru the bottom gap, at which time the viewport is rolled around x
Summing Up.
There are a number of techniques shown here to make animations going from base tetra to extremely high level pyramids, only using one tetra, or a few simple levels of faced meshes.