/* * 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 { public: 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; 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 { // 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; } } 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; } // 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) { float n = 0; float amp = 1.0f; while (i > 0) { n += amp; amp *= 0.5; i--; } return n; } };