Skip to content

Instantly share code, notes, and snippets.

@ylegall
Created March 10, 2021 22:26
Show Gist options
  • Save ylegall/c03aea2a4d76b349a04ac33fb21a00a3 to your computer and use it in GitHub Desktop.
Save ylegall/c03aea2a4d76b349a04ac33fb21a00a3 to your computer and use it in GitHub Desktop.
import bpy
import bmesh
import random
from mathutils import Vector, noise, Matrix
from math import sin, cos, tau, pi, radians, sqrt
from utils.interpolation import *
from utils.math import *
from utils.color import *
frame_start = 1
total_frames = 120
bpy.context.scene.render.fps = 30
bpy.context.scene.frame_start = frame_start
bpy.context.scene.frame_end = total_frames
random.seed(1)
initial_size = 9.0
max_depth = 4
cubes_per_block = int(4**(max_depth/2))
obj_index = 0
# colors = [0xe4e3db, 0x58a4b0, 0x2b303a, 0xd64933]
colors = [0xe4e3db, 0x588ab0, 0x2c2b3b, 0xEE6723]
material_assignments = [random.randint(0, 2 * len(colors) - 1) for _ in range(cubes_per_block * 8)]
sqrt2 = sqrt(2.0)
class Bounds:
def __init__(self, x0, x1, y0, y1):
self.x0 = x0
self.x1 = x1
self.y0 = y0
self.y1 = y1
def center(self) -> Vector:
center_x = (self.x0 + self.x1) / 2.0
center_y = (self.y0 + self.y1) / 2.0
return Vector((center_x, center_y, 0.0))
def __repr__(self):
return f'Bounds({self.x0},{self.x1},{self.y0},{self.y1})'
def make_materials():
for i in range(len(colors) * 2):
mat = bpy.data.materials.get(f'mat-{i}') or bpy.data.materials.new(f'mat-{i}')
mat.use_nodes = True
node = mat.node_tree.nodes['Principled BSDF']
type = i // len(colors)
color = hex_to_rgb(colors[i % len(colors)])
node.inputs[0].default_value = color # color
node.inputs[5].default_value = 0.6 # specular
node.inputs[15].default_value = 1.0 if type == 2 else 0.0 # transmission
node.inputs[4].default_value = 1.0 if type == 1 else 0.0 # metallic
if type == 0:
node.inputs[7].default_value = 0.35 # roughness
elif type == 1:
node.inputs[7].default_value = 0.20 # roughness
elif type == 2:
node.inputs[7].default_value = 0.15 # roughness
def setup():
col = bpy.data.collections.get('generated')
if not col:
col = bpy.data.collections.new('generated')
bpy.context.scene.collection.children.link(col)
block_parent = bpy.data.objects.get('block-parent')
if not block_parent:
block_parent = bpy.data.objects.new('block-parent', None)
col.objects.link(block_parent)
small_array_empty = bpy.data.objects.get('small_array_empty')
if not small_array_empty:
small_array_empty = bpy.data.objects.new('small_array_empty', None)
col.objects.link(small_array_empty)
small_array_empty.scale.x = 1.0 / 3.0
small_array_empty.scale.y = 1.0 / 3.0
main_obj = bpy.data.objects.get('main-object')
if not main_obj:
main_obj = bpy.data.objects.new('main-object', bpy.data.meshes.new('main-mesh'))
col.objects.link(main_obj)
small_array = main_obj.modifiers.new('small-array', type='ARRAY')
small_array.use_relative_offset = False
small_array.use_object_offset = True
small_array.offset_object = small_array_empty
small_array.count = 7
bevel = main_obj.modifiers.new('bevel', type='BEVEL')
bevel.segments = 3
bevel.width = 2.0
bevel.offset_type = 'PERCENT'
bevel.angle_limit = radians(30.0)
make_materials()
def noise_value(t: float, seed: int, radius: float = 0.23) -> float:
offset = polar(tau * t, radius) + Vector((seed * sqrt2, seed * pi, 0.0))
return noise.noise(offset, noise_basis='BLENDER')
def replace_mesh(obj, new_mesh):
old_mesh = obj.data
obj.data = new_mesh
if old_mesh:
bpy.data.meshes.remove(old_mesh, do_unlink=True)
def make_cube(
t: float,
bounds: Bounds,
offset: Vector,
bm: bmesh.types.BMesh
):
global obj_index
height = noise_value(t, obj_index, radius=0.09)
height = remap(height, -1.0, 1.0, 0.1, 2.3)
scale = (
Matrix.Scale(bounds.x1 - bounds.x0, 4, Vector((1.0, 0.0, 0.0))) @
Matrix.Scale(bounds.y1 - bounds.y0, 4, Vector((0.0, 1.0, 0.0))) @
Matrix.Scale(height, 4, Vector((0.0, 0.0, 1.0)))
)
tile_translation = Matrix.Translation(offset)
cube_translation = Matrix.Translation(bounds.center())
lift_translation = Matrix.Translation(Vector((0.0, 0.0, 0.5)))
matrix = cube_translation @ tile_translation @ scale @ lift_translation
bm2 = bmesh.new()
new_mesh = bpy.data.meshes.new('cube')
bmesh.ops.create_cube(bm2, size=1.0, matrix=matrix)
for face in bm2.faces:
face.material_index = material_assignments[obj_index]
bm2.to_mesh(new_mesh)
bm.from_mesh(new_mesh)
bpy.data.meshes.remove(new_mesh, do_unlink=True)
bm2.free()
obj_index += 1
def make_block_recursive(
t: float,
bounds: Bounds,
offset: Vector,
level: int,
split_seed: int,
bm: bmesh.types.BMesh
):
if level >= max_depth:
make_cube(t, bounds, offset, bm)
return
pct = noise_value(t, split_seed, radius=0.15)
pct = remap(pct, -1.0, 1.0, 0.15, 0.85)
is_x_split = level % 2 == 0
split = mix(bounds.x0, bounds.x1, pct) if is_x_split else mix(bounds.y0, bounds.y1, pct)
new_low_bounds = Bounds(bounds.x0, split, bounds.y0, bounds.y1) if is_x_split else \
Bounds(bounds.x0, bounds.x1, bounds.y0, split)
new_high_bounds = Bounds(split, bounds.x1, bounds.y0, bounds.y1) if is_x_split else \
Bounds(bounds.x0, bounds.x1, split, bounds.y1)
make_block_recursive(t, new_low_bounds, offset, level + 1, 2 * split_seed + 1, bm)
make_block_recursive(t, new_high_bounds, offset, level + 1, 2 * split_seed + 2, bm)
def make_block(
t: float,
tile_index: int,
bm: bmesh.types.BMesh
):
offset_x = initial_size * (-1 + (tile_index % 3))
offset_y = initial_size * (-1 + (tile_index // 3))
offset = Vector((offset_x, offset_y, 0.0))
bounds = Bounds(
-initial_size / 2.0,
initial_size / 2.0,
-initial_size / 2.0,
initial_size / 2.0,
)
make_block_recursive(t, bounds, offset, 0, 0, bm)
def make_geometry(t: float):
global obj_index
obj_index = 0
bm = bmesh.new()
for i in range(9):
if i != 4:
make_block(t, i, bm)
main_obj = bpy.data.objects.get('main-object')
new_mesh = bpy.data.meshes.new('new_mesh')
for i in range(len(colors) * 2):
new_mesh.materials.append(bpy.data.materials[f'mat-{i}'])
bm.to_mesh(new_mesh)
replace_mesh(main_obj, new_mesh)
bm.free()
def frame_update(scene):
frame = scene.frame_current
t = (frame - 1) / float(total_frames)
make_geometry(t)
main_obj = bpy.data.objects['main-object']
small_array_empty = bpy.data.objects['small_array_empty']
scale_value = 3.0**t
main_obj.scale.x = scale_value
main_obj.scale.y = scale_value
small_array_empty.scale.x = scale_value / 3.0
small_array_empty.scale.y = scale_value / 3.0
setup()
bpy.app.handlers.frame_change_pre.clear()
bpy.app.handlers.frame_change_pre.append(frame_update)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment