0.前言
这篇文章写于去年的暑假。大二的假期时间多,小组便开发一个手机游戏的项目,开发过程中忙里偷闲地了解了Unity的shader编写,而CG又与shaderLab相似,所以又阅读了《CG教程》、《GPU 编程与CG 语言之阳春白雪下里巴人》学习图形学的基础。尝试编写unity shader时还恶补了些3D数学。这些忙里偷闲的日子,坏了空调的闷热的实验室,还真是有点怀念。当时写这些文章并不是想作为教程,只是自己的总结方便日后温习,所以文章内容都很基础。
2015/08/04 于工学一号馆
1.基本的光照模型
OpenGL与Direct3D提供了几乎相同的固定功能光照模型。什么是固定功能光照模型?在过去只有固定绘制流水线的时候,该流水线被限制只能使用一个光照模型,也即是固定功能光照模型。该模型基于phong光照模型。在下面的这个例子里,我们使用一个“基本”模型对固定功能光照模型提供了简化版本。这个基本模型的数学描述为高级公式为:
surfaceColor = emissive + ambient + diffuse + specular
从式子可以看出:物体表面的颜色是自发光(放射 emissive)、环境反射(ambient)、漫反射(diffuse)和镜面反射(specular)等光照作用的总和。每种光照作用取决于表面材质性质(例如亮度和材质颜色)和光源的性质(例如光的位置和颜色)。
下面对这个基本模型的各个部分进行讲解,最后我们使用CG语言写出该基本模型。
1.1自发光(emissive)
- 自发光光照作用独立于所有的光源。物体的自发光并不能照亮场景中的其他物体。换句话说,物体自发光不能照亮其他物体或者投下阴影。因此,一个放射性物体本身并不是一个光源。
- 另一个解释放射项的方法:它是一种在计算完其他所有光照项后添加的颜色。
- 自发光的数学公式:emissive = Ke
其中Ke代表材质的放射光颜色
1.2 环境放射项(ambient)
- 环境光来自于四面八方,故环境放射光照项并不依赖于光源的位置。
- 环境放射项依赖:1.材质的反射能力 2.照射到材质上的环境光的颜色。
- 与放射项相比:1.同样是一种固定的颜色(依赖它本身)2.不同的是,环境反射项收到全局光照的影响。
- 用于环境放射项的数学公式:
ambient = Ka * globalAmbient
其中ka是材质的环境反射系数,globalAmbient是入射环境光的颜色。
1.3 漫反射项(diffuse)
漫反射项代表了从一个表面相等地向所有方向反射出去的方向光。
如下所示:
用来计算漫反射项的公式为:
diffuse = kd * lightColor * max ( N*L(点积) , 0 )
其中:
Kd是材质的漫反射颜色
lightColor 是灯光的颜色
N是标准化的顶点法向量
L是标准化的指向灯光的向量
P是被着色的点(如下图)
这里需要解释一下
max ( N*L(点积) , 0 )
规范化的向量N和L的点积是两个向量之间夹角的一个度量,夹角越小,P点受到更多的入射光照。而背向光源的表面将产生负数点积值,因此,公式**max ( N*L(点积) , 0 )使得背向光源的表面的漫反射光为0,确保这些表面不会显示漫反射光照。
**1.4 镜面反射项(specular)
镜面反射的作用依赖于观察者的位置,如果观测值位于一个无法接受反射光线的位置,观察者将不可能在表面上看到镜面反射强光。镜面反射项受到了表面光泽度的影响,越有光泽度的材质表面的高光区越小,下图从左到右材质光泽度递增:
镜面反射项的数学公式:
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表示要着色的点
1.5 CG语言实现上述基本模型
使用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; }
1.6 程序分析
1.6.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; //同理
1.6.2 Cg标准库函数
normalize(v)
Cg标准库函数,放回一个向量的规范化版本。
dot(a,b)
计算a,b的点积
max(a,b)
返回a,b中的最大值
pow(x,y)
计算x的y次幂。
2. 基本光照模型的拓展
2.1 实现距离衰减效果
在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的计算。
2.2. 重构基本光照模型代码
基本光照模型写到这里,大概你已经发现了问题了:函数的参数太多了,我们可以通过结构+函数来重构上述代码段。
2.2.1 使用结构简化函数参数
- 首先,我们可以把和材质有关的参数写成一个结构,如下:
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) { //光照计算 }
2.2.2 使用函数简化函数体
程序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 materailuniform 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;
}
2.3 加入聚光灯效果
为了创建一个聚光灯,我们需要知道聚光灯的位置、聚光灯的方向和将要试图进行着色的点的位置,使用这些信息就可以来计算从聚光灯到顶点的向量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;}
3. 常见光照模型解析
上面我们实现了一个基本的光照模型。接下来我们看看一些常见光照模型,这些光照模型在游戏或其他场景中被大量应用,或是加以改进后大量应用。
3.1 Lambert光照模型
Lambert光照模型是最简单的漫反射模型。物体发生理想漫反射时,光线照射到比较粗糙的物体表面,从物体表面向各个方向发生了反射,从而无论从哪个角度来看表面,表面某点的明暗程度都不随观测者的位置变化而变化。例如你观察黑板时(黑板上布满粉笔粉末),黑板上发生的就是漫反射。
Lambert光照模型的数学表达式可以写为:
Ip = Ia * kd + II * kd * ( dot ( N,L ) )
其中:
- kd为物体表面的漫反射系数。
- Ia为环境光,Ia*kd为环境光对物体表面漫反射所贡献的光照。
- II表示环境光外其他光如方向光或点光源。
- N为物体表面p点的法向量。
- L为P点指向灯源的方向向量。
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;}
3.2 Phong氏反射模型
在游戏渲染引擎中,最常用的局部光照模型就是Phong氏反射模型,此模型把从表面的光分解为3个独立项:
- 环境项:模拟场景的整体光照水平。
- 漫反射:模拟直接光源在表面均匀地向各个方向反射。
- 镜面反射:在光滑表面均匀反射的高光。
我们先来看一下phong光照模型的数学公式(单个光源):
I = Ka * LA + LL * Kd * max( ( dot (N,L),0 ) + LL * Ks* max (dot ( R,V )^,shininess,0 )
从公式可以看出,计算表面上某点的phong反射时需要输入一些参数,这些参数包括:
- 表面反射属性,包括了:
- 环境反射量Ka
- 漫反射量kd
- 镜面反射量Ks
- 镜面光滑度shininess
这部分我们可以用一个材质结构体来描述:
struct Matrial { float ka ; float kd ; float ks ; float shininess; }
- 光源的颜色及其强度LL
- 环境光强度A
- 从表面上某点(受到光照的那点)指向光源的方向向量L
- 从表面上某点指向虚拟摄像机的方向向量V
- 表面上某点的法向量N
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; }
3.3 Blinn-Phong光照模型
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; }
在游戏中,通常会在这些基本光照模型的基础上加以改进再应用到场景中。
原创文章,转载请注明出处: