前言
前面的基础文章列表
- iOS-零基础学习OpenGL ES入门教程(一)
- iOS-OpenGL ES入门教程(二)最简单的纹理Demo
- iOS-OpenGL ES入门教程(三)纹理取样,混合,多重纹理
下面来讲一下光照
光照
先直观看下使用3D灯光模拟技术和不使用的对比图
可以看到使用灯光模拟会让图形更加立体真实。
计算机模拟光照的通俗原理:GPU为每个三角形的顶点进行光线计算,再把结果进行插值,得出每个片元的最终颜色。
OpenGL ES的灯光模拟包括:环境光、漫反射光、镜面反射光。如上图所示。
一个渲染三角形中每个光线的组成部分取决于三个互相关联的因素
- 光线的设置
- 三角形相对于光线方向
- 三角形的材质
光线的计算依赖于表面法向量,法向量可以通过矢量积进行计算。
由于表面法向量决定了平面的方向。通过光线和法向量的角度则
可以计算出漫反射光,环境光,镜面反射光的模拟。这里主要是几何部分内容,不做细讲,使用GLkit,系统会内置模拟计算出灯光效果。
OpenGL ES程序为每个顶点指定了单独的法向量,和顶点的位置,纹理坐标一起保存起来,从而实现模拟灯光的效果
如果一个三角形的三个顶点赋予相同的法向量,则叫平面法线。
如果每个顶点使用包含该顶点的平均值,灯光模拟会创建三角形轻微弯曲感,如下图。
实例Demo
我们做一个Demo来直观的看一下灯光和法向量,依然使用GLkit框架为我们简化步骤。
先看一下demo效果绿色线是顶点法线,而黄色线是灯光方向。图中可以直观看到法向量随着顶点变化。
下面看下核心代码部分
数据部分
//顶点
typedef struct {
GLKVector3 position; //顶点
GLKVector3 normal; //法线
}
SceneVertex;
//三角形
typedef struct {
SceneVertex vertices[3];
}
SceneTriangle;
//9个数据顶点
static const SceneVertex vertexA =
{{-0.5, 0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexB =
{{-0.5, 0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexC =
{{-0.5, -0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexD =
{{ 0.0, 0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexE =
{{ 0.0, 0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexF =
{{ 0.0, -0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexG =
{{ 0.5, 0.5, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexH =
{{ 0.5, 0.0, -0.5}, {0.0, 0.0, 1.0}};
static const SceneVertex vertexI =
{{ 0.5, -0.5, -0.5}, {0.0, 0.0, 1.0}};
对应的九个数据顶点。
//8 triangles
#define NUM_FACES (8)
//48个法线顶点
#define NUM_NORMAL_LINE_VERTS (48)
//48法线顶点+两个灯光方向顶点
#define NUM_LINE_VERTS (NUM_NORMAL_LINE_VERTS + 2)
8个数据源三角形,每个三角形有三个顶点。也就是法线24条。每条法线绘制需要起始和终止两个顶点,也就是48个数据源顶点,额外两个顶点用于绘制灯光方向。这里宏定义出来。
属性部分
@interface OpenGLES_LightDemoViewController (){
//8个三角形
SceneTriangle triangles[NUM_FACES];
}
@property (strong, nonatomic) GLKBaseEffect *baseEffect;
@property (strong, nonatomic) GLKBaseEffect *extraEffect;
//顶点buffer
@property (nonatomic,assign) GLuint vertexBufferID;
//用于绘制法线方向的buffer
@property (nonatomic,assign) GLuint extraBufferID;
@property (nonatomic) GLfloat centerVertexHeight;
@property (nonatomic) BOOL shouldUseFaceNormals;
@property (nonatomic) BOOL shouldDrawNormals;
@end
下面列举下矢量的计算函数
给定两个顶点求出法向量函数
//法向量
GLKVector3 SceneVector3UnitNormal(
const GLKVector3 vectorA,
const GLKVector3 vectorB)
{
return GLKVector3Normalize(
GLKVector3CrossProduct(vectorA, vectorB));
}
triangle的法向量函数
//triangle的法向量
static GLKVector3 SceneTriangleFaceNormal(
const SceneTriangle triangle)
{
GLKVector3 vectorA = GLKVector3Subtract(
triangle.vertices[1].position,
triangle.vertices[0].position);
GLKVector3 vectorB = GLKVector3Subtract(
triangle.vertices[2].position,
triangle.vertices[0].position);
return SceneVector3UnitNormal(
vectorA,
vectorB);
}
构造triangle
//生成triangle
static SceneTriangle SceneTriangleMake(
const SceneVertex vertexA,
const SceneVertex vertexB,
const SceneVertex vertexC)
{
SceneTriangle result;
result.vertices[0] = vertexA;
result.vertices[1] = vertexB;
result.vertices[2] = vertexC;
return result;
}
如果采用顶点计算法向量,函数如下
//计算8个三角形的法向量,并且赋值更新
static void SceneTrianglesUpdateFaceNormals(
SceneTriangle someTriangles[NUM_FACES])
{
int i;
for (i=0; i
如果使用顶点所包含的所有三角形的平均法向量,函数计算如下
//更新三角形法向量 顶点采用平均法向量
static void SceneTrianglesUpdateVertexNormals(
SceneTriangle someTriangles[NUM_FACES])
{
SceneVertex newVertexA = vertexA;
SceneVertex newVertexB = vertexB;
SceneVertex newVertexC = vertexC;
SceneVertex newVertexD = vertexD;
SceneVertex newVertexE = someTriangles[3].vertices[0];
SceneVertex newVertexF = vertexF;
SceneVertex newVertexG = vertexG;
SceneVertex newVertexH = vertexH;
SceneVertex newVertexI = vertexI;
GLKVector3 faceNormals[NUM_FACES];
// Calculate the face normal of each triangle
for (int i=0; i
法线和灯光方向顶点数据源update函数
//更新三角形法线 还有灯光方向线
static void SceneTrianglesNormalLinesUpdate(
const SceneTriangle someTriangles[NUM_FACES],
GLKVector3 lightPosition,
GLKVector3 someNormalLineVertices[NUM_LINE_VERTS])
{
int trianglesIndex;
int lineVetexIndex = 0;
// 每条法向量的顶点确定,用于绘制法线
for (trianglesIndex = 0; trianglesIndex < NUM_FACES;
trianglesIndex++)
{
someNormalLineVertices[lineVetexIndex++] =
someTriangles[trianglesIndex].vertices[0].position;
someNormalLineVertices[lineVetexIndex++] =
GLKVector3Add(
someTriangles[trianglesIndex].vertices[0].position,
GLKVector3MultiplyScalar(
someTriangles[trianglesIndex].vertices[0].normal,
0.5));
someNormalLineVertices[lineVetexIndex++] =
someTriangles[trianglesIndex].vertices[1].position;
someNormalLineVertices[lineVetexIndex++] =
GLKVector3Add(
someTriangles[trianglesIndex].vertices[1].position,
GLKVector3MultiplyScalar(
someTriangles[trianglesIndex].vertices[1].normal,
0.5));
someNormalLineVertices[lineVetexIndex++] =
someTriangles[trianglesIndex].vertices[2].position;
someNormalLineVertices[lineVetexIndex++] =
GLKVector3Add(
someTriangles[trianglesIndex].vertices[2].position,
GLKVector3MultiplyScalar(
someTriangles[trianglesIndex].vertices[2].normal,
0.5));
}
// 添加法线顶点
someNormalLineVertices[lineVetexIndex++] =
lightPosition;
someNormalLineVertices[lineVetexIndex] = GLKVector3Make(
0.0,
0.0,
-0.5);
}
虽然计算部分函数比较繁琐,但是相对其实是简单的,因为这一块主要还是线性代数相关的。搞懂了gpu模拟灯光的原理,那么对应计算也就好理解了。
渲染部分代码
- (void)viewDidLoad {
[super viewDidLoad];
GLKView *view = (GLKView *)self.view;
NSAssert([view isKindOfClass:[GLKView class]],
@"View controller's view is not a GLKView");
view.context = [[EAGLContext alloc]
initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:view.context];
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.light0.enabled = GL_TRUE;
//设置灯光漫反射颜色
self.baseEffect.light0.diffuseColor = GLKVector4Make(
0.7f, // Red
0.7f, // Green
0.7f, // Blue
1.0f);// Alpha
//灯光位置
self.baseEffect.light0.position = GLKVector4Make(
1.0f,
1.0f,
0.5f,
0.0f);
//设置绘制法线的baseEffect
self.extraEffect = [[GLKBaseEffect alloc] init];
self.extraEffect.useConstantColor = GL_TRUE;
self.extraEffect.constantColor = GLKVector4Make(
0.0f, // Red
1.0f, // Green
0.0f, // Blue
1.0f);// Alpha
{
//这里是视点变换,暂时不做解释,用于下一章在讲解
GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(
GLKMathDegreesToRadians(-60.0f), 1.0f, 0.0f, 0.0f);
modelViewMatrix = GLKMatrix4Rotate(
modelViewMatrix,
GLKMathDegreesToRadians(-30.0f), 0.0f, 0.0f, 1.0f);
modelViewMatrix = GLKMatrix4Translate(
modelViewMatrix,
0.0f, 0.0f, 0.25f);
self.baseEffect.transform.modelviewMatrix = modelViewMatrix;
self.extraEffect.transform.modelviewMatrix = modelViewMatrix;
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//使用顶点初始化八个三角形数据
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);
//Bind vertexBuffer
glGenBuffers(1, &_vertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferID);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangles), triangles, GL_DYNAMIC_DRAW);
//Bind 法线绘制的Buffer 默认是不绘制的
glGenBuffers(1, &_extraBufferID);
glBindBuffer(GL_ARRAY_BUFFER, _extraBufferID);
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_DYNAMIC_DRAW);
//默认展示效果
self.centerVertexHeight = 0.0f;
self.shouldUseFaceNormals = YES;
}
依然使用GLkit框架的baseEffect帮我们简化灯光操作
light0.diffuseColor和light0.position指定了灯光位置和漫反射颜色。
transform.modelviewMatrix这里是绕着x和z轴做了变换,方便观看,下一章视点会详细讲这里。这里不做多解释
_vertexBufferID 生成三角形的缓存
_extraBufferID 生产法线缓存
同理baseEffect也对应的创建两个,用于绘制不同效果。
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
[self.baseEffect prepareToDraw];
glClear(GL_COLOR_BUFFER_BIT);
//位置缓存
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferID);
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SceneVertex), NULL + offsetof(SceneVertex, position));
//法线缓存
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferID);
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, sizeof(SceneVertex), NULL + offsetof(SceneVertex, normal));
//绘制
glDrawArrays(GL_TRIANGLES, 0, sizeof(triangles) / sizeof(SceneVertex));
if(self.shouldDrawNormals)
{
[self drawNormals];
}
}
绘制部分依然是常规的绘制步骤。这里不做累述。指定指针偏移,绘制。
法线绘制
//绘制法线
- (void)drawNormals
{
GLKVector3 normalLineVertices[NUM_LINE_VERTS];
//更新48个法向量顶点和两个灯光方向顶点
SceneTrianglesNormalLinesUpdate(triangles,
GLKVector3MakeWithArray(self.baseEffect.light0.position.v),
normalLineVertices);
glBindBuffer(GL_ARRAY_BUFFER, _extraBufferID);
glBufferData(GL_ARRAY_BUFFER, NUM_LINE_VERTS * sizeof(GLKVector3), normalLineVertices, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLKVector3), NULL);
//绘制每条顶点法线
self.extraEffect.useConstantColor = GL_TRUE;
self.extraEffect.constantColor =
GLKVector4Make(0.0, 1.0, 0.0, 1.0); // Green
[self.extraEffect prepareToDraw];
glDrawArrays(GL_LINES, 0, NUM_NORMAL_LINE_VERTS);
//绘制灯光方向
self.extraEffect.constantColor =
GLKVector4Make(1.0, 1.0, 0.0, 1.0); // Yellow
[self.extraEffect prepareToDraw];
glDrawArrays(GL_LINES, NUM_NORMAL_LINE_VERTS, NUM_LINE_VERTS);
}
使用绿色绘制法线。黄色绘制灯光方向。
可以自行代码调节灯光的位置和光线属性,查看对应的效果变化。
小思考:1. 我们仅仅使用了顶点的法向量模拟灯光效果,那么相应的是不是可以给每个片元都缓存法向量呢。这样更加真实。
答:是可以的,这里的偏远计算,在每个RGB纹素编码的过程中加入x,y,z的法向量分量,这样的纹理叫做法线贴图。(或者凹凸贴图,DOT3灯光,这三个名词本质都是一种描述)
灯光烘焙到纹理
同样我们可以把灯光烘焙到纹理中,GPU模拟灯光需要做出的运算量非常大,烘焙到纹理则可以避开模拟灯光的矢量运算。但是相应的光烘焙进纹理仅仅适用于静态场景。在灯光位置会改变,动态场景下显然是不适用的。
Demo代码地址:LearnOpenGLESDemo
源码来源于书籍:1. OpenGL ES应用开发实践指南:iOS卷