Lets look at the changes in the shaders first. We added a constant buffer storing a diffuse color. This will be used by our pixel shader.
cbuffer psMain
{
float4 DiffuseColor;
};
In addition to the WorldViewProjection matrix we now also need the World matrix. We use this to transform the normal to world space in order to calculate the angle between the light direction and the normal.
cbuffer vsMain
{
row_major matrix World;
row_major matrix WorldViewProjection;
};
Our Vertex shader now expects a normal in addition to the position for each vertex. It will transform the normal and pass it along to the pixel shader for the lighting calculation.
When calculating the Normal in world space we are not interested in its translation. Our light is a directional light and just described by a direction vector without a position. We therefor have to transform our normal only by the rotation in order for both vectors to be at the origin. Fortunately, that is exactly what happens when you multiply a vector3 with a matrix4x4. The last component of the vector is implicitly zero which effectively cancels any translation component the matrix may have had.
struct VSInput
{
float4 Position : POSITION;
float3 Normal : NORMAL;
};
struct VSOutput
{
float4 Position : SV_POSITION;
float3 Normal : NORMAL;
};
VSOutput VertShader(VSInput input )
{
VSOutput Out;
Out.Position = mul( input.Position, WorldViewProjection );
Out.Normal = mul( input.Normal, World );
return Out;
}
In the pixel shader we hard code the light direction for now. This should really be a pixel shader constant in order to change the light dynamically. The amount of light for the pixel is dot product between the normal and the light direction. We multiply that value by our diffuse color constant to get our final color. Later we will add some more properties like specular and ambient color.
float4 PixShader( VSOutput In ) : SV_TARGET
{
float3 LightDirection = float3(0.0f, 0.0f, 1.0f);
return float4(DiffuseColor.rgb*(dot(In.Normal, -LightDirection)), DiffuseColor.a);
}
The Geometry, Material and ShaderConstants types now reside in their own files.
For Geometry we now need to specify that our vertices consist of a position and a normal. A vertex is now defined as:
#nowarn "9"
[<Struct; StructLayout(LayoutKind.Sequential)>]
type Vertex(position : Vector3, normal : Vector3) =
member this.Position = position
member this.Normal = normal
Note that when describing our layout, we have to be explicit about the offset of the normal in relation to the position. Since our structure layout is defined as sequential the normal will start immediately after the position.
let inputLayout =
let position = InputElement(
SemanticName = "POSITION",
Format = Format.R32G32B32_Float,
Classification = InputClassification.PerVertexData)
let normal = InputElement(
SemanticName = "NORMAL",
Format = Format.R32G32B32_Float,
AlignedByteOffset = sizeof,
Classification = InputClassification.PerVertexData)
new InputLayout(
device,
ShaderSignature.GetInputSignature(vsByteCode),
[|position; normal|])
Our previous example only had a single set of vertex shader constants that represented only the world view projection transformation in a single matrix. We now also have a second set of constants associated with the pixel shader. I have moved the VS constants to be updated together with the geometry while the PS constants are updated together with the material.
Since we now have more than one VS constant we need to specify the structure.
#nowarn "9"
[<Struct; StructLayout(LayoutKind.Sequential)>]
type Transforms(world : Matrix,
worldViewProjection : Matrix) =
member this.World = world
member this.WorldViewProjection = worldViewProjection
Our PS constants only contains the diffuse color for now.
#nowarn "9"
[<Struct; StructLayout(LayoutKind.Sequential)>]
type MaterialProperties(diffuseColor : Color4) =
member this.DiffuseColor = diffuseColor
In our main application we now have to specify the normals for each of the vertices in our triangle. We also give the material color when asking the renderer to create a drawable object.
let triangleVertices = [|
Vertex(Vector3(-1.0f,0.0f,0.0f), Vector3(0.0f,0.0f,-1.0f));
Vertex(Vector3(0.0f,1.0f,0.0f), Vector3(0.0f,0.0f,-1.0f));
Vertex(Vector3(1.0f, 0.0f, 0.0f), Vector3(0.0f,0.0f,-1.0f))|]
let green = new Color4(Red = 0.0f, Green = 1.0f, Blue = 0.0f, Alpha = 1.0f)
let triangle = renderer.CreateDrawable(triangleVertices, MaterialProperties(green))
The other interesting change in this iteration is our main loop. In the previous examples we relied on the Paint event to signal when we should draw. It turns out this event is only sent when the form thinks it should be updated. Usually only the first time the window is shown or if it is minimized and then maximized. To make our triangle move, we need to draw several times per second. Our main loop now looks like this:
let OnApplicationIdle _ =
while (Windows.AppStillIdle()) do
...
// as before
...
let rotationSpeed = float32(Math.PI/2.0) // per second
let elapsedSeconds = float32(timer.ElapsedMilliseconds)/1000.0f;
let world = Matrix.RotationY(rotationSpeed*elapsedSeconds)
let transforms = Transforms(world, world*view*projection)
renderer.Draw(triangle, transforms)
do Application.Idle.Add(OnApplicationIdle)
do Application.Run(form)
We only update while our application is 'idle' As soon as any windows event happens we yield control to our window. The Windows.AppStillIdle() is a native method that does a PeekMessage() to see if there is anything on the event queue. When the app is done handling the event we will receive another idle event and we continue spinning our green triangle.
No comments:
Post a Comment