Skip to content

Instantly share code, notes, and snippets.

@ylegall
Last active April 8, 2021 17:58
Show Gist options
  • Save ylegall/2cdb8c49841a5b1bfee3bead153488c4 to your computer and use it in GitHub Desktop.
Save ylegall/2cdb8c49841a5b1bfee3bead153488c4 to your computer and use it in GitHub Desktop.
blocks transformed by circle inversion
import bpy
import bmesh
import random
from easing_functions import CubicEaseInOut, QuadEaseInOut
from mathutils import Vector, noise, Matrix
from math import sin, cos, tau, pi, sqrt, radians
from utils.interpolation import *
from utils.math import *
from utils.color import *
frame_start = 1
total_frames = 240
bpy.context.scene.render.fps = 30
bpy.context.scene.frame_start = frame_start
bpy.context.scene.frame_end = total_frames
random.seed(2)
size = 7.0
circle_radius = 1.0
post_scale = 2.0
max_subdivision_levels = 10
# colors = [0xbebbbb, 0x444054, 0x2f243a, 0xfac9b8, 0xdb8a74]
# colors = [0x0c0114, 0xEAECDF, 0xE9693F, 0x2fbae0, 0x3c6997]
colors = [0x292f36, 0xEAECDF, 0xE46338, 0x2fbae0, 0x3c6997]
material_assignments = [random.randint(0, len(colors)-1) for _ in range(31)]
block_idx = 0
def make_materials():
main_obj = bpy.data.objects['main']
materials = main_obj.data.materials
materials.clear()
for i in range(len(colors)):
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']
color = hex_to_rgb(colors[i])
node.inputs[0].default_value = color # color
node.inputs[17].default_value = color # emission color
node.inputs[5].default_value = 0.40 # specular
node.inputs[4].default_value = 1.0 if i == 4 else 0.0 # metallic
node.inputs[7].default_value = 0.1 if i == 4 else 0.27 # roughness
node.inputs[18].default_value = 0.0 # emission
materials.append(mat)
def setup():
col = bpy.data.collections.get('generated')
if not col:
col = bpy.data.collections.new('generated')
bpy.context.scene.collection.children.link(col)
obj = bpy.data.objects.get('main')
if not obj:
obj = bpy.data.objects.new('main', bpy.data.meshes.new('main'))
col.objects.link(obj)
original = bpy.data.objects['original']
original.hide_viewport = True
original.hide_render = True
make_materials()
bevel = obj.modifiers.get('bevel') or obj.modifiers.new('bevel', type='BEVEL')
bevel.segments = 2
bevel.width = 3.0
bevel.offset_type = 'PERCENT'
bevel.angle_limit = radians(40.0)
obj.data.use_auto_smooth = True
obj.data.auto_smooth_angle = radians(20)
def reflect(point: Vector, radius: float) -> Vector:
dist = max(point.length, 0.0001)
new_dist = radius / dist
new_dist = new_dist if new_dist > 1 else linearstep(0.09, 1.0, new_dist)
# new_dist = QuadEaseInOut().ease(new_dist)
return point.normalized() * new_dist * radius
def noise_value(t: float, seed: int, radius: float = 0.23) -> float:
offset = polar(tau * t, radius) + Vector((seed * pi, 0.0, 0.0))
return noise.noise(offset, noise_basis='BLENDER')
def noise_value_offset(t: float, offset: Vector, radius: float = 0.23) -> float:
offset = polar(tau * t, radius) + offset
return noise.noise(offset, noise_basis='BLENDER')
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 make_block(
t: float,
bounds: Bounds,
bm: bmesh.types.BMesh,
):
center = bounds.center()
if center.length <= 0.6:
return
mesh_copy = bpy.data.meshes['original'].copy()
translation = Matrix.Translation(bounds.center())
scale_x = 0.93 * (bounds.x1 - bounds.x0)
scale_y = 0.93 * (bounds.y1 - bounds.y0)
scale = scale_matrix(scale_x, scale_y, 1.0)
mesh_copy.transform(translation @ scale)
mesh_copy.polygons[0].material_index = material_assignments[block_idx % len(material_assignments)]
bm.from_mesh(mesh_copy)
bpy.data.meshes.remove(mesh_copy, do_unlink=True)
def subdivide(
t: float,
bounds: Bounds,
bm: bmesh.types.BMesh,
level: int = 0,
split_seed: int = 0,
):
global block_idx
if level >= max_subdivision_levels:
make_block(t, bounds, bm)
block_idx += 1
return
pct = noise_value(t, split_seed, radius=0.17)
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)
subdivide(t, new_low_bounds, bm, level + 1, 2 * split_seed + 1)
subdivide(t, new_high_bounds, bm, level + 1, 2 * split_seed + 2)
def frame_update(scene):
global block_idx
frame = scene.frame_current
t = frame / float(total_frames)
block_idx = 0
bm = bmesh.new()
subdivide(t, Bounds(-size, size, -size, size), bm)
# invert points
for vert in bm.verts:
vert.co = reflect(vert.co, circle_radius)
# dist = vert.co.length
# dist = remap(dist, 0.1, circle_radius, 0.0, 2.5 * circle_radius)
# vert.co = vert.co.normalized() * dist
vert.co *= post_scale
bmesh.ops.extrude_face_region(bm, geom=bm.faces)
for i, face in enumerate(bm.faces):
face.smooth = True
face.normal_update()
if face.normal.z > 0.9:
center = face.calc_center_median()
dist_factor = center.length
dist_factor = smoothstep(0.0, circle_radius + 1, dist_factor)
random_height = 0.6 + 0.3 * noise_value_offset(t, center, radius=0.47)
height = max(0.01, random_height * dist_factor)
for vert in face.verts:
vert.co.z = height
bm.to_mesh(bpy.data.meshes['main'])
bm.free()
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