Skip to content

Custom RenderTrees and the RenderInterface

jasu0x58 edited this page Apr 24, 2014 · 17 revisions

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.

The RenderInterface

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.

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

      // 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 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

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

})();
Clone this wiki locally