OpenGL渲染技巧

首先,我们来绘制一个3D图形“甜甜圈”,示例程序运行效果如下:
1.png

咋看,似乎没什么问题。但当我们试着向右旋转它的时候,会出现以下问题:
2.png

其实, 这个“甜甜圈”是由很多个三角形组成的实体对象,其中一些三角形在“甜甜圈”的背面,而另一些则在“甜甜圈”的正面。但我们看不到背面——至少我们应该是看不到的(不考虑透明几何体的特殊情况)。在默认情况下,第一个绘制的三角形可能会被后面绘制的三角形覆盖,三角形绘制的顺序就会一团糟,所以会导致上图的情况。对于这种问题可以有以下的解决办法:

油画法

  • 对这些三角形进行排序,并且首先渲染那些较远的三角形,再在它们上方渲染那些较近的三角形,这种方式称为“油画法”。

  • 也就是先绘制场景中离观察者较远的物体,再绘制较近的物体。

  • 例如下⾯的图例:先绘制红⾊部分,再绘制⻩⾊部分,最后再绘制灰⾊部分,即可解决隐藏⾯消除的问题
    3.png
  • 使⽤油画算法,只要将场景按照物理距离观察者的距离远近排序,由远及近的绘制即可。那么会出现什么问题? 如果三个三⻆形是叠加的情况,油画算法将⽆法处理。
    4.png
  • 这种方法在计算机图形处理中是非常低效的,主要原因有两个。其中一个原因是,我们必须对任何发生几何图形重叠地方的每个像素进行两次写操作,而在存储其中进行写操作会使速度变慢。另外一个原因是,对独立的三角形进行排序的开销会过高。我们有更好的办法。

在绘制3D场景的时候,我们需要决定哪些部分是对观察者可⻅的,或者哪些部分是对观察者不可⻅的。对于不可⻅的部分,应该及早丢弃。例如在⼀个不透明的墙壁后,就不应该渲染。这种情况叫做”隐藏⾯消除”(Hidden surface elimination)。所以延伸出了下面两种方法,由OpenGL为我们提供,我们只需开启即可:

正背⾯剔除(Face Culling)

  • 背景

    • 尝试相信⼀个3D图形,你从任何⼀个⽅向去观察,最多可以看到⼏个⾯?
    • 答案是,最多3⾯。 从⼀个⽴⽅体的任意位置和⽅向上看,你不可能看到多于3个⾯。
    • 那么思考? 我们为何要多余的去绘制那根本看不到的3个⾯?
    • 如果我们能以某种⽅式去丢弃这部分数据,OpenGL 在渲染的性能即可提⾼超过50%。
  • 解决问题

    • 如何知道某个⾯在观察者的视野中不会出现?
    • 任何平⾯都有2个⾯,正⾯/背⾯。意味着你⼀个时刻只能看到⼀⾯。
    • OpenGL 可以做到检查所有正⾯朝向观察者的⾯,并渲染它们。从⽽丢弃背⾯朝向的⾯。这样可以节约⽚元着⾊器的性能。
    • 如何告诉OpenGL 你绘制的图形,哪个⾯是正⾯,哪个⾯是背⾯?
    • 答案: 通过分析顶点数据的顺序

    分析顶点顺序

    分析顶点顺序.png
  • 正⾯/背⾯区分

    • 正⾯: 按照逆时针顶点连接顺序的三⻆形⾯
    • 背⾯: 按照顺时针顶点连接顺序的三⻆形⾯

    分析⽴⽅体中的正背⾯
    5.png

  • 分析

    • 左侧三⻆形顶点顺序为: 1—> 2—> 3;右侧三⻆形的顶点顺序为: 1—> 2—> 3。
    • 当观察者在右侧时,则右边的三⻆形⽅向为逆时针⽅向则为正⾯,⽽左侧的三⻆形为顺时针则为背⾯。
    • 当观察者在左侧时,则左边的三⻆形为逆时针⽅向判定为正⾯,⽽右侧的三⻆形为顺时针判定为背⾯。
  • 总结

    • 正⾯和背⾯是由三⻆形的顶点定义顺序和观察者⽅向共同决定的。随着观察者的⻆度⽅向的改变,正⾯背⾯也会跟着改变。

总之,对正面和背面三角形进行区分的原因之一就是为了剔除。背面剔除能够极大地提高性能,这种方式是非常高效的,在渲染的图元装配阶段就整体抛弃了一些三角形,并且没有执行任何不恰当的光栅化操作。一般正背面剔除按如下方式开启:

开启表⾯剔除(默认背⾯剔除)
void glEnable(GL_CULL_FACE);

关闭表⾯剔除(默认背⾯剔除)
void glDisable(GL_CULL_FACE);

⽤户选择剔除那个⾯(正⾯/背⾯)
void glCullFace(GLenum mode);
mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK

⽤户指定绕序那个为正⾯
void glFrontFace(GLenum mode);
mode参数为: GL_CW,GL_CCW,默认值:GL_CCW

例如,剔除正⾯实现(1)
glCullFace(GL_BACK);
glFrontFace(GL_CW);

例如,剔除正⾯实现(2)
glCullFace(GL_FRONT);

然后右键单击示例程序,并选择“开启背面剔除”菜单选项。
屏幕录制2020-08-16-下午7.56.29.gif

但当我们再继续向右旋转,旋转到一定角度是又出现了新的问题
6.png

所以接下来我们讲到另一种高效消除隐藏表面的技术,深度测试。

深度测试

  • 什么是深度?

  • 深度其实就是该像素点在3D世界中距离摄像机的距离,z值

  • 什么是深度缓冲区?

  • 深度缓存区,就是⼀块内存区域,专⻔存储着每个像素点(绘制在屏幕上的)深度值。深度值(z值)越⼤,则离摄像机就越远。

  • 为什么需要深度缓冲区?

  • 在不使⽤深度测试的时候,如果我们先绘制⼀个距离⽐较近的物理,再绘制距离较远的物理,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。 有了深度缓冲区后,绘制物体的顺序就不那么重要的。 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写⼊到缓冲区中。除⾮调用glDepthMask(GL_FALSE)来禁⽌写⼊。

  • 在绘制一个像素时,将一个值(称为z值)分配给它,这个值表示它到观察者的距离。然后,当另外一个像素需要在屏幕上的同样位置进行绘制时,新像素的z值将已经存储的像素的z值进行比较。如果新像素的z值比较大,那么它距离观察者就比较近,这样就在原来的像素上面,所以原来的像素就会被新的像素覆盖。如果新像素的z值更低,那么它就必须位于原来像素的后面,不能遮住原来的像素。在内部,这个任务是通过深度缓冲区实现的,它存储了屏幕上每个像素的深度值。这就是深度测试。

  • 深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)是对应的。颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息。在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较。如果⼤于深度缓冲区中的值,则丢弃这部分。否则利⽤这个像素对应的深度值和颜⾊值。分别更新深度缓冲区和颜⾊缓存区。这个过程称为”深度测试”。

深度测试在绘制多个对象时能够进一步解决性能问题。就算背面剔除能够消除位于对象背面的三角形,那么如果是重叠的独立对象又该怎么办呢?这就会出现图6所示的问题,开启深度测试将消除那些应该被已存在像素覆盖的像素,这将节省可观的存储器带宽。一般深度测试按如下方式开启:

申请一个颜色缓冲区和一个深度缓冲区
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);

开启深度测试
glEnable(GL_DEPTH_TEST);

在绘制场景前,清除颜⾊缓存区,深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

然后右键单击示例程序,并选择“开启深度测试”菜单选项。
屏幕录制2020-08-16-下午9.32.08.gif

多边形模式

多边形(含三角形)不一定是实心的。在默认情况下,多边形是作为实心图形绘制的,但我们可以通过将多边形指定为显示轮廓或只有点(只显示顶点)来改变这种行为。可以在多边形的两面都应用这个渲染模式,也可以只在正面或背面应用。

void glPolygonMode(GLenum face,GLenum mode);
和表面剔除一样,face参数的可用值为GL_FRONT、GL_BACK或GL_FRONT_AND_BACK。
mode参数的可用值为GL_FILL(默认值)、GL_LINE或GL_POINT。

然后右键单击示例程序,并选择“设置线模式”菜单选项。
7.png

然后右键单击示例程序,并选择“设置点模式”菜单选项。
8.png

z冲突(z-fighting)

  • 为什么会出现z冲突问题
  • 因为开启深度测试后,OpenGL 就不会再去绘制模型被遮挡的部分。这样实现的显示更加真实。但是由于深度缓冲区精度的限制对于深度相差⾮常⼩的情况下。(例如在同⼀平⾯上进⾏2次绘制),OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测。显示出来的现象时交错闪烁,前⾯2个画⾯交错出现。
9.png
10.png
  • 第⼀步: 启⽤ Polygon Offset ⽅式解决
  • 解决⽅法:让深度值之间产⽣间隔。如果2个图形之间有间隔,是不是意味着就不会产⽣⼲涉。可以理解为在执⾏深度测试前将⽴⽅体的深度值做⼀些细微的增加。于是就能将重叠的2个图形深度值与之前有所区分。
//启⽤Polygon Offset ⽅式
glEnable(GL_POLYGON_OFFSET_FILL)

参数列表: 
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT 
GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE 
GL_POLYGON_OFFSET_FILL 对应光栅化模式: GL_FILL
  • 第⼆步: 指定偏移量
  • 通过glPolygonOffset 来指定。glPolygonOffset 需要2个参数: factor ,units
  • 每个Fragment 的深度值都会增加如下所示的偏移量: 
    Offset = ( m * factor ) + ( r * units); 

    m : 多边形的深度的斜率的最⼤值,理解⼀个多边形越是与近裁剪⾯平⾏,m就越接近于0。
    r : 能产⽣于窗⼝坐标系的深度值中可分辨的差异最⼩值。r 是由具体是由具体OpenGL 平台指定的⼀个常量。
  • ⼀个⼤于0的Offset 会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset 会把模型拉近
  • ⼀般⽽⾔,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 基本可以满⾜需求。
void glPolygonOffset(Glfloat factor,Glfloat units);

应⽤到⽚段上总偏移计算⽅程式
Depth Offset = (DZ * factor) + (r * units);

DZ:深度值(Z值)
r:使得深度缓冲区产⽣变化的最⼩值

负值,将使得z值距离我们更近,⽽正值,将使得z值距离我们更远,一般把factor和units设置为-1,-1即可
  • 第三步: 关闭Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL)

示例程序的代码如下:

//GLTool.h头⽂件包含了⼤部分GLTool中类似C语⾔的独⽴函数
#include "GLTools.h"
//矩阵的⼯具类.可以利于GLMatrixStack 加载单元矩阵/矩阵相乘/压栈/出栈/缩放/平移/旋转
#include "GLMatrixStack.h"
//矩阵⼯具类,表示位置.通过设置vOrigin, vForward ,vUp
#include "GLFrame.h"
//矩阵⼯具类,⽤来快速设置正/透视投影矩阵.完成坐标从3D->2D映射过程.
#include "GLFrustum.h"
//三⻆形批次类,帮助类,利⽤它可以传输顶点/光照/纹理/颜⾊数据到存储着⾊器中.
#include "GLBatch.h"
//变换管道类,⽤来快速在代码中传输视图矩阵/投影矩阵/视图投影变换矩阵等.
#include "GLGeometryTransform.h"
//数学库
#include <math.h>

//在Mac 系统下,`#include<glut/glut.h>` 在Windows 和 Linux上,我们使⽤freeglut的静态库版本并且需要添加⼀个宏
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif


//设置观察者视图坐标
GLFrame viewFrame;
//设置图元绘制时的投影⽅式.
GLFrustum viewFrustum;
//帮助类/容器类
GLTriangleBatch torusBatch;
//模型视图矩阵
GLMatrixStack modelViewMatrix;
//投影矩阵
GLMatrixStack projectionMatrix;
//变换管道,存储投影/视图/投影视图变换矩阵
GLGeometryTransform transformPipeline;
//存储着⾊器管理⼯具类.
GLShaderManager shaderManager;

//标记:背⾯剔除、深度测试
int iCull = 0;
int iDepth = 0;





//ChangeSize 函数:⾃定义函数.通过glutReshaperFunc(函数名)注册为重塑函数.当屏幕⼤⼩发⽣变化/或者第⼀次创建窗⼝时,会调⽤该函数调整窗⼝⼤⼩/视⼝⼤⼩.
void ChangeSize(int w ,int h)
{
    //设置视口窗口尺寸
    glViewport(0, 0, w, h);
    //创建投影矩阵,并将它载⼊投影矩阵堆栈中
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 500.0f);
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    // 初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}

//RenderScene 函数:⾃定义函数.通过glutDisplayFunc(函数名)注册为显示渲染函数.当屏幕发⽣变化/或者开发者主动渲染会调⽤此函数,⽤来实现数据->渲染过程
void RenderScene(void)
{
    //清理缓存区(颜⾊,深度,模板缓存区等)
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
    
    //根据设置iCull标记来判断是否开启背⾯剔除
    if (iCull)
    {
        glEnable(GL_CULL_FACE);
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }
    else
    {
        glDisable(GL_CULL_FACE);
    }
    
    //根据设置iDepth标记来判断是否开启深度测试
    if (iDepth)
    {
        glEnable(GL_DEPTH_TEST);
    }
    else
    {
        glDisable(GL_DEPTH_TEST);
    }
    
    //把摄像机矩阵压⼊模型矩阵中
    modelViewMatrix.PushMatrix(viewFrame);
   
    GLfloat vRed[] = {1.0f,0.0f,0.0f,1.0f};


     //使用默认光源着色器
      //通过光源、阴影效果跟提现立体效果
      //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
      //参数2:模型视图矩阵
      //参数3:投影矩阵
      //参数4:基本颜色值
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,transformPipeline.GetModelViewMatrix(),transformPipeline.GetProjectionMatrix(),vRed);
    
    //绘制
    torusBatch.Draw();
    
    //绘制完毕则还原矩阵
    modelViewMatrix.PopMatrix();
    
    //交换缓存区
    glutSwapBuffers();
}


//SetupRC 函数: ⾃定义函数,设置你需要渲染的图形的相关顶点数据/颜⾊数据等数据装备⼯作
void SetupRC()
{
    //背景颜⾊
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    //存储着⾊器管理器初始化
    shaderManager.InitializeStockShaders();
    //将相机向后移动7个单元:⾁眼到物体之间的距离
    viewFrame.MoveForward(7.0f);
    
    //创建⼀个甜甜圈
    //void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
    //参数1:GLTriangleBatch 容器帮助类
    //参数2:外边缘半径
    //参数3:内边缘半径
    //参数4、5:主半径和从半径的细分单元数量
    gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
    //点的⼤⼩
    glPointSize(4.0f);
    
}


//键位设置,通过不同的键位对其进⾏设置
//控制Camera的移动,从⽽改变视⼝
void SpecialKeys(int key, int x, int y)
{
    if (key == GLUT_KEY_UP)
    {
        viewFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
    }
    
    if (key == GLUT_KEY_DOWN)
    {
        viewFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
    }
    
    if (key == GLUT_KEY_LEFT)
    {
        viewFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
    }
    
    if (key == GLUT_KEY_RIGHT)
    {
        viewFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
    }
    
    glutPostRedisplay();
}


//右键菜单栏选项
void ProcessMenu(int value)
{
    switch (value)
    {
        case 1:
            iDepth = !iDepth;
            break;
            
        case 2:
            iCull = !iCull;
            break;
            
        case 3:
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            break;
            
        case 4:
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            break;
        
        case 5:
            glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
            break;
    }
    
    glutPostRedisplay();
}

//main 函数: 程序⼊⼝.OpenGL 是⾯向过程编程.所以你会发现利⽤OpenGL处理图形/图像都是链式形式.以及基于OpenGL封装的图像处理框架也是链式编程
int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    //申请一个颜色缓存区、深度缓存区、双缓存区、模板缓存区
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
    //设置窗口的尺寸
    glutInitWindowSize(800, 800);
    //设置窗口的名称
    glutCreateWindow("OpenGL 渲染技巧");
    //注册回调函数(改变尺寸)
    glutReshapeFunc(ChangeSize);
    //注册显示函数
    glutDisplayFunc(RenderScene);
    //特殊键位函数(上下左右)
    glutSpecialFunc(SpecialKeys);
    //右击菜单
    glutCreateMenu(ProcessMenu);
    glutAddMenuEntry("开启深度测试",1);
    glutAddMenuEntry("开启背面剔除",2);
    glutAddMenuEntry("设置面模式", 3);
    glutAddMenuEntry("设置线模式", 4);
    glutAddMenuEntry("设置点模式", 5);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    
    
    
    //判断一下是否能初始化glew库,确保项目能正常使用OpenGL 框架
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return 1;
    }
    
    //设置绘制数据
    SetupRC();
    
    //runloop运行循环
    glutMainLoop();
    
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 上一篇我们绘制的一些基础图元样式,效果还是很粗糙的。代码也比较简单,今天我们来看看OpenGL的渲染技巧,通过绘制...
    红发_KVO阅读 799评论 0 0
  • 在本篇文章开始之前呢。我们需要先了解一个非常重要的概念:环绕。环绕是OpenGL中任何三角形都需要遵循的重要特性。...
    Mr_Lxh阅读 655评论 0 0
  • 1、渲染过程产⽣的问题 在绘制3D场景的时候,我们需要决定哪些部分是对观察者 可见的,或者哪些部分是对观察者不可⻅...
    希尔罗斯沃德_董阅读 473评论 0 0
  • 1、隐藏面消除 在渲染3D场景过程中可能会产生以下问题 我们需要决定哪些部分是对观察者可⻅的,或者哪些部分是对观察...
    小溜子阅读 287评论 0 1
  • OpenGL渲染技巧 了解了OpenGL的渲染流程和常用API后,就可以简单的绘制出图形了。但是在绘制中可能会碰到...
    silasjs阅读 313评论 0 4