particri / tests /test_trapcode_features.py
ArifiIslam's picture
Add Trapcode Particular VFX systems: Aux trails, Layer emitter, Bounce physics, Curves over life (v1.1.0)
e14f09d verified
Raw History Blame Contribute Delete
4.43 kB
import pytest
import numpy as np
import particri
def test_curves_over_life():
"""Verify that all curve types can be assigned and evaluated during rendering."""
curves = [
particri.CurveType.Linear,
particri.CurveType.Bell,
particri.CurveType.EaseIn,
particri.CurveType.EaseOut,
particri.CurveType.GrowAndFade,
particri.CurveType.Pulse,
particri.CurveType.Decay,
]
for curve in curves:
engine = particri.ParticleEngine(max_particles=500, width=320, height=240)
engine.add_emitter(
shape=particri.EmitterShape.Point,
position=(160.0, 120.0),
rate=100.0,
size_curve=curve,
opacity_curve=curve,
)
engine.burst(0, 20)
engine.update(0.1)
img = engine.render(bloom=False)
assert img.shape == (240, 320, 4)
assert engine.active_count >= 20
def test_aux_particle_system():
"""Verify that parent particles emit secondary aux particles and death bursts."""
engine = particri.ParticleEngine(max_particles=5000, width=640, height=480)
# Add a single parent emitter that bursts 5 particles
engine.add_emitter(
shape=particri.EmitterShape.Point,
position=(320.0, 240.0),
rate=0.0, # No continuous primary emission
speed_range=(50.0, 50.0),
lifetime_range=(0.5, 0.5), # Short lifetime
)
# Enable Aux system with rate and death burst
engine.enable_aux_system(
rate=20.0,
lifetime=1.0,
size_range=(4.0, 1.0),
spread=10.0,
particle_shape=particri.ParticleShape.Glitter,
emit_at_death=5,
)
engine.burst(0, 5)
assert engine.active_count == 5
# Advance physics: parent particles should emit aux particles along their path
engine.update(0.2)
assert engine.active_count > 5, "Aux system should have spawned child particles"
# Advance past parent lifetime (0.5s): parent particles die, triggering emit_at_death bursts
count_before = engine.active_count
engine.update(0.4)
# Child particles are still alive
assert engine.active_count > 0
# Test disabling aux system
engine.disable_aux_system()
engine.clear()
assert engine.active_count == 0
def test_bounce_plane_physics():
"""Verify that particles falling downwards bounce off a floor plane."""
engine = particri.ParticleEngine(max_particles=100, width=640, height=480)
floor_y = 400.0
engine.add_bounce_plane(coord=floor_y, bounce=0.6, friction=0.9, is_horizontal=True, is_max=True)
# Emit a particle directly downward towards the floor
engine.add_emitter(
shape=particri.EmitterShape.Point,
position=(320.0, 380.0),
rate=0.0,
speed_range=(100.0, 100.0),
angle_range=(1.5707963, 1.5707963), # Exactly downwards (pi/2)
lifetime_range=(2.0, 2.0),
)
engine.burst(0, 1)
# Update enough for particle to hit floor_y (20px at 100px/s takes 0.2s)
engine.update(0.3)
img = engine.render(bloom=False)
assert img.shape == (480, 640, 4)
assert engine.active_count == 1
def test_layer_mask_emitter():
"""Verify that particles are spawned exclusively from non-empty pixels of an image mask."""
engine = particri.ParticleEngine(max_particles=2000, width=200, height=200)
# Create a 200x200 mask with a 20x20 bright square in the upper-left (from x=10..30, y=10..30)
mask = np.zeros((200, 200, 4), dtype=np.float32)
mask[10:30, 10:30, 0] = 1.0 # Red
mask[10:30, 10:30, 1] = 0.5 # Green
mask[10:30, 10:30, 2] = 0.0 # Blue
mask[10:30, 10:30, 3] = 1.0 # Alpha
emitter_idx = engine.add_layer_emitter(
image=mask,
rate=0.0,
speed_range=(0.0, 0.0), # Stationary particles
lifetime_range=(5.0, 5.0),
threshold=0.1,
inherit_layer_color=True,
)
assert emitter_idx == 0
# Burst particles from layer
engine.burst(0, 50)
assert engine.active_count == 50
# Render frame
rendered = engine.render(bloom=False)
assert rendered.shape == (200, 200, 4)
# The region outside the box [0..100, 0..100] should not have particles born in the lower-right [150..200, 150..200]
lower_right_sum = np.sum(rendered[150:200, 150:200, :3])
assert lower_right_sum == 0, "No particles should be spawned outside the mask pixels"