Skip to content

Instantly share code, notes, and snippets.

@ylegall
Last active April 24, 2021 17:45
Show Gist options
  • Save ylegall/5c0feab1a335cdbc6a5b4e3097adcafe to your computer and use it in GitHub Desktop.
Save ylegall/5c0feab1a335cdbc6a5b4e3097adcafe to your computer and use it in GitHub Desktop.
Blender Blobs
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