-
Notifications
You must be signed in to change notification settings - Fork 25
Custom RenderTrees and the RenderInterface
Note: This is an experimental feature that isn't officially supported yet! An overhaul of this interface is planned for the near future, for that reason any feedback on the current implementation would be much appreciated!
In xml3d.js a RenderTree is a container for one or more RenderPasses that are typically rendered in a fixed hierarchy. A RenderPass may draw objects to the screen, draw a post processing effect, or can even be used to perform pre-processing steps, such as pre-render frustum culling, without actually drawing anything. They can be used to complement the internal rendering pass or to replace it entirely, giving the user full control over how the scene is rendered.
By creating your own RenderTree you are essentially replacing XML3D's internal rendering process with your own, giving you much more control over how the scene is drawn. This enables even complicated effects like SSAO, which is part of the internal RenderTree.
xml3d.js provides a RenderInterface object to define and use render trees, as well as set various rendering options. Each XML3D element provides its own RenderInterface and any changes made to it are specific to that XML3D element:
var renderInterface = document.getElementById("myXml3dElement").getRenderInterface();
A RenderInterface currently provides the following members and functions:
-
scene
- The scene tree including all lights, groups, views and meshes as they are defined in the DOM. -
context
- A GLContext object, which provides access to the WebGL rendering context and several helpful services. -
getRenderTree
- Returns the current render tree. Initially this will be the internal ForwardRenderTree that XML3D uses to draw the scene. -
setRenderTree
- Accepts a custom render tree to be used for drawing the scene in all subsequent frames. -
createRenderTarget(opt)
- Creates a new GLRenderTarget that can be rendered to and (optionally) as a texture in subsequent render passes (see below for a description of theopt
parameter) -
createScaledRenderTarget(maxDimension, opt)
- Creates a down scaled GLRenderTarget with a set maximum size. Useful if you don't need full precision during rendering (eg. when Gaussian blurring the scene) -
createFullscreenQuad
- Creates a simple quad mesh that covers the entire screen. Useful for post-processing effects. -
createSceneRenderPass(target)
- Creates a copy of the same scene render pass that XML3D uses internally to draw the scene. Useful if you want to draw the scene normally at some point during your rendering process.target
should be a GLRenderTarget or left blank if you want it to render directly to the canvas. -
getShaderProgram(name)
- gets or creates a copy of the shader program with the given name, which must be registered with theXML3D.materials.register
function prior to being created.
The opt
parameter in creating a RenderTarget takes the following parameters, which correspond to WebGL framebuffer options:
-
width
- the width of the target, can berenderInterface.context.canvasTarget.width
to match the canvas size -
height
- the height of the target, can berenderInterface.context.canvasTarget.height
to match the canvas size - wrapS
- wrapT
- minFilter
- magFilter
-
colorFormat
- should begl.RGBA
usually. If left blank this RenderTarget will not have a color buffer. -
colorType
-gl.UNSIGNED_BYTE
normally. Usegl.FLOAT
if you want a floating point buffer, note you'll have to enable the appropriate WebGL extensions first. NOTE: In WebGL it's currently not possible (or very unreliable) to use a gl.FLOAT target as an input texture for a shader! -
colorAsRenderbuffer
- should generally befalse
or blank, since you'll often want to use this output as an input texture for a subsequent shader -
depthFormat
- should begl.DEPTH_COMPONENT_16
or blank if the target doesn't require a depth buffer -
depthAsRenderbuffer
- should betrue
unless the depth buffer is going to be used as a texture in a shader -
stencilFormat
- controls the format of the stencil buffer. If left blank this RenderTarget won't have a stencil buffer.
RenderTrees should be defined using the following interface:
var MyRenderTree = function(renderInterface) {
XML3D.webgl.BaseRenderTree.call(this, renderInterface);
// Intialization code (eg. building render targets, render passes, etc.) goes here
this.createRenderPasses();
};
XML3D.createClass(MyRenderTree , XML3D.webgl.BaseRenderTree);
XML3D.extend(MyRenderTree.prototype, {
createRenderPasses : function() {
var screenTarget = this.renderInterface.context.canvasTarget;
this.mainRenderPass = new CustomRenderPass(renderInterface, screenTarget);
},
render : function(scene) {
// Any per-frame pre-rendering operations can be performed here, before any passes are rendered
// Remember to set all your render passes to unprocessed or they wont be drawn again!
this.mainRenderPass.setProcessed(false);
// The base render function simply calls .render() on the mainRenderPass
XML3D.webgl.BaseRenderTree.prototype.render.call(this, scene);
// Any post-rendering operations can be performed here, after all passes have been rendered
}
});
The RenderInterface should be passed as a constructor argument to enable access to the scene and the WebGL context. After the render pipeline has been defined an instance of it can be created and passed to the RenderInterface object:
var customTree;
var defaultTree;
var renderInterface;
function createCustomRenderTree() {
var xml3dElement = document.getElementById("myXml3dElement");
renderInterface = xml3dElement.getRenderInterface();
// The default pipeline can be stored to allow toggling the custom pipeline on and off later
defaultTree = renderInterface.getRenderTree();
customTree = new MyRenderTree(renderInterface);
};
function toggleCustomRenderTree() {
renderInterface.setRenderTree(customTree);
}
function toggleDefaultRenderTree() {
renderInterface.setRenderTree(defaultTree);
}
A RenderTree typically contains a mainRenderPass, which is actually the actual RenderPass drawn each frame (ie. the RenderPass that draws its output to the screen). This mainRenderPass may reference other RenderPasses as pre-passes which must be drawn before it. This is typically the case when a RenderPass uses the output of another RenderPass as input (eg. in a two-pass Gaussian blur).
Lets take a look at a simple one-step RenderPass:
var CustomRenderPass = function (renderInterface, output, opt) {
XML3D.webgl.BaseRenderPass.call(this, renderInterface, output, opt);
}
XML3D.createClass(CustomRenderPass, XML3D.webgl.BaseRenderPass);
XML3D.extend(CustomRenderPass.prototype, {
render : function(scene) {
var glContext = this.renderInterface.context.gl;
this.output.bind(); // Bind the output Framebuffer
var objectsToDraw = scene.ready;
// All the code necessary to draw the desired output to the output buffer should be included here
}
});
Custom render passes should always "extend" the BaseRenderPass class as shown above. Each RenderPass requires an output (typically a GLRenderTarget). The render() function can (and should) be overridden and should ensure that the output target is filled.
The opt parameter may contain the following members:
-
inputs
- A map of input names (typically the names of texture attributes in a shader) to input values. Typically these inputs would be GLRenderTargets provided by other RenderPasses, which should be registered with this pass through addPrePass during the RenderTree creation. -
id
- An optional string id for this RenderPass - Any other data that your RenderPass can make use of
Lets take a look at all the code required to create a simple box blur effect in post-processing. This includes registering the custom shaders, creating and linking the RenderPasses, creating the RenderTree and activating it through the RenderInterface.
(function() {
// Register the shader
XML3D.materials.register("box-blur-shader", {
vertex: [
"attribute vec3 position;",
"void main(void) {",
" gl_Position = vec4(position, 1.0);",
"}"
].join("\n"),
fragment: [
"uniform sampler2D sInTexture;",
"uniform vec2 canvasSize;",
"uniform vec2 blurOffset;",
"void main(void) {",
" vec2 texcoord = (gl_FragCoord.xy / canvasSize.xy);",
" vec4 sum = vec4(0.0);",
" float blurSizeY = blurOffset.y / canvasSize.y;",
" float blurSizeX = blurOffset.x / canvasSize.x;",
" sum += texture2D(sInTexture, vec2(texcoord.x, texcoord.y - 2.0*blurSizeY));",
" sum += texture2D(sInTexture, vec2(texcoord.x, texcoord.y - blurSizeY));",
" sum += texture2D(sInTexture, vec2(texcoord.x, texcoord.y + blurSizeY));",
" sum += texture2D(sInTexture, vec2(texcoord.x, texcoord.y + 2.0*blurSizeY));",
" sum += texture2D(sInTexture, vec2(texcoord.x - 2.0*blurSizeX, texcoord.y));",
" sum += texture2D(sInTexture, vec2(texcoord.x - blurSizeX, texcoord.y));",
" sum += texture2D(sInTexture, vec2(texcoord.x + blurSizeX, texcoord.y));",
" sum += texture2D(sInTexture, vec2(texcoord.x + 2.0*blurSizeX, texcoord.y));",
" gl_FragColor = sum / 8.0;",
"}"
].join("\n"),
uniforms: {
canvasSize : [512, 512],
blurOffset : [1.0, 1.0]
},
samplers: {
sInTexture : null
}
});
// Define the box blur RenderPass
var BoxBlurPass = function (renderInterface, output, opt) {
XML3D.webgl.BaseRenderPass.call(this, renderInterface, output, opt);
// XML3D provides a Fullscreen Quad as a helper for post processing effects
this.fullscreenQuad = renderInterface.createFullscreenQuad();
// The programFactory can be used to get instances of custom shaders that were registered through XML3D.materials.register
this.shaderProgram = renderInterface.getShaderProgram(opt.shader);
this.blurOffset = [1.0, 1.0];
};
XML3D.createClass(BoxBlurPass, XML3D.webgl.BaseRenderPass);
XML3D.extend(BoxBlurPass.prototype, {
render : function(scene) {
var gl = this.renderInterface.context.gl;
this.output.bind(); // Bind the output GLRenderTarget
this.shaderProgram.bind(); // Bind the box blur shader
gl.clear(gl.COLOR_BUFFER_BIT);
gl.disable(gl.DEPTH_TEST);
// Set the uniform variables required by the shader
var uniformVariables = {};
uniformVariables.canvasSize = [this.output.width, this.output.height];
uniformVariables.sInTexture = [this.inputs.sInTexture.colorTarget.handle];
uniformVariables.blurOffset = this.blurOffset;
this.shaderProgram.setSystemUniformVariables(Object.keys(uniformVariables), uniformVariables);
// Draw the full screen quad using the given shader program
this.fullscreenQuad.draw(this.shaderProgram);
// It's good practice to undo any changes you've made to the GL state after rendering
// failure to do so can have unintended side effects in subsequent render passes!
this.shaderProgram.unbind();
this.output.unbind();
gl.enable(gl.DEPTH_TEST);
},
setProcessed : function(processed) {
this.processed = processed;
this.prePasses[0].processed = processed;
}
});
// Define the RenderTree
var BlurExampleTree = function(renderInterface) {
XML3D.webgl.BaseRenderTree.call(this, renderInterface);
// Intialization code (eg. building render targets, render passes, etc.) goes here
this.createRenderPasses();
};
XML3D.createClass(BlurExampleTree, XML3D.webgl.BaseRenderTree);
XML3D.extend(BlurExampleTree.prototype, {
createRenderPasses : function() {
var context = this.renderInterface.context;
// Create the Framebuffer that we'll need to draw the blur effect
var backBuffer = this.renderInterface.createRenderTarget({
width: context.canvasTarget.width,
height: context.canvasTarget.height,
colorFormat: context.gl.RGBA,
depthFormat: context.gl.DEPTH_COMPONENT16,
depthAsRenderbuffer: true,
stencilFormat: null
});
// Create an instance of the standard scene render pass that XML3D uses to draw all the objects
// In this case we draw them to an offscreen buffer to be used as input for the blur pass
var drawObjectsPass = this.renderInterface.createSceneRenderPass(backBuffer);
var opts = {
inputs : { 'sInTexture' : backBuffer },
shader : "box-blur-shader",
id : "boxblur"
};
// canvasTarget is always available and draws to the screen
var boxBlurPass = new BoxBlurPass(this.renderInterface, context.canvasTarget, opts);
// Add the regular scene drawing pass as a pre-pass to our box blur
boxBlurPass.addPrePass(drawObjectsPass);
// mainRenderPass is required and should always be the final render pass that draws to the screen, in this case our box blur pass
this.mainRenderPass = boxBlurPass;
},
// Since we don't need to do anything special here this render function could be left out, but we include it
// for completeness
render : function(scene) {
this.mainRenderPass.setProcessed(false);
XML3D.webgl.BaseRenderTree.prototype.render.call(this, scene);
}
});
// Create and activate an instance of the box blur RenderTree
window.addEventListener("load", function() {
var xml3dElement = document.getElementById("myXml3dElement");
var renderInterface = xml3dElement.getRenderInterface();
var boxBlurRenderTree = new BlurExampleTree(renderInterface);
renderInterface.setRenderTree(boxBlurRenderTree);
});
})();