3Dプログラミングで使う色々なベクトル計算を直観的に身体に覚えさせてあげますのよ【基本編】
【基礎編】に続いて、3Dプログラミングで使うことが多い基本的なベクトル計算を身体に覚えさせてあげますのよ!
spi8823.hatenablog.com
- 内積と外積
- 点と直線の距離
- 直線に下した垂線の交点へのベクトル
- 直線に下した垂線のベクトル
- 直線に対する線対称位置
- 点と平面の距離
- 平面に下した垂線のベクトル
- 平面に下した垂線の交点へのベクトル
- 平面に対する鏡面位置
- 鏡面反射ベクトル
- まとめ
直線に下した垂線の交点へのベクトル
「点と直線の距離」と同様の考え方で簡単に求められますわね!
直線に下した垂線のベクトル
こちらも「点と直線の距離」の式で絶対値を求める過程を抜くだけですわ!
向きは直線側から点への方向だから、符号には気を付けるんですのよ!
直線に対する線対称位置
「点と直線の距離」の応用ですわ!
点と直線の相対位置が分かったのだから、直線から点とは逆の方向に垂線を伸ばせばそれが対象な位置ですわね!
点と平面の距離
ある点と、点を通り法線がである平面との距離ですわ!
「点と直線の距離」の、直線の方向ベクトルを平面の法線ベクトルに読み替えて考えるのよ!あなたなら大丈夫ですわよね!(ただし直線に平行なのと、平面に垂直という風に微妙に関係性は変わっているわ)
平面に下した垂線のベクトル
「点と平面の距離」の応用ですわ!
平面に下した垂線の交点へのベクトル
これも「点と平面の距離」の応用すればすぐですわね!
平面に対する鏡面位置
「点と平面の距離」の応用でちょちょいのちょいですわ!!
まとめ
鷹揚な上流階級にはなれたかしら???
3Dプログラミングで使う色々なベクトル計算を直観的に身体に覚えさせてあげますのよ【基礎編】
初めに申し上げておきますわ!
お~っほほほほ!!
よくいらっしゃったわね!迷える子犬ちゃんたち!
この記事では3D空間上のオブジェクトを扱うのに便利なベクトル計算についてまとめたいと思いますのよ!
例えば、「ベクトルの回転」あるいは「面と点との距離」なんていうものに興味があるんじゃないかしら?
ベクトルというのはとてもよく考えられた仕組みで、こういったものが簡単に、たとえあなたのような子犬ちゃんにでも計算できるようになっているのよ。
そして私たちがやりたいと思う大抵のベクトル計算については公式を誰かが作ってくれているものよ。
え?じゃあその公式を使えばいいじゃないか、ですって?
シイイイィィッッット!!!!!!!
そんなだからいつまで経ってもヘボヘボのまんまなんですのよ!
貴族たるもの、いついかなる時でも優雅に、そして鷹揚としていなければなりませんわ。鷹揚としているにはどうすればいいのか。そう、応用ですの!!
覚えた公式をただコピペして使っているだけでは何かあったときに応用できずに鷹揚とできなくなるのですわ!!
Oh you!!応用!!鷹揚!!
でも安心なさい?迷える子犬ちゃんたちを救ってあげるのも淑女の役目というものですわ。
今からあなたたちにみっちりきっちり教えてあげますわ!
それも公式の導出方法ではなく、視覚的かつ直観的に身体に覚えさせてあげますのよ!
直観的に理解してしまえば、公式の導出なんてあとはちょちょいのちょいですの!
よろしくって?では行きますのよ!
基礎的事項の確認
以下、単純化のために2次元ベクトルで考えていきますわ。
ベクトルとは
ベクトルが何かも知らない人なんてここには来ていないでしょうけど、念には念を入れて一応説明しておきますのよ。
ベクトルとは「大きさと向きを持った量」である(presented by Wikipedia)
この一言に尽きますわね。「あっちの方にバーーっと」というのがベクトルですわ。
ただこんな言い方をされても一部の選ばれし人間を除くほとんどの場合は、あっち?どっち?そっち?となるのが関の山ですわ。
だから北に一条、東に三坊、という風にちゃんとした基準となる方向と大きさを決めて、その組み合わせで指定するのが貴族というものですわ。
ベクトルの世界では基準となる方向をX軸方向やY軸方向、大きさを数字(スカラーと呼びますの)で表すといった風に決めていますわ。
図のようなpというベクトルは数式でと表されることが多いんですの。
ベクトルを基準となる方向毎に分解したものをベクトルのX成分、Y成分という風に呼ぶことがあるわ。
それぞれベクトルを表す変数に添え字でどの方向に対する成分なのかを書いて、やといった風に書くことが多いわ。
ベクトルの操作
ベクトルに対する操作は、各成分に対する操作に分解できることが多いわ。
例えばベクトルの大きさを倍したい場合、と各成分を倍してあげればいいのよ。
ベクトルの回転
ベクトルの回転は最も基本的なベクトル操作の一つですわ。
その中でも「ある単位ベクトルがX軸方向を向くように回転させる」方法を理解しておけば応用が利きますのよ。頑張りなさい?
回転させる前のベクトルを としますわよ?
これ各成分, を辺に持つ長方形の対角線だと考えると、ベクトルの回転はこの長方形を回転させる操作だと考えることができるわね。
ここで各ベクトル同士の角度関係を考えると、回転によって長方形の各辺は次のように移動するわ。
イメージをつかみやすいように、下の図と一緒に見てみるとよいのですわ。
- 回転前の長方形にとってのX軸方向 → ベクトルのY成分がマイナスになった方向
- 回転前の長方形にとってのY軸方向 → ベクトルのX成分とY成分が入れ替わった方向
というわけで、「ベクトル方向からX軸方向への回転」を「各成分ベクトルの回転に分解」することができたわね。
そして、長方形を回転させるために各辺を回転させるという操作は、じゃなくても任意のベクトルに当てはめることができるわ。
したがって同じ方法を使って、別のベクトルに対して、ベクトルをX軸方向に回転させるような操作を行うこともできるわ。
式を書くと説明が長くなってしまったのだけれど要するに、
- 任意のベクトルの回転は、各成分ベクトルの回転に分解することができる
- 各成分ベクトルの回転は、回転の度合いを表すベクトルの成分を一部マイナスにしたり、X成分とY成分を入れ替えたものをかけることで実現できる
と言えば直観的に理解できるんじゃないかしら?
内積
内積とは、2つのベクトルがあったとき、を方向とに直角な方向の成分に分解して方向成分の大きさを求めるという計算ですわ。
(厳密に言えばこれはが単位ベクトルだった場合の説明ですわ。そうでない場合、方向成分の大きさにの大きさだけかけたものが求まりますわ)
演算子はドットで表され、次のような式になりますの。
言い換えれば、ベクトルの先端からベクトルの線分上に垂線を下した交点と原点との長さを求めていることになりますわね。
ただし、任意のベクトルとを考えてしまうとどうしても話がややこしくなるから、両方に対してベクトルをX軸まで回転させるような操作をしてから考えれば、あとはベクトルの先端からX軸に対して垂線を下した交点と原点との距離、つまりのX成分を求めるという話に単純化することができるわ。
これまた具体的なイメージは下の図を見なさい。
そしてこれは「ベクトルの回転」でやった内容と全く同じ操作をしてX成分だけ抜き取ることになりますのよ。
回転の式の上部分だけを見ればとなっているでしょう?これが内積の値ですのよ。
外積
外積がベクトルの基礎の中では一番直観的に理解するのが難しいですわね……。
何しろ2次元の図では説明できないのだから、とてもイメージがし辛いわ。
外積とは、2つのベクトルが与えられたとき、どちらのベクトルにも垂直でかつ大きさがを2辺とする平行四辺形の面積であるベクトルを求める計算ですわ。
(ただしこれは3次元の場合で、2次元上では1つのベクトルから1つのベクトルを、4次元上では3つのベクトルから1つのベクトルをというように次元によって計算に必要なベクトルの数が増えていくようですのよ)
演算子はクロスで表され、次のような3次元ベクトルの式になりますの。かける順番、引く順番を間違えると正しい値を求められないので注意しなければなりませんわよ。
を2辺とする平行四辺形の面積
まずはこちらを考えてみましょう。
実はこれ、内積とよく似た操作を行っているのよ。
をXY平面上に落とし込んだベクトルとの各成分同士の計算によって、クロス積のZ成分が定められているわね。
これ、どこかで見た記憶はないかしら?
そうよ!えらいわ!「ベクトルの回転」の式の下部分だけ見れば同じような式になっているわね!(正負が入れ替わっているのは計算の順番の関係だから適宜読み替えて頂戴!)
つまり、ベクトルを回転させたY座標を求めている、というわけですわね!
の大きさが1なら、これはそのままとが成す平行四辺形の大きさを求めていることになるんですのよ!
ベクトルに垂直な向き
これが外積の難しいところね。
けど、これも2次元で考えればある程度直観に落とし込むことができるわ。
2次元上のベクトルに垂直なベクトルを求めるには「X成分とY成分の比を入れ替えたベクトルを、Y軸(X軸でも可)に対してひっくり返す」という操作を行ってあげればいいのよ。
この考え方を3次元にも応用すると、「XY成分、YZ成分、ZX成分の比を入れ替えたベクトルを、Z軸(X軸、Y軸でも可)に対してひっくり返す」という操作になるわ。
「XY成分」と呼んでいるのは、2次元上では軸に対して垂線を引いていたところを、3次元上では面に対して垂線を引く必要があるからね。
そしてXY成分というのがまさにをXY平面上に落とし込んだ時に成す平行四辺形の面積で表されるのよ。この値をZ成分に入れることが「成分の比を入れ替える」という操作になっていますのよ。
そして「軸に対してひっくり返す」という操作の結果、外積の式で表されるような引き算の順番になっているのですわ。
【Cg Programming/Unity】テクスチャ基礎 ~ テクスチャのアルファ値を活用する【順番にやっていく】
お~~~~~ヱ”ッほっほっほっけほっけほっ!
今日はこちらのチュートリアルをやっていきますわよ!!
en.wikibooks.org
en.wikibooks.org
この節では、テクスチャのアルファ値を活用して描画する方法を学びますわよ!
前回の内容が前提になるのでまずはそちらから見るのですわよ!せっかちなのは乙女に嫌われますの。
spi8823.hatenablog.com
前回はテクスチャのRGB値だけを使って描画していたのでしたわね!
そんなことじゃあ立派な貴族にはなれませんわ!わたくしのように第四の色を使いこなせないことには、社交界でみっともない姿をさらすだけですのよ!
この間なんて、全身アルファ値ゼロの服を纏った殿方が乱入してきて大変な騒ぎだったんですから!あなたはあんなことにならないように、ちゃんとここで勉強しておくのですわよ!
アルファ値を活用したテクスチャの描画
というわけで、あなたにはコレを渡しておきますわ。
何って、陸地の部分のアルファ値を1、海の部分のアルファ値を0にした地球の画像ですわよ。どうせ貧乏なあなたのことだから画像の一枚も持っていないと思って執事に用意させたのですわ。まったくもう、いい加減自分で画像くらい用意できるようになりなさい!
というわけで、まずは前回と同じように球の表面にテクスチャを貼り付けるんですのよ。もう一人で出来るわよね?
するとこんな風になったかしら?
こんな闇に包まれた地球なんて嫌ですけれども、今はこれが正しいんですのよ。海の部分は青色の代わりに(0, 0, 0, 0)となっていますの。このうちアルファ値を無視しているのだから真っ黒くなるのは当然ですわね。
テクスチャの透過
ではまずは一番オーソドックスなアルファ値の活用方法、アルファブレンディングをやってみるのですわ。
と言ってもtex2D
関数で取得してピクセルの色として出力しているテクスチャの色情報にはすでにアルファ値が含まれているんですの。ですから、頂点シェーダやフラグメントシェーダはそのままpassをコピペして、あとは適切にアルファブレンディングを指定してやるだけで出来ますわね。
アルファブレンディングについて忘れてしまったという困った人はこの記事を見るといいのですわ!
さて、上の闇地球儀をアルファブレンディングして描画するとこんな風になりますわ。日本の裏側に南米大陸が透けて見えていますわね。
ブラジルの人~~~~!!聞こえるかしら~~~~~?!
コホン……、とまあこんな感じでテクスチャから色を取得・出力すること以外はやってることは変わらないですわね。
これだけじゃあ面白くないのでもう少しアルファ値で遊んでみるのですわ。
アルファ値を反射に適用
アルファ値は何もアルファブレンディングだけに使わないといけないわけではありませんの。
例えば、アルファ値が0のところだけ反射を描画するといったこともできますのよ。
この記事でやっているように反射による表面の色を計算して、アルファ値が0の場合には出力に反射の色を足してやればいいんですのよ。
ざっくり言えばこんな感じのフラグメントシェーダを書けばいいだけですのよ。
float4 f(v2f input) : COLOR { float4 texColor = tex2D(_MainTex, input.texcoord); float4 reflectionColor = getReflectionColor(input); return texColor.a == 0 ? float4(reflectionColor.xyz, 0.7) : texColor; }
余談なのだけど、シェーダでif文を書くのはパフォーマンス的にあまり好ましくないのですわ。
その代わりに3項演算子を使えば問題ありませんのよ。
と、そんな感じで描画したのがこれになりますわ。
鏡面反射や環境光による色がアルファ値が0の部分、すなわち海の部分だけに描画されているのがわかりますわね!
コード
Shader "Custom/TransparentTextures" { Properties { _MainTex ("Main", 2D) = "white" {} } SubShader { Tags { "Queue" = "Transparent" } //CGINCLUDE~ENDCGの部分に書いた関数や構造体はSubShader内で共通のものとして使うことが出来ますのよ! //複数のパスに同じようなコードを長々と書く必要がある場合にはこのように共通処理としてまとめて差し上げればすっきりしますのよ! CGINCLUDE uniform sampler2D _MainTex; struct vertexInput { float4 vertexPos : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 sv_position : SV_POSITION; float2 texcoord : TEXCOORD0; float4 vertexPos : TEXCOORD1; float3 normal : TEXCOORD2; float4 ambientColor : TEXCOORD3; }; //法線ベクトルから環境光の計算をしてますの! float4 getAmbientColor(float3 normal) { float4 upAmbient = lerp(unity_AmbientEquator, unity_AmbientSky, normal.y); float4 downAmbient = lerp(unity_AmbientEquator, unity_AmbientGround, -normal.y); float4 ambientColor = upAmbient * step(0, normal.y) + downAmbient * step(normal.y, 0); ambientColor = ambientColor; return ambientColor; } //頂点シェーダのインプットからフラグメントシェーダに受け渡す値をとりまとめる関数ですわ! v2f vertexInput2Output(vertexInput input) { v2f output; output.sv_position = UnityObjectToClipPos(input.vertexPos); output.texcoord = input.texcoord; output.vertexPos = input.vertexPos; output.normal = input.normal; output.ambientColor = getAmbientColor(output.normal); return output; } //フラグメントシェーダのインプットから鏡面反射光の色を計算しますわ! float4 getReflectionColor(v2f input) { float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos); float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz; cameraDirection = normalize(cameraDirection); float4 incidentVector = normalize(-_WorldSpaceLightPos0); float isValid = step(dot(incidentVector, input.normal), 0); float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz); specularDirection = normalize(specularDirection) * isValid; float intensity = dot(cameraDirection, specularDirection.xyz); intensity = max(0, intensity); intensity = pow(intensity, 20); float4 color = float4(intensity * float3(1, 1, 1) + input.ambientColor, 1); return color; } //フラグメントシェーダで出力すべき色を計算しますの! float4 getFragmentColor(v2f input) { float4 texColor = tex2D(_MainTex, input.texcoord); float4 reflectionColor = getReflectionColor(input); //3項演算子を使って、アルファ値が0の場合は反射光を、それ以外の場合はテクスチャの色を出力しますのよ! return texColor.a == 0 ? float4(reflectionColor.xyz, 0.7) : texColor; } ENDCG pass { //まず裏面の描画 ZWrite Off Cull Front Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex v #pragma fragment f v2f v(vertexInput input) { return vertexInput2Output(input); } float4 f(v2f input) : COLOR { return getFragmentColor(input); } ENDCG } pass { //次に表面の描画 ZWrite Off Cull Back Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex v #pragma fragment f v2f v(vertexInput input) { return vertexInput2Output(input); } float4 f(v2f input) : COLOR { return getFragmentColor(input); } ENDCG } } }
アルファ値を活用したテクスチャの描画は以上になりますわ。
これであなたも社交界で人気者ですわね!
【Cg Programming/Unity】テクスチャ基礎 ~ テクスチャを描画する【順番にやっていく】
お~~~~っほっほっほっほっ!!!
庶民のみなさま!今回はこちらをやっていきますのよ!
en.wikibooks.org
en.wikibooks.org
この節では3Dオブジェクト上にテクスチャを描画する方法を学ぶそうですのよ?
テクスチャって何ですかですって!?テクスチャはテクスチャですのよ!!
テクスチャの描画
真っ白な球体を眺めているのはもう飽きてきたんじゃありませんこと?え、飽きてない?物好きなのねあなた……。
どうせまん丸球体を転がすなら、ど~んとでっかく地球ぐらいは転がしてあげようじゃないですの!そう、わたくしの手のひらの上で滑稽に回り続けるあなたたちのように転がしてあげるわ!
テクスチャの用意
まずすることはと言えば、そうですの。地球の画像をどこかから持ってくるのですわ!
めんどくさい?もう、相変わらずしょうのない人ね……。そんなこともあろうかと、うちの執事に用意させたからココからダウンロードしてきなさい。
忘れないうちにわたくしに感謝しておくこと。いいわね?
テクスチャの登録
用意したテクスチャをシェーダから使うには、シェーダ(を使用するマテリアル)にテクスチャを登録しないといけませんわね。
シェーダに外部から値を渡してあげるにはどうすればいいか覚えてらして?あら、ダチョウ頭のあなたにしては珍しくちゃんと覚えているじゃない。えらいわ。
そう、シェーダのPropertiesにパラメータを用意して、CGコードの中で同名のuniform変数を宣言するんでしたわね!
今回は_TestTex
という名前にいたしますわ!
Properties
この名前でPropertiesにテクスチャのパラメータを定義するには次のように書くといいんですの!
Properties { _TestTex ("Test", 2D) = "white" {} }
2D
というのがテクスチャを表していますわ!
"white"
としたことでデフォルトでは真っ白な画像が使用されますわ!
最後の波かっこは昔の記法の名残のようで、今のバージョンではもう意味が無いそうですわ。
uniform変数の宣言
Propertiesで定義したパラメータをCGコードから使うにはuniform変数を宣言しなければならないんでしたわね!
CGコードではテクスチャはsampler2D
という型になっていますのよ!
ですから、宣言は次のようになりますわね!
uniform sampler2D _TestTex; uniform float4 _TestTex_ST;
Tiling, Offset
あら、気づいたみたいね。_TestTex_ST
とはなんだ、でしょう?
この変数にはテクスチャのTiling
やOffset
の値が入っているのですわ!
Propertiesでテクスチャを定義すると、テクスチャと一緒にTiling
やOffset
という2次元ベクトルを一緒に設定できるようになるのですわ!
テクスチャの変数名の後ろに_STと付けたuniform変数にはこれらのベクトルの値が入るようになっているんですの。
Tiling
はテクスチャの繰り返し描画、Offset
はテクスチャの初期位置をずらすために用いられることが多いのですわよ!
テクスチャ座標
さて、これでシェーダからテクスチャを参照することができるようになりましたわ!
あとは、テクスチャ上の色を取得して、ピクセルの色として出力すればいいだけですわね!
それにはもう一つ、色を取得すべきテクスチャ上の座標の情報が必要なんですわよ。
そんなのどうやって計算すればいいのか、と困惑しなくてもよろしくってよ!
うちの執事に計算させたテクスチャ座標をTEXCOORD0
というセマンティクスを使って頂点シェーダのインプットとして渡すようにしておいたわ!
テクスチャ座標の値は頂点シェーダからフラグメントシェーダへそのまま受け渡すことが多いんですの。
このとき、これまたTEXCOORD0
というセマンティクスを使って受け渡すことが多いんですけれど、頂点シェーダのインプットとしてのTEXCOORD0
と、頂点シェーダからフラグメントシェーダへ受け渡す際のTEXCOORD0
は別のものだということは気を付けておきなさい。
テクスチャの色の取得
テクスチャ座標が取得できればあとはテクスチャ座標を使ってテクスチャ上の色を取得するだけですわね!
テクスチャ座標がtexcoord
という変数名だったなら、その座標の色は次のように取得できるわ!
float4 color = tex2D(_TestTex, texcoord);
驚くほど容易いですわね!
描画結果
実際に球に地球を描画いたしましたわ!
これの半分くらいはうちの土地ですの!
え?のっぺりしてる、ですって?そりゃそうなのですわ。だってライティングをしてないんですもの。
んもう、そう言うかと思って拡散反射で描画するテクスチャの色を変える方法もやってあげたのですわ!
日本の夜明け、ですわね!
コード
今回使ったコードですわ。
ありがたく拝見することね!
Shader "Custom/TexturedSpheres" { Properties { _TestTex ("Test", 2D) = "white" {} //テクスチャのパラメータ定義ですわ! } SubShader { pass { CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" uniform sampler2D _TestTex; uniform float4 _TestTex_ST; struct vertexInput { float4 vertexPos : POSITION; float4 texcoord : TEXCOORD0; //頂点シェーダにインプットとして渡されるテクスチャ座標ですわ! float3 normal : NORMAL; }; struct v2f { float4 sv_position : SV_POSITION; float2 uv_position : TEXCOORD0; //テクスチャ座標ですわ!vertexInputのtexcoordとは違うから注意するのですわよ! float diffuseRatio : TEXCOORD1; //拡散反射の度合いですわ! }; v2f v(vertexInput input) { v2f output; output.sv_position = UnityObjectToClipPos(input.vertexPos); output.uv_position = input.texcoord; //フラグメントシェーダにそのまま受け渡しますわ! output.diffuseRatio = dot(UnityObjectToWorldNormal(input.normal), normalize(_WorldSpaceLightPos0.xyz)); //拡散反射の計算をしますわ! return output; } float4 f(v2f input) : COLOR { //テクスチャの色を取得しますわ! //Tilingの値は_TestTex_ST.xy、Offsetの値は_TestTex_ST.zwに入っているんですの! float4 texColor = tex2D(_TestTex, input.uv_position.xy * _TestTex_ST.xy + _TestTex_ST.zw); return texColor * pow(max(0, input.diffuseRatio), 2); //テクスチャの色に拡散反射の値をかけて出力しますわ! } ENDCG } } }
【Cg Programming/Unity】ライティング基礎 ~ 鏡面ハイライト②「環境光」【【順番にやっていく】
前回に引き続きこちらのチュートリアルをやっていきますわ!
en.wikibooks.org
前回は「鏡面反射」で、今回学ぶのは「環境光」になりますのよ。
環境光
え?環境光とは何かですって?
またですの?もう、しょうがありませんわね!
高貴なるものの義務としてわたくしがじっくり丁寧に教えて差し上げますのよ!
環境光とは
前節でやった拡散反射や、前回の鏡面反射については覚えていらして?
三歩進めば忘れてしまう鳥頭のあなたにもわかるようにそれぞれ描画した画像を再掲して差し上げますわ!そのダチョウみたいに大きなお目目でしっかりと見ておきなさい!
これが拡散反射↓
こっちが鏡面反射↓ですのよ。
ダチョウ並みの脳みそでも見ればわかるでしょうけど、どちらも光の当たっている部分は真っ黒になっていますの。
でも考えてみなさい?現実では光源から直接光が当たっていなくてもモノが見えるわ。ほら、電気を消した部屋の中でも光源はないけれどちゃんとモノが見えているでしょう?
そんな場合には太陽光が一度だけ反射した光ではなく、外の建物の壁や地面に反射したうえで、例えば窓から入った光がさらに部屋の壁、天井と何度も複雑に反射した光が私たちの目に届いているの。これを「環境光」と言いますのよ。
そんなのどうやって計算するのか、ですって?あなたにしては珍しくまともな質問をするのね。
ええ、そうですわ。複雑に反射してカメラに到達する光を簡単に計算する方法は無いですのよ。これを計算するには、光源からたくさんのRayを飛ばして起こりうるすべての反射を計算するRay Tracingというものをする必要がありますの。だけどそんなに重たい処理を気楽にさせられるほど計算資源は潤沢でないというのが庶民の悲しいところですわね。みんながみんな、わたくしのように個人用のスーパーコンピュータを持っているというわけでもないでしょうし……。
環境光の近似
一様な環境光
そこで一番初めに思いつくのがどの場所、どの角度でも一様な環境光が当たっていると考える近似方法ですの。
この場合、環境光の色をそのまま出力に足してあげればいいので簡単ですわね。
Unityでは環境光の値は[Window]->[Rendering]->[Lighting]で開いたウィンドウの[Environment]タブの中にある[Environment Lighting]という項目で設定することができるんですのよ。
今考えている「一様な環境光」は[Environment Lighting]->[Source]を「Color」にして設定できる[Ambient Color]から変更できますの。
ここで指定した値はシェーダのコードの中からUNITY_LIGHTMODEL_AMBIENT
で取得できるんですのよ。
グラデーションする環境光
でも一様な環境光というのはそこまで現実的ではないんですのよ。だってほら、空は青いし、地面は茶色みたいなくすんだ色をしていることが多いじゃない。だから上を向いている面には青い環境光が、下を向いている面には茶色い環境光が差し込むとしたほうがよりそれらしく見えるわ。
こんな風に面の角度によってグラデーションする環境光も[Environment Lighting]から設定できますのよ。
[Source]を「Gradient」にすればほら、[Sky Color], [Equator Color], [Ground Color]が設定できるでしょう?これはそれぞれ「空の色」「水平線の色」「地面の色」を表しているの。これら3色を面の向きに合わせてグラデーションさせてあげれば、よりそれっぽく見えますの。
シェーダからはそれぞれunity_AmbientSky
, unity_AmbientEquator
, unity_AmbientGround
で取得できますわ。
Skyboxによる環境光
環境光にはSkyboxも指定できるんですの。
これを設定するとまさしく空の色として使っているSkyboxの色が環境光になるようですのよ。
デフォルトのシェーダではこちらを使って綺麗に描画してるのですけれど。ごめんなさい、これをシェーダから使用する方法は見つけられなかったわ。
というわけで今回は2番目の「グラデーションする環境光」を使った描画をすると次のようになりますのよ!
少しずつ「リッチ」な見た目に近づいてきたんじゃありませんこと?もちろん、わたくしのおうちのおリッチさには到底及びもしませんけれどね!おほほほほ!!
ついでにDiffuse Reflectionも上乗せした描画結果も置いておきますわ!
よくある見た目になったんじゃありませんこと?
コード
グラデーションする方法についてはコード中に詳しく書いてあるのでしっかりと感謝しながら読むんですのよ!
前回の鏡面反射のコードも一緒に書いてあるので気を付けて読むといいのですわ。具体的には頂点シェーダの後半部分が今回の「環境光」の部分ですのよ。
Shader "Custom/SpecularHighlights" { Properties { _SpecularRatio ("SpecularRatio", float) = 1 _AmbientRatio ("AmbientRatio", float) = 0.5 //環境光の度合い(大きいほど濃く環境光が描画されますわ) } SubShader { pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" uniform float _SpecularRatio; uniform float _AmbientRatio; struct v2f { float4 sv_position : SV_POSITION; float4 vertexPos : TEXCOORD0; float3 normal : TEXCOORD1; float4 ambientColor : TEXCOORD2; }; v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL) { v2f output; output.sv_position = UnityObjectToClipPos(vertexPos); output.vertexPos = vertexPos; output.normal = normal; //環境光の計算ですわ! float4 upAmbient = lerp(unity_AmbientEquator, unity_AmbientSky, normal.y); //空の色と地平線の色を法線の上向き加減で補間しますの float4 downAmbient = lerp(unity_AmbientEquator, unity_AmbientGround, -normal.y); //地面の色と地平線の色を法線の下向き加減で補完しますの //法線が上を向いている時はupAmbientを、下を向いているときはdownAmbientを使いますの。 //シェーダでは並列計算を行っているのでif文はあまり推奨されませんの。その代わりにstep関数を使えば同じようなことができますのよ! output.ambientColor = upAmbient * step(0, normal.y) + downAmbient * step(normal.y, 0); output.ambientColor = output.ambientColor * _AmbientRatio; //環境光の度合いをかけてるだけですのよ return output; } float4 f(v2f input) : COLOR { float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos); float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz; cameraDirection = normalize(cameraDirection); float4 incidentVector = normalize(-_WorldSpaceLightPos0); float isValid = step(dot(incidentVector, input.normal), 0); float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz); specularDirection = normalize(specularDirection) * isValid; float intensity = dot(cameraDirection, specularDirection.xyz); intensity = max(0, intensity); intensity = pow(intensity, _SpecularRatio); float4 color = float4(intensity * float3(1, 1, 1) + input.ambientColor, 1); return color; } ENDCG } } }
【Cg Programming/Unity】ライティング基礎 ~ 鏡面ハイライト①「鏡面反射」【【順番にやっていく】
皆様、お久しゅうございますわね!
ええ、今日はこちらのチュートリアルをやっていきますわ!
en.wikibooks.org
この節では「鏡面反射」と「環境光」について学びますのよ。
長くなるとお肌に悪いから前半と後半に記事を分けますのよ!
今回は「鏡面反射」についてやりますの。
鏡面反射
「鏡面反射」とは何か、ですって!?あなた、本気でそんなことをおっしゃってますの??そんなだからいつまでたってもうだつが上がらないんですのよ!!
もう、しょうがないですわね……、よろしくって?一度しか言わないからよくお聞きなさい。
鏡面反射とは
「鏡面反射」というものは「入射角と反射角が等しくなるような反射」をいうんですの!その名の通り、光が鏡で反射するときに「鏡面反射」してるんですのよ。まるでわたくしのおうちの床の大理石みたいにピカピカなんですの!
Wikipediaからわざわざ概念図を引っ張ってきて差し上げましたのよ、ふふん!これでも見てしっかりと理解することね!ふふん!
この図でいうとPが光源、ベクトルPOが入射ベクトルに当たりますわね。光源からぴったり鏡面反射したベクトルOQを次からは「鏡面反射ベクトル」と言うことにしますわ!この私が決めたのだから、しっかりと覚えておきなさい。いいですわね?
鏡面反射ベクトルの計算
そんな鏡面反射ベクトルは光源からの入射ベクトル、反射面の法線を使って以下のように計算できるんですの。
鏡面反射ベクトル = 入射ベクトル + 法線ベクトル×2×(入射ベクトル・法線ベクトル)
え?どうしてこんな式になるのか、ですって?
仕方がありませんわ。このわたくしが自ら筆を取ってわかりやすく図を書いてあげますの。
(iPadを買ったからお絵かきツールを使ってみたいだけなんてことは絶対にありませんのよ!)
これもノブレス・オブリージュというもの、顔を上げてもよろしくってよ?
男っぽい文字ですって?!傷つきますわ!!
3D面上の点Oに入射してきた光が鏡面反射ベクトルの方向にだけ反射されることを「鏡面反射」と言いますの。その場合、鏡面反射ベクトル上にカメラがあれば白、そうでなければ黒を描画してやればいいのですわ。
ただ、実際にはもう少し広がりを持った反射をすることが多いのですわ。その場合でもカメラ方向と鏡面反射ベクトルの方向が近いほどカメラに入る光の量は多くなるんですの。
え?方向が近いかどうかはどうやって計算するのか、ですって?そんなの「内積をとる」に決まってますのよ!
そういうわけで、鏡面反射を描画するには下のような値であらわされる明るさを各ピクセルの色に足してあげればいいんですの!
鏡面反射による明るさ = 元の光の強さ×(鏡面反射ベクトル・カメラ方向ベクトル)
鏡面反射成分の描画
上の方法で描画してみたものがこちらですのよ!
Diffuse Reflectionのときと違って鏡面反射の光が届くごく一部分しか明るく見えないのがわかりますわね!まるでわたくしのおうちの床の大理石みたいにピッカピカですわ!
ただ、なんだか変な格子模様が見えますわね……。
どうやら頂点シェーダ側ですべて計算してしまうと、フラグメントシェーダ側に渡す値の補間の関係で変なことになってしまっているようですわね。
頂点シェーダではローカル座標や法線を受け渡すだけにして、反射の計算はフラグメントシェーダでやってあげることにするとうまく描画できましたわ!
(あまりフラグメントシェーダで重たい計算はやりたくないのですけれど、何か他に方法はないんですの?)
どうやらこうやって法線ベクトルを補間して描画する技法を「フォンシェーディング」と呼ぶらしいんですの。
どこかでのタイミングで詳しく調べたいですわね。
ja.wikipedia.org
鏡面反射の度合い
実は、鏡面反射ベクトルとカメラ方向ベクトルの内積を反射の強度として使ってしまうと少し広がりすぎるんですの。
もっとくっきりはっきり鏡面反射らしさを出してあげるには、内積をある程度大きい値でべき乗してあげればいいんですのよ。(上の画像も実は内積を2乗した値を使ってますの)
内積を20乗した値を使ったのと、1乗した値を使ったのがそれぞれ以下の画像ですのよ。
上はピカピカ、下はのっぺりって感じですわね!
こんな風に、鏡面反射の度合いを変えることで質感にも差が出てくるのは覚えておくといいですわよ!
コード
というわけでわたくしが今回書いたコードですわ!
反射の計算を頂点シェーダでやったパターンとフラグメントシェーダでやったパターンを両方載せてありますの。
Shader "Custom/SpecularHighlights" { Properties { _SpecularRatio ("SpecularRatio", float) = 1 //鏡面反射の度合い(大きいほどはっきりした鏡面反射になる) } SubShader { pass { //頂点シェーダ側で反射の計算をやるパターンですの! Tags { "LightMode" = "ForwardBase" } //簡単のため、平行光源のみを考えますわ! CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" uniform float _SpecularRatio; struct vertexInput { float4 vertexPos : POSITION; float3 normal : NORMAL; }; struct v2f { float4 sv_position : SV_POSITION; float4 color : TEXCOORD0; }; v2f v(vertexInput input) { //頂点シェーダで反射の計算をやってしまうんですの! v2f output; output.sv_position = UnityObjectToClipPos(input.vertexPos); float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos); float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz; cameraDirection = normalize(cameraDirection); //カメラ方向 float4 incidentVector = normalize(-_WorldSpaceLightPos0); //入射ベクトルは光源方向の反対向き float isValid = step(dot(incidentVector, input.normal), 0); //光が面の裏側にある場合(入射ベクトルと法線の内積が負)の場合は0、そうでなければ1 float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz); specularDirection = normalize(specularDirection) * isValid; //鏡面反射ベクトル float intensity = dot(cameraDirection, specularDirection.xyz); //カメラ方向と鏡面反射ベクトルの内積 intensity = max(0, intensity); //内積が負の場合は0にする intensity = pow(intensity, _SpecularRatio); //より大きな値でべき乗してあげるとより強い鏡面反射になるんですのよ! output.color = float4(intensity * float3(1, 1, 1), 1); return output; } float4 f(v2f input) : COLOR { //フラグメントシェーダでは色の出力だけやりますわ! return input.color; } ENDCG } pass { //フラグメントシェーダ側で反射の計算をやるパターンですの! Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" uniform float _SpecularRatio; struct v2f { float4 sv_position : SV_POSITION; float4 vertexPos : TEXCOORD0; float3 normal : TEXCOORD1; }; v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL) { //頂点シェーダでは値の受け渡しだけやりますわ! v2f output; output.sv_position = UnityObjectToClipPos(vertexPos); output.vertexPos = vertexPos; output.normal = normal; return output; } float4 f(v2f input) : COLOR { //フラグメントシェーダで反射を計算してあげるんですの! float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos); float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz; cameraDirection = normalize(cameraDirection); //カメラ方向 float4 incidentVector = normalize(-_WorldSpaceLightPos0); //入射ベクトルは光源方向の反対向き float isValid = step(dot(incidentVector, input.normal), 0); //光が面の裏側にある場合(入射ベクトルと法線の内積が負)の場合は0、そうでなければ1 float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz); specularDirection = normalize(specularDirection) * isValid; //鏡面反射ベクトル float intensity = dot(cameraDirection, specularDirection.xyz); intensity = max(0, intensity); intensity = pow(intensity, _SpecularRatio); float4 color = float4(intensity * float3(1, 1, 1), 1); return color; } ENDCG } } }
次回
次回は「環境光」の描画を学びますの!しっかりとついてくることね!
spi8823.hatenablog.com
【Cg Programming/Unity】Basic Lighting ~ Diffuse Reflection【順番にやっていく】
今回はこちら。
en.wikibooks.org
この節ではDiffuse Reflection、つまり「拡散反射」というものの描画方法について学ぶ。
拡散反射
拡散反射とは何か。例に上がっているのは月面の反射である。
月の表面は非常に凹凸が激しいがゆえに、入射した光は入射時の角度の記憶を失い、完全にランダムな方向へ反射される。
つまり、一定の光線が入射している面をどの角度から見ようと、その明るさは変わらないというわけだ。言い換えれば前節において_WorldSpaceCamraPos
として使用したような視線方向には関わらない描画となる。
一方で、ある面が感じる光の強さというものは、その面に対して垂直に光線が入射するほど大きい。つまり車のヘッドライトを横から見ると別に何ともないが、真正面から浴びせられると目が潰れてしまうほど強く光を感じるのと同様である。
面とベクトルが垂直であるかはそのベクトルと、面の法線ベクトルの内積の絶対値が1に近いかどうかで判別できるのだった。
そして一定の光量下で単純な拡散反射を考えた場合、物体が見える明るさは光線のベクトルと物体の面の法線ベクトルとの内積によってのみ決まる。簡単に書くと以下のようにあらわせる。
物体の明るさ = dot(面の法線ベクトル, 光線の方向ベクトル)
ここまでわかれば後はシェーダで書くだけだ。
コード
Shader "Custom/DiffuseReflection" { SubShader { pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" struct v2f { float4 sv_position : SV_POSITION; float4 color : TEXCOORD0; }; v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL) { v2f output; output.sv_position = UnityObjectToClipPos(vertexPos); float4 worldPos = mul(unity_ObjectToWorld, vertexPos); //頂点のワールド座標 float3 worldNormal = UnityObjectToWorldNormal(normal); //面の法線(UnityCG.cgincの関数を私用) //光源への向きを計算 float3 lightDirection = _WorldSpaceLightPos0; //Directional Light(太陽光のようにすべての空間に一定の方向で光が降り注ぐ)の場合はこう //float3 lightDirection = normalize((_WorldSpaceLightPos0 - worldPos).xyz); //Point Light(点光源、つまり一点から等方的に光を発している)場合はこう //反射の度合いを計算 //光源への向きと面の法線が同じ方向を向いているほど大きくなる float reflection = dot(lightDirection, worldNormal); output.color = float4(float3(1, 1, 1) * max(reflection, 0), 1); return output; } float4 f(v2f input) : COLOR { return input.color; } ENDCG } pass { Tags { "LightMode" = "ForwardAdd" } Blend One One CGPROGRAM #pragma vertex v #pragma fragment f #include "UnityCG.cginc" struct v2f { float4 sv_position : SV_POSITION; float4 color : TEXCOORD0; }; v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL) { v2f output; output.sv_position = UnityObjectToClipPos(vertexPos); float4 worldPos = mul(unity_ObjectToWorld, vertexPos); //頂点のワールド座標 float3 worldNormal = UnityObjectToWorldNormal(normal); //面の法線(UnityCG.cgincの関数を私用) //光源への向きを計算 //Directional Lightかどうかは_WorldSpaceLightPos0のwを見て判別する float3 lightDirection; if(_WorldSpaceLightPos0.w == 0) { lightDirection = _WorldSpaceLightPos0; //Directional Light(太陽光のようにすべての空間に一定の方向で光が降り注ぐ)の場合はこう } else { lightDirection = normalize((_WorldSpaceLightPos0 - worldPos).xyz); //Point Light(点光源、つまり一点から等方的に光を発している)場合はこう } //反射の度合いを計算 //光源への向きと面の法線が同じ方向を向いているほど大きくなる float reflection = dot(lightDirection, worldNormal); output.color = float4(float3(1, 1, 1) * max(reflection, 0), 1); //output.color = float4(lightDirection, 1); return output; } float4 f(v2f input) : COLOR { return input.color; } ENDCG } } }
_WorldSpaceLightPos
光源のワールド座標は_WorldSpaceLightPos0
というuniform変数で取得できる。
末尾に”0”が入っていることからわかるように、複数の光源がある場合は_WorldSpaceLightPos1
のようにインデックスを変えた変数名でアクセスする。
LightModeの指定
さて、ここで注意しておきたいのがpassの先頭でTags { "LightMode" = "ForwardBase/ForwardAdd" }
と指定している点である。
ライティングの最適化等の観点から、優先して描画すべきライトと、簡略化してもよいライトに分けられているようです。
(簡略化というのは例えば、複数のDirectional Lightを1つの行列で表してしまうもの(多分。。。))
何もしていなければ1つ目のDirectional LightはForwardBaseで描画され、2番目以降のDirectional LightやPoint LightはForwardAddで描画される?
(ちょっとこの辺どうなってるかはわからんのでそのうち詳しく調べることにする)
とにかく、ForwardBase
と指定したpassではDirectional Lightの描画処理を、ForwardAdd
と指定したpassにはどっちが来るかわからんので_WorldSpaceLightPos
のw座標に入っている値で判別するらしい。
そしてpassを2つ以上通るため、2つ目のpassではBlend One One
を指定して1つ目のpassの結果に加算するように描画している。
Directional LightとPoint Lightを1つずつ用意して描画したものは以下のようになる。
passを2つ以上書き始めるといよいよコードが長くなってきましたな。
次回はこちら。
spi8823.hatenablog.com