Browse Source

Merge pull request #1139 from nockawa/engine2d

Many improvements on Canvas2D and also MapTexture
David Catuhe 9 years ago
parent
commit
cc785d625e

+ 103 - 27
src/Canvas2d/babylon.canvas2d.ts

@@ -30,7 +30,8 @@
         /**
          * In this strategy each group will have its own cache bitmap (except if a given group explicitly defines the DONTCACHEOVERRIDE or CACHEINPARENTGROUP behaviors).
          * This strategy is typically used if the canvas has some groups that are frequently animated. Unchanged ones will have a steady cache and the others will be refreshed when they change, reducing the redraw operation count to their content only.
-         * When using this strategy, group instances can rely on the DONTCACHEOVERRIDE or CACHEINPARENTGROUP behaviors to minize the amount of cached bitmaps.
+         * When using this strategy, group instances can rely on the DONTCACHEOVERRIDE or CACHEINPARENTGROUP behaviors to minimize the amount of cached bitmaps.
+         * Note that in this mode the Canvas itself is not cached, it only contains the sprites of its direct children group to render, there's no point to cache the whole canvas, sprites will be rendered pretty efficiently, the memory cost would be too great for the value of it.
          */
         public static CACHESTRATEGY_ALLGROUPS = 2;
 
@@ -40,7 +41,7 @@
         public static CACHESTRATEGY_CANVAS = 3;
 
         /**
-         * This strategy is used to recompose/redraw the canvas entierely at each viewport render.
+         * This strategy is used to recompose/redraw the canvas entirely at each viewport render.
          * Use this strategy if memory is a concern above rendering performances and/or if the canvas is frequently animated (hence reducing the benefits of caching).
          * Note that you can't use this strategy for WorldSpace Canvas, they need at least a top level group caching.
          */
@@ -50,11 +51,11 @@
          * Create a new 2D ScreenSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a position relative to the top/left corner of the screen.
          * ScreenSpace Canvas will be drawn in the Viewport as a 2D Layer lying to the top of the 3D Scene. Typically used for traditional UI.
          * All caching strategies will be available.
-         * @param engine
-         * @param name
-         * @param pos
-         * @param size
-         * @param cachingStrategy
+         * @param scene the Scene that owns the Canvas
+         * @param name the name of the Canvas, for information purpose only
+         * @param pos the position of the canvas, relative from the bottom/left of the scene's viewport
+         * @param size the Size of the canvas. If null two behaviors depend on the cachingStrategy: if it's CACHESTRATEGY_CACHECANVAS then it will always auto-fit the rendering device, in all the other modes it will fit the content of the Canvas
+         * @param cachingStrategy either CACHESTRATEGY_TOPLEVELGROUPS, CACHESTRATEGY_ALLGROUPS, CACHESTRATEGY_CANVAS, CACHESTRATEGY_DONTCACHE. Please refer to their respective documentation for more information.
          */
         static CreateScreenSpace(scene: Scene, name: string, pos: Vector2, size: Size, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
             let c = new Canvas2D();
@@ -64,9 +65,20 @@
             return c;
         }
 
+
         /**
-         * Create a new 2D WorldSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a world transformation matrix to place it in the world space.
-         * This kind of canvas can't have its Primitives directly drawn in the Viewport, they need to be cached in a bitmap at some point, as a consequence the DONT_CACHE strategy is unavailable. All remaining strategies are supported.
+         * Create a new 2D WorldSpace Rendering Canvas, it is a 2D rectangle that has a size (width/height) and a world transformation information to place it in the world space.
+         * This kind of canvas can't have its Primitives directly drawn in the Viewport, they need to be cached in a bitmap at some point, as a consequence the DONT_CACHE strategy is unavailable. For now only CACHESTRATEGY_CANVAS is supported, but the remaining strategies will be soon.
+         * @param scene the Scene that owns the Canvas
+         * @param name the name of the Canvas, for information purpose only
+         * @param position the position of the Canvas in World Space
+         * @param rotation the rotation of the Canvas in World Space
+         * @param size the dimension of the Canvas in World Space
+         * @param renderScaleFactor A scale factor applied to create the rendering texture that will be mapped in the Scene Rectangle. If you set 2 for instance the texture will be twice large in width and height. A greater value will allow to achieve a better rendering quality.
+         * BE AWARE that the Canvas true dimension will be size*renderScaleFactor, then all coordinates and size will have to be express regarding this size.
+         * TIPS: if you want a renderScaleFactor independent reference of frame, create a child Group2D in the Canvas with position 0,0 and size set to null, then set its scale property to the same amount than the renderScaleFactor, put all your primitive inside using coordinates regarding the size property you pick for the Canvas and you'll be fine.
+         * @param sideOrientation Unexpected behavior occur if the value is different from Mesh.DEFAULTSIDE right now, so please use this one.
+         * @param cachingStrategy Must be CACHESTRATEGY_CANVAS for now
          */
         static CreateWorldSpace(scene: Scene, name: string, position: Vector3, rotation: Quaternion, size: Size, renderScaleFactor: number=1, sideOrientation?: number, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
             if (cachingStrategy !== Canvas2D.CACHESTRATEGY_CANVAS) {
@@ -77,6 +89,10 @@
             //    throw new Error("CACHESTRATEGY_DONTCACHE cache Strategy can't be used for WorldSpace Canvas");
             //}
 
+            if (!sideOrientation) {
+                sideOrientation = Mesh.DEFAULTSIDE;
+            }
+
             let c = new Canvas2D();
             c.setupCanvas(scene, name, new Size(size.width*renderScaleFactor, size.height*renderScaleFactor), false, cachingStrategy);
 
@@ -93,12 +109,18 @@
             plane.position = position;
             plane.rotationQuaternion = rotation;
             plane.material = mtl;
+            c._worldSpaceNode = plane;
 
             return c;
         }
 
         protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean = true, cachingstrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
-            this._engineData = scene.getEngine().getOrAddExternalDataWithFactory("__BJSCANVAS2D__", k => new Canvas2DEngineBoundData());
+            let engine = scene.getEngine();
+            this._fitRenderingDevice = !size;
+            if (!size) {
+                size = new Size(engine.getRenderWidth(), engine.getRenderHeight());
+            }
+            this.__engineData = engine.getOrAddExternalDataWithFactory("__BJSCANVAS2D__", k => new Canvas2DEngineBoundData());
             this._cachingStrategy = cachingstrategy;
             this._depthLevel = 0;
             this._hierarchyMaxDepth = 100;
@@ -106,10 +128,10 @@
             this._hierarchyLevelMaxSiblingCount = 1000;
             this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
 
-            this.setupGroup2D(this, null, name, Vector2.Zero(), size);
+            this.setupGroup2D(this, null, name, Vector2.Zero(), size, this._cachingStrategy===Canvas2D.CACHESTRATEGY_ALLGROUPS ? Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE : Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY);
 
             this._scene = scene;
-            this._engine = scene.getEngine();
+            this._engine = engine;
             this._renderingSize = new Size(0, 0);
 
             // Register scene dispose to also dispose the canvas when it'll happens
@@ -127,11 +149,11 @@
             if (this._isScreeSpace) {
                 this._afterRenderObserver = this._scene.onAfterRenderObservable.add((d, s) => {
                     this._engine.clear(null, false, true);
-                    this.render();
+                    this._render();
                 });
             } else {
                 this._beforeRenderObserver = this._scene.onBeforeRenderObservable.add((d, s) => {
-                    this.render();
+                    this._render();
                 });
             }
 
@@ -139,6 +161,10 @@
 //            this._supprtInstancedArray = false; // TODO REMOVE!!!
         }
 
+        /**
+         * Don't forget to call the dispose method when you're done with the Canvas instance.
+         * But don't worry, if you dispose its scene, the canvas will be automatically disposed too.
+         */
         public dispose(): boolean {
             if (!super.dispose()) {
                 return false;
@@ -185,6 +211,17 @@
             return this._cachingStrategy;
         }
 
+        /**
+         * Only valid for World Space Canvas, returns the scene node that display the canvas
+         */
+        public get worldSpaceCanvasNode(): WorldSpaceCanvas2d {
+            return this._worldSpaceNode;
+        }
+
+        /**
+         * Check if the WebGL Instanced Array extension is supported or not
+         * @returns {} 
+         */
         public get supportInstancedArray() {
             return this._supprtInstancedArray;
         }
@@ -234,6 +271,10 @@
             this._background.levelVisible = true;
         }
 
+        /**
+         * You can set the roundRadius of the background
+         * @returns The current roundRadius
+         */
         public get backgroundRoundRadius(): number {
             if (!this._background || !this._background.isVisible) {
                 return null;
@@ -252,8 +293,8 @@
             this._background.levelVisible = true;
         }
 
-        public get engineData(): Canvas2DEngineBoundData {
-            return this._engineData;
+        public get _engineData(): Canvas2DEngineBoundData {
+            return this.__engineData;
         }
 
         private checkBackgroundAvailability() {
@@ -272,16 +313,23 @@
             return this._hierarchySiblingZDelta;
         }
 
+        /**
+         * Return the Z Factor that will be applied for each new hierarchy level.
+         * @returns The Z Factor
+         */
         public get hierarchyLevelZFactor(): number {
             return this._hierarchyLevelZFactor;
         }
 
-        private _engineData: Canvas2DEngineBoundData;
+        private __engineData: Canvas2DEngineBoundData;
+        private _worldSpaceNode: WorldSpaceCanvas2d;
         private _mapCounter = 0;
         private _background: Rectangle2D;
         private _scene: Scene;
         private _engine: Engine;
+        private _fitRenderingDevice: boolean;
         private _isScreeSpace: boolean;
+        private _cachedCanvasGroup: Group2D;
         private _cachingStrategy: number;
         private _hierarchyMaxDepth: number;
         private _hierarchyLevelZFactor: number;
@@ -295,13 +343,19 @@
         public _renderingSize: Size;
 
         /**
-         * Method that renders the Canvas
-         * @param camera the current camera.
+         * Method that renders the Canvas, you should not invoke
          */
-        public render() {
+        private _render() {
             this._renderingSize.width = this.engine.getRenderWidth();
             this._renderingSize.height = this.engine.getRenderHeight();
 
+            if (this._fitRenderingDevice) {
+                this.size = this._renderingSize;
+                if (this._background) {
+                    this._background.size = this.size;
+                }
+            }
+
             var context = new Render2DContext();
             context.forceRefreshPrimitive = false;
 
@@ -310,18 +364,28 @@
 
             this._prepareGroupRender(context);
             this._groupRender(context);
+
+            // If the canvas is cached at canvas level, we must manually render the sprite that will display its content
+            if (this._cachingStrategy === Canvas2D.CACHESTRATEGY_CANVAS && this._cachedCanvasGroup) {
+                this._cachedCanvasGroup._renderCachedCanvas(context);
+            }
         }
 
         /**
-         * Internal method that alloc a cache for the given group.
-         * Caching is made using a collection of MapTexture where many groups have their bitmapt cache stored inside.
+         * Internal method that allocate a cache for the given group.
+         * Caching is made using a collection of MapTexture where many groups have their bitmap cache stored inside.
          * @param group The group to allocate the cache of.
          * @return custom type with the PackedRect instance giving information about the cache location into the texture and also the MapTexture instance that stores the cache.
          */
-        public _allocateGroupCache(group: Group2D): { node: PackedRect, texture: MapTexture, sprite: Sprite2D } {
+        public _allocateGroupCache(group: Group2D, parent: Group2D, minSize?: Size): { node: PackedRect, texture: MapTexture, sprite: Sprite2D } {
             // Determine size
             let size = group.actualSize;
             size = new Size(Math.ceil(size.width), Math.ceil(size.height));
+            if (minSize) {
+                size.width  = Math.max(minSize.width, size.width);
+                size.height = Math.max(minSize.height, size.height);
+            }
+
             if (!this._groupCacheMaps) {
                 this._groupCacheMaps = new Array<MapTexture>();
             }
@@ -354,13 +418,25 @@
                 res = { node: node, texture: map }
             }
 
-            // Create a Sprite that will be used to render this cache, the "__cachedSpriteOfGroup__" starting id is a hack to bypass exception throwing in case of the Canvas doesn't normally allows direct primitives
+            // Check if we have to create a Sprite that will display the content of the Canvas which is cached.
             // Don't do it in case of the group being a worldspace canvas (because its texture is bound to a WorldSpaceCanvas node)
             if (group !== <any>this || this._isScreeSpace) {
                 let node: PackedRect = res.node;
-                let sprite = Sprite2D.Create(this, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
-                sprite.origin = Vector2.Zero();
-                res.sprite = sprite;
+
+                // Special case if the canvas is entirely cached: create a group that will have a single sprite it will be rendered specifically at the very end of the rendering process
+                if (this._cachingStrategy === Canvas2D.CACHESTRATEGY_CANVAS) {
+                    this._cachedCanvasGroup = Group2D._createCachedCanvasGroup(this);
+                    let sprite = Sprite2D.Create(this._cachedCanvasGroup, "__cachedCanvasSprite__", 0, 0, map, node.contentSize, node.pos);
+                    sprite.zOrder = 1;
+                    sprite.origin = Vector2.Zero();
+                }
+
+                // Create a Sprite that will be used to render this cache, the "__cachedSpriteOfGroup__" starting id is a hack to bypass exception throwing in case of the Canvas doesn't normally allows direct primitives
+                else {
+                    let sprite = Sprite2D.Create(parent, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
+                    sprite.origin = Vector2.Zero();
+                    res.sprite = sprite;
+                }
             }
             return res;
         }

+ 107 - 42
src/Canvas2d/babylon.group2d.ts

@@ -7,8 +7,8 @@
         public static actualSizeProperty: Prim2DPropInfo;
 
         /**
-           * Default behavior, the group will use the caching strategy defined at the Canvas Level
-           */
+         * Default behavior, the group will use the caching strategy defined at the Canvas Level
+         */
         public static GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY = 0;
 
         /**
@@ -22,11 +22,14 @@
          */
         public static GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP = 2;
 
+        /**
+         * Don't invoke directly, rely on Group2D.CreateXXX methods
+         */
         constructor() {
             super();
             this._primDirtyList = new Array<Prim2DBase>();
             this._childrenRenderableGroups = new Array<Group2D>();
-            this.groupRenderInfo = new StringDictionary<GroupInstanceInfo>();
+            this._renderGroupInstancesInfo = new StringDictionary<GroupInstanceInfo>();
         }
 
         static CreateGroup2D(parent: Prim2DBase, id: string, position: Vector2, size?: Size, cacheBehabior: number = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY): Group2D {
@@ -37,7 +40,15 @@
             return g;
         }
 
-        applyCachedTexture(vertexData: VertexData, material: StandardMaterial) {
+        static _createCachedCanvasGroup(owner: Canvas2D): Group2D {
+            var g = new Group2D();
+            g.setupGroup2D(owner, null, "__cachedCanvasGroup__", Vector2.Zero());
+
+            return g;
+            
+        }
+
+        protected applyCachedTexture(vertexData: VertexData, material: StandardMaterial) {
             this._bindCacheTarget();
 
             var uv = vertexData.uvs;
@@ -53,6 +64,9 @@
             this._unbindCacheTarget();
         }
 
+        /**
+         * Call this method to remove this Group and its children from the Canvas
+         */
         public dispose(): boolean {
             if (!super.dispose()) {
                 return false;
@@ -74,52 +88,47 @@
                 this._primDirtyList = null;
             }
 
-            if (this.groupRenderInfo) {
-                this.groupRenderInfo.forEach((k, v) => {
+            if (this._renderGroupInstancesInfo) {
+                this._renderGroupInstancesInfo.forEach((k, v) => {
                     v.dispose();
                 });
-                this.groupRenderInfo = null;
+                this._renderGroupInstancesInfo = null;
             }
 
             return true;
         }
 
-        /**
-         * Create an instance of the Group Primitive.
-         * A group act as a container for many sub primitives, if features:
-         * - Maintain a size, not setting one will determine it based on its content.
-         * - Play an essential role in the rendering pipeline. A group and its content can be cached into a bitmap to enhance rendering performance (at the cost of memory storage in GPU)
-         * @param owner
-         * @param id
-         * @param position
-         * @param size
-         * @param dontcache
-         */
-        protected setupGroup2D(owner: Canvas2D,
-            parent: Prim2DBase,
-            id: string,
-            position: Vector2,
-            size?: Size,
-            cacheBehavior: number = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY) {
+        protected setupGroup2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size?: Size, cacheBehavior: number = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY) {
             this._cacheBehavior = cacheBehavior;
             this.setupPrim2DBase(owner, parent, id, position);
             this.size = size;
             this._viewportPosition = Vector2.Zero();
         }
 
+        /**
+         * @returns Returns true if the Group render content, false if it's a logical group only
+         */
         public get isRenderableGroup(): boolean {
             return this._isRenderableGroup;
         }
 
+        /**
+         * @returns only meaningful for isRenderableGroup, will be true if the content of the Group is cached into a texture, false if it's rendered every time
+         */
         public get isCachedGroup(): boolean {
             return this._isCachedGroup;
         }
 
+
         @instanceLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 1, pi => Group2D.sizeProperty = pi, false, true)
         public get size(): Size {
             return this._size;
         }
 
+        /**
+         * Get/Set the size of the group. If null the size of the group will be determine from its content.
+         * BEWARE: if the Group is a RenderableGroup and its content is cache the texture will be resized each time the group is getting bigger. For performance reason the opposite won't be true: the texture won't shrink if the group does.
+         */
         public set size(val: Size) {
             this._size = val;
         }
@@ -129,17 +138,38 @@
         }
 
         @instanceLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 2, pi => Group2D.actualSizeProperty = pi)
+        /**
+         * Get the actual size of the group, if the size property is not null, this value will be the same, but if size is null, actualSize will return the size computed from the group's bounding content.
+         */
         public get actualSize(): Size {
+            // The computed size will be floor on both width and height
+            let actualSize: Size;
+
             // Return the size if set by the user
             if (this._size) {
-                return this._size;
+                actualSize = new Size(Math.ceil(this._size.width), Math.ceil(this._size.height));
             }
 
             // Otherwise the size is computed based on the boundingInfo
-            let m = this.boundingInfo.max();
-            return new Size(m.x, m.y);
+            else {
+                let m = this.boundingInfo.max();
+                actualSize = new Size(Math.ceil(m.x), Math.ceil(m.y));
+            }
+
+            // Compare the size with the one we previously had, if it differ we set the property dirty and trigger a GroupChanged to synchronize a displaySprite (if any)
+            if (!actualSize.equals(this._actualSize)) {
+                this._instanceDirtyFlags |= Group2D.actualSizeProperty.flagId;
+                this._actualSize = actualSize;
+                this.handleGroupChanged(Group2D.actualSizeProperty);
+            }
+
+            return actualSize;
         }
 
+        /**
+         * Get/set the Cache Behavior, used in case the Canvas Cache Strategy is set to CACHESTRATEGY_ALLGROUPS. Can be either GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP, GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE or GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY. See their documentation for more information.
+         * It is critical to understand than you HAVE TO play with this behavior in order to achieve a good performance/memory ratio. Caching all groups would certainly be the worst strategy of all.
+         */
         public get cacheBehavior(): number {
             return this._cacheBehavior;
         }
@@ -148,6 +178,12 @@
             this._primDirtyList.push(prim);
         }
 
+        public _renderCachedCanvas(context: Render2DContext) {
+            this.updateGlobalTransVis(true);
+            this._prepareGroupRender(context);
+            this._groupRender(context);
+        }
+
         protected updateLevelBoundingInfo() {
             let size: Size;
 
@@ -155,7 +191,7 @@
             if (this.size) {
                 size = this.size;
             }
-            // Otherwise the group's level bouding info is "collapsed"
+            // Otherwise the group's level bounding info is "collapsed"
             else {
                 size = new Size(0, 0);
             }
@@ -174,7 +210,7 @@
             }
 
             // Setup the size of the rendering viewport
-            // In non cache mode, we're rendering directly to the rendering canvas, in this case we have to detect if the canvas size changed since the previous iteration, if it's the case all primitives must be preprared again because their transformation must be recompute
+            // In non cache mode, we're rendering directly to the rendering canvas, in this case we have to detect if the canvas size changed since the previous iteration, if it's the case all primitives must be prepared again because their transformation must be recompute
             if (!this._isCachedGroup) {
                 // Compute the WebGL viewport's location/size
                 let t = this._globalTransform.getTranslation();
@@ -183,7 +219,7 @@
                 s.height = Math.min(s.height, rs.height - t.y);
                 s.width = Math.min(s.width, rs.width - t.x);
                 let x = t.x;
-                let y = (rs.height - s.height) - t.y;
+                let y = t.y;
 
                 // The viewport where we're rendering must be the size of the canvas if this one fit in the rendering screen or clipped to the screen dimensions if needed
                 this._viewportPosition.x = x;
@@ -200,8 +236,15 @@
                     this._viewportSize.width = vw;
                     this._viewportSize.height = vh;
                 }
-            } else {
-                this._viewportSize = this.actualSize;
+            }
+
+            // For a cachedGroup we also check of the group's actualSize is changing, if it's the case then the rendering zone will be change so we also have to dirty all primitives to prepare them again.
+            else {
+                let newSize = this.actualSize.clone();
+                if (!newSize.equals(this._viewportSize)) {
+                    context.forceRefreshPrimitive = true;
+                }
+                this._viewportSize = newSize;
             }
 
             if ((this._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
@@ -259,9 +302,9 @@
 
                 // For each different model of primitive to render
                 let totalRenderCount = 0;
-                this.groupRenderInfo.forEach((k, v) => {
+                this._renderGroupInstancesInfo.forEach((k, v) => {
 
-                    // This part will pack the dynfloatarray and update the instanced array WebGLBufffer
+                    // This part will pack the dynamicfloatarray and update the instanced array WebGLBufffer
                     // Skip it if instanced arrays are not supported
                     if (this.owner.supportInstancedArray) {
                         for (let i = 0; i < v._instancesPartsData.length; i++) {
@@ -274,13 +317,13 @@
                             let neededSize = array.usedElementCount * array.stride * 4;
 
                             // Check if we have to (re)create the instancesBuffer because there's none or the size is too small
-                            if (!v._instancesPartsBuffer[i] || (v._instancesPartsBufferSize[i] <= neededSize)) {
+                            if (!v._instancesPartsBuffer[i] || (v._instancesPartsBufferSize[i] < neededSize)) {
                                 if (v._instancesPartsBuffer[i]) {
                                     engine.deleteInstancesBuffer(v._instancesPartsBuffer[i]);
                                 }
                                 v._instancesPartsBuffer[i] = engine.createInstancesBuffer(neededSize);
                                 v._instancesPartsBufferSize[i] = neededSize;
-                                v._dirtyInstancesData = true;
+                                v._dirtyInstancesData = false;
 
                                 // Update the WebGL buffer to match the new content of the instances data
                                 engine._gl.bufferSubData(engine._gl.ARRAY_BUFFER, 0, instanceData);
@@ -294,7 +337,7 @@
                         }
                     }
 
-                    // Submit render only if we have something to render (everything may be hiden and the floatarray empty)
+                    // Submit render only if we have something to render (everything may be hidden and the floatarray empty)
                     if (!this.owner.supportInstancedArray || totalRenderCount > 0) {
                         // render all the instances of this model, if the render method returns true then our instances are no longer dirty
                         let renderFailed = !v._modelCache.render(v, context);
@@ -319,16 +362,34 @@
         }
 
         private _bindCacheTarget() {
-            // Check if we have to allocate a rendering zone in the global cache texture
+            let curWidth: number;
+            let curHeight: number;
+
+            if (this._cacheNode) {
+                let size = this._cacheNode.contentSize;
+                let groupWidth = Math.ceil(this.actualSize.width);
+                let groupHeight = Math.ceil(this.actualSize.height);
+
+                if ((size.width < groupWidth) || (size.height < groupHeight)) {
+                    curWidth = Math.floor(size.width * 1.07);    // Grow 5% more to avoid frequent resizing for few pixels...
+                    curHeight = Math.floor(size.height * 1.07);
+                    //console.log(`[${this._globalTransformProcessStep}] Resize group ${this.id}, width: ${curWidth}, height: ${curHeight}`);
+                    this._cacheTexture.freeRect(this._cacheNode);
+                    this._cacheNode = null;
+                }
+            }
+
             if (!this._cacheNode) {
-                var res = this.owner._allocateGroupCache(this);
+                // Check if we have to allocate a rendering zone in the global cache texture
+                var res = this.owner._allocateGroupCache(this, this.renderGroup, curWidth ? new Size(curWidth, curHeight) : null);
                 this._cacheNode = res.node;
                 this._cacheTexture = res.texture;
                 this._cacheRenderSprite = res.sprite;
+                let size = this._cacheNode.contentSize;
             }
 
             let n = this._cacheNode;
-            this._cacheTexture.bindTextureForRect(n, true);
+            this._cacheTexture.bindTextureForPosSize(n.pos, this.actualSize, true);
         }
 
         private _unbindCacheTarget() {
@@ -351,6 +412,9 @@
                 this._cacheRenderSprite.rotation = this.rotation;
             } else if (prop.id === Prim2DBase.scaleProperty.id) {
                 this._cacheRenderSprite.scale = this.scale;
+            } else if (prop.id === Group2D.actualSizeProperty.id) {
+                this._cacheRenderSprite.spriteSize = this.actualSize.clone();
+                //console.log(`[${this._globalTransformProcessStep}] Sync Sprite ${this.id}, width: ${this.actualSize.width}, height: ${this.actualSize.height}`);
             }
         }
 
@@ -370,7 +434,7 @@
                     this._isRenderableGroup = true;
                     this._isCachedGroup = true;
                 } else {
-                    this._isRenderableGroup = false;
+                    this._isRenderableGroup = this.id === "__cachedCanvasGroup__";
                     this._isCachedGroup = false;
                 }
             }
@@ -423,6 +487,7 @@
         private _cacheGroupDirty: boolean;
         protected _childrenRenderableGroups: Array<Group2D>;
         private _size: Size;
+        private _actualSize: Size;
         private _cacheBehavior: number;
         private _primDirtyList: Array<Prim2DBase>;
         private _cacheNode: PackedRect;
@@ -431,7 +496,7 @@
         private _viewportPosition: Vector2;
         private _viewportSize: Size;
 
-        groupRenderInfo: StringDictionary<GroupInstanceInfo>;
+        _renderGroupInstancesInfo: StringDictionary<GroupInstanceInfo>;
     }
 
 }

+ 3 - 3
src/Canvas2d/babylon.prim2dBase.ts

@@ -113,7 +113,7 @@
          * The origin is used only to compute transformation of the primitive, it has no meaning in the primitive local frame of reference
          * For instance:
          * 0,0 means the center is top/left
-         * 0.5,0.5 means the center is at the center of the primtive
+         * 0.5,0.5 means the center is at the center of the primitive
          * 0,1 means the center is bottom/left
          * @returns The normalized center.
          */
@@ -360,12 +360,12 @@
         private _scale: number;
         private _origin: Vector2;
 
-        // Stores the step of the parent for which the current global tranform was computed
+        // Stores the step of the parent for which the current global transform was computed
         // If the parent has a new step, it means this prim's global transform must be updated
         protected _parentTransformStep: number;
 
         // Stores the step corresponding of the global transform for this prim
-        // If a child prim has an older _parentTransformStep it means the chidl's transform should be updated
+        // If a child prim has an older _parentTransformStep it means the child's transform should be updated
         protected _globalTransformStep: number;
 
         // Stores the previous 

+ 3 - 3
src/Canvas2d/babylon.rectangle2d.ts

@@ -44,7 +44,7 @@
                 let count = instanceInfo._instancesPartsData[partIndex].usedElementCount;
                 if (instanceInfo._owner.owner.supportInstancedArray) {
                     if (!this.instancingFillAttributes) {
-                        // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+                        // Compute the offset locations of the attributes in the vertex shader that will be mapped to the instance buffer data
                         this.instancingFillAttributes = this.loadInstancingAttributes(Shape2D.SHAPE2D_FILLPARTID, this.effectFill);
                     }
 
@@ -218,7 +218,7 @@
             let renderCache = <Rectangle2DRenderCache>modelRenderCache;
             let engine = this.owner.engine;
 
-            // Need to create webgl resources for fill part?
+            // Need to create WebGL resources for fill part?
             if (this.fill) {
                 let vbSize = ((this.notRounded ? 1 : Rectangle2D.roundSubdivisions) * 4) + 1;
                 let vb = new Float32Array(vbSize);
@@ -245,7 +245,7 @@
                 });
             }
 
-            // Need to create webgl resource for border part?
+            // Need to create WebGL resource for border part?
             if (this.border) {
                 let vbSize = (this.notRounded ? 1 : Rectangle2D.roundSubdivisions) * 4 * 2;
                 let vb = new Float32Array(vbSize);

+ 3 - 3
src/Canvas2d/babylon.renderablePrim2d.ts

@@ -346,7 +346,7 @@
                 if (this._modelRenderCache) {
                     this._modelRenderCache.dispose();
                 }
-                this._modelRenderCache = this.owner.engineData.GetOrAddModelCache(this.modelKey, (key: string) => {
+                this._modelRenderCache = this.owner._engineData.GetOrAddModelCache(this.modelKey, (key: string) => {
                     let mrc = this.createModelRenderCache(key, this.isTransparent);
                     setupModelRenderCache = true;
                     return mrc;
@@ -418,7 +418,7 @@
                 // The Rendering resources (Effect, VB, IB, Textures) are stored in the ModelRenderCache
                 // But it's the RenderGroup that will store all the Instanced related data to render all the primitive it owns.
                 // So for a given ModelKey we getOrAdd a GroupInstanceInfo that will store all these data
-                gii = this.renderGroup.groupRenderInfo.getOrAddWithFactory(this.modelKey, k => new GroupInstanceInfo(this.renderGroup, this._modelRenderCache));
+                gii = this.renderGroup._renderGroupInstancesInfo.getOrAddWithFactory(this.modelKey, k => new GroupInstanceInfo(this.renderGroup, this._modelRenderCache));
 
                 // First time init of the GroupInstanceInfo
                 if (gii._instancesPartsData.length === 0) {
@@ -456,7 +456,7 @@
 
                 // Fetch the GroupInstanceInfo if we don't already have it
                 if (!gii) {
-                    gii = this.renderGroup.groupRenderInfo.get(this.modelKey);
+                    gii = this.renderGroup._renderGroupInstancesInfo.get(this.modelKey);
                 }
 
                 // For each Instance Data part, refresh it to update the data in the DynamicFloatArray

+ 3 - 3
src/Canvas2d/babylon.smartPropertyPrim.ts

@@ -220,7 +220,7 @@
         }
 
         private static _checkUnchanged(curValue, newValue): boolean {
-            // Nothing to nothing: nothign to do!
+            // Nothing to nothing: nothing to do!
             if ((curValue === null && newValue === null) || (curValue === undefined && newValue === undefined)) {
                 return true;
             }
@@ -244,7 +244,7 @@
         private static propChangedInfo = new PropertyChangedInfo();
 
         private _handlePropChanged<T>(curValue: T, newValue: T, propName: string, propInfo: Prim2DPropInfo, typeLevelCompare: boolean) {
-            // Trigger propery changed
+            // Trigger property changed
             let info = SmartPropertyPrim.propChangedInfo;
             info.oldValue = curValue;
             info.newValue = newValue;
@@ -343,7 +343,7 @@
                     if (propInfo.dirtyBoundingInfo) {
                         prim._levelBoundingInfoDirty = true;
 
-                        // Escalade the dirty flag in the instance hierarchy, stop when a renderable group is found or at the end
+                        // Escalate the dirty flag in the instance hierarchy, stop when a renderable group is found or at the end
                         if (prim instanceof Prim2DBase) {
                             let curprim = prim.parent;
                             while (curprim) {

+ 9 - 1
src/Canvas2d/babylon.sprite2d.ts

@@ -13,7 +13,7 @@
                 return false;
             }
 
-            // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+            // Compute the offset locations of the attributes in the vertex shader that will be mapped to the instance buffer data
             var engine = instanceInfo._owner.owner.engine;
 
             engine.enableEffect(this.effect);
@@ -181,6 +181,14 @@
             return sprite;
         }
 
+        static _createCachedCanvasSprite(owner: Canvas2D, texture: MapTexture, size: Size, pos: Vector2): Sprite2D {
+
+            let sprite = new Sprite2D();
+            sprite.setupSprite2D(owner, null, "__cachedCanvasSprite__", new Vector2(0, 0), texture, size, pos, false);
+
+            return sprite;
+        }
+
         protected createModelRenderCache(modelKey: string, isTransparent: boolean): ModelRenderCache {
             let renderCache = new Sprite2DRenderCache(this.owner.engine, modelKey, isTransparent);
             return renderCache;

+ 3 - 0
src/Canvas2d/babylon.worldSpaceCanvas2d.ts

@@ -1,4 +1,7 @@
 module BABYLON {
+    /**
+     * This is the class that is used to display a World Space Canvas into a scene
+     */
     export class WorldSpaceCanvas2d extends Mesh {
         constructor(name: string, scene: Scene, canvas: Canvas2D) {
             super(name, scene);

+ 38 - 4
src/Materials/Textures/babylon.mapTexture.ts

@@ -41,7 +41,7 @@
         }
 
         /**
-         * Return the avaible space in the range of [O;1]. 0 being not space left at all, 1 being an empty texture map.
+         * Return the available space in the range of [O;1]. 0 being not space left at all, 1 being an empty texture map.
          * This is the cumulated space, not the biggest available surface. Due to fragmentation you may not allocate a rect corresponding to this surface.
          * @returns {} 
          */
@@ -54,13 +54,47 @@
          * Use this method when you want to render into the texture map with a clipspace set to the location and size of the given rect.
          * Don't forget to call unbindTexture when you're done rendering
          * @param rect the zone to render to
+         * @param clear true to clear the portion's color/depth data
          */
         public bindTextureForRect(rect: PackedRect, clear: boolean) {
+            return this.bindTextureForPosSize(rect.pos, rect.contentSize, clear);
+        }
+
+        /**
+         * Bind the texture to the rendering engine to render in the zone of the given size at the given position.
+         * Use this method when you want to render into the texture map with a clipspace set to the location and size of the given rect.
+         * Don't forget to call unbindTexture when you're done rendering
+         * @param pos the position into the texture
+         * @param size the portion to fit the clip space to
+         * @param clear true to clear the portion's color/depth data
+         */
+        public bindTextureForPosSize(pos: Vector2, size: Size, clear: boolean) {
+
             let engine = this.getScene().getEngine();
             engine.bindFramebuffer(this._texture);
-            this._replacedViewport = engine.setDirectViewport(rect.pos.x, rect.pos.y, rect.contentSize.width, rect.contentSize.height);
+            this._replacedViewport = engine.setDirectViewport(pos.x, pos.y, size.width, size.height);
             if (clear) {
-                engine.clear(new Color4(0,0,0,0), true, true);
+                let gl = engine._gl;
+                // We only want to clear the part of the texture we're binding to, only the scissor can help us to achieve that
+
+                // Save state
+                var curScissor = gl.getParameter(gl.SCISSOR_TEST);
+                var curScissorBox = gl.getParameter(gl.SCISSOR_BOX);
+
+                // Change state
+                gl.enable(gl.SCISSOR_TEST);
+                gl.scissor(pos.x, pos.y, size.width, size.height);
+
+                // Clear
+                engine.clear(new Color4(0, 0, 0, 0), true, true);
+
+                // Restore state
+                gl.scissor(curScissorBox[0], curScissorBox[1], curScissorBox[2], curScissorBox[3]);
+                if (curScissor===true) {
+                    gl.enable(gl.SCISSOR_TEST);
+                } else {
+                    gl.disable(gl.SCISSOR_TEST);
+                }
             }
         }
 
@@ -89,7 +123,7 @@
             return false;
         }
 
-        // Note, I don't know what behevior this method should have: clone the underlying texture/rectPackingMap or just reference them?
+        // Note, I don't know what behavior this method should have: clone the underlying texture/rectPackingMap or just reference them?
         // Anyway, there's not much point to use this method for this kind of texture I guess
         public clone(): MapTexture {
             return null;

+ 1 - 1
src/Tools/babylon.stringDictionary.ts

@@ -128,7 +128,7 @@
         }
 
         /**
-         * Execute a callback on every occurence of the dictionary until it returns a valid TRes object.
+         * Execute a callback on every occurrence of the dictionary until it returns a valid TRes object.
          * If the callback returns null or undefined the method will iterate to the next key/value pair
          * Note that you can remove any element in this dictionary in the callback implementation
          * @param callback the callback to execute, if it return a valid T instanced object the enumeration will stop and the object will be returned