您的位置:首页 > 其它

GPU程序在GameByro中的使用

2009-04-22 11:17 351 查看

引言:

GameBryo拥有一套复杂的材质系统,这套材质系统可以根据渲染对象的状态和属性生成不同的shader代码,提高了渲染流程的适应性,可以使你定义一套材质能适应多种渲染对象。同时,GameByro将shader的初始化和使用插件化,方便与美术工具集成,并且实现了平台无关性。为了实现这些目的,GameByro使用了一套复杂的机制,本文主要解析GameByro如何生成、编译并使用shader代码。

Shader

GameBryo的shader的接口封装在NiShader中,顶点数据流声明,常量表的访问,渲染状态的设置都是通过这个类(有点类似于D3Deffect)。在程序运行NiShader是由NiShaderFactory负责管理的,NiShaderFactory通过NiShaderLibrary从文件中创建shader,用全局性的map管理起来。NiShaderLibrary通过解析shader文本创建NiShader对象,并调用3D图形接口编译shader代码,将这个类以dll的形式封装,就可以作为插件来使用。NiShader类的创建可以通过解析文件来进行,也可以通过C++的类来定制,只需从NiShader上继承即可。GameByro为PC平台提供了一个NiD3DXEffectShaderLib库,这个库提供了解析shader文件和初始化shader对象的功能。用户只需按GameByro定义的格式编写shader代码的语意和注释,NiD3DXEffectShaderLibrary就会根据文本来创建NiD3Dshader对象,在应用程序中就可以通过Techinqe的名称来访问这个对象。通过这种机制,我们将shader文本文件放在相关美术工具指定的目录下,在工具中就可以使用这些shader,并且能够通过shader的语意和注释为相关参数和变量生成UI,方便美术调试。 WIN平台上的整个流程如下: 1. 应用程序在启动时会先初始化整个shader系统,接下来导入Shader解析库和加载库(dll的形式)。 2. 接下来应用程序将NiD3DShader的初始化工作委托给NiShaderLibrary来处理,NiShaderLibrary首先通过NiD3DXEffectLoader载入所有的shader文本文件,并通过NiD3DXEffectParser解析文本生成NiD3DXEffectFile对象,同时NiD3DXEffectLoader还负责将shader代码编译成二进制形式的GPU程序。 3. 最后由NiD3DXEffectTechnique负责通过NiD3DXEffectFile上的信息生成NiD3Dshader对象。 4. 所有的shader对象创建后,NiShaderLibrary的初始化就结束了,最后由NiShaderFactory负责统一管理。

材质:

NiMaterial为渲染对象生成和定义Shader,NiMaterialInstance为渲染对象分配 和Cach Shader。NiFragmentMaterial提供了一个Shader Tree框架,在它的继承类中可以使用这个框架搭建shader tree。这个机制允许NiFragmentMaterial根据对象不同的渲染状态生成不同的shader代码,Cach在内存中,并保存到磁盘文件。GameByro描述符的概念大量使用,包括前面提到的Shader解析过程也是通过描述符来传递信息。在材质系统中主要使用了NiMaterialDescriptor和NiGPUProgramDescriptor这个两个类做描述符,这两个类中保存的信息是兼容的,都是为了描述某种材质在渲染对象的某一特定渲染状态下所对应的GPU程序的特征。NiFragmentMaterial通过渲染目标的状态和属性生成NiMaterialDescriptor,并通过NiMaterialDescriptor查找匹配的shader,如果找不到,则通过shader tree生成相应的shader程序,并保存到磁盘文件中。当下一次应用程序启动时就可以通过这个文件直接创建NiShader对象。可以说通过NiFragmentMaterial生成的shader代码是为特定的渲染对象在特定的情况下量身打造的。 整个过程的详细流程如下: 1. 在每次渲染一个物体之前,NiMaterialInstance会先判断这个物体的shader程序是否需要更新,如果不需要更新,就直接返回当前Cach的NiShader;如果需要更新, NiMaterialInstance首先会根据物体的渲染状态为其生成一个NiMaterialDescriptor,然后将这个NiMaterialDescriptor和当前Cach住的NiShader进行比较,如果匹配仍然返回当前Cach的NiShader,如果不匹配,将获得shader的工作转交给NiMaterial进行。 2. NiMaterial首先通过这个NiShaderFactory 查询匹配这个NiMaterialDescriptor的NiShader,如果找不到,就通过NiMaterialDescriptor生成NiShader,同时生成一段Shader代码,并保存到以shader描述符中的特征码来命名对应的shader文件。 3. 当获得相应的NiShader对象后,NiMaterialInstance会调用NiShader的SetupGeometry接口,在这个接口中会进行顶点声明。 以下是NiMaterialInstance为Geometry选择shader的代码: NiShader* NiMaterialInstance::GetCurrentShader(NiRenderObject* pkGeometry, const NiPropertyState* pkState, const NiDynamicEffectState* pkEffects) { if (m_spMaterial) { bool bGetNewShader = m_eNeedsUpdate == DIRTY; if (m_eNeedsUpdate == UNKNOWN) bGetNewShader = pkGeometry->GetMaterialNeedsUpdateDefault(); // Check if shader is still current if (bGetNewShader && m_spCachedShader) { bGetNewShader = !m_spMaterial->IsShaderCurrent(m_spCachedShader, pkGeometry, pkState, pkEffects, m_uiMaterialExtraData); } // Get a new shader if (bGetNewShader) { NiShader* pkNewShader = m_spMaterial->GetCurrentShader( pkGeometry, pkState, pkEffects, m_uiMaterialExtraData); if (pkNewShader) { NIASSERT(m_spCachedShader != pkNewShader); ClearCachedShader(); m_spCachedShader = pkNewShader; if (!pkNewShader->SetupGeometry(pkGeometry, this)) ClearCachedShader(); } else { ClearCachedShader(); } } m_eNeedsUpdate = UNKNOWN; } return m_spCachedShader; } 如果想通过NiFragmentMaterial实现自己的shader tree就需要在NiFragmentMaterial提供的接口中实现自己拼装代码的逻辑,代码块由NiMaterialLibraryNode封装,NiMaterialLibraryNode既可以直接写C++代码来定义,也可以先写成XML脚本,再由专门的解析工具转换成C++代码。 由NiStandardMaterial生成的shader代码文件如下图所示:

文件名就是NiMaterialDescriptor的掩码,用来标识的shader代码的行为。 Shader代码的行为描述如下: Shader description: APPLYMODE = 1 WORLDPOSITION = 0 WORLDNORMAL = 0 WORLDNBT = 0 WORLDVIEW = 0 NORMALMAPTYPE = 0 PARALLAXMAPCOUNT = 0 BASEMAPCOUNT = 1 NORMALMAPCOUNT = 0 DARKMAPCOUNT = 0 DETAILMAPCOUNT = 0 BUMPMAPCOUNT = 0 GLOSSMAPCOUNT = 0 GLOWMAPCOUNT = 0 CUSTOMMAP00COUNT = 0 CUSTOMMAP01COUNT = 0 CUSTOMMAP02COUNT = 0 CUSTOMMAP03COUNT = 0 CUSTOMMAP04COUNT = 0 DECALMAPCOUNT = 0 FOGENABLED = 0 ENVMAPTYPE = 0 PROJLIGHTMAPCOUNT = 0 PROJLIGHTMAPTYPES = 0 PROJLIGHTMAPCLIPPED = 0 PROJSHADOWMAPCOUNT = 0 PROJSHADOWMAPTYPES = 0 PROJSHADOWMAPCLIPPED = 0 PERVERTEXLIGHTING = 1 UVSETFORMAP00 = 0 UVSETFORMAP01 = 0 UVSETFORMAP02 = 0 UVSETFORMAP03 = 0 UVSETFORMAP04 = 0 UVSETFORMAP05 = 0 UVSETFORMAP06 = 0 UVSETFORMAP07 = 0 UVSETFORMAP08 = 0 UVSETFORMAP09 = 0 UVSETFORMAP10 = 0 UVSETFORMAP11 = 0 POINTLIGHTCOUNT = 0 SPOTLIGHTCOUNT = 0 DIRLIGHTCOUNT = 0 SHADOWMAPFORLIGHT = 0 SPECULAR = 1 AMBDIFFEMISSIVE = 0 LIGHTINGMODE = 1 APPLYAMBIENT = 0 BASEMAPALPHAONLY = 0 APPLYEMISSIVE = 0 SHADOWTECHNIQUE = 0 ALPHATEST = 0 NiStanderMaterial就是根据这些掩码的数据来生成shader代码,用户可以通过重载GenerateVertexShadeTree、GeneratePixelShadeTree、CreateShader这些接口来定义自己的shader生成规则。

增加自己的渲染效果:

通过前几节我们可以了解到,想定义自己的材质,一是通过编写shader代码完成。在应用程序初始化的时候,这些shader代码会被初始化成NiShader对象,进一步的通过NiShader对象来初始化NiSingleShaderMaterial对象,并分配给渲染对象。在GameByro默认的渲染流程中,这些步骤都是自动进行的,美术只需在3DMAX插件中为几何体的材质指定Shader程序,导出到nif文件,应用程序就能正确加载并渲染;二是定义自己的NiMaterialFragment类,在类中定义如何生成shader,在应用程序运行时只要将这个类的实例指派给几何体,这个类就会自动为几何体生成shader。这两种方式对于美术人员来说,主要区别在于,采用第一种方法定义的材质,其渲染数据的设置必须严格符合shader代码中所需的数据,否则就会报错。(比如说,顶点数据流必须严格符合shader程序的定义,必须为shader中每个采样器提供格式正确的纹理);而采用第二种方法定义的材质,就有很高的容错和适应性,但是这种容错性和适应性需要自己写代码来完成,GameByro提供的NiStanderMaterial就提供了这套完整的机制。每个贴图槽内的贴图如果你设置就会生成相应的贴图处理流程,如果不设置,就没有这张贴图的处理流程。 为了验证这个过程,笔者尝试增加了一个自己的shader特效——SubSurfaceScattering,简称3s,其原理是模拟光在半透明物体中散射的效果。由于该效果无须预处理过程,所有的贴图均来自磁盘文件,所以比较容易融合到GameByro工作流中。 笔者将在FX COMPOSER中调试通过的fx文件放入SDK中的SDK\Win32\Shaders\Data目录下,在3DMAX的材质面板选择GameByroShader,然后就可在显示shader的组合框中看到文件中定义的Techinqe,选择点击apply按钮,就会出现自定义的参数调整界面。



通过调整参数,最终得到皮肤和玉器的渲染效果如下:

皮肤

玉器

总结

GameByro的这套开发流程非常方便直观,但是美术仅能为shader程序分配静态的数据源,比如说光照图等,CubeMap等;而一些在程序中实时生成的纹理数据则无法整合到美术工具中,比如说阴影图、折射图、反射图等,这些都需要程序写代码来实现。调试起来就不大方便了。大部分情况下,我们只需要使用GameByro提供的NiStanderMaterial就可以完成大部分材质的需求,特殊的效果可以自己写shader或者通过引擎提供的shader库来完成,只有当我们需要即根据复杂的情况做很多不同的处理时,我们才需要重载NiFragmentMaterial搭建自己的shader tree。不过搭建shader tree的程序一般比较复杂,编写难度大,虽然引擎允许通过XML文件来编写材质节点,但是使用起来仍然不方便。GameByro并没有提供相关的后期处理的开发工具,后期处理的特效并不能所见即所得,这方面还需完善。 GameByro为几何体在特定的环境下生成专用的shader代码,具有一定的灵活性,但是也付出了以下代价: l 分析几何体的属性和当前状态,为其生成shader代码的过程有性能损耗。 l Shader代码生成后会保存到磁盘文件中,这个过程如果不使用异步,可能会引起阻塞。 l 生成的NiShader对象会有内存消耗。由于GameByro默认的实现是将所有的shader文件初始化成NiShader对象,所以当游戏运行的时间久了以后会生成大量的shader文件,这时候内存的消耗可能会很可观,同时加载的时间也会增加。不过可以自己控制加载的流程,在这里进行性能优化。

作者:叶起涟漪
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: