Tiny Planets Coding

The other day on Google+ I read a post from Ana Andres talking about coding Tiny Planets, and how it was running really slow in Matlab somewhere in the pixel drawing code.

That got me thinking about how I would code it in C#, and Ana had a good blog post showing one way of thinking about how the transformation was happening , one of her goals was to make an animated GIF so people could see the wrap bend happening.

Tiny Planet Code A
Tiny Planet Code A
Tiny Planet Code A GIF

Now the this was plenty fast enough, I was getting 2-3 fps on my laptop using only one core, so from a speed perspective moved on.

My first plan was to interpolate between the points (all though that would really slow, and look bad at the outside), but when I showed my co-worker my method, he said I was silly and should just traverse the output space, and find the input pixel that maps, so I did that and got:

Tiny Planet Code C
Tiny Planet Code C

I then had to put the bend code back in, as that was a special aspect needed for the animation and got:

Tiny Planet Code D GIF

I then started playing with sub-sampling to make the output less jaggie (F), and then I decided blend in YUV (or YCrCb) colour space, and finally settled on five point YUV average where you have a center of the pixel and the four 1/4 towards the pixel diagonal corners. Giving:

Tiny Planet Code H GIF

For now the GIF’s at coverted to videos, but if they are to slow I will have to go back to like links

Now improvement on the process would be to allow scaling and translation of the origin on the polar focus, this might be useful in an interactive UI to allow exploring the space.

Here’s my source code for Step H, it was fun to learn to make GIFs in C#.

The full source is part of my Old School Graphics repo on GitHub

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Windows.Media.Imaging;
using System.Windows;
using System.IO;

namespace TinyPlanet
{
    class Program
    {
        static void Main(string[] args)
        {
            Bitmap src = new Bitmap(@"C:\temp\tinyplanet-1.png");

            GifH(src);
        }

        static void GifH(Bitmap src)
        {
            GifBitmapEncoder gEnc = new GifBitmapEncoder();

            for (int i = 0; i <= 100; i++)
            {
                Bitmap dst = Bend_H(src, i);
                Debug.WriteLine(string.Format("frame {0}", i));

                var frame = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
                        dst.GetHbitmap(),
                        IntPtr.Zero,
                        Int32Rect.Empty,
                        BitmapSizeOptions.FromEmptyOptions());
                gEnc.Frames.Add(BitmapFrame.Create(frame));
            }

            gEnc.Save(new FileStream(@"C:\temp\tinyplanet-out_H.gif", FileMode.Create));
        }

        static Bitmap Bend_H(Bitmap src, int bend_i)
        {
            int src_half_width = src.Width / 2;
            int dst_width = (src.Height * 2) + src.Width;
            int dst_height = src.Height * 2;
            int dst_origin_x = dst_width / 2;
            int dst_origin_y = dst_height / 2;

            double bend = bend_i / 100.0; // turn to percentage;


            int bend_x = (int)(src_half_width * (1.0 - bend));
            double bent_pixels = src_half_width - bend_x;
            double final_ang_d = 180.0 * bend;
            double final_ang = final_ang_d * (Math.PI / 180.0);


            Bitmap dst = new Bitmap(dst_width, dst_height);
            Graphics g = Graphics.FromImage(dst);
            g.Clear(Color.White);
            g.DrawImage(dst, 0, 0, dst.Width, dst.Height);

            int bend_start_x = dst_origin_x - bend_x;
            int bend_end_x = dst_origin_x + bend_x;

            for (int x = 0; x < dst_width; x++)
            {
                int fix_x = (x < dst_origin_x ? bend_x : -bend_x);
                int ox = (x - bend_start_x) + (src_half_width - bend_x);

                for (int y = 0; y < dst_height; y++)
                {
                    if (x > bend_start_x &&
                        x < bend_end_x)
                    {
                        // rectilinear
                        if (y < src.Height)
                        {
                            dst.SetPixel(x, y, src.GetPixel(ox, y));
                        }
                    }
                    else
                    {
                        // map from output to input
                        int dx = x - dst_origin_x + fix_x;
                        int dy = y - dst_origin_y;

                        int p0 = ARGBAtPoint(dx - 0.25, dy - 0.25, final_ang, ref src, bent_pixels, fix_x);
                        int p1 = ARGBAtPoint(dx - 0.25, dy + 0.25, final_ang, ref src, bent_pixels, fix_x);
                        int p2 = ARGBAtPoint(dx + 0.25, dy - 0.25, final_ang, ref src, bent_pixels, fix_x);
                        int p3 = ARGBAtPoint(dx + 0.25, dy + 0.25, final_ang, ref src, bent_pixels, fix_x);
                        int p4 = ARGBAtPoint(dx, dy, final_ang, ref src, bent_pixels, fix_x);

                        int yy, cr, cb;
                        int yt = 0, crt = 0, cbt = 0;
                        YCbCrFromRGB(p0, out yy, out cr, out cb);
                        yt += yy; crt += cr; cbt += cb;
                        YCbCrFromRGB(p1, out yy, out cr, out cb);
                        yt += yy; crt += cr; cbt += cb;
                        YCbCrFromRGB(p2, out yy, out cr, out cb);
                        yt += yy; crt += cr; cbt += cb;
                        YCbCrFromRGB(p3, out yy, out cr, out cb);
                        yt += yy; crt += cr; cbt += cb;
                        YCbCrFromRGB(p4, out yy, out cr, out cb);
                        yt += yy; crt += cr; cbt += cb;

                        dst.SetPixel(x, y, Color.FromArgb(ARGBFromYCbCr(yt / 5, cbt / 5, crt / 5)));
                    }
                }
            }

            return dst;
        }

        static void YCbCrFromRGB(int RGB, out int Y, out int Cb, out int Cr)
        {
            int r = (RGB >> 16) & 0xFF;
            int g = (RGB >> 8) & 0xFF;
            int b = RGB & 0xFF;
            Y = (int)(0.299 * r + 0.587 * g + 0.114 * b);
            Cb = (int)(128 - 0.169 * r - 0.331 * g + 0.500 * b);
            Cr = (int)(128 + 0.500 * r - 0.419 * g - 0.081 * b);
        }

        static int ARGBFromYCbCr(int Y, int Cb, int Cr)
        {
            int r = Y + (int)((1.4f * (Cb - 128))+0.5);
            int g = Y + (int)((-0.343f * (Cr - 128) - 0.711f * (Cb - 128))+0.5);
            int b = Y + (int)((1.765f * (Cr - 128)) + 0.5);
            return (int)(0xFF000000 + (r << 16) + (g << 8) + (b));
        }


        static int ARGBAtPoint(double dx,
                double dy,
                double final_ang,
                ref Bitmap src,
                double bent_pixels,
                int fix_x)
        {
            double r = Math.Sqrt((dx * dx) + (dy * dy));
            double q = Math.Atan2(dy, dx);

            double pic_ang = q + (Math.PI / 2.0);
            double mod_ang = ((pic_ang + Math.PI) % (Math.PI * 2.0)) - Math.PI;

            if (Math.Abs(mod_ang) <= final_ang)
            {
                int dev_x = (int)((mod_ang / final_ang) * bent_pixels) - fix_x + (src.Width/2);
                int dev_y = (int)r;

                if (dev_x < src.Width && dev_x >= 0 &&
                    dev_y < src.Height)
                {
                    return src.GetPixel(dev_x, src.Height - dev_y - 1).ToArgb();
                }
            }

            return Color.White.ToArgb();
        }
    }

}