冯氏光照模型
首先,在现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)光照、漫反射(Diffuse)光照和镜面(Specular)光照。 下面我们用示例图来看一下这几种光照:
环境(Ambient)光照:即使在最黑暗的正常环境下,世界上依然会存在一些光亮,也就是说物体几乎永远不会是完全黑暗的。环境光照一般是由室外的太阳通过各种折射来让我们看到,这样的光是没有任何起点,没有方向的光。主要通过设置物体颜色来获取。 漫反射(Diffuse)光照:模拟的是光源对物体方向的影响,一个物体面向光源的时候,朝向光的那个面会亮一点,物体背面和其他面会暗一点。 镜面(Specular)光照:模拟的是有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。光照基础
OpenGL在处理光照时把光照系统分为三部分:分别是光源、材质和光照环境。光源就是光的来源,可以是前面所说的太阳或者电灯,蜡烛等。材质是指接受光照的各种物体的表面,由于物体如何反射光线只由物体表面决定,材质特点就决定了物体反射光线的特点。光照环境是指一些额外的参数,它们将影响最终的光照画面,比如一些光线经过多次反射后,已经无法分清它究竟是由哪个光源发出,这时,指定一个环境亮度参数,可以使最后形成的画面更接近于真实情况。
在物理学中,光线如果射入理想的光滑平面,则反射后的光线是很规则的(这样的反射称为镜面反射)。光线如果射入粗糙的、不光滑的平面,则反射后的光线是杂乱的(这样的反射称为漫反射)。现实生活中的物体在反射光线时,并不是绝对的镜面反射或漫反射,但可以看成是这两种反射的叠加。对于光源发出的光线,可以分别设置其经过镜面反射和漫反射后的光线强度。对于被光线照射的材质,也可以分别设置光线经过镜面反射和漫反射后的光线强度。这些因素综合起来,就形成了最终的光照效果。
光照特性 1.发射光:由物体自身发光 2.环境光:就是在环境中充分散射的光,而且无法分辨光的方向 3.漫反射光:光线来自某个方向,但是在物体上各个方向反射 4.镜面高光:光线来自一个特定的方向,然后在物体表面上以一个特定的方向反射出去
材质属性 1.泛射材质:光线直射,反射率较高 2.漫反射材质:需要考虑光的入射角和反射角的 3.镜面反射材质:斑点 4.发射材质:物体本身就可以发光的材质
光照计算
1.环境光的计算
环境光是不来自任何特定方向的光,在整个场景中经典光照模型把它当成一个常量,组成一个合适的第一近似值来缩放场景中的光照部分。 环境光 = 光源的环境光颜色 * 物体的材质颜色
varying vec3 objectColor;
void main()
{
//⾄至少有%10的光找到物体所有⾯面
float ambientStrength = 0.1;
//环境光颜⾊色
vec3 ambient = ambientStrength * lightColor;
//最终颜⾊色 = 环境光颜⾊色 * 物体颜⾊色
vec3 result = ambient * objectColor;
gl_FragColor = vec4(result, 1.0);
}
复制代码
2.发射光的计算
如果这个物体本身就是有颜色的,比如说夜明珠,那么这个时候这个光就是这个物体材质的颜色 发射颜色 = 物体的反射材质颜色
3.漫反射光的计算
漫反射光是散射在各个方向上的均匀的表面特定光源,漫反射光依赖于表面法线方向和光源方向来计算,但是没有包含视线方向,它同样依赖于表面的颜色。 首先来看一下环境光和漫反射光的比较:
可以看到环境光下的苹果是没有阴面和阳面的,看到的苹果感觉不够逼真,而漫反射光加深了苹果的真实度,模拟出了在现实生活中的真实环境。 上图是当漫反射光照射到物体表面时:其中N表示法向量,L表示光源,法向量N和光源L之间的夹角决定了光照射的面积。夹角越大照射面积越大。光线照射到物体表面,决定了物体表面的光照强度,光照强度是光本身强度和光线与物体表面法线夹角cos的乘积
有效光的光照方向是与物体表面法线夹角在0~90度之间的。漫反射颜色 = 光源的漫反射光颜色 × 物体的漫反射材质颜色 × 漫反射因子
DiffuseFactor = max(0, dot(N, L))
uniform vec3 lightColor; //光源色
uniform vec3 lightPo; //光源位置
uniform vec3 objectColor; //物体⾊色
uniform vec3 viewPo; //物体位置
varying vec3 outNormal; //传⼊当前顶点平面的法向量
//确保法线为单位向量量
vec3 norm = normalize(outNormal);
//顶点指向光源 单位向量量
vec3 lightDir = normalize(lightPo - FragPo);
//得到两向量量的cos值 ⼩小于0则则为0
float diff = max(dot(norm, lightDir),0.0);
//得到漫反射收的光源向量量
vec3 diffuse = diff * lightColor;
vec3 result = diffuse * ojbectColor;
gl_FragColor = vec4(result,1.0);
复制代码
4.镜面光照计算 镜面光是由表面直接反射的高亮光,这个高亮光就像镜子一样跟表面材质多少有关。
其中:N表示平面法线,R表示反射光线,@表示视点与反射光的夹角镜面反射颜色 = 光源的镜面光颜色 × 物体的镜面材质颜色 × 镜面反射因子
镜面反射因子SpecularFactor = power(max(0,dot(N,R)),shininess)
dot(N,R):H,R的点积几何意义:平⽅线与法线夹角的cos值
shiniess : ⾼光的反光度/发光值;(值越大反射度越强)
一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响:
一般我们不希望镜面成分过于显眼,所以我们通常把shiniess指数设置为32.镜面光的GLSL实现代码:
//镜⾯面强度
float specularStrength = 0.5;
//顶点指向观察点的单位向量量
vec3 viewDir = normalize(viewPo - FragPo);
//求得光线 在 顶点的反射线(传⼊入光源指向顶点的向量量)
vec3 reflectDir = reflect(-lightDir ,outNormal);
// 求得夹⻆角cos值 取256次幂 注意 pow(float,float)函数参数类型 float spec = pow(max(dot(viewDir, reflectDir),0.0),256.0);
vec3 specular = specularStrength * spec * lightColor;
复制代码
我们都知道光的传播是会衰减的,所以光照颜色的公式可总结为:
光照颜色 =(环境颜色 + 漫反射颜色 + 镜⾯反射颜色)* 衰减因子
衰减因子 = 1.0/(距离衰减常量 + 线性衰减常量 * 距离 + ⼆次衰减常量 * 距离的平⽅)
//距离衰减常量量
float constantPara = 1.0f;
//线性衰减常量量
float linearPara = 0.09f;
//⼆二次衰减因⼦子
float quadraticPara = 0.032f;
//距离
float LFDistance = length(lightPo - FragPo);
//衰减因⼦子
float lightWeakPara = 1.0/(constantPara + linearPara * LFDistance + quadraticPara * (LFDistance*LFDistance));
复制代码
距离衰减常量,线性衰减常量和⼆次衰减常量均为常量值.
环境光,漫反射光和镜面光的强度都会受距离的增大⽽衰减,只有发射光和全局环境光的强度不会受影响.
聚光灯夹角cos值 = power(max(0,dot(单位光源位置,单位光线向量)),聚光灯指数);
单位光线向量是从光源指向顶点的单位向量 聚光灯指数,表示聚光灯的亮度程度 公式解读:单位光源位置 * 单位光线向量点积的聚光灯指数次⽅。
聚光灯因子 = clamp((外环的聚光灯角度cos值 - 当前顶点的聚光灯角度cos值)/ (外环的聚光灯角度cos值- 内环聚光灯的角度的cos值),0,1);
//聚光灯过渡计算
//(⼀些复杂的计算操作 应该让CPU做,提⾼效率,不变的量也建议外部传输,避免重复计算)
//内锥角cos值
float inCutOff = cos(radians(10.0f));
//外锥角cos值
float outCutOff = cos(radians(15.0f)); //聚光朝向
vec3 spotDir = vec3(-1.2f,-1.0f,-2.0f);
//光源指向物体的向量和聚光朝向的 cos值
float theta = dot(lightDir ,normalize(-spotDir));
//内外锥⻆角cos差值
float epsilon = inCutOff - outCutOff;
//clamp(a,b,c);若b=1
//若theta的⻆度大于外锥角,则其值<=0 这样光线就在内外锥角之间平滑变化.
float intensity = clamp((theta - outCutOff)/epsilon, 0.0,1.0)
复制代码
光照颜色的最终公式为:
光照颜色 = 发射颜色 + 全局环境颜色 + (环境颜色 + 漫反射颜色 + 镜⾯反射颜色) * 聚光灯效果 * 衰减因子
下面通过一个GLKit绘制金字塔案例来看一下光照的使用:
如果把效果图里的三角形的顶点全部画到平面上来,如下图所示:
1.设置OpenGL ES
@property(nonatomic,strong)EAGLContext *mContext;
//基本Effect 绘图
@property(nonatomic,strong)GLKBaseEffect *baseEffect;
//额外Effect 辅助线段
@property(nonatomic,strong)GLKBaseEffect *extraEffect;
//顶点缓存区 (顶点,颜色,纹理, 法线...)
@property(nonatomic,strong)AGLKVertexAttribArrayBuffer *vertexBuffer;
//法线位置缓存区(法线辅助线段也有顶点)
@property(nonatomic,strong)AGLKVertexAttribArrayBuffer *extraBuffer;
//是否绘制法线
@property(nonatomic,assign)BOOL shouldDrawNormals;
//中心点的高 默认在(0,0,0)
@property(nonatomic,assign) GLfloat centexVertexHeight;
{
//三角形-8面
SceneTriangle triangles[NUM_FACES];
}
复制代码
设置GLKitView并设置上下文
//1.新建OpenGL ES 上下文
self.mContext = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
//2.设置GLKView
GLKView *view = (GLKView *)self.view;
view.context = self.mContext;
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[EAGLContext setCurrentContext:self.mContext];
复制代码
2. 设置金字塔Effect
//1.金字塔Effect
self.baseEffect = [[GLKBaseEffect alloc]init];
self.baseEffect.light0.enabled = GL_TRUE;
//光的漫射部分 GLKVector4Make(R,G,B,A)
self.baseEffect.light0.diffuseColor = GLKVector4Make(0.7f, 0.7f, 0.7, 1.0f);
//世界坐标中的光的位置。
self.baseEffect.light0.position = GLKVector4Make(1.0f, 1.0f, 0.5f, 0.0f);
//2.法线Effect
self.extraEffect = [[GLKBaseEffect alloc]init];
self.extraEffect.useConstantColor = GL_TRUE;
//3.调整模型矩阵,更好的观察
//可以尝试不执行这段代码,改为false
if (true) {
//围绕x轴旋转-60度
//返回一个4x4矩阵进行绕任意矢量旋转
GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(GLKMathDegreesToRadians(-60.0f), 1.0f, 0.0f, 0.0f);
//围绕z轴,旋转-30度
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix,GLKMathDegreesToRadians(-30.0f), 0.0f, 0.0f, 1.0f);
//围绕Z方向,移动0.25f
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0.0f, 0.0f, 0.25f);
//设置baseEffect,extraEffect 模型矩阵
self.baseEffect.transform.modelviewMatrix = modelViewMatrix;
self.extraEffect.transform.modelviewMatrix = modelViewMatrix;
}
复制代码
3.设置顶点
//确定图形的8个面
triangles[0] = SceneTriangleMake(vertexA, vertexB, vertexD);
triangles[1] = SceneTriangleMake(vertexB, vertexC, vertexF);
triangles[2] = SceneTriangleMake(vertexD, vertexB, vertexE);
triangles[3] = SceneTriangleMake(vertexE, vertexB, vertexF);
triangles[4] = SceneTriangleMake(vertexD, vertexE, vertexH);
triangles[5] = SceneTriangleMake(vertexE, vertexF, vertexH);
triangles[6] = SceneTriangleMake(vertexG, vertexD, vertexH);
triangles[7] = SceneTriangleMake(vertexH, vertexF, vertexI);
//初始化缓存区
self.vertexBuffer = [[AGLKVertexAttribArrayBuffer alloc]initWithAttribStride:sizeof(SceneVertex) numberOfVertices:sizeof(triangles)/sizeof(SceneVertex) bytes:triangles usage:GL_DYNAMIC_DRAW];
self.extraBuffer = [[AGLKVertexAttribArrayBuffer alloc]initWithAttribStride:sizeof(SceneVertex) numberOfVertices:0 bytes:NULL usage:GL_DYNAMIC_DRAW];
self.centexVertexHeight = 0.0f;
复制代码
4.开始绘制
-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
//1.
glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//2.
[self.baseEffect prepareToDraw];
//准备绘制顶点数据
[self.vertexBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition numberOfCoordinates:3 attribOffset:offsetof(SceneVertex,position)shouldEnable:YES];
//准备绘制光照数据
[self.vertexBuffer prepareToDrawWithAttrib:GLKVertexAttribNormal numberOfCoordinates:3 attribOffset:offsetof(SceneVertex, normal) shouldEnable:YES];
[self.vertexBuffer drawArrayWithMode:GL_TRIANGLES startVertexIndex:0 numberOfVertices:sizeof(triangles)/sizeof(SceneVertex)];
//3.是否要绘制光照法线
if (self.shouldDrawNormals) {
[self drawNormals];
}
}
复制代码
绘制法线
//绘制法线
-(void)drawNormals
{
GLKVector3 normalLineVertices[NUM_LINE_VERTS];
//1.以每个顶点的坐标为起点,顶点坐标加上法向量的偏移值作为终点,更新法线显示数组
//参数1: 三角形数组
//参数2:光源位置
//参数3:法线显示的顶点数组
SceneTrianglesNormalLinesUpdate(triangles, GLKVector3MakeWithArray(self.baseEffect.light0.position.v), normalLineVertices);
//2.为extraBuffer重新开辟空间
[self.extraBuffer reinitWithAttribStride:sizeof(GLKVector3) numberOfVertices:NUM_LINE_VERTS bytes:normalLineVertices];
//3.准备绘制数据
[self.extraBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition numberOfCoordinates:3 attribOffset:0 shouldEnable:YES];
//4.修改extraEffect
//法线
/*
指示是否使用常量颜色的布尔值。
如果该值设置为gl_true,然后存储在设置属性的值为每个顶点的颜色值。如果该值设置为gl_false,那么你的应用将使glkvertexattribcolor属性提供每顶点颜色数据。默认值是gl_false。
*/
self.extraEffect.useConstantColor = GL_TRUE;
//设置光源颜色为绿色,画顶点法线
self.extraEffect.constantColor = GLKVector4Make(0.0f, 1.0f, 0.0f, 1.0f);
//准备绘制-绿色的法线
[self.extraEffect prepareToDraw];
//绘制线段
[self.extraBuffer drawArrayWithMode:GL_LINES startVertexIndex:0 numberOfVertices:NUM_NORMAL_LINE_VERTS];
//设置光源颜色为黄色,并且画光源线
//Red+Green =Yellow
self.extraEffect.constantColor = GLKVector4Make(1.0f, 1.0f, 0.0f, 1.0f);
//准备绘制-黄色的光源方向线
[self.extraEffect prepareToDraw];
//(NUM_LINE_VERTS - NUM_NORMAL_LINE_VERTS) = 2 .2点确定一条线
[self.extraBuffer drawArrayWithMode:GL_LINES startVertexIndex:NUM_NORMAL_LINE_VERTS numberOfVertices:2];
}
//更新法向量
-(void)updateNormals
{
//更新每个点的平面法向量
SceneTrianglesUpdateFaceNormals(triangles);
[self.vertexBuffer reinitWithAttribStride:sizeof(SceneVertex) numberOfVertices:sizeof(triangles)/sizeof(SceneVertex) bytes:triangles];
}
复制代码
更新中心顶点
- (IBAction)changeCenterVertexHeight:(UISlider *)sender {
self.centexVertexHeight = sender.value;
}
-(void)setCentexVertexHeight:(GLfloat)centexVertexHeight
{
_centexVertexHeight = centexVertexHeight;
//更新顶点 E
SceneVertex newVertexE = vertexE;
newVertexE.position.z = _centexVertexHeight;
triangles[2] = SceneTriangleMake(vertexD, vertexB, newVertexE);
triangles[3] = SceneTriangleMake(newVertexE, vertexB, vertexF);
triangles[4] = SceneTriangleMake(vertexD, newVertexE, vertexH);
triangles[5] = SceneTriangleMake(newVertexE, vertexF, vertexH);
//更新法线
[self updateNormals];
}
复制代码
在实际的开发中,我们在绘图时是不会把法线绘制在图形中的,所以绘制法线部分代码可以忽略。