Skip to content

File: ShaderFlow/Modules/Dynamics.py

ShaderFlow.Modules.Dynamics

DynType

DynType: TypeAlias = numpy.ndarray

INSTANT_FREQUENCY

INSTANT_FREQUENCY = 1000000.0

NumberDunder

Bases: Number

Boring dunder methods for number-like objects

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class NumberDunder(Number):
    """Boring dunder methods for number-like objects"""

    def __float__(self) -> float:
        return float(self.value)
    def __int__(self) -> int:
        return int(self.value)
    def __str__(self) -> str:
        return str(self.value)

    # Multiplication
    def __mul__(self, other) -> DynType:
        return self.value * other
    def __rmul__(self, other) -> DynType:
        return self * other

    # Addition
    def __add__(self, other) -> DynType:
        return self.value + other
    def __radd__(self, other) -> DynType:
        return self + other

    # Subtraction
    def __sub__(self, other) -> DynType:
        return self.value - other
    def __rsub__(self, other) -> DynType:
        return self - other

    # Division
    def __truediv__(self, other) -> DynType:
        return self.value / other
    def __rtruediv__(self, other) -> DynType:
        return self / other

    # Floor division
    def __floordiv__(self, other) -> DynType:
        return self.value // other
    def __rfloordiv__(self, other) -> DynType:
        return self // other

    # Modulus
    def __mod__(self, other) -> DynType:
        return self.value % other
    def __rmod__(self, other) -> DynType:
        return self % other

    # Power
    def __pow__(self, other) -> DynType:
        return self.value ** other
    def __rpow__(self, other) -> DynType:
        return self ** other

__float__

__float__() -> float
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
26
27
def __float__(self) -> float:
    return float(self.value)

__int__

__int__() -> int
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
28
29
def __int__(self) -> int:
    return int(self.value)

__str__

__str__() -> str
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
30
31
def __str__(self) -> str:
    return str(self.value)

__mul__

__mul__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
34
35
def __mul__(self, other) -> DynType:
    return self.value * other

__rmul__

__rmul__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
36
37
def __rmul__(self, other) -> DynType:
    return self * other

__add__

__add__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
40
41
def __add__(self, other) -> DynType:
    return self.value + other

__radd__

__radd__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
42
43
def __radd__(self, other) -> DynType:
    return self + other

__sub__

__sub__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
46
47
def __sub__(self, other) -> DynType:
    return self.value - other

__rsub__

__rsub__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
48
49
def __rsub__(self, other) -> DynType:
    return self - other

__truediv__

__truediv__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
52
53
def __truediv__(self, other) -> DynType:
    return self.value / other

__rtruediv__

__rtruediv__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
54
55
def __rtruediv__(self, other) -> DynType:
    return self / other

__floordiv__

__floordiv__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
58
59
def __floordiv__(self, other) -> DynType:
    return self.value // other

__rfloordiv__

__rfloordiv__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
60
61
def __rfloordiv__(self, other) -> DynType:
    return self // other

__mod__

__mod__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
64
65
def __mod__(self, other) -> DynType:
    return self.value % other

__rmod__

__rmod__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
66
67
def __rmod__(self, other) -> DynType:
    return self % other

__pow__

__pow__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
70
71
def __pow__(self, other) -> DynType:
    return self.value ** other

__rpow__

__rpow__(other) -> DynType
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
72
73
def __rpow__(self, other) -> DynType:
    return self ** other

DynamicNumber

Bases: NumberDunder, Number

Simulate on time domain a progressive second order system

Sources:

This is a Python-port of the video's math, with custom implementation and extras

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
@define(slots=False)
class DynamicNumber(NumberDunder, Number):
    """
    Simulate on time domain a progressive second order system

    ### Sources:
    - Control System classes on my university which I got 6/10 final grade but survived
    - https://www.youtube.com/watch?v=KPoeNZZ6H4s <- Math mostly took from here, thanks @t3ssel8r
    - https://en.wikipedia.org/wiki/Semi-implicit_Euler_method

    This is a Python-port of the video's math, with custom implementation and extras
    """

    # # Base system values

    def _ensure_numpy(self, value) -> numpy.ndarray:
        if isinstance(value, numpy.ndarray):
            return value
        return numpy.array(value, dtype=getattr(value, "dtype", self.dtype))

    def _ensure_numpy_setattr(self, attribute, value) -> numpy.ndarray:
        return self._ensure_numpy(value)

    value: DynType = field(default=0, on_setattr=_ensure_numpy_setattr)
    """The current value of the system. Prefer explicitly using it over the object itself"""

    target: DynType = field(default=0, on_setattr=_ensure_numpy_setattr)
    """The target value the system is trying to reach, modeled by the parameters"""

    dtype: numpy.dtype = field(default=numpy.float64)
    """Data type of the NumPy vectorized data"""

    initial: DynType = field(default=None)
    """Initial value of the system, defaults to first value set"""

    def __attrs_post_init__(self):
        self.set(self.target or self.value)

    def set(self, value: DynType, *, instant: bool=True) -> None:
        value = self._ensure_numpy(value)
        self.value = deepcopy(value) if (instant) else self.value
        self.target = deepcopy(value)
        self.initial = deepcopy(value)
        self.previous = deepcopy(value) if (instant) else self.previous

        zeros = numpy.zeros_like(value)
        self.integral = deepcopy(zeros)
        self.derivative = deepcopy(zeros)
        self.acceleration = deepcopy(zeros)

    def reset(self, instant: bool=False):
        self.set(self.initial, instant=instant)

    # # Dynamics system parameters

    frequency: float = 1.0
    """Natural frequency of the system in Hertz, "the speed the system responds to a change in input".
    Also, the frequency it tends to vibrate at, doesn't affect shape of the resulting motion"""

    zeta: float = 1.0
    """Damping coefficient, z=0 vibration never dies, z=1 is the critical limit where the system
    does not overshoot, z>1 increases this effect and the system takes longer to settle"""

    response: float = 0.0
    """Defines the initial response "time" of the system, when r=1 the system responds instantly
    to changes on the input, when r=0 the system takes a bit to respond (smoothstep like), when r<0
    the system "anticipates" motion"""

    precision: float = 1e-6
    """If `max(target - value) < precision`, the system stops updating to save computation"""

    # # Auxiliary intrinsic variables

    integral: DynType = 0.0
    """Integral of the system, the sum of all values over time"""

    integrate: bool = False
    """Whether to integrate the system's value over time"""

    derivative: DynType = 0.0
    """Derivative of the system, the rate of change of the value in ($unit/second)"""

    acceleration: DynType = 0.0
    """Acceleration of the system, the rate of change of the derivative in ($unit/second^2)"""

    previous: DynType = 0.0
    """Previous target value"""

    @property
    def instant(self) -> bool:
        """Update the system immediately to the target value"""
        return (self.frequency >= INSTANT_FREQUENCY)

    @property
    def k1(self) -> float:
        """Y velocity coefficient"""
        return self.zeta / (pi * self.frequency)

    @property
    def k2(self) -> float:
        """Y acceleration coefficient"""
        return 1.0 / (self.radians*self.radians)

    @property
    def k3(self) -> float:
        """X velocity coefficient"""
        return (self.response * self.zeta) / (tau * self.frequency)

    @property
    def radians(self) -> float:
        """Natural resonance frequency in radians per second"""
        return (tau * self.frequency)

    @property
    def damping(self) -> float:
        """Damping ratio of some sort"""
        return self.radians * (abs(self.zeta*self.zeta - 1.0))**0.5

    def next(self, target: Optional[DynType]=None, dt: float=1.0) -> DynType:
        """
        Update the system to the next time step, optionally with a new target value
        # Fixme: There is a HUGE potential for speed gains if we don't create many temporary ndarray

        Args:
            target: Next target value to reach, None for previous
            dt:     Time delta since last update

        Returns:
            The system's self.value
        """
        if (not dt):
            return self.value

        # Update target and recreate if necessary
        if (target is not None):
            self.target = self._ensure_numpy(target)

            if (self.target.shape != self.value.shape):
                self.set(target)

        # Todo: instant mode

        # Optimization: Do not compute if within precision to target
        if (numpy.abs(self.target - self.value).max() < self.precision):
            if (self.integrate):
                self.integral += (self.value * dt)
            return self.value

        # "Estimate velocity"
        velocity = (self.target - self.previous)/dt
        self.previous = self.target

        # "Clamp k2 to stable values without jitter"
        if (self.radians*dt < self.zeta):
            k1 = self.k1
            k2 = max(k1*dt, self.k2, 0.5*(k1+dt)*dt)

        # "Use pole matching when the system is very fast"
        else:
            t1 = math.exp(-1 * self.zeta * self.radians * dt)
            a1 = 2 * t1 * (math.cos if self.zeta <= 1 else math.cosh)(self.damping*dt)
            t2 = 1/(1 + t1*t1 - a1) * dt
            k1 = t2 * (1 - t1*t1)
            k2 = t2 * dt

        # Integrate values
        self.value       += (self.derivative * dt)
        self.acceleration = (self.target + self.k3*velocity - self.value - k1*self.derivative)/k2
        self.derivative  += (self.acceleration * dt)
        if (self.integrate):
            self.integral += (self.value * dt)
        return self.value

    @staticmethod
    def extract(*objects: Union[Number, Self]) -> tuple[Number]:
        """Extract the values from DynamicNumbers objects or return the same object"""
        return tuple(obj.value if isinstance(obj, DynamicNumber) else obj for obj in objects)

value

value: DynType = field(
    default=0, on_setattr=_ensure_numpy_setattr
)

The current value of the system. Prefer explicitly using it over the object itself

target

target: DynType = field(
    default=0, on_setattr=_ensure_numpy_setattr
)

The target value the system is trying to reach, modeled by the parameters

dtype

dtype: numpy.dtype = field(default=numpy.float64)

Data type of the NumPy vectorized data

initial

initial: DynType = field(default=None)

Initial value of the system, defaults to first value set

__attrs_post_init__

__attrs_post_init__()
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
112
113
def __attrs_post_init__(self):
    self.set(self.target or self.value)

set

set(value: DynType, *, instant: bool = True) -> None
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
115
116
117
118
119
120
121
122
123
124
125
def set(self, value: DynType, *, instant: bool=True) -> None:
    value = self._ensure_numpy(value)
    self.value = deepcopy(value) if (instant) else self.value
    self.target = deepcopy(value)
    self.initial = deepcopy(value)
    self.previous = deepcopy(value) if (instant) else self.previous

    zeros = numpy.zeros_like(value)
    self.integral = deepcopy(zeros)
    self.derivative = deepcopy(zeros)
    self.acceleration = deepcopy(zeros)

reset

reset(instant: bool = False)
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
127
128
def reset(self, instant: bool=False):
    self.set(self.initial, instant=instant)

frequency

frequency: float = 1.0

Natural frequency of the system in Hertz, "the speed the system responds to a change in input". Also, the frequency it tends to vibrate at, doesn't affect shape of the resulting motion

zeta

zeta: float = 1.0

Damping coefficient, z=0 vibration never dies, z=1 is the critical limit where the system does not overshoot, z>1 increases this effect and the system takes longer to settle

response

response: float = 0.0

Defines the initial response "time" of the system, when r=1 the system responds instantly to changes on the input, when r=0 the system takes a bit to respond (smoothstep like), when r<0 the system "anticipates" motion

precision

precision: float = 1e-06

If max(target - value) < precision, the system stops updating to save computation

integral

integral: DynType = 0.0

Integral of the system, the sum of all values over time

integrate

integrate: bool = False

Whether to integrate the system's value over time

derivative

derivative: DynType = 0.0

Derivative of the system, the rate of change of the value in ($unit/second)

acceleration

acceleration: DynType = 0.0

Acceleration of the system, the rate of change of the derivative in ($unit/second^2)

previous

previous: DynType = 0.0

Previous target value

instant

instant: bool

Update the system immediately to the target value

k1

k1: float

Y velocity coefficient

k2

k2: float

Y acceleration coefficient

k3

k3: float

X velocity coefficient

radians

radians: float

Natural resonance frequency in radians per second

damping

damping: float

Damping ratio of some sort

next

next(
    target: Optional[DynType] = None, dt: float = 1.0
) -> DynType

Update the system to the next time step, optionally with a new target value

Fixme: There is a HUGE potential for speed gains if we don't create many temporary ndarray

Parameters:

  • target (Optional[DynType], default: None ) –

    Next target value to reach, None for previous

  • dt (float, default: 1.0 ) –

    Time delta since last update

Returns:

  • DynType

    The system's self.value

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def next(self, target: Optional[DynType]=None, dt: float=1.0) -> DynType:
    """
    Update the system to the next time step, optionally with a new target value
    # Fixme: There is a HUGE potential for speed gains if we don't create many temporary ndarray

    Args:
        target: Next target value to reach, None for previous
        dt:     Time delta since last update

    Returns:
        The system's self.value
    """
    if (not dt):
        return self.value

    # Update target and recreate if necessary
    if (target is not None):
        self.target = self._ensure_numpy(target)

        if (self.target.shape != self.value.shape):
            self.set(target)

    # Todo: instant mode

    # Optimization: Do not compute if within precision to target
    if (numpy.abs(self.target - self.value).max() < self.precision):
        if (self.integrate):
            self.integral += (self.value * dt)
        return self.value

    # "Estimate velocity"
    velocity = (self.target - self.previous)/dt
    self.previous = self.target

    # "Clamp k2 to stable values without jitter"
    if (self.radians*dt < self.zeta):
        k1 = self.k1
        k2 = max(k1*dt, self.k2, 0.5*(k1+dt)*dt)

    # "Use pole matching when the system is very fast"
    else:
        t1 = math.exp(-1 * self.zeta * self.radians * dt)
        a1 = 2 * t1 * (math.cos if self.zeta <= 1 else math.cosh)(self.damping*dt)
        t2 = 1/(1 + t1*t1 - a1) * dt
        k1 = t2 * (1 - t1*t1)
        k2 = t2 * dt

    # Integrate values
    self.value       += (self.derivative * dt)
    self.acceleration = (self.target + self.k3*velocity - self.value - k1*self.derivative)/k2
    self.derivative  += (self.acceleration * dt)
    if (self.integrate):
        self.integral += (self.value * dt)
    return self.value

extract

extract(*objects: Union[Number, Self]) -> tuple[Number]

Extract the values from DynamicNumbers objects or return the same object

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
250
251
252
253
@staticmethod
def extract(*objects: Union[Number, Self]) -> tuple[Number]:
    """Extract the values from DynamicNumbers objects or return the same object"""
    return tuple(obj.value if isinstance(obj, DynamicNumber) else obj for obj in objects)

ShaderDynamics

Bases: ShaderModule, DynamicNumber

Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
@define
class ShaderDynamics(ShaderModule, DynamicNumber):
    name: str  = "iShaderDynamics"
    real: bool = False

    primary: bool = True
    """Whether to output the value of the system as a uniform"""

    differentiate: bool = False
    """Where to output the derivative of the system as a uniform"""

    def build(self) -> None:
        DynamicNumber.__attrs_post_init__(self)

    def setup(self) -> None:
        self.reset(instant=self.scene.freewheel)

    def update(self) -> None:
        # Note: abs(dt) the system is unstable backwards in time (duh)
        self.next(dt=abs(self.scene.rdt if self.real else self.scene.dt))

    @property
    def type(self) -> Optional[str]:
        if not (shape := self.value.shape):
            return "float"
        elif (shape[0] == 1):
            return "float"
        elif (shape[0] == 2):
            return "vec2"
        elif (shape[0] == 3):
            return "vec3"
        elif (shape[0] == 4):
            return "vec4"
        return None

    def pipeline(self) -> Iterable[ShaderVariable]:
        if (not self.type):
            return None

        if (self.primary):
            yield Uniform(self.type, f"{self.name}", self.value)

        if (self.integrate):
            yield Uniform(self.type, f"{self.name}Integral", self.integral)

        if (self.differentiate):
            yield Uniform(self.type, f"{self.name}Derivative", self.derivative)

name

name: str = 'iShaderDynamics'

real

real: bool = False

primary

primary: bool = True

Whether to output the value of the system as a uniform

differentiate

differentiate: bool = False

Where to output the derivative of the system as a uniform

build

build() -> None
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
268
269
def build(self) -> None:
    DynamicNumber.__attrs_post_init__(self)

setup

setup() -> None
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
271
272
def setup(self) -> None:
    self.reset(instant=self.scene.freewheel)

update

update() -> None
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
274
275
276
def update(self) -> None:
    # Note: abs(dt) the system is unstable backwards in time (duh)
    self.next(dt=abs(self.scene.rdt if self.real else self.scene.dt))

type

type: Optional[str]

pipeline

pipeline() -> Iterable[ShaderVariable]
Source code in Projects/ShaderFlow/ShaderFlow/Modules/Dynamics.py
292
293
294
295
296
297
298
299
300
301
302
303
def pipeline(self) -> Iterable[ShaderVariable]:
    if (not self.type):
        return None

    if (self.primary):
        yield Uniform(self.type, f"{self.name}", self.value)

    if (self.integrate):
        yield Uniform(self.type, f"{self.name}Integral", self.integral)

    if (self.differentiate):
        yield Uniform(self.type, f"{self.name}Derivative", self.derivative)