Newer
Older
/*
* Three-dimensional pattern in C++ based on the "Rings" Processing example.
*
* This version samples colors from an image, rather than using the HSV colorspace.
*
* Uses noise functions modulated by sinusoidal rings, which themselves
* wander and shift according to some noise functions.
*
* (c) 2014 Micah Elizabeth Scott
* http://creativecommons.org/licenses/by/3.0/
*/
#pragma once
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include "lib/color.h"
#include "lib/effect.h"
#include "lib/noise.h"
#include "lib/texture.h"
class RingsEffect : public Effect
RingsEffect(const char *palette)
: palette(palette)
{
reseed();
}
static const float xyzSpeed = 0.6;
static const float xyzScale = 0.08;
static const float wSpeed = 0.2;
static const float wRate = 0.015;
static const float ringScale = 1.5;
static const float ringScaleRate = 0.01;
static const float ringDepth = 0.2;
static const float wanderSpeed = 0.04;
static const float wanderSize = 1.2;
static const float brightnessContrast = 8.0;
static const float colorContrast = 4.0;
static const float targetBrightness = 0.1;
static const float thresholdGain = 0.1;
static const float thresholdStepLimit = 0.02;
static const float initialThreshold = -1.0f;
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
static const unsigned brightnessOctaves = 4;
static const unsigned colorOctaves = 2;
// Sample colors along a curved path through a texture
Texture palette;
// State variables
Vec4 d;
float timer;
float seed;
float threshold;
// Calculated once per frame
float spacing;
float colorParam;
float pixelTotalNumerator;
unsigned pixelTotalDenominator;
bool is3D;
Vec3 center;
virtual void beginFrame(const FrameInfo &f)
{
timer += f.timeDelta;
spacing = sq(0.5 + noise2(timer * ringScaleRate, 1.5)) * ringScale;
// Rotate movement in the XZ plane
float angle = noise2(timer * 0.01, seed + 30.5) * 10.0;
float speed = pow(fabsf(noise2(timer * 0.01, seed + 40.5)), 2.5) * xyzSpeed;
d[0] += cosf(angle) * speed * f.timeDelta;
d[2] += sinf(angle) * speed * f.timeDelta;
// Random wander along the W axis
d[3] += noise2(timer * wRate, seed + 3.5) * wSpeed * f.timeDelta;
// Update center position
center = Vec3(noise2(timer * wanderSpeed, seed + 50.9),
noise2(timer * wanderSpeed, seed + 51.4),
noise2(timer * wanderSpeed, seed + 51.7)) * wanderSize;
// Wander around the color palette
colorParam = seed + timer * 0.05f;
// Reset pixel total accumulators, used for the brightness calc in endFrame
pixelTotalNumerator = 0;
pixelTotalDenominator = 0;
// Is this 2D or 3D?
is3D = false;
for (Effect::PixelInfoIter i = f.pixels.begin(), e = f.pixels.end(); i != e; ++i) {
const Effect::PixelInfo &p = *i;
if (p.point[1] != 0.0f) {
is3D = true;
}
}
}
virtual void shader(Vec3& rgb, const PixelInfo &p) const
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
{
// Noise sampling location
Vec4 s = Vec4(p.point * xyzScale, seed) + d;
// Ring function, displaces the noise sampling coordinate
float dist = len(p.point - center);
Vec4 pulse = Vec4(sinf(d[2] + dist * spacing) * ringDepth, 0, 0, 0);
/*
* Brightness is calculated by:
*
* n = (fbm_noise4(s + pulse, octaves) + threshold) * brightnessContrast;
*
* But if we can determine that n <= 0, we can exit early. Check this after
* each fbm octave, to see if we can save another costly noise calculation.
* Also, use 3D noise instead of 4D if the Y axis is unused.
*/
float n = threshold * brightnessContrast;
float amplitude = brightnessContrast;
Vec4 arg = s + pulse;
unsigned i = brightnessOctaves;
while (true) {
n += amplitude * dNoise(arg);
--i;
if (!(n > -amplitude * fbmTotal(i))) {
// Too low for further octaves to bring back above 0.
// On the last octave, note fbmTotal(0) == 0
// Should also exit in case of NaN.
return;
}
if (!i) {
break;
}
amplitude *= 0.5f;
arg *= 2.0f;
}
n /= fbmTotal(brightnessOctaves);
/*
* Another hybrid 2D/3D fbm for chroma. Use half the octaves.
*/
float m = 0;
amplitude = colorContrast;
arg = s + Vec4(0, 0, 0, 10);
i = colorOctaves;
while (true) {
m += amplitude * dNoise(arg);
if (--i == 0) {
break;
}
amplitude *= 0.5f;
arg *= 2.0f;
}
m /= fbmTotal(colorOctaves);
// Assemble color using a lookup through our palette
rgb = color(colorParam + m, sq(n));
inline void postProcess(const Vec3& rgb, const PixelInfo& p)
{
// Keep a rough approximate brightness total, for closed-loop feedback
for (unsigned i = 0; i < 3; i++) {
pixelTotalNumerator += sq(std::min(1.0f, std::max(0.0f, rgb[i])));
pixelTotalDenominator++;
}
}
virtual void endFrame(const FrameInfo &f)
{
// Per-frame brightness calculations.
// Adjust threshold in brightness-determining noise function, in order
// to try and keep the average pixel brightness at a particular level.
float target = targetBrightness;
float current = pixelTotalDenominator ? pixelTotalNumerator / pixelTotalDenominator : 0.0f;
bool blackLevel = current <= 0.0f;
if (wantToReseed()) {
// Fade to black
target = 0;
if (blackLevel) {
// At black level. Reseed invisibly!
reseed();
}
}
// Rate limited servo loop.
// Disabled if we aren't calculating pixel values.
if (pixelTotalDenominator) {
float step = (target - current) * thresholdGain;
if (step > thresholdStepLimit) step = thresholdStepLimit;
if (step < -thresholdStepLimit) step = -thresholdStepLimit;
threshold += step;
}
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
}
virtual void debug(const DebugInfo &di)
{
fprintf(stderr, "\t[rings] %s model\n", is3D ? "3D" : "2D");
fprintf(stderr, "\t[rings] seed = %f%s\n", seed, wantToReseed() ? " [reseed pending]" : "");
fprintf(stderr, "\t[rings] timer = %f\n", timer);
fprintf(stderr, "\t[rings] center = %f, %f, %f\n", center[0], center[1], center[2]);
fprintf(stderr, "\t[rings] d = %f, %f, %f, %f\n", d[0], d[1], d[2], d[3]);
fprintf(stderr, "\t[rings] threshold = %f\n", threshold);
}
private:
// Totally reinitialize our state variables. We do this periodically
// during normal operation, during blank periods.
void reseed()
{
// Get okay seed mixing even with depressing rand() implementations
srand(time(0));
for (int i = 0; i < 50; i++) {
rand();
}
seed = rand() / double(RAND_MAX / 1024);
// Starting point
d = Vec4(0,0,0,0);
timer = 0;
// Initial threshold gives us time to fade in
threshold = initialThreshold;
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
}
// Do our state variables need resetting? This is like a watchdog timer,
// keeping an eye on the simulation parameters. If we need to start over,
// we'll start fading out and reseed during the darkness. This will happen
// periodically in order to keep our numbers within the useful resolution of
// a 32-bit float.
bool wantToReseed()
{
// Comparisons carefully written to NaN always causes reseed
return !(timer < 9000.0f) |
!(threshold < 10.0f) |
!(threshold > -10.0f) |
!(d[0] < 1000.0f) |
!(d[1] < 1000.0f) |
!(d[2] < 1000.0f) |
!(d[3] < 1000.0f) |
!(d[0] > -1000.0f) |
!(d[1] > -1000.0f) |
!(d[2] > -1000.0f) |
!(d[3] > -1000.0f) ;
}
// Sample a color from our palette, using a lissajous curve within an image texture
Vec3 color(float parameter, float brightness) const
{
return palette.sample( sinf(parameter) * 0.5f + 0.5f,
sinf(parameter * 0.86f) * 0.5f + 0.5f) * brightness;
}
// Sample 3 or 4 dimensional noise. If (!is3D), we use 3 dimensional noise, ignoring the Y axis.
float dNoise(Vec4 v) const
{
return is3D ? noise4(v) : noise3(v[0], v[2], v[3]);
}
// Normalization factor for fractional brownian motion with N octaves
static float fbmTotal(int i)