-
Notifications
You must be signed in to change notification settings - Fork 25
Custom RenderTrees and the RenderInterface
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.
xml3d.js provides a RenderInterface object to define and use render pipelines, 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 - An XML3D context object, which provides access to the WebGL rendering context and several helpful services.
- getRenderTree/getRenderPipeline(deprecated) - Returns the current render pipeline. Initially this will be the internal ForwardRenderTree.
- setRenderTree/setRenderPipeline(deprecated) - Accepts a custom render pipeline to be used for drawing the scene in all subsequent frames.
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
// 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 FrameBuffer). 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 Framebuffer targets provided by other RenderPasses, which should be registered through addPrePass during the RenderTree creation.
- id - An optional string id for this RenderPass
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.shaders.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;",
"const float blurSize = 1.0/512.0;",
"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 = new XML3D.webgl.FullscreenQuad(renderInterface.context);
// The programFactory can be used to get instances of custom shaders that were registered through XML3D.shaders.register
this.shaderProgram = renderInterface.context.programFactory.getProgramByName(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 Framebuffer
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(uniformVariables.keys, uniformVariables);
// Draw the full screen quad using the given shader program
this.fullscreenQuad.draw(this.program);
// It's good practice to "clean up" the WebGL state after each pass
this.shaderProgram.unbind();
this.output.unbind();
gl.enable(gl.DEPTH_TEST);
}
});
// 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 = new XML3D.webgl.GLRenderTarget(context, {
width: context.canvasTarget.width,
height: context.canvasTarget.height,
colorFormat: context.gl.RGBA,
depthFormat: null,
stencilFormat: null
});
// ForwardRenderPass is provided by XML3D and simply draws all objects to the given target
// In this case we draw them to an offscreen buffer to be used as input for the blur pass
var drawObjectsPass = new XML3D.webgl.ForwardRenderPass(this.renderInterface, backBuffer);
var opts = {
inputs : { sInTexture : backBuffer },
shader : "horizontal-blur-shader",
id : "horizontalBlur"
};
// context.canvasTarget is always available and draws to the screen
var boxBlurPass = new BoxBlurPass(this.renderInterface, context.canvasTarget, opts);
boxBlurPass.addPrePass(drawObjectsPass);
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) {
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);
});
})();