XNA Framework GameEngine Development. (Part 4, ShaderManager:GameComponent)
Introduction
Welcome to Part 4 of the XNA Framework GameEngine Development series. In this article, I will introduce the shader interface, ShaderManager and the custom effect code generator. This article is the first part of a sub series that will deal with shaders, textures and materials. I know many readers are itching for eye candy by now, it is coming, but for now I am focusing on good base classes and design.
First of many interfaces.
Just a reminder this series of articles is not about using C#, design practices or architecture. That being said, as an Engine developer we need to focus on keeping common interfaces and design principles. This first interface will be used to group effects together into a common parent class. We want to make certain every effect that is used in the engine have common features and are stored in a common way.
Since I use interfaces almost exclusively to accomplish this goal. I added the folder Interfaces to the project and added a class called IRoeShader to the engine project.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Interfaces
{
public interface IRoeShader
{
Effect BaseEffect
{
get;
}
bool ReadyToRender
{
get;
}
void Initialize(GraphicsDevice graphicsDevice);
void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions);
void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool);
}
}
Simple enough, every shader needs to have an effect, indicate if it is ready to be rendered and be initialized. So every class that inherits from IRoeShader will need to implement everything you see here. This will guaruntee the engine can identify a shader effect from every other object in the engine, a nice thing in a huge and complex application.
Building an Effect Proxy Generator.
It might just be the WCF developer in me, but I simply love proxy classes. There is just something wonderfully elegant about using code generators to create useful and powerful object classes. That is why I loved this CodePlex project. With a few simple adjustments we can adjust the code to include our IRoeShader interface.
Note: Many developers with experience using XNA may want to use the generic Content Pipeline for their effects. In a way we are using it here, just I would much rather have code that looks like effect.World = CameraManager.World, than effect.Parameter["World"].SetValue(CameraManager.World). To me it is much more natural to use Properties than Parameter setters.
Create a new console project, I called mine ShaderEffectGenerator.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework.Graphics;
using System.Windows.Forms;
using System.Text;
namespace ShaderEffectCodeGenerator
{
internal class Program
{
// Fields
private static List<string> effectParameters = new List<string>();
private static StreamWriter streamWriter;
private static int tabLevel = 0;
// Methods
private static int LoadEffectFromFile(string fullPath, out Effect effect, out byte[] windowsByteCode, out byte[] xboxByteCode)
{
try
{
Form form = new Form();
PresentationParameters parameters = new PresentationParameters();
GraphicsDevice device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, form.Handle, parameters);
CompiledEffect effect2 = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Windows);
windowsByteCode = effect2.GetEffectCode();
effect = new Effect(device, effect2.GetEffectCode(), CompilerOptions.None, null);
xboxByteCode = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Xbox360).GetEffectCode();
}
catch (Exception exception)
{
ReportError(exception.ToString());
effect = null;
windowsByteCode = null;
xboxByteCode = null;
return -1;
}
return 0;
}
private static void Main(string[] args)
{
if (args.Length < 1)
{
WriteInstructions();
ReportError("Please drag an .fx file into the app.");
}
else
{
Effect effect;
byte[] buffer;
byte[] buffer2;
string path = args[0];
if (path.StartsWith("\"") && path.EndsWith("\""))
{
path = path.Substring(0, path.Length - 1).Substring(1);
}
path = Path.GetFullPath(path);
string str2 = Path.GetDirectoryName(path) + "/" + Path.GetFileNameWithoutExtension(path) + "Effect.cs";
if (LoadEffectFromFile(path, out effect, out buffer, out buffer2) >= 0)
{
if (File.Exists(str2))
{
File.Delete(str2);
}
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
FileStream stream = new FileStream(str2, FileMode.Create);
using (StreamWriter writer = new StreamWriter(stream))
{
streamWriter = writer;
WriteUsingStatements(writer);
WriteLine("namespace RoeEngine2.Shaders");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("public class {0}Effect : IRoeShader", fileNameWithoutExtension));
WriteLine("{");
tabLevel++;
WriteTechniqueEnumeration(effect);
streamWriter.WriteLine();
WriteRoeSpecificProperties(writer);
WriteLine("#region Effect Parameters");
writer.WriteLine();
foreach (EffectParameter parameter in effect.Parameters)
{
WriteEffectParameter(parameter);
writer.WriteLine();
}
WriteLine("#endregion");
writer.WriteLine();
WriteLine("#region Effect Techniques");
writer.WriteLine();
foreach (EffectTechnique technique in effect.Techniques)
{
WriteLine(string.Format("private EffectTechnique _{0}Technique;", technique.Name));
writer.WriteLine();
}
WriteLine("#endregion");
writer.WriteLine();
WriteLine("#region Initialize Methods");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice)");
WriteLine("{");
tabLevel++;
WriteLine("Initialize(graphicsDevice, CompilerOptions.None, null);");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)");
WriteLine("{");
tabLevel++;
WriteLine("Initialize(graphicsDevice, compilerOptions, null);");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.");
WriteLine("///</summary");
WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
WriteLine("///<param name=\"effectPools\">The EffectPool to use with the effect.</param>");
WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)");
WriteLine("{");
tabLevel++;
WriteLine("_baseEffect = new Effect(graphicsDevice, byteCode, compilerOptions, effectPool);");
WriteLine("_readyToRender = true;");
writer.WriteLine();
foreach (string str4 in effectParameters)
{
WriteLine(string.Format("_{0}Param = _baseEffect.Parameters[\"{0}\"];", str4));
}
writer.WriteLine();
foreach (EffectTechnique technique2 in effect.Techniques)
{
WriteLine(string.Format("_{0}Technique = _baseEffect.Techniques[\"{0}\"];", technique2.Name));
}
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("#endregion");
writer.WriteLine();
WriteLine("///<summary>");
WriteLine("///Sets the current technique for the effect.");
WriteLine("///</summary>");
WriteLine("///<param name=\"technique\">The technique to use for the current technique.</param>");
WriteLine(string.Format("public void SetCurrentTechnique({0}Effect.Techniques technique)", fileNameWithoutExtension));
WriteLine("{");
tabLevel++;
WriteLine("switch (technique)");
WriteLine("{");
tabLevel++;
foreach (EffectTechnique technique3 in effect.Techniques)
{
WriteLine(string.Format("case {0}Effect.Techniques.{1}:", fileNameWithoutExtension, technique3.Name));
tabLevel++;
WriteLine(string.Format("_baseEffect.CurrentTechnique = _{0}Technique;", technique3.Name));
WriteLine("break;");
writer.WriteLine();
tabLevel--;
}
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("#region Compiled Byte Code");
writer.WriteLine();
WriteLine("#if XBOX");
StringBuilder builder = new StringBuilder();
builder.Append("static readonly byte[] byteCode = { ");
foreach (byte num in buffer2)
{
builder.Append(num.ToString() + ",");
}
builder.Append(" };");
WriteLine(builder.ToString());
WriteLine("#else");
builder = new StringBuilder();
builder.Append("static readonly byte[] byteCode = { ");
foreach (byte num2 in buffer)
{
builder.Append(num2.ToString() + ",");
}
builder.Append(" };");
WriteLine(builder.ToString());
WriteLine("#endif");
writer.WriteLine();
WriteLine("#endregion");
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
}
}
}
}
private static void WriteUsingStatements(StreamWriter writer)
{
WriteLine("using System;");
WriteLine("using Microsoft.Xna.Framework;");
WriteLine("using Microsoft.Xna.Framework.Content;");
WriteLine("using Microsoft.Xna.Framework.Graphics;");
WriteLine("using RoeEngine2.Interfaces;");
writer.WriteLine();
}
private static void WriteRoeSpecificProperties(StreamWriter writer)
{
WriteLine("private Effect _baseEffect;");
WriteLine("///<summary>");
WriteLine("///Gets the underlying Effect.");
WriteLine("///</summary>");
WriteLine("public Effect BaseEffect");
WriteLine("{");
tabLevel++;
WriteLine("get { return _baseEffect; }");
tabLevel--;
WriteLine("}");
writer.WriteLine();
WriteLine("private bool _readyToRender = false;");
WriteLine("///<summary>");
WriteLine("///Is the shader ready to be rendered.");
WriteLine("///</summary>");
WriteLine("public bool ReadyToRender");
WriteLine("{");
tabLevel++;
WriteLine("get { return _readyToRender; }");
tabLevel--;
WriteLine("}");
writer.WriteLine();
}
private static void WriteEffectParameter(EffectParameter param)
{
string item = param.Name;
string str2 = String.Empty;
string str3 = String.Empty;
string str5 = String.Empty;
switch (param.ParameterType)
{
case EffectParameterType.Bool:
str2 = "bool";
str3 = "Boolean";
break;
case EffectParameterType.Int32:
str2 = "int";
str3 = "Int32";
break;
case EffectParameterType.Single:
if ((param.RowCount > 1) && (param.ColumnCount > 1))
{
str2 = "Matrix";
str3 = "Matrix";
}
else if (param.ColumnCount > 1)
{
str2 = "Vector" + param.ColumnCount.ToString();
str3 = str2;
}
break;
case EffectParameterType.String:
str2 = "string";
str3 = "String";
break;
case EffectParameterType.Texture:
str2 = "Texture2D";
str3 = "Texture2D";
break;
case EffectParameterType.Texture1D:
str2 = "Texture1D";
str3 = "Texture1D";
break;
case EffectParameterType.Texture2D:
str3 = "Texture2D";
str3 = "Texture2D";
break;
case EffectParameterType.Texture3D:
str2 = "Texture3D";
str3 = "Texture3D";
break;
case EffectParameterType.TextureCube:
str2 = "TextureCube";
str3 = "TextureCube";
break;
default:
return;
}
if (param.Elements.Count > 0)
{
str2 = str2 + "[]";
}
effectParameters.Add(item);
string str4 = item.Substring(0, 1).ToUpper() + item.Substring(1);
WriteLine(string.Format("private EffectParameter _{0}Param;", item));
WriteLine(string.Format("public {0} {1}", str2, str4));
WriteLine("{");
tabLevel++;
WriteLine("get");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("if (_{0}Param == null)", item));
tabLevel++;
WriteLine(string.Format("throw new Exception(\"Cannot get value of {0}; {0} EffectParameter is null.\");", str4));
tabLevel--;
if (param.Elements.Count > 0)
{
WriteLine(string.Format("return _{0}Param.GetValue{1}Array({2});", item, str3, param.Elements.Count));
}
else
{
WriteLine(string.Format("return _{0}Param.GetValue{1}();", item, str3));
}
tabLevel--;
WriteLine("}");
WriteLine("set");
WriteLine("{");
tabLevel++;
WriteLine(string.Format("if (_{0}Param == null)", item));
tabLevel++;
WriteLine(string.Format("throw new Exception(\"Cannot set value of {0}; {0} EffectParameter is null.\");", str4));
tabLevel--;
WriteLine(string.Format("_{0}Param.SetValue(value);", item));
tabLevel--;
WriteLine("}");
tabLevel--;
WriteLine("}");
}
private static void WriteInstructions()
{
Console.WriteLine("The EffectCodeGenerator is a drop-in application. Simply drop in any .fx file to have");
Console.WriteLine("the EffectCodeGenerator produce you a new class written in C# that enables easy use");
Console.WriteLine("with your .fx file.");
}
private static void WriteLine(string line)
{
string str = string.Empty;
for (int i = 0; i < tabLevel; i++)
{
str = str + "\t";
}
streamWriter.WriteLine(str + line);
}
private static void WriteTechniqueEnumeration(Effect effect)
{
WriteLine("public enum Techniques");
WriteLine("{");
tabLevel++;
foreach (EffectTechnique technique in effect.Techniques)
{
WriteLine(string.Format("{0},", technique.Name));
}
tabLevel--;
WriteLine("}");
}
private static void ReportError(string error)
{
Console.WriteLine("Error: {0}", error);
Console.ReadKey(true);
}
}
}
ShaderManager class (Updated to use Dictionary)
Note: Thanks again to KaBaL, Please notice I changed the Manager namespace to Managers. This is because there will be more than one.
This class is our main container for everything that has to do with effects and shaders. It includes some very simple principles that you may have seen from the Ramblings of a Hazy Mind series. There are some minor adjustments, but I am not ashamed to say, the Ramblings series was a major contributer to this design principle. Feel free to read the series, it is an excellent primer for many of the next articles in this series.
Add a new class to the Managers folder, I called mine ShaderManager.
Note: Updated to use Dictionary thanks to leaf and KaBaL.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Shaders;
namespace RoeEngine2.Managers
{
public class ShaderManager : GameComponent
{
private static Dictionary<string, IRoeShader> _shaders = new Dictionary<string, IRoeShader>();
private static bool _initialized = false;
/// <summary>
/// Is the ShaderManagers Initialized, used for test cases and setup of Effects.
/// </summary>
public static bool Initialized
{
get { return _initialized; }
}
/// <summary>
/// Create the shader Managers.
/// </summary>
/// <param name="game"></param>
public ShaderManager(Game game)
: base(game)
{
}
/// <summary>
/// Add a shader of type IRoeShader.
/// </summary>
/// <param name="newShader"></param>
/// <param name="shaderLabel"></param>
public static void AddShader(IRoeShader newShader, string shaderLabel)
{
if (shaderLabel != null && !_shaders.ContainsKey(shaderLabel))
{
_shaders.Add(shaderLabel, newShader);
newShader.Initialize(EngineManager.Device);
}
}
/// <summary>
/// Get a shader of type IRoeShader.
/// </summary>
/// <param name="shaderLabel"></param>
/// <returns></returns>
public static IRoeShader GetShader(string shaderLabel)
{
if (shaderLabel != null && _shaders.ContainsKey(shaderLabel))
{
return _shaders[shaderLabel];
}
return null;
}
/// <summary>
/// Create the shaders.
/// </summary>
public override void Initialize()
{
base.Initialize();
foreach (IRoeShader shader in _shaders.Values)
{
if (!shader.ReadyToRender)
{
shader.Initialize(EngineManager.Device);
}
}
AddShader(new basicEffect(), "BasicEffect");
_initialized = true;
}
}
}
There may be some readers that will ask why not use a generic list instead of a hashtable? I am okay with either design, I chose a HashTable because I liked having instant access to each object rathre than getting an item out of a list by index. If you are the kind of person that wants to explore deeper into engine design you might write test cases for each design. It is very possible, a hashtable is sub optimal for the small number of shaders we will most likely be adding to the project. If you can prove it with numbers to support your findings, I will definitely redesign what you see here and give you credit for your work.
Using the ShaderManager GameComponent
Add the following code to the Properties section of RoeEngine2
private static ShaderManager _shaderManagers = null;
And in the RoeEngine2 Constructor
// Init shader Managers _shaderManagers = new ShaderManager(this); Components.Add(_shaderManagers); //TODO include other inits here!
Encapsulating the BasicEffect
This might not be necessary, but since I would like to use the BasicEffect in some samples I will encapsulate it here. This will most likely grow some in the future, it is here now for test cases.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Shaders
{
public class basicEffect : IRoeShader
{
private BasicEffect _baseEffect;
///<summary>
///Gets the underlying Effect.
///</summary>
public Effect BaseEffect
{
get { return _baseEffect; }
}
private bool _readyToRender = false;
///<summary>
///Is the shader ready to be rendered.
///</summary>
public bool ReadyToRender
{
get { return _readyToRender; }
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
public void Initialize(GraphicsDevice graphicsDevice)
{
Initialize(graphicsDevice, CompilerOptions.None, null);
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)
{
Initialize(graphicsDevice, compilerOptions, null);
}
///<summary>
///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.
///</summary
///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
///<param name="effectPools">The EffectPool to use with the effect.</param>
public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)
{
_baseEffect = new BasicEffect(graphicsDevice, effectPool);
_readyToRender = true;
}
}
}
Optional TDD – Updated, no longer using InvalidOperationException
Add the following test cases to the RoeEngineTestCases class.
[Test, Category("ShaderManager")]
public void ShaderManagerCreated()
{
Assert.IsTrue(ShaderManager.Initialized);
Assert.IsNotNull(ShaderManager.GetShader("BasicEffect"));
Assert.AreEqual(typeof(basicEffect), ShaderManager.GetShader("BasicEffect").GetType());
}
[Test, Category("ShaderManager")]
public void ShaderManagerAddGetFailed()
{
// It is always a good idea to test for Failures too!
Assert.IsNull(ShaderManager.GetShader("testshader"));
}
Conclusion
In this article I introduced the IRoeShader, shaders, shader proxy generation and the ShaderManager. I will be revisting this class from time to time, in a growing engine it is always necessary to Refactor your work. Hopefully, you can use some of these design practices in other projects as well.
Your feedback is very important, please leave me a comments or suggestions. I will respond as soon as possible.
Challenge accepted!
Awesome KaBaL, options here are HashTable, generic List, or Dictionary to name a few. Each will have slightly different performances.
A update here: http://www.phase9studios.com/2008/01/08/DictionaryVSHashTable.aspx
It would appear Dictionary is superior in every way, due to the fact a HashTable needs to box and unbox objects. I will most likely be updating the code to use a Dictionary.
Grrr… You beat me too it, haha.
Note, I would still prefer Dictionary over Hashtable because the former is strongly typed but in your particular case the performance difference would have been minimal. The Dictionary vs Hashtable article you link to is comparing storing Guids, which are value types (struct in C#). Boxing and unboxing only occurs with value types, not with reference types such as your shader classes.
Also, a minor comment but you seem to be suggesting that using the content pipeline would mean looking up parameters by string. This is not the case, you could have generated effect classes that work with the content pipeline or use other ways to reference parameters such as using an effect parameter standard like DXSAS, or rolling your own.
Thank you for your feedback leaf. Very good information to know about Hashtables and reference types.
Yes, you are correct, I could have generated effect classes… which I do. I am using the content pipeline to do so. Just I do not compile the effects into xnb files using the pipeline. Rather I compile the effects into bytecode stored in the generated effect “proxy”.
Sorry for any confusion, thank you for the Hashtable info.
There is a missing condition I think in WriteEffectParameter function, the condition of (“Single”,”float”), if the effect file contains a float variable, it won’t be detected so this condition must be increased to detect it :
case EffectParameterType.Single:
if (…)
{
…
}
else if (…)
{
…
}
// Missing Condition
else
{
str2 = “float”;
str3 = “Single”;
}
break;
Yup, those conditions can be added, by no means is this code complete. These are just ideas to spark other peoples interests.
Also, I am considering depreciating this code, I am working on a new materials processor that will take the place of this code.
Hi, roecode, could you tell what is this new material ? and why not any longer using this one ? is a great site, I hope I can learn a lot from here.
I’ve tried to follow all of your tutorials step by step and so far, nothing, i can’t make anything new cause you won’t explain exactly what file needs to be made, which folder it goes into, not to mention you even forgot the whole part where you MADE a managers folder, this “Tutorial” is not for beginners to XNA!
Wow, you certainly sound frustrated. Without a doubt this tutorial series is not for beginners to XNA, nor is it for beginners to development in general. Actually, it isn’t even for people interested in game development.
I think I did make this clear in the first tutorial. Rather, my goal here is to outline basic, repeatable concepts for making a workable game engine. I do not want, nor do I expect every single line of code to be copied from this series to make a working engine.
You can definitely use this as a helper for developing your own engine, but copying code is a bad way to make an engine. You might want to look for a prebuilt engine, there are plenty available.
Later in this series I started posting links to code and working examples. Getting to this step is very good, but not a whole lot can be seen at this point in the engine.
Sorry, i was a bit frustrated, i just got off of a hard day of work and i tried an hour or so of following 2 of your tutorials but no luck, is there any chance you can help me Via MSN?
The generator miss a condition that is the array of a parameters. i.e. float a[8] = {1.2f, 2.0f, …..};
bool b[6] = {true, false, …..}
baseEffect has a property where it returns itself, but as an Effect class, but its not of Effect type. What am I missing?
Just wanted to be able to use the BaseEffect as an IRoeShader.
I’m just wondering was is exactly is the use of this shader manager ? Does it allows us to compile hlsl in runtime ?
I didn’t follows any of this tutorial but I think I would be able to learn fews tricks. Keep going
cheers
The shader manager does not compile shaders. Rather it is a “container” that groups logical objects together… in this case shaders. The shader itself is in fact HLSL, but the content pipeline in XNA is used to actually load and compile the shader.
This is actually the 4th installment in the series of many many articles on creating a game engine. The key concepts here are not loading and compiling shaders. Rather the focus here is to maintain a logical group of shaders that will be used by other “drawable” objects in the engine.
Note:
If you are getting FileNotFoundExceptions for the Console application and are running VS on a 64-bit OS, make sure you go to the build properties (right click on solution > properties > build) and change the target platform to x86.
You should no longer have the issue.