Table of Contents
The core idea of our approach is that the base shader defines
points where calls to user-defined functions may be inserted. We call
these places plugs, as they act like sockets where logic
may be added. Each plug has a name and a given set of parameters.
The effects can use special function names, starting with PLUG_
and followed by the plug name. These declarations will be found
and the renderer will insert appropriate calls to them from the base shader.
A trivial example of an effect that makes colors two times brighter
is below. This is a complete X3D file, so you can save
it as test.x3dv
and open with any tool supporting our
extensions, like view3dscene
.
#X3D V3.2 utf8
PROFILE Interchange
Shape {
appearance Appearance {
material Material { }
effects Effect {
language "GLSL"
parts EffectPart {
type "FRAGMENT"
url "data:text/plain,
void PLUG_fragment_modify(
inout vec4 fragment_color)
{
fragment_color.rgb *= 2.0;
}"
}
}
}
geometry Sphere { }
}
Our extensions to X3D are marked with the bold font in the example above.
The GLSL code inside our extensions is marked with the italic font.
The special GLSL function name PLUG_fragment_modify
indicates that we use the fragment_modify
plug.
This particular plug is called after calculating everything else for this pixel
(textures, lighting)
and allows to intuitively “modify the final pixel color”.
fragment_color
is an “inout” parameter, by modifying it
we modify the color that will be displayed on the screen.
A reference of all the plugs available in our implementation
is at the end of this paper, see Appendix A, Reference of available plugs.
For each plug, like this PLUG_fragment_modify
,
we define a list of parameters
and when it is called.
Many usage scenarios are possible:
The
Effect
nodes may use plug names defined inside the renderer internal shaders. This is the most usual case. It allows the authors to extend or override a particular shading parameter.The
Effect
nodes may also use the plug names defined in the previousEffect
nodes on the same shape. It is trivially easy (just add a “magic” comment) to define plugs in your own shader code. This way your own effects can be customized.Inside the renderer implementation, the same approach can be used to implement some internal effects. We have reimplemented many internal effects of our engine, like the fog, shadow maps (see our shadow mapping extensions for X3D [X3D Shadow Maps]) and the bump mapping to use our “plugs” approach. This made their implementation very clean, short and nicely separated from each other. It also proves that the authors have the power to implement similar effects easily by themselves.
Actually, there are even more possibilities.
We have been talking above about the “renderer internal shaders”,
but the truth is a little more flexible.
When you place a standard shader node
(like a ComposedShader
node for GLSL shaders) on the
Appearance.shaders
list,
then it replaces the internal renderer shaders.
If you define the same (or compatible) plugs inside your shader,
then the internal renderer effects are even added to your own
shader. Of course user effects are added to your shader too.
This way even the standard X3D shader nodes become more flexible.
Note that if you do not define any plugs inside your ComposedShader
node,
it continues to function as before — no effects
will be added.
New Effect
node holds information about
the source code and uniform values specific to a given effect.
The node specification below follows the style of
the X3D specification [X3D].
Effect : X3DChildNode SFString [] language "" # Language like "GLSL", "CG", "HLSL". # This effect will be used # only when the base renderer shader # uses the same language. SFBool [in,out] enabled TRUE # Easily turn on/off the effect. # You could also remove/add the node # from the scene, but often toggling # this field is easier for scripts. MFNode [] parts [] # EffectPart # Source code of the effect. # A number of uniform values may also be # declared inside this node.
Inside the Effect
node a number of uniform values may be defined,
passing any X3D value to the shader. Examples include passing
current world time or a particular texture to the shader.
Uniform values are declared exactly like described in the standard
X3D Programmable shaders component [X3D Shaders].
The effect source code is split into a number of parts:
EffectPart : X3DNode, X3DUrlObject SFString [] type "VERTEX" # Like ShaderPart.type: # allowed values are # VERTEX | GEOMETRY | FRAGMENT. MFString [in,out] url [] # The source code, like ShaderPart.url. # May come from an external file (url), # or inline (following "data:text/plain,"). # In XML encoding, may also be inlined in CDATA.
Inside the effect part source code, the functions that enhance
standard shaders behavior are recognized by names starting with PLUG_
.
Of course other functions can also be defined and used.
Uniform variables can be passed to the effect,
also varying variables can be passed between the vertex and fragment
parts, just like with standard shader nodes.
In a single EffectPart
node, many PLUG_
functions may be declared. However, all plug functions must be declared in the appropriate
effect type. For example, the fragment_modify
plug cannot be used
within a VERTEX
shader.
If the effect requires some processing per-vertex and some per-fragment,
it is necessary to use two EffectPart
nodes, with different types.
This allows to implement our system for
shading languages with
separate namespaces for vertex and fragment parts (like GLSL).
A single part may declare many variables and functions,
but it must be completely contained within a given shader type.
Note that it is completely reasonable to have an EffectPart
node
with source code that does not define any PLUG_xxx
functions.
Such EffectPart
node may be useful for defining shading language
utility functions, used by other effect parts.
For shading languages that have separate compilation units (like the OpenGL Shading Language) the implementation may choose to place each effect part in such separate unit. This forces the shader code to be cleaner, as you cannot use undeclared functions and variables from other parts. It also allows for cleaner error detection (parsing errors will be detected inside the given unit).