Created
March 10, 2021 22:26
-
-
Save ylegall/c03aea2a4d76b349a04ac33fb21a00a3 to your computer and use it in GitHub Desktop.
blender python script for https://www.instagram.com/p/CMK5uxKH9qY/?utm_source=ig_web_copy_link
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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