API Docs for: 1.4.0
Show:

File: src\render\GLRenderManager.ts

/**
* @module Kiwi
* @submodule Renderers 
* @main Renderers
* @namespace Kiwi.Renderers
*/

module Kiwi.Renderers {

	/**
	* Manages all rendering using WebGL.
	* Directly manages renderer objects, including factory methods
	* for their creation. 
	* Creates manager objects for shaders and textures.
	* Manages gl state at game initialisation, at state start and end,
	* and per frame.
	* Runs the recursive scene graph rendering sequence every frame. 
	* @class GLRenderManager
	* @extends IRenderer
	* @constructor
	* @param game {Kiwi.Game} The game that this renderer belongs to.
	* @return {Kiwi.Renderers.GLRenderManager}
	*/
	export class GLRenderManager implements IRenderManager {

		constructor(game: Kiwi.Game) {
			this._game = game;
			this._currentBlendMode = new Kiwi.Renderers.GLBlendMode(this._game.stage.gl, {mode:"DEFAULT"} );
		}

		/**
		* Initialises all WebGL rendering services
		* @method boot
		* @public
		*/
		public boot() {
			this._textureManager = new GLTextureManager();
			this._shaderManager = new Kiwi.Shaders.ShaderManager();
			//this.filters = new Kiwi.Filters.GLFilterManager(this._game, this._shaderManager);
			this._init();
			
		}

		/**
		* The type of object that this is.
		* @method objType
		* @return {string} "GLRenderManager"
		* @public
		*/
		public objType() {
			return "GLRenderManager";
		}

		/**
		* The game that this renderer is used with.
		* @property _game
		* @type Game
		* @private
		*/
		private _game: Kiwi.Game;

		/**
		* The texture manager object used to allocate GL Textures.
		* @property _textureManager
		* @type Kiwi.Renderes.GLTextureManager
		* @private
		*/
		private _textureManager: GLTextureManager;

		/**
		* The shader manager object used to allocate GL Shaders.
		* @property _shaderManager
		* @type Kiwi.Renderes.GLShaderManager
		* @private
		*/

		private _shaderManager: Kiwi.Shaders.ShaderManager;

		//public filters: Kiwi.Filters.GLFilterManager;

		/**
		* The stage resolution in pixels
		* @property _stageResolution
		* @type Float32Array
		* @private
		*/
		private _stageResolution: Float32Array;

		/**
		* The renderer object that is in use during a rendering batch.
		* @property _currentRenderer
		* @type Kiwi.Renderers.Renderer
		* @private
		*/
		private _currentRenderer: Renderer = null;

		/**
		* The current blend mode.
		* @property _currentBlendMode
		* @type Kiwi.Renderers.GLBlendMode
		* @private
		* @since 1.1.0
		*/
		private _currentBlendMode: GLBlendMode;

		/**
		* Tally of number of entities rendered per frame 
		* @property _entityCount
		* @type number
		* @default 0
		* @private
		*/
		private _entityCount: number = 0;

		/**
		* Tally of number of draw calls per frame
		* @property numDrawCalls
		* @type number
		* @default 0
		* @public
		*/
		public numDrawCalls: number = 0;

		/**
		* Maximum allowable sprites to render per frame.
		* Note: Not currently used  - candidate for deletion
		* @property _maxItems
		* @type number
		* @default 1000
		* @private
		*/
		private _maxItems: number = 1000;

		/**
		* Camera matrix used on graphics card
		* @property camMatrix
		* @type Float32Array
		* @public
		*/
		public camMatrix: Float32Array;

		/**
		* Geometry data used to create `camMatrix`
		* @property _camMatrixOffset
		* @type Kiwi.Geom.Matrix
		* @private
		*/
		private _camMatrixOffset: Kiwi.Geom.Matrix;

		/**
		* The most recently bound texture atlas.
		* @property _currentTextureAtlas
		* @type TextureAtlas
		* @private
		*/
		private _currentTextureAtlas: Kiwi.Textures.TextureAtlas = null;

		/**
		* Adds a texture to the Texture Manager. 
		* @method addTexture
		* @param {WebGLRenderingContext} gl
		* @param {Kiwi.Textures.TextureAtlas} atlas
		* @public
		*/
		public addTexture(gl: WebGLRenderingContext, atlas: Kiwi.Textures.TextureAtlas) {
			this._textureManager.uploadTexture(gl, atlas);
		}

		/**
		* An array of renderers.
		*
		* Shared renderers are used for batch rendering.
		* Multiple gameobjects can use the same renderer instance and add
		* rendering info to a batch rather than rendering individually. 
		* This means only one draw call is necessary to render a number of
		* objects. The most common use of this is standard 2d sprite rendering,
		* and the TextureAtlasRenderer is added by default as a shared
		* renderer. Sprites, StaticImages and Tilemaps (core gameobjects)
		* can all use the same renderer/shader combination and be drawn as
		* part of the same batch.
		*
		* Custom gameobjects can also choose to use a shared renderer, fo example in the case that a custom gameobject's rendering requirements matched the TextureAtlasRenderer
		* capabilities.
		*  
		* @property _sharedRenderers
		* @type Array
		* @private
		*/
		private _sharedRenderers: any = {};

		/**
		* Adds a renderer to the sharedRenderer array.
		*
		* The rendererID is a string that must match a renderer property
		* of the Kiwi.Renderers object. If a match is found and an instance
		* does not already exist, then a renderer is instantiated and added
		* to the array.
		* @method addSharedRenderer
		* @param {string} rendererID
		* @param {object} params
		* @return {boolean} success
		* @public
		*/

		public addSharedRenderer(rendererID:string,params:any = null):boolean {
			//does renderer exist?
			if (Kiwi.Renderers[rendererID]) {

				//already added?
				if (!(rendererID in this._sharedRenderers)) {
					this._sharedRenderers[rendererID] = new Kiwi.Renderers[rendererID](this._game.stage.gl,this._shaderManager,params);
					return true;
				}
			}
			return false;
		}


		/**
		* Adds a cloned renderer to the sharedRenderer array.
		* The rendererID is a string that must match a renderer property of
		* the Kiwi.Renderers object. The cloneID is the name for the
		* cloned renderer.
		*
		* If a match is found and an instance does not already exist,
		* then a renderer is instantiated and added to the array.
		*
		* Cloned shared renderers are useful if some items in your scene
		* will use a special shader or blend mode, but others will not.
		* You can subsequently access the clones with a normal
		* `requestSharedRenderer()` call. You should use this instead of
		* `requestRendererInstance()` whenever possible, because shared
		* renderers are more efficient than instances.
		*
		* @method addSharedRendererClone
		* @param {string} rendererID
		* @param {string} cloneID
		* @param {object} params
		* @return {boolean} success
		* @public
		* @since 1.1.0
		*/
		public addSharedRendererClone(rendererID: string, cloneID: string, params: any = null): boolean {
			if (typeof params === "undefined") { params = null; }
			//does renderer exist?
			if (Kiwi.Renderers[rendererID]) {
				//already added?
				if (!(cloneID in this._sharedRenderers)) {
					this._sharedRenderers[cloneID] = new Kiwi.Renderers[rendererID](this._game.stage.gl, this._shaderManager, params);
					return true;
				}
			}
			return false;
		}


		/**
		* Requests a shared renderer. A game object that wants to use a shared
		* renderer uses this method to obtain a reference to the shared
		* renderer instance.
		* @method requestSharedRenderer
		* @param {string} rendererID
		* @param {object} params
		* @return {Kiwi.Renderers.Renderer} A shared renderer
		*	or null if none found.
		* @public
		*/

		public requestSharedRenderer(rendererID: string,params:any = null): Kiwi.Renderers.Renderer {
			var renderer: Kiwi.Renderers.Renderer = this._sharedRenderers[rendererID];
			if (renderer) {
				return renderer;
			} else {
				if (this.addSharedRenderer(rendererID,params)) {
					return this._sharedRenderers[rendererID];
				} else {
					Kiwi.Log.log("No renderer called " + rendererID, '#renderer', '#webgl');
				}
			}

			//failed request
			return null;
		}


		/**
		* Requests a new renderer instance. This factory method is the only
		* way gameobjects should instantiate their own renderer.
		*
		* The rendererID is a string that must match a renderer property
		* of the Kiwi.Renderers object. If a match is found then a renderer
		* is instantiated and returned. Gameobjects which have rendering
		* requirements that do not suit batch rendering use this technique.
		* @method requestRendererInstance
		* @param {string} rendererID The name of the requested renderer
		* @param {object} params
		* @return {Kiwi.Renderers.Renderer} A renderer or null if none found.
		* @public
		*/

		public requestRendererInstance(rendererID: string, params: any = null): Kiwi.Renderers.Renderer {
			if (rendererID in Kiwi.Renderers) {
				var renderer = new Kiwi.Renderers[rendererID](this._game.stage.gl, this._shaderManager, params); 
				return renderer
			} else {
				Kiwi.Log.log("No renderer with id " + rendererID + " exists", '#renderer', '#webgl');
			}
			return null; //fail
		}

		/*
		public get filtersEnabled(): boolean {
			return this._filtersEnabled;
		}

		public set filtersEnabled(val: boolean) {
			this._filtersEnabled = val;
		}

		private _filtersEnabled: boolean = false;
		
		/**
		* Performs initialisation required for single game instance -
		* happens once, at bootup. Sets global GL state.
		* Initialises managers for shaders and textures.
		* Instantiates the default shared renderer (`TextureAtlasRenderer`)
		* @method _init
		* @private
		*/
		private _init() {
			Kiwi.Log.log("Initialising WebGL", '#renderer', '#webgl');

			var gl: WebGLRenderingContext = this._game.stage.gl;

			//init stage and viewport
			this._stageResolution = new Float32Array([this._game.stage.width, this._game.stage.height]);

			// Manually override scaling under CocoonJS
			if( this._game.deviceTargetOption === Kiwi.TARGET_COCOON ) {
				this.scaleViewport( gl, this._game.stage.scaleType, window.innerWidth, window.innerHeight );
			}
			else {
				this.scaleViewport( gl, Kiwi.Stage.SCALE_NONE, this._game.stage.width, this._game.stage.height );
			}

			//set default gl state
			gl.enable(gl.BLEND);
			this._switchBlendMode(gl, this._currentBlendMode);

			//shader manager
			this._shaderManager.init(gl, "TextureAtlasShader");

			//camera matrix
			this.camMatrix = new Float32Array( [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ] );
			this._camMatrixOffset = new Kiwi.Geom.Matrix();

			//stage res needs update on stage resize
			this._game.stage.onResize.add(function (width, height) {
				this._stageResolution = new Float32Array([width, height]);
				if (this.currentRenderer) this._currentRenderer.updateStageResolution(gl, this._stageResolution);
				//this.filters.updateFilterResolution(gl,width, height);
				// Manually override scaling under CocoonJS
				if( this._game.deviceTargetOption === Kiwi.TARGET_COCOON ) {
					this.scaleViewport( gl, this._game.stage.scaleType, window.innerWidth, window.innerHeight );
				}
				else {
					this.scaleViewport( gl, Kiwi.Stage.SCALE_NONE, width, height );
				}
			},this);

			/* if (this.filtersEnabled && !this.filters.isEmpty) {
				this.filters.enableFrameBuffers(gl);
			}*/

		}

		/**
		* Scales the viewport according to a scale mode and space dimensions.
		*
		* This is used internally for compatibility with CocoonJS and should not be called.
		* @method scaleViewport
		* @param gl {WebGLRenderingContext}
		* @param mode {number} Scale mode; should be either
		*	Kiwi.Stage.SCALE_FIT, Kiwi.Stage.SCALE_STRETCH, or
		*	Kiwi.Stage.SCALE_NONE. Defaults to Kiwi.Stage.SCALE_NONE
		* @param width {number} Width of the target space
		* @param height {number} Height of the target space
		* @public
		* @since 1.1.1
		*/
		public scaleViewport( gl:WebGLRenderingContext, mode:number, width:number, height:number ) {
			var offset = new Kiwi.Geom.Point(0, 0);
			switch( mode ) {
				case Kiwi.Stage.SCALE_FIT:
					// Compute aspect ratios
					var arStage = this._game.stage.width / this._game.stage.height;
					var arSpace = width / height;
					if(arStage < arSpace) {
						// Width is too wide
						var newWidth = height * arStage;
						offset.x = (width - newWidth) / 2;
						// Compress target space
						width = newWidth;
					}
					else {
						// Height is too wide
						var newHeight = width / arStage;
						offset.y = (height - newHeight) / 2;
						// Compress target space
						height = newHeight;
					}
					break;
				case Kiwi.Stage.SCALE_STRETCH:
					break;
				case Kiwi.Stage.SCALE_NONE:
					width = this._game.stage.width;
					height = this._game.stage.height;
					break;
				default:
					break;
			}

			gl.viewport(offset.x, offset.y, width, height);
		}


		/**
		* Performs initialisation required when switching to a different state.
		* Called when a state has been switched to.
		* The textureManager is told to clear its contents from video memory,
		* then rebuild its cache of textures from the state's texture library.
		* @method initState
		* @public
		*/

		public initState(state: Kiwi.State) {
			this._textureManager.clearTextures(this._game.stage.gl);
			this._textureManager.uploadTextureLibrary(this._game.stage.gl, state.textureLibrary);
		}

		/**
		* Performs cleanup required before switching to a different state.
		* Called whwn a state is about to be switched from.
		* The textureManager is told to empty its cache.
		* @method endState
		* @param state {Kiwi.State}
		* @public
		*/

		public endState(state: Kiwi.State) {
			this._textureManager.clearTextures(this._game.stage.gl);
			Kiwi.Log.log("Ending WebGL on State", '#renderer', '#webgl');
		}

		/**
		* Manages rendering of the scene graph - called once per frame.
		* Sets up per frame gl uniforms such as the view matrix and
		* camera offset. Clears the current renderer ready for a new batch. 
		* Initiates recursive render of scene graph starting at the root. 
		* @method render
		* @param camera {Camera}
		* @public
		*/
		public render(camera: Kiwi.Camera) {

			var gl: WebGLRenderingContext = this._game.stage.gl;

			//clear stage every frame
			var col = this._game.stage.normalizedColor;
			// Colour must be multiplied by alpha to create consistent results.
			// This is probably due to browsers implementing an inferior
			// blendfunc: ONE, ONE_MINUS_SRC_ALPHA is most common, and gives
			// bad results with alphas. When this is used on a partially
			// transparent game canvas, it does not blend correctly.
			// Without being able to override the browser's own object renderer,
			// this is a necessary kludge. The "clean" solution is as follows:
			// gl.clearColor(col.r, col.g, col.b, col.a);
			gl.clearColor(col.r * col.a, col.g * col.a, col.b * col.a, col.a);
			gl.clear(gl.COLOR_BUFFER_BIT);

			// Reset current renderer.
			// This prevents runtime created shaders from being uploaded
			// and the render manager failing to notice, causing crashes.
			this._currentRenderer = null;

			// Stop drawing if there is nothing to draw
			if (this._game.states.current.members.length == 0) {
				return;
			}

			// Reset stats
			this.numDrawCalls = 0;
			this._textureManager.numTextureWrites = 0;
			this._entityCount = 0;

			// Set cam matrix uniform
			var cm: Kiwi.Geom.Matrix = camera.transform.getConcatenatedMatrix();
			var ct: Kiwi.Geom.Transform = camera.transform;
			this._camMatrixOffset.identity();
			this._camMatrixOffset.translate(
				-ct.anchorPointX, -ct.anchorPointY );
			this._camMatrixOffset.prependMatrix( cm );

			// Overwrite cam matrix properties rather than recreating matrix.
			// This is necessary to keep the cam matrix synchronised
			// with other renderers: if there is only one render batch,
			// then `enable` will only pass the cam matrix on the first frame,
			// and subsequent camera movements will not be passed to the shader.
			this.camMatrix[ 0 ] = this._camMatrixOffset.a;
			this.camMatrix[ 1 ] = this._camMatrixOffset.b;
			this.camMatrix[ 3 ] = this._camMatrixOffset.c;
			this.camMatrix[ 4 ] = this._camMatrixOffset.d;
			this.camMatrix[ 6 ] = this._camMatrixOffset.tx;
			this.camMatrix[ 7 ] = this._camMatrixOffset.ty;

			// Mandate blend mode in CocoonJS
			// This must be called per-frame, because CocoonJS seems to
			// interfere with blend modes on a per-frame basis.
			if( this._game.deviceTargetOption == Kiwi.TARGET_COCOON ) {
				this._switchBlendMode( gl, this._currentBlendMode );
			}

			this.collateRenderSequence();
			this.collateBatches();
			this.renderBatches(gl, camera);

			/*if (this._filtersEnabled && !this.filters.isEmpty) {
				this.filters.applyFilters(gl);
				gl.useProgram(this._shaderManager.currentShader.shaderProgram);
				gl.bindTexture(gl.TEXTURE_2D, this._currentTextureAtlas.glTextureWrapper.texture);
			}*/
		}

		private _sequence: any[];
		private _batches: any[];


		/**
		* Creates a new render sequence
		* @method collateRenderSequence
		* @public
		*/
		public collateRenderSequence() {
			this._sequence = [];
			var root: IChild = this._game.states.current;
			this.collateChild(root);
		}

		/**
		* Adds a child to the render sequence
		* (may be a group with children of its own).
		* @method collateChild
		* @public
		*/
		public collateChild(child:IChild) {

			// Do not render non-visible objects or their children
			if( !child.visible) return;
			
			if (child.childType() === Kiwi.GROUP) {
				for (var i = 0; i < (<Kiwi.Group>child).members.length; i++) {
					this.collateChild( (<Kiwi.Group>child).members[i]);
				}
			} else {
				var entity: Entity = <Entity>child;
				this._sequence.push({
					entity: entity,
					renderer: entity.glRenderer,
					shader: entity.glRenderer.shaderPair,
					isBatchRenderer: entity.glRenderer.isBatchRenderer,
					texture: entity.atlas
				});
			}
		}

		/**
		* Sorts the render sequence into batches.
		* Each batch requires the same renderer/shader/texture combination. 
		* @method collateBatches
		* @public
		*/
		public collateBatches() {
			var currentRenderer: Renderer = null;
			var currentShader: Shaders.ShaderPair = null;
			var currentTexture: Textures.TextureAtlas = null;

			this._batches = [];
			var batchIndex:number;

			for (var i = 0; i < this._sequence.length; i++) {
				if (!this._sequence[i].isBatchRenderer ||
					this._sequence[i].renderer !== currentRenderer ||
					this._sequence[i].shader !== currentShader ||
					this._sequence[i].texture !== currentTexture) {
					//create a new batch
					var batchIndex = this._batches.push(new Array()) - 1;
					currentRenderer = this._sequence[i].renderer; 
					currentShader = this._sequence[i].shader;
					currentTexture = this._sequence[i].texture;

				} 
				this._batches[batchIndex].push(this._sequence[i]);
			}
		}

		/**
		* Renders all the batches 
		* @method renderBatches
		* @param {WebGLRenderingContext} gl
		* @param {Kiwi.Camera} camera
		* @public
		*/
		public renderBatches(gl: WebGLRenderingContext, camera) {
			for (var i = 0; i < this._batches.length; i++)
				this.renderBatch( gl, this._batches[i], camera );
		}

		/**
		* Renders a single batch 
		* @method renderBatch
		* @param {WebGLRenderingContext} gl
		* @param {object} batch
		* @param {Kiwi.Camera} camera
		* @public
		*/
		public renderBatch(gl, batch, camera) {
			// Acquire renderer
			var rendererSwitched = false;
			if (batch[0].entity.glRenderer !== this._currentRenderer) {
				rendererSwitched = true;
				this._switchRenderer(gl, batch[0].entity);
			}

			// Clear renderer for fresh data
			this._currentRenderer.clear(gl, { camMatrix: this.camMatrix });

			// Call render functions
			for( var i = 0;  i < batch.length;  i++ )
				batch[i].entity.renderGL(gl, camera);

			// Upload textures
			if( batch[0].entity.atlas !== this._currentTextureAtlas  ||  batch[0].entity.atlas.dirty ||  (rendererSwitched  &&  batch[0].entity.atlas == this._currentTextureAtlas) )
				this._switchTexture(gl, batch[0].entity);

			// Manage blend mode
			// We must always apply BlendMode under CocoonJS, because some (but not all) operations on other canvases may silently change the blend mode and not change it back.
			if( !this._currentBlendMode.isIdentical( batch[0].entity.glRenderer.blendMode )  ||  this._currentBlendMode.dirty  ||  this._game.deviceTargetOption === Kiwi.TARGET_COCOON )
				this._switchBlendMode(gl, batch[0].entity.glRenderer.blendMode);

			// Render
			this._currentRenderer.draw(gl);
		}

		/**
		* Calls the render function on a single entity
		* @method renderEntity
		* @param {WebGLRenderingContext} gl
		* @param {Kiwi.Entity} entity
		* @param {Kiwi.Camera} camera
		* @public
		* @deprecated Used internally; should not be called from external functions
		*/
		public renderEntity(gl: WebGLRenderingContext, entity: any, camera) {
			this.renderBatch( gl, [entity], camera );
		}

		/**
		* Ensures the atlas and renderer needed for a batch is setup
		* @method setupGLState
		* @param {WebGLRenderingContext} gl
		* @public
		* @deprecated Used internally; should not be called from external functions.
		*/
		public setupGLState(gl:WebGLRenderingContext,entity) {
			if (entity.atlas !== this._currentTextureAtlas) this._switchTexture(gl, entity);
			if (entity.glRenderer !== this._currentRenderer) this._switchRenderer(gl, entity);
		}

		/**
		* Switch renderer to the one needed by the entity that needs rendering
		* @method _switchRenderer
		* @param gl {WebGLRenderingContext}
		* @param entity {Kiwi.Entity}
		* @private
		*/
		private _switchRenderer(gl: WebGLRenderingContext, entity: Entity) {
			if (this._currentRenderer) this._currentRenderer.disable(gl);
			this._currentRenderer = entity.glRenderer;
			this._currentRenderer.enable(gl, { camMatrix: this.camMatrix, stageResolution: this._stageResolution });
		}

		/**
		* Switch texture to the one needed by the entity that needs rendering
		* @method _switchTexture
		* @param gl {WebGLRenderingContext}
		* @param entity {Kiwi.Entity}
		* @private
		*/
		private _switchTexture(gl: WebGLRenderingContext, entity: Entity) {
			this._currentTextureAtlas = entity.atlas;
			entity.atlas.enableGL(gl, this._currentRenderer, this._textureManager);
		}

		/**
		* Switch blend mode to a new set of constants
		* @method _switchBlendMode
		* @param gl {WebGLRenderingContext}
		* @param blendMode {Kiwi.Renderers.GLBlendMode}
		* @private
		* @since 1.1.0
		*/
		private _switchBlendMode(gl: WebGLRenderingContext, blendMode: GLBlendMode) {
			this._currentBlendMode = blendMode;
			this._currentBlendMode.apply(gl);
		}

	}

}