这篇文章写于去年的暑假。大二的假期时间多,小组便开发一个手机游戏的项目,开发过程当中忙里偷闲地了解了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