3.4 渲染器核心(二)
3.4.4 加载和解析着色程序
为一个效果加载和解析着色程序的入口是ShaderEffect::LoadPrograms函数,这个函数在Renderer::ApplyEffect函数里面被调用。第一部分源码是
void ShaderEffect::LoadProgram( int iPass, int iMaxColors, int iMaxTCoords,
int iMaxVShaderImages, int iMaxPShaderImages )
{
Program* pkVProgram = m_kVShader[iPass]->GetProgram();
Program* pkPProgram = m_kPShader[iPass]->GetProgram();
assert( (pkVProgram!=0) == (pkPProgram!=0) );
if( pkVProgram )
{
// the programs have already been loaded
return;
}
VertexProgramPtr spkVProgram = VertexProgramCatalog::GetActive()->Find(
m_kVShader[iPass]->GetShaderName() );
PixelProgramPtr spkPProgram = PixelProgramCatalog::GetActive()->Find(
m_kPShader[iPass]->GetShaderName() );
<remainder of implementation>
}
函数检查着色程序是否已经加载。在一般情况下它们是,因为加载应该只发生一次,或者在第一次尝试绘制,或者是程序代码的强制显式加载。
当资源没有被加载,函数会在目录里查找顶点和像素程序。例如,查找顶点程序的Find函数为
VertexProgram* VertexProgramCatalog::Find( const std::string& rkProgramName )
{
if( rkProgramName==ms_kNullString || rkProgramName==ms_kDefaultString )
{
return StaticCast<VertexProgram>(m_spkDefaultVProgram);
}
// attempt to find the program in the catalog.
stdext::hash_map<std::string,VertexProgram*>::iterator pkIter =
m_kEntry.find( rkProgramName );
if( pkIter != m_kEntry.end() )
{
// the program exists in the catalog, so return it
Return pkIter->second;
}
// attempt to load the program from dist
assert( m_cCommentChar );
VertexProgram* pkProgram = VertexProgram::Load( rkProgramName,
m_kRendererType, m_cCommentChar );
if( pkProgram )
{
// the program exists on disk. Insert it into the catalog
m_kEntry.insert( std::make_pair(rkProgramName, pkProgram) );
return pkProgram;
}
// the program does not exist. Use the default program
return StaticCast<VertexProgram>(m_spkDefaultVProgram);
}
目录包含一个由字符串名字和顶点程序对组成的哈希图,这保证了查找速度。就如注释所说,如果字符串名字指定的着色程序已经在图中,那么那个程序之前就已经被加载,并且它的指针由Find函数返回。如果不在图中,那么它必须从磁盘里加载。会尝试加载它。如果从磁盘加载成功,那么着色程序会插入到哈希图里,这样以后它就可以被其它效果共享。如果不在磁盘里,默认的顶点程序就会返回。
目录设计为允许任何类型渲染器使用,因此可以保存OpenGL,Direct3D或者软件渲染器的着色程序。平台特定的信息保存在数据成员m_kRendererType,它的值是ogl,dx9或sft。数据成员m_cCommentChar的值为OpenGL渲染器的#或Direct3D和软件渲染器的/。应用层提供默认的目录,因此你不需要知道存在有哪些目录系统。目录在main之前就创建,但渲染器还没创建。目录数据成员的赋值延迟到渲染器在WindowApplication::OnInitialize中被创建。目录类提供函数SetInformation来支持延迟赋值。这个函数的输入是通过调用Renderer::GetExtension获得的m_kRendererType和调用Renderer::GetCommentCharacter获得的m_cCommentChar。
顶点程序的加载函数源码为
VertexProgram* VertexProgram::Load( const std::string& rkProgramName,
const std::string& rkRendererType, char cCommentPrefix )
{
std::string kFilename = std::string(“v_”)+rkProgramName+std::string(“.”)
+rkRendererType+std:string(“.wmsp”);
VertexProgram* pkProgram = WM4_NEW VertexProgram;
bool bLoaded = Program::Load( kFileName, cCommentPrefix, pkProgram );
if( !bLoaded )
{
WM4_DELETE pkProgram;
Return 0;
}
pkProgram->SetName( rkProgramName.c_str() );
VertexProgramCatalog::GetActive()->Insert( pkProgram );
return pkProgram;
}
第一行代码创建保存顶点程序的完整磁盘文件名字。所有这些程序有前缀v_,像素程序有前缀p_。如果你创建你自己的着色程序,你的文件名需要按照这个协定。
顶点程序使用默认构造函数来创建(注释:如果你看VertexProgram和Program的源代码,你会注意到默认构造函数是保护的。从应用程序的角度来看,创建一个着色程序的唯一方法是由从磁盘加载。最终,我会增加一个子系统来实现程序生成着色程序和着色缝合)。静态成员函数Program::Load通过真正加载和解析文件来填充顶点程序的数据成员。假如加载成功,为了其它共享这个程序的效果也可以访问到,顶点程序会被插入到目录,并且着色程序返回给调用函数。
Program::Load函数带我们到加载系统的实际工作场所。Program类设计为纯粹支持从磁盘加载着色程序,并且解析它来识别输入,输出,着色常量和相关寄存器,以及纹理采样器。这个类的公共接口是
class Program : public Object, public Bindable
{
// abstract base class
virtual ~Program();
// member read-only access
const std::string& GetProgramText() const;
const Attributes& GetInputAttributes() const;
const Attributes& GetOutputAttributes() const;
// access to renderer constants
int GetRCQuantity() const;
RendererConstant* GetRC( int i );
RendererConstant* GetRC( RendererConstant::Type eType );
// access to numerical constants
int GetNCQuantity() const;
NumericalConstant* GetNC( int i );
// access to user constants
int GetUCQuantity() const;
UserConstant* GetUC( int i );
UserConstant* GetUC( const std::string& rkName );
// access to samplers
int GetSIQuantity() const;
SamplerInformation* GetSI( int i );
SamplerInformation* GetSI( const std::string& rkName );
}
这是一个抽象类,但设计为支持仅仅两个继承类:VertexProgram和PixelProgram。一旦保护的Load函数被调用,并且解析成功,你就可以访问程序的文本字符串,输入属性和输出属性。就如前面提到的,有三种着色常量:渲染器常量,数值常量(仅仅Direct3D)和用户自定义常量。这些都可以通过公共接口访问。最后,你还可以访问着色程序需要的采样器数目(可能没有)和每个采样器的信息。
Program::Load里面的分析代码相当长,因此我不会在这里深入细节。我手工编写这些代码,通过观察来推断汇编文本文件的语法规则。这个解析器不是完整的。当前,它不支持数组作为输入的着色程序。手工生成着色程序的解析器在产品环境里大概不是一种最好的方法。更好的是使用一些工具,诸如Lex,Yacc,Flex和Bison(等,http://dinosaur.compilertools.net)来自动从语法生成解析器代码。那样,你就可以集中精力在维护语法规则上,例如着色程序演变,或者去支持多种着色程序而不是仅仅Cg或者HLSL。
属性
当你看着色程序样本的时候,不管他们是用什么语言写,你会发现你必须传入各种不同的很多数量的输入诸如位置,法线,颜色和纹理坐标。Wild Magic封装属性的概念在一个类里,恰好的,叫Attributes。这被证明是一个非常方便的类。它用于保存着色程序的输入输出属性,但它也用于指定几何图元相关的顶点缓存的结构。这个类也允许对比两组属性是否相同或者最少是兼容的(从子集包含的意义上说)。
Attributes类允许你设置位置,法线,颜色和纹理坐标的通道数量。当前,你可能有三或四个通道的位置和法线。三通道值是一般在你的模型里面使用的3D分量(x,y,z)。四通道值支持齐次点和向量,(x,y,z,w)。设置这些可以通过
// 3 or 4 channels, (xyz, xyzw)
void Attributes::SetPChannels( int iPChannels );
void Attributes::SetNChannels( int iNChannels );
你被允许任意数量的颜色属性,即使着色程序当前只支持两种颜色。你指定每个单元以及单元支持的通道数,数量从一到四。
// 1 to 4 channels, ( r, rg, rgb, rgba )
void Attributes::SetCChannels( int iUnit, int iCChannels );
相似的,你被允许任意数量的纹理坐标属性。你指定每个单元以及单元支持的通道数,数量从一到四。
// 1 to 4 channels, ( s, st, str, strq )
void Attributes::SetTChannels( int iUnit, int iTChannels );
属性被内部组织为拥有顺序
position, normal, color[0], color[1], …, tcoord[0], tcoord[1], …
是自动组织的,因此你不用担心需要以一些特殊顺序调用Attributes函数。
渲染器常量
渲染器常量之前已经讨论过了。RendererConstant类有一组着色程序用来命名渲染器常量的枚举值。这个类也有一组与这些枚举值相对应的字符串名字。Program::Load里的解析器对比着色程序文件里面的着色常量的名字和RendererConstant里的字符串名字。当它找到一对匹配的,常量会被添加到Program对象里的一个RendererConstant对象数组。除了保存常量的值,解析器也知道哪个寄存器分配给了常量,并且保存了这信息。当启用着色程序,用常量值设置寄存器的时候,渲染器读取着色常量和寄存器。
数值常量
NumericalConstant类表示着色程序里面需要的数值。仅仅Direct3D渲染器需要你显式指定数值常量。OpenGL和软件渲染器自动处理数值常量。Program::Load里的解析器创建NumericalConstant对象,即使是OpenGL和软件渲染器,并且将它们保存在Program对象管理的数组里。保存的信息包括常量值和分配给它们的寄存器。当启用着色程序时,渲染器读取数值常量。
用户自定义常量
UserConstant类表示着色程序里面需要的用户自定义常量。当着色程序启用的时候,渲染器常量和数值常量会自动分配给寄存器;程序代码不需要为这个做任何事。然而,用户自定义常量需要应用程序按需修改。这样,为了允许程序和用户自定义常量内存之间的通讯,UserConstant类拥有比RendererConstant或NumericalConstant更丰富的接口。
用户自定义常量的构造函数需要你提供一个常量的字符串名字。这个字符串名字允许应用程序查询着色程序,获得用户自定义常量的句柄。应用程序然后可以按需修改常量。然而,问题是常量存储在那里?
Shader类为用户自定义常量提供存储区。这是一件自然的事,有以下原因。一个Program对象可能会被多个着色器共享。需要被共享的是程序文本字符串,这是程序的汇编源码。就像一个C函数或者C++函数,着色器函数由图形系统为当前生效的几何图元调用。函数的输入由几何图元决定。而且,这些输入,包括着色器常量,可能在每次调用着色程序的时候都会改变。Program函数不能保存这些常量——它们必须由着色程序的客户程序传递给它,在我们目前的情况下是一个Shader对象。
应用程序可能创建Shader类的多个实例,并且这些实例附加到不同的几何图元上。实例间共享Program对象,但每个实例可能有自己一组用户自定义常量。这样,Shader类保存Program对象,和保存用户自定义常量相关的一个浮点值数组。这个类也保存Texture对象数组和相关的Image对象。保存这个的原因跟用户自定义常量的原因一样。一个拥有纹理采样器输入的着色程序会使用任何当前有效的纹理和图像。纹理和图像是基于每个物体改变的,即使着色程序不改变。
采样器信息
着色程序里的纹理采样器有各种需要在Program::Load里的解析器进行识别的信息。第一,着色程序里的每个采样器都有一个关联的名字,因此你可以在程序里使用这个采样器。第二,有必要知道需要什么类型的采样器。类型包括1D,2D和3D纹理采样器。几种类型支持一些一般效果。立方体采样器支持立方体环境映射。本质上,这是处理立方体贴图的六个面的六个2D采样器的集合。投射采样器支持投射纹理和阴影贴图。本质上,这是一个2D采样器,但纹理坐标是齐次空间的点。第三,编译的着色程序包括纹理/采样器单元,这些在着色程序被调用的时候会进行实际的采样操作。必须记住单元的数量,这样,当纹理在绘制指令调用期间被启用的时候,图形引擎能设置相应的单元。
保存所有这些信息的类是SamplerInformation。Program类维护一个与它相关的这些对象的数组。
着色程序相关数据
一旦Program::Load加载和解析完着色程序,Program对象有下列:
#一个程序文本字符串,包含由图形系统执行的汇编指令。
#一组输入保存为Attributes对象。
#一组输出保存为Attributes对象。
#一个RendererConstant对象数组,这个数组总是最少有一个元素——映射输入位置到裁剪空间的变换,因为光栅器需要这个信息。每个对象有一个类型识别符,数据存储,和准备加载数据的寄存器。
#一个NumericalConstant对象数组,这个数组Direct3D渲染器需要(但OpenGL和软件渲染器自动处理)。
#一个UserConstant对象数组。每个对象拥有一个名字,这是为了方便应用程序访问,一个数据存储位置的指针,和准备加载数据的寄存器。
#一个SamplerInformation对象数组。每个对象有一个名字,采样器类型,和纹理单元数量,这个数量指的是真正进行采样的单元数量。
每个Program对象是由一个Shader对象管理;特别的,一个VertexProgram对象是由一个VertexShader对象管理,一个PixelProgram对象是由一个PixelShader对象管理。Shader对象有下列:
#一个字符串名字(主要为了在着色程序目录里查找)。
#一个Program对象。
#保存着色程序使用的用户自定义常量。
#一个图像名字数组。图像它们自己实际上是保存在一个图像目录。这些名字是用来在目录里查找图像。
#一个Texture对象数组。这些纹理与着色程序的采样器有关。
一个ShaderEffect对象管理多组VertexShader和PixelShader对象。每对顶点和像素着色器对应一个绘制通道。ShaderEffect也管理一个AlphaState对象数组,每个对象指定一个通道怎样与之前通道的渲染结果混合。
现在的问题是,用户自定义着色常量,纹理,和图像怎样和着色程序关联起来?这使我们兜一个圈子到我们开始的地方——在绘制操作期间加载一个着色效果。让我们总结到目前为止我们有什么。一个准备绘制的几何图元。全局状态,变换,和已经设置好的索引缓存。现在我们通过函数ApplyEffect应用一个ShaderEffect到图元。对于每个通道,会执行下面的步骤。
#ShaderEffect对象有一个VertexShader和PixelShader对象。VertexShader对象有一个VertexProgram对象,PixelShader对象有一个PixelProgram对象。会试图通过ShaderEffect::Load函数加载着色程序。
#加载函数从着色程序目录取得着色程序,这个着色程序或者已经保存在内存,或者必须从磁盘加载。
#当着色程序从磁盘加载完毕,它们被解析来识别输入输出属性,渲染器常量,数值常量,用户自定义常量和纹理采样器。
然后我们使实际的属性,常量,纹理和图像与着色程序关联起来。最后,ShaderEffect::Load的最后部分为
void ShaderEffect::LoadPrograms( int iPass, int iMaxColors, int iMaxTCoords,
int iMaxVShaderImages, int iMaxPShaderImages )
{
<first portion of implementation>
m_kVShader[iPass]->OnLoadProgram( spkVProgram );
m_kPShader[iPass]->OnLoadProgram( spkPProgram );
OnLoadPrograms( iPass, spkVProgram, spkPProgram );
}
着色程序加载完毕后,每个着色程序都会有一个机会通过OnLoadProgram函数进行加载后操作。这个函数使数据和着色程序关联起来。源代码为
void Shader::OnLoadProgram( Program* pkProgram )
{
assert( !m_spkProgram && pkProgram );
m_spkProgram = pkProgram;
// the data sources must be set for the user constants.
// determine how many float channels are needed for the storage
int iUCQuantity = m_spkProgram->GetUCQuantity();
int i, iChannels;
UserConstant* pkUC;
for( i=0,iChannels=0; i<iUCQuantity; i++ )
{
pkUC = m_spkProgram->GetUC(i);
assert( pkUC );
iChannels += 4*pkUC->GetRegisterQuantity();
}
m_kUserData.resize( iChannels );
// set the data sources for the user constants
for( i=0,iChannels=0; i<iUCQuantity; i++ )
{
pkUC = m_spkProgram->GetUC(i);
assert( pkUC );
pkUC->SetDataSource( &m_kUserData[iChannels] );
iChannels += 4*pkUC->GetRegisterQuantity();
}
// load the images into the textures. If the image is already in system memory(in
// the image catalog), it is ready to be used. If it is not in system memory, an attempt
// is made to load it from disk storage. If the image file does not exist on disk, a
// default magenta image is used.
int iSIQuantity = m_spkProgram->GetSIQuantity();
m_kImageNames.resize( iSIQuantity );
m_kTextures.resize( iSIQuantity );
for( i=0; i<iSIQuantity; i++ )
{
Image* pkImage = ImageCatalog::GetActive()->Find( m_kImageNames[i] );
assert( pkImage );
m_kTextures[i].SetImage( pkImage );
m_kTextures[i].SetSamplerInformation( m_spkProgram->GetSI(i) );
}
}
第一部分代码遍历UserConstant对象,准确测定用户自定义常量需要多少浮点通道来保存数据。Shader里的存储区会缩放到这个数量。
第二部分代码设置UserConstant对象里的指针指向保存它们数据的数组的正确位置。当应用程序需要访问这些数据时,它必须用常量的字符串名字做适当的查询来获取指向数据存储位置的指针。
第三部分代码缩放图像和纹理数组到足够大来满足着色程序的需求。纹理图像通过查询当前起作用的图像目录来完成加载。这个目录表现得和程序着色目录相似。如果图像已经在内存,一个指向那个图像的指针就会返回。如果图像不在内存,会尝试从磁盘加载它。如果尝试成功了,图像会放到哈希图来达到共享的效果,并且图像指针返回。如果图像不在磁盘上,默认的图像会返回(一种紫色)。每个Texture对象被赋予它自己的纹理图像和相应的采样器信息。当纹理被加载/启用来绘制的时候,图形系统会访问图像和采样器信息。
也有一个OnLoadProgram的结伴函数叫OnReleaseProgram。这个函数简单的减少着色程序的引用。如果着色器对象是最后一个引用着色程序的对象,这个着色程序会自动销毁。
ShaderEffect::Load里最后一行代码是一个虚函数ShaderEffect::OnLoadPrograms的调用。这给予ShaderEffect继承类对象一个执行加载后操作的机会。最一般的操作是为用户自定义常量提供本地存储空间。例如,如果你看例子应用程序文件,
GeometricTools\WildMagic4\SampleGraphics\Iridescence\IridescenceEffect.cpp
你会发现IridescenceEffect类实现了这个虚函数:
void IridescenceEffect::OnLoadPrograms( int, Program* pkVProgram, Program* )
{
pkVProgram->GetUC(“InterpolateFactor”)->SetDataSource( m_afInterpolate );
}
第一个输入参数是绘制通道的索引。这个类实现一个单通效果,因此通道索引总是为0。它在这里不需要,因此根据ANSI标准,省略了形参名。第二个输入参数是我们会访问的顶点程序。最后一个输入参数是像素程序,但我们不会在这里访问,因此省略了形参。
IridescenceEffect类保存一个用户自定义常量叫m_afinterpolate。这个常量是Cg着色程序的其中一个输入,也就是,输入InterpolateFactor:
void IridescenceVProgram
(
in float4 kModelPosition : POSITION,
in float3 kModelNormal : NORMAL,
in float2 kInBaseTCoord : TEXCOORD0,
out float4 kClipPosition : POSITION,
out float2 kOutBaseTCoord : TEXCOORD0,
out float fOutInterpolateFactor : TEXCOORD1,
out float3 kWorldNormal : TEXCOORD2,
out float3 kEyeDirection : TEXCOORD3,
uniform float4x4 WVPMatrix,
uniform float4x4 WMatrix,
uniform float3 CameraWorldPosition,
uniform float InterpolateFactor )
{
// … code goes here …
}
Program::Load里的解析器在一个UserConstant对象里保存名字InterpolateFactor。ShaderEffect::OnLoadProgram函数为这个常量提供存储区,并且设置UserConstant对象里面的指针指向这个存储区。然而,IridescenceEffect::OnLoadPrograms函数重载了。它用名字InterpolateFactor查询UserConstant对象,并重新指定数据指针指向本地拷贝m_afInterpolate。现在每次你的应用程序代码需要改变用户自定义常量的值,你只需要通过成员存取器IridescenceEffect::SetInterpolate来修改m_afInterpolate。这比使SetInterpolate查询用户自定义常量,访问它的数据指针,并赋新值,要更有效率;就是说,查询是你需要避免的额外花费。
如果查询是效率低的,为什么从一开始就在Shader里为用户自定义常量提供存储区呢?IridescenceEffect类是开发期间创建的,因此是很容易在类里指定用户自定义常量。然而,对于程序生成着色程序与通过ShaderEffect(没有相关继承类给你操控)进行一般设置,你需要一些自动提供的存储区。继承ShaderEffect的类可能被认为是固定函数管线的一部分,但其实对于Wild Magic,是真的有可能通过ShaderEffect来处理所有着色程序而不是一些继承类。
3.4.5 校验着色程序
在之前的讨论里,我提到ShaderEffect::Load的第一和最后部分。第一部分加载着色程序,最后部分使用户自定义常量,纹理和图形与着色程序关联起来。我省略掉程序中间部分的讨论。它的工作是执行校验。有三个事情你需要校验:
1. 几何图元的顶点缓存数据必须包含顶点程序需要的输入顶点属性。
2. 顶点程序的输出属性必须匹配像素程序的输入属性。
3. 顶点和像素程序需求的资源不能超过渲染器的限制。
第一个校验发生在Renderer::LoadVBuffer,这个函数我在3.3章提到,当时没有提出校
验的问题。这个函数有代码块
ResourceIdentifier* pkId;
for( int i=0; i<=iPass; i++ )
{
pkId = pkVBuffer->GetIdentifier( this, i );
if( pkId )
{
if( rkIAttr.IsSubsetOf( *(Attributes*)pkId ) )
{
// found a compatible vertex buffer in video memory
return;
}
}
}
rkIAttr参数是一个对应顶点程序输入属性的Attributes对象。资源标识符安全地类型转换到一个Attribute对象。这个对象对应几何图元相关的顶点缓存的属性。Attributes::IsSubsetOf函数测试调用者,rkIAttr,是否拥有包含在函数输入参数里的属性。就是说,检查图元的顶点缓存是否拥有顶点程序要求的最少属性。如果拥有,那么顶点缓存会被用来加载顶点数据到显存。后面,LoadVBuffer里面有一行代码
OnLoadVBuffer( pkID, rkIAttr, pkVBuffer );
渲染器继承类会实现这个函数。它们调用
int iChannels;
float* afCompatible;
pkVBuffer->BuildCompatibleArray( rkIAttr, false, iChannels, afCompatible );
在调用这个的时候,我们知道着色程序的输入属性,rkIAttr,是顶点缓存的属性的子集。顶点缓存调用Attributes::BuildCompatibleArray,这个函数分配一个数组afCompatible,并且按照rkIAttr里的属性用数据填充。输入中的flase与颜色数据怎样打包有关:Direct3D要求将8位颜色通道打包为一个32位分量。OpenGL和软件渲染器直接保存浮点通道(在[0,1])。打包的数组然后由图形API处理,上传到显存,返回给你一个以后用来查找这个顶点缓存的资源标识符。
校验列表里面的第二和第三项是由ShaderEffect::Load执行。第二项的实现由代码块
std::string kDefault( “Default” );
const Attributes& rkVOAttr = spkVProgram->GetOutputAttributes();
const Attributes& rkPIAttr = spkPProgram->GetInputAttributes();
if( !rkVOAttr.Matches( rkPIAttr, false, true, true, true ) )
{
// the output attributes of the vertex program and the input attributes of the pixel
// program are incompatible. Use the default shader objects
if( spkVProgram->GetName() != kDefault )
{
m_kVShader[iPass] = WM4_NEW VertexShader( kDefault );
spkVProgram = VertexProgramCatalog::GetActive()->Find( kDefault );
}
if( spkPProgram->GetName() != kDefault )
{
m_kPShader[iPass] = WM4_NEW PixelShader( kDefault );
spkPProgram = PixelProgramCatalog::GetActive()->Find( kDefault );
}
}
Attributes::Matches函数对比顶点程序的输出属性,rkVOAttr,和像素程序的输入属性,rkPIAttr。函数的四个布尔输入指示你是否应该比较位置,法线,颜色或纹理。注意第一个布尔输入是false。顶点程序为顶点位置输出用在光栅器的裁剪空间坐标。像素程序不需要这个信息,因此Matches函数忽略了位置。然而,像素程序有可能关心法线,颜色和纹理坐标。
几何图元的顶点缓存和着色程序程序的输入之间的不匹配可以由图形引擎处理,但因为图形API的着色系统在光栅化和提供原料给像素程序期间不允许调停,所以不可能程序化处理顶点程序的输出和像素程序的输入之间的不匹配。有可能图形驱动会处理这个问题。无论如何,我处理不匹配的实现是忽略加载的着色程序,用默认的着色程序替代。
一旦你有一对兼容的顶点和像素程序,最后的校验步骤是确认是否有足够的资源存在。处理这个的代码块为
const Attributes& rkVIAttr = spkVProgram->GetInputAttributes();
if( rkVIAttr.GetMaxColors() > iMaxColors
|| rkVIAttr.GetMaxTCoords() > iMaxTCoords
|| rkVOAttr.GetMaxColors() > iMaxColors
|| rkVOAttr.GetMaxTCoords() > iMaxTCoords
|| rkPIAttr.GetMaxColors() > iMaxColors
|| rkPIAttr.GetMaxTCoords() > iMaxTCoords
|| spkVProgram->GetSIQuantity() > iMaxVShaderImages
|| spkPProgram->GetSIQuantity() > iMaxPShaderImages )
{
// the renderer cannot support the requested resources
if( spkVProgram->GetName() != kDefault )
{
m_kVShader[iPass] = WM4_NEW VertexShader( kDefault );
spkVProgram = VertexProgramCatalog::GetActive()->Find( kDefault );
}
if( spkPProgram->GetName() != kDefault )
{
m_kPShader[iPass] = WM4_NEW PixelShader( kDefault );
spkPProgram = PixelProgramCatalog::GetActive()->Find( kDefault );
}
}
这就是为什么当前起作用的渲染器的资源限制传递给ShaderEffect::Load函数的原因。着色程序的输入,输出,和采样器信息与渲染器能处理的限制做对比。如果任何资源超过了限制,加载的着色程序会被忽略,默认的着色程序会被使用。