Reaction Diffusion using WriteableBitmap

For a while, I’ve been experimenting with reaction diffusion to provide for more “life like” surfaces. This reaction diffusion algorithm spreads colors out for a diffuse effect. To ensure responsiveness in the UI, the diffusion logic is run in a timer thread that notifies the UI thread to re-do the WriteableBitmap (texture) when it is done. As for performance, 300 by 300 is about all the algorithm can handle on my rather beefy machine before visible artifacts begin to show up. Multi-threading through ParallelFor doesn’t seem to help, but I’ll keep working on it to see if we can get a bigger buffer. Here is a sample of what the diffusion looks like:

image

I’ve done this using RenderTargetBitmap and a pixel shader before, however the behavior of diffusion was very difficult to control, using WriteableBitmap is more manageable. Code below, .NET 3.5 SP1 is required to run this program.

DiffusionBuffer.cs:

 using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace PhysicsLib {
  public class DiffusionBuffer : Canvas {
    private readonly System.Windows.Media.Imaging.WriteableBitmap texture;
    private byte[] bufferA;
    private byte[] bufferB;
    private readonly int stride_t;
    private readonly int PixelWidth;
    private readonly int PixelHeight;
    private readonly Timer timer;
    public DiffusionBuffer(Vector size, int updateTime) {
      this.Width = size.X;
      this.Height = size.Y;
      this.texture = new System.Windows.Media.Imaging.WriteableBitmap((int)size.X, (int)size.Y, 96, 96, PixelFormats.Bgra32, null);
      int bytesPerPixel_t = (texture.PixelWidth) / 8;//  bytesPerPixel=  25=200/8
      stride_t = texture.PixelWidth * bytesPerPixel_t;//  stride=  5000=200*25 
      int arraySize_t = stride_t * texture.PixelHeight;//  arraysize=  1000000=5000*200
      bufferA = new byte[arraySize_t];
      bufferB = new byte[arraySize_t];

      for (int ij = 0; ij < texture.PixelWidth * texture.PixelHeight; ij++) {
        int i = ij % (texture.PixelWidth);
        int j = ij / texture.PixelWidth;

        bufferA[(i * 4) + (j * stride_t) + 0] = 000;
        bufferA[(i * 4) + (j * stride_t) + 1] = 000;
        bufferA[(i * 4) + (j * stride_t) + 2] = 000;
        bufferA[(i * 4) + (j * stride_t) + 3] = 255;
      }
      {
        var rect0 = new Int32Rect(0, 0, (int)texture.PixelWidth, (int)texture.PixelHeight);
        texture.WritePixels(rect0, bufferA, stride_t, 0);
      }
      {
        var image = new Image() { Source = texture };
        Canvas.SetLeft(image, 0);
        Canvas.SetTop(image, 0);
        this.Children.Add(image);
      }
      this.PixelHeight = texture.PixelHeight;
      this.PixelWidth = texture.PixelWidth;


      timer = new Timer(new TimerCallback((x) => ((DiffusionBuffer)x).updateA()), this, updateTime, updateTime);
    }
    private int getPx(int i, int j, int k, int def) {
      if (i < 0 || i >= PixelWidth) return def;
      if (j < 0 || j >= PixelHeight) return def;

      return bufferA[(i * 4) + (j * stride_t) + k];
    }
    private byte getPx0(int i, int j, int sp, int k) {
      var at = bufferA[(i * 4) + (j * stride_t) + k];
      long value = 0;
      value += getPx(i - sp, j - 00, k, at);
      value += getPx(i + sp, j + 00, k, at);
      value += getPx(i - 00, j - sp, k, at);
      value += getPx(i + 00, j + sp, k, at);

      var tp = sp - 1;
      value += getPx(i - tp, j - tp, k, at);
      value += getPx(i + tp, j + tp, k, at);
      value += getPx(i + tp, j - tp, k, at);
      value += getPx(i - tp, j + tp, k, at);
      var ret = (value / 7.97);
      if (ret > 255) ret = 255;
      return (byte)ret;
    }
    private delegate void DummyDelegate(DiffusionBuffer buffer);

    private void updateA() {
      for (int ij = 0; ij < PixelWidth * PixelHeight; ij++) {
        int i = ij % (PixelWidth);
        int j = ij / PixelWidth;

        bufferB[(i * 4) + (j * stride_t) + 3] = bufferA[(i * 4) + (j * stride_t) + 3];
        bufferB[(i * 4) + (j * stride_t) + 0] = getPx0(i, j, 3, 0);
        bufferB[(i * 4) + (j * stride_t) + 1] = getPx0(i, j, 3, 1);
        bufferB[(i * 4) + (j * stride_t) + 2] = getPx0(i, j, 3, 2);
      }
      this.Dispatcher.BeginInvoke(new DummyDelegate((buffer) => buffer.updateB()), this);
    }
    public void rectangle(Point p, Vector size, Color c) {
      var e = p + size;
      for (int i = (int)p.X; i < (int)e.X; i++) {
        for (int j = (int)p.Y; j < (int)e.Y; j++) {
          add(bufferB, (i * 4) + (j * stride_t) + 0, c.B);
          add(bufferB, (i * 4) + (j * stride_t) + 1, c.G);
          add(bufferB, (i * 4) + (j * stride_t) + 2, c.R);
        }
      }
    }
    private static void add(byte[] buf, int idx, byte value) {
      if (idx < 0 || idx >= buf.Length) return;
      int x = buf[idx];
      if (x < 0) x = 255 + x;
      x += value;
      if (x > 255) x = 255;
      buf[idx] = (byte) x;
    }

    public virtual void updateB() {
      var rect0 = new Int32Rect(0, 0, (int)texture.PixelWidth, (int)texture.PixelHeight);
      texture.WritePixels(rect0, bufferB, stride_t, 0);
      var old = bufferA;
      bufferA = bufferB;
      bufferB = old;
    }
  }
}

Program.cs:

 using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace TestForWriteablebitmap {

  public class MyDiffusionBuffer : PhysicsLib.DiffusionBuffer {
    public readonly Thumb[] thumbs = new Thumb[4];
    public readonly Color[] colors = { Colors.Red, Colors.Green, Colors.Blue, Colors.White };
    public MyDiffusionBuffer()
      : base(new Vector(200,200), 15) {

      for (int i = 0; i < thumbs.Length; i++) {
        thumbs[i] = new Thumb() { Background = new SolidColorBrush() { Color = colors[i] }, Width = 10, Height = 10 };
        Canvas.SetZIndex(thumbs[i], 1);
        thumbs[i].DragDelta += (x, y) => {
          Canvas.SetLeft(((Thumb)x), Canvas.GetLeft((Thumb)x) + y.HorizontalChange);
          Canvas.SetTop(((Thumb)x), Canvas.GetTop((Thumb)x) + y.VerticalChange);
        };
        Canvas.SetLeft(thumbs[i], 0);
        Canvas.SetTop(thumbs[i], 0);
        Children.Add(thumbs[i]);
      }
    }
    public override void updateB() {
      foreach (var t in thumbs)
        rectangle(new Point(Canvas.GetLeft(t), Canvas.GetTop(t)), 
          new Vector(t.Width, t.Height), ((SolidColorBrush)t.Background).Color);
      base.updateB();
    }
  }
  class Program {
    [STAThread]
    static void Main(string[] args) {
      var buf = new MyDiffusionBuffer();
      buf.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2, CenterX = 150, CenterY = 150 };
      var win = new Window() { Content = buf };
      Application app = new Application();
      app.Run(win);
    }
  }
}