Skip to content
Snippets Groups Projects
rings.h 8.99 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * 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;
    
        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;
    
            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;
    
    
                    // 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;
        }
    };