講義プリント

CG プログラミング論 平成 26 年 7 月 28 日
第13章.補間曲線-ベジェ曲線
【学習のねらい】
① 前回までの学習では、球を題材にして、それに隠面消去さらにはシェーディングを施
すことによって立体的に描く方法を学習しました。これら隠面消去およびシェーディ
ングについては、一般に任意の形状の図形についても適用できます。しかし、(関数で
表現できない様な)任意の形状を描くためには、表面上の各点の座標(x,y,z)を一つ
ずつ指定しなければなりません。複雑な図形の場合は、膨大な点の座標を与えなけれ
ばならず、大変な労力がかかります。そこで、その労力を減らすため、幾つかの代表
的な点を与え、それらを適当な関数で結ぶ(補間する)事で任意の形状(曲線)を表
現する方法があります。今回は、CG の世界で最もよく用いられるベジェ曲線による一
般の曲線の近似方法を学習しましょう。
13-1
曲線の近似
これまで、円や楕円を描く方法は学習してきました。また、下の(c)の様に、円と楕円を
組み合わせて色々な曲線を描くことも可能です。
(a)
(b)
(c)
しかし、これらの2次曲線(楕円や放物線、双曲線の総称です)を組み合わせて複雑な曲
線を描こうとする場合には、かなり細かく分解しなければうまく近似できなくなります。
実際、下図のような場合には、曲線を分解することがやっかいになります。
143
CG プログラミング論 平成 26 年 7 月 28 日
そこで、このような複雑な曲線をできる限り少ないデータで記述することができる方法
が色々と工夫されました。その中の一つがここで学習するベジェ曲線です。これは、もと
もとフランスの自動車メーカー「ルノー社」の技術者ピエール・E・ベジェ(Pierre E.Bezier)
が、自動車の設計のために考え出したものと言われています。その後、自動車のボディに
止まらず、様々な形状を表現するのに威力を発揮したため、CG の世界でも頻繁に用いられ
るようになったのです。
13-2
ベジェ曲線による補間①
ベジェ曲線では、制御点と呼ばれる複数の点によって、曲線の形が決まります。ここで
は、制御点の個数が4である場合のベジェ曲線について学習します。
今、下のように P1~P4 の 4 つの点が与えられた時に、これを順次直線で結ぶと下のよう
な図が描けます。
P2 (x2,y2)
P3 (x3,y3)
P1 (x1,y1)
P4 (x4,y4)
一方、これら 4 点からベジェ曲線を定義して描いたものが、上の曲線です。このベジェ曲
線は次のようにして描けます。
144
CG プログラミング論 平成 26 年 7 月 28 日
平面上の4点 P1(x1,y1)、P2(x2,y2)、P3(x3,y3)、P4(x4,y4)が与えられたときに、これから定
義されるベジェ曲線は、tをパラメータとして次の様に表されます。
x (t )  x1 B1 (t )  x 2 B2 (t )  x3 B3 (t )  x 4 B4 (t )
4
  xi Bi (t )
i 1
y (t )  y1 B1 (t )  y 2 B2 (t )  y 3 B3 (t )  y 4 B4 (t )

0≦t≦1
(1)
4
 y B (t )
i 1
i
i
そして、ベジェ係数 Bi(t)は次のように与えられます。
B1 (t )  (1  t ) 3
B2 (t ) 3t (1  t ) 2
B3 (t ) 3t 2 (1  t )
(2)
B4 (t ) t 3
パラメータtを、例えば、{0, 0.1, 0.20, 0.3, ・・・, 1.0}の様に適当な刻み幅で区切り、各tに
おける(x,y)を(1)式に従って計算して、それら各点を結べばベジェ曲線を描くことができま
す。なお、(1)、(2)式から分かるように、
t=0 の時: (x,y)=(x1,y1)
t=1 の時: (x,y)=(x4,y4)
となります。つまり、ベジェ曲線は 4 点からなる区間の両端の点は必ず通ります。
それでは、次の課題で p.144 のベジェ曲線を描いてみましょう。
【応用課題 13-A】
プログラムを起動して[描画]
ボタンをクリックすると、次のよ
うなベジェ曲線を、点を直線で結
んだものと同時に描画するプロ
グラムを作成しましょう。
このプログラムは次のように
なります。
145
CG プログラミング論 平成 26 年 7 月 28 日
void DrawGraphics(Graphics g) {
double[] x={ 50, 0,250, 200 };//最初に与える点のx座標
①
double[] y={ 0, 150, 150,0 }; //最初に与える点のy座標
int x1,y1,x2,y2;
int Nb=50; //一区間のメッシュ数 ②
int Np=4; //最初に与える点の数
int Kukan=1; //ベジェ曲線で近似する区間の数:今の場合1③
double[] xb=new double[Kukan*(Nb+1)]; //ベジェ曲線上の各点のx座標
double[] yb=new double[Kukan*(Nb+1)]; //ベジェ曲線上の各点のy座標
int xc=10; //描画中心のx座標
④
int yc=200; //描画中心のy座標
//ベジェ曲線の描画
g.setColor(Color.blue);
Bezie(Kukan,Nb,x,y,xb,yb); ⑤//ベジェ曲線で近似した点 Xb,Yb の計算
x1=xc+(int) xb[0]; //描画開始点のx座標
y1=yc-(int) yb[0]; //描画開始点のy座標
for (int i=1;i<Kukan*(Nb+1);i++) {
x2=xc+(int) xb[i];
y2=yc-(int) yb[i];
g.drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
}
// 最初に与えた点の(直線による)連結
x1=xc+(int) x[0];
y1=yc-(int) y[0];
g.setColor(Color.red);
for(int i=1;i<Np;i++) {
x2=xc+(int)x[i];
y2=yc-(int)y[i];
g.drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
}
}
<解説>
① 4 点 P1~P4 の座標を与えています。
② ベジェ曲線で用いるパラメータt{ 0≦t≦1 }の t の分割数(メッシュ数)を Nb とい
う変数で与えています。ここでは分割数(メッシュ数)を 50 点としています。
③ ベジェ曲線で近似する区間はここでは一つです。複数の区間をつなげる場合は、
【応
用課題 13-C】で扱います。
④ ベジェ曲線では、P1~P4 の区間内の任意の座標( x(t),y(t) )をパラメータt{ 0≦t≦1 }
で指定します(p.145 の(1)式参照)。そして、tを{t0,t1,t2,・・・,tNb}というように、刻
んでいます。そこで、パラメータ ti に対応する x(ti),y(ti)をそれぞれ、xb[i]、yb[i]と表
しています。
146
CG プログラミング論 平成 26 年 7 月 28 日
⑤ ④で定義した xb[i]、yb[i]、
(i=0~Nb)を求めるメソッド Bezie()を呼び出してい
ます。これは新たに(独自に)定義するメソッドです。このプログラム作成の後、下の
様に記述して下さい。この部分の記述時には(Bezie が未定義なので)警告が出ます。
上の記述が完成したら、ベジェ曲線上の座標 xb[i]、yb[i]、
(i=0~Nb)を求めるメソッド
Bezie を次のように記述して下さい。場所は DrawGraphics()の前後いずれでも結構です。
void Bezie(int Kukan,int Nb,double[] x, double[] y
,double[] Xb,double[] Yb) {
double[][] B=new double[4][Nb+1]; //ベジェ係数 Bi(t)の宣言
double Db=1.0/Nb; //区間内の刻み幅
double t; //パラメータt
//ベジェ係数 Bi(t)の計算
for(int i=0; i<= Nb; i++) {
t=Db*i;
B[0][i]=(1-t)*(1-t)*(1-t);
B[1][i]=3*t*(1-t)*(1-t);
B[2][i]=3*t*t*(1-t);
B[3][i]=t*t*t;
}
//ベジェ曲線上の座標(x(tm),y(tm))の計算
int m=0;
for (int i=0;i<Kukan;i++) {
for (int j=0;j<=Nb; j++) {
Xb[m]=0;
Yb[m]=0;
for (int k=0;k<4;k++) {
Xb[m]=Xb[m]+x[i*3+k]*B[k][j];
Yb[m]=Yb[m]+y[i*3+k]*B[k][j];
}
m=m+1;
}
}
}
ベジェ係数 Bi(tj), ( i=0~3, j=0~Nb )を 2 次元配列 B[i][j]として表しています。それ
以外は、p.145 の(2)式および(1)式にしたがって、Bi(t)および ( x(t),y(t) )を計算しているだ
けなので、内容は理解できるでしょう。
※ この課題は、実行結果を確認後「実行結果を確認しました」と書いて送って下さい。
147
CG プログラミング論 平成 26 年 7 月 28 日
【応用課題 13-B】
上のプログラムにおいて、P1~P4 の座標をそれぞれ次の①~③のように変更しました。
このとき描かれるベジェ曲線は、それぞれ下図のいずれでしょうか。①~③それぞれに該
当する図の記号を答えて下さい。
① P1(,x,y)=(50,0), P2(x,y)=(250,150), P3(x,y)=(0,150), P4(x,y)=(200,0)
② P1(,x,y)=(50,0), P2(x,y)=(0,150), P3(x,y)=(200,0), P4(x,y)=(250,150)
② P1(,x,y)=(50,0), P2(x,y)=(200,0), P3(x,y)=(0,150), P4(x,y)=(250,150)
(A)
(B)
(C)
148
CG プログラミング論 平成 26 年 7 月 28 日
13-3
ベジェ曲線による補間①
-
複数区間の場合
ベジェ曲線は4点で1つの曲線を近似するので、4点を超える場合には、図のように接
続を繰り返す事になります。その際、第3番目と第5番目の点の間に第4番目の点が位置
するようにしておくと、順次なめらかに接続されて行きます。
P2
P3
P1
P4
P7
P5
P6
ここで、点の個数と区間数の間には次の関係があることが分かります。
点の個数
= 3 × 区間数 + 1
【応用課題 13-C】
【応用課題 13-A】で作成したプログラムを用いて、次のような 7 点を補間するベジェ曲
線を描きましょう。
P2(0,100)
P3(100,100)
P1(0,0)
P4(100,0)
P7(250,0)
P5(100,-100)
P6(175,-125)
149
CG プログラミング論 平成 26 年 7 月 28 日
このプログラムは、
【応用課題 13-A】のプログラムにおいて、下の下線部を変更し、空欄
①を埋めることで作成できます(メソッド Bezie はそのまま使えます)。任意の点の個数
Np の場合の区間数を与えるように①を埋めてプログラムを完成させて下さい。
void DrawGraphics(Graphics g) {
double[] x={ 0,0,100,100,100,175,250 };//最初に与える点のx座標
double[] y={ 0,100,100,0,-100,-125,0 };//最初に与える点のy座標
int x1,y1,x2,y2;
int Nb=50; //一区間のメッシュ数
int Np=7; //最初に与える点の数
int Kukan=
; //ベジェ曲線で近似する区間の数
①
double[] xb=new double[Kukan*(Nb+1)]; //ベジェ曲線上の各点のx座標
double[] yb=new double[Kukan*(Nb+1)]; //ベジェ曲線上の各点のy座標
int xc=10; //描画中心のx座標
int yc=200; //描画中心のy座標
//ベジェ曲線の描画
g.setColor(Color.blue);
Bezie(Kukan,Nb,x,y,xb,yb); //ベジェ曲線で近似した点 Xb,Yb の計算
x1=xc+(int) xb[0]; //描画開始点のx座標
y1=yc-(int) yb[0]; //描画開始点のy座標
for (int i=1;i<Kukan*(Nb+1);i++) {
x2=xc+(int) xb[i];
y2=yc-(int) yb[i];
g.drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
}
// 最初に与えた点の(直線による)連結
x1=xc+(int) x[0];
y1=yc-(int) y[0];
g.setColor(Color.red);
for(int i=1;i<Np;i++) {
x2=xc+(int)x[i];
y2=yc-(int)y[i];
g.drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
}
}
ヒント
前ページの式より
区間数=(点の個数-1)/3
となります。今の場合、点の個数は Np で与えられています。
150
CG プログラミング論 平成 26 年 7 月 28 日
第14章.回転体の描画
【学習のねらい】
① CG の世界では、前章で学習したベジェ曲線で表した曲線を(適当な軸を中心にして)
回転させることで立体的な図形を作成することがよく行われます。これは、大変有力
な方法なので、本講義の最後の単元として学習しておきましょう。
14-1
回転体の描画
回転体は、適当な回転軸の回りに外形となる曲線(輪郭線)を回転させて描きます。輪郭
線はベジェ曲線を用いて描きます。
y
②y軸を中心に回転させる。
①ベジェ曲線を用いて輪郭線を描く。
x
z
例えば下の左図は曲線上の点群をy軸に関して回転させたときに描かれる円形断面を表し
ています。そしてこれを用いて隣接する円形断面間でポリゴン(四角形)を生成し、12 章
で学習したシェーディングを行なうと、右図が得られます。本章ではこういった立体図形
を 描
きま
し ょ
う。
151
CG プログラミング論 平成 26 年 7 月 28 日
さて、回転体の輪郭を与えるベジェ曲線上の点の各々は、下図のように回転体断面の半径
と高さの意味を持っています。例えば断面を xy 平面にとると、半径は x 座標の値、高さは
y座標の値となります。このベジェ曲線上の各点(x,y)を、y軸を中心に回転すれば、回
転図形が得られます。
ベジェ曲線上の点
(2 次元)
回転角Φ
【応用課題 14-A】
【応用課題 12-A】のプ
ログラムを改良して、回
転体を描くプログラムを
作成しましょう。
【応用課題 12-A】のプロ
ジェクトをコピーしてそ
れを利用します。
ここで作成するのは、左
のような図形です。隠面
処理およびシェーディン
グは次の課題で行います。
まず、この描画図形に合
わせて、フレームを縦に
伸ばして下さい。
152
CG プログラミング論 平成 26 年 7 月 28 日
続いて、NewJFrame.java 内の DrawGraphics()を次のように修正します。下線と点線枠
内が【応用課題 12-A】からの変更部分です。
void DrawGraphics(Graphics g) {
// 視点の座標の定義
double Vx=Double.parseDouble(jTextFieldVx.getText());
double Vy=Double.parseDouble(jTextFieldVy.getText());
double Vz=Double.parseDouble(jTextFieldVz.getText());
double[][][] faceX=new double[300][200][4];
double[][][] faceY=new double[300][200][4];
double[][][] faceZ=new double[300][200][4];
①
// ----( グラスの輪郭線データ )--------double[ ] x =
{ 100, 50, 10, 10, 10, 40, 70, 100, 110, 70, 60, 56, 52 };
double[ ] y =
{ 0, 6, 30, 70, 120, 140, 160, 180, 220, 270, 280, 290, 310 };
int Np3=13; //最初に与える点の数
int Nb=20; //ベジェ補間の一区間のメッシュ数
int Kukan=(Np3-1)/3; //区間数
int Np1=Kukan*(Nb+1),Np2=50,Np=4;
KaitenTai(Np1,Np2,Kukan,Nb,x,y,faceX,faceY,faceZ); //回転体の座
標の計算
//光線ベクトルの定義
double RayX=Double.parseDouble(jTextFieldRayX.getText());
double RayY=Double.parseDouble(jTextFieldRayY.getText());
double RayZ=Double.parseDouble(jTextFieldRayZ.getText());
double P_a=0.3,P_d=1.0,P_s=1.0; //各成分の最大強度の設定
②
Hidden3D h3D= new Hidden3D(); //hdden3D オブジェクトの生成
h3D.setData(Vx,Vy,Vz,Np,Np1,Np2,faceX,faceY,faceZ); //描画に必
要なデータを設定する
h3D.MyPaint(g); //画像の描画
}
ここに、①は、
【応用課題 12-A】において Sphere()というメソッドを用いて球面上の(x,y,z)
座標を求めていた部分を、回転体の(x,y,z)座標を求める部分に置き換えたものです。そのた
め、Sphere()ではなく新たに Kaitentai()というメソッドを定義してこれを呼び出して
います。これは、xy 平面で与えた座標(x,y)を基に回転体の座標(faceX、faceY、faceZ)を
求めるメソッドです。これを次ページのように記述して下さい。
なお、②では、まだシェーディングを行わないので、Shading クラスではなく、Hidden3D
クラスを用いています。
153
CG プログラミング論 平成 26 年 7 月 28 日
続いて、NewJFrame.java 内に新たなメソッド Kaitentai()を次ページのように記述
して下さい。
154
CG プログラミング論 平成 26 年 7 月 28 日
void KaitenTai(int N_Y,int N_Fai,int Kukan,int Nb
,double[] x,double[] y,double faceX[][][]
,double faceY[][][],double faceZ[][][] ) {
double[][] B=new double[4][100];
double[] Xb=new double[N_Y];
double[] Yb=new double[N_Y];
double y1,y2,Fai1,Fai2;
double d_Fai=2*Math.PI/N_Fai;
double[] KaitenX=new double[4];//四角形ポリゴンのx座標
double[] KaitenY=new double[4];//四角形ポリゴンのy座標
double[] KaitenZ=new double[4];//四角形ポリゴンのz座標
Bezie(Kukan,Nb,x,y,Xb,Yb); //ベジェ曲線で補間する
//回転体の各ポリゴンの座標
Bezie()は次ページで定義する。
for(int i=0;i<N_Y-1;i++) {
y1=Yb[i];
y2=Yb[i+1];
KaitenY[0]=y2;
KaitenY[1]=y2;
KaitenY[2]=y1;
KaitenY[3]=y1;
for(int j=0;j<N_Fai;j++) {
Fai1=j*d_Fai;
Fai2=(j+1)*d_Fai;
KaitenX[0]=Xb[i+1]*Math.cos(Fai2);
KaitenX[1]=Xb[i+1]*Math.cos(Fai1);
KaitenX[2]=Xb[i]*Math.cos(Fai1);
KaitenX[3]=Xb[i]*Math.cos(Fai2);
KaitenZ[0]=Xb[i+1]*Math.sin(Fai2);
KaitenZ[1]=Xb[i+1]*Math.sin(Fai1);
KaitenZ[2]=Xb[i]*Math.sin(Fai1);
KaitenZ[3]=Xb[i]*Math.sin(Fai2);
for (int k=0;k<4;k++) {
faceX[i][j][k]=KaitenX[k]; //(i,j)番目のポリゴンのx座標
faceY[i][j][k]=KaitenY[k]; //(i,j)番目のポリゴンのy座標
faceZ[i][j][k]=KaitenZ[k]; //(i,j)番目のポリゴンのz座標
}
}
}
}
p.152 の図より、
回転体表面の座標(x,y,z)は、xy 平面上に描いたベジェ曲線の座標を(xb,yb)
とすると、次のように与えられます。
X
x  xb cos
y  yb
xb
,   / 2  
x
z  xb sin 
φ
Φ
z
155
Z
CG プログラミング論 平成 26 年 7 月 28 日
xy 平面上に描いたベジェ曲線の座標(xb,yb)を求めるメソッド Bezie()は【応用課題 13-A】
(p.147)と同様に次のように記述します。
void Bezie(int Kukan,int Nb,double[] x, double[] y
,double[] Xb,double[] Yb) {
double[][] B=new double[4][Nb+1]; //ベジェ係数 Bi(t)の宣言
double Db=1.0/Nb; //区間内の刻み幅
double t; //パラメータt
//ベジェ係数 Bi(t)の計算
for(int i=0; i<= Nb; i++) {
t=Db*i;
B[0][i]=(1-t)*(1-t)*(1-t);
B[1][i]=3*t*(1-t)*(1-t);
B[2][i]=3*t*t*(1-t);
B[3][i]=t*t*t;
}
//ベジェ曲線上の座標(x(tm),y(tm))の計算
int m=0;
for (int i=0;i<Kukan;i++) {
for (int j=0;j<=Nb; j++) {
Xb[m]=0;
Yb[m]=0;
for (int k=0;k<4;k++) {
Xb[m]=Xb[m]+x[i*3+k]*B[k][j];
Yb[m]=Yb[m]+y[i*3+k]*B[k][j];
}
m=m+1;
}
}
}
156
CG プログラミング論 平成 26 年 7 月 28 日
次に、Hidden3D.java に移って、MyPaint()メソッドを次のように変更します。今の場合、
隠面消去を行わないので、可視判定部分をコメントアウトしておきます。
public void MyPaint(Graphics g) {
int[] xp=new int[Np];
int[] yp=new int[Np];
x=new double[Np]; //ポリゴンの各頂点のx座標
y=new double[Np]; //ポリゴンの各頂点のy座標
z=new double[Np]; //ポリゴンの各頂点のz座標
int xc=150; //描画中心のx座標
int yc=300; //描画中心のy座標
g.setColor(Color.blue);
for(int i=0;i<Np1;i++) {
for(int j=0;j<Np2;j++) {
for(int k=0;k<Np;k++) {
x[k]=faceX[i][j][k]; //ポリゴンの各頂点のx座標
y[k]=faceY[i][j][k]; //ポリゴンの各頂点のx座標
z[k]=faceZ[i][j][k]; //ポリゴンの各頂点のx座標
}
//
if(Kashi_hantei()) {
zahyo_henkan(); //ポリゴンの各頂点の座標を視点座標系へ変換
for(int k=0;k<Np;k++) {
xp[k]=xc+(int) xv[k];
可視判定を行わないので、この部
yp[k]=yc-(int) yv[k];
分をコメントにして外しておく。
}
g.drawPolygon(xp,yp,Np);
//
}
}
}
}
プログラムを作成したら実行し、p.152 のように描画されるか確認して下さい。
※ この課題は、実行結果を確認後「実行結果を確認しました」書いて送ってください。
157
CG プログラミング論 平成 26 年 7 月 28 日
14-2
シェーディングの改良
【応用課題 14-B】
次に、隠面消去を行ってみましょう。
【応用課題 14-A】のプログラムで、Hidden3D.java
の MyPaint()メソッドでコメントアウトした部分を復活させて下さい。
public void MyPaint(Graphics g) {
・・・
for(int i=0;i<Np1;i++) {
for(int j=0;j<Np2;j++) {
for(int k=0;k<Np;k++) {
・・・
}
if(Kashi_hantei()) {
zahyo_henkan(); //ポリゴンの各頂点の座標を視点座標系へ変換
for(int k=0;k<Np;k++) {
xp[k]=xc+(int) xv[k];
この 2 行を復活させる。
yp[k]=yc-(int) yv[k];
}
g.drawPolygon(xp,yp,Np);
}
}
}
}
隠面処理を復活させた後、プログラムを実行すると、隠面消去が施されます。ところが、
この場合、下図のように描画されるはずです。ワイングラスのグラスの内側が表示されて
いませんね。
これは、隠面処理で消去されてしまったためです。
第 11 章で学習した隠面処理によると確かに該当す
る面の外側は見えません。しかし、本来は面の内側
が見えるはずです。事情は次の通りです。
第 11 章(p.116)で学習した通り、下の様な状況
では面 C は見えませんが、裏面の面 D は見えます。
球面のように閉じた図形と違って、ワイングラスの
ように開いた図形の場合は、裏面が見えることを考
慮しなければなり
面C
ません。この点を
改良しましょう。
α
見えない
法線ベクトル
面D
見える
158
CG プログラミング論 平成 26 年 7 月 28 日
改良に進む前に、後の説明の都合上、これにシェーディングを施すようにしましょう。
DrawGraphics()メソッドの中で Hidden3D クラスを用いていた部分を、下の枠線部のよう
に、Shading クラスを用いるように修正して下さい。
void DrawGraphics(Graphics g) {
// 視点の座標の定義
・・・
double RayY=Double.parseDouble(jTextFieldRayY.getText());
double RayZ=Double.parseDouble(jTextFieldRayZ.getText());
double P_a=0.3,P_d=1.0,P_s=1.0; //各成分の最大強度の設定
Shading shade= new Shading(); //shading オブジェクトの生成
shade.setData(Vx,Vy,Vz,Np,Np1,Np2,faceX,faceY,faceZ
,RayX,RayY,RayZ,P_a,P_d,P_s); //描画に必要なデータを設定する
shade.MyPaint(g); //画像の描画
}
もちろん、この段階ではシェーディングを行っても次のようにワイングラスの口の部分
が描画されないままです。
プログラムを作成したら実行し、上のように描画されることを確認して下さい。
※ この課題は、実行結果を確認後「実行結果を確認しました」書いて送ってください。
159
CG プログラミング論 平成 26 年 7 月 28 日
さて、裏面が見えるようにするためには、次の様にします。
1. 可視判定で「見えない」と判定された場合、裏面について、第 12 章(p.133~134)で
説明した、ディフーズ成分の強度dおよびスペキュラー成分の強度sを計算する。
2. 裏面の法線ベクトルは表面のそれと逆向きになる。そこで、p.133 に示した法線ベクト
ルと光源との間の角度α’d は次のようになる。
光源

VN 法線ベクトル

VR
αd
光線ベクトル
表面
α’d
裏面
裏面の法線ベクトル
3. このとき、 cosα’d=-cosαd
となる。
4. 同様に、裏面に対する法線ベクトルと p.134 で示した VH ベクトルとの角度をα’s とす
ると、cosα’s=-cosαs
となる。
5. cosα’d と cosα’s を用いて、dおよびsを計算する。
この考えを用いて、次の【応用課題 14-C】で裏面を描けるように改良しましょう。
160
CG プログラミング論 平成 26 年 7 月 28 日
【応用課題 14-C】
【応用課題 14-B】のプログラムを改良しましょう。Shading.java に戻って、MyPaint()
を次のように修正して下さい。点線枠の部分が修正部分です。
public void MyPaint(Graphics g) {
int[] xp=new int[Np];
int[] yp=new int[Np];
x=new double[Np]; //ポリゴンの各頂点のx座標
y=new double[Np]; //ポリゴンの各頂点のy座標
z=new double[Np]; //ポリゴンの各頂点のz座標
int xc=150; //描画中心のx座標
int yc=300; //描画中心のy座標
double intensity,Max_intensity;
int rgb;
Max_intensity=P_a+P_d+P_s;
for(int i=0;i<Np1;i++) {
for(int j=0;j<Np2;j++) {
for(int k=0;k<Np;k++) {
x[k]=faceX[i][j][k]; //ポリゴンの各頂点のx座標
y[k]=faceY[i][j][k]; //ポリゴンの各頂点のx座標
z[k]=faceZ[i][j][k]; //ポリゴンの各頂点のx座標
}
zahyo_henkan(); //ポリゴンの各頂点の座標を視点座標系へ変換
for (int k = 0; k < Np; k++) {
xp[k] = xc + (int) xv[k];
yp[k] = yc - (int) yv[k];
}
if(Kashi_hantei()) {
ShadeCalc();
intensity=P_a+P_d*D_strength+P_s*S_strength;//目に入って来
る光の強度
rgb=(int) (255*intensity/Max_intensity);
}
else {
ShadeCalc2();
intensity=P_a+P_d*D_strength+P_s*S_strength;//目に入って来
る光の強度
rgb=(int) (255*intensity/Max_intensity);
}
g.setColor(new Color(rgb, rgb, rgb ));
g.fillPolygon(xp, yp, Np);
}
}
}
ここで、裏面を描くために新たに ShadeCalc2()を用意しています。これを次ページの
ように記述して下さい。ShadeCalc()からの変更部分は、二つの枠で囲んだ部分のみです。
161
CG プログラミング論 平成 26 年 7 月 28 日
void ShadeCalc2() {
double HousenV_x,HousenV_y,HousenV_z; //面の法線ベクトルの成分
double HousenV; //面の法線ベクトルの大きさ
double V1_x,V1_y,V1_z,V2_x,V2_y,V2_z;//面上のベクトル V1,V2 の成分
double cos_alfad; //cosαd αd:法線ベクトルと光線ベクトルの間の角度
double HV_x,HV_y,HV_z; //H ベクトルの成分
double HV,RayV; //H ベクトル、光線ベクトルの大きさ
double cos_alfas; //cosαs αs:法線ベクトルと H ベクトルの間の角度
// V1 ベクトルの定義
V1_x=x[3]-x[1];
V1_y=y[3]-y[1];
V1_z=z[3]-z[1];
// V2 ベクトルの定義
V2_x=x[2]-x[0];
V2_y=y[2]-y[0];
V2_z=z[2]-z[0];
// 面(ポリゴン)の法線ベクトルの定義
HousenV_x=V1_y*V2_z-V1_z*V2_y;
HousenV_y=V1_z*V2_x-V1_x*V2_z;
HousenV_z=V1_x*V2_y-V1_y*V2_x;
//法線ベクトルの大きさ
HousenV=Math.sqrt( HousenV_x*HousenV_x+HousenV_y*HousenV_y
+HousenV_z*HousenV_z);
//光線ベクトルの大きさ
RayV=Math.sqrt(RayX*RayX+RayY*RayY+RayZ*RayZ);
// 光線ベクトルと面(ポリゴン)の法線ベクトルとの内積
cos_alfad=(HousenV_x*RayX+HousenV_y*RayY+HousenV_z*RayZ)
/HousenV/RayV;
cos_alfad=-cos_alfad;//裏面に対する修正部分
// ディフューズ(Diffuse)成分強度の計算
D_strength=Math.max(0,cos_alfad);
// スペキュラー(Specular)成分強度の計算
HV_x=Vx+RayX;
HV_y=Vy+RayY;
HV_z=Vz+RayZ;
HV=Math.sqrt(HV_x*HV_x+HV_y*HV_y+HV_z*HV_z);
cos_alfas=(HousenV_x*HV_x+HousenV_y*HV_y+HousenV_z*HV_z)
/HousenV/HV;
cos_alfas=-cos_alfas;//裏面に対する修正部分
S_strength=Math.pow(cos_alfas,40);
}
この修正の後に実行すると、次のようにワイングラスの口(裏面)部分がきちんと描かれ
ます。
162
CG プログラミング論 平成 26 年 7 月 28 日
プログラムを作成したら実行し、上のように描画されることを確認して下さい。
※ この課題は、実行結果を確認後「実行結果を確認しました」書いて送ってください。
さて、実はまだもう一つ改良すべき点が残っています。上のプログラムで、グラスの下か
ら眺めるケースを試してみましょう。Vy=-1 に変更して描画して下さい。すると次のよう
になるはずです。
163
CG プログラミング論 平成 26 年 7 月 28 日
これは少しおかしな描画になっています。本来であれば、次のように描画されなければな
りません。
これは、手前のグラス表面の後に、本来は
視点から遠くにあって見えない(向こう
側)の面を描いてしまったために起こった
現象です。
164
CG プログラミング論 平成 26 年 7 月 28 日
このような問題はどの様に解決できるでしょうか?具体的に下図のような場合を考えてみ
ましょう。この例の場合、面Cは、面Aに(全部、あるいは部分が)隠れて見えない面と
なります。また、面 B の裏面も同様です。
法線ベクトル
面C
面B
視線ベクトル
面A
このような場合、
「面を塗る順番を工夫する」ことによって、正しく描画することができま
す。つまり、視点から見て奥の方にある面を先に描き、手前にある面を後から描くのです。
すると、遠方の面と手前の面が重なっていても、後から塗る(手前の)面が上書きされる
ので、自然に正しく描画されることになります。すると、面の奥行きに従って、描画順序
の並べ替え(ソート)を行うことがポイントになります。
この考えにしたがって、次の【応用課題 14-D】でシェーディングを改良しましょう。
【応用課題 14-D】
【応用課題 14-C】のプログラムにおいて、Shading.java を開いて下さい。そして、
MyPaint()メソッドを次のように修正して下さい。下線部が修正部分で、点線枠部分が新
たに加えた部分です。
165
CG プログラミング論 平成 26 年 7 月 28 日
public void MyPaint(Graphics g) {
int[] xp=new int[Np];
int[] yp=new int[Np];
・・・
double intensity,Max_intensity;
int[] rgb=new int[Np1*Np2]; //光の強度を格納する配列
Max_intensity=P_a+P_d+P_s;
double[] dist=new double[Np1*Np2]; //視点から面までの距離
int[][] xp_sort=new int[Np1*Np2][Np];
int[][] yp_sort=new int[Np1*Np2][Np];
int No=0;
for(int i=0;i<Np1;i++) {
for(int j=0;j<Np2;j++) {
for(int k=0;k<Np;k++) {
x[k]=faceX[i][j][k]; //ポリゴンの各頂点のx座標
y[k]=faceY[i][j][k]; //ポリゴンの各頂点のx座標
z[k]=faceZ[i][j][k]; //ポリゴンの各頂点のx座標
}
zahyo_henkan(); //ポリゴンの各頂点の座標を視点座標系へ変換
for (int k = 0; k < Np; k++) {
xp[k] = xc + (int) xv[k];
yp[k] = yc - (int) yv[k];
xp_sort[No][k]=xp[k];
yp_sort[No][k]=yp[k];
}
①
dist[No]=zv[0]+zv[1]+zv[2]+zv[3]; //面から視点までの距離
if(Kashi_hantei()) {
ShadeCalc();
intensity=P_a+P_d*D_strength+P_s*S_strength;//目に入って来
る光の強度
rgb[No]=(int) (255*intensity/Max_intensity);
}
else {
ShadeCalc2();
intensity=P_a+P_d*D_strength+P_s*S_strength;//目に入って来
る光の強度
rgb[No]=(int) (255*intensity/Max_intensity);
}
No=No+1;
}
}
②
SortFace(Np1*Np2,dist,xp_sort,yp_sort,rgb); //面の順序をソート
for(int i=0;i<Np1*Np2;i++) { //視点から遠い順にシェーディングを行う
g.setColor(new Color(rgb[i], rgb[i], rgb[i] ));
g.fillPolygon(xp_sort[i], yp_sort[i], Np);
}
}
166
CG プログラミング論 平成 26 年 7 月 28 日
【プログラムの解説】
① 視点から面までの距離 dist は、各点の距離の合計としています。
YV
0
zv[0]
zv[1]
1
ZV
zv[3]
3
2
zv[2]
XV
② 視点からの距離が遠い順に描画面をソートするメソッドです。次のように記述して下
さい。ここに、選択ソートを使って、距離 dist が大きい順にソートしています。
void SortFace(int NMax,double[] dist,int[][] xp,int[][] yp
,int[] rgb) {
int TempI,Pos;
double TempD;
int[] TempP=new int[Np];
for (int i=0;i<NMax-1;i++) {
Pos=i;
for (int j=i+1; j<NMax;j++) {
if(dist[j] > dist[Pos] ) {
Pos=j;
}
}
TempD=dist[i];
dist[i]=dist[Pos];
dist[Pos] = TempD;
TempP=xp[i];
xp[i]=xp[Pos];
xp[Pos] = TempP;
TempP=yp[i];
yp[i]=yp[Pos];
yp[Pos] = TempP;
TempI=rgb[i];
rgb[i]=rgb[Pos];
rgb[Pos] = TempI;
}
}
作成したら、プログラムを実行し、次ページの様に表示されることを確認して下さい。
167
CG プログラミング論 平成 26 年 7 月 28 日
※ この課題は、実行結果を確認後「実行結果を確認しました」書いて送ってください。
168