Summary
This article explains key differences between shaders and effects and describes how to implement high-level “effect” functionality and why you might want to.
If you just want the code:
- Follow the procedure to setup the HLSL Build Task with Visual Studio Integration
- Download the Babylon Engine and look at the Effect class
Named Parameters vs. Registers
Silverlight provides pixel and vertex shader support in the core runtime instead of a high-level “effect” class. The shader model requires that you carefully assign constant parameters to specific registers in your HLSL so that you can set them from your application code. Consider the following HLSL.
1 2 3 4 5 6 7 8 |
// transform object vertices to world-space float4x4 WorldMatrix : register(c0); // transform object normals, tangents, & binormals to world-space float4x4 WorldInverseTransposeMatrix : register(c4); // transform object vertices to view space and project them in perspective float4x4 WorldViewProjectionMatrix : register(c8); |
By assigning specific registers to these variables, the constants are guaranteed to be in the specified positions so that they can be set from the application side.
1 |
device.SetVertexShaderConstantFloat4(8, ref WorldViewProjectionMatrix); |
Wouldn’t it be nice if you didn’t have to manually allocate and track registers and could just refer to the parameter name “WorldViewProjectionMatrix” in your application?
1 |
effect.SetConstant("WorldViewProjectionMatrix", WorldViewProjectionMatrix); |
Your HLSL could then look like this and the compiler could allocate registers as it sees fit:
1 2 3 4 5 6 7 8 |
// transform object vertices to world-space float4x4 WorldMatrix; // transform object normals, tangents, & binormals to world-space float4x4 WorldInverseTransposeMatrix; // transform object vertices to view space and project them in perspective float4x4 WorldViewProjectionMatrix; |
In order to make this work, you would need information about how the compiler is allocating registers to constant parameters. As it turns out this information can be obtained at compile-time from the shader compiler. However, Silverlight doesn’t ship a shader compiler with the runtime or the install size would increase considerably. A solution to this dilemma is to extract the information at compile-time into a format your application can read and then embed this as a resource alongside your compiled shader.
Export the Named Parameter to Register Map
For the first part of the process we need to do the following:
- Compile HLSL to shader byte code
- Extract the constants table during compilation and write it to an application readable format
- Embed the shader byte code and the constants table file as resources in an assembly
Conveniently, I posted the previous article on the HLSL Build Task with Visual Studio Integration which does all of those. The constant table is exported to an XML file which looks like the following.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <ShaderConstants FileFormatVersion="1.0" Version="2.0" Constants="5" Creator="Microsoft (R) HLSL Shader Compiler 9.29.952.3111"> <Constant Index="0" Descriptions="1"> <Description> <Name>TotalSeconds</Name> <RegisterSet>Float4</RegisterSet> <RegisterIndex>16</RegisterIndex> <RegisterCount>1</RegisterCount> <Rows>1</Rows> <Columns>1</Columns> <Elements>1</Elements> <StructMembers>0</StructMembers> <Bytes>4</Bytes> <Class>Scalar</Class> <Type>Float</Type> </Description> </Constant> … |
This XML can be easily parsed by application code to determine which registers were allocated to a specific constant, and that’s what the rest of the article is about.
Effect Class
We’re going to create a new class to do the following:
- Load a vertex and pixel shader pair from an assembly – Implementing an effect typically means configuring both a vertex and pixel shader which collaborate to produce the final result, so it makes sense to group these together under a single “effect”.
- Load shader byte code
- Load shader constants map
- Set shader constants by name
- Configure the graphics device to use the loaded shaders
David Catuhe did exactly this for the Effect class in his Babylon Engine, and so I’m going to walk through what that looks like.
Create the Effect and Load the Shaders
This constructor loads the shader byte code from the named assembly and loads the constants XML file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public Effect(GraphicsDevice device, string assemblyName, string vertexRootName, string pixelRootName) { this.device = device; // Uri string vertexRootUri = string.Format(@"/{0};component/{1}.vs", assemblyName, vertexRootName); string pixelRootUri = string.Format(@"/{0};component/{1}.ps", assemblyName, pixelRootName); // Shaders Stream vertexShaderStream = Application.GetResourceStream( new Uri(vertexRootUri, UriKind.Relative)).Stream; Stream pixelShaderStream = Application.GetResourceStream( new Uri(pixelRootUri, UriKind.Relative)).Stream; vertexShader = VertexShader.FromStream(Device, vertexShaderStream); pixelShader = PixelShader.FromStream(Device, pixelShaderStream); vertexShaderStream.Dispose(); pixelShaderStream.Dispose(); // Constant tables ExtractConstantsRegisters(vertexRootUri + ".constants", stringsToIndexesVS); ExtractConstantsRegisters(pixelRootUri + ".constants", stringsToIndexesPS); } |
Parse Constants Table
The constants file contains all of the information needed to determine which register a named parameter is in. This implementation stores the values in a dictionary for later reference when setting a named constant. It also allows the parameter to be referenced directly using the EffectParameter pattern to avoid dictionary lookups for greater performance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void ExtractConstantsRegisters(string rootUri, bool isForPixelShader) { using (Stream stream = Application.GetResourceStream(new Uri(rootUri, UriKind.Relative)).Stream) { XmlReader xmlReader = XmlReader.Create(stream); while (xmlReader.ReadToFollowing("Constant")) { xmlReader.ReadToDescendant("Name"); string name = xmlReader.ReadElementContentAsString(); EffectParameter effectParameter; if (parameters.ContainsKey(name)) { effectParameter = parameters[name]; } else { effectParameter = new EffectParameter(device, name); parameters.Add(name, effectParameter); } xmlReader.ReadToNextSibling("RegisterIndex"); int index = xmlReader.ReadElementContentAsInt(); if (isForPixelShader) effectParameter.PixelShaderRegisterIndex = index; else effectParameter.VertexShaderRegisterIndex = index; xmlReader.ReadToNextSibling("RegisterCount"); effectParameter.RegisterCount = xmlReader.ReadElementContentAsInt(); } xmlReader.Close(); } } |
Set Constant
With the mapping loaded, it becomes very easy to set a constant by name.
1 2 3 4 5 6 7 |
public void SetConstant(string propertyName, Vector4 vector4) { EffectParameter effectParameter = GetParameter(propertyName); if (effectParameter != null) effectParameter.SetValue(vector4); } |
Configure Device
Finally, we can configure the graphics device to use the pixel and vertex shader in the effect and apply the constants.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class Effect : IDisposable { // Apply public virtual void Apply() { // Shaders Device.SetVertexShader(vertexShader); Device.SetPixelShader(pixelShader); // Parameters foreach (EffectParameter effectParameter in parameters.Values) { effectParameter.Apply(); } } } public class EffectParameter { internal void Apply() { if (data == null) return; int size = Math.Min(RegisterCount, data.Length); for (int index = 0; index < size; index++) { if (VertexShaderRegisterIndex >= 0) device.SetVertexShaderConstantFloat4(VertexShaderRegisterIndex + index, ref data[index]); if (PixelShaderRegisterIndex >= 0) device.SetPixelShaderConstantFloat4(PixelShaderRegisterIndex + index, ref data[index]); } } } |
Example
Putting it all together, you can use the effect like this.
1 2 3 4 |
var effect = new Effect(device, "MyAssembly", "Shaders/MyShader"); effect.SetConstant("World", World); effect.SetConstant("Levels", 1.0f, 0.5f, 0, 1.3f); effect.Apply(); |
So there you go, no more messing about with manual register allocation!
Leave a Reply
You must be logged in to post a comment.