Residual Decay shader implementation
Posted 2009-11-16
I was thinking on rewriting whole shader implementation in RD for a long time. New system consists of three objects:
- NMSceneShader - which contain all shader specific properties and pointer to UNMShaderProgram. It's more of a link between Unreal Script and renderer. Changing properties here will affect a shader.
- NMShaderObject - which contain shader program (as text buffer) - done because it's easier, faster and better to use native UEngine format to store shaders.
- UNMShaderProgram - which contain compiled program itself, pointer to NMSceneShader and NMShaderObject and all texture data. It's also and object that have to be extended in a renderer in order to have shader support. It's written only in C++.
All NMSceneShader objects are stored in dynamic array inside PlayerPawn and also get's ticked so it's very easy to create complicated dynamic post-process effects. All shaders are created using CreateShader function (this process is driver independent) which works as follows:
- NMSceneShader object is created
- Check is performed to be sure that effect can be added to a stack
- Shader is added to a stack
That is pretty much it - everything else is handled by a renderer. Now to render shaders, renderer is iterating through a stack and call DrawShader function on already initialized programs, if shader is not compiled InitShader function is called:
- Correct UNMShaderProgram subclass is created
- Pointer in NMSceneShader is added to newly created UNMSceneShader object
- Correct NMShaderObject is loaded
- Pointer to NMSceneShader and NMShaderObject is added
- Program from NMShaderObject is compiled
- NMSceneShader is marked as initialized
This solution allows me to easily extend shaders with new functionality and create much more complicated effects. Below NMSceneShader source:
class NMSceneShader extends NMObject native;
//opacity
var() byte Opacity;
//allow only one instance of a shader
var() bool bOneInstanceOnly;
//if true will be ticked
var() bool bDynamic;
//shader class
var() const editconst string ShaderClass;
var() const editconst string ShaderClassAlt;
//shader name
var() const editconst string ShaderName;
/** internal native variables */
var const string ShaderTag; //unique shader tag (by Player)
var const NMPlayerPawn Owner; //owner (by Player)
var transient const int ShaderPointer; //pointer to UNMShaderProgram (by RenDev)
var transient const bool bShaderInitialized; //true if initialized (by RenDev)
var transient const bool bCompiled; //true if shader is compiled (by UNMShaderProgram)
var transient const bool bCriticalError; //true if shader can not be compiled (by UNMShaderProgram)
var transient float timer; //for time based (dynamic) effects (by Script)
/**
* Any object can be used as a template as long as all variable types and names match
*/
native(1680) simulated final function ApplyTemplate(object Template);
/**
* All shaders get ticked if bDynamic is set to true
*/
event Tick(float DeltaTime){}
/**
* Called before shader is compiled
*/
event BeforeShaderInit(){}
/**
* Called after shader is compiled
*/
event AfterShaderInit(){}
And here's how rendering function looks like in Residual Decay:
void UNMRenderDevice::RenderShaders()
{
if( LevelEvents != NULL )
{
ANMPlayerPawn* PP = LevelEvents->GetNemesisPlayer();
if( PP != NULL )
{
if( PP->ShaderStack.Num() > 0 )
{
for( int i=0; iShaderStack.Num(); i++ )
{
if( PP->ShaderStack(i)->bShaderInitialized )
{
UNMShaderProgram* ShdPrg = (UNMShaderProgram*)PP->ShaderStack(i)->ShaderPointer;
if( ShdPrg != NULL && !ShdPrg->bRequestedDestroy )
ShdPrg->DrawShader(PP);
}
else
{
InitShader(PP->ShaderStack(i));
}
}
}
}
}
}