https://www.gamedev.net/blog/1930/entry-2260721-a-custom-variant-class/
Now for something completely different. Since I'm currently working on the graphics module, I thought I'll talk about one of the coolest features of the engine in that regard: The custom shading language.
Why a custom language?
I started the renderer as DX9, and later added DX11-support. And at some point, I also added GL4-support for a schools project. Now the render backend and interface is build so that you can write grahpic algorithms indifferent of the actually used API. The only problem was, I still had to have each shader in 3 different variations. I already noticed that all those different shaders actually just differed in syntax, but most actualy code was nearly identically. So thus my idea was to write a kind of meta-language, of which I can generate shaders for all different APIs, while just writing the shader once.
The syntax:
So syntactically, what I came up with is a concept of blocks & lines, that make up the base file format. Take a look at a vertex-shader
// the definition of the vertex shadervertexShader{ // defines the vertex-attributes, handed over by the input assembler in { float4 vPos; } // defines the output of this shader out { float4 vPos; } // defines a block of shader constants, "Instance" is an engine-specific macro for a certain cbuffer ID input Instance { matrix mWVP; } // the main execution point of the vertex shader main { out.vPos = mul(in.vPos, mWVP); }}
So as you can see, the definition of the shader is really slim. Compare it to the equivalent shader in DX11:
struct VS_INPUT{ float4 vPos : SV_POSITION;};struct VS_OUTPUT{ float4 vPos : SV_POSITION;};cbuffer Instance : register(b0){ matrix mWVP;}VS_OUTPUT mainVS(VS_INPUT in){ VS_OUTPUT out = {0}; out.vPos = mul(in, mWVP); return out;}
There is lots of more syntactical "garage" you have to take care of in plain HLSL5. My language automatically takes care of that, allowing for faster creation of shaders. I think most of what is in the vertex-shader I posted should be pretty self-explanatory, alongside the additional comments. Lets look at a pixel-shader next. The concept is pretty much the same:
pixelShader{ // pixel shader only has an "out"-block, as input is implicitely generated from the vertex shaders "out" out { float4 vColor; } // this block is new - used textures are declared here textures { 2D Texture; } main { out.vColor = Sample(Texture, float2(0.5f, 0.5f)); }}
Again, pretty lightweight. Maybe I'm biased towards my own work, but I'd consider writing shaders in such a manner way easier and faster than "plain" HLSL/GLSL-shader. Of course you first have to know the syntax.
Writing actual shader code:
So writing actual shader code (what is in the "main" methods), its most of the time pretty straightforward. I've based the language syntax on HLSL5, and all the parser really has to do is replace some function names that are unique to the language (eg. Sample()), as well as replace all used types (float, matrix) for GLSL-targets. So if you are familiar with writing HLSL-code, then coding in the acclimate shading language should be extremely straight-forward.
Moar blocks pls:
In order to support most basic shader features, there are a few blocks that are shared between the different types of shader, while other blocks are shader-specific. Some of the shadered once you've already seen, but there is a few more:
- out: The output of the current shader stage, acts eigther as input for the next stage or writes to the framebuffer (in case of a pixel-shader)
- textures: The main declaration point for textures of all kinds. Eventually needs to be reworked a bit, once I want to support regular buffers, since they shader texture registers, at least in DX11.
- input: The equivalent of DX11-cbuffers, and GL4-uniform-buffers. The actual buffer qualifiers (Instance, Stage, ...) are given by the using framework, since its easier to remember a name than a number, and also easier to change. Thats one thing really hard to emulate right in DX9, everything else is really quite easily translated to the different shading-languages.
- functions: That block is new. Since you can't freely write code all over the place, for being able to declare functions, this block is needed.
functions{ float calculateSomething(float4 vPos) { return 0.5f; } float calculateSomethingElse(float2 vTexcoords) { return vTexcoords.x / vTexcoords.y; }}
Wrapping it up:
So thats are the basics of the language. I consider it even pretty good objectively, and really don't want to write shader in plain HLSL/GLSL anymore. Thats not even all though - there are a few more features, which I'm going to talk about next time. For example, there are geometry-shaders, but also direct support for permutations, and a plugin-system for shaders. Thanks for reading, and see you next time!
PS: As a practical example, I've attached an SSAO shader written in this language. Enjoy!
Its great fun writing languages and I've been guilty in the past of doing it when I didn't really need to Seems here it solves a very real problem.