プチコン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のピクセルの明るさだけを計算すれば済みましたが、今回は全体の半分の計算が必要で、かつ計算自体も増えているためです。

コメント

  1. TE より:

    最近プチコン4を始め、こちらのサイトが大変に助けになっています。
    図形描画による3DやOOPなど自分の興味にもハマりました。
    貴重な情報本当にありがとうございます!

  2. boochow より:

    TEさま、コメントありがとうございます。
    こうして「役に立った」と言っていただくのは大変励みになります。
    プチコンも4代目になっているんですね。プチコンは、楽しみながら使えるという点で、今でも貴重なプログラミング環境だと思っています。

  3. TE より:

    本当に面白いですね、自分もArduinoやシンセなどを触るので大変興味深く拝見しております。
    これからもチェックさせていただきますね。