2006年03月09日
川俣晶の縁側ソフトウェア技術雑記 total 5176 count

描画システムの座標変換機能と、ビットマップ、文字描画の制約の思い出

Written By: 川俣 晶連絡先

 99%ある種のノスタルジーの話題です。

座標系変形の昔話 §

 マイコン/パソコン系の最も最初のグラフィック描画システムは、おそらくクリッピングの機能すらろくに持たない極めて貧弱なものだったと思います。それこそ、「画面を消す」「点を打つ」「点を消す」ぐらいの機能しかないものです。

 これが、徐々に進化を始め、16bit版Windowsの描画システムであるGDIでは、クリッピングができるだけでなく、座標系のスケールを自由に取れるようになりました。つまり画面上の1ピクセルが指定座標数値のどれぐらいに対応するかが自由に指定できるようになったわけです。これによって、描画データが持つ本来の数値を渡しつつ、画面上では適切なピクセル数で描くということが可能になりました。(いやまて、N88-BASICぐらいで、既にウィンドウやビューポートの機能を持っていて可能だったか?)

 これはX座標とY座標について独立したスケーリングを指定できるために、わざと扁平にひしゃげるように描かせることも可能でした。

 更にこれが16bit版のOS/2(当時はWindowsの後継OS)になると、座標変換行列が指定可能になり、それこそ好きなように座標系を変形させることができたわけです。

 ちなみに、この「座標変換行列が指定可能」という機能を知ったときには痺れましたねぇ。本当にまともなスピードで処理できるのか疑問に思いましたが、機能としては夢があって魅力があると思いました。

昔の制約 §

 もう確かな記憶ではありませんが。

 これらの座標系の変形の機能は、直線などベクトル系の描画では機能したものの、文字の描画とビットマップの描画では動作しなかったという記憶があります。

 ベクトルデータは座標値を演算するだけで変形に対応できますが、高速描画のために特殊な構造になっていて、デバイスドライバレベルで描かれる文字と、データ量が大きく、これまた特殊な構造で処理されるビットマップは、座標系の変換を指定しても、機能してくれなかったように思います。

 それゆえに、実は「座標変換行列が指定可能」というような機能はあまり使い勝手が良くなかったという印象があります。

そして制約からは解放された §

 .NET Framework(あるいはGDI+?)は、このような制約をスカッと飛ばして、変換行列によって文字もビットマップも変形させることができます。

 というわけで、テストプログラムを作ってみました。

 以下は画面のスナップショットです。

画面スナップショット

 プログラム本体は、ノータッチデプロイメントですぐ実行して試せるようになっています。

 以下のリンクをクリックしてみてください。

回転サンプルプログラム .NET Framework 2.0要 ノータッチデプロイメント

 いや~、ほんとに。回転する画像と文字を見るのは、ある種のトラウマから解放された快感ですね。

 とはいえ、本当にこれで良いのは分かりませんが。

例外という問題 §

 実は実行させると、しばしば例外が発生します。

 この例外はテキストボックスに記録するように作成しました。

 ArgumentExceptionは、どのようなケースで出るのかドキュメントに記述が無く、詳細不明です。何となく不都合があるパラメータがあるのだろうということは推測できますが、どこからどこまでの値に不都合が出るのか、詳細不明です。

 OutOfMemoryExceptionの方は、そもそも出る理由が不明です。メモリ不足ということはありませんし、そのまま継続して次のフレームの描画に成功していることから考えて、何らかのシステムリソース不足とも思えません。

テストプログラム主要部 §

 Visual Studio 2005のC#によるWindowsアプリケーションのプロジェクトです。

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

namespace RotateTransform001

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        private float param1 = 0.0f;

        private float param2 = 0.0f;

        private float param3 = 0.0f;

        private float param4 = 0.0f;

        private float param5 = 0.0f;

        private float param6 = 0.0f;

        private void pictureBox1_Paint(object sender, PaintEventArgs e)

        {

            try

            {

                int centerX = this.pictureBox1.Width / 2;

                int centerY = this.pictureBox1.Height / 2;

                using (System.Drawing.Drawing2D.Matrix transformMatrix = 

                                        new System.Drawing.Drawing2D.Matrix(

                    (float)Math.Cos(param3 / 360.0f * Math.PI),

                    (float)Math.Sin(param4 / 360.0f * Math.PI),

                    -(float)Math.Sin(param5 / 360.0f * Math.PI),

                    (float)Math.Cos(param6 / 360.0f * Math.PI), 0, 0

                    ))

                {

                    e.Graphics.TranslateTransform(centerX, centerY);

                    e.Graphics.MultiplyTransform(transformMatrix);

                    e.Graphics.DrawRectangle(Pens.Blue, 0, 0, 10, 10);

                    e.Graphics.DrawImage(this.pictureBox2.Image, 

                        -this.pictureBox2.Image.Width / 2,

                        -this.pictureBox2.Image.Height / 2);

                    e.Graphics.ResetTransform();

                    e.Graphics.TranslateTransform(centerX, centerY);

                    e.Graphics.MultiplyTransform(transformMatrix);

                    const string message = "ROTATING";

                    e.Graphics.DrawString(message, new Font("Arial", 20.0f), 

                                     Brushes.Black, new PointF(0.0f, 0.0f));

                    e.Graphics.DrawString(message, new Font("Arial", 20.0f), 

                                   Brushes.Magenta, new PointF(1.0f, 1.0f));

                }

            }

            catch (System.Exception ex)

            {

                this.label2.Text = (int.Parse(this.label2.Text) + 1).ToString();

                this.textBox1.Text = DateTime.Now.ToString() + ":\r\n" 

                            + ex.ToString() + "\r\n\r\n" + this.textBox1.Text;

            }

        }

        private void timer1_Tick(object sender, EventArgs e)

        {

            param1 = (param1 + 5.0f);

            param2 = (param2 - 4.0f);

            param3 = (param3 + 3.5f);

            param4 = (param4 + 3.0f);

            param5 = (param5 + 2.5f);

            param6 = (param6 + 2.0f);

            this.pictureBox1.Invalidate();

        }

    }

}

あっ! §

 サンプルソース、最後にいろいろいじったら、凄く無駄だらけのコードになってる。

 取っても良いコードや、何の機能も持っていない行が……。

 そこは見なかったことにしてください (汗。