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:

  1. NMSceneShader object is created
  2. Check is performed to be sure that effect can be added to a stack
  3. 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:

  1. Correct UNMShaderProgram subclass is created
  2. Pointer in NMSceneShader is added to newly created UNMSceneShader object
  3. Correct NMShaderObject is loaded
  4. Pointer to NMSceneShader and NMShaderObject is added
  5. Program from NMShaderObject is compiled
  6. 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));
					}
				}
			}
		}
	}
}