[OpenGL ES 07-2]Per-Vertex Light及深度缓存

[OpenGL ES 07-2]Per-Vertex Light及深度缓存

罗朝辉 (http://blog.csdn/net/kesalin/)

本文遵循“署名-非商业用途-保持一致”创作公用协议

 

这是《OpenGL ES 2.0 教程》的第八篇,前七篇请参考如下链接:

 

前言

在前文《[OpenGL ES 07-1]光照原理》中已经介绍 Opengl 中的光照原理,接下来将演示如何将这些原理用 OpenGL ES 2.0 来实现。今天的这篇文章将介绍 Per-Vertex Light 以及深度缓存,下一篇文章将介绍 Per-Pixel Light 以及卡通效果。还记得在第六篇文章的末尾留了一个小作业,用顶点缓存描绘一个立方体么 Cube?在这篇文章就会用到它。Per-Vertex Light 示例源码在这里,运行效果如下:


 

在开始之前,先来回顾一下 Per-Vertex Light 是怎么回事。Per-Vertex Light 也称为 Gauroud 着色,它是在顶点着色阶段对每一个顶点进行颜色计算,然后在光栅化阶段利用这些顶点颜色进行线性插值形成片元的颜色。

 

一,准备工作

1,新建工程

和前面的文章一样,新建名为 Tutorial07 的 Single View Application,导入 OpenGLES.framework 和 QuartzCore.framework。然后将 Tutorial06 中的 Utils, Shader,Surface三个目录以及 OpenGLView.h/m 两个文件拷贝到 Tutorial07 中,并在 XCode 中将它们加入进来。

2,添加 Cube 类型的 VBO

将第六篇的小作业:为 Cube 的 VBO 的那部分代码 - (DrawableVBO *)createVBOsForCube 加入到 OpenGLView.m 中,并修改 - (void)setupVBOs 的实现,在 _vboArray 加入 Cube 这个类型。这部分代码与本文主题不相干,所以就不在这里累述了,详情请参看源代码

3,添加控制控件

参照效果图,在 Storyboard 中添加相关控件:


4,添加响应代码

和前面的示例一样,在 ViewController 中加入相关响应代码,并使用拖拽技巧与 Storyboard 中的对应控件关联起来。下面只列出一部分代码,完整代码请参考源代码

@property (nonatomic, strong) IBOutlet OpenGLView * openGLView;

@property (nonatomic, strong) IBOutlet UISlider * lightXSlider;
// ...

- (IBAction)lightXSliderValueChanged:(id)sender;
// ...

- (IBAction)segmentSelectionChanged:(id)sender;

 

二,Per-Vertex Light 实现

1,修改顶点着色器

在本文中,光照计算的实际工作都是在顶点着色器中进行的,因此首先修改顶点着色器 VertexShader.glsl 如下:

uniform mat4 projection;
uniform mat4 modelView;
attribute vec4 vPosition;

uniform mat3 normalMatrix;
uniform vec3 vLightPosition;
uniform vec4 vAmbientMaterial;
uniform vec4 vSpecularMaterial;
uniform float shininess;

attribute vec3 vNormal;
attribute vec4 vDiffuseMaterial;

varying vec4 vDestinationColor;

void main(void)
{
    gl_Position = projection * modelView * vPosition;
    
    vec3 N = normalMatrix * vNormal;
    vec3 L = normalize(vLightPosition);
    vec3 E = vec3(0, 0, 1);
    vec3 H = normalize(L + E);

    float df = max(0.0, dot(N, L));
    float sf = max(0.0, dot(N, H));
    sf = pow(sf, shininess);

    vDestinationColor = vAmbientMaterial + df * vDiffuseMaterial + sf * vSpecularMaterial;
    
    //vDestinationColor = vec4(1.0, 0.0, 0.0, 1.0);
}

在这里,添加了光源位置:vLightPosition,以及三种类型的材质属性:环境材质 vAmbientMaterial,漫反射材质 vDiffuseMaterial 和镜面反射材质 vSpecularMaterial。这些都在前文《光照原理》中介绍过了,在这里就不再重复了。光照计算的最终颜色等于三种类型的光照效果的累加:

vDestinationColor = vAmbientMaterial + df * vDiffuseMaterial + sf * vSpecularMaterial;

df 漫反射因子:它是光线与顶点法线向量的点积,几何意义就是光线 L 与法线 N 之间夹角的 cos 值;

sf 镜面反射因子:它是视线 E 与光线 L 形成的夹角的平分线 H 与顶点法线向量的点积(几何意义就是平分线 H 与顶点法线 N 之间夹角的 cos 值),再 shininess 次方;

平分线向量 H:它是通过将视线向量 E 与 光线向量 L 相加,并规范化计算而来;

shininess 光泽强度:它由 OpenGL 程序传入。当然啦,最终是在 UI 通过 shininess 这个滑块控制的,该值越小,光泽强度越大。

前面提到过,OpenGL 中的很多计算都要将向量规划化,在这里就体现出来了,比如法线,平分线,位置向量等。在上面的代码中,还从 OpenGL 程序中传入了法线变换矩阵 normalMatrix,这个值得一说。

2,法线变换矩阵

为什么需要法线变换矩阵呢?因为法线向量与顶点向量一样,是在物体的模型空间中,而光照计算通常是在视图空间中进行的,因此我们需要将模型空间中的法线向量变换到视图空间,这是原因之一。或许你或说,这个变换直接用和用于顶点变换的模型视图变换矩阵 modelView 就可以了呀。诚然,当模型视图变换是刚体变换时,法线变换矩阵与模型视图变换矩阵完全一样;但如果模型视图变换不是刚体变换时,两者就不相同了。所谓刚体变换就是说物体在 x, y, z 三个方向进行了等比的缩放操作。只有刚体变换这种情况下,顶点的法线向量方向才不会改变,而在非刚体变换时,法线向量的方向会有变化。试想一下,圆球上顶点的法线向量是从球心指向顶点,当将圆球在 y 方向进行缩放而 x,z 方向保持不变,经过这样的非刚体变换之后,这个压扁的“橄榄球”上的顶点的法线向量肯定有一部分不再是从球心指向顶点了。非刚体变换的法线变换矩阵计算公式如下:

非刚体变换的法线变换矩阵 = 模型视图变换矩阵的逆矩阵的倒置矩阵

这个计算过程分为两步:首先对模型视图变换矩阵求逆,然后再倒置(即交换行列元素)。进行刚体变换的模型视图变换矩阵的逆矩阵的倒置矩阵就等于模型视图变换矩阵自身,所以在进行刚体变换时(如本例),只需要将,法线变换矩阵的值设置为模型视图变换矩阵的值即可。

 

三,设置光照

1,访问顶点着色器变量

和教程 6 一样,需要在 OpenGLView 中添加访问顶点着色器中变量的相关槽位变量,以及设置光照参数的变量。在OpenGLView.h 中,添加如下变量:

    GLuint _positionSlot;
    GLuint _modelViewSlot;
    GLuint _projectionSlot;
    GLuint _normalMatrixSlot;
    GLuint _lightPositionSlot;
    
    GLint _normalSlot;
    GLint _ambientSlot;
    GLint _diffuseSlot;
    GLint _specularSlot;
    GLint _shininessSlot;
    
    KSMatrix4 _modelViewMatrix;
    KSMatrix4 _projectionMatrix;
    
    KSVec3 _lightPosition;
    KSColor _ambient;
    KSColor _diffuse;
    KSColor _specular;
    
    GLfloat _shininess;

由于我们需要在 UI 上控制一些光照参数,因此需要将上面中的一些变量声明为属性,以方便 UI 更新它们,这些光照参数在更新之后需要重绘才能立即看到效果:

// OpenGLView.h
//
@property (nonatomic, assign) KSVec3 lightPosition;
@property (nonatomic, assign) KSColor ambient;
@property (nonatomic, assign) KSColor diffuse;
@property (nonatomic, assign) KSColor specular;
@property (nonatomic, assign) GLfloat shininess;

// OpenGLView.m
//
@synthesize lightPosition = _lightPosition;
@synthesize ambient = _ambient;
@synthesize diffuse = _diffuse;
@synthesize specular = _specular;
@synthesize shininess = _shininess;

#pragma mark Properties

-(void)setAmbient:(KSColor)ambient
{
    _ambient = ambient;
    [self render];
}

-(void)setSpecular:(KSColor)specular
{
    _specular = specular;
    [self render];
}

- (void)setLightPosition:(KSVec3)lightPosition
{
    _lightPosition = lightPosition;
    [self render];
}

-(void)setDiffuse:(KSColor)diffuse
{
    _diffuse = diffuse;
    [self render];
}

-(void)setShininess:(GLfloat)shininess
{
    _shininess = shininess;
    [self render];
}

2,访问槽位

在 OpenGLView.m 中添加 getSlotsFromProgram 方法,并在 setupProgram 方法的最后调用它。

- (void)getSlotsFromProgram
{
    // Get the attribute and uniform slot from program
    //
    _projectionSlot = glGetUniformLocation(_programHandle, "projection");
    _modelViewSlot = glGetUniformLocation(_programHandle, "modelView");
    _normalMatrixSlot = glGetUniformLocation(_programHandle, "normalMatrix");
    _lightPositionSlot = glGetUniformLocation(_programHandle, "vLightPosition");
    _ambientSlot = glGetUniformLocation(_programHandle, "vAmbientMaterial");
    _specularSlot = glGetUniformLocation(_programHandle, "vSpecularMaterial");
    _shininessSlot = glGetUniformLocation(_programHandle, "shininess");
    
    _positionSlot = glGetAttribLocation(_programHandle, "vPosition");
    _normalSlot = glGetAttribLocation(_programHandle, "vNormal");
    _diffuseSlot = glGetAttribLocation(_programHandle, "vDiffuseMaterial");
}

3,初始化和更新光照参数

在 OpenGLView.m 中添加 setupLights 和 updateLights 的方法,对光照参数进行初始化和更新光照参数到顶点着色器中:

- (void)setupLights
{                 
    // Initialize various state.
    //
    glEnableVertexAttribArray(_positionSlot);
    glEnableVertexAttribArray(_normalSlot);
    
    // Set up some default material parameters.
    //
    _lightPosition.x = _lightPosition.y = _lightPosition.z = 1.0;
    
    _ambient.r = _ambient.g = _ambient.b = 0.04;
    _specular.r = _specular.g = _specular.b = 0.5;
    _diffuse.r = 0.0;
    _diffuse.g = 0.5;
    _diffuse.b = 1.0;

    _shininess = 10;
}

- (void)updateLights
{
    glUniform3f(_lightPositionSlot, _lightPosition.x, _lightPosition.y, _lightPosition.z);
    glUniform4f(_ambientSlot, _ambient.r, _ambient.g, _ambient.b, _ambient.a);
    glUniform4f(_specularSlot, _specular.r, _specular.g, _specular.b, _specular.a);
    glVertexAttrib4f(_diffuseSlot, _diffuse.r, _diffuse.g, _diffuse.b, _diffuse.a);
    glUniform1f(_shininessSlot, _shininess);
}

setupLights 方法在 - (id)initWithCoder:(NSCoder *)aDecoder 中 setProjection 之后被调用,而 updateLights 在 updateSurface 的最后被调用,该方法在每次渲染时都会被调用(其实只需要在光照参数有变换时调用即可,在这里偷懒没有做这个优化了)。

前面说过,在顶点着色器中需要利用法线变换矩阵变换法线到视图空间,因此,也需要在程序中设置法线变换矩阵。在这里进行的是刚体变换,所以只需要将模型变换矩阵的值赋值给法线变换矩阵即可。这个赋值是在 updateSurface 方法中进行的,下面是 updateSurface 的完整代码:

- (void)updateSurface
{
    ksMatrixLoadIdentity(&_modelViewMatrix);
    
    ksTranslate(&_modelViewMatrix, 0.0, 0.0, -8);
    
    ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
    
    // Load the model-view matrix
    glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
    
    // Load the normal matrix.
    // It's orthogonal, so its Inverse-Transpose is itself!
    //
    KSMatrix3 normalMatrix3;
    ksMatrix4ToMatrix3(&normalMatrix3, &_modelViewMatrix);
    glUniformMatrix3fv(_normalMatrixSlot, 1, GL_FALSE, (GLfloat*)&normalMatrix3.m[0][0]);
    
    [self updateLights];
}

4,至此,光照设置完成。如果没出什么差错的话,编译应该是能够运行了。效果如下:


在上图中,我们确实可以看到光照的效果了。但是,这样的效果实在太二了!为什么会有不该出现的阴影呢?哪些阴影就是啥?且看下段分解!

 

四,Depth Buffer-深度缓存

1,上面的阴影问题分析

从上面的图中我们可以看到圆球的表面有一部分似乎被一些阴影给遮盖了,没错,确实是这样的。那这些阴影又是从何而来呢?它们也是球体的一部分,只不过是属于另外一个半球的-后半球面上的。在现实生活中,当我们看到一个球时,只能看到一个球的前半球,后半球是被挡住了,看不到。但在 OpenGL 中渲染一个球时,前半球和后半球都会被渲染出来,这样就会出现前后两个半球上的x,y相同只是z值不同两个点会被描绘到屏幕上的同一个像素上,先渲染的点会被后渲染的点给覆盖掉。因此,如果前半球面上的点(Z值较小的那一个)先被渲染,随后后半球面上的点(Z值较大的那一个)被渲染,会覆盖前半球面上的点,因而出现了上面那样的情况。解决这个问题的办法就是在渲染之前,对Z值进行比较,不渲染 Z 值较大的点。在 OpenGL 中,这是通过 Depth Test 实现的,之所以叫做深度测试就是因为比较的是 Z 值 - 深度-从屏幕往里。还记得教程02中渲染管线流程图么?深度测试是在模版测试之后,blending 之前。


2,Depth Buffer 简介

要进行Z值的比较(深度测试),那么 OpenGL 需要有一个地方来保存Z值,这个地方就是 Depth Buffer。Depth Buffer 与 Stencil Buffer,Color Buffer 并称 OpenGL 三大缓存。Depth Buffer 和 Stencil Buffer 也是 render buffer,但与 Color Buffer 不同(RGBA四元组),它们均只有一个组成元素,Depth Buffer 只需要保存Z值,而 Stencil Buffer(模版缓存)也只需要保存一个用于模版测试的值(后面会有文章介绍)。

3,使用 Depth Buffer

既然 Depth Buffer 也是 render buffer,那么其创建与删除与之前在教程01中介绍的 color buffer 别无二样:

// OpenGLView.h
//
GLuint _depthRenderBuffer;

// OpenGLView.m
//

// Create a depth buffer that has the same size as the color buffer.
int width, height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

glGenRenderbuffers(1, &_depthRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                              GL_RENDERBUFFER, _depthRenderBuffer);

删除也是使用 glDeleteRenderbuffers 方法。

默认情况下,OpenGL 是不会开启深度测试的,因此需要明确调用 glEnable(GL_DEPTH_TEST) 来开启。在 setupProjection 方法的最后添加这一句即可。

4,编译运行,啊哈,一切OK!


在本示例中,有很多滑块用来控制各种光照参数(环境材质,漫反射材质,镜面材质以及光泽强度),并有两个模型可供切换。不妨多多滑动,体验下不同的参数会有什么效果,从而加深对 OpenGL 光照的理解。

 

五,结语

有了前文《光照原理》 的理论基础,今天来实践 Per-Vextex 光照,非常容易吧。接下来将介绍 Per-Pixel 光照以及卡通效果,性急的童鞋可以先浏览源代码,看看能不能看个明白。BTW,教程代码已经非常超前了-写到教程13了,写文章的速度大大落后了。写文章花费的时间和精力实在是不少的,深度体会在中国写一本书的辛苦,而往往回报远不及付出,嗯,要多多支持正版。

 


【目录】- MATLAB神经网络30个案例分析(开发实例系列图书) 第1章 BP神经网络的数据分类——语音特征信号分类1 本案例选取了民歌、古筝、摇滚和流行四类不同音乐,用BP神经网络实现对这四类音乐的有效分类。 第2章 BP神经网络的非线性系统建模——非线性函数拟合11 本章拟合的非线性函数为y=x21+x22。 第3章 遗传算法优化BP神经网络——非线性函数拟合21 根据遗传算法和BP神经网络理论,在MATLAB软件中编程实现基于遗传算法优化的BP神经网络非线性系统拟合算法。 第4章 神经网络遗传算法函数极值寻优——非线性函数极值寻优36 对于未知的非线性函数,仅通过函数的输入输出数据难以准确寻找函数极值。这类问题可以通过神经网络结合遗传算法求解,利用神经网络的非线性拟合能力和遗传算法的非线性寻优能力寻找函数极值。 第5章 基于BP_Adaboost的强分类器设计——公司财务预警建模45 BP_Adaboost模型即把BP神经网络作为弱分类器,反复训练BP神经网络预测样本输出,通过Adaboost算法得到多个BP神经网络弱分类器组成的强分类器。 第6章 PID神经元网络解耦控制算法——多变量系统控制54 根据PID神经元网络控制器原理,在MATLAB中编程实现PID神经元网络控制多变量耦合系统。 第7章 RBF网络的回归——非线性函数回归的实现65 本例用RBF网络拟合未知函数,预先设定一个非线性函数,如式y=20+x21-10cos(2πx1)+x22-10cos(2πx2)所示,假定函数解析式不清楚的情况下,随机产生x1,x2和由这两个变量按上式得出的y。将x1,x2作为RBF网络的输入数据,将y作为RBF网络的输出数据,分别建立近似和精确RBF网络进行回归分析,并评价网络拟合效果。 第8章 GRNN的数据预测——基于广义回归神经网络的货运量预测73 根据货运量影响因素的分析,分别取国内生产总值(GDP),工业总产值,铁路运输线路长度,复线里程比重,公路运输线路长度,等级公路比重,铁路货车数量和民用载货汽车数量8项指标因素作为网络输入,以货运总量,铁路货运量和公路货运量3项指标因素作为网络输出,构建GRNN,由于训练数据较少,采取交叉验证方法训练GRNN神经网络,并用循环找出最佳的SPREAD。 第9章 离散Hopfield神经网络的联想记忆——数字识别81 根据Hopfield神经网络相关知识,设计一个具有联想记忆功能的离散型Hopfield神经网络。要求该网络可以正确地识别0~9这10个数字,当数字被一定的噪声干扰后,仍具有较好的识别效果。 第10章 离散Hopfield神经网络的分类——高校科研能力评价90 某机构对20所高校的科研能力进行了调研和评价,试根据调研结果中较为重要的11个评价指标的数据,并结合离散Hopfield神经网络的联想记忆能力,建立离散Hopfield高校科研能力评价模型。 第11章 连续Hopfield神经网络的优化——旅行商问题优化计算100 现对于一个城市数量为10的TSP问题,要求设计一个可以对其进行组合优化的连续型Hopfield神经网络模型,利用该模型可以快速地找到最优(或近似最优)的一条路线。 第12章 SVM的数据分类预测——意大利葡萄酒种类识别112 将这178个样本的50%做为训练集,另50%做为测试集,用训练集对SVM进行训练可以得到分类模型,再用得到的模型对测试集进行类别标签预测。 第13章 SVM的参数优化——如何更好的提升分类器的性能122 本章要解决的问题就是仅仅利用训练集找到分类的最佳参数,不但能够高准确率的预测训练集而且要合理的预测测试集,使得测试集的分类准确率也维持在一个较高水平,即使得得到的SVM分类器的学习能力和推广能力保持一个平衡,避免过学习和欠学习状况发生。 第14章 SVM的回归预测分析——上证指数开盘指数预测133 对上证指数从1990.12.20-2009.08.19每日的开盘数进行回归分析。 第15章 SVM的信息粒化时序回归预测——上证指数开盘指数变化趋势和变化空间预测141 在这个案例里面我们将利用SVM对进行模糊信息粒化后的上证每日的开盘指数进行变化趋势和变化空间的预测。 若您对此书内容有任何疑问,可以凭在线交流卡登录中文论坛与作者交流。 第16章 自组织竞争网络在模式分类中的应用——患者癌症发病预测153 本案例中给出了一个含有60个个体基因表达水平的样本。每个样本中测量了114个基因特征,其中前20个样本是癌症病人的基因表达水平的样本(其中还可能有子类), 中间的20个样本是正常人的基因表达信息样本, 余下的20个样本是待检测的样本(未知它们是否正常)。以下将设法找出癌症与正常样本在基因表达水平上的区
相关推荐
<p> <span style="font-size:14px;color:#E53333;">限时福利1:</span><span style="font-size:14px;">购课进答疑群专享柳峰(刘运强)老师答疑服务</span> </p> <p> <br /> </p> <p> <br /> </p> <p> <span style="font-size:14px;"></span> </p> <p> <span style="font-size:14px;color:#337FE5;"><strong>为什么需要掌握高性能的MySQL实战?</strong></span> </p> <p> <span><span style="font-size:14px;"><br /> </span></span> <span style="font-size:14px;">由于互联网产品用户量大、高并发请求场景多,因此对MySQL的性能、可用性、扩展性都提出了很高的要求。使用MySQL解决大量数据以高并发请求已经是程序员的必备技能,也是衡量一个程序员能力和薪资的标准之一。</span> </p> <p> <br /> </p> <p> <span style="font-size:14px;">为了让大家快速系统了解高性能MySQL核心知识全貌,我为你总结了</span><span style="font-size:14px;">「高性能 MySQL 知识框架图」</span><span style="font-size:14px;">,帮你梳理学习重点,建议收藏!</span> </p> <p> <br /> </p> <p> <img alt="" src="https://img-bss.csdnimg.cn/202006031401338860.png" /> </p> <p> <br /> </p> <p> <span style="font-size:14px;color:#337FE5;"><strong>【课程设计】</strong></span> </p> <p> <span style="font-size:14px;"><br /> </span> </p> <p> <span style="font-size:14px;">课程分为四大篇章,将为你建立完整的 MySQL 知识体系,同时将重点讲解 MySQL 底层运行原理、数据库的性能调优、高并发、海量业务处理、面试解析等。</span> </p> <p> <span style="font-size:14px;"><br /> </span> </p> <p> <span style="font-size:14px;"></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>一、性能优化篇:</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括经典 MySQL 问题剖析、索引底层原理和事务与锁机制。通过深入理解 MySQL 的索引结构 B+Tree ,学员能够从根本上弄懂为什么有些 SQL 走索引、有些不走索引,从而彻底掌握索引的使用和优化技巧,能够避开很多实战中遇到的“坑”。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>二、MySQL 8.0新特性篇:</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括窗口函数和通用表表达式。企业中的许多报表统计需求,如果不采用窗口函数,用普通的 SQL 语句是很难实现的。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>三、高性能架构篇:</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">主要包括主从复制和读写分离。在企业的生产环境中,很少采用单台MySQL节点的情况,因为一旦单个节点发生故障,整个系统都不可用,后果往往不堪设想,因此掌握高可用架构的实现是非常有必要的。</span> </p> <p style="text-align:justify;"> <br /> </p> <p style="text-align:justify;"> <span style="font-size:14px;"><strong>四、面试篇:</strong></span> </p> <p style="text-align:justify;"> <span style="font-size:14px;">程序员获得工作的第一步,就是高效的准备面试,面试篇主要从知识点回顾总结的角度出发,结合程序员面试高频MySQL问题精讲精练,帮助程序员吊打面试官,获得心仪的工作机会。</span> </p>
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页