zoukankan      html  css  js  c++  java
  • GPU程序在GameByro中的使用

    引言:

    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代码文件如下图所示:

    clip_image002

    文件名就是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按钮,就会出现自定义的参数调整界面。

    clip_image004

    clip_image006

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

    clip_image007

    皮肤

    clip_image009

    玉器

    总结

    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文件,这时候内存的消耗可能会很可观,同时加载的时间也会增加。不过可以自己控制加载的流程,在这里进行性能优化。

    作者:叶起涟漪

  • 相关阅读:
    springMVC中添加<mvc:resource>时的问题
    package
    mybatis 解决属性名和字段名不一致
    Linux常用命令
    Hibernate下载
    Hibernate初识
    js根据身份证获取出生年月日
    spring-Boot 热部署
    Struts2---动态action以及应用
    Struts2基础
  • 原文地址:https://www.cnblogs.com/flying_bat/p/1441175.html
Copyright © 2011-2022 走看看