21.处理Effect
首先还是强调请先看懂Shader系列4:材质和光源,接下去的大部分思路来自于这个示例。
对于普通effect应用XNA框架自带的BasicEffect类就够了,但我想使用自己的Effect实现BasicEffect,如:漫反射、镜面高光、雾化等,而且还要实现BasicEffect不能处理的功能,如:点光源、聚光灯、法线映射等。因此首先在引擎类StunEngine.cs中添加以下变量及其对应的属性:
////// 使用Generic.fx effect文件的Effect /// private Effect baseEffect; ////// 获取使用Generic.fx effect文件的Effect /// public Effect BaseEffect { get { return baseEffect; } }
并在Initialize方法中初始化这个Effect:
baseEffect = Content.Load("Effects\\Generic");
对应的effect文件是Generic.fx,为了让代码分离易于使用,Generic.fx是由三部分组成的,分别是主体文件Generic.fx:
#include "Standard.inc"
#include "Lights.inc"
//-----------------------------------------------------------------------------------------------
// Technique: TexturedLights
//
// 作者: E. Riba (Riki) 08.08.2009
//
// 描述:
// VS: 传入位置,法线和纹理坐标
// 注意:顶点颜色不会被保留
//
// PS: 漫反射纹理采样,
// 细节纹理采样
// 单向光计算
// 点光源计算
// 聚光灯光源计算
//
//-----------------------------------------------------------------------------------------------
// TexturedLights使用的顶点着色器的输入结构
struct TexturedLightsVSIn
{
float4 Position : POSITION0;
float3 Normal : NORMAL;
float2 TexCoords : TEXCOORD0;
};
// TexturedLights使用的顶点着色器的输出结构
struct TexturedLightsVSOut
{
float4 Position : POSITION0;
float2 TexCoords : TEXCOORD0;
float3 WorldNormal : TEXCOORD1;
float3 WorldPosition : TEXCOORD2;
};
TexturedLightsVSOut TexturedLightsVS(TexturedLightsVSIn input)
{
TexturedLightsVSOut Output = (TexturedLightsVSOut)0;
//生成world-view-projection矩阵
float4 worldPosition = mul(input.Position, gWorld);
float4x4 wvp = mul(mul(gWorld, gView), gProjection);
// 转换输入顶点的位置、法线
Output.Position = mul(input.Position, wvp);
Output.WorldPosition = worldPosition / worldPosition.w;
Output.WorldNormal = mul(input.Normal, gWorld);
Output.TexCoords = input.TexCoords;
//返回输出结构
return Output;
}
float4 TexturedLightsPS(TexturedLightsVSOut input) : COLOR
{
float3 finalColor = 0;
float4 diffuseColor = 0;
// 漫反射颜色来自于漫反射纹理或材质,首先将漫反射颜色设置为材质的漫反射颜色。
diffuseColor = float4(gMaterialDiffuse, 1.0);
//如果包含纹理,则gDiffuseEnabled为true,漫反射颜色取自纹理的颜色
if(gDiffuseEnabled)
{
float4 textureColor = tex2D(textureSampler, input.TexCoords* gDiffuseUVTile);
diffuseColor = textureColor;
}
//而细节颜色只取自细节纹理
if(gDetailUVTile.x > 0&&gDetailUVTile.y>0)
{
float4 detail = tex2D(detailSampler, input.TexCoords * gDetailUVTile);
//将细节颜色调制到漫反射颜色上
diffuseColor.rgb *= detail.rgb;
}
//归一化法线
float3 N = normalize(input.WorldNormal);
//----------------------------
// 遍历所有光源
//----------------------------
for(int i=0; i < gTotalLights; i++)
{
//只处理可用的光源
if(gLights[i].enabled)
{
finalColor+= CalculateSingleLight(gLights[i], input.WorldPosition, N,diffuseColor);
}
}
//----------------------------------------
// 添加环境光颜色和自发光颜色
//----------------------------------------
finalColor += (gAmbient * diffuseColor.rgb);
finalColor += gMaterialEmissive;
//---------------------------------------------------------------------------------
// 如果打开雾化,则计算雾化颜色,基于发表在Xna社区论坛的John Watts公式
//---------------------------------------------------------------------------------
if(gFogEnabled)
finalColor = LinearFog(finalColor, input.WorldPosition);
return float4(finalColor, diffuseColor.a);
}
technique TexturedLights
{
pass Pass0
{
vertexshader = compile vs_3_0 TexturedLightsVS();
pixelshader = compile ps_3_0 TexturedLightsPS();
}
}
//--------------------------------------------
// technique:TexturedLights的结尾
//--------------------------------------------
//-----------------------------------------------------------------------------------------------
// Technique: SimpleTextured
//
// 作者: E. Riba (Riki) 13.02.2009
//
// 描述: VS: 传入(position,uv)
// PS: 采样颜色纹理 + 采样细节纹理(如果enabled)
//-----------------------------------------------------------------------------------------------
// SimpleTextured使用的顶点着色器输入结构
struct SimpleTexturedVSIn
{
float4 Position : POSITION0;
float2 TexCoords : TEXCOORD0;
};
// SimpleTextured使用的顶点着色器输出结构
struct SimpleTexturedVSOut
{
float4 Position : POSITION0;
float2 TexCoords : TEXCOORD0;
float3 WorldPosition : TEXCOORD1;
};
SimpleTexturedVSOut SimpleTexturedVS(SimpleTexturedVSIn input)
{
SimpleTexturedVSOut Output=(SimpleTexturedVSOut)0;
//将顶点坐标转换到世界坐标系
float4x4 viewProjection = mul (gView, gProjection);
float4x4 worldViewProjection = mul (gWorld, viewProjection);
Output.Position = mul(input.Position, worldViewProjection);
//纹理坐标直接输出
Output.TexCoords = input.TexCoords;
//计算顶点在世界空间中的位置
float4 worldPosition = mul(input.Position, gWorld);
Output.WorldPosition = worldPosition / worldPosition.w;
return Output;
}
float4 SimpleTexturedPS(SimpleTexturedVSOut input) : COLOR0
{
float4 color;
// 漫反射颜色来自于漫反射纹理或材质,首先将漫反射颜色设置为材质的漫反射颜色。
color = float4(gMaterialDiffuse, 1.0);
// 如果包含纹理,则gDiffuseEnabled为true,漫反射颜色取自纹理的颜色
if(gDiffuseEnabled)
color = tex2D(textureSampler, input.TexCoords* gDiffuseUVTile);
//如果细节纹理平铺因子不为零,则采样细节纹理,并将细节纹理的颜色调制到最终颜色中
if(gDetailUVTile.x > 0&&gDetailUVTile.y>0)
{
float4 detail = tex2D(detailSampler, input.TexCoords * gDetailUVTile);
color.rgb *= detail;
}
// 如果打开雾化,则计算雾化颜色
if(gFogEnabled)
color.rgb = LinearFog(color.rgb, input.WorldPosition);
return color;
}
technique SimpleTextured
{
pass Pass0
{
vertexshader = compile vs_3_0 SimpleTexturedVS();
pixelshader = compile ps_3_0 SimpleTexturedPS();
}
}
//--------------------------------------------
// technique: SimpleTextured的结尾
//--------------------------------------------
和两个include文件,其中standard.inc文件包含最常用的一些effect参数:
//----------------------------------------------------
// 共享变量,下列变量会被缓存到EffectPool中。
//----------------------------------------------------
shared uniform extern float4x4 gProjection : PROJECTION; //投影矩阵
shared uniform extern float4 gFogColor = {0.1, 0.2, 0.3, 1.0}; // 雾化颜色
shared uniform extern float gFogStart = 50; // 雾化的起始距离
shared uniform extern float gFogEnd = 400; // 雾化的结束距离
shared uniform extern float gTime; // 时间
//----------------------------------------------------
// 下列变量在每一帧都要被设置
//----------------------------------------------------
uniform extern float4x4 gView : VIEW; // 视矩阵
uniform extern float4x4 gWorld : WORLD; // 世界矩阵
uniform extern float3 gCameraPos; // 相机位置
uniform extern bool gDiffuseEnabled; // 是否从纹理采样漫反射颜色
uniform extern texture gDiffuseTexture; // 漫反射纹理,覆盖在整个模型上
uniform extern float2 gDiffuseUVTile; // 漫反射纹理的平铺次数
uniform extern texture gDetailTexture; // 细节(噪点)纹理,平铺在模型上
uniform extern float2 gDetailUVTile; // 细节纹理平铺次数
uniform extern bool gFogEnabled; // 如果设为true,则计算雾化
// 漫反射颜色纹理采样器
sampler2D textureSampler = sampler_state
{
Texture =
;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = mirror;
AddressV = mirror;
};
// 细节纹理采样器
sampler2D detailSampler = sampler_state
{
Texture = ;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = wrap;
AddressV = wrap;
};
//-----------------------------------------------------------------------------------------------
// 函数: LinearFog
//
// 作者: E. Riba (Riki) 19.02.2009
//
// 描述: 计算线性雾化颜色
//
// Shared参数: gFogStart,gFogEnd,gFogColor
//
//-----------------------------------------------------------------------------------------------
float3 LinearFog(float3 color, float3 worldPosition)
{
float d = length(worldPosition - gCameraPos);
float l = clamp((d - gFogStart) / (gFogEnd - gFogStart), 0, 1);
color.rgb = lerp(color, gFogColor.rgb, l);
return color;
}
而Light.inc包含与光照相关的effect参数和计算光照的函数:
#define MAX_TOTAL_LIGHTS 8
// 光源类型
#define DIR_LIGHT 0
#define POINT_LIGHT 1
#define SPOT_LIGHT 2
#define NO_COLOR float4(0,0,0,0);
//----------------------------------------------------
// 共享变量
//----------------------------------------------------
shared uniform extern int gTotalLights; // 光源数量
shared uniform extern float3 gAmbient; // 环境光颜色
//----------------------------------------------------
// 材质参数
//----------------------------------------------------
uniform extern float3 gMaterialDiffuse; // 材质漫反射颜色
uniform extern float3 gMaterialSpecular; // 材质镜面高光颜色
uniform extern float gMaterialSpecPower; // 材质镜面高光强度
uniform extern float3 gMaterialEmissive; // 材质自发光颜色
// 包含光源数据的结构
struct Light
{
float enabled; //光源是否打开
float lightType; //光源类型
float3 color; //光源颜色
float3 position; //光源位置
float3 direction; //光线方向
float4 spotData; //四个分量分别保存range,falloff,theta,phi数据
};
//光源数组
shared Light gLights[MAX_TOTAL_LIGHTS];
//-----------------------------------------------------------------------------------------------
// 函数: Attenuation
//
// 作者: E. Riba (Riki) 12.08.2009
//
// 描述: 计算随距离增大而减小的衰减值,例如lRange为10,falloff为1,则距离光源为5的顶点的光照只有一半
//
//-----------------------------------------------------------------------------------------------
float Attenuation(float lDistance, float lRange, float falloff)
{
float att = pow(saturate((lRange - lDistance) / lRange), falloff);
// 以下为简化计算方程
//float att = 1 - saturate(lDistance*falloff / lRange);
return att;
}
//-----------------------------------------------------------------------------------------------
// 函数: Lambertian因子
//
// 作者: E. Riba (Riki) 07.08.2009
//
// 描述: 基本的lambertian光照方程
//
//-----------------------------------------------------------------------------------------------
float3 LambertianComponent(in float3 directionToLight, in float3 worldNormal, in float3 lightColor)
{
float3 NdotL = saturate(dot(worldNormal,directionToLight));
return NdotL * lightColor;
}
//-----------------------------------------------------------------------------------------------
// 函数: Specular分量
//
// 作者: E. Riba (Riki) 08.08.2009
//
// 描述: 基于Phong公式的镜面高光公式
//
//-----------------------------------------------------------------------------------------------
float3 SpecularComponent(in float3 directionToLight, in float3 worldNormal, in float3 worldPosition, in float3 lightColor, in float3 specularColor, in float specularPower)
{
float3 specular = 0;
if(length(specularColor)>0)
{
// 计算反射光方向
float3 reflectionVector = normalize(reflect(-directionToLight, worldNormal));
// 计算像素指向相机的向量,即视线反方向
float3 directionToCamera = normalize(gCameraPos - worldPosition);
// 计算视线反方向和反射光方向的夹角
float VdotR = saturate(dot(directionToCamera,reflectionVector));
// 计算镜面高光颜色
specular = lightColor * specularColor *pow(VdotR,specularPower);
specular = saturate(specular);
}
return specular;
}
//-----------------------------------------------------------------------------------------------
// 函数: BlinnPhongSpecular
//
// 作者: E. Riba (Riki) 12.08.2009
//
// 描述: 基于Bilnn Phong公式的镜面高光公式
//
//-----------------------------------------------------------------------------------------------
float3 BlinnPhongSpecular( in float3 directionToLight, in float3 worldNormal, in float3 worldPosition, in float3 lightColor, in float3 specularColor, in float3 specularPower)
{
float3 specular = 0;
if(length(specularColor)>0)
{
//计算像素指向相机的向量,即视线反方向
float3 viewer = normalize(gCameraPos - worldPosition);
// 计算入射光反方向和视线反方向之间的角平分线方向
float3 half_vector = normalize(directionToLight + viewer);
// 计算角平分线与法线方向的夹角
float NdotH = saturate(dot( worldNormal, half_vector));
// 计算镜面高光颜色
specular = lightColor * specularColor * pow( NdotH, specularPower) ;
specular = saturate(specular);
}
return specular;
}
//////////////////////////////////////////////////////////////
// 这个函数计算一个像素上的一个光源的effect //
// 它要考虑镜面高光颜色、漫反射颜色还有材质属性 //
// 这个函数计算Lambertian漫反射光照和Phong镜面高光 //
// 并返回它们的和。 //
//////////////////////////////////////////////////////////////
float3 CalculateSingleLight(Light light, float3 worldPosition, float3 worldNormal, float3 diffuseColor,float3 specularColor,float specularPower)
{
//获取当前光源的颜色、光源类型
float3 lightColor = light.color;
float lightType = light.lightType;
//将衰减值初始化为0
float attenuation = 0;
//顶点指向光源的方向,即入射光线反方向
float3 directionToLight ;
//单向光源的衰减设为1即不衰减
if(lightType == DIR_LIGHT)
{
directionToLight = -normalize(light.direction);
attenuation = 1;
}
//如果不是单向光源,则获取光源的range,falloff,theta,phi
else
{
float range =light.spotData.x; // 光源作用距离
float falloff = light.spotData.y; // 聚光灯内外光锥之间的衰减或点光源距离衰减
float cosTheta = cos(light.spotData.z); // 内光锥弧度的余弦值
float cosPhi = cos(light.spotData.w); // 外光锥弧度的余弦值
//计算光源与当前顶点的距离
float lightDist = distance(light.position, worldPosition);
directionToLight = normalize(light.position - worldPosition);
//如果是点光源且顶点位于光源作用范围之内,则计算距离衰减值attenuation
if( (lightType == POINT_LIGHT) && (range > lightDist) )
{
attenuation = Attenuation(lightDist, range, falloff);
}
//如果是聚光灯光源,则首先判断顶点是否在光照范围之内
else if( lightType == SPOT_LIGHT)
{
//如果在光照范围之内
if(lightDist < range)
{
//首先计算距离衰减
float fOuterAtten = Attenuation(lightDist, range, falloff);
// 计算当前光源指向顶点方向和聚光灯光线方向的夹角Alpha的余弦值
float cosAlpha = saturate(dot(-directionToLight, normalize(light.direction)));
// 计算聚光灯内外光锥间的衰减因子
float fSpotAtten = 0.0f;
//如果夹角Alpha小于内光锥顶角,即cosAlpha大于cosTheta,则不衰减,衰减因子为1
if( cosAlpha > cosTheta)
fSpotAtten = 1.0f;
//如果夹角Alpha大于内光锥顶角小于外光锥顶角,则需要根据falloff指数计算衰减因子
else if( cosAlpha > cosPhi)
fSpotAtten = pow((cosAlpha - cosPhi)/(cosTheta - cosPhi), falloff);
//将距离衰减因子乘以光锥间衰减因子得到最终的衰减值
attenuation = fOuterAtten * fSpotAtten;
}
else
{
attenuation = 0;
}
}
}
//计算漫反射光照分量
float3 Lc = LambertianComponent(directionToLight, worldNormal, lightColor);
float3 Sc = SpecularComponent(directionToLight, worldNormal, worldPosition, lightColor, specularColor, specularPower);
//使用BlinnPhone光照公式计算镜面高光分量
//float3 Sc = BlinnPhongSpecular(directionToLight, worldNormal, worldPosition, lightColor, specularColor, specularPower);
return (Lc + Sc) * (attenuation * diffuseColor.rgb);
}
关于include文件
include文件虽然是Game Studio项目文件的一部分,但它不是content项目的一部分。因为它被effect引用,所以会自动被生成,所以无需将它复制到content项目中。我将它们包含在content项目中是为了可以容易地找到这个文件进行编辑,但是在属性面板中要将“生成操作”设置为“无”,不然通不过shader的编译。include文件的后缀其实可以取任何名称,有时我将后缀改成.fx,这样我的shader高亮插件NShader(http://nshader.codeplex.com/)可以发挥作用,让调试更容易。
关于输入和输出结构
如果你在Visual Studio中添加一个Effect文件,它默认会有以下两个顶点着色器输入和顶点着色器输出结构:
struct VertexShaderInput
{
float4 Position : POSITION0;
// TODO: add input channels such as texture
// coordinates and vertex colors here.
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// TODO: add vertex shader outputs such as colors and texture
// coordinates here. These values will automatically be interpolated
// over the triangle, and provided as input to your pixel shader.
};
这不是必须的,例如:如果你使用VertexShaderInput,则代码可以写成:
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
......
}
如果不用VertexShaderInput,也可以写成:
VertexShaderOutput VertexShaderFunction(float3 position : POSITION0)
{
......
}
在我的effect文件中使用了诸如TexturedLightsVSIn、TexturedLightsVSOut等的结构,虽然对于简单的shader来说这样做有点啰嗦,但为了代码可读性,建议使用结构。
关于uniform extern
你能看到许多参数之前都有uniform extern的前缀,有些参数之前还有shared,它们的意义可见下表:
| 前缀 | 描述 |
| static | static用于全局变量,表示值为一个内部变量,仅能被当前shader中的代码访问。用于局部变量则表示这个值将在多次调用间驻留。静态变量只能进行一次初始化操作,如果没有使用特定值进行初始化,则默认为0。 |
| uniform | 使用uniform声明的全局变量表示对整个shader来说,它是一个统一的输入值,即不能在shader运行期间改变。所有非静态全局变量都被认为是统一的。 |
| extern | 使用extern声明的全局变量表示对shader来说,它是一个外部输入的值。所有非静态全局变量都被认为是外部的。 |
| volatile | 这个关键字提示编译器变量将频繁改变。 |
| shared | 这个关键字用来告诉编译器这个值将被多个effect共享。 |
| const | 声明为const的变量表示在初始化之后,就不能改变它的值。 |
表中说到非静态全局变量默认是uniform和extern的,所以你不在参数前加uniform和extern也可以,我加上的目的是为了看起来清楚点。
关于effect参数的设置
我学习Shader系列4:材质和光源最大的收获是知道了原来无需每帧都设置每个effect参数,以前看到的所有代码都是在Draw方法中对每个参数进行设置,但是很多参数是不会频繁改变的,这样做会拖慢程序。图像设备就像一个状态机,每个状态都有一个初始值,状态设置后就不会发生改变,直到设置一个新的状态。
因此我是这样设置effect参数的:
Generic.fx共有23个参数,其中世界矩阵gWorld,视矩阵gView,相机位置gCameraPos变动频繁,所以在材质类的SetEffectParameters方法中调用,每帧都要设置。
共享参数投影矩阵gProjection,时间gTime在SceneManager类中的Draw方法中调用,每帧都要设置。
共享参数环境光颜色gAmbient,雾化颜色gFogColor,雾化开始距离gFogStart,雾化结束距离gFogEnd 在Scene类的相应属性中设置,光源总数gTotalLights在Scene类的AddLight和RemoveLightAt方法中设置。
共享的光源数组gLights的各个变量在Light类中设置。
有关材质的自发光颜色gMaterialEmissive,漫反射颜色gMaterialDiffuse,镜面反射颜色gMaterialSpecular,镜面反射强度gMaterialSpecPower,开启漫反射颜色gDiffuseEnabled,漫反射纹理gDiffuseTexture,漫反射纹理平铺次数gDiffuseUVTile,细节纹理gDetailTexture,细节纹理平铺次数gDetailUVTile,是否开启雾化gFogEnabled在材质类GenericMaterial中设置。
还有两个参数漫反射纹理采样器textureSampler,细节纹理采样器detailSampler无需设置。
Generic.fx代码的具体解释请见下一个教程。
文件下载(已下载 1466 次)发布时间:2010/4/21 下午2:07:52 阅读次数:7320
