Created
December 1, 2022 13:18
-
-
Save carson-katri/d7b875ff405c23685660d6c05d8e7b28 to your computer and use it in GitHub Desktop.
Verlet integration in Geometry Script
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
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