渲染基本流程

  1. 基本流程
  2. GPU渲染流程
  3. GPU渲染过程中的空间变换过程
  4. Draw Call
  5. 渲染优化

基本流程

  • 开发准备流程
    • 1:搭建场景,添加摄像机,添加光源,设置摄像机,设置光源.
    • 2:剔除无需渲染的物体,包括不限于 2D 物体,3D 物体
    • 3:设置物体的渲染状态,包括不限于设置材质,纹理,shader 等
  • 电脑 CPU 运行流程
    • 1:准备数据,上面开发准备流程最后会在CPU中生成几何信息,一般叫做”渲染图元”,几何信息就是 点,线,面 的数据.会将这些数据加载到系统内存(Random Access Memory,RAM).在 Unity3d 对应的类是 Mesh
    • 2:准备数据,将设置的渲染状态也读进内存中.渲染状态定义了场景内的网格是怎样被渲染的.比如:设置顶点着色器/片元着色器,光源,材质,纹理等,动态修改渲染状态等.在 Unity3d 对应的类是 MeshRenderer
    • 3:CPU 向 GPU 发送 DrawCall 命令,开始渲染.这个命令指向一个等待被渲染的图元列表(就是点线面数据,不包含渲染状态).GPU 会拿到渲染状态和图元列表进行渲染成像.在 Unity3d 里面被隐藏了细节,无法看到
    • 其他知识:一般情况下,DrawCall命令,不应该频繁,unity 官方设定固定时间内 250 个以下.DrawCall命令指向的图元列表不应该过多,这个也是应该注意的.
  • 电脑GPU运行流程
    • –>A:GPU 将对这些几何数据进行处理,会输出下一个阶段需要的信息.比如会将顶点坐标转换到屏幕空间中,输出一些二维顶点坐标,每个顶点对应的深度值,着色等信息.一般叫做”几何阶段”,开启几何阶段
      • –>1:顶点着色器.(由顶点数据传入,传出顶点数据,这些数据可能被增加或者被删减).
      • –>2:曲面细分着色器
      • –>3:几何着色器
      • –>4:裁剪
      • –>5:屏幕映射
    • –>B:将上一阶段生成的数据,用来生产屏幕上的像素,渲染出最终的图像.一般叫做”光栅化阶段”,开启光栅化阶段
      • –>1:三角形设置
      • –>2:三角形遍历
      • –>3:片元着色器
      • –>4:逐片元操作
    • –>C:生成最终的屏幕2D图像
    • 其他知识:一般情况下,网格和纹理等数据会被加载到显卡上的存储空间,显存(Video Random Access Memory,VRAM),这是由于在显存上面读取数据比在内存读取数据渲染更快.

GPU渲染流程

  • 顶点数据–>顶点着色器–>曲面细分着色器–>几何着色器–>裁剪–>屏幕映射–>三角形设置–>三角形遍历–>片元着色器–>逐片元操作–>屏幕2D图像,屏幕 2d 图像也是可以表示三维立体感受的(主要靠脑补).
    • A:开启几何阶段
    • 1:顶点着色器(Vertex Shader)是完全可编程的,通常用于实现顶点的控件变换,顶点着色,坐标变换,逐顶点光照,后续阶段的数据等功能.输入进来的每个顶点都会调用一次顶点着色器,不可以创建或者销毁顶点,无法得到顶点与顶点之间的关系.(无法得知2 个顶点是否属于同一个三角网络),这样情况下,GPU 可以并行处理很多个顶点.必须完成的工作
    • 把顶点坐标从模型空间(本身空间)转换到齐次裁剪空间*
      就是将一个三维坐标系中的点,变为一个立体正方形中的一个点,由硬件做透视除法之后,得到最终的归一化设备坐标(Normalized Device Coordinates,NDC)
        o.pos = mul(UNITY_MVP,v.position);
        UnityObjectToClipPos(v.vertex);
    • 2:曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元.
    • 3:几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元的着色操作,或者被用于产生更多的图元.
    • 4:裁剪(Clipping),这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片.这个阶段可以配置,不可以编程,比如:可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面.
      一个图元和摄像机视野的关系有 3 种,完全在视野内,部分在视野内,完全在视野外.部分在视野内的物体需要被裁剪.即在齐次裁剪空间里面,所有的顶点都被放入这个立方体内了,不在立方体内的映射,直接被裁剪掉,不会被渲染.
    • 5:屏幕映射(Screen Mapping),不可以编程和配置,负责把每个图元的坐标转换到屏幕坐标系中.作用是从齐次裁剪的立方体内将三维坐标系x,y,z –> 变成二维坐标的 x,y.
      这个地方需要理解屏幕坐标系(Screen Coordinates),这个是纯二维坐标系,对应的是立方体内的 x,y,那么 z 值跑哪里了?
      屏幕坐标系和 z 坐标构成了窗口坐标系(Window Coordinates),z值参与了GPU的光栅化阶段.窗口里面的内容可以让人眼有明显的三维立体感受,而屏幕坐标系没有三维立体感受.z值参与了表示在三维立体感受中的物体距离屏幕坐标系有多远,也就是一个点距离一个面有多远,这个需要想象力
    • B:开启光栅化阶段,接收上个阶段输出的数据(例如屏幕坐标系下的顶点位置,深度值(z坐标),法线方向,视角方向等数据),概览:计算每个图元覆盖了屏幕二维坐标系的哪些像素,以及为这些像素计算它们的颜色
    • 6:三角形设置(Triangle Setup):固定函数,不可编程与配置.上个阶段我们得到的都是三角网格的顶点,即我们得到的是三角形的每条边的 2 个端点,根据这些数据计算三角形每条边的像素坐标,从而知道三角网格对像素的覆盖情况.
    • 7:三角形遍历(Triangle Traversal):固定函数,不可编程与配置.检查每个像素是否被一个三角网格所覆盖,如果这个像素被覆盖,就生成一个片元(fragment)对象.找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion).
      寻找过程中,会对整个覆盖区域的像素进行插值计算,得出深度值,最终输出一个片元对象.
      片元对象不是像素,是数据的集合,包括屏幕坐标,深度信息,顶点信息,法线,颜色,纹理坐标uv等等.
    • 8:片元着色器(Fragment Shader),完全可配置与编程.用于实现逐片元的着色操作.在DirectX中,被称为像素着色器(Pixel Shader).
      得到的一些数据,是根据从顶点着色器中输出的数据插值得到的,最终输出是一个或者多个颜色值.
      程序开发会修改插值得到的颜色,将顶点着色器输出的纹理坐标,对应片元的纹理坐标,进行颜色的修改等.
    • 9:逐片元操作(Per-Fragment Operations),可配置,负责修改颜色,深度缓冲,混合等等.在DirectX中,被称为输出合并阶段(Output-Merger)).
      主要工作是:
      1):决定每个片元的可见性,深度测试(Depth Test),模板测试(Stencil Test),如果不通过测试,则放弃这个片元,测试的含义类似于闯关.这个地方的测试尤其重要.
      模板测试–>拿到颜色缓冲区的参考值–>对比片元的参考值–>舍弃或者写入颜色缓冲区
      深度测试–>拿到颜色缓冲区的深度值–>对比片元的深度值–>舍弃或者进入下一步–>是否开启深度写入–>舍弃或者写入颜色缓冲区
      2):当你闯关成功,就会把片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合.
      3)混合之后的颜色,写入颜色缓冲区中,当你进行渲染的时候,颜色缓冲区中已经有数据了,这次的颜色数据是覆盖颜色缓冲区的数据,还是进行合并融合,就是这一步需要做的事情.
      4)为了避免我们看到正在进行光栅化的图元,GPU 使用了双重缓冲,前置缓冲区和后置缓冲区来回进行交换,因此保证了我们看到的图像总是连续的
  • 注意上面讲到的顺序,在不同平台可能排列不一致,这是因为图像编程的实现不太相同,也可能是GPU 做了很多优化.

GPU渲染过程中的空间变换过程

  • 1:模型空间(model space),对象空间(object space)或者局部空间(local space),使用的左手坐标系,都表示当前你这个物体在自身坐标系下的空间.
  • 2:世界空间(world space)就是 global 空间,使用的左手坐标系,它建立了我们想象中的无限大空间,但是在程序中,是有限的,有界的.这个空间在我们正常交流中,可以指一个办公室,一座大楼,一座农场等等,它们才是玩家所能达到最大的场景,这叫做世界空间,也就是说一个办公室,一座大楼,一座农场就是世界空间.要理解这个,真的靠想象力.
    世界空间的原点,就是游戏空间的中心点.我们可以把 unity 想象成一个树状图结构,有一个虚拟的根节点,根节点下有多个场景(在 Scenes In Build 中有很多可以打包的场景),这些场景又是一个节点,这个场景节点下面又有很多物体,就放在 Hierarchy 下面,可以把GameObject 的父节点认为是当前的这个场景.在Hierarchy 下面第一层的GameObject就是世界空间,第二层以及以后才是模型空间.
  • 第一步变换:将顶点坐标从模型空间变换到世界空间,叫做模型变换*
  • 3:观察空间(view space)也被成为摄像机空间(camera space),模型空间中的特殊空间,使用的右手坐标系,这是一个三维空间.而屏幕空间是一个二维空间,采用的是人的脑补才出现的三维空间.
  • 第二步变换:将顶点坐标从世界空间变换到观察空间,叫做观察变换*
  • 4:裁剪空间(clip space)也被称为齐次裁剪空间,使用的矩阵叫做裁剪矩阵(clip matrix),也叫做投影矩阵(projection matrix),作用是对渲染图元进行裁剪,位于空间内部的图元会被保留,位于外部的被剔除,与这块空间边界相交的图元会被裁剪,这块空间由摄像机的视椎体决定.视椎体有 6 个平面,被称为裁剪平面(clip plans).
  • 第三步变换:将顶点坐标从观察空间变换到裁剪空间,叫做裁剪变换*
  • 5:屏幕空间(screen space)需要经过真正的投影.需要进行标准齐次除法(homogeneous division),在 openGL 中被称为归一化设备坐标(Normalized Device Coordinates,NDC),经过这个步骤之后,裁剪空间变成了立方体空间,之前是梯形空间.通过映射算法,直接映射在屏幕上面.
  • 第四步变换:将顶点坐标从裁剪空间映射到屏幕空间*
  • 6:总结,
    模型空间—>模型变换—>世界空间—>观察变换—>观察空间—>投影变换—>裁剪空间—>屏幕映射—>屏幕空间
    将模型变换,观察变换以及投影变换串联成一个矩阵,即 MVP 矩阵,用于将顶点从模型空间中转换到裁剪空间中.

Draw Call

  • 1:draw call 本身的含义很简单.就是 CPU 调用图像编程接口,命令 GPU 进行渲染的操作.接口命令的例子:OpenGL 中的 glDrawElements 命令或者 DirectX 中的 DrawIndexedPrimitive 命令.
  • 2:我们需要让 CPU 以及 GPU 并行工作,以提高效率,就产生了一个命令缓冲区(Command Buffer).命令缓冲区包含了一个命令队列,由 CPU 向里面添加命令,GPU 从中读取命令,添加和读取的过程是相互独立的.先进先出模式,CPU 生产,GPU 消费.
  • 3:命令缓冲区里面不止draw call一种命令,还有改变渲染状态的命令(更加耗时).
  • 4:如果频繁调用 draw call,CPU就会向 GPU 发送很多内容,包括数据,状态和命令,检查渲染状态等.如果draw call太多,CPU 就会花费大量时间在上面,造成 CPU 过载,影响帧率.
  • 5:优化draw call,就是将相同渲染状态的网格,合并成一个大的网格,提交一次即可.这就是批处理(Batching)的方法.一般情况下静态批处理很好做,动态批处理也会合并,但是因为物体是在不断运动的,所以每帧都会有合批,这对 CPU 是一种压榨性能的操作.避免使用大量很小的网格,非要用,考虑合批.尽量使用相同的材质(同一渲染状态).

渲染优化

  • 影响性能的因素有哪些?

    A:CPU 中过多的 draw call ,复杂的脚本或者物理模拟
    B:GPU 顶点处理:过多的顶点,过多的逐顶点计算.片元处理,过多的片元(即可能是由于分辨率造成的,也可能是由于 overdraw 造成的),过多的逐片元计算.
    C:带宽,也就是 CPU 向 GPU 提交 draw call 命令携带的数据需要一个通道送给 GPU,这条通道就是带宽.CPU 在每次通知 GPU 进行渲染之前,都需要提前准备好顶点数据(如:位置,法线,颜色,纹理坐标等),然后调用一系列 API 把它们放到 GPU 可以访问到的指定位置,最后,调用一个绘制命令(会改变很多渲染状态的设置),来告诉 GPU 进行渲染.调用时会产生一个 draw call ,过多的 draw call 或者 一次 draw call 的数据量过大,都会导致 CPU 将大部分时间都花费在提交 draw call 的工作上面.
    D:填充率, 降低显示分辨率并运行游戏。如果较低的显示分辨率使游戏运行得更快,你可能会受到GPU填充率的限制。

  • 优化手段

    1:CPU 使用(动态,静态)批处理降低 draw call
    2:GPU 让美术同学降低顶点,有时候 GPU 会将一个顶点拆成多个顶点,原因是分离纹理坐标(uv splits)或者产生平滑的边界(smoothing splits),优化建议:尽可能移除不必要的硬边以及纹理衔接,避免边界平滑和纹理分离.减少模型面数,使用 LOD 技术减少顶点数,模型离摄像机很远时,减少模型上面的面数.
    3:GPU 程序方面减少需要处理的顶点数目,优化几何体,使用模型的 LOD(Level of Detail)技术.使用遮挡剔除(Occlusion Culling)技术
    4:减少需要处理的片元数目,在于减少overdraw,就是同一个像素被绘制了多次.控制绘制程序,避免使用半透明队列,时刻警惕透明物体,减少实时光照.
    5:在 shader 中减少计算复杂度,不要使用 分支语句与循环语句,避免使用 sin,tan,pow,log 等数学运算,使用查找表来作为代替,不要使用 discard 操作.使用 Shader 的 LOD(Level of Detail)技术,只有 shader 的 LOD 值小于某个设定的值,这个 shader 才会被使用,而使用了那些超过设定值的 shader 的物体将不会被渲染.
    6:图片设置,减小纹理大小,尽量小于 1024x1024,长宽值最好是 2 的整数幂.
    7:使用 渲染统计窗口 Rendering Statistics Window,性能分析器(Profiler),以及帧调试器(Frame Debugger)

  • 用好工具

    1:渲染统计窗口 Rendering Statistics Window

    Graphic 的右侧显示了 FPS 数目以及毫秒数.毫秒数表示处理和渲染一帧所需的时间.FPS 表示一段时间内的平均值。平均 FPS = 帧数 / 一段时长。帧数可以用每次进入 Update 时加一的变量来统计。一段时长就是进入 Update 时 Time.deltaTime 的累加因为是平均值,所以当时间越长时,这个值才是稳定的。一般情况是在 Update 记录时长与调用次数,用调用次数/记录时长=FPS,如果记录时长=1秒则表示每秒多少帧.每秒 60 帧是非常好的,现在手游基本保持在每秒 30 帧以上的标准.用记录时长 * 1000 表示毫秒数,记录时长 * 1000 / 调用次数,表示多少毫秒每帧.如果记录时长=1秒,则一帧所用时长为 1000 / 调用次数 毫秒.稳定情况下 16.6ms一帧 是非常好的,不需要优化的,如果达到了 33ms每帧 则需要进行优化了.因为 33ms每帧 则一秒才有30帧,显然是需要优化的.(https://www.freesion.com/article/1005726774/)
    batches 一帧中需要进行批处理的数目,官方建议低于 250 个.
    Saved By Batching 合并的批处理数目,这个数字表明了批处理为我们节省了多少 Draw Call
    Tris 和 Verts 表示需要绘制的三角面片(官方建议在手机上低于200K面)与顶点数目(官方建议在手机上低于100K 个顶点)
    Screen 屏幕的大小以及它占用的内存大小
    SetPass 渲染使用的 Pass 的数目,每个 Pass 都需要 Unity 的 runtime 来绑定一个新的 shader ,这可能造成 CPU 的瓶颈
    Visible Skinned Meshes 渲染蒙皮网格的数目
    Animations 播放的动画数目

2: 性能分析器(Profiler)

Rendering 里面展示了很详细的信息.

3:帧调试器(Frame Debugger)

可以得到 unity 是如何渲染当前帧的

  • 批处理

    1:静态批处理:自由度高,限制很少,需要共享同一材质.缺点是占用更多的内存,经过静态批处理的物体都不可以移动了,即时在脚本里面尝试改变物体的位置.如果在静态批处理前一些物体共享了相同的网格,那么再内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发送给 GPU,如果有 1000 个相同网格的模型,就会多出 1000 倍的内存.在内存上面 VBO total 会变大.

2:动态批处理:优点是一切处理都是 unity 自动完成的,不需要我们自己做任何操作,而且物体是可以移动的,但缺点是限制很多,需要相同的渲染状态,CPU 压力大.原理是每一帧把可以进行批处理的模型网格进行合并,再把合并后的模型数据传递给 GPU,然后使用同一个材质对齐渲染,因为是每帧都在合并,所以物体可以移动.要保证相同材质,相同 shader,相同缩放,相同顶点.测试:在一个空场景把所有运动的模型导入,让其运动,然后测试 draw call 是否是一个,Save By Baching 的数目是否大于 0,这种就是动态合批了.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com

Love

Title:渲染基本流程

文章字数:5.1k

Author:诸子百家-谁的天下?

Created At:2020-05-08, 11:41:32

Updated At:2021-03-28, 02:59:27

Url:http://yoursite.com/2020/05/08/Renderer/%E6%B8%B2%E6%9F%93%E5%9F%BA%E6%9C%AC%E6%B5%81%E7%A8%8B/

Copyright: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

爱你,爱世人