Skip to content

Instantly share code, notes, and snippets.

@carson-katri
Created December 1, 2022 13:18
Show Gist options
  • Save carson-katri/d7b875ff405c23685660d6c05d8e7b28 to your computer and use it in GitHub Desktop.
Save carson-katri/d7b875ff405c23685660d6c05d8e7b28 to your computer and use it in GitHub Desktop.
Verlet integration in Geometry Script
from geometry_script import *
# Constants
SAMPLES = 3
# Inputs
class ClothInputs(InputGroup):
gravity: Vector = (0, 0, -0.5)
friction: Vector = (0.999, 0.999, 0.999)
stick_length: Float = 0.1
resolution: Int = 10
track: Object
track_end: Object
should_track_end: Bool
seed: Int
subdivisions: Int
# Attributes
old_position = Attribute("old_position", NamedAttribute.DataType.FLOAT_VECTOR)
@tree("Stick Offset")
def stick_offset(
a: Vector,
b: Vector,
stick_length: Float
):
"""
A helper function for calculating offsets applied from each connection between points.
"""
delta = b - a
distance = delta.vector_math(operation=VectorMath.Operation.LENGTH)
difference = stick_length - distance
percent = difference / distance / 2
return delta * percent
@simulation # Decorated with `@simulation` to use Blender 3.5+'s simulation nodes.
def verlet_integration(
points: Geometry,
cloth: ClothInputs,
dt: SimulationInput.DeltaTime,
et: SimulationInput.ElapsedTime
):
"""
Simulation block, implementing Verlet integration for simple cloth simulation.
Points are moved based on their velocity, friction, gravity, and connections between them.
"""
velocity = (position() - old_position()) * cloth.friction
# Store the old position so we can calculate the velocity on the next frame
points = old_position.store(points, position())
# Used for pinning
is_start = index() == 0
is_end = index() == (cloth.resolution - 1)
# Add velocity and gravity
points = points.set_position(
selection=~is_start,
offset=velocity + cloth.gravity
)
# Pin
points = points.set_position(
selection=is_start,
position=object_info(object=cloth.track).location
)
points = points.set_position(
selection=cloth.should_track_end & is_end,
position=object_info(object=cloth.track_end).location
)
can_offset = ~is_start & ~(cloth.should_track_end & is_end)
# Sample multiple times to remove jitter.
# This loop happens on the Python side, so SAMPLES needs to be a constant.
# These nodes will be create multiple times for each sample.
samples = []
for _ in range(SAMPLES):
last_point = points[position():index() - 1]
next_point = points[position():index() + 1]
# Find the offset we apply to the next point.
self_offset = switch(
input_type=Switch.InputType.VECTOR,
switch=is_end,
true=(0, 0, 0),
false=stick_offset(a=position(), b=next_point, stick_length=cloth.stick_length)
)
# Find the offset the last point applies to us.
parent_offset = stick_offset(a=last_point, b=position(), stick_length=cloth.stick_length)
# Randomize the order the offsets are applied in.
# If the order is not randomized, the points will skew to one side of the line when pinning both the start and the end.
application_order = random_value(
data_type=RandomValue.DataType.BOOLEAN,
# This should be able to use `ElapsedTime`, but it does not work in the current version of the simulation nodes branch.
seed=scene_time().frame
)
# Offset by negative self_offset and positive parent_offset, in random order.
sample = switch(
input_type=Switch.InputType.VECTOR,
switch=application_order,
true=parent_offset,
false=~self_offset
) + switch(
input_type=Switch.InputType.VECTOR,
switch=application_order,
true=~self_offset,
false=parent_offset
)
# Add the sample
samples.append(sample)
# Get the average of the samples and offset the point.
added = (0, 0, 0)
for sample in samples:
added += sample
points = points.set_position(
selection=can_offset,
offset=added / SAMPLES
)
# Floor limit
points = points.set_position(
position=combine_xyz(
x=position().x,
y=position().y,
z=clamp(max=9999, value=position().z)
)
)
# The returned points will be passed back in on the next frame when using `@simulation`.
return points
@tree("Cloth")
def cloth(geometry: Geometry, cloth: ClothInputs):
"""
The main node tree to apply to geometry that should be simulated.
"""
simulated = geometry.curve_to_points(count=cloth.resolution).points
# Simulate
simulated = verlet_integration(simulated, cloth)
# Copy the simulated points back onto a curve, which is converted to a mesh
return mesh_line(count=cloth.resolution) \
.set_position(position=simulated[position()]) \
.subdivision_surface(level=cloth.subdivisions) \
.mesh_to_curve() \
.curve_to_mesh(
profile_curve=curve_circle(radius=0.1),
fill_caps=True
) \
.set_shade_smooth(shade_smooth=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment