Source code for genesis.engine.force_fields

import numpy as np
import gstaichi as ti

import genesis as gs
from genesis.repr_base import RBC


[docs] @ti.data_oriented class ForceField(RBC): """ Base class for all force fields. This class should not be used directly. Note ---- It's called `ForceField`, but it's actually an acceleration field, as force doesn't have a notion of spatial density. """ def __init__(self): self._active = ti.field(gs.ti_bool, shape=()) self._active[None] = False
[docs] def activate(self): """ Activate the force field. """ self._active[None] = True
[docs] def deactivate(self): """ Deactivate the force field. """ self._active[None] = False
[docs] @ti.func def get_acc(self, pos, vel, t, i): acc = ti.Vector.zero(gs.ti_float, 3) if self._active[None]: acc = self._get_acc(pos, vel, t, i) return acc
@property def active(self): """ Whether the force field is active. """ return self._active[None]
[docs] class Constant(ForceField): """ Constant force field with a static acceleration everywhere. Parameters: ----------- direction: array_like, shape=(3,) The direction of the force (acceleration). Will be normalized. strength: float The strength of the force (acceleration). """ def __init__(self, direction=(1, 0, 0), strength=1.0): super().__init__() direction = np.array(direction) if direction.shape != (3,): raise ValueError("direction must have shape (3,)") self._direction = direction / np.linalg.norm(direction) self._strength = strength self._acc_ti = ti.Vector(self._direction * self._strength, dt=gs.ti_float) @ti.func def _get_acc(self, pos, vel, t, i): return self._acc_ti @property def direction(self): return self._direction @property def strength(self): return self._strength
[docs] class Wind(ForceField): """ Wind force field with a static acceleration in a cylindrical region. Parameters: ----------- direction: array_like, shape=(3,) The direction of the wind. Will be normalized. strength: float The strength of the wind. radius: float The radius of the cylinder. center: array_like, shape=(3,) The center of the cylinder. """ def __init__(self, direction=(1, 0, 0), strength=1.0, radius=1, center=(0, 0, 0)): super().__init__() direction = np.array(direction) if direction.shape != (3,): raise ValueError("direction must have shape (3,)") center = np.array(center) if center.shape != (3,): raise ValueError("center must have shape (3,)") self._center = center self._direction = direction / np.linalg.norm(direction) self._strength = strength self._radius = radius self._direction_ti = ti.Vector(self._direction, dt=gs.ti_float) self._center_ti = ti.Vector(self._center, dt=gs.ti_float) self._acc_ti = ti.Vector(self._direction * self._strength, dt=gs.ti_float) @ti.func def _get_acc(self, pos, vel, t, i): # distance to the center of the cylinder pointing in the direction of the wind dist = (pos - self._center_ti).cross(self._direction_ti).norm() acc = self._acc_ti if dist > self._radius: acc = ti.Vector.zero(gs.ti_float, 3) return acc @property def direction(self): return self._direction @property def strength(self): return self._strength @property def radius(self): return self._radius @property def center(self): return self._center
[docs] class Point(ForceField): """ Point force field gives a constant force towards (positive strength) or away from (negative strength) the point. Parameters: ----------- strength: float The strength of the wind. position: array_like, shape=(3,) The position of the point. flow: float The flow of the force field. falloff_pow: float The power of the falloff. """ def __init__(self, strength=1.0, position=(0, 0, 0), falloff_pow=0.0, flow=1.0): super().__init__() position = np.array(position) if position.shape != (3,): raise ValueError("position must have shape (3,)") self._strength = strength self._position = position self._falloff_pow = falloff_pow self._flow = flow self._position_ti = ti.Vector(self._position, dt=gs.ti_float) @ti.func def _get_acc(self, pos, vel, t, i): relative_pos = pos - self._position_ti radius = relative_pos.norm(gs.EPS) direction = relative_pos / radius falloff = 1 / (radius + 1.0) ** self._falloff_pow acc = self._strength * direction # flow acc += (acc - vel) * self._flow acc *= falloff return acc @property def strength(self): return self._strength @property def position(self): return self._position
[docs] class Drag(ForceField): """ Drag force field gives a force opposite to the velocity. Parameters: ----------- linear: float The linear drag coefficient. quadratic: float The quadratic drag coefficient. """ def __init__(self, linear=0.0, quadratic=0.0): super().__init__() self._linear = linear self._quadratic = quadratic @ti.func def _get_acc(self, pos, vel, t, i): return -self._linear * vel - self._quadratic * vel.norm() * vel @property def linear(self): return self._linear @property def quadratic(self): return self._quadratic
[docs] class Noise(ForceField): """ Noise force field samples random noise at each point. """ def __init__(self, strength=1.0): super().__init__() self._strength = strength @ti.func def _get_acc(self, pos, vel, t, i): noise = ( ti.Vector( [ ti.random(gs.ti_float), ti.random(gs.ti_float), ti.random(gs.ti_float), ], dt=gs.ti_float, ) * 2 - 1 ) return noise * self._strength @property def strength(self): return self._strength
[docs] class Vortex(ForceField): """ Vortex force field revolving around z-axis. Parameters: ----------- strength_perpendicular: float The strength of the vortex flow in the perpendicular direction. Positive for counterclockwise, negative for clockwise. strength_radial: float The strength of the vortex flow in the radial direction. Positive for inward, negative for outward. center: array_like, shape=(3,) The center of the vortex. falloff_pow: float The power of the falloff. falloff_min: float The minimum distance (in meters) for the falloff. Under this distance, the force is effective with full strength. falloff_max: float The maximum distance (in meters) for the falloff. Above this distance, the force is ineffective. """ def __init__( self, direction=(0.0, 0.0, 1.0), center=(0.0, 0.0, 0.0), strength_perpendicular=20.0, strength_radial=0.0, falloff_pow=2.0, falloff_min=0.01, falloff_max=np.inf, damping=0.0, ): super().__init__() direction = np.array(direction) if direction.shape != (3,): raise ValueError("direction must have shape (3,)") center = np.array(center) if center.shape != (3,): raise ValueError("center must have shape (3,)") self._center = center self._direction = direction / np.linalg.norm(direction) self._damping = damping self._strength_perpendicular = strength_perpendicular self._strength_radial = strength_radial self._falloff_pow = falloff_pow self._falloff_min = falloff_min self._falloff_max = falloff_max self._direction_ti = ti.Vector(self._direction, dt=gs.ti_float) self._center_ti = ti.Vector(self._center, dt=gs.ti_float) @ti.func def _get_acc(self, pos, vel, t, i): relative_pos = ti.Vector([pos[0] - self._center_ti[0], pos[1] - self._center_ti[1]]) radius = relative_pos.norm() perpendicular = ti.Vector([-relative_pos[1], relative_pos[0], 0.0], dt=gs.ti_float) radial = -ti.Vector([relative_pos[0], relative_pos[1], 0.0], dt=gs.ti_float) falloff = gs.ti_float(0.0) if radius < self._falloff_min: falloff = 1.0 elif radius < self._falloff_max: falloff = 1 / (radius - self._falloff_min + 1.0) ** self._falloff_pow else: falloff = 0.0 acceleration = falloff * (self._strength_perpendicular * perpendicular + self._strength_radial * radial) acceleration -= self._damping * vel return acceleration @property def direction(self): return self._direction @property def radius(self): return self._radius @property def center(self): return self._center @property def strength_perpendicular(self): return self._strength_perpendicular @property def strength_radial(self): return self._strength_radial @property def falloff_pow(self): return self._falloff_pow @property def falloff_min(self): return self._falloff_min @property def falloff_max(self): return self._falloff_max
[docs] class Turbulence(ForceField): """ Turbulence force field generated using Perlin noise. Parameters: ----------- strength: float The strength of the turbulence. frequency: float The spatial frequency of repeated patterns used for Perlin noise. flow: float The flow of the turbulence. seed: int | None The seed for the Perlin noise. """ def __init__(self, strength=1.0, frequency=3, flow=0.0, seed=None): super().__init__() self._strength = strength self._frequency = frequency self._flow = flow self._perlin_x = PerlinNoiseField(frequency=self._frequency, seed=seed, seed_offset=0) self._perlin_y = PerlinNoiseField(frequency=self._frequency, seed=seed, seed_offset=1) self._perlin_z = PerlinNoiseField(frequency=self._frequency, seed=seed, seed_offset=2) @ti.func def _get_acc(self, pos, vel, t, i): acc = ti.Vector( [ self._perlin_x._noise(pos[0], pos[1], pos[2]), self._perlin_y._noise(pos[0], pos[1], pos[2]), self._perlin_z._noise(pos[0], pos[1], pos[2]), ], dt=gs.ti_float, ) acc *= self._strength # flow acc += (acc - vel) * self._flow return acc @property def strength(self): return self._strength @property def frequency(self): return self._frequency
[docs] class Custom(ForceField): """ Custom force field with a user-defined force(acceleration) function `f(pos, vel, t, i)`. Parameters: ----------- func: A callable taichi func (a python function wrapped by `@ti.func`) The acceleration function. Must have the signature `f(pos: ti.types.vector(3), vel: ti.types.vector(3), t: ti.f32) -> ti.types.vector(3)`. """ def __init__(self, func): super().__init__() self.get_acc = func
[docs] @ti.data_oriented class PerlinNoiseField: """ Perlin noise field for generating 3D noise. Each PerlinNoiseField object has will create a different noise field. """ def __init__(self, wrap_size=256, frequency=10, seed=None, seed_offset=0): self._wrap_size = wrap_size self._permutation = np.arange(self._wrap_size, dtype=np.int32) self._frequency = frequency if seed is not None: state = np.random.get_state() np.random.seed(seed + seed_offset) np.random.shuffle(self._permutation) np.random.set_state(state) self._permutation_ti = ti.field(ti.i32, shape=(self._wrap_size * 2,)) self._permutation_ti.from_numpy(np.concatenate([self._permutation, self._permutation])) @ti.func def _fade(self, t): """Fade function for smoothing the interpolation.""" return t * t * t * (t * (t * 6 - 15) + 10) @ti.func def _lerp(self, t, a, b): """Linear interpolation between a and b.""" return a + t * (b - a) @ti.func def _grad(self, hash, x, y, z): """Calculate dot product between gradient vector and distance vector.""" h = hash & 15 # Convert low 4 bits of hash code u = x if h >= 8: u = y v = y if h >= 4: v = z if h == 12 or h == 14: v = x return (u if (h & 1) == 0 else -u) + (v if (h & 2) == 0 else -v) @ti.func def _noise(self, x, y, z): x *= self._frequency y *= self._frequency z *= self._frequency # Find unit cube that contains the point X = gs.ti_int(ti.floor(x)) & (self._wrap_size - 1) Y = gs.ti_int(ti.floor(y)) & (self._wrap_size - 1) Z = gs.ti_int(ti.floor(z)) & (self._wrap_size - 1) # Find relative x, y, z of point in the cube x -= ti.floor(x) y -= ti.floor(y) z -= ti.floor(z) # Compute fade curves for each coordinate u = self._fade(x) v = self._fade(y) w = self._fade(z) # Hash coordinates of the 8 cube corners A = self._permutation_ti[X] + Y AA = self._permutation_ti[A] + Z AB = self._permutation_ti[A + 1] + Z B = self._permutation_ti[X + 1] + Y BA = self._permutation_ti[B] + Z BB = self._permutation_ti[B + 1] + Z # Add blended results from the 8 corners of the cube return self._lerp( w, self._lerp( v, self._lerp( u, self._grad(self._permutation_ti[AA], x, y, z), self._grad(self._permutation_ti[BA], x - 1, y, z), ), self._lerp( u, self._grad(self._permutation_ti[AB], x, y - 1, z), self._grad(self._permutation_ti[BB], x - 1, y - 1, z), ), ), self._lerp( v, self._lerp( u, self._grad(self._permutation_ti[AA + 1], x, y, z - 1), self._grad(self._permutation_ti[BA + 1], x - 1, y, z - 1), ), self._lerp( u, self._grad(self._permutation_ti[AB + 1], x, y - 1, z - 1), self._grad(self._permutation_ti[BB + 1], x - 1, y - 1, z - 1), ), ), )