這篇文章寫於去年的暑假。大二的假期時間多,小組便開發一個手機遊戲的項目,開發過程當中忙裏偷閒地瞭解了Unity的shader編寫,而CG又與shaderLab類似,因此又閱讀了《CG教程》、《GPU 編程與CG 語言之陽春白雪下里巴人》學習圖形學的基礎。嘗試編寫unity shader時還惡補了些3D數學。這些忙裏偷閒的日子,壞了空調的悶熱的實驗室,還真是有點懷念。當時寫這些文章並非想做爲教程,只是本身的總結方便往後溫習,因此文章內容都很基礎。編程
2015/08/04 於工學一號館函數
OpenGL與Direct3D提供了幾乎相同的固定功能光照模型。什麼是固定功能光照模型?在過去只有固定繪製流水線的時候,該流水線被限制只能使用一個光照模型,也便是固定功能光照模型。該模型基於phong光照模型。在下面的這個例子裏,咱們使用一個「基本」模型對固定功能光照模型提供了簡化版本。這個基本模型的數學描述爲高級公式爲:post
surfaceColor = emissive + ambient + diffuse + specular性能
從式子能夠看出:物體表面的顏色是自發光(放射 emissive)、環境反射(ambient)、漫反射(diffuse)和鏡面反射(specular)等光照做用的總和。每種光照做用取決於表面材質性質(例如亮度和材質顏色)和光源的性質(例如光的位置和顏色)。學習
下面對這個基本模型的各個部分進行講解,最後咱們使用CG語言寫出該基本模型。3d
其中Ke表明材質的放射光顏色code
ambient = Ka * globalAmbientorm
其中ka是材質的環境反射係數,globalAmbient是入射環境光的顏色。blog
漫反射項表明了從一個表面相等地向全部方向反射出去的方向光。教程
以下所示:
用來計算漫反射項的公式爲:
diffuse = kd * lightColor * max ( N*L(點積) , 0 )
其中:
Kd是材質的漫反射顏色
lightColor 是燈光的顏色
N是標準化的頂點法向量
L是標準化的指向燈光的向量
P是被着色的點(以下圖)
這裏須要解釋一下
max ( N*L(點積) , 0 )
規範化的向量N和L的點積是兩個向量之間夾角的一個度量,夾角越小,P點受到更多的入射光照。而背向光源的表面將產生負數點積值,所以,公式**max ( N*L(點積) , 0 )使得背向光源的表面的漫反射光爲0,確保這些表面不會顯示漫反射光照。
**
鏡面反射的做用依賴於觀察者的位置,若是觀測值位於一個沒法接受反射光線的位置,觀察者將不可能在表面上看到鏡面反射強光。鏡面反射項受到了表面光澤度的影響,越有光澤度的材質表面的高光區越小,下圖從左到右材質光澤度遞增:
鏡面反射項的數學公式:
specular = ks * lightColor * facing * (max ( N * H ),0 )^shininess
其中:
ks是材質的鏡面反射顏色
lightColor是入射鏡面反射光的顏色。
N是規範化的表面法向量
V是指向視點的規範化的向量
L是指向燈源的規範化向量
H是v與l向量的中間向量
facing的取值爲0或1:當NL大於0時爲1,當NL小於0時爲0
p表示要着色的點
使用CG語言來實現上面所說的基本模型,代碼以下:
void BaseLight( float4 position :POSITION,//被着色點的位置 float3 normal : NORMAL, //表面在P點的標準化法向量 out float4 oPosition : POSITION, out float4 color : COLOR, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , //入射環境光顏色 uniform float3 lightColor , //燈光顏色 uniform float3 lightPosition, //燈光的位置 uniform float eyePosition, //攝像機位置 uniform float3 Ke, //Ke是材質的放射光(自發光)顏色 uniform float3 Ka, //Ka是材質的環境反射係數 uniform float3 Kd, //Kd是材質的漫反射顏色 uniform float3 Ks, //Ks是材質的鏡面反射顏色 uniform float shininess //材質表面光澤度 ) { oPosition = mul(modelViewPrij,position); float3 P = position.xyz ; float3 N = normal; //公式一計算放射光 float3 emissive = ke; //公式二計算環境光 float3 ambient = Ka * globalAmbient; //公式三計算漫反射光 float3 L = normalize (lightPosition - P); //L爲標準化指向燈光的向量。 float diffuseLight = max(dot(N,L),0); float diffuse = Kd *lightColor *diffuseLight; //公式四計算鏡面放射 float3 V = normalize(eyePosition - P); float3 H = normalize (L+V); float specularLight = pow(max (dot (N,H),0), shininess); if(diffuseLight < = 0) specularLight = 0; float3 specular = Ks * lightColor * specularLight ; //基本光照模型完成 color.xyz = emissive + ambient + diffuse + specular; color.w = 1; }
position.xyz
這種新語法是CG語言被稱爲重組的一個功能。重組容許你使用任何你選擇的方法從新安排一個向量的份量來建立一個新的向量。注意C與C++都沒有支持重組功能,由於C與C++並無對向量數據有內置支持。下面是一些重組的例子:
float4 vec1 = float4 (1,2,3,4); float3 vec2 = vec1.xyz ; //vec2 = (1,2,3); float3 vec3 = vec1.xxx ; //vec3 = (1,1,1); float3 vec4 = vec2.yyy ; //vec4 = (2,2,2);
另外,還能夠重組矩陣,採用_m <行> <列> 的形式取得矩陣的元素來構成所需的向量:
float4x4 myMatrix ; float myFloatScalar; float4 myFloatVec4; myFloatScalar = myMatrix._m32 //myFloatScalar的值爲:myMatrix[3][2] myFloatVec4 = myMatrix._m00_m01_m22_m33; //同理
normalize(v)
Cg標準庫函數,放回一個向量的規範化版本。
dot(a,b)
計算a,b的點積
max(a,b)
返回a,b中的最大值
pow(x,y)
計算x的y次冪。
在OpenGL或Direct3D中,在任意給定點的衰減使用下面這公式來進行模擬:
attenuationFactor = 1/ ( Kc + kld + KQd^2 )
其中:
d是到光源的距離
Kc、Kl、KQ是控制衰減量的常量
對於距離d來講,kc、Kl、KQ分別是d的常數項、一次係數項、二次係數項。在真實世界中一個點光源的光照強度以1/d^2衰減。使用3個係數來控制衰減可以讓咱們對光照有更多的控制。
因而在上面提到的從固定光照模型簡化而來的基本光照模型公式:
公式一:lighting = emissive + ambient +diffuse + specualr
在加入衰減做用後,公式就變爲:
公式二:lighting = emissive + ambient + attenuationFactor * (diffuse + specualr)
這裏先貼出上篇文章中的代碼(對應於公式一):
// //程序001:基本光照模型 // void BaseLight( float4 position :POSITION,//被着色點的位置 float3 normal : NORMAL, //表面在P點的標準化法向量 out float4 oPosition : POSITION, out float4 color : COLOR, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , //入射環境光顏色 uniform float3 lightColor , //燈光顏色 uniform float3 lightPosition, //燈光的位置 uniform float eyePosition, //攝像機位置 uniform float3 Ke, //Ke是材質的放射光(自發光)顏色 uniform float3 Ka, //Ka是材質的環境反射係數 uniform float3 Kd, //Kd是材質的漫反射顏色 uniform float3 Ks, //Ks是材質的鏡面反射顏色 uniform float shininess //材質表面光澤度 ) { oPosition = mul(modelViewPrij,position); float3 P = position.xyz ; float3 N = normal; //公式一計算放射光 float3 emissive = ke; //公式二計算環境光 float3 ambient = Ka * globalAmbient; //公式三計算漫反射光 float3 L = normalize (lightPosition - P); //L爲標準化指向燈光的向量。 float diffuseLight = max(dot(N,L),0); float diffuse = Kd *lightColor *diffuseLight; //公式四計算鏡面放射 float3 V = normalize(eyePosition - P); float3 H = normalize (L+V); float specularLight = pow(max (dot (N,H),0), shininess); if(diffuseLight < = 0) specularLight = 0; float3 specular = Ks * lightColor * specularLight ; //基本光照模型完成 color.xyz = emissive + ambient + diffuse + specular; color.w = 1; }
在基本光照模型的基礎上加上漫反射光照與鏡面反射項的衰減效果,咱們只須要把Kc、Kl、KQ加入到代碼中便可:
// //程序002:基本關照模型拓展:衰減係數 // void BaseLight_attenuate( float4 position :POSITION,//被着色點的位置 float3 normal : NORMAL, //表面在P點的標準化法向量 out float4 oPosition : POSITION, out float4 color : COLOR, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , //入射環境光顏色 uniform float3 lightColor , //燈光顏色 uniform float3 lightPosition, //燈光的位置 uniform float eyePosition, //攝像機位置 uniform float3 Ke, //Ke是材質的放射光(自發光)顏色 uniform float3 Ka, //Ka是材質的環境反射係數 uniform float3 Kd, //Kd是材質的漫反射顏色 uniform float3 Ks, //Ks是材質的鏡面反射顏色 uniform float shininess //材質表面光澤度 //新增 uniform float Kc; //衰減常數項 uniform float Kl; //衰減一次係數 uniform float kQ; //衰減二次係數 ) { float d = distance (P,lightPosition); //計算衰減距離 float attenuate = 1/(Kc + Kl*d + KQ * d * d); //衰減因子(由公式計算) oPosition = mul(modelViewPrij,position); float3 P = position.xyz ; float3 N = normal; //公式一計算放射光 float3 emissive = ke; //公式二計算環境光 float3 ambient = Ka * globalAmbient; //公式三計算漫反射光 float3 L = normalize (lightPosition - P); //L爲標準化指向燈光的向量。 float diffuseLight = max(dot(N,L),0); float diffuse = Kd *lightColor *diffuseLight*attenuate; //公式四計算鏡面放射 float3 V = normalize(eyePosition - P); float3 H = normalize (L+V); float specularLight = pow(max (dot (N,H),0), shininess); if(diffuseLight < = 0) specularLight = 0; float3 specular = Ks * lightColor * specularLight *attenuate; //基本光照模型完成 color.xyz = emissive + ambient + diffuse + specular; color.w = 1; }
相比較於以前的基本光照模型的代碼,這裏添加了計算衰減因子的步驟,同時將衰減因子參與diffuse與specular的計算。
基本光照模型寫到這裏,大概你已經發現了問題了:函數的參數太多了,咱們能夠經過結構+函數來重構上述代碼段。
struct Material { float3 Ke; float3 Ka; float3 Kd; float3 Ks; float3 shininess; }
struct Light { float4 position; float3 color; float Kc; float Kl; float KQ; }
這樣,程序002可使用結構做爲參數來改進:
void BaseLight_attenuate(Material materaial, Light light , float3 globalAmbient, float3 P, float3 N, float3 eyePosition) { //光照計算 }
程序002中對於漫反射光照與鏡面反射光照使用了大段的代碼進行模擬,咱們能夠寫一個函數來進行光照計算:
// //代碼003:漫反射和鏡面反射函數 // void computeLighting(Light light, float3 P, float3 N, float3 eyePosition, float shininess, out float3 diffuseResult , out float3 specularResult), float attenuate { //計算漫反射 float3 L = normalize(light.position-P); float diffuseLight = max (dot (N,L),0); diffuseResult = light.color * diffuseLight*attenuate; //計算鏡面反射 float3 V = normalize(eyePosition -P); float3 H = normalize(L+V); float specularLight = pow (max (dot (N,H),0),shininess); if(diffuseLight<=0) specularLight = 0; specularResult = light.color*specularLight*attenuate; }
那麼原來的002程序通過結構與函數的重構以後,能夠寫成這樣:
// //程序003:重構後基本關照模型拓展:衰減係數 // void BaseLight_attenuate( float4 position :POSITION,//被着色點的位置 float3 normal : NORMAL, //表面在P點的標準化法向量 out float4 oPosition : POSITION, out float4 color : COLOR, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , //入射環境光顏色 uniform float eyePosition, //攝像機位置 uniform Light light, uniform Material materail uniform float Kc; //衰減常數項 uniform float Kl; //衰減一次係數 uniform float kQ; //衰減二次係數 ) { float d = distance (P,lightPosition); //計算衰減距離 float attenuate = 1/(Kc + Kl*d + KQ * d * d); //衰減因子(由公式計算) oPosition = mul(modelViewPrij,position); float3 P = position.xyz ; float3 N = normal; //公式一計算放射光 float3 emissive = materaial.ke; //公式二計算環境光 float3 ambient = materaial.Ka * globalAmbient; float3 diffuseLight ; float3 specularLight ; computeLighting(light,position.xyz, normal, eyePosition, material.shininess, diffuseLight, specularLight, attenuate); float3 diffuse = materaial.kd*diffuseLight; float3 specular = materaial.ks*specularLight; //基本光照模型完成 color.xyz = emissive + ambient + diffuse + specular; color.w = 1;
}
爲了建立一個聚光燈,咱們須要知道聚光燈的位置、聚光燈的方向和將要試圖進行着色的點的位置,使用這些信息就能夠來計算從聚光燈到頂點的向量V和聚光燈的方向向量D。
而爲了判斷着色點P是否受到聚光燈的做用,要看P點是否在聚光燈的取捨角以內。什麼是聚光燈的取捨角?聚光燈的取捨角(cut-off angle)控制了聚光燈圓錐體的傳播,只有在聚光燈圓錐體內的物體才能受到光照。
當規範化的D與V點乘積dot(V,D)大於聚光燈的取捨角時的餘弦值時,P點才能受到聚光燈的影響。
咱們在燈光結構體Light中加入以下屬性:
struct Light { float4 position; float3 color; float Kc; float Kl; float KQ; //新增 float cosLightAngle ;//聚光燈取捨角餘弦值 float3 direction ; //聚光燈的方向向量 }
接下來寫一個判斷P點是否受聚光燈光照的函數,若是是函數返回1,不然放回0
float spotlight(float3 P,Light light) { float3 V= normalize(P - light.position); float cosCone = light.cosLightAngle;//聚光燈取捨角餘弦值 float cosDirection = dot(V,light.direction); if(cosCone<=cosDirection) return 1; return 0; }
迄今爲止,咱們所寫的聚光燈的光照強度並不會發生變化,這種聚光燈的光照效果以下圖:
然而實際聚光燈是幾乎不會這樣均勻聚焦的,爲了模擬真實的聚光燈光照效果,咱們要把聚光燈的圓錐體分紅內椎和外椎兩部分:
內椎部分發出均勻強度的光,外椎部分光照強度平滑減小,以造成以下這種光照效果:
標準庫函數smoothstep能夠用來平滑插值:
咱們須要再次擴展Light結構體:
struct Light { float4 position; float3 color; float Kc; float Kl; float KQ; //新增 float cosInnerCone ; float cosOuterCone; float3 direction ; //聚光燈的方向向量 }
接下來咱們寫一個內部函數來建立這個帶內外椎的聚光燈:
float dualConeSpotlight(float3 P , Light light) { float3 V = normalize(P-light.position); float cosOuterCone = light.cosOuterCone; float cosInnerCone = light.cosInnerCone; float cosDirection = dot(V,light.direction); return smoothstep(cosOuterCone, cosInnerCone, cosDirection); }
最後改寫代碼003:漫反射和鏡面反射函數,使得漫反射和鏡面反射結合衰減和聚光燈項
void computeLighting(Light light, float3 P, float3 N, float3 eyePosition, float shininess, out float3 diffuseResult , out float3 specularResult), float attenuate { float spotEffect = dualConeSpotlight(P,light); //計算漫反射 float3 L = normalize(light.position-P); float diffuseLight = max (dot (N,L),0); diffuseResult = light.color * diffuseLight*attenuate; //計算鏡面反射 float3 V = normalize(eyePosition -P); float3 H = normalize(L+V); float specularLight = pow (max (dot (N,H),0),shininess); if(diffuseLight<=0) specularLight = 0; specularResult = light.color*specularLight*attenuate*spotEffect; }
上面咱們實現了一個基本的光照模型。接下來咱們看看一些常見光照模型,這些光照模型在遊戲或其餘場景中被大量應用,或是加以改進後大量應用。
Lambert光照模型是最簡單的漫反射模型。物體發生理想漫反射時,光線照射到比較粗糙的物體表面,從物體表面向各個方向發生了反射,從而不管從哪一個角度來看表面,表面某點的明暗程度都不隨觀測者的位置變化而變化。例如你觀察黑板時(黑板上佈滿粉筆粉末),黑板上發生的就是漫反射。
Lambert光照模型的數學表達式能夠寫爲:
Ip = Ia * kd + II * kd * ( dot ( N,L ) )
其中:
Lambert光照模型的CG代碼爲:
//燈光結構體 struct Light { float3 color ; float3 position; } //物體材質結構體 struct Material { float kd ; } void LambertModel( out float4 oposition:POSITION, out float3 color :COLOR, loat4 position:POSITION, float3 normal:NORMAL, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , uniform float3 eyePosition, uniform Light light , uniform Material material, ) { oposition = mul (modelViewPrij,P); float3 P = position.xyz; float3 N = normal; float3 ambient = material.kd * globalAmbient; float3 L = normalize( light.position -P ); float3 specular = light.color * material.kd * max( dot(N,L),0 ) ; color.xyz = ambient + specular ; color.w = 1; }
在遊戲渲染引擎中,最經常使用的局部光照模型就是Phong氏反射模型,此模型把從表面的光分解爲3個獨立項:
咱們先來看一下phong光照模型的數學公式(單個光源):
I = Ka * LA + LL * Kd * max( ( dot (N,L),0 ) + LL * Ks* max (dot ( R,V )^,shininess,0 )
從公式能夠看出,計算表面上某點的phong反射時須要輸入一些參數,這些參數包括:
這部分咱們能夠用一個材質結構體來描述:
struct Matrial { float ka ; float kd ; float ks ; float shininess; }
L關於N的反射向量R
這些向量能夠參考下面這個圖。圖中的H向量在這裏並無用到,它是參與另外一個光照模型Blina-Phong計算的一個向量,後面會講到。
其中R向量的計算方法爲:
任何向量均可以表示爲切線向量和法線向量之和,例如對於向量L,它能夠表示爲:
L = Ln + Lt ;
Ln指的是L在法線向量N上的投影長度,它能夠這樣計算:
Ln = dot ( N, L )N ; (N是個單位向量)
Ln計算出來了,天然的,咱們的Lt能夠由L與Ln來計算:
Lt = L - Ln;
對於R向量,它是向量L關於法向量N的反射向量,故R與L有同一個法線份量Ln,但又相反的切線份量Lt,所以,咱們能夠這樣求R:
R = Rn + Rt
= Ln - Lt
= Ln - (L- Ln)
= 2Ln - L
= 2( dot ( N , L )N ) - L
至此,依據公式,咱們能夠寫以下phong光照模型的CG代碼:
struct Matrial { float ka ; //環境反射量 float kd ; //漫反射量 float ks ; //鏡面反射量 float shininess; //物體表面光澤度 } struct Light { float3 position ; //燈光的位置 float3 color ; //燈光的顏色 } void PhongModle ( out float3 oposition:POSITION, out float3 color :COLOR, float4 position:POSITION, float3 normal:NORMAL, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , uniform float3 eyePosition, uniform Light light , uniform Material material, ) { oposition = mul (modelViewPrij,P); float3 P = position.xyz; float3 N = normal; //計算環境光貢獻 float3 ambient = material.ka * globalAmbient; //計算向量L float3 L = normalize( light.position -P ); //計算向量V float3 V = normalize (eyePosition -P); //計算向量R float3 R = 2 * (dot (N,L)*N )-L ; //計算漫反射貢獻 float3 diffuse = material.kd * light.color * max (dot (N,L),0); //計算鏡面反射貢獻 float3 specular = material.ks * light.color * max (dot (R,V)^shininess,0); //三種光加和 color.xyz = ambient + diffuse +specular ; color .w = 1; }
Blinn-Phong反射模型是Phong模型的變種,它們的區別在於在計算鏡面反射項時,Phong採用的向量是R與V,而該模型採用的向量是H與N,H向量是什麼?
H = V + L.
Blinn-Phong模型以下降準確度來換取更高的性能,然而Blinn-Phong模型實際上模擬某些材質時,比Phong模型更加接近實驗測量數據。Blinn-phong模型幾乎是早起計算機遊戲的惟一之選,而且以硬件形式入駐早起GPU固定管線。
對phong代碼稍做修改,能夠得Blinn-Phong模型的代碼:
struct Matrial { float ka ; //環境反射量 float kd ; //漫反射量 float ks ; //鏡面反射量 float shininess; //物體表面光澤度 } struct Light { float3 position ; //燈光的位置 float3 color ; //燈光的顏色 } void PhongModle ( out float3 oposition:POSITION, out float3 color :COLOR, float4 position:POSITION, float3 normal:NORMAL, uniform float4x4 modelViewPrij, uniform float3 globalAmbient , uniform float3 eyePosition, uniform Light light , uniform Material material, ) { oposition = mul (modelViewPrij,P); float3 P = position.xyz; float3 N = normal; //計算環境光貢獻 float3 ambient = material.ka * globalAmbient; //計算向量L float3 L = normalize( light.position -P ); //計算向量V float3 V = normalize (eyePosition -P); //計算向量R float3 H = normalize(V+L) ; //計算漫反射貢獻 float3 diffuse = material.kd * light.color * max (dot (N,L),0); //計算鏡面反射貢獻 float3 specular = material.ks * light.color * max (dot (N,H)^shininess,0); //三種光加和 color.xyz = ambient + diffuse +specular ; color .w = 1; }
在遊戲中,一般會在這些基本光照模型的基礎上加以改進再應用到場景中。
原創文章,轉載請註明出處:http://i.cnblogs.com/EditPosts.aspx?postid=5189831