HLSL Shader


HLSL Shader A customizable HLSL shader implementation.

The HLSL Shader node acts as a host for a user-designed shader program written in the High-Level Shading Level. By using customized shaders, a large portion of the rendering process can be modified to create a unique look and feel. This document discusses the function of the HLSL node and the interaction between shader code and the Ventuz rendering engine. For more information on the the user interface, see Shader Editor.

Shader Programming is an advanced topic that requires strong knowledge of both linear algebra as well as the inner workings of a graphics card. It is out of the scope of this documentation to teach a novice user how to develop or debug shaders. The following will thus only give a brief introduction to shading in general followed by a description of the Ventuz specific aspects of shader development.


Shader Programming

The standard rendering behavior of Ventuz for the most part uses the so called fixed function pipeline of the graphics card. This pipeline is a set of operations that is responsible for transforming geometry from 3D to 2D space, calculating the influence of a light source, texture or material on the final shading of a pixel and so on. The term fixed-function derives from the fact that previous to the introduction of shaders, a graphics card had only a single, hard-wired rendering pipeline built into the graphics chip. The only degree of freedom lay in enabling/disabling specific stages in this pipeline or modify input parameters to those stages.

As graphics cards became more powerful, GPUs changed from dedicated hardware operations to multi-purpose processing units in many aspects similar to a CPU. To harness this flexibility, a description language was needed which is why Microsoft introduced the High Level Shader Language as part of the DirectX standard. Other software/hardware vendors have developed different shader languages such as GLSlang, CGFX and many more. While most of those languages are quite similar, they are not directly compatible.

In summary, shaders are small code fragments that replace the fixed-function pipeline functionality and allow the user to re-program the GPU. The author of a shader can either choose to mimic and extend the fixed-function functionality or implement completely different operations.

By using shaders to render a geometry, large parts of the Ventuz render engine will not have any effect on the subtree below the shader. For example, light source will not have any effect unless the shader author explicitly writes code to use them. Same with material information, same with textures, ...


HLSL Shader

For the purposes of Ventuz, a shader is a text that contains a shader description in the HLSL language. The following is an example of a minimalistic shader:

float4x4 WorldViewProjection  : WORLDVIEWPROJECTION;

float4 VS( float4 Position : POSITION ) : POSITION
{
    return mul(Position, WorldViewProjection);
}

float4 PS() : COLOR
{
    // This just returns a solid white color.
    return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

technique Tech1
{
    pass pass0
    {
        vertexshader = compile vs_2_0 VS();
        pixelshader  = compile ps_2_0 PS();
    }
}

A shader consists of two programs, the vertex and the pixel shader. The vertex shader is executed for each vertex that is rendered and must at least generate a transformed output position. The graphics card uses this output position to transform the primitive that is rendered into a number of fragments. For each fragment, the pixel shader is executed to calculate the final color of the fragment as it is turned into a pixel. In the example above, the vertex position is transformed using the world-view-projection matrix and each fragment is transformed into a solid white pixel regardless of any light or material nodes in the scene.

This is probably the simplest, non-trivial shader there is. More elaborate shaders may require the geometry to be drawn multiple times which is supported by defining multiple passes. Each pass is a combination of exactly one vertex shader and one pixel shader used to render the content associated with the shader. Finally, a shader may contain multiple techniques. A technique is a collection of passes and is usually used to implement the same effect in different variants. For example, one technique might be the high-end fire shader that only runs on the fastest of graphics cards, another technique is a trimmed down version that does not look as nice but runs on the weak embedded chips common in laptops.

Ventuz HLSL Shader

The shader node is a hierarchy node that can be used anywhere in the scene hierarchy. Similar to a Material Node, a shader will be applied to all geometries that are part of the subtree attached to the shader's output. By itself, a shader node only has a single property which is used to select the active Technique. The options available of course depend on the techniques defined in the shader code. All other properties are dynamically generated based on the global variables used in the shader source code.

Variables

A shader usually requires a number of values from the hosting application - in this case Ventuz - to operate properly. Even the simple shader in the example above requires the world-view-projection matrix to properly transform a vertex. There are three ways to assign a variable with proper values:

If both semantics and annotations exist for the same variable, the annotations are used.

The shader parameters will be updated to the current values just before a geometry is rendered. So for example the world matrix will reflect the value active during geometry rendering, not at the time the shader node is traversed in the scene hierarchy.

Textures

A special case are textures. If a texture is bound to a texture property on the shader node, that texture will be activated when the shader node is processed. A texture bound to a shader's property will overwrite any texture on the respective texture stage that has been set by a Texture node before the shader in the scene hierarchy. Likewise, a texture node used after the shader will overwrite the texture assignment done by the shader to the same texture stage. If no texture is bound to the shader's texture property, the shader will use the most recent texture in the render order when processing a geometry.

A shader addresses a texture via a sampler object. A lot of shader source codes do not contain an explicit texture variable definition but rather rely on the host application to associate the appropriate texture with a specific sampler. In contrast, Ventuz requires a texture variable declaration to create a bindable texture property. The assignment of a texture to a sampler has to be done from within the shader source code:

texture Image;
sampler ImageSampler : register (s[0]) = sampler_state
{
	Texture = <Image>;
};

Which texture variable is assigned to which stage depends on what the DirectX shader compiler thinks is best. Usually the first texture used in a render pass (e.g. the respective vertex shader and pixel shader) is assigned to stage 1, the second to stage two and so on. To ensure proper interoperability with the standard Texture nodes, a dedicated stage should be assigned to each sampler, especially when multiple textures are used in multiple passes. This is done by adding : register(s[x]) after the sample definition where x is a 0 for stage1 up to 7 for stage8.


Vertex Textures

Vertex shaders do not get textures from the normal texture stages. One of the special stages Vertex1 or Vertex2 needs to be selected in the Texture node.

To use the texture in the vertex shader, the sampler must be declared:

sampler Vertex1 : register(s0);

Register s0 will use Vertex1 and register s1 will use Vertex2.

A vertex texture must be sampled with the tex2Dlod() function instead of the normal tex2D() function used in pixel shaders. This is because mipmapping information can not be computed automatically and must be specified manually. Usually you will want a mipmap lod of 0.

    // [...]

    float2 uv = input.uv;
    float lod = 0;
    float4 tex = tex2Dlod(Vertex1,float4(uv,0,lod));

    // [...]

Filtering works as with pixel shader textures.

Passes

A shader node has a number of outputs that corresponds to the maximum of the number of passes over all techniques in the shader. Each output is labeled with the name of the pass as used in the shader source code for easy identification. If the number of passes changes due to modifications of the shader source code, the number of outputs is adapted as well. Note however that an output will not be removed if there are nodes attached to it.


If multiple passes are required to render the same geometry, the respective nodes should be linked to both outputs (using CTRL + SHIFT while dragging, see Hierarchy Editor). This avoids creating two copies of all the nodes in the subtree, but there is still a memory overhead compared to using the content only on one output.

Reference Information

Supported Semantics

One of the biggest drawback of shader semantics is the fact that they are not standarized. It is up to the application to define a string that is used to describe a certain semantic and many applications use incompatible definitions. At the time of writing, Ventuz only supports the following semantics:

Semantic Type Description
WORLD float4x4 The object-to-world matrix at the geometry currently rendered. Usually the result of a number of Axis nodes.
WORLDINVERSE float4x4 Inverse of the object-to-world matrix.
VIEW float4x4 The world-to-view matrix at the geometry currently rendered. Usually the result of a View or Camera node.
VIEWINVERSE float4x4 Inverse of the world-to-view matrix.
PROJECTION float4x4 The view-to-clip space matrix at the geometry currently rendered. Usually the result of a Projection node.
PROJECTIONINVERSE float4x4 Inverse of the view-to-clip space matrix.
WORLDVIEWPROJECTION float4x4 Combined matrix that contains the complete transformation from object to clip space. This is usually used to compute the output position of a vertex.
WORLDVIEWPROJECTIONINVERSE float4x4 Inverse of the world-view-projection matrix.
WORLDVIEW float4x4 Combined matrix that contains the object-view transformation.
WORLDVIEWINVERSE float4x4 Inverse of the world-view matrix.
VIEWPROJECTION float4x4 Combined matrix that contains the transformation from world to clip space.
VIEWPROJECTIONINVERSE float4x4 Inverse of the view-projection matrix.

Example:

float4x4 WorldViewProjection  : WORLDVIEWPROJECTION;

Supported Annotations

Annotations are an effort by Microsoft to address the disadvantages of using semantics. There are two different categories of annotations: User interface and data binding annotations. All annotations are specified by variable declarations inside less and greater than characters right after the original variable.

Data bindings work similar to semantics. They define the data source for a variable by declaring the SasBindAddress variable.

float4x4 View
<
    string SasBindAddress = "Sas.Camera.WorldToView";
>;
Semantic Type Description
Sas.Skeleton.MeshToJointToWorld[0] float4x4 Object-to-world matrix.
Sas.Camera.WorldToView float4x4 World-to-view matrix.
Sas.Camera.Projection float4x4 View-to-clip space matrix.
Sas.Light[0].Direction float3 Direction vector of the first light source. Indices 0-7 are supported.
Ventuz.Lights.Ambient float4 Ambient color of the most recently set light source with respect to the rendering order of the scene hierarchy.
Ventuz.Lights.Diffuse float4 Diffuse color of the most recently set light source with respect to the rendering order of the scene hierarchy.
Ventuz.Lights.Direction float3 Direction vector of the most recently set light source with respect to the rendering order of the scene hierarchy.
Ventuz.Lights.Position float3 Position of the most recently set light source with respect to the rendering order of the scene hierarchy.
Ventuz.Lights.Specular float4 Specular color of the most recently set light source with respect to the rendering order of the scene hierarchy.
Ventuz.Material.Alpha float Alpha value at the geometry currently rendered. Multiple Alpha nodes or Material nodes may have contributed to this value based on their AlphaRelation properties.
Ventuz.Material.Ambient float4 Ambient material color active at the time the geometry is rendered, usually the result of a parent Material node.
Ventuz.Material.Color float4 Color active at the time the geometry is rendered. In case the geometry is influenced by a Material node, this will be the diffuse color of that node. In case the geometry is influenced by a Color node, this will be the color of that node. Note that the alpha value may be the accumulated alpha of multiple nodes based on the AlphaRelation property of the material node.
Ventuz.Material.Diffuse float4 Diffuse material color active at the time the geometry is rendered, usually the result of a parent Material node. Note that the alpha value may be the accumulated alpha of multiple nodes based on the AlphaRelation property of the material node.
Ventuz.Material.Emissive float4 Emissive material color active at the time the geometry is rendered, usually the result of a parent Material node.
Ventuz.Material.Sharpness float Specular material exponent active at the time the geometry is rendered, usually the result of a parent Material node.
Ventuz.Material.Specular float4 Specular material color active at the time the geometry is rendered, usually the result of a parent Material node.
Ventuz.RenderTarget.Size float2 Dimension in pixel of the Render Target active during rendering of the geometry.
Ventuz.Texture.Active bool True if at least one texture is set to any of the texture stages, usually the result of a parent Texture node.
Ventuz.Texture.Mapping float4x4 Texture mapping matrix active at the time the geometry is rendered, usually the result of a Mapping node attached to a parent Texture node. A shader that wants to respect texture mapping should multiply the input texture coordinates by this matrix to apply the mapping transformation.
Ventuz.Viewport.Size float2 Dimension in pixel of the Viewport active at the time the geometry is rendered.
Ventuz.DirectionalLight[*] VentuzDirectionalLight[8] All directional lights, see Light Source Information
Ventuz.PointLight[*] VentuzPointLight[8] All point-lights, see Light Source Information
Ventuz.SpotLight[*] VentuzSpotLight[8] All spot-lights, see Light Source Information
Ventuz.Light[*] VentuzLight[8] All lights, see Light Source Information
Ventuz.NumDirectionalLights int Number of directional lights
Ventuz.NumPointLights int Number of point-lights
Ventuz.NumSpotLights int Number of spot-lights
Ventuz.NumLights int Number of lights, independent of type
Ventuz.Textures[*].Size float4[8] Size of the texture image in pixels, see Texture Information
Ventuz.Textures[*].Mapping float4x4[8] Mapping matrices for all textures, see Texture Information
Ventuz.Textures[*].Info uint4[8] Information about texture shading and UV mapping, see Texture Information
Ventuz.NumTextures int Number of textures. Exactly: Index of the last used texture stage plus one.

Each binding may only be used for exactly one variable.


UI Annotations do not cause parameters to be filled with values but rather influence the way a shader parameter appears in the Property Editor. For example below, the shader variable DiffColor will appear as a color picker labeled Diffuse. If the annotations would not exist, the default behavior would create four separate float properties, one for each components in the vector. The assignment of a default value can be used both with or without annotations. If it exists, it will be used as the value a property is reset when the reset button next to a property is pressed in the Property Editor.

float4 DiffColor
<
	string SasUiControl = "ColorPicker"; 
	string SasUiLabel = "Diffuse";
> = { 1.0f, 0.022f, 0.0f, 1.0f };
Annotation Type Description
SasUiControl string Supported values are ColorPicker and Direction.
SasUiLabel string Name of the property as it will appear in the Property Editor, must be unique with respect to the other properties generated. If the same name is used for different properties, the automatic property generation algorithm will most likely fail or produce incorrect results.
SasUiMin int/float Minimum value for a float or int variable, not supported for vector types.
SasUiMax int/float Maximum value for a float or int variable, not supported for vector types.
SasUiSteps int/float Increments by which a property can be increased/decreased. Can for example be used to only allow odd numbers.
SasUiVisible bool If set to false, no property will be generated for this variable.
VentuzFavored bool By default, all parameters will produce a favored property. Use this annotation to specify whether a property should be favored or not.

State Changes

HLSL allows adding state change information to a pass (see MSDN: Effect states. While one could also use the respective Ventuz nodes, it is a convenient way to influence the rendering behavior.

technique Tech1
{
	pass pass0
	{
		vertexshader = compile vs_2_0 VS();
		pixelshader  = compile ps_2_0 PS();
        
		AlphaBlendEnable = FALSE;       
	}
}

To ensure proper interaction with the rest of the Ventuz nodes, only a limited subset of state changes is supported:

Supported States
AlphaBlendEnable
AlphaTestEnable
BlendOp
CullMode
DestBlend
SrcBlend
StencilEnable
StencilFail
StencilFunc
StencilMask
StencilPass
StencilRef
StencilWriteMask
StencilZFail
ZEnable
ZFunc
ZWriteEnable

The state changes are applied when rendering of the respective pass starts, so at the time of the shader node in the rendering order. Any relevant node (for example Z-Testing for ZEnable) before the shader is overwritten and any node after the shader will overwrite the shader's state change.

Light Source Information

Information about all light source can be retrieved in the shader by using the Ventuz.DirectionalLight[*], Ventuz.PointLight[*], Ventuz.SpotLight[*] and Ventuz.Light[*] annotations. First one has to declare the following structures:

struct VentuzDirectionalLight
{
    float3 DiffuseColor;
    float3 SpecularColor;
    float3 AmbientColor;
    float3 Direction;
};

struct VentuzPointLight
{
    float3 DiffuseColor;
    float3 SpecularColor;
    float3 AmbientColor;
    float3 Position;
    float3 Attenuation;
    float Range;
};

struct VentuzSpotLight
{
    float3 DiffuseColor;
    float3 SpecularColor;
    float3 AmbientColor;
    float3 Position;
    float3 Direction;
    float3 Attenuation;
    float4 RangeFalloffOuterInner;
};

struct VentuzLight
{
    int Type;
    float3 DiffuseColor;
    float3 SpecularColor;
    float3 AmbientColor;
    float3 Position;
    float3 Direction;
    float3 Attenuation;
    float4 RangeFalloffOuterInner;
};

Once the structures have been defined, the actual binding is done as follows:

VentuzDirectionalLight DirLights[8]
<
   string SasBindAddress = "Ventuz.DirectionalLight[*]";
>;
VentuzPointLight PointLights[8]
<
   string SasBindAddress = "Ventuz.PointLight[*]";
>;
VentuzSpotLight SpotLights[8]
<
   string SasBindAddress = "Ventuz.SpotLight[*]";
>;
VentuzLight SpotLights[8]
<
   string SasBindAddress = "Ventuz.Light[*]";
>;

int DirectionalLightCount
<
   string SasBindAddress = "Ventuz.NumDirectionalLights";
>;
int PointLightCount
<
   string SasBindAddress = "Ventuz.NumPointLights";
>;
int SpotLightCount
<
   string SasBindAddress = "Ventuz.NumSpotLights";
>;
int LightCount
<
   string SasBindAddress = "Ventuz.NumLights";
>;

The Ventuz.NumXXXLights] bindings indicate how many lights are set in the corresponding Ventuz.XXXLight[*] binding. For example, if there are two point lights and one directional light in the scene, PointLightCount will be two, DirectionalLightCount will be one, SpotLightCount will be zero. and LightCount will be three.

Reducing the size of the arrays will work just fine, the shader will use
less constant registers, be shorter and faster, compile faster, but obviously will handle less lights.

When the shader is supposed to work only with certain types of lights, bind Ventuz.DirectionalLight[*] Ventuz.SpotLight[*] or Ventuz.PiontLight[*]. When the shader is supposed to handle all lights, binding
to Ventuz.Light[*] will result in a faster shader that uses much less constant registers.

The field RangeFallofOuterInner holds for information:

These four values are packed into a single vector to save constant registers in the shader.

Shaders emulating fixed function lighting can become quite inefficient. Be sure to exclude all unnecessary
features. The most efficient version is iterating over all Ventuz.Light[*] with an unrolled for loop,
in this case the compiler will use static branching.

The light Type is:

Texture Information

The following bindings give you information about all textures:

float4 TexSize[8] 
< 
    string SasBindAddress = "Ventuz.Textures[*].Size"; 
>;
float4x4 TexMap[8] 
< 
    string SasBindAddress = "Ventuz.Textures[*].Mapping"; 
>;
uint4 TexInfo[8] 
< 
    string SasBindAddress = "Ventuz.Textures[*].Info"; 
>;
int TexCount 
< 
    string SasBindAddress = "Ventuz.NumTextures"; 
>;

Ventuz.NumTextures indicate the number of used textures, or more precisely the index of the last
used texture plus one.

If information for less than 8 textures is required, the arrays can be reduced in size to save constant registers.

Ventuz.Textures[*].Size tells the size of the texture in pixels. A float4 must be used although only the x and y components are used.

Ventuz.Textures[*].Mapping hold the texture transform matrix.

Ventuz.Textures[*].Info contains various information about selected texture options. Interpreting this in
the pixel shader will always be inefficient, but it allows to create more flexible shaders.

Passing all texture stage shading information to the shader is impractical. Only a few selected options are
given in info.x and info.y:

The UV mapping options in info.z are

The sampling options in info.z are

Your shader should sample each texture only once. It should sample the same texture once in an if block and again in an else block since the shader compiler will usually create code that samples the texture twice
and then selects one of the results using arithmetic instructions, and that is unnecessary slow.

Adapting External Shaders

For a shader to work properly, a certain amount of functionality is required in the hosting application. Since the implementation of that functionality various from application to application, it is in general not possible to simply copy-and-paste the shader source code from another application into Ventuz and having it produce the same results without modifications. The following is a list of pointers based on the experiences on porting shaders to Ventuz:

See also: