OpenGL ES 入门之旅 -- GLSL光照计算

冯氏光照模型

首先,在现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此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 是光线与顶点法线向量的点积 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<a<c 则函数返回值为a 
//若不是,则返回值最小为b ,最大为c
// (theta - outCutOff)/epsilon 若theta的角度⼩于内锥角 则其值 >=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];
}
复制代码

在实际的开发中,我们在绘图时是不会把法线绘制在图形中的,所以绘制法线部分代码可以忽略。

转载于:https://juejin.im/post/5d09f5cf6fb9a07eaa227fd3

猜你喜欢

转载自blog.csdn.net/weixin_33712987/article/details/93182313