JefferyZhou

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

整个渲染管线N3把它分为从Frame,pass, batch 架构。这个架构要从DX8中废弃掉固定管线开始了解。我们知道在固定管线时代,每次渲染都需要设置好各种渲染状态,比如ZEnable, ZFunc, CullMode, 等,现在DX引入了一个Effect文件用来配置渲染管线,如下:

Image(20)

关于特效文件的说明 参看 [DirectX | Dx effect 文件 .fx 与 Shader - 顶点着色和像素着色]

N3的渲染系统,每个View在创建的时候都会制定相应的FrameShader,而这个FrameShaer是一个渲染批次的配置文件。在 export_win32/frame目录下,可以看到如下的一些文件:

Image(21)

默认的是dx9default.xml   一个 FrameShader 中会定义若干的 RenderTarget, 以及若干的FramePass 和 PostEffect。其中FramePass 和 PostEffect 本质是一致的。这里我们就把它先归结到FramePass。

而一个 FramePass 是真对一个 RenderTarget(也可以是MultiRenderTarget)进行操作的,一个FramePass 下可以分为多个 FrameBatch。

其中每一个FrameBatch 就会定义当前的完整渲染状态,以及场景中对象的过滤方式,排序方式,着色配置等。一个FrameBatch可以是对场景的一次完整渲染,渲染结构会放到RenderTarget。

对于FrameShader 存在专门的加载器。后话再提。

这里深入了解一下FrameShader 是这么渲染的,怎么设置好渲染状态,然后进行场景绘制的。

渲染一个 Frame 会对 Frame 中的  framePass 依次 Render,如下代码:

Image(22)

而对应到 一个 framePass 的渲染 具体代码如下,这一这里的framePass并不是指 effect 文件中的一个 pass:

Image(23)

一个 FramePass 的渲染 会 做三件事情:

  • 一个, 应用framePass中定义的 shader变量, 这个 shader变量是属于 framePass 定义的 shader ,如下图所示,一个pass中定义的ShaderVariable是属于 p_prelight。

Image(24)

  • 一个,是应用pass对应的shader。
  • 一个,是调用pass下面的所有batch进行渲染。

这里我们不妨先看一个简单的 Pass,也是N3渲染系统中的第一个FramePass。

Image(25)

这里的渲染对象是GBuffer, p_depth 具体是什么。先贴上代码,

Image(26)

发现它本身设置了渲染管线的状态。而且其中有一个比较重要的设置项。发现是直接把值写入颜色缓冲区,用的Z值比较函数是Greater,但是管线关闭了深度缓存。

然后看下RenderTarget,GBuffer

Image(27)

发现是一个MultiRenderTarget, 定了两个 sub RenderTarget. 

Image(28)

其中一个 是 NormalBuffer,  并且设置颜色缓冲区为(0.5, 0.5, 0.0, 0.0),清空蒙版缓冲区,设置深度缓存为 1.0,也就是最大深度.

Image(29)

其中一个 是 DSFObjectDepthBuffer,简单的清空相应的颜色缓冲区。所以总结起来,它应该是先收集场景的两个缓冲区buffer.其中NormalBuffer 是法线缓冲区,每个像素点都存储了normal.x 和 normal.y,而DSFObjectDepthBuffer是一个深度定制的深度缓冲区,存储了 objectId, NormalGroupId, depth。这里提到了一个名词,深度缓存也叫z buffer。我们在稍后补充一个z buffer的定义:

[补充说明深度缓存 z buffer]


简介

  深度缓存算法(depth—buffer method)是一种常用的判定对象表面可见性的物空间算法,它在投影面上的每一像素位置比较场景中所有面的深度。由于通常沿着观察系统的z轴来计算各对象距观察平面的深度,该算法也称为z缓存(z-buffer)算法。

编辑本段算法描述

  该算法需要两个缓存器,一个是用来存放颜色的颜色缓存器,另一个是用来存放深度的深度缓存器。利用深度缓存器,可以进行可见性的判断,消除隐藏对象。其具体做法是:  首先对深度缓存器和颜色缓存器进行初始化,把深度缓存器中所有单元置成一个最大可能的深度值,把颜色缓存器中各单元置成背景颜色。然后将场景中的物体不分次序地投影到象平面上去。对于每个投影点(象素),把投影物体在该点处的深度与深度缓存器中相应位置上的深度值进行比较,如果前者小于后者,那么就把当前被投影物体的颜色写到颜色缓存器中去,同时用当前投影物体的深度去更新深度缓存器中相应象素的深度,否则不做任何操作。

编辑本段优缺点

  深度缓存算法中物体投影到象平面上的次序是任意的,无须将场景中的表面进行排序,物体之间的遮挡关系是通过深度缓存器进行深度比较加以确定的,算法易于实现。  深度缓存算法只能显示距离视点最近的物体,而且这些物体都是不透明的,无法看到被遮挡的物体。  深度缓存算法经常执行一些最终不起作用的中间计算过程。由于对象按任意次序进行处理,因此有些表面进行了颜色计算但事后又被更近的表面所代替。为了缓减这一问题,有些图形软件提供选项让用户调整表面测试的深度范围。例如,通过深度测试排除较远的对象。使用该选项还可以排除非常靠近投影平面的对象。高档计算机图形系统一般集成了深度缓存算法的硬件实现。


上述虽然看到了对渲染管线的一些状态参数的设置,但还是没有看到N3中具体的是怎么用颜色缓冲区来填充z buffer的 。是的。具体的填充肯定是在某一个 batch里面,再次贴上上述的framepass的定义:

Image(30)

发现这个pass 只有一个批次,这个batch 应用了一个 b_empty 的shader。然后选择激活其中的 NormalDepth technique,然后正常的渲染模式type="Solid",渲染场景中的所有 nodeFilter="Solid"节点。

看下b_empty, 不过故名思议,这个应该是个空的 shader。

Image(31)

是的。你没有看错,他是空的。那么当前Batch中指向的 shdFeatures="NormalDepth" 到底会激活哪个shader 的相应 technique呢。答案是:当前激活的shader 的相应 technique。下面我们将从Frame系统中寻找这个当前激活的shader到底来自哪里。贴上frameBatch的渲染代码:

Image(32)

深入 RenderBatch() 

Image(33)

是的上面的modeNode->ApplySharedState(frameIndex) 会设置当前激活的shader,具体是通过

Image(34)

所以我们需要去mode中寻找shader来激活 batch中设置的shdFeatures.  N3标准模型设置的shader 是 static.fx。下面我们看下static.fx 中的 NormalDepth technique。

N3 做了一个宏,用来声明一个 technique: 

Image(35)

而static.fx 中关于depth的定义如下:

Image(36)

其中用到的顶点shader如下:

Image(37)

看到,做了一些工作,计算一个顶点坐标变换到投影空间,然后提取UV坐标到oUv0, 最后把顶点相关的三个法线向量(法线,切线和副法线)变换到视图空间,同时还会把坐标变换到投影空间,接下来是像素着色代码了

其中用到的像素shader如下:

Image(38)

像素着色,先在组合出了像素点的法线空间变换矩阵,法线空间与视图空间的变换矩阵。然后用uv坐标在法线贴图进行采样得到像素点对应的法线向量,然后我们把法线向量从法线空间变换到视图空间,同时作为第一个rendertarget的颜色值保存到第一个renderTarget中,然后我们第二个rendertarget的颜色值,第二个rendertarget将会保存一个objeId, 一个顶点坐标在视图空间中的w值,最后是顶点在视图空间的z值【视图空间向量的长度,也就是点到视点的距离,当然也就是深度值】。至此一个完整的渲染完成了。我们填充了两个rendertarget。

上述的描述中,有提到一些概念:法线空间,法线贴图(normalMap), 法线,切线,副法线。这些概念应该算是相对熟悉的内容。这里也会对概念做一个回顾。

[补充说明:法线空间,凹凸贴图,法线贴图]

1. 顶点法线,切线,和副法线

如何根据模型的顶点位置坐标和纹理坐标计算顶点的法线、切线和副法线?
我们把顶点数据记作P(x,y,z,u,v),(x,y,z)是位置坐标,(u,v)纹理坐标
三角形的3个顶点就可以表示成
P0(x0,y0,z0,u0,v0)
P1(x1,y1,z1,u1,v2)
P2(x2,y2,z2,u2,v1)
因为u,v的变化对x的影响是线性的,则有
x = C1 u + C2 v + C3
不妨整理一下,写成
A0 x + B0 u + C0 v + D0 = 0 (1)
同理u,v的变化对y,z的影响是线性的,有
A1 y + B1 u + C1 v + D1 = 0 (2)
A2 z + B2 u + C2 v + D2 = 0 (3)
可以看到 x,u,v 是成平面的,而A0,B0,C0就是平面的法线,可以通过三角形的3个顶点求得
(A0,B0,C0) = ((x0,u0,v0)-(x1,u1,v1))×((x0,u0,v0)-(x2,u2,v2))
D0 = -(A0,B0,C0)·(x0,s0,t0)
同理也可以求得(A1,B1,C1,D1),(A2,B2,C2,D2)
通过(1),(2),(3)式联立可以求得
d(x,y,z)/du = (-B0/A0,-B1/A1,-B2/A2)
d(x,y,z)/dv = (-C0/A0,-C1/A1,-C2/A2)
我们就可以取d(x,y,z)/du为切线T,d(x,y,z)/dv为副法线B,法线N = T×B

{The.Cg.Tutorial.The.Definitive.Guide.to.Programmable.Real-Time.Graphics 8.4.1 Examining a Single Triangle }

2. 法线,切线和副法线构成了所谓的切线空间(tangnet space),法线纹理(Normal map)中存储的法线值就是在切线空间内的。

这里提到的normal map ,就会提到另外一个概念凹凸贴图。 注意凹凸贴图和法线贴图的区别,虽然他们呢之间由千丝万缕的联系,但他们不是同一个东西。


Bump Map(凹凸贴图)。Bump Mapping是Blin大师在1978年提出的图形学算法,目的是以低代价给予计算机几何体以更丰富的表面信息(高模盖低模)。

30年来,这项技术不断延展,尤其是计算机图形学成熟以后,相继出现了不少算法变体,90年代末的Normal Map解放了必须自行计算纹理像素法线的痛苦,

新世纪以来相继又出现了Parallax Mapping, Relief Mapping等技术。开始Bump Map须要我们计算纹理图上每个像素的法线信息,简单的还可能做到,对复杂的纹理要搞清面光背光份量简直要命,

于是就用Height Map,在一张高度图上记录每个像素对应的纹理位置的高度信息(这个比较容易办到,NEHE22也是这类)。

看上去就是一张地形网格——这样的话,计算每个像素点的法线就不那么难了。XY方向相邻像素的高度相减就是两条正交的切向量,叉乘外加左/右手定则就获得法线。

所以可以这样理解,凹凸贴图就是模型顶点的坐标值压缩到255然后存储到一张贴图里面。这张贴图就代表了物体表面的高低起伏,粗糙程度。对于BumpMap的一个进阶就是NormalMap。


NormalMap(法线贴图)。NormalMap的思想就是把BumpMap中高度值换成法线向量,高度图也是更具顶点坐标和顶点的UV计算出来,

这里就直接从顶点坐标和UV计算出法线然后存到贴图中。也有很多算法是根据Bump Map计算法线图的。

每个像素的RGB分别存储该像素对应法线的XYZ分量,只要把法线的分量由(-1,1)映射成(0,255)就可了。

观察一张法线图,以蓝色为主,是因为朝向图面外的法线(0,0,1)都被编码成(0,0,127)了(读入OpenGL后即(0,0,0.5)),

而图上越红的地方表明法线越向右,越绿的地方表明法线越向上,就可以理解了。总体来说,就是一张紫蓝色的图。

每个像素根据高度图生成的三轴坐标系,就是被称为切线空间坐标系的东西,这样NormalMap中的每个法线都存在每个像素人手一个。

可见Normal Map里面每个像素的法线就是定义在这个切线空间的。

FramePass中渲染状态的设置

从结构上来说,一次渲染,就是设置好渲染状态,然后提交渲染数据(顶点,材质等),然后交给硬件,进行渲染,把渲染结果放到设置好的renderTarget中。对于数据的组织和提交,在前面都已经提到了。这里我们主要关注一次完整的渲染过程,是在哪里设置好渲染状态的。有哪些地方设置渲染状态。设置渲染状态也就是设置effect文件,对应到 N3中也就是设置Shader。

下面再一次跟踪一个FramePass的渲染流程,然后在这个流程中,明确的指出渲染状态的设置点。

在前面的配置解析中,我们知道 一个 FramePass 都会配置一个 shader , 而一个 FramePass会包含 多个 FrameBatch ,每一个FrameBatch 也会配置一个 shader, 同时还会配置一个 shdFeature,然后具体的渲染对象,是ModeNodeInstace,而我们知道,有一个 叫 StateNodeInstance 以及他的子类,都是可以设置一个 shader的。

所以在这个渲染流程中总共有 四个地方与渲染状态严密相关,

开始设置 FramePass 的 shader :

Image(39)

注意这里设置渲染状态为 : Image(40)

也就是说,我们的framebatch 应该是一个只有一个可用的technique 并且里面值有 pass 0, 而且可以猜测到这里应该是不会设置 着色入口的。

然后接着是 FrameBatch  的 shader :

Image(41)

注意这里设置渲染状态为:Image(42)

设置完 Frame 和 Batch 的 shader后,需要设置 当前渲染对象的 shader。 也就是批次所对应的shader。

batch 总共分为如下几类:

Image(43) 其中红线标注的是 pc 平台上有的batch类型。

这里重点关注两个 批次类型 Solid, 和 Lights。 先看 Solid。

对于solid 来说,他会渲染场景中所有可见的模型对象。 

Image(44)

Image(45)

Image(46)

通过上述的遍历,这样就获取了场景中当前所有可见的对象,其中ModelNode是可见对象的基本元件,他负责可见对象渲染的各种数据和状态。所以在渲染一个ModelNode对应的所有实例的时候:

Image(47)这样就激活了ModeNode本身的材质。

然后结合batch中设置的shdFeature。我们激活当前可选的Feature作为最终的状态。这里大体会更具lighting的设置模式分为两种,这里值列举一个简单的:

Image(48)

这样整个渲染管线的渲染状态设置就算完毕了。

从前面列举的各种shader,你可能已经有一种分类了。是的。 FramePass , FrameBatch, ModelNode 他们都有 shader,也就是说他们都设置渲染管线,但是他们的各自分工不一样。

FramePass 设置的是类似如下的设定:

Image(49)       其中非常明显的是,他不会涉及到着色,N3把framePass 也加了一个前缀"p_"。

然后看下 FrameBatch的 设定:

Image(50)         发现他也不会涉及到 着色。

然后看下 ModelNode的:

Image(51)

是的,这定义了很多的 feature,而且定义了顶点着色和像素着色的入口函数。对于渲染设定的分工,可能有了基本了解。

总结一下:


通过定义不同的FramePass, 然后再 FramePass中定义不同的FrameBatch, 就可以完成渲染工作。每一个FramePass 都会陪着一个渲染状态的设置的shader。FramePass 中的所有FrameBatch 都共享这份设置,同时他们还共享FramePass中生命的RenderTarget。而具体的着色代码由FrameBatch中定义的渲染类容(如果是Solid, 就由具体模型的的材质决定,如果是灯光,就由lightsource.fx决定, 如果是UI。。。)和FrameBatch生命的shdFeature共同决定。

posted on 2012-09-24 16:47  JefferyZhou  阅读(518)  评论(0编辑  收藏  举报