Problem
With the light you configured, BasicEffect can draw scenarios well. However, if you want to define some cool effects, the first thing you need to achieve is correct lighting.
In this tutorial, you will learn how to write a basic HLSL effect to implement vertex-by-vertex illumination.
Solution
Transmits the 3D position and normal of each vertex to effect. The vertex shader on the video card needs to do two things for each vertex.
First, when drawing a 3D world, you always need to use the World matrix. The view matrix and projection matrix convert the 3D position to the corresponding 2D screen coordinates.
Second, calculate the illumination intensity of the vertex through the cross-multiplication of the light direction and the normal direction.
Working Principle
First, you must define the vertex in the XNA project. Obviously, you need to store 3D positions in each vertex. To calculate the correct illumination in vertex shader, you also need to provide the normal for each vertex. See tutorial 6-1 to understand the concept of the normal.
You can use the same code in tutorial 6-1 to create six vertices that contain a 3D position and a normal (also contain texture coordinates, but you do not use them here.
Create a new. fx file in the XNA project and add the following code. It contains HLSL variables that can be changed from the XNA application.
float4x4 xWorld; float4x4 xView; float4x4 xProjection; float xAmbient; float3 xLightDirection;
When converting a 3D coordinate to a 2D screen coordinate, you always need to view the matrix and projection matrix (see tutorial 2-1 ). Because you want to move objects in the scene, you also need a world matrix (see tutorial 4-2 ). Because this teaching material deals with light, you need to define the direction of light. The Ambient variable allows you to set the minimum illumination level so that an object is invisible even if it is not directly illuminated by a light source.
Before entering vertex shader and pixel shader, you must first define the output structure. First, the output of vertex shader is the input of pixel shader, And the 2D screen coordinates of each vertex must be saved. Second, vertex shader also calculates the illumination intensity of each vertex.
Between vertex shader and pixel shader, these values are interpolated so that each pixel can obtain their own interpolation.
Pixel shader only calculates the final color of each pixel.
struct VSVertexToPixel { float4 Position : POSITION; float LightingFactor : TEXCOORD0; };struct VSPixelToFrame { float4 Color : COLOR0; };Vertex Shader
Vertex shader combines the World, View, and Projection Matrices Into a matrix to convert 3D coordinates to 2D screen coordinates.
Given the light direction and normal direction, vertex shader can calculate the Light Intensity Based on Figure 6-7. The smaller the angle between the light and the regular line, the more intense the light, the larger the angle, the less light.
You can obtain this value through dot multiplication. Returns a value between 0 and 1 (if the length of both vectors is 1 ).
Figure 6-7 point multiplication in the light direction and normal direction
However, when calculating the multiplication of two vertices, you must first reverse one of the two directions. Otherwise, the two directions are the opposite. For example, in Figure 6-7, you find that the normal is in the opposite direction of the light, which leads to a negative result of point multiplication.
VSVertexToPixel VSVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0) { VSVertexToPixel Output = (VSVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); float3 normal = normalize(inNormal); Output.LightFactor = dot(rotNormal, -xLightDirection); return Output; }
The result of point multiplication is a single value, based on the angle and length of two normal. In most cases, you only need light based on the angle between the two. This means that you need to ensure that all the normal and light direction lengths in the 3D space are the same; otherwise, vertices with longer normal will get more light.
This can be done by setting the length of all the normal to 1, that is, the normal needs to be normalized.
Note:Normalizing does not mean that the normal is not operated, but the length of a vector is changed to 1. See tutorial 6-1.
Ensure correct illumination when using world Matrix
The previous code works well when the world matrix is a matrix of units, that is, the object needs to be placed in the initial position of (0, 0, 0) 3D space (see tutorial 5-2 ).
But in most cases, you want to use another world matrix so that you can move, rotate, or scale objects.
As shown in Figure 6-1, if you rotate an object, the normal will also rotate along with it. This means that the normal needs to be transformed by the rotation amount in the world matrix.
The scaling operation in the world matrix does not affect the illumination calculation. You always need to normalize the normal in vertex shader to change the vector length to 1.
However, if the world matrix contains translation, you may encounter problems. This is because the normal is a vector with a maximum length of 1. For example, when you use a matrix that contains more than two units to transform the normal, all the normal will point to that direction.
As shown in 6-8, when an object uses a world matrix that contains a certain distance to translate to the right, the vertex position moves to the right. The normal will also move to the right side according to the world matrix, but their direction should remain unchanged. Therefore, when using the world matrix to transform the normal, you need to strip the translation part of the world matrix.
Figure 6-8 normal that is affected by translation in the world Matrix
A matrix is a table containing 4x4 numbers. You should only use the rotating part of the world matrix to transform the normal, instead of the moving part. You can extract the rotating part of the matrix, which is located in the 3 × 3 number on the top left. Simply transform the 4x4 World matrix into a 3x3 matrix, you can get only the rotation information, which is exactly what you need! Use this matrix to rotate the normal. The Code is as follows:
float3 normal = normalize(inNormal); float3x3 rotMatrix = (float3x3)xWorld; float3 rotNormal = mul(normal, rotMatrix); Output.LightFactor = dot(rotNormal, -xLightDirection);
Pixel Shader
First, vertex shader processes the three vertices of a triangle and calculates the illumination value. Then, for each pixel in the triangle, the illumination value is interpolated between three vertices. The interpolation illumination value is passed to pixel shader.
In this simple example, the blue color is the basic color of the object. To add a shading effect to a triangle, multiply the color by LightFactor (calculated in the previous vertex shader) and ambient light (set by the XNA program through the xAmbient variable ). The ambient factor ensures that all objects are not completely dark, while the LightFactor applies the corresponding light to the light direction:
VSPixelToFrame VSPixelShader(VSVertexToPixel PSIn) : COLOR0 { VSPixelToFrame Output = (VSPixelToFrame)0; float4 baseColor = float4(0,0,1,1); Output.Color = baseColor*(PSIn.LightFactor+xAmbient); return Output; }Define technique
Finally, define technique:
technique VertexShading { pass Pass0 { VertexShader = compile vs_2_0 VSVertexShader(); PixelShader = compile ps_2_0 VSPixelShader(); }}XNA code
In the XNA project, import the HLSL file and store it in an Effect variable, which is similar to texture operations in tutorial 3-1. In this example, the HLSL file name is vertexshading. fx:
effect = content.Load<Effect>("vertexshading");
When drawing an object, you must first set the effect parameter, which requires BasicEffect:
effect.CurrentTechnique = effect.Techniques["VertexShading"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xLightDirection"].SetValue(new Vector3(1, 0, 0)); effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); }effect.End();Code
XNA Code draws multiple instances of an object. Because different world matrices are used, these objects are drawn in different locations.
The final result is the same as the tutorial 6-1, but this time you used your HLSL effect:
effect.CurrentTechnique = effect.Techniques["VertexShading"]; effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xLightDirection"].SetValue(new Vector3(1, 0, 0)); effect.Parameters["xAmbient"].SetValue(0.0f); for (int i = 0; i < 9; i++) { Matrix world = Matrix.CreateTranslation(4, 0, 0) * Matrix.CreateRotationZ((float)i * MathHelper.PiOver2 / 8.0f); effect.Parameters["xWorld"].SetValue(world); effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionNormalTexture> (PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); } effect.End(); }
The complete content of the. fx file is as follows:
float4x4 xWorld; float4x4 xView; float4x4 xProjection; float xAmbient; float3 xLightDirection; struct VSVertexToPixel { float4 Position : POSITION; float LightFactor : TEXCOORD0; }; struct VSPixelToFrame { float4 Color : COLOR0; }// Technique: VertexShading VSVertexToPixel VSVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0) { VSVertexToPixel Output = (VSVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); float3 normal = normalize(inNormal); float3x3 rotMatrix = (float3x3)xWorld; float3 rotNormal = mul(normal, rotMatrix); Output.LightFactor = dot(rotNormal, -xLightDirection); return Output; }VSPixelToFrame VSPixelShader(VSVertexToPixel PSIn) : COLOR0 { VSPixelToFrame Output = (VSPixelToFrame)0; float4 baseColor = float4(0,0,1,1); Output.Color = baseColor*(PSIn.LightFactor+xAmbient); return Output; }technique VertexShading { pass Pass0 { VertexShader = compile vs_2_0 VSVertexShader(); PixelShader = compile ps_2_0 VSPixelShader(); }}