2015年05月09日

プチコン3号 32日目 球を描く(2)

pc32-1.jpg

今回は、前回の続きで、上の画面のような前回よりもリアルな球を描いてみます。

前回は球を正面から光で照らした場合の陰影を描画しました。
しかし、球を正面から光で照らすシーンは、日常ではあまり出会いません。
もっとも一般的なのは、満月でしょうか・・・


今回は日常的に目にする、斜め上からの光で照らされている球を描画します。
また、光が物体の表面で鏡面反射されて生じる、ハイライトも描き込みます。

前回説明した「光の当たる角度」と「環境光」、さらに今回の「ハイライト」という3つの成分の合成でシェーディングを表現する手法は、「フォンの反射モデル」と呼ばれる古典的な手法です。
(ちなみに「フォン」は発明者の名前です。)


今回のプログラムでは、光のあたる角度は、球の正面から照らす場合を回転角0、球の真上から照らす場合を回転角π/2(90度)とし、X軸の周りの回転角だけで表します。左右へは傾けません。
これによって、画像が左右対称になり、全ピクセルの半分の計算で描画を終えることができます。
(左右に傾けても、対称軸が傾くだけで対称にはなりますが、ピクセルの位置を計算しづらくなります。)

光を斜め上方向に移動するには、25日目で解説した回転の座標変換を使います。

下図では、図左は前回のプログラムのように上(Z軸方向)から光が当たっています。
図右は、光が角度θだけ傾いています。
このとき、球面で一番明るくなるのは、真上からθだけ傾いた点Bになります。
では点Aの明るさはどうなるかというと、図左で真上から−θだけ傾いた点Cの明るさと同じになります。
pc32-3.jpg

以上は、球の中心の周りに、球も光も含めた全体が角度θだけ回転したと考えれば、直観的にも理解できると思います。


また、ハイライトはその強さのみを、0から1の範囲で与えることにします。

ハイライトの出現位置は、球の最も明るい場所ではなく、光の角度の半分の角度の位置に出ます。
たとえば

・光の方向が0度のときはハイライトの位置も0度(円の中心)
・光の方向が90度(上方)のときは、ハイライトの位置は45度(下図参照)

となります。
pc32-4.jpg

ハイライトの光の分布にもいろいろなモデルがありますが、要は鋭いピークがありつつ滑らかに変化していればもっともらしく見えます。
今回は「半分の角度で光を当てたときの明るさ」を500乗して作っています(Phong分布相当)。
また、反射光の色は物体の色ではなく、白色としました。


次の図は、今回のプログラムで角度とハイライトのパラメータをいろいろ変えてみた画像です。
上段は光の角度を左から0度(正面)、30度、60度、90度(真上)の4通りに変えています。
下段は、角度は60度のまま、ハイライトの係数を0.1、0.3、0.5、0.7と変えています。

pc32-2.jpg


プログラムは以下の通りです。
傾いた光に関する部分を紫、ハイライトに関する部分をオレンジにしています。
CLS:GCLS
T=MAINCNT
DRAWSPHERE 100,20,170,192,192,128,0.2,PI()/4,0.4
PRINT (MAINCNT-T)/60;" SECONDS ELAPSED"

DEF DRAWSPHERE X0,Y0,SIZE,R0,G0,B0,GL,T,SP
VAR R,RR,X,XX,Y,YY,Z,ZZ,YMAX,NZ,NY,C,R1,G1,B1
R=SIZE/2+0.5
RR=R*R
X0=X0+R
Y0=Y0+R
X=-0.1
WHILE R>=X
Y=-0.1
XX=X*X
YY=Y*Y
YMAX=RR-XX
ZZ=YMAX-YY
WHILE ZZ > 0
Z=SQR(ZZ)
ROTATE Y/R,Z/R,T OUT NY,NZ
NZ=MAX(0,NZ)
C=SHADING(NZ,1-GL,GL)
RGB_ADD 0,0,0,C,R0,G0,B0 OUT R1,G1,B1

ROTATE Y/R,Z/R,T/2 OUT NY,NZ
NZ=MAX(0,NZ)
C=SPECULAR(NZ,SP,0.002)
RGB_ADD R1,G1,B1,C,255,255,255 OUT R1,G1,B1

C=RGB2(255,R1,G1,B1)
GPSET X0-X,Y0-Y,C
GPSET X0+X,Y0-Y,C

ROTATE -Y/R,Z/R,T OUT NY,NZ
NZ=MAX(0,NZ)
C=SHADING(NZ,1-GL,GL)
RGB_ADD 0,0,0,C,R0,G0,B0 OUT R1,G1,B1

ROTATE -Y/R,Z/R,T/2 OUT NY,NZ
NZ=MAX(0,NZ)
C=SPECULAR(NZ,0.4,0.002)
RGB_ADD R1,G1,B1,C,255,255,255 OUT R1,G1,B1

C=RGB2(255,R1,G1,B1)
GPSET X0+X,Y0+Y,C
GPSET X0-X,Y0+Y,C
Y=Y+1
YY=Y*Y
ZZ=YMAX-YY
WEND
X=X+1
WEND
END

DEF SHADING(VAL,DIF,AMB)
RETURN AMB+DIF*VAL
END

DEF SPECULAR(VAL,SP_I,SP_SIZE)
RETURN SP_I*POW(VAL,1/SP_SIZE)
END

DEF RGB_ADD R0,G0,B0,K,R1,G1,B1 OUT R2,G2,B2
R2=MIN(255,R0+K*R1)
G2=MIN(255,G0+K*G1)
B2=MIN(255,B0+K*B1)
END

DEF RGB2(A,R,G,B)
IF RND(8) < (R AND 7) THEN R=R+8
IF RND(8) < (G AND 7) THEN G=G+8
IF RND(8) < (B AND 7) THEN B=B+8
RETURN RGB(A,R,G,B)
END

DEF ROTATE X,Y,T OUT X1,Y1
X1=COS(T)*X - SIN(T)*Y
Y1=SIN(T)*X + COS(T)*Y

END

回転角は変数Tに入っています。
回転の処理(手続きROTATE)は25日目に使ったものと同じです。

ハイライトの計算については、まず光がT/2の角度で入ってきた場合のピクセルの反射の強さを求めます。
そして、反射の強さを関数SPECULARでハイライトに変換します。
ハイライトは強さのパラメータが変数SPに入っています。
SPECULARは、反射の強さをVAL、ハイライトの強さをSP_I、ハイライトの広がり具合をSP_SIZEで与えます。
最後のSP_SIZEは小さくするほどハイライトも小さくなります。
ただし、実際にハイライト部分のピクセル数などを指定しているわけではなく、VALを1/SP_SIZE乗するだけです。
値としては、0.01以下が良いでしょう。
今回は0.002にしています。

画像の明暗は左右対称になりますが、形状自体は上下にも対象です。
そこで、ループの1回ごとに、上半分のピクセルの明るさと下半分のピクセルの明るさの計算を行っています。
ほぼ同じコードがループの前半と後半にありますが、前半が上半分、後半が下半分に関する計算です。

このプログラムで前回と同じ大きさの球(直径170ピクセル)を描くのに3.5秒かかります。
前回は全体の1/8のピクセルの明るさだけを計算すれば済みましたが、今回は全体の半分の計算が必要で、かつ計算自体も増えているためです。
posted by boochow at 22:24| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年05月04日

プチコン3号 31日目 球を描く(1)

pc31-1.jpg

前回紹介したレイトレーシングの手法は、時間がかかりすぎるのでゲームの画像表示に使うには向いていません。
今回は、球を一つだけ、レイトレーシングよりも高速に描画するプログラムを作ってみます。
上の画面が実行結果で、直径170ピクセルの球を0.6秒弱で描画しています。
アニメーションに使うのは難しいですが、スプライトやBGの作成には使うことができるでしょう。

球を球らしく見せるには、陰影が重要です。
光の方向や強さなどに応じてピクセルの明るさを決めることを「シェーディング」と言います。

下図は球面が上方から来る光に照らされている様子を示しています。
(以下、球面の光の当たらない下半分は無視します。)
地球が太陽に照らされている様子を思い浮かべれば分かりやすいでしょう。
このときシェーディングは基本的には以下のようにして行えます。

まず、球面のごく小さな一部を考えます。
これは、ある傾きを持った平面にほぼ等しいと考えられます。
この小さな平面の明るさは、光が来る方向(図では垂直方向)に対して平面が直角(図では水平)になっているとき、最大になります。
平面が傾くにつれて暗くなっていき、面が垂直になったときに明るさはゼロになります。
この明るさは、ある長さの棒を平面に対して垂直に立てるとき、その棒の両端の座標のZ値の差分(図の赤い矢印)に比例します。
pc31-2.jpg

プログラムにするために、上記を数式に直してみます。
図では上から光が来ていますが、この上方向をZ軸とします。
そして、球面の中心を原点とします。
球面上の点は、半径をrとすると

 x2+y2+z2=r2

を満たします。

このとき、シェーディングに必要な値である赤い矢印の長さは、実は球の半径とzの値だけで決まります。
直感的にはちょっとわかりにくいかもしれませんが、zが最大となるのは図の球面の一番高い部分(中央部分)で、このときx=y=0でz=rとなり、図の球面の両端部分ではz=0となるのはすぐわかると思います。

球面に垂直に棒を立てると、その棒は必ず球の中心を指します。
従って

・棒を斜辺とし、水平および垂直な辺を持つ直角三角形
・球面の中心から棒を立てた点までの線分を斜辺とし、水平および垂直な辺を持つ直角三角形

は相似の関係になります。
すると、

・棒の長さと、赤い矢印の長さの比
・球面の半径と、棒を立てた点のZ座標値の比

が等しいことが言えるので、棒の長さが1のとき、赤い矢印の長さは(棒を立てた点のZ座標値/球面の半径)となります。

以上を用いて、球を光源側から見た場合(図では上方向から見た場合)の画像を描いてみます。
先に書いたとおり、球面の方程式は

 x2+y2+z2=r2

です。
従って半径rの球面を描くには、x,yを変化させて、それに対応するzの値を

 z=SQR(r2-x2-y2)

で求め、zの値に応じて(x,y)のピクセルの明るさを変化させます。

プログラムは以下のようになります。

CLS:GCLS
T=MAINCNT
DRAWSPHERE 100,20,170,255,255,176,0.2
PRINT (MAINCNT-T)/60;" SECONDS ELAPSED"

DEF DRAWSPHERE X0,Y0,SIZE,R0,G0,B0,GL
VAR R,RR,X,XX,Y,YY,Z,ZZ,YMAX,NZ,NY,C,R1,G1,B1
R=SIZE/2+0.5
RR=R*R
X0=X0+R
Y0=Y0+R
X=-0.08
WHILE R>=X
Y=X
XX=X*X
YY=XX
YMAX=RR-XX
ZZ=YMAX-YY
WHILE ZZ > 0
Z=SQR(ZZ)
NZ=Z/R
C=SHADING(NZ,1-GL,GL)
RGB_ADD 0,0,0,C,R0,G0,B0 OUT R1,G1,B1
C=RGB2(255,R1,G1,B1)
GPSET X0+X,Y0+Y,C
GPSET X0-X,Y0+Y,C
GPSET X0+X,Y0-Y,C
GPSET X0-X,Y0-Y,C
GPSET X0+Y,Y0+X,C
GPSET X0-Y,Y0+X,C
GPSET X0+Y,Y0-X,C
GPSET X0-Y,Y0-X,C
Y=Y+1
YY=Y*Y
ZZ=YMAX-YY
WEND
X=X+1
WEND
END

DEF SHADING(VAL,DIF,AMB)
RETURN AMB+DIF*VAL
END

DEF RGB_ADD R0,G0,B0,K,R1,G1,B1 OUT R2,G2,B2
R2=MIN(255,R0+K*R1)
G2=MIN(255,G0+K*G1)
B2=MIN(255,B0+K*B1)
END

DEF RGB2(A,R,G,B)
IF RND(8) < (R AND 7) THEN R=R+8
IF RND(8) < (G AND 7) THEN G=G+8
IF RND(8) < (B AND 7) THEN B=B+8
RETURN RGB(A,R,G,B)
END


手続きDRAWSPHEREが描画処理の本体です。
パラメータは

・左端のX座標
・上端のY座標
・直径
・色(R,G,B)
・環境光係数

となっています。

球のサイズは半径ではなく直径で指定しています。
球は一辺が直径に等しい正方形の領域に描画されます。

最後のパラメータ「環境光係数」には、通常0.1〜0.2程度を指定します。
このパラメータは球全体の明るさを底上げします。
1.0を指定すると、陰影のない塗りつぶした円が描画されます。

DRAWSPHEREの中身は2重のループになっています。
外側のループがX座標を1ずつ増やし、内側のループがY座標を1ずつ増やします。
(X0,Y0)が中心の座標です。

内側のループの中で、GPSETを8回連続して行っています。
今回描画する球では、明るさの値は原点を中心とした同心円上で同じ値になります。
従って下図のように、座標を入れ替えたり符号を反転することによって、一度の計算で8か所のピクセルの値が決まります。
全体の1/8を計算すれば十分なので、ループは図の網掛けした部分だけをカバーしています。
pc31-3.jpg

RGB_ADD手続きは、その名の通り色の加算(と定数倍)を行う手続きです。
色(R0,G0,B0)に色(R1,G1,B1)をK倍したものを加算し、結果を(R2,G2,B2)に代入します。

RGB2関数は前回も説明しましたが、疑似的に中間色を生成して色の変化を滑らかに見せます。

なお、ピクセルが球の内部か外部かを判定する際、境界値付近での判定を意図的に制御するために、変数の初期値等を小数点以下のオーダーで幾何学的な値よりも大きくしたり小さくしたりしています。
たとえば

・半径Rが直径の1/2よりも0.5大きい
・Xの初期値が0ではなく-0.08

などです。
いずれも幾何学的には、0.5や-0.1は不要ですが、特に径が小さい円を描くとき、形状に微妙な違いが出てきます。
掲載したプログラムは、直径が16ピクセルや14ピクセルの形状が私の好みのもの(下図)になるように調整しています。
(その代わり、直径15ピクセルを指定しても16ピクセルになってしまいますが・・・)
PC31-4.png


今回のプログラムは、球を正面から光で照らした場合の陰影を計算しています。
ちょうど、満月のようなものです。

次回はこのプログラムを少し拡張して、前回のレイトレーシングの例で示したような、斜め方向から照らした場合の陰影を計算してみます。
posted by boochow at 10:58| Comment(6) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事