第十二章 使用Hight Level Shader Language(下)
添加镜面高光(Specular Highlights)
至今为止还有一种灯光没有讨论过,就是镜面高光。镜面高光让物体呈现出闪闪发亮的效果,同时也让物体看起来更加真实。虽然使用固定管道也能实现镜面高光,但这种计算是基于顶点的。这一节,我们将用编成管道来实现镜面高光。
使用上一章渲染茶壶的例子作为开始。保留之前实现漫射光的代码,这样可以对两种光照效果做一个对比。另外,显示一些文字告诉用户当前使用的灯光类型。添加字体变量:
private Direct3D.Font font = null;
自然,在创建了茶壶之后初始化这个变量:
font = new Microsoft.DirectX.Direct3D.Font(device,new System.Drawing.Font("Arial",12.0f));
好了,现在集中注意力来编写着色代码:
float4x4 WorldViewProj : WORLDVIEWPROJECTION;
float4x4 WorldMatrix : WORLD;
float4 DiffuseDirection;
float4 EyeLocation;
const float4 MatallicColor = {0.8f,0.8f,0.8f,1.0f};
const float4 AmbientColor = {0.05f,0.05f,0.05f,1.0f};
混合的世界、观察、投影矩阵将用于顶点变换。单独的世界矩阵用于法线位置的变换。这一次,我们不对漫射光方向硬编码,而是把它作为一个变量DiffuseDirection。最后一个变量表示观察点的位置。镜面高光是通过法线和观察点的位置来计算反射强度的。再看接下来的两个常量。由于镜面高光通常发生在金属材质表面,所以我们选择了一个类似于金属的颜色。至于最后的环境颜色在这里实际上是一个没用的量。之所以需要他只是为了满足数学公式的需要。
这个例子我们只关心每个顶点的位置和颜色:
struct VS_OUTPUT_PER_VERTEX
{
float4 pos : POSITION;
float4 diff : COLOR0;
};
在编写高光代码之前,先更新一下原来的漫射光着色器。每种光照类型将用独立的着色器来编写。更新代码:
VS_OUTPUT_PER_VERTEX TransformDiffuse(
float4 inputPos : POSITION,
float3 inputNormal : NORMAL,
uniform bool metallic
)
{
// Declare our return variable
VS_OUTPUT_PER_VERTEX Out = (VS_OUTPUT_PER_VERTEX)0;
// Transform our position
Out.pos = mul(inputPos, WorldViewProj);
// Transform the normal into the same coord system
float3 Normal = normalize(mul(inputNormal, WorldViewProj));
//make our diffuse color metallic for now
float4 diffuseColor = MatallicColor;
if(!metallic)
diffuseColor.rgb = sin(Normal + inputPosition);
//store our diffuse component
float4 diffuse = saturate(dot(DiffuseDirection,Normal));
//return the combined color
Out.Color = AmbientColor + diffuseColor * diffuse;
return Out;
}
这次添加了一个标记为“uniform”属性的布尔变量。Uniform属性告诉Direct3D在着色程序中把这个变量当作一个常量来使用,也就是说我们不能在着色过程中改变它的值。后面的代码都很简单,进行各种变换,把漫射颜色设置为先前订一的金属颜色。另外需要注意的是我们这次添加了一些流程控制语句。HLSL支持多种流程控制机制,包括if语句,do循环,while循环以及for循环。同时,这些流程控制语句的语法和C#几乎是一样的。
如果metallic变量为true,我们就保留金属颜色,如果不是,那么就把它换为一种动态颜色。最后,根据法线方向计算顶点颜色。由于这个着色器使用了2种类型的颜色,相应的添加两个techniques:
technique TransformDiffuseMetallic
{
pass P0
{
// shaders
VertexShader = compile vs_1_1 TransformDiffuse(true);
PixelShader = NULL;
}
}
technique TransformDiffuseColorful
{
pass P0
{
// shaders
VertexShader = compile vs_1_1 TransformDiffuse(false);
PixelShader = NULL;
}
}
这里两个techniques的区别只在于传递给着色器的参数值而已。另外还需要更新C#代码来使用新的technique:
effect.Technique = "TransformSpecularPerVertexMetallic";
接下来编写实现高光的代码:
VS_OUTPUT_PER_VERTEX TransformSpecular(
float4 inputPosition : POSITION,
float3 inputNormal : NORMAL,
uniform bool metallic
)
{
VS_OUTPUT_PER_VERTEX Out = (VS_OUTPUT_PER_VERTEX)0;
Out.Position = mul(inputPosition, WorldViewProj);
float3 Normal = normalize(mul(inputNormal, WorldMatrix));
float4 diffuseColor = MetallicColor;
float3 worldPosition = normalize(mul(inputPosition, WorldMatrix));
float3 eye = EyeLocation - worldPosition;
float3 normal = normalize(Normal);
float3 light = normalize(DiffuseDirection);
float3 eyeDirection = normalize(eye);
if(!metallic)
diffuseColor.rgb = cos(normal + eye);
float4 diffuse = saturate(dot(light, normal));
float3 reflection = normalize(2 * diffuse * normal - light);
float4 specular = pow(saturate(dot(reflection, eyeDirection)),8);
Out.Color = AmbientColor + diffuseColor * diffuse + specular;
return Out;
}
代码稍微有一点点多,我们来仔细看看。开始的部分和一前一样,对顶点和法线进行坐标变换,然后设置茶壶的漫射颜色。接下来的内容则是新的。首先,把每个顶点转换为世界坐标,接下来,用观察点位置减去这个制,获得从顶点指向观察点的矢量eye。接下来标准化所有矢量,把他们变换为单位长度。接下来检查bool变量的值,使用和刚才一样的公式,更新顶点颜色。之后,计算高光元素的值。计算高光的公式原理请参看SDK中的光照模型信息。最后,使用和之前一样的公式混合几种颜色。
同样编写相应的technique:
technique TransformSpecularPerVertexMetallic
{
pass P0
{
VertexShader = compile vs_1_1 TransformSpecular(true);
PixelShader = NULL;
}
}
technique TransformSpecularPerVertexColorful
{
pass P0
{
VertexShader = compile vs_1_1 TransformSpecular(false);
PixelShader = NULL;
}
}
使用这个新的technique:
effect.Technique = "TransformSpecularPerVertexMetallic";
现在运行程序看看已经可以看到闪闪发光的茶壶了。
基于像素的高光效果
你看,茶壶现在看起来比原来真是多了。但是,由于这种计算是基于顶点的,所以在茶壶曲面上造成了一种不平滑的效果。当然,由于我们只使用了顶点找色器,所以出现这种效果也是必然的。为了达到更真实的效果,让我们使用基于像素的方法来计算灯光。由于接下来的计算需要更多指令(instructions),我们必须保证显卡可以支持pixel shader 2.0。添加如下代码检查设备性能:
if (hardware.VertexShaderVersion >= new Version(1, 1) && (hardware.PixelShader1xMaxValue >= new Version(2,0)))
当然,我们同样需要一个vertex shader来变换顶点。
struct VS_OUTPUT_PER_VERTEX_PER_PIXEL
{
float4 Position : POSITION;
float3 LightDirection : TEXCOORD0;
float3 Normal : TEXCOORD1;
float3 EyeWorld : TEXCOORD2;
};
VS_OUTPUT_PER_VERTEX_PER_PIXEL Transform(
float4 inputPosition : POSITION,
float3 inputNormal : NORMAL
)
{
VS_OUTPUT_PER_VERTEX_PER_PIXEL Out = (VS_OUTPUT_PER_VERTEX_PER_PIXEL)0;
Out.Position = mul(inputPosition, WorldViewProj);
Out.LightDirection = DiffuseDirection;
Out.Normal = normalize(mul(inputNormal, WorldMatrix));
float3 worldPosition = normalize(mul(inputPosition, WorldMatrix));
Out.EyeWorld = EyeLocation - worldPosition;
return Out;
}
float4 ColorSpecular(
float3 lightDirection : TEXCOORD0,
float3 normal : TEXCOORD1,
float3 eye : TEXCOORD2,
uniform bool metallic) : COLOR0
{
float4 diffuseColor = MetallicColor;
if(!metallic)
diffuseColor.rgb = cos(normal + eye);
float3 normalized = normalize(normal);
float3 light = normalize(lightDirection);
float3 eyeDirection = normalize(eye);
float4 diffuse = saturate(dot(light, normalized));
float3 reflection = normalize(2 * diffuse * normalized - light);
float4 specular = pow(saturate(dot(reflection, eyeDirection)), 8);
return AmbientColor + diffuseColor * diffuse + specular;
};
technique TransformSpecularPerPixelMetallic
{
pass P0
{
// shaders
VertexShader = compile vs_1_1 Transform();
PixelShader = compile ps_2_0 ColorSpecular(true);
}
}
代码同样很简单,注意顶点变换时,把灯光方向,顶点法线,以及观察点位置都作为纹理来使用。再次运行程序看看吧,现在效果就好得多了。