美文网首页OpenGLES程序员OpenGL
iOS-OpenGL ES入门教程(四)光照

iOS-OpenGL ES入门教程(四)光照

作者: 安东_Ace | 来源:发表于2018-06-14 11:37 被阅读263次

    前言

    前面的基础文章列表

    1. iOS-零基础学习OpenGL ES入门教程(一)
    2. iOS-OpenGL ES入门教程(二)最简单的纹理Demo
    3. iOS-OpenGL ES入门教程(三)纹理取样,混合,多重纹理

    下面来讲一下光照

    光照

    先直观看下使用3D灯光模拟技术和不使用的对比图


    灯光效果

    可以看到使用灯光模拟会让图形更加立体真实。

    计算机模拟光照的通俗原理:GPU为每个三角形的顶点进行光线计算,再把结果进行插值,得出每个片元的最终颜色。

    模拟光照

    OpenGL ES的灯光模拟包括:环境光、漫反射光、镜面反射光。如上图所示。

    一个渲染三角形中每个光线的组成部分取决于三个互相关联的因素

    1. 光线的设置
    2. 三角形相对于光线方向
    3. 三角形的材质

    光线的计算依赖于表面法向量,法向量可以通过矢量积进行计算。

    由于表面法向量决定了平面的方向。通过光线和法向量的角度则
    可以计算出漫反射光,环境光,镜面反射光的模拟。这里主要是几何部分内容,不做细讲,使用GLkit,系统会内置模拟计算出灯光效果。

    OpenGL ES程序为每个顶点指定了单独的法向量,和顶点的位置,纹理坐标一起保存起来,从而实现模拟灯光的效果

    如果一个三角形的三个顶点赋予相同的法向量,则叫平面法线。
    如果每个顶点使用包含该顶点的平均值,灯光模拟会创建三角形轻微弯曲感,如下图。

    平面法线和平均法线

    实例Demo

    我们做一个Demo来直观的看一下灯光和法向量,依然使用GLkit框架为我们简化步骤。

    先看一下demo效果 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<NUM_FACES; i++)
        {
            GLKVector3 faceNormal = SceneTriangleFaceNormal(
                                                            someTriangles[i]);
            someTriangles[i].vertices[0].normal = faceNormal;
            someTriangles[i].vertices[1].normal = faceNormal;
            someTriangles[i].vertices[2].normal = faceNormal;
        }
    }
    

    如果使用顶点所包含的所有三角形的平均法向量,函数计算如下

    //更新三角形法向量 顶点采用平均法向量
    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<NUM_FACES; i++)
        {
            faceNormals[i] = SceneTriangleFaceNormal(
                                                     someTriangles[i]);
        }
        
        //每个顶点的平均法向量
        newVertexA.normal = faceNormals[0];
        newVertexB.normal = GLKVector3MultiplyScalar(
                                                     GLKVector3Add(
                                                                   GLKVector3Add(
                                                                                 GLKVector3Add(
                                                                                               faceNormals[0],
                                                                                               faceNormals[1]),
                                                                                 faceNormals[2]),
                                                                   faceNormals[3]), 0.25);
        newVertexC.normal = faceNormals[1];
        newVertexD.normal = GLKVector3MultiplyScalar(
                                                     GLKVector3Add(
                                                                   GLKVector3Add(
                                                                                 GLKVector3Add(
                                                                                               faceNormals[0],
                                                                                               faceNormals[2]),
                                                                                 faceNormals[4]),
                                                                   faceNormals[6]), 0.25);
        newVertexE.normal = GLKVector3MultiplyScalar(
                                                     GLKVector3Add(
                                                                   GLKVector3Add(
                                                                                 GLKVector3Add(
                                                                                               faceNormals[2],
                                                                                               faceNormals[3]),
                                                                                 faceNormals[4]),
                                                                   faceNormals[5]), 0.25);
        newVertexF.normal = GLKVector3MultiplyScalar(
                                                     GLKVector3Add(
                                                                   GLKVector3Add(
                                                                                 GLKVector3Add(
                                                                                               faceNormals[1],
                                                                                               faceNormals[3]),
                                                                                 faceNormals[5]),
                                                                   faceNormals[7]), 0.25);
        newVertexG.normal = faceNormals[6];
        newVertexH.normal = GLKVector3MultiplyScalar(
                                                     GLKVector3Add(
                                                                   GLKVector3Add(
                                                                                 GLKVector3Add(
                                                                                               faceNormals[4],
                                                                                               faceNormals[5]),
                                                                                 faceNormals[6]),
                                                                   faceNormals[7]), 0.25);
        newVertexI.normal = faceNormals[7];
        
        //更新triangles
        someTriangles[0] = SceneTriangleMake(
                                             newVertexA,
                                             newVertexB,
                                             newVertexD);
        someTriangles[1] = SceneTriangleMake(
                                             newVertexB,
                                             newVertexC,
                                             newVertexF);
        someTriangles[2] = SceneTriangleMake(
                                             newVertexD,
                                             newVertexB,
                                             newVertexE);
        someTriangles[3] = SceneTriangleMake(
                                             newVertexE,
                                             newVertexB,
                                             newVertexF);
        someTriangles[4] = SceneTriangleMake(
                                             newVertexD,
                                             newVertexE,
                                             newVertexH);
        someTriangles[5] = SceneTriangleMake(
                                             newVertexE,
                                             newVertexF,
                                             newVertexH);
        someTriangles[6] = SceneTriangleMake(
                                             newVertexG,
                                             newVertexD,
                                             newVertexH);
        someTriangles[7] = SceneTriangleMake(
                                             newVertexH,
                                             newVertexF,
                                             newVertexI);
    }
    

    法线和灯光方向顶点数据源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卷

    相关文章

      网友评论

        本文标题:iOS-OpenGL ES入门教程(四)光照

        本文链接:https://www.haomeiwen.com/subject/jfqreftx.html