6
$\begingroup$

enter image description here

Animation nodes method has been provided by @lemon, Vote his answer UP UP UP!

sTRIKE1 This is it, figured out via "delta transform", manually. Though it would look better if elements never separated

STRIKE2 NOT QUITE, DAMN IT.

2 A more simplistic version, done manually, as I've not yet figured out how to do the desired version manually or in script.

Mesh. I want to create it not by dissection of edges, but instead by splitting 1 into 4, or should I say, adding 3 more to the original location before moving all 4 away from Local 0,0,0 , and then doing the same with the 4 replicas, as each of them is splitting the same way (the subsequent movements of replicas becoming more and more complex/derived), so that (if animated) it really looks like the one is splitting into 4, and each of the 4 results are then splitting into 4 (and so on).
If Empties were placed in each start/end position of each object it would, looking at the Empties hierarchically connected, appear like a branching tree, with old branches continuously lengthening.

Though I wouldn't mind knowing the opposite too. Having 1 edges dissected(halved) and created vertexes connected, but always so that connection doesn't happen toward the inside (coordinate 0,0,0), THOUGH THAT WOULDN'T really be the opposite of the previous, the real opposite would look like the 1 is scaling in (previously mentioned) 4 directions (toward the vertexes of the 1), and so on, in each of the 4 newly created. Yeah XD definitely wouldn't mind knowing how to do this.

Method using python script or nodes would be just FINE!

IRL construction

BTW I have a hunch it would be easier to construct it if there was some alignment with the coordinate system, such as edge midpoints aligning on the axes.

enter image description here

$\endgroup$
  • 6
    $\begingroup$ en.wikipedia.org/wiki/… $\endgroup$ – batFINGER Jun 19 at 13:36
  • $\begingroup$ Nice to remind me to check wikipedia for terminology, but there's no blender instructions. Since I'm getting into blender and python simultaneously, you can see how this is good practice matter. $\endgroup$ – t8ja Jun 19 at 14:02
  • 1
    $\begingroup$ Yes agree, I like these kind of challenges in blender. Can bmesh script the example as shown in mathematica code in wiki, is that what you are after? Also would improve (IMO) the question if you add a picture. Not everyone will go off to yt to watch a vid. $\endgroup$ – batFINGER Jun 19 at 14:08
  • $\begingroup$ I'm not sure XD literally started python yesterday, but I'll definitely look into it. Opened the question since Tetrix doesn't come up anywhere, and input cant hurt, as you've proven. $\endgroup$ – t8ja Jun 19 at 14:16
  • 3
    $\begingroup$ For more general fractals: github.com/buddhi1980/mandelbulber2 $\endgroup$ – miceterminator Jun 20 at 10:42
5
+250
$\begingroup$

Animation nodes version

One tetrahedron is used as model.

Another one is used as duplivert. Its parent's vertices will be constructed by the nodes.

An empty is used to control the iterations. And this is triggered when the empty is scaled.

enter image description here

The nodes setup is composed of 4 parts.

  • A main: get the input and produce the resulting vertices into the dupli parent
  • A first sub part: calculate the scale factors and iterate to construct each level
  • A second sub part: iterates on the model to produce each vertex
  • And a last sub part: duplicate each vertex of the model and place it

The scales are calculated by power of two: 0, 1, 2, 4, etc. For each level, depending on the scale of the empty, we have a scale that is proportional to the intervals [0,1], [1,2], [2, 4], etc. The final unit is the base tetrahedron obtained from regular solid addon.

Main

enter image description here

This main part calculates the amount of iteration and the relative scale. For instance, if the empty is scale by 3.20, we have 3 iterations and a position of 0.2 for the scale.

First sub part

enter image description here

From that the iteration is done from a base vertex input which is reassigned to itself after each iteration.

The calculation is to determinate the scale level ([0,1], [1,2], [2, 4], etc.) and adjust it from the scale position (the 0.2 above).

Second and third sub parts

enter image description here

Simply iterates on the model for each cumulated vertex.

And in the last sub part, place the vertex scaled and offset from the model.

enter image description here

When the empty is scaled up to ten, that will generate more than 5 millions vertices. So... be careful.

enter image description here

Python version

Uses the same principles as the AN version above.

I've added the possibility to use other shapes.

So we can have:

enter image description here

Or:

enter image description here

The principle is to use:

  • An empty to scale the figure (scale the empty to scale it)
  • A model object which gives the base vertices
  • A dupli object which gives the shape of the inner elements
  • A target object (dupli parent of the dupli) for which the code calculates the vertices

The script itself is in the following parts:

  • Setup a hook function on bpy.app.handlers.scene_update_pre so that it can be animated
  • Get the key objects (hard coded) to apply the generative code
  • The generation itself
  • A function to clean the target mesh
  • A function to calculate the wanted scales depending of the depth of the figure

Scene handler

Remove previous ones and setup/update the hook

def setup_handler():
    for h in [x for x in bpy.app.handlers.scene_update_pre]:
        if h == update_sierpinsky:
            bpy.app.handlers.scene_update_pre.remove( update_sierpinsky )

    bpy.app.handlers.scene_update_pre.append( update_sierpinsky )

Get key objects

Here if you change (in the given .blend below) model1/dupli1 by model2/dupli2 you'll have cubes instead of Sierpinski.

def update_sierpinsky( scene ):
    scale_object = scene.objects['scale']
    model_object = scene.objects['model1']
    dupli_object = scene.objects['dupli1']
    target_object = scene.objects['target']

    sierpinsky( scale_object, model_object, dupli_object, target_object )

Scales calculations

Calculate 0, 1, 2, 4, etc. plus a value between 0 and 1 to make the transitions. More precisely, this is 0, 1, sqrt of the model vertex count as the scales are not the same depending on the figure (but not sure of this formula...).

def scales( scale_object, scale_factor ):
    scale = scale_object.scale.z - 1
    scale = max( scale, 0 )
    scale_floor = int( scale )
    scale_delta = scale - scale_floor
    iterations = scale_floor + 1

    def calc( sd, i, sf ):
        i2 = sf ** i
        prev = i - 1
        prev2 = sf ** prev if i != 0 else 0
        return prev2 + sd * (i2 - prev2)

    return [calc( scale_delta, i, scale_factor ) for i in reversed( range( iterations ) )]

Vertices generation

I've choose an iterative one (for a variant).

It loops overs the scales and generates the vertices locations level by level.

def sierpinsky( scale_object, model_object, dupli_object, target_object ):

    model_vertices = [Vector( v.co ) for v in model_object.data.vertices]
    result_vertices = [Vector()]
    scale_factor = len(model_vertices) ** 0.5

    for scale in scales( scale_object, scale_factor ):
        scaled_vertices = [scale * v for v in model_vertices]
        result_vertices = [r + s for r in result_vertices for s in scaled_vertices]

    clear_mesh( target_object )
    target_object.data.from_pydata( result_vertices, [], [] )
    target_object.data.update()

Target mesh clean up

The vertices of the target mesh are removed before each calculation.

There may be a better way to do that but don't know how.

def clear_mesh( obj ):
    bm = bmesh.new()
    bm.from_mesh( obj.data )
    bm.verts.ensure_lookup_table()
    for v in [x for x in bm.verts]: # use a copy before removing
        bm.verts.remove(v)
    bm.to_mesh( obj.data )
    obj.data.update()

The blend file for the Python version:

$\endgroup$
  • $\begingroup$ I'd like to mark this as a correct answer but I'd still like to get the Python version, and I don't know if the incentive will still be there if the question gets answered. Vote this one UP PEOPLE $\endgroup$ – t8ja Jul 10 at 0:38
  • $\begingroup$ @t8ja, answer edited for the Python version + a little extra ;) $\endgroup$ – lemon Jul 10 at 11:22
  • $\begingroup$ How would I go about rotating the whole composit object/animation ? Translate works, but rotation messes it up by moving every individual element. $\endgroup$ – t8ja Jul 10 at 11:34
  • $\begingroup$ @t8ja, simply rotate the 'target' object (it is at the center of the scene). The rest is depending on the relative positions when dupli and target are parented. So you may need to reparent the dupli to the target having them at the same position (or simply move the dupli at the position of the 'target'). $\endgroup$ – lemon Jul 10 at 11:37
  • $\begingroup$ I see now, I didn't consider the resultVertices to be an object I could move/rotate. Man, I feel you yould answer every question I have, after seeing this LOL $\endgroup$ – t8ja Jul 10 at 11:46
11
$\begingroup$

Updated answer with animation technique (sort'a cheating)

So to generate an animation of the tetrix levels increasing I kind'a cheated. Since my previous answer already used a dictionary to store all the levels, I simply wrote that data structure into a json file, then used an app handler function to read it prior to every frame change and load the current level matching the frame number.

enter image description here

So this has solution has two steps:

Generate data file

import numpy as np
from time import time
import bpy, bmesh, json
from mathutils import Vector

def generate_quad( original, quads, level, max_level ):
    ''' Recursive function generating 4 tetrahedra off of the the input tetrahedron '''

    # The 1st new tetrahedron is simply half the scale of the original starting from the original tet's bottom left corner
    origin = original.min(axis=0) / 2    
    half  = original / 2
    half += origin

    # Calculate the dimensions of the new tetrahedra
    newdims = Vector( half.max(axis=0) - half.min(axis=0) )

    # The 2nd moves by the new tet's x dimension to the right
    right = half.copy()
    right[:,0] += newdims.x

    # The 3rd moves by half the x dim and all the y dim
    forward = half.copy()
    forward[:,0] += newdims.x / 2
    forward[:,1] += newdims.y

    # The 4th moves by half the x, a third of the y and all the z
    up = half.copy()
    up[:,0] += newdims.x / 2
    up[:,1] += newdims.y / 3
    up[:,2] += newdims.z

    quad = [ half, right, forward, up ]
    quads[ level ].extend( quad )

    if level < max_level:
        for q in quad:
            quads = generate_quad( q, quads, level + 1, max_level )

    return quads

## Main Code
start = time()
n     = 5  # Number of steps of the fractal
scale = 10 # Scale of the original tetrahedron

# Generate regular tetrahedron at the desired scale
orig = np.array([
    [0,   0, 0],
    [1,   0, 0],
    [0.5, 3**0.5/2, 0],
    [0.5, 1/3*3**0.5/2, ((3**0.5/2)**2 - (1/3*3**0.5/2)**2)**0.5]
]) * scale

# Initialize data structure for all tetrahedra in all steps
quads = { i : [] for i in range( n+1 ) }

# Generate tetrix via recursive function
quads = generate_quad( orig, quads, 0, n )

# From numpy to simple lists to prepare to save as json
quads = { i : np.array(quads[i]).tolist() for i in quads }

quad_cache_file_path = r'C:\Users\username\Documents\tetrix.json'
with open( quad_cache_file_path, 'w' ) as fh:
    quads = json.dump( quads, fh )

print( f'Generated {n} tetrix levels in {round(time() - start,1)} seconds' )

Add frame_change_pre app handler function to link between the frame number and the tetrix's level

import itertools
import numpy as np
import bpy, bmesh, json
from mathutils import Vector

def update_tetrix( scene ):
    C = bpy.context
    D = bpy.data
    S = C.scene

    regular_tet_faces = np.array([[3, 0, 1], [1, 0, 2], [0, 3, 2], [3, 1, 2]])

    quad_cache_file_path = r'C:\Users\username\Documents\tetrix.json'
    with open( quad_cache_file_path ) as fh:
        quads = json.load( fh )

    # Select level to display at current frame (match 
    n = S.frame_current - 1
    n = n if str(n) in quads else max( quads.keys() )

    # Generate mesh data
    verts = []
    faces = regular_tet_faces.tolist()
    for q in quads[str(n)]:
        # Find last vert index and add new verts
        last_vert_index = max([ len( verts ) - 1, 0 ])
        verts.extend( q )

        # Use base face arrangement of original regular tetrahedron and offset indices
        faces_indices = regular_tet_faces + last_vert_index + 1
        faces.extend( faces_indices.tolist() )

    # Remove duplicates
    faces = sorted([ sorted(f) for f in faces])
    faces = list(f for f,_ in itertools.groupby(faces))

    if 'tetrix' in D.objects:
        print( f"Updating mesh with level {n}" )
        o = D.objects['tetrix']
        m = o.data

        bm = bmesh.new()    

        bmverts = [ bm.verts.new(co) for co in verts ]
        bmfaces = [ bm.faces.new([ bmverts[i] for i in f]) for f in faces ]

        bm.to_mesh( m )    
        m.update()       

    else:
        # Generate mesh data and initialize vertex and face arrays
        m = D.meshes.new('tetrix')

        # Generate mesh object and add to scene
        m.from_pydata( verts, [], faces )
        print({ k : len( quads[k] ) for k in quads })
        o = D.objects.new('tetrix',m)
        S.collection.objects.link( o )

bpy.app.handlers.frame_change_pre.append(update_tetrix)

Caveats

  1. This solution doesn't enable animating a transition between levels, simply a harsh switch. You also don't get a lot of control over how long the pauses are between switches (though this could be done in post in any video editing software by multiplying or delaying frames).

  2. Currently the entire data file is read prior to every frame change. This can be pretty slow if you've generate a big file with many levels (above 7), depending on your computer's specs. It could probably be improved by loading the file once to memory as a global scene object then reading directly from that prior to every frame change, rather than reading the file from disk every time.

Original Answer

enter image description here

This script can generate a n-level tetrix. It will work only in Blender 2.8 and utilizes some Numpy tricks for more efficient numerical manipulation.

import bpy, bmesh
import numpy as np
from mathutils import Vector

def generate_quad( original, quads, level, max_level ):
    ''' Recursive function generating 4 tetrahedra off of the the input tetrahedron '''

    # The 1st new tetrahedron is simply half the scale of the original starting from the original tet's bottom left corner
    origin = original.min(axis=0) / 2    
    half  = original / 2
    half += origin

    # Calculate the dimensions of the new tetrahedra
    newdims = Vector( half.max(axis=0) - half.min(axis=0) )

    # The 2nd moves by the new tet's x dimension to the right
    right = half.copy()
    right[:,0] += newdims.x

    # The 3rd moves by half the x dim and all the y dim
    forward = half.copy()
    forward[:,0] += newdims.x / 2
    forward[:,1] += newdims.y

    # The 4th moves by half the x, a third of the y and all the z
    up = half.copy()
    up[:,0] += newdims.x / 2
    up[:,1] += newdims.y / 3
    up[:,2] += newdims.z

    quad = [ half, right, forward, up ]
    quads[ level ].extend( quad )

    if level < max_level:
        for q in quad:
            quads = generate_quad( q, quads, level + 1, max_level )

    return quads

## Main Code
C = bpy.context
D = bpy.data
S = C.scene

n     = 2  # Number of steps of the fractal
scale = 10 # Scale of the original tetrahedron

# Generate regular tetrahedron at the desired scale
orig = np.array([
    [0,   0, 0],
    [1,   0, 0],
    [0.5, 3**0.5/2, 0],
    [0.5, 1/3*3**0.5/2, ((3**0.5/2)**2 - (1/3*3**0.5/2)**2)**0.5]
]) * scale

regular_tet_faces = np.array([[3, 0, 1], [1, 0, 2], [0, 3, 2], [3, 1, 2]])

# Initialize data structure for all tetrahedra in all steps
quads = { i : [] for i in range( n+1 ) }

# Generate tetrix via recursive function
quads = generate_quad( orig, quads, 0, n )

# Generate mesh data and initialize vertex and face arrays
m     = D.meshes.new('tetrix')
verts = []
faces = regular_tet_faces.tolist()

# Generate mesh data
for q in quads[n]:
    # Find last vert index and add new verts
    last_vert_index = max([ len( verts ) - 1, 0 ])
    verts.extend( q.tolist() )

    # Use base face arrangement of original regular tetrahedron and offset indices
    faces_indices = regular_tet_faces + last_vert_index + 1
    faces.extend( faces_indices.tolist() )

# Generate mesh object and add to scene
m.from_pydata( verts, [], faces )
print({ k : len( quads[k] ) for k in quads })
o = D.objects.new('base',m)
S.collection.objects.link( o )

Here's a higher level tetrix from all orthographic angles and a free one (quad view):

enter image description here

$\endgroup$
  • $\begingroup$ Sorry for not commenting earlier. Nice result, but the sequence of creation doesn't fit the description, judging from the animation. If you're up to it, you can update the answer putting an alternative on top. $\endgroup$ – t8ja Jun 25 at 0:49
  • $\begingroup$ My description somewhat resembles an orderly explosion. $\endgroup$ – t8ja Jun 25 at 1:28
  • $\begingroup$ I should have realized immediately that the easiest way is to simply move duplicates by using the vertice coordinates of the original (which is what I was saying before, only in a more convoluted way, since coincidence was escaping me). The trouble is, I'm noticing the vertex coordinates of the moved tetrahedrons, aren't exact, even though they should be. For example I get 2,000002,000002 $\endgroup$ – t8ja Jun 29 at 21:46
7
$\begingroup$

I consider TLousky's answer superior, especially for pedagogic reasons (expressiveness and documentation of the code).

A few minutes late but without numpy (also 2.8 only):

import bpy
from itertools import combinations
from mathutils import *

scale = .333 #scale of one tetrahedon
tetrons = 0  #index of current tetron
steps = 4    # recursion steps

vec =(
(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))
verts = Matrix(vec)
vertices = []
faces = []

def Tetron(vec=Vector([0,0,0]), ):
    #Adds tetrahedons to vertices and faces
    global tetrons
    base_vecs = verts*scale
    for base_vec in base_vecs:
        base_vec += vec*scale
        vertices.append(base_vec)
    faces_ = list(combinations(list(range(tetrons*4, tetrons*4+4)), r = 3))
    faces.extend(faces_)
    print("ind {}".format(tetrons))
    tetrons+=1

def SiPyramid(depth, vec=Vector([0,0,0])):
    #Recursive function to add Tetrahedons
    if depth < 0:
        return 0 
    if depth==0:
        Tetron(vec)
    else:
        for base_vec in verts:
            print(depth-1)
            print((2**(depth-1)*base_vec + vec ))
            SiPyramid(depth-1, 2**(depth-1)*base_vec + vec   )

SiPyramid(steps)

#Link data to blender objects
mesh_data = bpy.data.meshes.new("sierpinsky")
mesh_data.from_pydata(vertices, [], faces)
mesh_data.update()
obj = bpy.data.objects.new("Sierpinsky", mesh_data)
bpy.context.collection.objects.link(obj)

At 10 steps (might take a few seconds to generate):

enter image description here

enter image description here

$\endgroup$
  • 1
    $\begingroup$ You're too kind @miceterminator (though maybe not to mice? :-D). Great job reconstructing that Mathematica code in an elegant, efficient way. I suspect your code might run faster, it took me 120 seconds on my 13" Macbook pro to generate a 10 step tetrix. Did you run any benchmarks? $\endgroup$ – TLousky Jun 20 at 10:13
  • 2
    $\begingroup$ Speed counts for nothing without readability. My code runs a bit faster, but it is not really well documented. I spend too much time trying to get a cool animation to go with the answer :). $\endgroup$ – miceterminator Jun 20 at 10:24
  • $\begingroup$ Very nice results, but can you tell me do they relate to the methods I was describing, since my idea was to do the multiplication and repositioning in a way such that every step is animatable ? Are the original single tetra in your scripts oriented in the way I described in the end ? I'd like to confirm my hunch, that this approach would slim down the script. $\endgroup$ – t8ja Jun 20 at 12:15
  • 1
    $\begingroup$ I don't follow. There are many ways to animate the creation of a Tetrix and I am unable to deduce the way you want to do it from your question. The alignment is somewhat flexible as you can change that by rotating the original vertex coordinates defined in the "vec" variable. $\endgroup$ – miceterminator Jun 20 at 12:28
  • $\begingroup$ So far I've identified the "manipulator elements" I need to get the desired result. For the first case"multiplication case", the sequence goes as follows: first there is one, one multiplies to 4, the 4 go in the directions determined by the vertices (Ctrl+Alt+Space to get a custom orientation from the 4 vertices). $\endgroup$ – t8ja Jun 20 at 12:53
7
$\begingroup$

Bmesh version.

Pyramid

enter image description here
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.

enter image description here 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

enter image description here

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.

enter image description here 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.

enter image description here
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.

enter image description here 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.

enter image description here 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.

enter image description here 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.

$\endgroup$
  • $\begingroup$ Try to do it as I described in reply to miceterminator, and then animate it, and if you also tell me how to position the tetrahedron the way I want, that's a bonus, because it's really bugging me. $\endgroup$ – t8ja Jun 20 at 14:41
  • $\begingroup$ Edited such that origin in centroid of tetra verts. $\endgroup$ – batFINGER Jun 20 at 15:03
  • $\begingroup$ I can't tell if this is doing what I described LOL you'd have to animate it. $\endgroup$ – t8ja Jun 20 at 16:28
  • $\begingroup$ Crazy compact and cool solution! $\endgroup$ – TLousky Jun 20 at 17:38
  • 1
    $\begingroup$ Cheers TLousky, great group of answers. @t8ja possible this could be two questions, creating and animating. $\endgroup$ – batFINGER Jun 24 at 10:51
6
$\begingroup$

I've produced a Sierpinski Pyramid using Array modifiers. Two are needed per iteration.

First, create a tetrahedron. This can be done by creating a cube, deleting two opposite vertices from the top face and the other two opposite vertices from the bottom face, then reconnecting the remaining vertices. Then rotate it and align it so that one face is flat and the top vertex is the object's origin.

Next create an empty at the origin, and rotate it 120 degrees. Call this RotEmpty.

Finally create an empty at one of the bottom vertices. Call this LocEmpty1.

Apply two Array modifiers to the object. The first should use LocEmpty1 for its offset and have a count of 2. The second should use RotEmpty and have a count of 3. Check the boxes to merge vertices - this will help reduce the geometry as it's not optimal...

To add another tier, place a new Empty at the bottom of the pyramid, duplicate both modifiers on the pyramid and set the Loc one to the new empty. Repeat as needed until you have the number of tiers you need.

The advantage to generating it this way is that you can animate it. I did this by parenting everything to the pyramid, then animating each Empty going from the origin to its "full" location in sequence, and moving the whole pyramid up to keep its center at the origin. Set the camera to zoom out over the whole thing, slap a sun light on there, and... ta-da!

Animated Sierpinski!

$\endgroup$
  • $\begingroup$ Nice method, and it visually reproduces the "opposite procedure" I described in the second paragraph, though only in the first iteration. General visual impression is GREAT though. Post another answer or make an addendum to this one if you can modify the procedure so that every individual newly created tetrahedron experiences the internal division, without overlapping the prior stage , as is happening here. The closest answer yet (or at least because I can actually see the steps because you cared to animate) $\endgroup$ – t8ja Jun 21 at 11:06
  • $\begingroup$ Did you use 2.8 ? Because the blend file appears empty, and also when I do the procedure manually in 2.79 it doesn't turn out right. $\endgroup$ – t8ja Jun 21 at 11:53
  • $\begingroup$ Yes, this was done with 2.8. Sorry, I should have specified that. I don't think it uses 2.8-specific features, so if you recreate it in 2.79 it should work. $\endgroup$ – Niet the Dark Absol Jun 21 at 14:33
  • $\begingroup$ You didn't specify the rotation axis of the RotEmpty, and I have a difficulty understanding how just stacking tetrahedrons (if that is what this array is doing) is giving this appearance of scaling (in the first stage). Your instructions are probably incomplete. $\endgroup$ – t8ja Jun 21 at 17:54
  • 1
    $\begingroup$ I've re-created the first level in 2.79: Download - Hope that helps. $\endgroup$ – Niet the Dark Absol Jun 21 at 18:57
0
$\begingroup$

This is a non-script approach. You can create this fractal adding a tetrahedron with Extra Objects add-on. Then subdivide it once, remove middle triangles and make inner faces. Repeat the subdivide once and remove and make faces steps a few times. Scale the fractal down and then animate scaling it up to show the structure. enter image description here

$\endgroup$
  • $\begingroup$ Read the comments. $\endgroup$ – t8ja Jun 30 at 13:14

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.