diff --git a/examples/cpp/data/sky.png b/examples/cpp/data/sky.png
new file mode 100644
index 0000000000000000000000000000000000000000..587b869486d85ed42389db7a31a33255bb4f9654
Binary files /dev/null and b/examples/cpp/data/sky.png differ
diff --git a/examples/cpp/rings.cpp b/examples/cpp/rings.cpp
index 78076077a0eb12c7cd6bb0b14344cdab364309c5..d15b7aa9be801d807aea3a367f9137db9f46665b 100644
--- a/examples/cpp/rings.cpp
+++ b/examples/cpp/rings.cpp
@@ -1,6 +1,8 @@
 /*
  * 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.
  *
@@ -8,80 +10,124 @@
  */
 
 #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 Rings : public Effect
 {
 public:
     Rings()
-        : d(0, 0, 0, 0) {}
+        : palette("data/sky.png"),
+          d(0, 0, 0, 0),
+          threshold(-1.0)
+    {
+        srand(time(0));
+        seed = fmod(rand() / 1e3, 1e3);
+    }
 
     static const float xyzSpeed = 0.6;
     static const float xyzScale = 0.08;
     static const float wSpeed = 0.2;
-    static const float wRate = 0.001;
+    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.8;
-    static const float hueScale = 5.0;
-    static const float hueRate = 0.001;
+    static const float wanderSize = 1.2;
+    static const float brightnessContrast = 15.0;
+    static const float colorContrast = 2.0;
+    static const float targetBrightness = 0.1;
+    static const float thresholdGain = 0.1;
+
+    // Sample colors along a curved path through a texture
+    Texture palette;
+
+    // Pseudorandom number seed
+    float seed;
 
     // State variables
     Vec4 d;
+    float threshold;
 
     // Calculated once per frame
-    float hue, saturation;
     float spacing;
+    float colorParam;
+    float pixelTotal;
+    unsigned pixelCount;
     Vec3 center;
 
     virtual void beginFrame(const FrameInfo &f)
     {
-        hue = noise2(f.time * hueRate, 20.5) * hueScale;
-
-        saturation = sq(std::min(std::max(0.7f * (0.5f + noise2(f.time * 0.01, 0.5)), 0.0f), 1.0f));
         spacing = sq(0.5 + noise2(f.time * ringScaleRate, 1.5)) * ringScale;
 
         // Rotate movement in the XZ plane
-        float angle = noise2(f.time * 0.01, 30.5) * 10.0;
-        float speed = pow(fabsf(noise2(f.time * 0.01, 40.5)), 2.5) * xyzSpeed;
+        float angle = noise2(f.time * 0.01, seed + 30.5) * 10.0;
+        float speed = pow(fabsf(noise2(f.time * 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(f.time * wRate, 3.5) * wSpeed * f.timeDelta;
+        d[3] += noise2(f.time * wRate, seed + 3.5) * wSpeed * f.timeDelta;
 
         // Update center position
-        center = Vec3(noise2(f.time * wanderSpeed, 50.9),
-                      noise2(f.time * wanderSpeed, 51.4),
-                      noise2(f.time * wanderSpeed, 51.7)) * wanderSize;
-    }
+        center = Vec3(noise2(f.time * wanderSpeed, seed + 50.9),
+                      noise2(f.time * wanderSpeed, seed + 51.4),
+                      noise2(f.time * wanderSpeed, seed + 51.7)) * wanderSize;
 
-    virtual void debug(const DebugInfo &di)
-    {
-        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] hue = %f, saturation = %f\n", hue, saturation);
+        // Wander around the color palette
+        colorParam = seed + f.time * 0.05f;
+
+        // Reset pixel total accumulators, used for the brightness calc in endFrame
+        pixelTotal = 0;
+        pixelCount = 0;
     }
 
     virtual void calculatePixel(Vec3& rgb, const PixelInfo &p)
     {
         float dist = len(p.point - center);
         Vec4 pulse = Vec4(sinf(d[2] + dist * spacing) * ringDepth, 0, 0, 0);
-        Vec4 s = Vec4(p.point * xyzScale, 0) + d;
+        Vec4 s = Vec4(p.point * xyzScale, seed) + d;
         Vec4 chromaOffset = Vec4(0, 0, 0, 10);
 
-        float n = fbm_noise4(s + pulse, 4) * 2.5f + 0.1f;
-        float m = fbm_noise4(s + chromaOffset, 2);
+        float n = (fbm_noise4(s + pulse, 4) + threshold) * brightnessContrast;
+        float m = fbm_noise4(s + chromaOffset, 2) * colorContrast;
+
+        rgb = color(colorParam + m, sq(std::max(0.0f, n)));
 
-        hsv2rgb(rgb,
-            hue + 0.5 * m,
-            saturation,
-            std::min(0.9f, sq(std::max(0.0f, n)))
-        );
+        // Keep a rough approximate brightness total, for closed-loop feedback
+        for (unsigned i = 0; i < 3; i++) {
+            pixelTotal += sq(std::min(1.0f, std::max(0.0f, rgb[i])));
+            pixelCount++;
+        }
+    }
+
+    virtual void endFrame(const FrameInfo &f)
+    {
+        // Adjust threshold in brightness-determining noise function, in order
+        // to try and keep the average pixel brightness at a particular level.
+
+        if (pixelCount > 0) {
+            threshold += (targetBrightness - pixelTotal / pixelCount) * thresholdGain;
+        }
+    }
+
+    virtual void debug(const DebugInfo &di)
+    {
+        fprintf(stderr, "\t[rings] seed = %f\n", seed);
+        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);
+    }
+
+    virtual Vec3 color(float parameter, float brightness)
+    {
+        // Sample a color from our palette, using a lissajous curve within an image texture
+        return palette.sample( sinf(parameter) * 0.5f + 0.5f,
+                               sinf(parameter * 0.86f) * 0.5f + 0.5f) * brightness;
     }
 };