Last active
April 24, 2021 17:45
-
-
Save ylegall/5c0feab1a335cdbc6a5b4e3097adcafe to your computer and use it in GitHub Desktop.
Blender Blobs
This file contains hidden or 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, kdtree | |
from math import sin, cos, tau, pi, sqrt, radians | |
frame_count = 0.0 | |
frame_start = 1 | |
total_frames = 100 | |
bpy.context.scene.render.fps = 30 | |
bpy.context.scene.frame_start = frame_start | |
bpy.context.scene.frame_end = total_frames | |
random.seed(1) | |
num_blobs = 3 | |
points_per_blob = 150 | |
blob_polygon = [i for i in range(points_per_blob)] | |
blob_spring_len = 0.01 | |
blob_spring_strength = 0.1 | |
blob_point_min_radius = 0.1 | |
blob_point_max_radius = 0.4 | |
blob_point_repel_radius = 0.7 | |
# linear interpolation between 2 values | |
def lerp(a: float, b: float, t: float) -> float: | |
return (1 - t) * a + t * b | |
# polar coordinates | |
def polar(angle: float, radius: float) -> Vector: | |
return Vector((radius * cos(angle), radius * sin(angle), 0.0)) | |
class Point: | |
def __init__(self, position: Vector, radius: float): | |
self.position = position | |
self.prev_position = position + (noise.random_vector().xy.to_3d() * 0.01) | |
self.radius = radius | |
self.damping = 0.8 | |
def set_radius(self, radius: float): | |
self.radius = radius | |
def add_force(self, force: Vector): | |
self.position += force | |
def attract_constant(self, target: Vector, strength: float): | |
diff = target - self.position | |
self.add_force(diff.normalized() * strength) | |
def repel(self, target: Vector, radius: float, strength: float): | |
diff = target - self.position | |
diff_mag = diff.magnitude | |
combined_radius = radius | |
if (diff_mag < combined_radius): | |
force_mag = combined_radius - diff_mag | |
force = diff.normalized() * force_mag | |
self.add_force(-force * strength) | |
def update(self, dt: float): | |
velocity = self.position - self.prev_position | |
self.prev_position = self.position | |
self.position = self.position + velocity * self.damping * dt | |
class Spring: | |
def __init__(self, point_a: Point, point_b: Point, length: float, strength: float): | |
self.point_a = point_a | |
self.point_b = point_b | |
self.strength = strength | |
self.length = length | |
def update(self, dt: float): | |
diff = self.point_b.position - self.point_a.position | |
diff_mag = diff.magnitude | |
force_mag = self.length - diff_mag | |
force = diff.normalized() * force_mag * self.strength * dt | |
self.point_a.add_force(-force) | |
self.point_b.add_force(force) | |
class Blob: | |
def __init__(self, center_position: Vector, radius: float, num_points: int): | |
self.points = [] | |
self.springs = [] | |
self.center = center_position | |
# Init points | |
for i in range(num_points): | |
angle = tau * i / num_points | |
position = center_position + polar(angle, radius) | |
# point_radius = 0.05 * (0.5 + 0.5 * noise.noise(position, noise_basis='BLENDER')) + 0.1 | |
point_radius = 0.5 | |
point = Point(position=position, radius=point_radius) | |
self.points.append(point) | |
# Init springs | |
for i, point in enumerate(self.points): | |
point_a = point | |
point_b = self.points[(i + 1) % num_points] | |
spring = Spring( | |
point_a=point_a, | |
point_b=point_b, | |
length=blob_spring_len, | |
strength=blob_spring_strength | |
) | |
self.springs.append(spring) | |
def update(self, dt: float, t: float): | |
# Update springs | |
for i, spring in enumerate(self.springs): | |
spring.update(dt) | |
# Update points | |
for i, point in enumerate(self.points): | |
# NOTE: Using a brute force method for keeping the points separate | |
# for j, other_point in enumerate(self.points): | |
# if j != i: | |
# # Keep point away from other points | |
# point.repel(other_point.position, other_point.radius * blob_point_repel_radius, 0.0005) | |
# point.repel(other_point.position, other_point.radius * 2, 0.04) | |
# Attract point towards the center of the blob | |
point.attract_constant(target=self.center, strength=0.003) | |
# Attract point towards the world origin | |
point.attract_constant(target=Vector((0.0, 0.0, 0.0)), strength=0.007) | |
# add a twist force: | |
twist_force = point.position.normalized().yxz | |
twist_force.y *= -1 | |
point.attract_constant(target=point.position + twist_force, strength=0.003) | |
# Run update on point | |
point.update(dt) | |
# Change the radius of the point | |
# NOTE: this is a temporary logic for making the blob more alive | |
# point.set_radius( | |
# pow(noise.noise(Vector(((i * 0.1 + t) * 0.1, (i * 3 + t * 0.5) * 0.2, (i * 7 + t * 3) * 0.3)), | |
# noise_basis='BLENDER'), 1) * ( | |
# blob_point_max_radius - blob_point_min_radius) + blob_point_min_radius) | |
def to_mesh(self, bm: bmesh.types.BMesh): | |
temp_mesh = bpy.data.meshes.new('tmp') | |
temp_mesh.from_pydata(vertices=[p.position for p in self.points], edges=[], faces=[blob_polygon]) | |
bm.from_mesh(temp_mesh) | |
bpy.data.meshes.remove(temp_mesh, do_unlink=True) | |
# create blob objects and meshes | |
blobs = [] | |
for i in range(num_blobs): | |
# place each blob along a spiral to give them space initially | |
percent = i / float(max(num_blobs - 1, 1)) | |
angle = i * pi * (3 - sqrt(5)) | |
radius = 1 + 2 * percent | |
location = polar(angle, radius) | |
blob_radius = (points_per_blob * blob_spring_len) / (2 * pi) * 5 | |
blobs.append(Blob(center_position=location, radius=blob_radius, num_points=points_per_blob)) | |
# setup the objects and meshes for the scene: | |
def setup(): | |
# create a collection for generated objects: | |
col = bpy.data.collections.get('generated') | |
if not col: | |
col = bpy.data.collections.new('generated') | |
bpy.context.scene.collection.children.link(col) | |
main = bpy.data.objects.get('main') | |
if not main: | |
main = bpy.data.objects.new('main', bpy.data.meshes.new('main')) | |
col.objects.link(main) | |
main.data.use_auto_smooth = True | |
main.data.auto_smooth_angle = radians(30) | |
solidify = main.modifiers.get('solidify') or main.modifiers.new('solidify', type='SOLIDIFY') | |
solidify.thickness = -0.07 | |
bevel = main.modifiers.get('bevel') or main.modifiers.new('bevel', type='BEVEL') | |
bevel.segments = 3 | |
bevel.width = 0.01 | |
bevel.angle_limit = radians(60) | |
array = main.modifiers.get('array') or main.modifiers.new('array', type='ARRAY') | |
array.count = 4 | |
array.relative_offset_displace = (0, 0, 1.2) | |
# update the points on each frame: | |
def update_blobs( | |
t: float, | |
bm: bmesh.types.BMesh | |
): | |
kd_tree = kdtree.KDTree(num_blobs * points_per_blob) | |
for i, blob in enumerate(blobs): | |
# blob.update(1.0 / total_frames) | |
blob.update(t=t * 10, dt=1) | |
for j, point in enumerate(blob.points): | |
point_index = i * points_per_blob + j | |
kd_tree.insert(point.position, point_index) | |
kd_tree.balance() | |
# make a second pass to repel points using the kd-tree: | |
for i, blob in enumerate(blobs): | |
for j, point in enumerate(blob.points): | |
point_index = i * points_per_blob + j | |
for (other_position, index, dist) in kd_tree.find_range(point.position, blob_point_repel_radius): | |
if index != point_index: | |
point.repel(other_position, 0.7, 0.007) | |
blob.to_mesh(bm) | |
def frame_update(scene): | |
frame = scene.frame_current | |
t = frame / float(total_frames) | |
bm = bmesh.new() | |
update_blobs(t, bm) | |
for face in bm.faces: | |
face.normal_update() | |
face.smooth = face.normal.z < 0.95 | |
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