Skip to content

Custom RenderTrees and the RenderInterface

Christian Schlinkmann edited this page Feb 4, 2016 · 17 revisions

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!

What are RenderTrees?

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.

The RenderInterface

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 the opt 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 the XML3D.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 be renderInterface.context.canvasTarget.width to match the canvas size
  • height - the height of the target, can be renderInterface.context.canvasTarget.height to match the canvas size
  • wrapS
  • wrapT
  • minFilter
  • magFilter
  • colorFormat - should be gl.RGBA usually. If left blank this RenderTarget will not have a color buffer.
  • colorType - gl.UNSIGNED_BYTE normally. Use gl.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 be false or blank, since you'll often want to use this output as an input texture for a subsequent shader
  • depthFormat - should be gl.DEPTH_COMPONENT_16 or blank if the target doesn't require a depth buffer
  • depthAsRenderbuffer - should be true 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.

Defining a custom RenderTree

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);
}

Creating RenderPasses

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

Full example: Post-processing box blur

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);
    });

})();
Clone this wiki locally