Intro
When writing shader or during any procedural creation process (texturing, modeling, shading, lighting, animating...) you often find yourself modifying signals in different ways so they behave the way you want. It is common to use smoothstep() to threshold some values and still keep a smooth transition, or to pow() a signal to modify its contrast, to use a clamp() to clip, a fmod() to repeat, an over() to combine, a mix() to blend, a noise() to enrich, an exp() to attenuate, etc etc. These are nice functions cause they are available to you by default in most systems, as hardware instructions or as function calls, so we tend to use them. However there are some operations that are often used that don't exist in any language that you still use a lot. Never found yourself subtracting to smoothstep()'s to isolate some range, ie, create a ring? Never found yourself doing some smooth clipping to avoid dividing by huge numbers? Of course. This are some of these functions that I have collected over the years.
Almost Identity
Say you don't want to change a value unless it's too small and screws some of your computations up. Then, rather than doing a sharp conditional branch, you can blend your value with your threshold, and do it smoothly (say, with a cubic polynomial). Set m to be your threshold (anything above m stays unchanged), and n the value things will take when your value is zero. Then set
p(0) = n
p(m) = m
p'(0) = 0
p'(m) = 1
therefore, if p(x) is a cubic, then p(x) = (2n-m)(x/m)^3 + (2m-3n)(x/m)^2 + n
float almostIdentity( float x, float m, float n ) { if( x>m ) return x; const float a = 2.0*n - m const float b = 2.0*m - 3.0*n; const float t = x/m; return (a*t + b)*t*t + n; } |
Impulse
Great for triggering behaviours or making envelopes for music or animation, and for anything that grows fast and then slowly decays. Use k to control the stretching o the function. Btw, it's maximum, which is 1.0, happens at exactly x = 1/k.
float impulse( float k, float x ) { const float h = k*x; return h*exp(1.0-h); } |
Cubic Pulse
Of course you found yourself doing smoothstep(c-w,c,x)-smoothstep(c,c+w,x) very often, probably cause you were trying to isolate some features. Then this cubicPulse() is your friend. Also, why not, you can use it as a cheap replacement for a gaussian.
float cubicPulse( float c, float w, float x ) { x = fabs(x - c); if( x>w ) return 0.0; x /= w; return 1.0 - x*x*(3.0-2.0*x); } |
Exponential Step
A natural attenuation is an exponential of a linearly decaying quantity: yellow curve, exp(-x). A gaussian, is an exponential of a quadratically decaying quantity: light green curve, exp(-x²). You can go on increasing powers, and get a sharper and sharper smoothstep(), until you get a step() in the limit.
float expStep( float x, float k, float n ) { return exp( -k*pow(x,n) ); } |
Gain
Remapping the unit interval into the unit interval by expanding the sides and compressing the center, and keeping 1/2 mapped to 1/2, that can be done with the gain() function. This was a common function in RSL tutorials (the Renderman Shading Language). k=1 is the identity curve, k<1 produces the classic gain() shape, and k>1 produces "s" shaped curces. The curves are symmetric (and inverse) for k=a and k=1/a.
float gain(float x, float k) { float a = 0.5*pow(2.0*((x<0.5)?x:1.0-x), k); return (x<0.5)?a:1.0-a; } |
k<1 |
k>1 |
Parabola
A nice choice to remap the 0..1 interval into 0..1, such that the corners are remapped to 0 and the center to 1. In other words, parabola(0) = parabola(1) = 0, and parabola(1/2) = 1.
float parabola( float x, float k ) { return pow( 4.0*x*(1.0-x), k ); } |
Power curve
A nice choice to remap the 0..1 interval into 0..1, such that the corners are remapped to 0. Very useful to skew the shape one side or the other in order to make leaves, eyes, and many other interesting shapes
float pcurve( float x, float a, float b ) { float k = pow(a+b,a+b) / (pow(a,a)*pow(b,b)); return k * pow( x, a ) * pow( 1.0-x, b ); }Note that k is chosen such that pcurve() reaches exactly 1 at its maximum for illustration purposes, but in many applications the curve needs to be scaled anyways so the slow computation of k can be simply avoided. |
Sinc curve
A phase shifter sinc curve can be useful if it starts at zero and ends at zero, for some bouncing behaviors (suggested by Hubert-Jan). Give k different integer values to tweak the amount of bounces. It peaks at 1.0, but that take negative values, which can make it unusable in some applications.
float sinc( float x, float k ) { const float a = PI * ((float(k)*x-1.0); return sin(a)/a; } |