シェルピンスキーのクリスマスツリー(前編)

こんにちは、きうちです。

速いものでもう年末ですね!

お仕事をなさっている方の中にはまもなく仕事納め、という方も多いのではないかと思います。

その前にクリスマスですね!?

なんだかんだと世間もクリスマス一色。クリスマスソングなんかもそこかしこでかかり、

クリスマス気分を嫌が上にも盛り上げますね!

今回は、そんな盛り上げにいっちょかんでみようかと思います。

結論を言うと、「クリスマスツリーを描くプログラム」をご紹介します!

・・・ですが、ただ描いても面白くないので・・・

私が以前から構想していた「シェルピンスキーのギャスケットを使ってクリスマスツリーを描く」をやります^^

★ ★ ★

シェルピンスキーのギャスケットというのはフラクタル図形(自己相似図形)の一種で、
三角の中に一定のルールで三角を書く、というのを繰り返していって描画する図形で、
ポーランドの数学者”ヴァツワフ・シェルピンスキ”にちなんで名づけられています。

雑ですいませんが、図にするとこんな感じ。

ツリーを作る前に、まずは単純に1つのシェルピンスキーのギャスケットを描かせてみましょう。

言語は例によってC#にします。Windowsフォームアプリとして作成します。

画面サイズは1024×768にします。

背景は黒にします。

また、フォームのPaintイベントメソッドを作っておきます。

再帰レベルとして maxLevel というフィールドを用意して5に。

また、3頂点のデータを作っておきます。なんとなく正三角形っぽくなるように適当に。

なお、コンストラクタはデフォルトのまま弄る必要はないので、ここに書くのは略します。

private int maxLevel = 5; // 再帰レベル

private Point[] pp = { // 3頂点のデータ
    new Point(512, 30),
    new Point(768, 430),
    new Point(256, 430),
};

続いて画面描画処理。先ほど作って置いたPaintイベントメソッドに追加をします。

private void Form1_Paint(object sender, PaintEventArgs e) // 画面描画処理
{
    var g = e.Graphics;

    DrawGasket(pp, g, new Pen(Color.Lime, 2), 0); // 一番外の三角形を描く
    DrawSGasket(pp, e.Graphics, new Pen(Color.Lime, 2), 0); // 中の三角形を描く
}

ここで DrawGasketは三角形を描くためのメソッドであり、
DrawSGasket は中の三角形を描くためのメソッドです。以下のような感じにします。

private void DrawGasket(Point[] pp, Graphics g, Pen pen, int level) // 三角形を描く
{
    g.DrawPolygon(pen, pp);
}

private void DrawSGasket(Point[] pp, Graphics g, Pen pen, int level) // シェルピンスキーのギャスケットを描く
{
    if (level == maxLevel) return; // 指定した再帰レベルに到達したら何もせずreturn

    // 3辺のそれぞれの中点を求める
    Point[] mp = new Point[3];
    for (int i = 1; i <= pp.Length; i++)
    {
        int idx1 = i - 1;
        int idx2 = i == pp.Length ? 0 : i;
        mp[i - 1] = GetMiddlePoint(pp[idx1], pp[idx2]);
    }

    DrawGasket(mp, g, pen, level);

    // 3頂点と中点を基に新たな三角形を3つ定義する            
    Point[] pp1 = { pp[0], mp[0], mp[2] };
    Point[] pp2 = { mp[0], pp[1], mp[1] };
    Point[] pp3 = { mp[2], mp[1], pp[2] };

    // 定義した新たな三角形でシェルピンスキーのギャスケットを描く
    DrawSGasket(pp1, g, pen, level + 1);
    DrawSGasket(pp2, g, pen, level + 1);
    DrawSGasket(pp3, g, pen, level + 1);
}

DrawGasket は単純に DrawPolygon を Graphics のオブジェクトで呼び出しているだけです。
そして、DrawSGasket は先ほど図解した内部の三角形の座標を計算し、
まずは中点の3点で三角形を描いた後、3つの新たな三角形について自身を再帰呼出しします。

この中で中点を求めるメソッド GetMiddlePoint が出てきますが、以下のようにします。

private Point GetMiddlePoint(Point p1, Point p2) // 中点を求める
{
    return new Point(CalcAverage(p1.X, p2.X), CalcAverage(p1.Y, p2.Y));
}

private int CalcAverage(int v1, int v2) // 2値の平均を求める
{
    return (int)((v1 + v2) / 2.0);
}

ここまで書いたら実行させてみましょう。

描かれましたね!

★ ★ ★

次はこれを、こんな感じに並べます。相変わらず図が雑で申し訳ない。

プログラムは以下の通りです。今回は一気に載せますね。説明は次回やります。

class Part // 絵のパーツを定義するためのデータクラス
{
    public Point[] PP { get; set; } // 3頂点

    public Pen Pen { get; set; } // ベースの色

    public bool WithSnow { get; set; } = true; // 雪をまとうか

    public Part(Point[] pp, Pen pen, bool withSnow) // コンストラクタ
    {
        PP = pp;
        Pen = pen;
        WithSnow = withSnow;
    }
}

public partial class Form1 : Form // フォームクラス
{
    private List parts = new List(); // パーツのリスト

    private int maxLevel = 5; // 再帰レベル

    private Random random = new Random(); // 乱数発生用

    private System.Drawing.Font font = new System.Drawing.Font("Comic Sans MS", 64); // 文字描画フォント

    public Form1() // コンストラクタ
    {
        InitializeComponent();

        MakeData(); // データ作成処理呼出し
    }

    private void MakeData() // データ作成
    {
        for (int i = 0; i <= 450; i += 150) // ▲を縦に4つ作る
        {
            parts.Add(new Part(MakeData(i), new Pen(Brushes.Green, 3), true));
        }

        // 最後の1つはちょっと幅を縮めて、色を茶色にする。雪は無しにする。
        Part last = parts[parts.Count - 1];
        last.PP[1].X = 612;
        last.PP[2].X = 412;
        last.Pen = new Pen(Color.FromArgb(100, 50, 0), 3); // 色を焦げ茶色に変更…余談ですが、これを Pens.DarkGoldenrod にすると何か松っぽくなります^^
        last.WithSnow = false; // 幹には雪はまとわせないでおく

        // 雪原
        for (int y = Height - 32; y < Height; y += 16)
        {
            for (int x = 0; x < Width; x += 32)
            {
                parts.Add(new Part(MakeData2(x, y), new Pen(Color.White, 3), false));
            }
        }
    }

    private Point[] MakeData(int i) // データ作成(木)
    {
        return [
            new Point(512, 30 + i),
            new Point(768, 230 + i),
            new Point(256, 230 + i),
        ];
    }

    private Point[] MakeData2(int x, int y) // データ作成(雪原)
    {
        return [
            new Point(x, y - 64),
            new Point(x + 32, y - 1),
            new Point(x - 32, y - 1),
        ];
    }

    private void Form1_Paint(object sender, PaintEventArgs e) // 画面描画処理
    {
        var g = e.Graphics;

        // パーツを描く
        foreach (Part p in parts)
        {
            DrawGasket(g, p.PP, p.Pen!, 0);
            DrawSGasket(g, p.PP, p.Pen!, p.WithSnow, 0);
        }

        // 文字を描く
        g.DrawString("Merry Christmas!", font, Brushes.Red, 148, Height / 2 - 112);
        g.DrawString("Merry Christmas!", font, Brushes.Yellow, 140, Height / 2 - 120);
    }

    private void DrawSGasket(Graphics g, Point[] pp, Pen pen, bool withSnow, int level) // シェルピンスキーのギャスケットを描く
    {
        if (level == maxLevel) return; // 指定した再帰レベルに到達したら何もせずreturn

        // 3辺のそれぞれの中点を求める
        Point[] mp = new Point[3];
        for (int i = 1; i <= pp.Length; i++)
        {
            int idx1 = i - 1;
            int idx2 = i == pp.Length ? 0 : i;
            mp[i - 1] = GetMiddlePoint(pp[idx1], pp[idx2]);
        }
        DrawGasket(g, mp, pen, level);

        // 3頂点と中点を基に新たな三角形を3つ定義する
        Point[] pp1 = { pp[0], mp[0], mp[2] };
        Point[] pp2 = { mp[0], pp[1], mp[1] };
        Point[] pp3 = { mp[2], mp[1], pp[2] };

        // 定義した新たな三角形でシェルピンスキーのギャスケットを描く
        DrawSGasket(g, pp1, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
        DrawSGasket(g, pp2, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
        DrawSGasket(g, pp3, random.Next(0, 100) < 30 && withSnow ? Pens.White : pen, withSnow, level + 1);
    }

    private void DrawGasket(Graphics g, Point[] pp, Pen pen, int level) // 三角形を描く
    {
        g.DrawPolygon(pen, pp);
    }

    private Point GetMiddlePoint(Point p1, Point p2) // 中点を求める
    {
        return new Point(CalcAverage(p1.X, p2.X), CalcAverage(p1.Y, p2.Y));
    }

    private int CalcAverage(int v1, int v2) // 2値の平均を求める
    {
        return (int)((v1 + v2) / 2.0);
    }
}

実行するとこんな感じ。

メリークリスマス!

★ ★ ★

というわけで、今回はここまでです。

次回は、このプログラムの説明をお伝えします^^

それでは!