2010.12. 3.
石立 喬

Visual C++ 2010 Expressの易しい使い方(20)

―――― 三次元グラフで隠線処理を行う ――――

概要
 三次元グラフは、z=f(x,y)で与えられる関数を分かりやすく見せてくれる。さきに「易しい使い方(13)」で取り上げたものを一般化して、色々なグラフを描画するのに便利なようにした。

三次元グラフをパソコン画面上に二次元座標で表す方法
 すでに、「易しい使い方(13)」で述べたように、図1に示すように、z軸からφの角度を持ち、x軸からθの角度を持つ無限遠の視点(無限遠とすると式が簡単になる)から絶対座標(x,y,z)で示された座標上の一点を見た(投影した)とし、そのときの二次元表示をpoint.Xとpoint.Yとすると下式の関係で表すことができる(Point構造体のpoint.Xとpoint.Yを使用)。


図1 三次元座標をパソコン画面上に二次元で表すための変換


 上の式の変換のために、関数 changeTo2D(x, y, z)を用意(画面上の原点位置X0とY0を加味している)した。グラフの色付けには実数の引数、ワイヤーフレームには整数の引数を用いるので、両者ともに使用できるように、オーバーロードしてある。

三次元グラフの作り方
 三次元グラフは、広く使われているので、目にする機会が多いと思われる。多くはワイヤーフレーム方式と呼ばれるもので、一定間隔で関数値を計算し、それらの点を結ぶ。この方式は、線に途切れが生じることが無く、計算回数が少なく高速である長所がある。しかし、この方法は、ワイヤーの本数が少ないと、細かい変化が表示できなくなり、多すぎるとかえって見にくくなる。
 ここで述べる三次元グラフは、比較的細かい間隔で関数値を計算し、それらの点をそのままプロットし、着色によっておおよその関数値を知らせると同時に、やや粗い間隔でのワイヤーフレーム方式と併用する。

隠線処理
 三次元グラフをパソコン上の二次元画面に表示しようとすると、手前の図形によって隠れるべき部分が生じる。隠れるべき部分を描画しないテクニックが隠線処理である。最も一般的な方法は、グラフを手前の見える部分から順に描画し、パソコン画面上のy軸(垂直)方向の最大値(y軸では下方に向かって)と最小値(y軸では上方に向かって最大)を蓄えておき、次に描画するグラフのプロット位置がその最小値より小さい場合は隠れていないので描画し、最小値を更新する。大きい場合は隠れているので、描画しない。ただし、次に描画するグラフのプロット位置がそれまでの最大値よりも大きい場合には、下から見えている(裏側が見えている)ので描画し、最大値を更新する。
 最大値と最小値を格納するメモリとして、ymax[i]とymin[i]を用意する。ただし、iはパソコン画面上で使用するi(水平)方向のピクセル値である。ymax[i]とymin[i]はy(垂直)方向のピクセル値である。
 ワイヤーフレームのように間隔を置いて描画される線に対する隠線処理は工夫を要する。実際に線が描かれていない部分に対しても隠線処理が必要だからである。したがって、ここでは、線を引かない間隙部分に対しても関数値の計算を行い、ymax[i]とymin[i]の更新を行っている。

プログラムの概要
 方向角θと天頂角φは、THETA、PHIにより固定的に決めてあるが、必要に応じ変更できる。座標変換に頻繁に使用されるsinθ、sinφ、cosθ、cosφは、あらかじめ計算してSINT、SINP、COST、COSPとして保存しておき、演算時間の短縮を図る。
 カラー表示の場合の処理を、図2に示す。グラフを描くための2変数関数は、zz=function(xx,yy)の形で与えられ、ここで使用される数値は、-100から100までの値で、ピクセルには対応しない論理空間上の実数値である。
 表示しようとする三次元の値(二次元化の前のもの、方向角θがゼロの場合や天頂角φがゼロの場合などは、そのままパソコン上のピクセル値となる)はx、y、zであり、ピクセルに対応する物理空間上の値である。パソコン画面上の表示位置はx、y、zを二次元化して使用し、着色はzzに基づいて実施する。
 ワイヤーフレーム表示の場合は、実数x、yの変わりに整数xp、ypを使用する。


図2 カラー表示のために使用する各変数と変数間の演算


 三次元グラフの元になる関数の変数値をスキャンしてグラフを描くのが本筋ではないかとの考えもあるが、関数によってはグラフ上に粗密のバラツキが現れるので、逆に、描かれるグラフの、画面上のピクセル値をスキャンし、対応する元の関数の変数値を求め、それを用いて関数値を計算する方法を選んだ。
 関数値を色に変換する関数changeToColorは、「易しい使い方(19)」などで使用したものを流用した。

プログラム

private:
    static int THETA=-40,PHI=60;   //視点の方向角θと天頂角φ
    static int X0=90,Y0=260;     //画面上の原点
    static int SIZE=120;       //三次元座標のサイズ
    static float SINT=(float)(Math::Sin(Math::PI/180*THETA));    //sinθ
    static float SINP=(float)(Math::Sin(Math::PI/180*PHI));     //sinφ
    static float COST=(float)(Math::Cos(Math::PI/180*THETA));     //cosθ
    static float COSP=(float)(Math::Cos(Math::PI/180*PHI));      //cosφ

private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

    Graphics^ g=e->Graphics;
    array<String^>^ scale={"-100"," -80"," -60"," -40"," -20","  0","  20","  40","  60","  80","  100"};
    int XMAX=X0+changeTo2D(SIZE,SIZE,SIZE).X;
    int YMAX=Y0+changeTo2D(SIZE,-SIZE,-SIZE).Y;
    Point point1,point2;
    Point point,point_old;
    Pen^ pen1;
    Brush^ brush1;
    array<int>^ ymin=gcnew array<int>(XMAX+1);
    array<int>^ ymax=gcnew array<int>(XMAX+1);
    float x,y,z;
    float xx,yy,zz;
    int xp,yp;

    //--------------------Z軸の描画-----------------------
    //軸を描く
    point1=changeTo2D(-SIZE,-SIZE,-SIZE);
    point2=changeTo2D(-SIZE,-SIZE,SIZE);
    g->DrawLine(Pens::Black,point1,point2);
    //値に応じて着色する
    for(int z=-SIZE;z<=SIZE;z++){
        point1=changeTo2D(-SIZE,-SIZE-1,z);
        point2=changeTo2D(-SIZE,-SIZE-10,z);
        pen1=gcnew Pen(changeToColor(z));
        g->DrawLine(pen1,point1,point2);
    }
    //目盛を入れる
    for(int i=0;i<11;i++){
        point1=changeTo2D(-SIZE,-SIZE,SIZE/5*i-SIZE);
        point2=changeTo2D(-SIZE,-SIZE-15,SIZE/5*i-SIZE);
        g->DrawLine(Pens::Black,point1,point2);
        g->DrawString(scale[i],Font,Brushes::Black,point2.X-35,point2.Y-5);
        if(i==5)
            g->DrawString("Z-Axis",Font,Brushes::Black,point2.X-70,point2.Y);
    }

    //--------------------X軸の描画-----------------------
    //軸を描く
    point1=changeTo2D(-SIZE,-SIZE,-SIZE);
    point2=changeTo2D(SIZE,-SIZE,-SIZE);
    g->DrawLine(Pens::Black,point1,point2);
    //目盛を入れる
    for(int i=0;i<21;i++){
        point1=changeTo2D(SIZE/10*i-SIZE,-SIZE,-SIZE);
        if(i % 2==0){
            point2=changeTo2D(SIZE/10*i-SIZE,-SIZE-15,-SIZE);
            g->DrawLine(Pens::Black,point1,point2);                 //黒で刻みを入れる
            g->DrawString(scale[i/2],Font,Brushes::Black,point2.X-35,point2.Y-5);      //数字を入れる
            if(i==10)
                g->DrawString("X-Axis",Font,Brushes::Black,point2.X-70,point2.Y+10);  //X-Axisと表示
        }
        else{
            point2=changeTo2D(SIZE/10*i-SIZE,-SIZE-10,-SIZE);
            g->DrawLine(Pens::Gray,point1,point2);                  //グレイで刻みを入れる
        }
    }

    //--------------------Y軸の描画-----------------------
    //軸を描く
    point1=changeTo2D(SIZE,-SIZE,-SIZE);
    point2=changeTo2D(SIZE,SIZE,-SIZE);
    g->DrawLine(Pens::Black,point1,point2);
    //目盛を入れる
    for(int i=0;i<21;i++){
        point1=changeTo2D(SIZE,SIZE/10*i-SIZE,-SIZE);
        if(i % 2==0){
            point2=changeTo2D(SIZE+15,SIZE/10*i-SIZE,-SIZE);
            g->DrawLine(Pens::Black,point1,point2);          //黒で刻みを入れる
            g->DrawString(scale[i/2],Font,Brushes::Black,point2.X-10,point2.Y+5);       //数字を入れる
            if(i==10)
                g->DrawString("Y-Axis",Font,Brushes::Black,point2.X+25,point2.Y+30);  //Y-Axisと表示
        }
        else{
            point2=changeTo2D(SIZE+10,SIZE/10*i-SIZE,-SIZE);
            g->DrawLine(Pens::Gray,point1,point2);           //グレイで刻みを入れる
        }
    }

    //-------------------- 関数値を表示し、値に応じて着色する ----------------
    //最大最小法による陰線処理のための準備
    for(int i=0;i<=XMAX;i++){
        ymin[i]=YMAX;
        ymax[i]=0;
    }
    //X軸,Y軸方向を細かくスキャンし、表示と着色を行う
    for(x=SIZE;x>=-SIZE;x-=0.1f)
        for(y=-SIZE;y<=SIZE;y+=0.1f){
            //座標上の位置を関数の変数値に変換(物理空間→論理空間)
            xx=x*100.0f/SIZE;
            yy=y*100.0f/SIZE;
            //関数の計算
            zz=function1(xx,yy);
            //関数の計算値を座標上の位置に変換(論理空間→物理空間)
            z=zz*SIZE/100.0f;
            //陰線処理をしながら色を変えてプロットする
            point=changeTo2D(x,y,z);
            if(point.Y<ymin[point.X]){       //上に抜けているので着色して描画
                ymin[point.X]=point.Y;
                brush1=gcnew SolidBrush(changeToColor(zz));
                g->FillRectangle(brush1,point.X,point.Y,1,1);
            }
            if(point.Y>ymax[point.X]){       //下に出ている(裏返っている)のでグレイで描画
                ymax[point.X]=point.Y;
                g->FillRectangle(Brushes::Gray,point.X,point.Y,1,1);
            }
        }

    //-------------------- Xの値をワイヤーフレームで表示する ----------------
    //最大最小法による陰線処理のための準備
    for(int i=0;i<=XMAX;i++){
        ymin[i]=YMAX;
        ymax[i]=0;
    }
    //X軸方向を粗くスキャンし、線を引く
    for(xp=SIZE;xp>=-SIZE;xp-=2)
        for(yp=-SIZE;yp<=SIZE;yp++){
            //座標上の位置を関数の変数値に変換(物理空間→論理空間)
            xx=xp*100.0f/SIZE;
            yy=yp*100.0f/SIZE;
            //関数の計算
            zz=function1(xx,yy);
            //関数の計算値を座標上の位置に変換(論理空間→物理空間)
            z=zz*SIZE/100.0f;
            //点を計算する
            point=changeTo2D(xp,yp,z);
            //最初の点をpoint_oldに設定する
            if(yp==-SIZE) point_old=point;
            else{
                //陰線処理をしながら線を引く
                if(point.Y<ymin[point.X]){          //上に抜けているので描画
                    ymin[point.X]=point.Y;
                    if(xp % (SIZE/10)==0){
                        if(xp % (SIZE/5)==0) g->DrawLine(Pens::Black,point_old,point);  //黒
                        else           g->DrawLine(Pens::Gray,point_old,point);    //グレイ
                    }
                }
                if(point.Y>ymax[point.X]){          //下に出ている(裏返っている)ので描画
                    ymax[point.X]=point.Y;
                    if(xp % (SIZE/10)==0){
                        if(xp % (SIZE/5)==0) g->DrawLine(Pens::Black,point_old,point);   //黒
                        else           g->DrawLine(Pens::White,point_old,point);   //白
                    }
                }
                point_old=point;
            }
        }

    //-------------------- Yの値をワイヤーフレームで表示する ----------------
    //最大最小法による陰線処理のための準備
    for(int i=0;i<=XMAX;i++){
        ymin[i]=YMAX;
        ymax[i]=0;
    }
    //Y軸方向を粗くスキャンし、線を引く
    for(yp=-SIZE;yp<=SIZE;yp+=2)
        for(xp=SIZE;xp>=-SIZE;xp--){
            //座標上の位置を関数の変数値に変換(物理空間→論理空間)
            xx=xp*100.0f/SIZE;
            yy=yp*100.0f/SIZE;
            //関数の計算
            zz=function1(xx,yy);
            //関数の計算値を座標上の位置に変換(論理空間→物理空間)
            z=zz*SIZE/100.0f;
            //点を計算する
            point=changeTo2D(xp,yp,z);
            //最初の点をdpoint_oldに設定する
            if(xp==SIZE) point_old=point;
            else{
                //陰線処理をしながら線を引く
                if(point.Y<ymin[point.X]){        //上に抜けているので描画
                    ymin[point.X]=point.Y;
                    if(yp % (SIZE/10)==0){
                        if(yp % (SIZE/5)==0) g->DrawLine(Pens::Black,point_old,point);   //黒
                        else           g->DrawLine(Pens::Gray,point_old,point);    //グレイ
                    }
                }
                if(point.Y>ymax[point.X]){       //下に出ている(裏返っている)ので描画
                    ymax[point.X]=point.Y;
                    if(yp % (SIZE/10)==0){
                        if(yp % (SIZE/5)==0) g->DrawLine(Pens::Black,point_old,point);   //黒
                        else           g->DrawLine(Pens::White,point_old,point);    //白
                    }
                }
                point_old=point;
            }
        }

}

private:float function1(float x,float y){

    return (float)50*Math::Cos(Math::Sqrt((x*x+y*y)/50));

}

private:Point changeTo2D(int x,int y,float z){

    Point point;
    point.X=X0+(-SINT*(x+SIZE)+COST*(y+SIZE));
    point.Y=Y0-(-COST*COSP*(x+SIZE)-SINT*COSP*(y+SIZE)+SINP*(z+SIZE));
    return point;

}

private:Point changeTo2D(float x,float y,float z){

    Point point;
    point.X=X0+(-SINT*(x+SIZE)+COST*(y+SIZE));
    point.Y=Y0-(-COST*COSP*(x+SIZE)-SINT*COSP*(y+SIZE)+SINP*(z+SIZE));
    return point;

}

private:Color changeToColor(float z){

    if(z<0) z=z+SIZE;
    int d=(int)(z/SIZE*255);
    return NumberToRGBColor(d);

}

private: Color NumberToRGBColor(int n){

    int h=n/43;
    int r,g,b;
    switch(h){
        case 0:r=255;     g=n*6;     b=0;      break;  //n= 0〜 42
        case 1:r=(85-n)*6;  g=255;     b=0;      break;  //n= 43〜 85
        case 2:r=0;      g=255;    b=(n-86)*6;  break;  //n= 86〜128
        case 3:r=0;      g=(171-n)*6; b=255;     break;  //n=129〜171
        case 4:r=(n-172)*6; g=0;     b=255;    break;  //n=172〜214
        case 5:r=255;     g=0;      b=(255-n)*6; break;  //n=215〜255
    };
    return Color::FromArgb(r,g,b);

}


実行結果
 図3の例では、画面上でのY方向(縦方向)の急峻な変化に対して、陰線処理が適応できず、一部不具合が見られる。
図3は、   を描いたものである。
図4は、   を描いたものである。
図5は、   を描いたものである。
図6は、   を描いたものである。


図3



図4



図5



図6



「Visual C++ の勉強部屋」(目次)へ