前几天看了两个shader分别是聚光灯和法线贴图,于是想把这两个shader结合起来,产生手电照射潮湿的凹凸墙面效果:
本想很容易就能实现但是由于之前不理解光照模型的计算原理,所以我改起来相当费劲,经过努力终于弄明白了两者的原理,然后写出了shader:
struct Mtrl
{
float4 ambient;
float4 diffuse;
float4 spec;
float specPower;
};
struct DirLight
{
float4 ambient;
float4 diffuse;
float4 spec;
float3 posW;
float3 dirW;
};
uniform extern float4x4 gWorldInv;
uniform extern float4x4 gWVP;
uniform extern float4x4 gWorld; //世界矩阵
uniform extern Mtrl gMtrl;
uniform extern DirLight gLight;
uniform extern float3 gEyePosW;
uniform extern texture gTex;
uniform extern texture gNormalMap;
uniform extern float3 gAttenuation012; //光源衰减参数
uniform extern float gSpotPower; //光源聚合度,越大聚合越小
sampler TexS = sampler_state
{
Texture = <gTex>;
MinFilter = ANISOTROPIC;
MaxAnisotropy = 8;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
sampler NormalMapS = sampler_state
{
Texture = <gNormalMap>;
MinFilter = ANISOTROPIC;
MaxAnisotropy = 8;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
struct OutputVS
{
float4 posH : POSITION0;
float3 toEyeT : TEXCOORD0;
float3 lightDirT : TEXCOORD1;
float2 tex0 : TEXCOORD2;
float4 posW : TEXCOORD3;
};
OutputVS NormalMapVS(float3 posL : POSITION0,
float3 tangentL : TANGENT0,
float3 binormalL : BINORMAL0,
float3 normalL : NORMAL0,
float2 tex0 : TEXCOORD0)
{
// 初始化输出结构体对象
OutputVS outVS = (OutputVS)0;
//创建TBN坐标系,用来计算法线贴图
float3x3 TBN;
TBN[0] = tangentL;
TBN[1] = binormalL;
TBN[2] = normalL;
// 将顶点渲染器输入的参数 传入到TBN坐标系中
float3x3 toTangentSpace = transpose(TBN);
// 将摄像机的位置转换到本地坐标系中,只需乘上世界矩阵的转置逆矩阵就可以了
float3 eyePosL = mul(float4(gEyePosW, 1.0f), gWorldInv);
// 计算从顶点到摄像机的方向向量 并转换到TBN坐标系中
float3 toEyeL = eyePosL - posL;
outVS.toEyeT = mul(toEyeL, toTangentSpace);
// 将灯光的方向转换到本地坐标系后,再转换到TBN坐标系中
float3 lightDirL = mul(float4(gLight.dirW, 0.0f), gWorldInv).xyz;
outVS.lightDirT = mul(lightDirL, toTangentSpace);
// posH为经过世界、观察、投影等坐标转换的最终可以直接渲染的顶点位置,其他的则转换到世界坐标系中
outVS.posH = mul(float4(posL, 1.0f), gWVP);
outVS.posW = mul(float4(posL, 1.0f), gWorld);
gLight.posW = mul(float4(gLight.posW, 1.0f), gWorld); //将灯光坐标转换到世界坐标系
gLight.dirW = mul(float4(gLight.dirW, 1.0f), gWorld); //将灯方向转换到世界坐标系
// 放大uv贴图的坐标,因为该图片使用了WRAP寻址模式,所以放大的效果是加倍纹理的uv平铺
outVS.tex0 = tex0*3.f;
// 返回输出结构体,然后将里面的内容传入到下面的像素渲染器里继续处理!
return outVS;
}
float4 NormalMapPS(float3 toEyeT : TEXCOORD0,
float3 lightDirT : TEXCOORD1,
float2 tex0 : TEXCOORD2,
float4 posW :TEXCOORD3) : COLOR
{
// 单位化
toEyeT = normalize(toEyeT);
lightDirT = normalize(lightDirT);
// 灯光向量即为灯光方向的相反
float3 lightVecT = -lightDirT;
// 对法线纹理进行采样
float3 normalT = tex2D(NormalMapS, tex0);
// 将法线参数范围从[0, 1] 转换到[-1, 1]
normalT = 2.0f*normalT - 1.0f;
//单位化
normalT = normalize(normalT);
// 计算镜面光向量
float3 r = reflect(-lightVecT, normalT);
// 计算将有多少镜面光射入到人眼中
float t = pow(max(dot(r, toEyeT), 0.0f), gMtrl.specPower);
// 通过灯光向量和法线向量的夹角来决定漫反射光的强度
float s = max(dot(lightVecT, normalT), 0.0f);
// 如果漫反射强度过低则将镜面光置零
if(s <= 0.0f)
t = 0.0f;
// 计算三个系数 spec diffuse 随时变化 ambient 保证物体总是有一定的亮度
float3 spec = t*(gMtrl.spec*gLight.spec).rgb;
float3 diffuse = s*(gMtrl.diffuse*gLight.diffuse).rgb;
float3 ambient = gMtrl.ambient*gLight.ambient;
// 对原纹理进行采样
float4 texColor = tex2D(TexS, tex0);
//计算锥光源衰减参数
float d = distance(gLight.posW, posW.xyz);
float A = gAttenuation012.x + gAttenuation012.y*d + gAttenuation012.z*d*d;
float3 lightVector = normalize(gLight.posW - posW.xyz);
gLight.dirW = normalize(gLight.dirW);
// 计算聚光灯系数
float spot = pow(max(dot(-lightVector, gLight.dirW), 0.0f), gSpotPower);
//最终颜色
float3 color = spot*2.f*( ambient*texColor.rgb +diffuse*texColor.rgb /A+spec);
// 输出颜色
return float4(color, gMtrl.diffuse.a*texColor.a);
}
technique NormalMapTech
{
pass P0
{
vertexShader = compile vs_2_0 NormalMapVS();
pixelShader = compile ps_2_0 NormalMapPS();
}
}
一定要搞清各坐标系中的计算 不要弄混。对于shader的调试也很重要,SDK中可以做到,也可以借助renderMonkey,但是我还不熟,有空研究一下,这工具会非常的方便。