Skip to content

Creating a custom effect

MWstudios edited this page Jan 28, 2022 · 17 revisions

How to do it

SharpDX's CustomEffectBase class is by itself useless, however I've set up a much better class, PVShaderBase, which allows you to create a new shader within several lines. This tutorial assumes you already have a CSO shader file prepared, because covering the shading side and HLSL creation would be a long story. To summarize, you would download HLSL tools for Visual Studio and create a C++ project where you would write the HLSL files.

This is a basic HLSL file that shows red and green color based on coordinates.

Texture2D InputTexture : register(t0);
SamplerState InputSampler : register(s0);

float4 main(float4 pos : SV_POSITION, float4 posScene : SCENE_POSITION, float4 uv0 : TEXCOORD0) : SV_Target
{
	return float4(uv0.xy, 0, 1); //R and G, B, A
}

First, you create a class that represents the shader. Copy-paste this class everytime you want to add a new one.

public class Shader1 : PVShaderBase
{
	static Guid sGuid = Guid.NewGuid();
	
	public Shader1() : base(sGuid) { }
}

In your Program class, create an Effect object and add this to the main method:

static Effect testShader;

static void Main()
{
	Global.Form = new("Window Title") { ClientSize = new(1600, 900) };
	Global.Initialize(1);

	Global.D2DFactory.RegisterEffect<Shader1>();     //first we register the effect
	Global.ShaderFile = "your shader file.cso"       //a global variable that tells Direct2D what pixel shader is going to be loaded
	testShader = new Effect<Shader1>(Global.Setups[0].DC); //then we create the effect

	Global.Run(Render); //IMPORTANT!!! This needs to be at the end, because this is where the code stops!
}

Let's test the shader out. You can wither apply it to the whole screen...

Global.Setups[0].RenderScreenShaders(false, testShader);

...or you can load a bitmap and then apply the shader on that. In that case the shader only covers the bitmap, the rest of the screen remains untouched. Feel free to render the bitmap itself without the shader, just to assure it shows up.

var bmp = Global.Setups[0].DC.LoadBitmapFromFile("your bitmap.png");
testShader.SetInput(0, bmp, true);
Global.Setups[0].DC.DrawImage(testShader, new RawVector2(100, 100)); //bitmap position
bmp.Dispose(); //remember to dispose !!!

Small note about RenderScreenShaders()

If you've rendered multiple screen shaders at once while using texture coordinates (uv0 in HLSL), you may have noticed that all shaders beyond the first one have moved their coordinates a bit and that the right half of the screen doesn't render if the window size is too large. That's because chaining shaders via Effect.SetInputEffect() limits their size to a maximum of 1024×1024. Setting the first parameter in RenderScreenShaders to true creates a second render target and then keeps drawing one into another, which works around the issue.

Automation

If you're doing a simple pixel shader test and you don't want to waste time creating effect classes, there is a faster alternative: CloneablePixelShader. It's a shader template with 1 texture input and nothing else which you can use for testing purposes:

static void Main()
{
	Global.ShaderFile = "your shader file.cso"
	testShader = new Effect<CloneablePixelShader>(Global.DCs[0]); //no registering required, that was already done by the library
}

static void Render()
{
	Global.Setups[0].DC.BeginDraw();
	
	Global.Setups[0].RenderScreenShaders(false, testShader);
	
	Global.Setups[0].DC.EndDraw();
	Global.EndRender();
}

How does it work?

Each custom effect needs to implement several methods in order to work. When created, the effect loads a pixel shader and sets the buffer precision, in this case a 32-bit floating point. The only way to change the shader path on the fly is to make the variable static and expose it via a method, which is exactly what I did via Global.ShaderFile.

Then, the effect specifies the rectangle size to draw on. By default, it's the same size as the size of the texture inputs, but I've made a workaround where you can change it (more on that in "Adding a constant buffer and changing border size"). Then, before rendering, the effect sets up its constant buffer and uses the SetSingleTransformNode() shortcut to quickly create a shader nodegraph (more on that in "Nesting multiple effects inside an effect").

It'd be probably worth noting that an effect has a title, author and a description field, but these are absolutely useless and cannot be accessed in any way. I'm not sure what role does it play in Direct2D, most likely an unfinished feature that was supposed to show up in some node editor. But so far, Microsoft has only made a shader node editor for Direct3D, which is currently beyond our scope. If you're interested, you can enable the Direct3D node editor in Visual Studio Installer's components.

In this wiki you can find out all the information on how to use the Ensoftener library. For more information on how to add Ensoftener to your project, see "Installing and running". The rest is dedicated to the library's features.

Notice: This wiki shows information for Ensoftener 5.0 and is currently outdated.

Clone this wiki locally