引言:

GameByro作为一款次世代引擎,使用了复杂的材质系统,用来满足各种各样的需求。材质代表了物体受到光照后所呈现出的质感,而这种质感在计算机图形学中需要着色代码来完成,所以当前流行的图形引擎设计是使用被渲染对象的材质与shader相关联,GameByro也不例外。GameByro的材质系统可以通过shade tree生成shader程序,增强了应用程序层对可编程渲染管线的控制能力。

渲染架构概览:

在GameByro中,对象表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等可视属性与传统的材质系统分离,独立的成为了对象的渲染属性(NiProperty),而材质(NiMaterial)仅用来对着色程序的封装,这样就实现了渲染数据和渲染方法的分离,降低了耦合性。如上所说的这些可视属性在Gamebyro中会封装成一个属性对象,在应用程序中如果对对象挂载这个属性对象,在GPU程序中就可以访问这个属性对象的值。渲染属性对象可以在创建时指定其类型,如纹理、浮点、矩阵、向量或数组,此外一些全局性的对象也可以通过在Shader中用语意声明为全局object对象,如灯光和摄影机等,这样就可以以同样的方式来访问这些对象上的属性。

GameByro每一帧的渲染(NiRenderFrame)划分为多个步骤(NiRenderStep),每个步骤又包含很多个批(NiRenderClick),NiRenderFrame封装了上层对渲染系统调用的接口,而NiRenderClick则代表了图形硬件的一次绘制操作(对渲染队列中所有的对象的顶点缓存调用DrawPrimitive),当应用程序调用NiRenderFrame的Display接口时, NiRenderFrame会依次调用每一个NiRenderStep的Render()接口,NiRenderStep就会执行所有的NiRenderClick操作。

对于每个NiRenderClick来说,首先要设置视口和渲染目标,也就是渲染数据流的入口和出口。视口建立以后就可以通过关联的摄影机对场景图中的对象进行裁剪(默认的有视口裁剪和遮挡裁剪,此外还可以通过回调函数加入自己的裁剪方式),将未被裁剪的对象放入渲染队列。然后Gambyro会根据材质来对渲染队列中的对象进行排序,让材质相同的对象处于相邻位置,这样可以减少切换shader的开销。

如图所示为帧渲染系统的结构图(简化版)

材质系统:

GameByro中的材质代表渲染对象所采用的方法。前面说过。纹理属性包含了着色所需的原料,那么材质就指定了对这些原料的加工方法。基于当前可编程渲染管线设计,材质就成为连接对象与GPU程序的中间层,应用程序可以通过材质将shader应用于几何体。

NiMaterial类是所有材质的基类,这个类通过一个Map来保存当前应用程序中所有NiMaterial的指针,当然这个Map是静态也就是说相当于全局变量,通过static NiMaterial* NiMaterial:: GetMaterial(const NiFixedString& kName)接口对这个全局的Map进行访问。也就是说,当前环境中所有的NiMaterial对象是通过NiMaterial类来管理的。此外NiMaterial类还通过静态成员变量保存了一个工作路径(即shader文件路径),通过这个路径加载shader文件。NiMaterial就像是一个中介,全权代理对对象的渲染工作。用户可以通过重载NiMaterial来实现自己的渲染机制。每个NiMaterial都是全局性的,可以作用于多个甚至是所有的渲染对象,但一个渲染对象也可以拥有多个NiMaterial,但只能有一个处于激活状态的NiMaterial。

NiMaterial的派生类NiFragmentMaterial提供了对可编程渲染管线完整的控制机制,内部保存了NiShader的哈希表、一个NiGPUProgramCache数组。并且NiFragmentMaterial会生成一个用来编译GPU程序的shade tree(后面会有解释)。这样的话,每个NiFragmentMaterial可以对应多个shader程序,这样就提供了一种机制,在运行时根据不同的运行环境和渲染对象不同的状态,来选择合适的shader程序。在NiRenderClick依次渲染可见集中的每个对象时,首先会判断其是否需要被渲染的标记(flag),如果需要被渲染,则使用NiMaterial::IsShaderCurrent接口判断当前shader(上一次渲染所使用的shader)是否有效,所谓有效就是仍然存在并且可以应用于本次的渲染对象,如果无效,则会调用NiMaterial::GetCurrentShader获得shader,用于本次渲染。NiMaterial::GetCurrentShader会根据渲染对象的属性和当前环境硬件条件来选择合适的shader程序。当然,可以通过重载IsShaderCurrent和GetCurrentShader接口来指定自己的有效性判断规则和如何选择shader程序的方案。 NiFragmentMaterial提供了一套搭建shade tree的框架,用户可以通过重载来搭建自己的shade tree,当然,如果不想通过shade tree的形式生成shader程序也可以,使用NiSingleShaderMaterial可以从文件生成shader程序。

Shade Tree

什么是shade tree呢?我们通常编写的shader代码是线性执行的,即每个pass流程执行的是文本上定义好的shader流程,每一段shader功能模块是按一定顺序依次执行的。如果需要修改流程中的某一部分就需要更改相关的shader代码并重新编译。而shade tree将shader代码以树形结构组织起来,每一个shader代码块(一般是一个函数)都会被编译成一个节点,通过定义输入变量和输出变量来提供数据流的入口和出口,这些节点的插入和删除可以通过应用程序来控制,从而灵活的控制整个渲染过程。这样shader程序中的一些核心模块可以由美术通过工具生成,然后插入到shade tree中,只要输入和输出的接口不变,就无须修改其他代码,从而降低了美术开发shader的门槛。

GameByro通过以下几个类搭建shade tree:

l NiMaterialConfigurator:shade tree被封装在这个对象中,Uniform constants被封装在NiMaterialResource中,而NiMaterialNode封装了相关的shader代码,所有的资源和节点通过NiMaterialResourceBinding连接起来。当所有的连接都确立以后,NiMaterialConfigurator会调用Evaluate接口生成GPU程序和一个输入Uniform资源的集合。

l NiMaterialFragmentNodes:这个类包含了一个shader代码片段的集合,这些代码片段为不同的平台和编程语言所编写。这就为shader程序员提供了更大的灵活性,用以控制他们的代码在不同平台和图形硬件上的表现。例如:在高端平台可以采用高级的shader model提供更好的效果,而在低端平台上可以关闭一些特效来加快速度。

l NiMaterialNodeLibraries:这个类是一个NiMaterialNode的集合,其实也就是一个shader库。它允许shade tree节点完全基于数据驱动。shader库的生成可以通过两种方式,解析XML文件或用XML文件生成C++代码。GameByro提供了相关的解析器和代码生成器。

l NiMaterialResources:shade tree中的Uniform constants,支持多种数据类型,包括Constant、Predefined、Attribute、Global、Object。

固定管线的渲染:

GameByro支持固定管线的渲染,其纹理混合过程如下。

固定管线的着色处理流程

上图很清楚的显示出了每个stage的操作,平行的表示两张纹理的采样是同时进行的,特定情况下右边的纹理可能被忽略。

大部分情况下,应用程序不会使用上面所有的stage,开启或者关闭那个stage可以由应用程序来指定。

以下为多重采样的原理图:

固定管线的纹理多重采样

缺省的着色处理流程:

GameByro提供了一个默认的着色处理流程,封装在NiMaterial的派生类NiStandardMaterial中,这个类执行类似于固定管线的流程,在不同阶段将纹理采样、并将采样到的数据混合到最终的结果中去。

GameByro默认的材质系统的特性如下:

  • Skinned and unskinned transformations. Skinned transformations can support up to 30 bones per draw call.
  • Vertex colors
  • Base maps
  • Normal maps
  • Parallax maps
  • Dark maps
  • Detail maps
  • Bump environment maps
  • Gloss maps
  • Glow maps
  • Decal maps (up to 3)
  • Cubic and spherical environment maps
  • Point/Spot/Directional/Ambient lights contributing to the diffuse, specular, and ambient color. Up to 8 total lights. Per-pixel or per-vertex.
  • Projected light maps. Clipped or unclipped. (Up to 3)
  • Projected shadow maps. Clipped or unclipped. (Up to 3)
  • Texture transforms per map.
  • Per-vertex fog

下图显示为不同的纹理、灯光、材质属性的组合过程,不过需要注意的是,视差贴图和凹凸贴图属于特殊的情况,它们仅仅影响到纹理采样的UV坐标,而并非直接对最后的颜色值产生贡献。视差贴图会改变所有贴图采样的UV坐标,而凹凸贴图仅对环境贴图的UV产生影响。

以上流程完全由shade tree构建,NiStandardMaterial提供了大量的函数接口用于对每个流程的控制,用户可以通过重载相关的接口,插入自己的shade tree节点,修改每一步的操作或处理过程。例如:

virtual bool HandleBaseMap(Context& kContext, NiMaterialResource* pkUVSet,

NiMaterialResource*& pkDiffuseColorAccum,

NiMaterialResource*& pkOpacity, bool bOpacityOnly);

当然,整个流程的顺序和结构修改起来比较困难,如果有需要可以定制自己的材质系统,搭建自己的shade tree。

NiStandardMaterial提供了若干回调函数,这些函数可以动态的修改流程,分割PASS,对shader运行失败进行容错。

l SplitPerPixelLights/SplitPerVertexLights:这两个函数分别作用于逐顶点光照和逐像素光 照,当物体所受的光源数量太多,超过了顶点或像素着色器的能力时,通过这些函数可以将失败的pass分割成两个,如果分割出的pass仍然不能执行,那么函数会被递归调用,直到每个pass只有一个光源为止。

l SplitTextureMaps:这个函数会把对纹理采样的pass进行分割,当纹理查询过多时,顶点或像素着色器就会过于复杂,这时就可能导致shader运行失败。此函数只能迭代一次,生成一个额外的pass。

l DropParallaxMap:这个函数用来从几何体上移除视差贴图,且不产生额外的pass。

l DropParallaxMapThenSplitLights:这个函数首先调用DropParallaxMap移除视差贴图,然后一直调用SplitPerPixelLights直到失败为止。

NiMaterialInstance:

NiMaterial并不是直接与NiRenderObject相关联,而是经过了NiMaterialIstance这个中间层,由它来代理将NiMaterial关联到几何体,它负责调用NiMaterial为NiRenderObject生成NiShader,通过改变NiMaterialIstance上的接口SetMaterialNeedsUpdate,可以决定每一帧NiRenderObject所使用的材质是否需要被更换,通过接口SetDefaultMaterialNeedsUpdateFlag,可以决定当前材质所需的数据(即当前渲染流程所需的数据)是否要被更新。这样每个NiMaterialIstance只能被一个NiRenderObject所拥有,而多个NiMaterialIstance可以共享一个NiMaterial,这样就减少了重复创建NiMaterial的时间和空间上的开销,同时降低了渲染对象个材质之间的耦合度。

如下为材质系统的类结构简化图:

渲染属性:

前面提到过,GameByro将渲染所需要加工的数据全部封装在了NiProperty中,只要在shader中用指定的语法进行声明,就可以访问这些属性的值。

目前引擎中已经定义了12种属性,均派生自NiProperty,分别代表渲染数据的12种不同类型:

l NiAlphaProperty

l NiDitherProperty

l NiFogProperty

l NiMaterialProperty

l NiRendererSpecificProperty

l NiShadeProperty

l NiSpecularProperty

l NiStencilProperty

l NiTexturingProperty

l NiVertexColorProperty

l NiWireframeProperty

l NiZBufferProperty

用户也可以自定义属性类型,但所对应的数据类型要被shader语言所支持。

光照与阴影:

光照与阴影密不可分,因为阴影就是由光照产生的,前面在材质系统中已经提到过光照对着色的影响,这里重点阐述,GameByro是怎样根据光源产生阴影的。由GameByro提供的阴影均基于ShadowMap技术,

但也提供了ShaowVolume的示例代码。

Shadowing System是完全建立在帧渲染系统上的, 通过一个RenderClick生成ShadowMap,然后在正常渲染流程开始之前将ShadowMap更新到可见集内每一个渲染对象上。这样当渲染对象使用NiStandardMaterial时就会根据光源的阴影技术来对ShadowMap进行采样,并将结果与最终的输出颜色按一定比例混合。

阴影系统由以下几个类构成:

  • Shadow Write Materials:从NiFragmentMaterial派生,封装了生成 ShadowMap的算法和着色程序。

GameByro提供了三种类型的Shadow Write Materials,分别为NiPointShadowWriteMaterial、

NiDirectionalShadowWriteMaterial、NiSpotShadowWriteMaterial,适用于三种不同的光源类型。

  • Shadow Technique:这个类封装了阴影算法的细节,包括生成ShadowMap和使用ShadowMap投射阴影。
  • Shadow Render Click: 这个类是一个生成ShadowMap的批,这个类的对象是由shadow click generator负责生成。
  • Shadow Click Validator: Shadow Render Click:通过此类对象判断接受阴影的几何体对于shadow generator来说是否可见。
  • Shadow Map: 每个ShadowMap对象包含一个作为阴影图的纹理,shadowmap对象由shadowManager直接管理,每个shadowmap对象都被一个shadow generator引用。
  • Shadow Cube Map: 同shadowmap作用相同,只是阴影图的纹理类型为CubeMap。主要用于点光源生成的全方向阴影。
  • Shadow Generator: 阴影生成器,每个ShadowGenerator都对应一个NiDyamicEffect(NiLight的基类),也就是为这个NiDyamicEffect代表的光源生成阴影, 生成阴影所采用的技术由对象引用的ShadowTechnique来决定。
  • Shadow Click Generator: 生成ShadowMap的Shadow Render Click都由此类负责创建。这个类为每个ShadowGenerator指定ShadowMap,并负责每帧更新ShadowMap和ShadowMap所对应的变换矩阵。
  • Shadow Manager:所有的ShadowMap、ShadowTechnique、ShadowGenerator、shadow render click对象都由ShadowManager统一管理,并负责使用一个shadow click generator 在每一帧生成一个shadow render click的列表。

阴影系统静态结构如下:

整个阴影渲染的流程大致如下:

1. 在应用程序初始化阶段,通过调用NiShadowManager的Initialize()接口实现对整个阴影系统的初始化,此时应用程序会注册所有的ShadowTechnique,并初始化NiShadowClickGenerator。

2. 当我们创建一个NiLight以后,我们可以通过NiShadowManager为这个NiLight新建一个NiShadowGenerator,NiShadowGenerator会通过NiLight的类型来选择合适的NiShadowTechnique,此时NiShadowManager会为新的NiShadowGenerator创建一个NiShadowRenderClick。

3. 当帧渲染系统启动后,NiShadowRenderClick的PerformRendering()接口会被调用,此时NiShadowRenderClick会通过引用的NiGenerator获得阴影生成的着色程序和所需的数据(例如深度偏移),同时通过NiGenerator引用的Camera获得场景图中的可见集。下一步就是对可见集中的渲染对象添加ShadowWriteMaterial并设为激活状态,而ShadowWriteMaterial的类型是根据NiDyamicEffect的类型指定的。最后NiRenderClick就会启动渲染流水线,将可见集中的对象的深度全部渲染到ShadowMap中。

4. 在第一个RenderClick中生成了ShadowMap,下面就要使用这些ShadowMap投射阴影。在每一帧开始之前,用户还可以自己指定不接受阴影的节点,手动将其插入NiShadowGenerator::m_kUnaffectedReceiverList中。在渲染BackBuffer的RenderClick中,首先会对场景图中的节点进行一次遍历,将不受阴影的节点放入NiShadowGenerator:: m_kUnaffectedCasterList的列表。在对每个节点进行渲染时,会遍历NiShadowManager中所有的NiShadowGenerator,判断这些NiShadowGenerator是否对这个节点有影响,判断的规则是此节点是否存在于UnaffectedReceiverList和UnaffectedCasterList这两个链表中,如果存在于任何一个链表,则节点不受此NiShadowGenerator影响,如果受此NiShadowGenerator影响,那么就将该NiShadowGenerator上的ShadowMap和数据更新到节点上的渲染属性中,NiStandardMaterial会根据这些数据选择对ShadowMap采样的方式,并将结果混合到最终的输出颜色中。

值得注意的是,点光源的shadowMap默认的是采用CubeMap实现,用户可以通过接口选择不使用CubeMap实现,当采用CubeMap实现时,光源无法产生软阴影。

渲染系统的扩展:

为了验证GameByro渲染系统的扩展性,笔者尝试着加入了一个后期处理特效Screen Space Ambient Occlusion(SSAO),即屏幕空间的遮蔽,由于仅仅为了熟悉GameByro的渲染流程,所以笔者并未对SSAO算法做深究,仅仅用了自己简化的算法。

在渲染过程中先单独使用一个RanderClick将场景中的深度渲染到一张纹理上,然后在渲染到后台缓冲区的RenderClick中对深度纹理进行采样,执行SSAO算法,将结果混合到最终的结果中。采样点的偏移坐标是通过对一组随机向量进行归一化再乘以0~1之间的随机数而生成的,即长度为0~1之间的随机向量。

PS代码如下:

float VerticalRange:GLOBAL; //控制XY方向采样范围变量,可以在应用程序层对其进行调整

float HorizontalRange:GLOBAL; //控制在Z方向采样范围的变量

float calAO(float2 texCoord,float dw, float dh ) //通过当前像素所标和偏移量计算AO

{

float2 coord = float2(texCoord.x + dw, texCoord.y + dh);

float4 CenterPos = tex2D(DepthSampler,texCoord);

float4 CurPos = tex2D(DepthSampler,coord);

float depthDiff = clamp(CenterPos.z - CurPos.z,0,VerticalRange);

float ao = depthDiff/length(CurPos.xyz - CenterPos.xyz);

return ao;

}

// Pixel shader

float4 PS_SSAO(VS_OUTPUT In) : COLOR

{

float2 texCoord = In.BaseTex;

float depth = tex2D(DepthSampler,texCoord).z;

float ao = 0.0;

float scale = HorizontalRange/depth; //因为采样范围会受深度影响,故除以此系数。

for(int i=0; i<32; ++i)

{

float2 offset = arrRandomPt[i].xy* scale;

ao += calAO(texCoord, offset.x, offset.y);

}

ao/=32;

float4 color = tex2D(BaseSampler,texCoord);

color.xyz *= (1.0 - ao);

return color;

}

最终实现效果如下:

SSAO生成的明暗图 无SSAO材质

以下两图上图为无SSAO效果,下图为开启SSAO后的效果。

总结:

GameByro的帧渲染系统是比较灵活,想加入自己的渲染流程是比较容易的,此外由于RenderTarget和RenderView都可以由用户指定,所以想实现自己的shader效果不是很难。然而,NiStanderMaterial的shade tree比较复杂,总共高达6000行代码以上,过程非常复杂,这就是说,如果想实现自己的材质处理流程也要付出相当大的工作量,阴影系统虽然实现了较低的耦合度,但是实现过于复杂,不够简洁高效。

作者:叶起涟漪

GameByro渲染系统剖析相关推荐

  1. pyqt创建窗口没有句柄_Filament 渲染引擎剖析 之 FrameGraph 1 虚拟资源的定义与创建...

    Filament 使用了可扩展渲染管线(FrameGraph)来组织渲染通道和管理渲染资源,网上也搜了下可扩展渲染管线的相关的文章,一般认为可扩展渲染管线是次时代渲染引擎应该具备的比较先进的管线组织架 ...

  2. Nebula3 渲染系统

    游戏引擎首先解决的任务就是渲染,N3的渲染架构是一个多线程渲染架构.渲染线程是主线程外的一个线程,主线程操作的是GraphicsEntity , 而渲染渲染线程操作的是InternalGraphics ...

  3. Filament 渲染引擎剖析 之 FrameGraph 1 虚拟资源的定义与创建

    Filament 使用了可扩展渲染管线(FrameGraph)来组织渲染通道和管理渲染资源,网上也搜了下可扩展渲染管线的相关的文章,一般认为可扩展渲染管线是次时代渲染引擎应该具备的比较先进的管线组织架 ...

  4. antd 中table上加不同字体颜色_字体渲染系统!微软终于决定优化Win10字体模糊问题...

    据外媒WindowsLatest报道 , 微软可能会在明年为Windows 10系统带来新的字体渲染系统和便捷的颜色选择器. 在目前版本的Windows 10中字体管理已经比较方便,在设置里可以轻松查 ...

  5. Filament渲染引擎剖析 之 通过图元构建几何体

    Filament渲染引擎剖析 之 通过图元构建几何体 什么是图元 filament可绘制的图元类型 构建图元的工具 VertexBuffer IndexBuffer Primitive 什么是图元 图 ...

  6. 设计渲染系统,为什么要特别关注“显卡”? | GAMES104实录 - 现代游戏引擎:从入门到实践

    本期为GAMES104<现代游戏引擎:从入门到实践>视频公开课文字实录第11期.本课程由GAMES(图形学与混合现实研讨会)发起,游戏引擎技术专家王希携手游戏引擎一线开发者共同研发. 课程 ...

  7. 3D游戏引擎系统源码C++本科毕业设计,C++ 3D引擎源码,渲染系统使用的OpenGL 及 OpenGL ES

    Effective 3D Engine 渲染系统使用的OpenGL 及 OpenGL ES,Windows上OpenGL ES使用AMD的ES模拟器. 环境部署 完整代码下载地址:3D游戏引擎系统源码 ...

  8. 全国高校cct联合计算机考试,全国高校CCT联合考试系统剖析

    <全国高校CCT联合考试系统剖析>由会员分享,可在线阅读,更多相关<全国高校CCT联合考试系统剖析(31页珍藏版)>请在人人文库网上搜索. 1.全国高校CCT联合考试系统剖析1 ...

  9. Filament 渲染引擎剖析 之 FrameGraph 2 动态构建渲染管线

    一.渲染通道的设计与实现 1 Frostbite 构建FrameGraph的准则 我们先看下Frostbite 构建FrameGraph原则,包括三个阶段: 设置阶段 setup.编译阶段compil ...

最新文章

  1. 添加日志文件组与日志文件成员
  2. flask+sqlite3+echarts3+ajax 异步数据加载
  3. 攻击者使用“非恶意软件”也能识别,将在RSA 2017上发布的新技术
  4. python弹出窗口 闪烁_Python。得到闪烁/闪烁的窗口
  5. UVa11426——欧拉函数
  6. LeetCode - Easy - 637. Average of Levels in Binary Tree
  7. python语言goto_如何在 Python 中实现 goto 语句
  8. 让低版本浏览器支持html5的标签
  9. DeFi一体化平台Parsec获125万美元种子轮融资并正式启动
  10. git添加远程库遇到的问题
  11. 一文看懂3D封装技术
  12. 【抽象代数】半群、子群、商群
  13. Alien Skin Exposure v6.x 最新通用完整版汉化补丁
  14. deepin20 外接显示器,标题栏美化
  15. python查询12306余票_使用 Python 在 12306 查询火车票余票
  16. pinia 的使用(三)—— actions
  17. 卐 4-3D图形的数学
  18. matlab零序五次谐波,基于5次谐波的小电流接地系统故障选线方法仿真与分析.docx...
  19. (3.1E)Shortest Distance (20)
  20. python爬虫win10程序_Python爬虫教程:批量提取Win10锁屏壁纸

热门文章

  1. 计算机专业名词术语raid,RAID中的9个专业术语详解
  2. lua学习笔记之闭包
  3. javascript高级程序设计之变量、作用域和内存问题
  4. UVa1418 - WonderTeam(构造法)
  5. 数据库数据满足树结构时,求一个结点的子结点有哪些
  6. 如何快速阅读一篇英文文献
  7. C++智能指针简单剖析
  8. 洛谷P3388 【模板】割点(割顶)
  9. 实现磁贴的效果的一种方法
  10. 手机休眠监测wifi