浏览代码

Merge pull request #1126 from nockawa/engine2d

Canvas2d first version
David Catuhe 9 年之前
父节点
当前提交
88c204a973
共有 40 个文件被更改,包括 6989 次插入31 次删除
  1. 17 2
      Tools/Gulp/config.json
  2. 5 4
      dist/preview release/what's new.md
  3. 102 0
      src/Canvas2d/babylon.bounding2d.js
  4. 169 0
      src/Canvas2d/babylon.bounding2d.ts
  5. 113 0
      src/Canvas2d/babylon.brushes2d.js
  6. 209 0
      src/Canvas2d/babylon.brushes2d.ts
  7. 305 0
      src/Canvas2d/babylon.canvas2d.js
  8. 363 0
      src/Canvas2d/babylon.canvas2d.ts
  9. 341 0
      src/Canvas2d/babylon.group2d.js
  10. 394 0
      src/Canvas2d/babylon.group2d.ts
  11. 54 0
      src/Canvas2d/babylon.modelRenderCache.js
  12. 166 0
      src/Canvas2d/babylon.modelRenderCache.ts
  13. 360 0
      src/Canvas2d/babylon.prim2dBase.js
  14. 344 0
      src/Canvas2d/babylon.prim2dBase.ts
  15. 174 0
      src/Canvas2d/babylon.rectangle2d.js
  16. 280 0
      src/Canvas2d/babylon.rectangle2d.ts
  17. 372 0
      src/Canvas2d/babylon.renderablePrim2d.js
  18. 523 0
      src/Canvas2d/babylon.renderablePrim2d.ts
  19. 193 0
      src/Canvas2d/babylon.shape2d.ts
  20. 344 0
      src/Canvas2d/babylon.smartPropertyPrim.js
  21. 391 0
      src/Canvas2d/babylon.smartPropertyPrim.ts
  22. 233 0
      src/Canvas2d/babylon.sprite2d.js
  23. 226 0
      src/Canvas2d/babylon.sprite2d.ts
  24. 331 0
      src/Canvas2d/babylon.text2d.ts
  25. 11 0
      src/Canvas2d/babylon.worldSpaceCanvas2d.ts
  26. 4 0
      src/Materials/Textures/babylon.baseTexture.ts
  27. 289 0
      src/Materials/Textures/babylon.fontTexture.ts
  28. 95 0
      src/Materials/Textures/babylon.mapTexture.js
  29. 98 0
      src/Materials/Textures/babylon.mapTexture.ts
  30. 5 0
      src/Materials/Textures/babylon.texture.ts
  31. 8 1
      src/Math/babylon.math.ts
  32. 5 0
      src/Shaders/rect2d.fragment.fx
  33. 177 0
      src/Shaders/rect2d.vertex.fx
  34. 10 0
      src/Shaders/sprite2d.fragment.fx
  35. 67 0
      src/Shaders/sprite2d.vertex.fx
  36. 10 0
      src/Shaders/text2d.fragment.fx
  37. 59 0
      src/Shaders/text2d.vertex.fx
  38. 12 3
      src/Tools/babylon.stringDictionary.ts
  39. 40 6
      src/Tools/babylon.tools.ts
  40. 90 15
      src/babylon.engine.ts

+ 17 - 2
Tools/Gulp/config.json

@@ -126,7 +126,7 @@
       "../../src/Tools/babylon.loadingScreen.js",
       "../../src/Audio/babylon.audioEngine.js",
       "../../src/Audio/babylon.sound.js",
-      "../../src/Audio/babylon.soundtrack.js"
+      "../../src/Audio/babylon.soundtrack.js"
     ]
   },
   "shadersDirectories": [
@@ -157,6 +157,21 @@
       "../../src/Math/babylon.math.SIMD.js",
       "../../src/Tools/babylon.rectPackingMap.js",
       "../../src/Tools/babylon.dynamicFloatArray.js",
+      "../../src/Materials/Textures/babylon.fontTexture.js",
+      "../../src/Materials/Textures/babylon.mapTexture.js",
+      "../../src/Canvas2d/babylon.bounding2d.js",
+      "../../src/Canvas2d/babylon.brushes2d.js",
+      "../../src/Canvas2d/babylon.smartPropertyPrim.js",
+      "../../src/Canvas2d/babylon.prim2dBase.js",
+      "../../src/Canvas2d/babylon.modelRenderCache.js",
+      "../../src/Canvas2d/babylon.renderablePrim2d.js",
+      "../../src/Canvas2d/babylon.shape2d.js",
+      "../../src/Canvas2d/babylon.group2d.js",
+      "../../src/Canvas2d/babylon.rectangle2d.js",
+      "../../src/Canvas2d/babylon.sprite2d.js",
+      "../../src/Canvas2d/babylon.text2d.js",
+      "../../src/Canvas2d/babylon.canvas2d.js",
+      "../../src/Canvas2d/babylon.worldspacecanvas2d.js",
       "../../src/Materials/babylon.shaderMaterial.js",
       "../../src/Tools/babylon.tools.dds.js",
       "../../src/Physics/Plugins/babylon.cannonJSPlugin.js",
@@ -211,8 +226,8 @@
       "../../src/tools/hdr/babylon.tools.hdr.js",
       "../../src/tools/hdr/babylon.tools.pmremGenerator.js",
       "../../src/materials/textures/babylon.hdrcubetexture.js",
-      "../../src/debug/babylon.skeletonViewer.js",
       "../../src/Materials/Textures/babylon.colorGradingTexture.js",
+      "../../src/debug/babylon.skeletonViewer.js",
       "../../src/materials/babylon.pbrmaterial.js"
     ]
   }

+ 5 - 4
dist/preview release/what's new.md

@@ -15,6 +15,7 @@
     - Unity3D exporter: Added support for export and run (local webserver) ([davrous](https://github.com/davrous), [deltakosh](https://github.com/deltakosh))
     - Moved PBR Material to core ([deltakosh](https://github.com/deltakosh))
     - StandardMaterial.maxSimultaneousLights can define how many dynamic lights the material can handle ([deltakosh](https://github.com/deltakosh))
+	- Introduced Canvas2D feature: a 2D engine to render primitives, sprites in 2D, text. Canvas2D can be displayed in Screen Space (above the 3D scene) or in World Space to be a part of the Scene. [overview](http://doc.babylonjs.com/overviews/Using_The_Canvas2D), [tutorial](http://doc.babylonjs.com/tutorials/Using_the_Canvas2D) ([nockawa](https://github.com/nockawa))	
   - **Updates**
     - Added postprocess.enablePixelPerfectMode to avoid texture scaling/stretching when dealing with non-power of 2 resolutions. cannot be used on post-processes chain ([deltakosh](https://github.com/deltakosh))
     - Enabled other post processes to be used when also using a 3D Rig ([jcpalmer](https://github.com/Palmer-JC))
@@ -33,10 +34,10 @@
     - LinesMesh class now supports Intersection. Added the intersectionThreshold property to set a tolerance margin during intersection with wire lines. ([nockawa](https://github.com/nockawa))
     - Geometry.boundingBias property to enlarge the boundingInfo objects ([nockawa](https://github.com/nockawa))
     - Tools.ExtractMinAndMax & ExtractMinAndMaxIndexed now supports an optional Bias for Extent computation.
-	  - Added StringDictionary<T> class to implement an efficient generic typed string dictionary based on Javascript associative array. ([nockawa](https://github.com/nockawa))
-	  - Added RectanglePackingMap class to fix several rectangles in a big map in the most optimal way. ([nockawa](https://github.com/nockawa))
-	  - Added DynamicFloatArray class to store float32 based elements of a given size (stride) into one big Float32Array, with allocation/free/pack operations to then access an optimal buffer that can be used to update a WebGLBuffer dynamically. ([nockawa](https://github.com/nockawa))
-	  - Scene.onPointerObservable property added to enable a unique Observable event for user input (see ArcRotateCamera inputs for examples) ([nockawa](https://github.com/nockawa))
+	- Added StringDictionary<T> class to implement an efficient generic typed string dictionary based on Javascript associative array. ([nockawa](https://github.com/nockawa))
+	- Added RectanglePackingMap class to fix several rectangles in a big map in the most optimal way. ([nockawa](https://github.com/nockawa))
+	- Added DynamicFloatArray class to store float32 based elements of a given size (stride) into one big Float32Array, with allocation/free/pack operations to then access an optimal buffer that can be used to update a WebGLBuffer dynamically. ([nockawa](https://github.com/nockawa))
+	- Scene.onPointerObservable property added to enable a unique Observable event for user input (see ArcRotateCamera inputs for examples) ([nockawa](https://github.com/nockawa))
   - **Exporters**
     - Support for 3dsmax 2017 ([deltakosh](https://github.com/deltakosh))
     - Added support for up to 8 bones influences per vertex for 3dsmax exporter ([deltakosh](https://github.com/deltakosh))

+ 102 - 0
src/Canvas2d/babylon.bounding2d.js

@@ -0,0 +1,102 @@
+var BABYLON;
+(function (BABYLON) {
+    /**
+     * Stores 2D Bounding Information.
+     * This class handles a circle area and a bounding rectangle one.
+     */
+    var BoundingInfo2D = (function () {
+        function BoundingInfo2D() {
+            this.extent = new BABYLON.Size(0, 0);
+        }
+        /**
+         * Duplicate this instance and return a new one
+         * @return the duplicated instance
+         */
+        BoundingInfo2D.prototype.clone = function () {
+            var r = new BoundingInfo2D();
+            r.radius = this.radius;
+            r.extent = this.extent.clone();
+            return r;
+        };
+        /**
+         * Apply a transformation matrix to this BoundingInfo2D and return a new instance containing the result
+         * @param matrix the transformation matrix to apply
+         * @return the new instance containing the result of the transformation applied on this BoundingInfo2D
+         */
+        BoundingInfo2D.prototype.transform = function (matrix) {
+            var r = new BoundingInfo2D();
+            this.transformToRef(matrix, r);
+            return r;
+        };
+        /**
+         * Compute the union of this BoundingInfo2D with a given one, return a new BoundingInfo2D as a result
+         * @param other the second BoundingInfo2D to compute the union with this one
+         * @return a new instance containing the result of the union
+         */
+        BoundingInfo2D.prototype.union = function (other) {
+            var r = new BoundingInfo2D();
+            this.unionToRef(other, r);
+            return r;
+        };
+        /**
+         * Transform this BoundingInfo2D with a given matrix and store the result in an existing BoundingInfo2D instance.
+         * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it everytime.
+         * @param matrix The matrix to use to compute the transformation
+         * @param result A VALID (i.e. allocated) BoundingInfo2D object where the result will be stored
+         */
+        BoundingInfo2D.prototype.transformToRef = function (matrix, result) {
+            // Extract scale from matrix
+            var xs = BABYLON.MathTools.Sign(matrix.m[0] * matrix.m[1] * matrix.m[2] * matrix.m[3]) < 0 ? -1 : 1;
+            var ys = BABYLON.MathTools.Sign(matrix.m[4] * matrix.m[5] * matrix.m[6] * matrix.m[7]) < 0 ? -1 : 1;
+            var scaleX = xs * Math.sqrt(matrix.m[0] * matrix.m[0] + matrix.m[1] * matrix.m[1] + matrix.m[2] * matrix.m[2]);
+            var scaleY = ys * Math.sqrt(matrix.m[4] * matrix.m[4] + matrix.m[5] * matrix.m[5] + matrix.m[6] * matrix.m[6]);
+            // Get translation
+            var trans = matrix.getTranslation();
+            var transLength = trans.length();
+            if (transLength < BABYLON.Epsilon) {
+                result.radius = this.radius * Math.max(scaleX, scaleY);
+            }
+            else {
+                // Compute the radius vector by applying the transformation matrix manually
+                var rx = (trans.x / transLength) * (transLength + this.radius) * scaleX;
+                var ry = (trans.y / transLength) * (transLength + this.radius) * scaleY;
+                // Store the vector length as the new radius
+                result.radius = Math.sqrt(rx * rx + ry * ry);
+            }
+            // Construct a bounding box based on the extent values
+            var p = new Array(4);
+            p[0] = new BABYLON.Vector2(this.extent.width, this.extent.height);
+            p[1] = new BABYLON.Vector2(this.extent.width, -this.extent.height);
+            p[2] = new BABYLON.Vector2(-this.extent.width, -this.extent.height);
+            p[3] = new BABYLON.Vector2(-this.extent.width, this.extent.height);
+            // Transform the four points of the bounding box with the matrix
+            for (var i = 0; i < 4; i++) {
+                p[i] = BABYLON.Vector2.Transform(p[i], matrix);
+            }
+            // Take the first point as reference
+            var maxW = Math.abs(p[0].x), maxH = Math.abs(p[0].y);
+            // Parse the three others, compare them to the reference and keep the biggest
+            for (var i = 1; i < 4; i++) {
+                maxW = Math.max(Math.abs(p[i].x), maxW);
+                maxH = Math.max(Math.abs(p[i].y), maxH);
+            }
+            // Store the new extent
+            result.extent.width = maxW * scaleX;
+            result.extent.height = maxH * scaleY;
+        };
+        /**
+         * Compute the union of this BoundingInfo2D with another one and store the result in a third valid BoundingInfo2D object
+         * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it everytime.
+         * @param other the second object used to compute the union
+         * @param result a VALID BoundingInfo2D instance (i.e. allocated) where the result will be stored
+         */
+        BoundingInfo2D.prototype.unionToRef = function (other, result) {
+            result.radius = Math.max(this.radius, other.radius);
+            result.extent.width = Math.max(this.extent.width, other.extent.width);
+            result.extent.height = Math.max(this.extent.height, other.extent.height);
+        };
+        return BoundingInfo2D;
+    }());
+    BABYLON.BoundingInfo2D = BoundingInfo2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.bounding2d.js.map

+ 169 - 0
src/Canvas2d/babylon.bounding2d.ts

@@ -0,0 +1,169 @@
+module BABYLON {
+
+    /**
+     * Stores 2D Bounding Information.
+     * This class handles a circle area and a bounding rectangle one.
+     */
+    export class BoundingInfo2D {
+
+        /**
+         * The coordinate of the center of the bounding info
+         */
+        public center: Vector2;
+
+        /**
+         * The radius of the bounding circle, from the center of the bounded object
+         */
+        public radius: number;
+
+        /**
+         * The extent of the bounding rectangle, from the center of the bounded object.
+         * This is an absolute value in both X and Y of the vector which describe the right/top corner of the rectangle, you can easily reconstruct the whole rectangle by negating X &| Y.
+         */
+        public extent: Vector2;
+
+        constructor() {
+            this.radius = 0;
+            this.center = Vector2.Zero();
+            this.extent = Vector2.Zero();
+        }
+
+        public static CreateFromSize(size: Size): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.CreateFromSizeToRef(size, r);
+            return r;
+        }
+
+        public static CreateFromRadius(radius: number): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.CreateFromRadiusToRef(radius, r);
+            return r;
+        }
+
+        public static CreateFromPoints(points: Vector2[]): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.CreateFromPointsToRef(points, r);
+
+            return r;
+        }
+
+        public static CreateFromSizeToRef(size: Size, b: BoundingInfo2D) {
+            b.center = new Vector2(size.width / 2, size.height / 2);
+            b.extent = b.center.clone();
+            b.radius = b.extent.length();
+        }
+
+        public static CreateFromRadiusToRef(radius: number, b: BoundingInfo2D) {
+            b.center = Vector2.Zero();
+            b.extent = new Vector2(radius, radius);
+            b.radius = radius;
+        }
+
+        public static CreateFromPointsToRef(points: Vector2[], b: BoundingInfo2D) {
+            let xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = Number.MIN_VALUE, ymax = Number.MIN_VALUE;
+            for (let p of points) {
+                xmin = Math.min(p.x, xmin);
+                xmax = Math.max(p.x, xmax);
+                ymin = Math.min(p.y, ymin);
+                ymax = Math.max(p.y, ymax);
+            }
+            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, b);
+        }
+
+
+        public static CreateFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D) {
+            b.center = new Vector2(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2);
+            b.extent = new Vector2(xmax - b.center.x, ymax - b.center.y);
+            b.radius = b.extent.length();
+        }
+
+        /**
+         * Duplicate this instance and return a new one
+         * @return the duplicated instance
+         */
+        public clone(): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            r.center = this.center.clone();
+            r.radius = this.radius;
+            r.extent = this.extent.clone();
+            return r;
+        }
+
+        public max(): Vector2 {
+            let r = Vector2.Zero();
+            this.maxToRef(r);
+            return r;
+        }
+
+        public maxToRef(result: Vector2) {
+            result.x = this.center.x + this.extent.x;
+            result.y = this.center.y + this.extent.y;
+        }
+
+        /**
+         * Apply a transformation matrix to this BoundingInfo2D and return a new instance containing the result
+         * @param matrix the transformation matrix to apply
+         * @return the new instance containing the result of the transformation applied on this BoundingInfo2D
+         */
+        public transform(matrix: Matrix, origin: Vector2=null): BoundingInfo2D {
+            var r = new BoundingInfo2D();
+            this.transformToRef(matrix, origin, r);
+            return r;
+        }
+
+        /**
+         * Compute the union of this BoundingInfo2D with a given one, return a new BoundingInfo2D as a result
+         * @param other the second BoundingInfo2D to compute the union with this one
+         * @return a new instance containing the result of the union
+         */
+        public union(other: BoundingInfo2D): BoundingInfo2D {
+            var r = new BoundingInfo2D();
+            this.unionToRef(other, r);
+            return r;
+        }
+
+        /**
+         * Transform this BoundingInfo2D with a given matrix and store the result in an existing BoundingInfo2D instance.
+         * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it everytime.
+         * @param origin An optional normalized origin to apply before the transformation. 0;0 is top/left, 0.5;0.5 is center, etc.
+         * @param matrix The matrix to use to compute the transformation
+         * @param result A VALID (i.e. allocated) BoundingInfo2D object where the result will be stored
+         */
+        public transformToRef(matrix: Matrix, origin: Vector2, result: BoundingInfo2D) {
+            // Construct a bounding box based on the extent values
+            let p = new Array<Vector2>(4);
+            p[0] = new Vector2(this.center.x + this.extent.x, this.center.y + this.extent.y);
+            p[1] = new Vector2(this.center.x + this.extent.x, this.center.y - this.extent.y);
+            p[2] = new Vector2(this.center.x - this.extent.x, this.center.y - this.extent.y);
+            p[3] = new Vector2(this.center.x - this.extent.x, this.center.y + this.extent.y);
+
+            //if (origin) {
+            //    let off = new Vector2((p[0].x - p[2].x) * origin.x, (p[0].y - p[2].y) * origin.y);
+            //    for (let j = 0; j < 4; j++) {
+            //        p[j].subtractInPlace(off);
+            //    }
+            //}
+
+            // Transform the four points of the bounding box with the matrix
+            for (let i = 0; i < 4; i++) {
+                Vector2.TransformToRef(p[i], matrix, p[i]);
+            }
+            BoundingInfo2D.CreateFromPointsToRef(p, result);
+        }
+
+        /**
+         * Compute the union of this BoundingInfo2D with another one and store the result in a third valid BoundingInfo2D object
+         * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it everytime.
+         * @param other the second object used to compute the union
+         * @param result a VALID BoundingInfo2D instance (i.e. allocated) where the result will be stored
+         */
+        public unionToRef(other: BoundingInfo2D, result: BoundingInfo2D) {
+            let xmax = Math.max(this.center.x + this.extent.x, other.center.x + other.extent.x);
+            let ymax = Math.max(this.center.y + this.extent.y, other.center.y + other.extent.y);
+            let xmin = Math.min(this.center.x - this.extent.x, other.center.x - other.extent.x);
+            let ymin = Math.min(this.center.y - this.extent.y, other.center.y - other.extent.y);
+            BoundingInfo2D.CreateFromMinMaxToRef(xmin, xmax, ymin, ymax, result);
+        }
+
+    }
+}

+ 113 - 0
src/Canvas2d/babylon.brushes2d.js

@@ -0,0 +1,113 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var BABYLON;
+(function (BABYLON) {
+    /**
+     * Base class implemting the ILocable interface.
+     * The particularity of this class is to call the protected onLock() method when the instance is about to be locked for good.
+     */
+    var LockableBase = (function () {
+        function LockableBase() {
+        }
+        LockableBase.prototype.isLocked = function () {
+            return this._isLocked;
+        };
+        LockableBase.prototype.lock = function () {
+            if (this._isLocked) {
+                return true;
+            }
+            this.onLock();
+            this._isLocked = true;
+            return false;
+        };
+        /**
+         * Protected handler that will be called when the instance is about to be locked.
+         */
+        LockableBase.prototype.onLock = function () {
+        };
+        return LockableBase;
+    }());
+    BABYLON.LockableBase = LockableBase;
+    /**
+     * This classs implements a Border that will be drawn with a uniform solid color (i.e. the same color everywhere in the border).
+     */
+    var SolidColorBorder2D = (function (_super) {
+        __extends(SolidColorBorder2D, _super);
+        function SolidColorBorder2D(color, lock) {
+            if (lock === void 0) { lock = false; }
+            _super.call(this);
+            this._color = color;
+            if (lock) {
+                this.lock();
+            }
+        }
+        Object.defineProperty(SolidColorBorder2D.prototype, "color", {
+            /**
+             * The color used by this instance to render
+             * @returns the color object. Note that it's not a clone of the actual object stored in the instance so you MUST NOT modify it, otherwise unexpected behavior might occurs.
+             */
+            get: function () {
+                return this._color;
+            },
+            set: function (value) {
+                if (this.isLocked()) {
+                    return;
+                }
+                this._color = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+         * Return a unique identifier of the instance, which is simply the hexadecimal representation (CSS Style) of the solid color.
+         */
+        SolidColorBorder2D.prototype.toString = function () {
+            return this._color.toHexString();
+        };
+        return SolidColorBorder2D;
+    }(LockableBase));
+    BABYLON.SolidColorBorder2D = SolidColorBorder2D;
+    /**
+     * This class implements a Fill that will be drawn with a uniform solid color (i.e. the same everywhere inside the primitive).
+     */
+    var SolidColorFill2D = (function (_super) {
+        __extends(SolidColorFill2D, _super);
+        function SolidColorFill2D(color, lock) {
+            if (lock === void 0) { lock = false; }
+            _super.call(this);
+            this._color = color;
+            if (lock) {
+                this.lock();
+            }
+        }
+        Object.defineProperty(SolidColorFill2D.prototype, "color", {
+            /**
+             * The color used by this instance to render
+             * @returns the color object. Note that it's not a clone of the actual object stored in the instance so you MUST NOT modify it, otherwise unexpected behavior might occurs.
+             */
+            get: function () {
+                return this._color;
+            },
+            set: function (value) {
+                if (this.isLocked()) {
+                    return;
+                }
+                this._color = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+         * Return a unique identifier of the instance, which is simply the hexadecimal representation (CSS Style) of the solid color.
+         */
+        SolidColorFill2D.prototype.toString = function () {
+            return this._color.toHexString();
+        };
+        return SolidColorFill2D;
+    }(LockableBase));
+    BABYLON.SolidColorFill2D = SolidColorFill2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.brushes2d.js.map

+ 209 - 0
src/Canvas2d/babylon.brushes2d.ts

@@ -0,0 +1,209 @@
+module BABYLON {
+    /**
+     * This interface is used to implement a lockable instance pattern.
+     * Classes that implements it may be locked at any time, making their content immutable from now on.
+     * You also can query if a given instance is locked or not.
+     * This allow instances to be shared among several 'consumers'.
+     */
+    export interface ILockable {
+        /**
+         * Query the lock state
+         * @returns returns true if the object is locked and immutable, false if it's not
+         */
+        isLocked(): boolean;
+
+        /**
+         * A call to this method will definitely lock the instance, making its content immutable
+         * @returns the previous lock state of the object. so if true is returned the object  were already locked and this method does nothing, if false is returned it means the object wasn't locked and this call locked it for good.
+         */
+        lock(): boolean;
+    }
+
+    /**
+     * This interface defines the IBrush2D contract.
+     * Classes implementing a new type of Brush2D must implement this interface
+     */
+    export interface IBrush2D extends ILockable {
+        /**
+         * Define if the brush will use transparency/alphablending
+         * @returns true if the brush use transparency
+         */
+        isTransparent(): boolean;
+
+        /**
+         * It is critical for each instance of a given Brush2D type to return a unique string that identifies it because the Border instance will certainly be part of the computed ModelKey for a given Primitive
+         * @returns A string identifier that uniquely identify the instance
+         */
+        toString(): string;
+    }
+
+    /**
+     * Base class implemting the ILocable interface.
+     * The particularity of this class is to call the protected onLock() method when the instance is about to be locked for good.
+     */
+    export class LockableBase implements ILockable {
+        isLocked(): boolean {
+            return this._isLocked;
+        }
+
+        private _isLocked: boolean;
+
+        lock(): boolean {
+            if (this._isLocked) {
+                return true;
+            }
+
+            this.onLock();
+            this._isLocked = true;
+            return false;
+        }
+
+        /**
+         * Protected handler that will be called when the instance is about to be locked.
+         */
+        protected onLock() {
+
+        }
+    }
+
+    /**
+     * This classs implements a Brush that will be drawn with a uniform solid color (i.e. the same color everywhere in the content where the brush is assigned to).
+     */
+    @className("SolidColorBrush2D")
+    export class SolidColorBrush2D extends LockableBase implements IBrush2D {
+        constructor(color: Color4, lock: boolean = false) {
+            super();
+            this._color = color;
+            if (lock) {
+                {
+                    this.lock();
+                }
+            }
+        }
+
+        isTransparent(): boolean {
+            return this._color && this._color.a < 1.0;
+        }
+
+        /**
+         * The color used by this instance to render
+         * @returns the color object. Note that it's not a clone of the actual object stored in the instance so you MUST NOT modify it, otherwise unexpected behavior might occurs.
+         */
+        public get color(): Color4 {
+            return this._color;
+        }
+
+        public set color(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color = value;
+        }
+
+        /**
+         * Return a unique identifier of the instance, which is simply the hexadecimal representation (CSS Style) of the solid color.
+         */
+        public toString(): string {
+            return this._color.toHexString();
+        }
+        private _color: Color4;
+    }
+
+    @className("GradientColorBrush2D")
+    export class GradientColorBrush2D extends LockableBase implements IBrush2D {
+        constructor(color1: Color4, color2: Color4, translation: Vector2 = Vector2.Zero(), rotation: number = 0, scale: number = 1, lock: boolean = false) {
+            super();
+
+            this._color1 = color1;
+            this._color2 = color2;
+            this._translation = translation;
+            this._rotation = rotation;
+            this._scale = scale;
+
+            if (lock) {
+                this.lock();
+            }
+        }
+
+        isTransparent(): boolean {
+            return (this._color1 && this._color1.a < 1.0) || (this._color2 && this._color2.a < 1.0);
+        }
+
+
+        public get color1(): Color4 {
+            return this._color1;
+        }
+
+        public set color1(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color1 = value;
+        }
+
+        public get color2(): Color4 {
+            return this._color2;
+        }
+
+        public set color2(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color2 = value;
+        }
+
+        public get translation(): Vector2 {
+            return this._translation;
+        }
+
+        public set translation(value: Vector2) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._translation = value;
+        }
+
+        public get rotation(): number {
+            return this._rotation;
+        }
+
+        public set rotation(value: number) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._rotation = value;
+        }
+
+        public get scale(): number {
+            return this._scale;
+        }
+
+        public set scale(value: number) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._scale = value;
+        }
+
+        public toString(): string {
+            return `C1:${this._color1};C2:${this._color2};T:${this._translation.toString()};R:${this._rotation};S:${this._scale};`;
+        }
+
+        public static BuildKey(color1: Color4, color2: Color4, translation: Vector2, rotation: number, scale: number) {
+            return `C1:${color1};C2:${color2};T:${translation.toString()};R:${rotation};S:${scale};`;
+        }
+
+        private _color1: Color4;
+        private _color2: Color4;
+        private _translation: Vector2;
+        private _rotation: number;
+        private _scale: number;
+    }
+
+}

+ 305 - 0
src/Canvas2d/babylon.canvas2d.js

@@ -0,0 +1,305 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var GroupsCacheMap = (function () {
+        function GroupsCacheMap() {
+            this.groupSprites = new Array();
+        }
+        return GroupsCacheMap;
+    }());
+    var Canvas2D = (function (_super) {
+        __extends(Canvas2D, _super);
+        function Canvas2D() {
+            _super.apply(this, arguments);
+            this._mapCounter = 0;
+        }
+        /**
+         * 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
+         */
+        Canvas2D.CreateScreenSpace = function (scene, name, pos, size, cachingStrategy) {
+            if (cachingStrategy === void 0) { cachingStrategy = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS; }
+            var c = new Canvas2D();
+            c.setupCanvas(scene, name, size, true, cachingStrategy);
+            c.position = pos;
+            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.
+         * @param engine
+         * @param name
+         * @param transform
+         * @param size
+         * @param cachingStrategy
+         */
+        Canvas2D.CreateWorldSpace = function (scene, name, transform, size, cachingStrategy) {
+            if (cachingStrategy === void 0) { cachingStrategy = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS; }
+            if (cachingStrategy === Canvas2D.CACHESTRATEGY_DONTCACHE) {
+                throw new Error("CACHESTRATEGY_DONTCACHE cache Strategy can't be used for WorldSpace Canvas");
+            }
+            var c = new Canvas2D();
+            c.setupCanvas(scene, name, size, false, cachingStrategy);
+            c._worldTransform = transform;
+            return c;
+        };
+        Canvas2D.prototype.setupCanvas = function (scene, name, size, isScreenSpace, cachingstrategy) {
+            if (isScreenSpace === void 0) { isScreenSpace = true; }
+            if (cachingstrategy === void 0) { cachingstrategy = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS; }
+            this._cachingStrategy = cachingstrategy;
+            this._hierarchyLevelZFactor = 100;
+            this._hierarchyLevelMaxSiblingCount = 1000;
+            this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
+            this.setupGroup2D(this, null, name, BABYLON.Vector2.Zero(), size);
+            this._scene = scene;
+            this._engine = scene.getEngine();
+            this._renderingSize = new BABYLON.Size(0, 0);
+            if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                this._background = BABYLON.Rectangle2D.Create(this, "###CANVAS BACKGROUND###", 0, 0, size.width, size.height);
+                this._background.levelVisible = false;
+            }
+            this._isScreeSpace = isScreenSpace;
+        };
+        Object.defineProperty(Canvas2D.prototype, "scene", {
+            /**
+             * Accessor to the Scene that owns the Canvas
+             * @returns The instance of the Scene object
+             */
+            get: function () {
+                return this._scene;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Canvas2D.prototype, "engine", {
+            /**
+             * Accessor to the Engine that drives the Scene used by this Canvas
+             * @returns The instance of the Engine object
+             */
+            get: function () {
+                return this._engine;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Canvas2D.prototype, "cachingStrategy", {
+            /**
+             * Accessor of the Caching Strategy used by this Canvas.
+             * See Canvas2D.CACHESTRATEGY_xxxx static members for more information
+             * @returns the value corresponding to the used strategy.
+             */
+            get: function () {
+                return this._cachingStrategy;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Canvas2D.prototype, "backgroundFill", {
+            /**
+             * Property that defines the fill object used to draw the background of the Canvas.
+             * Note that Canvas with a Caching Strategy of
+             * @returns If the background is not set, null will be returned, otherwise a valid fill object is returned.
+             */
+            get: function () {
+                if (!this._background || !this._background.isVisible) {
+                    return null;
+                }
+                return this._background.fill;
+            },
+            set: function (value) {
+                this.checkBackgroundAvailability();
+                if (value === this._background.fill) {
+                    return;
+                }
+                this._background.fill = value;
+                this._background.isVisible = true;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Canvas2D.prototype, "border", {
+            /**
+             * Property that defines the border object used to draw the background of the Canvas.
+             * @returns If the background is not set, null will be returned, otherwise a valid border object is returned.
+             */
+            get: function () {
+                if (!this._background || !this._background.isVisible) {
+                    return null;
+                }
+                return this._background.border;
+            },
+            set: function (value) {
+                this.checkBackgroundAvailability();
+                if (value === this._background.border) {
+                    return;
+                }
+                this._background.border = value;
+                this._background.isVisible = true;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Canvas2D.prototype.checkBackgroundAvailability = function () {
+            if (this._cachingStrategy === Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                throw Error("Can't use Canvas Background with the caching strategy TOPLEVELGROUPS");
+            }
+        };
+        Object.defineProperty(Canvas2D.prototype, "hierarchySiblingZDelta", {
+            /**
+             * Read-only property that return the Z delta to apply for each sibling primitives inside of a given one.
+             * Sibling Primitives are defined in a specific order, the first ones will be draw below the next ones.
+             * This property define the Z value to apply between each sibling Primitive. Current implementation allows 1000 Siblings Primitives per level.
+             * @returns The Z Delta
+             */
+            get: function () {
+                return this._hierarchySiblingZDelta;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+         * Method that renders the Canvas
+         * @param camera the current camera.
+         */
+        Canvas2D.prototype.render = function (camera) {
+            this._renderingSize.width = this.engine.getRenderWidth();
+            this._renderingSize.height = this.engine.getRenderHeight();
+            var context = new BABYLON.Render2DContext();
+            context.camera = camera;
+            context.parentVisibleState = this.levelVisible;
+            context.parentTransform = BABYLON.Matrix.Identity();
+            context.parentTransformStep = 1;
+            context.forceRefreshPrimitive = false;
+            this.updateGlobalTransVis(context, false);
+            this._prepareGroupRender(context);
+            this._groupRender(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.
+         * @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.
+         */
+        Canvas2D.prototype._allocateGroupCache = function (group) {
+            // Determine size
+            var size = group.actualSize;
+            size = new BABYLON.Size(Math.ceil(size.width), Math.ceil(size.height));
+            if (!this._groupCacheMaps) {
+                this._groupCacheMaps = new Array();
+            }
+            // Try to find a spot in one of the cached texture
+            var res = null;
+            for (var _i = 0, _a = this._groupCacheMaps; _i < _a.length; _i++) {
+                var g = _a[_i];
+                var node = g.texture.allocateRect(size);
+                if (node) {
+                    res = { node: node, texture: g.texture };
+                    break;
+                }
+            }
+            // Couldn't find a map that could fit the rect, create a new map for it
+            if (!res) {
+                var mapSize = new BABYLON.Size(Canvas2D._groupTextureCacheSize, Canvas2D._groupTextureCacheSize);
+                // Check if the predefined size would fit, other create a custom size using the nearest bigger power of 2
+                if (size.width > mapSize.width || size.height > mapSize.height) {
+                    mapSize.width = Math.pow(2, Math.ceil(Math.log(size.width) / Math.log(2)));
+                    mapSize.height = Math.pow(2, Math.ceil(Math.log(size.height) / Math.log(2)));
+                }
+                g = new GroupsCacheMap();
+                var id = "groupsMapChache" + this._mapCounter + "forCanvas" + this.id;
+                g.texture = new BABYLON.MapTexture(id, this._scene, mapSize);
+                this._groupCacheMaps.push(g);
+                var node = g.texture.allocateRect(size);
+                res = { node: node, texture: g.texture };
+            }
+            // 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
+            var sprite = BABYLON.Sprite2D.Create(this, "__cachedSpriteOfGroup__" + group.id, 10, 10, g.texture, res.node.contentSize, res.node.pos, true);
+            sprite.origin = BABYLON.Vector2.Zero();
+            g.groupSprites.push({ group: group, sprite: sprite });
+            return res;
+        };
+        /**
+         * Get a Solid Color Fill instance matching the given color.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorFill2D class that use the given color
+         */
+        Canvas2D.GetSolidColorFill = function (color) {
+            return Canvas2D._solidColorFills.getOrAddWithFactory(color.toHexString(), function () { return new BABYLON.SolidColorFill2D(color.clone(), true); });
+        };
+        /**
+         * Get a Solid Color Border instance matching the given color.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorBorder2D class that use the given color
+         */
+        Canvas2D.GetSolidColorBorder = function (color) {
+            return Canvas2D._solidColorBorders.getOrAddWithFactory(color.toHexString(), function () { return new BABYLON.SolidColorBorder2D(color.clone(), true); });
+        };
+        /**
+         * Get a Solid Color Fill instance matching the given color expressed as a CSS formatted hexadecimal value.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorFill2D class that use the given color
+         */
+        Canvas2D.GetSolidColorFillFromHex = function (hexValue) {
+            return Canvas2D._solidColorFills.getOrAddWithFactory(hexValue, function () { return new BABYLON.SolidColorFill2D(BABYLON.Color4.FromHexString(hexValue), true); });
+        };
+        /**
+         * Get a Solid Color Border instance matching the given color expressed as a CSS formatted hexadecimal value.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorBorder2D class that use the given color
+         */
+        Canvas2D.GetSolidColorBorderFromHex = function (hexValue) {
+            return Canvas2D._solidColorBorders.getOrAddWithFactory(hexValue, function () { return new BABYLON.SolidColorBorder2D(BABYLON.Color4.FromHexString(hexValue), true); });
+        };
+        /**
+         * In this strategy only the direct children groups of the Canvas will be cached, their whole content (whatever the sub groups they have) into a single bitmap.
+         * This strategy doesn't allow primitives added directly as children of the Canvas.
+         * You typically want to use this strategy of a screenSpace fullscreen canvas: you don't want a bitmap cache taking the whole screen resolution but still want the main contents (say UI in the topLeft and rightBottom for instance) to be efficiently cached.
+         */
+        Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS = 1;
+        /**
+         * 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.
+         */
+        Canvas2D.CACHESTRATEGY_ALLGROUPS = 2;
+        /**
+         * In this strategy the whole canvas is cached into a single bitmap containing every primitives it owns, at the exception of the ones that are owned by a group having the DONTCACHEOVERRIDE behavior (these primitives will be directly drawn to the viewport at each render for screenSpace Canvas or be part of the Canvas cache bitmap for worldSpace Canvas).
+         */
+        Canvas2D.CACHESTRATEGY_CANVAS = 3;
+        /**
+         * This strategy is used to recompose/redraw the canvas entierely 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.
+         */
+        Canvas2D.CACHESTRATEGY_DONTCACHE = 4;
+        /**
+         * Define the default size used for both the width and height of a MapTexture to allocate.
+         * Note that some MapTexture might be bigger than this size if the first node to allocate is bigger in width or height
+         */
+        Canvas2D._groupTextureCacheSize = 1024;
+        Canvas2D._solidColorFills = new BABYLON.StringDictionary();
+        Canvas2D._solidColorBorders = new BABYLON.StringDictionary();
+        Canvas2D = __decorate([
+            BABYLON.className("Canvas2D")
+        ], Canvas2D);
+        return Canvas2D;
+    }(BABYLON.Group2D));
+    BABYLON.Canvas2D = Canvas2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.canvas2d.js.map

+ 363 - 0
src/Canvas2d/babylon.canvas2d.ts

@@ -0,0 +1,363 @@
+module BABYLON {
+    @className("Canvas2D")
+    export class Canvas2D extends Group2D {
+        /**
+         * In this strategy only the direct children groups of the Canvas will be cached, their whole content (whatever the sub groups they have) into a single bitmap.
+         * This strategy doesn't allow primitives added directly as children of the Canvas.
+         * You typically want to use this strategy of a screenSpace fullscreen canvas: you don't want a bitmap cache taking the whole screen resolution but still want the main contents (say UI in the topLeft and rightBottom for instance) to be efficiently cached.
+         */
+        public static CACHESTRATEGY_TOPLEVELGROUPS = 1;
+
+        /**
+         * 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.
+         */
+        public static CACHESTRATEGY_ALLGROUPS = 2;
+
+        /**
+         * In this strategy the whole canvas is cached into a single bitmap containing every primitives it owns, at the exception of the ones that are owned by a group having the DONTCACHEOVERRIDE behavior (these primitives will be directly drawn to the viewport at each render for screenSpace Canvas or be part of the Canvas cache bitmap for worldSpace Canvas).
+         */
+        public static CACHESTRATEGY_CANVAS = 3;
+
+        /**
+         * This strategy is used to recompose/redraw the canvas entierely 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.
+         */
+        public static CACHESTRATEGY_DONTCACHE = 4;
+
+        /**
+         * 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
+         */
+        static CreateScreenSpace(scene: Scene, name: string, pos: Vector2, size: Size, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
+            let c = new Canvas2D();
+            c.setupCanvas(scene, name, size, true, cachingStrategy);
+            c.position = pos;
+
+            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.
+         */
+        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) {
+                throw new Error("Right now only the CACHESTRATEGY_CANVAS cache Strategy is supported for WorldSpace Canvas. More will come soon!");
+            }
+
+            //if (cachingStrategy === Canvas2D.CACHESTRATEGY_DONTCACHE) {
+            //    throw new Error("CACHESTRATEGY_DONTCACHE cache Strategy can't be used for WorldSpace Canvas");
+            //}
+
+            let c = new Canvas2D();
+            c.setupCanvas(scene, name, new Size(size.width*renderScaleFactor, size.height*renderScaleFactor), false, cachingStrategy);
+
+            let plane = new WorldSpaceCanvas2d(name, scene, c);
+            let vertexData = VertexData.CreatePlane({ width: size.width/2, height: size.height/2, sideOrientation: sideOrientation });
+            let mtl = new StandardMaterial(name + "_Material", scene);
+
+            c.applyCachedTexture(vertexData, mtl);
+            vertexData.applyToMesh(plane, false);
+
+            mtl.specularColor = new Color3(0, 0, 0);
+            mtl.disableLighting =true;
+            mtl.useAlphaFromDiffuseTexture = true;
+            plane.position = position;
+            plane.rotationQuaternion = rotation;
+            plane.material = mtl;
+
+            return c;
+        }
+
+        protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean = true, cachingstrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+            this._cachingStrategy = cachingstrategy;
+            this._depthLevel = 0;
+            this._hierarchyMaxDepth = 100;
+            this._hierarchyLevelZFactor = 1 / this._hierarchyMaxDepth;
+            this._hierarchyLevelMaxSiblingCount = 1000;
+            this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
+
+            this.setupGroup2D(this, null, name, Vector2.Zero(), size);
+
+            this._scene = scene;
+            this._engine = scene.getEngine();
+            this._renderingSize = new Size(0, 0);
+
+            if (cachingstrategy !== Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                this._background = Rectangle2D.Create(this, "###CANVAS BACKGROUND###", 0, 0, size.width, size.height);
+                this._background.origin = Vector2.Zero();
+            }
+            this._isScreeSpace = isScreenSpace;
+
+            if (this._isScreeSpace) {
+                this._afterRenderObserver = this._scene.onAfterRenderObservable.add((d, s) => {
+                    this._engine.clear(null, false, true);
+                    this.render();
+                });
+            } else {
+                this._beforeRenderObserver = this._scene.onBeforeRenderObservable.add((d, s) => {
+                    this.render();
+                });
+            }
+
+            this._supprtInstancedArray = this._engine.getCaps().instancedArrays !== null;
+//            this._supprtInstancedArray = false; // TODO REMOVE!!!
+        }
+
+        public dispose(): boolean {
+            if (!super.dispose()) {
+                return false;
+            }
+
+            if (this._beforeRenderObserver) {
+                this._scene.onBeforeRenderObservable.remove(this._beforeRenderObserver);
+                this._beforeRenderObserver = null;
+            }
+
+            if (this._afterRenderObserver) {
+                this._scene.onAfterRenderObservable.remove(this._afterRenderObserver);
+                this._afterRenderObserver = null;
+            }
+        }
+
+        /**
+         * Accessor to the Scene that owns the Canvas
+         * @returns The instance of the Scene object
+         */
+        public get scene(): Scene {
+            return this._scene;
+        }
+
+        /**
+         * Accessor to the Engine that drives the Scene used by this Canvas
+         * @returns The instance of the Engine object
+         */
+        public get engine(): Engine {
+            return this._engine;
+        }
+
+        /**
+         * Accessor of the Caching Strategy used by this Canvas.
+         * See Canvas2D.CACHESTRATEGY_xxxx static members for more information
+         * @returns the value corresponding to the used strategy.
+         */
+        public get cachingStrategy(): number {
+            return this._cachingStrategy;
+        }
+
+        public get supportInstancedArray() {
+            return this._supprtInstancedArray;
+        }
+
+        /**
+         * Property that defines the fill object used to draw the background of the Canvas.
+         * Note that Canvas with a Caching Strategy of
+         * @returns If the background is not set, null will be returned, otherwise a valid fill object is returned.
+         */
+        public get backgroundFill(): IBrush2D {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.fill;
+        }
+
+        public set backgroundFill(value: IBrush2D) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.fill) {
+                return;
+            }
+
+            this._background.fill = value;
+            this._background.isVisible = true;
+        }
+
+        /**
+         * Property that defines the border object used to draw the background of the Canvas.
+         * @returns If the background is not set, null will be returned, otherwise a valid border object is returned.
+         */
+        public get backgroundBorder(): IBrush2D {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.border;
+        }
+
+        public set backgroundBorder(value: IBrush2D) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.border) {
+                return;
+            }
+
+            this._background.border = value;
+            this._background.isVisible = true;
+        }
+
+        public get backgroundRoundRadius(): number {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.roundRadius;
+        }
+
+        public set backgroundRoundRadius(value: number) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.roundRadius) {
+                return;
+            }
+
+            this._background.roundRadius = value;
+            this._background.isVisible = true;
+        }
+
+        private checkBackgroundAvailability() {
+            if (this._cachingStrategy === Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                throw Error("Can't use Canvas Background with the caching strategy TOPLEVELGROUPS");
+            }
+        }
+
+        /**
+         * Read-only property that return the Z delta to apply for each sibling primitives inside of a given one.
+         * Sibling Primitives are defined in a specific order, the first ones will be draw below the next ones.
+         * This property define the Z value to apply between each sibling Primitive. Current implementation allows 1000 Siblings Primitives per level.
+         * @returns The Z Delta
+         */
+        public get hierarchySiblingZDelta(): number {
+            return this._hierarchySiblingZDelta;
+        }
+
+        public get hierarchyLevelZFactor(): number {
+            return this._hierarchyLevelZFactor;
+        }
+
+        private _mapCounter = 0;
+        private _background: Rectangle2D;
+        private _scene: Scene;
+        private _engine: Engine;
+        private _isScreeSpace: boolean;
+        private _cachingStrategy: number;
+        private _hierarchyMaxDepth: number;
+        private _hierarchyLevelZFactor: number;
+        private _hierarchyLevelMaxSiblingCount: number;
+        private _hierarchySiblingZDelta: number;
+        private _groupCacheMaps: MapTexture[];
+        private _beforeRenderObserver: Observer<Scene>;
+        private _afterRenderObserver: Observer<Scene>;
+        private _supprtInstancedArray : boolean;
+
+        public _renderingSize: Size;
+
+        /**
+         * Method that renders the Canvas
+         * @param camera the current camera.
+         */
+        public render() {
+            this._renderingSize.width = this.engine.getRenderWidth();
+            this._renderingSize.height = this.engine.getRenderHeight();
+
+            var context = new Render2DContext();
+            context.forceRefreshPrimitive = false;
+
+            ++this._globalTransformProcessStep;
+            this.updateGlobalTransVis(false);
+
+            this._prepareGroupRender(context);
+            this._groupRender(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.
+         * @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 } {
+            // Determine size
+            let size = group.actualSize;
+            size = new Size(Math.ceil(size.width), Math.ceil(size.height));
+            if (!this._groupCacheMaps) {
+                this._groupCacheMaps = new Array<MapTexture>();
+            }
+
+            // Try to find a spot in one of the cached texture
+            let res = null;
+            for (var map of this._groupCacheMaps) {
+                let node = map.allocateRect(size);
+                if (node) {
+                    res = { node: node, texture: map }
+                    break;
+                }
+            }
+
+            // Couldn't find a map that could fit the rect, create a new map for it
+            if (!res) {
+                let mapSize = new Size(Canvas2D._groupTextureCacheSize, Canvas2D._groupTextureCacheSize);
+
+                // Check if the predefined size would fit, other create a custom size using the nearest bigger power of 2
+                if (size.width > mapSize.width || size.height > mapSize.height) {
+                    mapSize.width = Math.pow(2, Math.ceil(Math.log(size.width) / Math.log(2)));
+                    mapSize.height = Math.pow(2, Math.ceil(Math.log(size.height) / Math.log(2)));
+                }
+
+                let id = `groupsMapChache${this._mapCounter}forCanvas${this.id}`;
+                map = new MapTexture(id, this._scene, mapSize);
+                this._groupCacheMaps.push(map);
+
+                let node = map.allocateRect(size);
+                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
+            // 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;
+            }
+            return res;
+        }
+
+        /**
+         * Define the default size used for both the width and height of a MapTexture to allocate.
+         * Note that some MapTexture might be bigger than this size if the first node to allocate is bigger in width or height
+         */
+        private static _groupTextureCacheSize = 1024;
+
+        /**
+         * Get a Solid Color Brush instance matching the given color.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorBrush2D class that use the given color
+         */
+        public static GetSolidColorBrush(color: Color4): IBrush2D {
+            return Canvas2D._solidColorBrushes.getOrAddWithFactory(color.toHexString(), () => new SolidColorBrush2D(color.clone(), true));
+        }
+
+        /**
+         * Get a Solid Color Brush instance matching the given color expressed as a CSS formatted hexadecimal value.
+         * @param color The color to retrieve
+         * @return A shared instance of the SolidColorBrush2D class that uses the given color
+         */
+        public static GetSolidColorBrushFromHex(hexValue: string): IBrush2D {
+            return Canvas2D._solidColorBrushes.getOrAddWithFactory(hexValue, () => new SolidColorBrush2D(Color4.FromHexString(hexValue), true));
+        }
+
+        public static GetGradientColorBrush(color1: Color4, color2: Color4, translation: Vector2 = Vector2.Zero(), rotation: number = 0, scale: number = 1): IBrush2D {
+            return Canvas2D._gradientColorBrushes.getOrAddWithFactory(GradientColorBrush2D.BuildKey(color1, color2, translation, rotation, scale), () => new GradientColorBrush2D(color1, color2, translation, rotation, scale, true));
+        }
+
+        private static _solidColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
+        private static _gradientColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
+    }
+}

+ 341 - 0
src/Canvas2d/babylon.group2d.js

@@ -0,0 +1,341 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var Group2D = (function (_super) {
+        __extends(Group2D, _super);
+        function Group2D() {
+            _super.call(this);
+            this._primDirtyList = new Array();
+            this._childrenRenderableGroups = new Array();
+            this.groupRenderInfo = new BABYLON.StringDictionary();
+        }
+        Group2D.CreateGroup2D = function (parent, id, position, size, cacheBehabior) {
+            if (cacheBehabior === void 0) { cacheBehabior = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY; }
+            BABYLON.Prim2DBase.CheckParent(parent);
+            var g = new Group2D();
+            g.setupGroup2D(parent.owner, parent, id, position, size, cacheBehabior);
+            return g;
+        };
+        /**
+         * 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
+         */
+        Group2D.prototype.setupGroup2D = function (owner, parent, id, position, size, cacheBehavior) {
+            if (cacheBehavior === void 0) { cacheBehavior = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY; }
+            this._cacheBehavior = cacheBehavior;
+            this.setupPrim2DBase(owner, parent, id, position);
+            this.size = size;
+            this._viewportPosition = BABYLON.Vector2.Zero();
+        };
+        Object.defineProperty(Group2D.prototype, "isRenderableGroup", {
+            get: function () {
+                return this._isRenderableGroup;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Group2D.prototype, "isCachedGroup", {
+            get: function () {
+                return this._isCachedGroup;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Group2D.prototype, "size", {
+            get: function () {
+                return this._size;
+            },
+            set: function (val) {
+                this._size = val;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Group2D.prototype, "viewportSize", {
+            get: function () {
+                return this._viewportSize;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Group2D.prototype, "actualSize", {
+            get: function () {
+                // Return the size if set by the user
+                if (this._size) {
+                    return this._size;
+                }
+                // Otherwise the size is computed based on the boundingInfo
+                var size = this.boundingInfo.extent.clone();
+                return size;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Group2D.prototype, "cacheBehavior", {
+            get: function () {
+                return this._cacheBehavior;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Group2D.prototype._addPrimToDirtyList = function (prim) {
+            this._primDirtyList.push(prim);
+        };
+        Group2D.prototype.updateLevelBoundingInfo = function () {
+            var size;
+            // If the size is set by the user, the boundingInfo is compute from this value
+            if (this.size) {
+                size = this.size;
+            }
+            else {
+                size = new BABYLON.Size(0, 0);
+            }
+            this._levelBoundingInfo.radius = Math.sqrt(size.width * size.width + size.height * size.height);
+            this._levelBoundingInfo.extent = size.clone();
+        };
+        // Method called only on renderable groups to prepare the rendering
+        Group2D.prototype._prepareGroupRender = function (context) {
+            var childrenContext = this._buildChildContext(context);
+            var sortedDirtyList = null;
+            // Update the Global Transformation and visibility status of the changed primitives
+            if ((this._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
+                sortedDirtyList = this._primDirtyList.sort(function (a, b) { return a.hierarchyDepth - b.hierarchyDepth; });
+                this.updateGlobalTransVisOf(sortedDirtyList, childrenContext, true);
+            }
+            // 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
+            if (!this._isCachedGroup) {
+                // Compute the WebGL viewport's location/size
+                var t = this._globalTransform.getTranslation();
+                var s = this.actualSize.clone();
+                var rs = this.owner._renderingSize;
+                s.height = Math.min(s.height, rs.height - t.y);
+                s.width = Math.min(s.width, rs.width - t.x);
+                var x = t.x;
+                var y = (rs.height - s.height) - 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;
+                this._viewportPosition.y = y;
+                var vw = s.width;
+                var vh = s.height;
+                if (!this._viewportSize) {
+                    this._viewportSize = new BABYLON.Size(vw, vh);
+                }
+                else {
+                    if (this._viewportSize.width !== vw || this._viewportSize.height !== vh) {
+                        context.forceRefreshPrimitive = true;
+                    }
+                    this._viewportSize.width = vw;
+                    this._viewportSize.height = vh;
+                }
+            }
+            else {
+                this._viewportSize = this.actualSize;
+            }
+            if ((this._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
+                // If the group is cached, set the dirty flag to true because of the incoming changes
+                this._cacheGroupDirty = this._isCachedGroup;
+                // If it's a force refresh, prepare all the children
+                if (context.forceRefreshPrimitive) {
+                    for (var _i = 0, _a = this._children; _i < _a.length; _i++) {
+                        var p = _a[_i];
+                        p._prepareRender(childrenContext);
+                    }
+                }
+                else {
+                    // Each primitive that changed at least once was added into the primDirtyList, we have to sort this level using
+                    //  the hierarchyDepth in order to prepare primitives from top to bottom
+                    if (!sortedDirtyList) {
+                        sortedDirtyList = this._primDirtyList.sort(function (a, b) { return a.hierarchyDepth - b.hierarchyDepth; });
+                    }
+                    sortedDirtyList.forEach(function (p) {
+                        // We need to check if prepare is needed because even if the primitive is in the dirtyList, its parent primitive may also have been modified, then prepared, then recurse on its children primitives (this one for instance) if the changes where impacting them.
+                        // For instance: a Rect's position change, the position of its children primitives will also change so a prepare will be call on them. If a child was in the dirtyList we will avoid a second prepare by making this check.
+                        if (p.needPrepare()) {
+                            p._prepareRender(childrenContext);
+                        }
+                    });
+                    // Everything is updated, clear the dirty list
+                    this._primDirtyList.splice(0);
+                }
+            }
+            // A renderable group has a list of direct children that are also renderable groups, we recurse on them to also prepare them
+            this._childrenRenderableGroups.forEach(function (g) {
+                g._prepareGroupRender(childrenContext);
+            });
+        };
+        Group2D.prototype._groupRender = function (context) {
+            var engine = this.owner.engine;
+            var failedCount = 0;
+            // First recurse to children render group to render them (in their cache or on screen)
+            var childrenContext = this._buildChildContext(context);
+            for (var _i = 0, _a = this._childrenRenderableGroups; _i < _a.length; _i++) {
+                var childGroup = _a[_i];
+                childGroup._groupRender(childrenContext);
+            }
+            // Render the primitives if needed: either if we don't cache the content or if the content is cached but has changed
+            if (!this.isCachedGroup || this._cacheGroupDirty) {
+                if (this.isCachedGroup) {
+                    this._bindCacheTarget();
+                }
+                else {
+                    var curVP = engine.setDirectViewport(this._viewportPosition.x, this._viewportPosition.y, this._viewportSize.width, this._viewportSize.height);
+                }
+                // For each different model of primitive to render
+                this.groupRenderInfo.forEach(function (k, v) {
+                    // If the instances of the model was changed, pack the data
+                    var instanceData = v._instancesData.pack();
+                    // Compute the size the instance buffer should have
+                    var neededSize = v._instancesData.usedElementCount * v._instancesData.stride * 4;
+                    // Check if we have to (re)create the instancesBuffer because there's none or the size doesn't match
+                    if (!v._instancesBuffer || (v._instancesBufferSize !== neededSize)) {
+                        if (v._instancesBuffer) {
+                            engine.deleteInstancesBuffer(v._instancesBuffer);
+                        }
+                        v._instancesBuffer = engine.createInstancesBuffer(neededSize);
+                        v._instancesBufferSize = neededSize;
+                        v._dirtyInstancesData = true;
+                        // Update the WebGL buffer to match the new content of the instances data
+                        engine._gl.bufferSubData(engine._gl.ARRAY_BUFFER, 0, instanceData);
+                    }
+                    else if (v._dirtyInstancesData) {
+                        // Update the WebGL buffer to match the new content of the instances data
+                        engine._gl.bindBuffer(engine._gl.ARRAY_BUFFER, v._instancesBuffer);
+                        engine._gl.bufferSubData(engine._gl.ARRAY_BUFFER, 0, instanceData);
+                        v._dirtyInstancesData = false;
+                    }
+                    // render all the instances of this model, if the render method returns true then our instances are no longer dirty
+                    var renderFailed = !v._modelCache.render(v, context);
+                    // Update dirty flag/related
+                    v._dirtyInstancesData = renderFailed;
+                    failedCount += renderFailed ? 1 : 0;
+                });
+                // The group's content is no longer dirty
+                this._cacheGroupDirty = failedCount !== 0;
+                if (this.isCachedGroup) {
+                    this._unbindCacheTarget();
+                }
+                else {
+                    if (curVP) {
+                        engine.setViewport(curVP);
+                    }
+                }
+            }
+        };
+        Group2D.prototype._bindCacheTarget = function () {
+            // Check if we have to allocate a rendering zone in the global cache texture
+            if (!this._cacheNode) {
+                var res = this.owner._allocateGroupCache(this);
+                this._cacheNode = res.node;
+                this._cacheTexture = res.texture;
+            }
+            var n = this._cacheNode;
+            this._cacheTexture.bindTextureForRect(n);
+        };
+        Group2D.prototype._unbindCacheTarget = function () {
+            if (this._cacheTexture) {
+                this._cacheTexture.unbindTexture();
+            }
+        };
+        Group2D.prototype.detectGroupStates = function () {
+            var isCanvas = this instanceof BABYLON.Canvas2D;
+            var canvasStrat = this.owner.cachingStrategy;
+            // In Don't Cache mode, only the canvas is renderable, all the other groups are logical. There are not a single cached group.
+            if (canvasStrat === BABYLON.Canvas2D.CACHESTRATEGY_DONTCACHE) {
+                this._isRenderableGroup = isCanvas;
+                this._isCachedGroup = false;
+            }
+            else if (canvasStrat === BABYLON.Canvas2D.CACHESTRATEGY_CANVAS) {
+                if (isCanvas) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = true;
+                }
+                else {
+                    this._isRenderableGroup = false;
+                    this._isCachedGroup = false;
+                }
+            }
+            else if (canvasStrat === BABYLON.Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                if (isCanvas) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = false;
+                }
+                else {
+                    if (this.hierarchyDepth === 1) {
+                        this._isRenderableGroup = true;
+                        this._isCachedGroup = true;
+                    }
+                    else {
+                        this._isRenderableGroup = false;
+                        this._isCachedGroup = false;
+                    }
+                }
+            }
+            else if (canvasStrat === BABYLON.Canvas2D.CACHESTRATEGY_ALLGROUPS) {
+                var gcb = this.cacheBehavior;
+                if ((gcb === Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE) || (gcb === Group2D.GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP)) {
+                    this._isRenderableGroup = gcb === Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE;
+                    this._isCachedGroup = false;
+                }
+                if (gcb === Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = true;
+                }
+            }
+            // If the group is tagged as renderable we add it to the renderable tree
+            if (this._isCachedGroup) {
+                var cur = this.parent;
+                while (cur) {
+                    if (cur instanceof Group2D && cur._isRenderableGroup) {
+                        cur._childrenRenderableGroups.push(this);
+                        break;
+                    }
+                    cur = cur.parent;
+                }
+            }
+        };
+        Group2D.GROUP2D_PROPCOUNT = BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 10;
+        /**
+         * Default behavior, the group will use the caching strategy defined at the Canvas Level
+         */
+        Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY = 0;
+        /**
+         * When used, this group's content won't be cached, no matter which strategy used.
+         * If the group is part of a WorldSpace Canvas, its content will be drawn in the Canvas cache bitmap.
+         */
+        Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE = 1;
+        /**
+         * When used, the group's content will be cached in the nearest cached parent group/canvas
+         */
+        Group2D.GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP = 2;
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 1, function (pi) { return Group2D.sizeProperty = pi; }, false, true)
+        ], Group2D.prototype, "size", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 2, function (pi) { return Group2D.actualSizeProperty = pi; })
+        ], Group2D.prototype, "actualSize", null);
+        Group2D = __decorate([
+            BABYLON.className("Group2D")
+        ], Group2D);
+        return Group2D;
+    }(BABYLON.Prim2DBase));
+    BABYLON.Group2D = Group2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.group2d.js.map

+ 394 - 0
src/Canvas2d/babylon.group2d.ts

@@ -0,0 +1,394 @@
+module BABYLON {
+    @className("Group2D")
+    export class Group2D extends Prim2DBase {
+        static GROUP2D_PROPCOUNT: number = Prim2DBase.PRIM2DBASE_PROPCOUNT + 5;
+
+        public static sizeProperty: Prim2DPropInfo;
+        public static actualSizeProperty: Prim2DPropInfo;
+
+      /**
+         * Default behavior, the group will use the caching strategy defined at the Canvas Level
+         */
+        public static GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY = 0;
+
+        /**
+         * When used, this group's content won't be cached, no matter which strategy used.
+         * If the group is part of a WorldSpace Canvas, its content will be drawn in the Canvas cache bitmap.
+         */
+        public static GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE = 1;
+
+        /**
+         * When used, the group's content will be cached in the nearest cached parent group/canvas
+         */
+        public static GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP = 2;
+
+        constructor() {
+            super();
+            this._primDirtyList = new Array<Prim2DBase>();
+            this._childrenRenderableGroups = new Array<Group2D>();
+            this.groupRenderInfo = new StringDictionary<GroupInstanceInfo>();
+        }
+
+        static CreateGroup2D(parent: Prim2DBase, id: string, position: Vector2, size?: Size, cacheBehabior: number = Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY): Group2D {
+            Prim2DBase.CheckParent(parent);
+            var g = new Group2D();
+            g.setupGroup2D(parent.owner, parent, id, position, size, cacheBehabior);
+
+            return g;
+        }
+
+        applyCachedTexture(vertexData: VertexData, material: StandardMaterial) {
+            this._bindCacheTarget();
+
+            var uv = vertexData.uvs;
+            let nodeuv = this._cacheNode.UVs;
+            for (let i = 0; i < 4; i++) {
+                uv[i * 2 + 0] = nodeuv[i].x;
+                uv[i * 2 + 1] = nodeuv[i].y;
+            }
+
+            material.diffuseTexture = this._cacheTexture;
+            material.emissiveColor = new Color3(1, 1, 1);
+            this._cacheTexture.hasAlpha = true;
+            this._unbindCacheTarget();
+        }
+
+        /**
+         * 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) {
+            this._cacheBehavior = cacheBehavior;
+            this.setupPrim2DBase(owner, parent, id, position);
+            this.size = size;
+            this._viewportPosition = Vector2.Zero();
+        }
+
+        public get isRenderableGroup(): boolean {
+            return this._isRenderableGroup;
+        }
+
+        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;
+        }
+
+        public set size(val: Size) {
+            this._size = val;
+        }
+
+        public get viewportSize(): ISize {
+            return this._viewportSize;
+        }
+
+        @instanceLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 2, pi => Group2D.actualSizeProperty = pi)
+        public get actualSize(): Size {
+            // Return the size if set by the user
+            if (this._size) {
+                return this._size;
+            }
+
+            // Otherwise the size is computed based on the boundingInfo
+            let m = this.boundingInfo.max();
+            return new Size(m.x, m.y);
+        }
+
+        public get cacheBehavior(): number {
+            return this._cacheBehavior;
+        }
+
+        public _addPrimToDirtyList(prim: Prim2DBase) {
+            this._primDirtyList.push(prim);
+        }
+
+        protected updateLevelBoundingInfo() {
+            let size: Size;
+
+            // If the size is set by the user, the boundingInfo is computed from this value
+            if (this.size) {
+                size = this.size;
+            }
+            // Otherwise the group's level bouding info is "collapsed"
+            else {
+                size = new Size(0, 0);
+            }
+
+            BoundingInfo2D.CreateFromSizeToRef(size, this._levelBoundingInfo);
+        }
+
+        // Method called only on renderable groups to prepare the rendering
+        protected _prepareGroupRender(context: Render2DContext) {
+            let sortedDirtyList: Prim2DBase[] = null;
+
+            // Update the Global Transformation and visibility status of the changed primitives
+            if ((this._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
+                sortedDirtyList = this._primDirtyList.sort((a, b) => a.hierarchyDepth - b.hierarchyDepth);
+                this.updateGlobalTransVisOf(sortedDirtyList, true);
+            }
+
+            // 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
+            if (!this._isCachedGroup) {
+                // Compute the WebGL viewport's location/size
+                let t = this._globalTransform.getTranslation();
+                let s = this.actualSize.clone();
+                let rs = this.owner._renderingSize;
+                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;
+
+                // 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;
+                this._viewportPosition.y = y;
+                let vw = s.width;
+                let vh = s.height;
+
+                if (!this._viewportSize) {
+                    this._viewportSize = new Size(vw, vh);
+                } else {
+                    if (this._viewportSize.width !== vw || this._viewportSize.height !== vh) {
+                        context.forceRefreshPrimitive = true;
+                    }
+                    this._viewportSize.width = vw;
+                    this._viewportSize.height = vh;
+                }
+            } else {
+                this._viewportSize = this.actualSize;
+            }
+
+            if ((this._primDirtyList.length > 0) || context.forceRefreshPrimitive) {
+                // If the group is cached, set the dirty flag to true because of the incoming changes
+                this._cacheGroupDirty = this._isCachedGroup;
+
+                // If it's a force refresh, prepare all the children
+                if (context.forceRefreshPrimitive) {
+                    for (let p of this._children) {
+                        p._prepareRender(context);
+                    }
+                } else {
+                    // Each primitive that changed at least once was added into the primDirtyList, we have to sort this level using
+                    //  the hierarchyDepth in order to prepare primitives from top to bottom
+                    if (!sortedDirtyList) {
+                        sortedDirtyList = this._primDirtyList.sort((a, b) => a.hierarchyDepth - b.hierarchyDepth);
+                    }
+
+                    sortedDirtyList.forEach(p => {
+
+                        // We need to check if prepare is needed because even if the primitive is in the dirtyList, its parent primitive may also have been modified, then prepared, then recurse on its children primitives (this one for instance) if the changes where impacting them.
+                        // For instance: a Rect's position change, the position of its children primitives will also change so a prepare will be call on them. If a child was in the dirtyList we will avoid a second prepare by making this check.
+                        if (p.needPrepare()) {
+                            p._prepareRender(context);
+                        }
+                    });
+
+                    // Everything is updated, clear the dirty list
+                    this._primDirtyList.splice(0);
+                }
+            }
+
+            // A renderable group has a list of direct children that are also renderable groups, we recurse on them to also prepare them
+            this._childrenRenderableGroups.forEach(g => {
+                g._prepareGroupRender(context);
+            });
+        }
+
+        protected _groupRender(context: Render2DContext) {
+            let engine = this.owner.engine;
+            let failedCount = 0;
+
+            // First recurse to children render group to render them (in their cache or on screen)
+            for (let childGroup of this._childrenRenderableGroups) {
+                childGroup._groupRender(context);
+            }
+
+            // Render the primitives if needed: either if we don't cache the content or if the content is cached but has changed
+            if (!this.isCachedGroup || this._cacheGroupDirty) {
+                if (this.isCachedGroup) {
+                    this._bindCacheTarget();
+                } else {
+                    var curVP = engine.setDirectViewport(this._viewportPosition.x, this._viewportPosition.y, this._viewportSize.width, this._viewportSize.height);
+                }
+
+                // For each different model of primitive to render
+                this.groupRenderInfo.forEach((k, v) => {
+                    for (let i = 0; i < v._instancesPartsData.length; i++) {
+                        // If the instances of the model was changed, pack the data
+                        let instanceData = v._instancesPartsData[i].pack();
+
+                        // Compute the size the instance buffer should have
+                        let neededSize = v._instancesPartsData[i].usedElementCount * v._instancesPartsData[i].stride * 4;
+
+                        // Check if we have to (re)create the instancesBuffer because there's none or the size doesn't match
+                        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;
+
+                            // Update the WebGL buffer to match the new content of the instances data
+                            engine._gl.bufferSubData(engine._gl.ARRAY_BUFFER, 0, instanceData);
+                        } else if (v._dirtyInstancesData) {
+                            // Update the WebGL buffer to match the new content of the instances data
+                            engine._gl.bindBuffer(engine._gl.ARRAY_BUFFER, v._instancesPartsBuffer[i]);
+                            engine._gl.bufferSubData(engine._gl.ARRAY_BUFFER, 0, instanceData);
+
+                            v._dirtyInstancesData = false;
+                        }
+                    }
+                    // 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);
+
+                    // Update dirty flag/related
+                    v._dirtyInstancesData = renderFailed;
+                    failedCount += renderFailed ? 1 : 0;
+                });
+
+                // The group's content is no longer dirty
+                this._cacheGroupDirty = failedCount !== 0;
+
+                if (this.isCachedGroup) {
+                    this._unbindCacheTarget();
+                } else {
+                    if (curVP) {
+                        engine.setViewport(curVP);
+                    }
+                }
+            }
+        }
+
+        private _bindCacheTarget() {
+            // Check if we have to allocate a rendering zone in the global cache texture
+            if (!this._cacheNode) {
+                var res = this.owner._allocateGroupCache(this);
+                this._cacheNode = res.node;
+                this._cacheTexture = res.texture;
+                this._cacheRenderSprite = res.sprite;
+            }
+
+            let n = this._cacheNode;
+            this._cacheTexture.bindTextureForRect(n, true);
+        }
+
+        private _unbindCacheTarget() {
+            if (this._cacheTexture) {
+                this._cacheTexture.unbindTexture();
+            }
+        }
+
+        protected handleGroupChanged(prop: Prim2DPropInfo) {
+            // This method is only for cachedGroup
+            if (!this.isCachedGroup || !this._cacheRenderSprite) {
+                return;
+            }
+
+            // For now we only support these property changes
+            // TODO: add more! :)
+            if (prop.id === Prim2DBase.positionProperty.id) {
+                this._cacheRenderSprite.position = this.position.clone();
+            } else if (prop.id === Prim2DBase.rotationProperty.id) {
+                this._cacheRenderSprite.rotation = this.rotation;
+            } else if (prop.id === Prim2DBase.scaleProperty.id) {
+                this._cacheRenderSprite.scale = this.scale;
+            }
+        }
+
+        private detectGroupStates() {
+            var isCanvas = this instanceof Canvas2D;
+            var canvasStrat = this.owner.cachingStrategy;
+
+            // In Don't Cache mode, only the canvas is renderable, all the other groups are logical. There are not a single cached group.
+            if (canvasStrat === Canvas2D.CACHESTRATEGY_DONTCACHE) {
+                this._isRenderableGroup = isCanvas;
+                this._isCachedGroup = false;
+            }
+
+            // In Canvas cached only mode, only the Canvas is cached and renderable, all other groups are logicals
+            else if (canvasStrat === Canvas2D.CACHESTRATEGY_CANVAS) {
+                if (isCanvas) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = true;
+                } else {
+                    this._isRenderableGroup = false;
+                    this._isCachedGroup = false;
+                }
+            }
+
+            // Top Level Groups cached only mode, the canvas is a renderable/not cached, its direct Groups are cached/renderable, all other group are logicals
+            else if (canvasStrat === Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+                if (isCanvas) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = false;
+                } else {
+                    if (this.hierarchyDepth === 1) {
+                        this._isRenderableGroup = true;
+                        this._isCachedGroup = true;
+                    } else {
+                        this._isRenderableGroup = false;
+                        this._isCachedGroup = false;
+                    }
+                }
+            }
+
+            // All Group cached mode, all groups are renderable/cached, including the Canvas, groups with the behavior DONTCACHE are renderable/not cached, groups with CACHEINPARENT are logical ones
+            else if (canvasStrat === Canvas2D.CACHESTRATEGY_ALLGROUPS) {
+                var gcb = this.cacheBehavior;
+                if ((gcb === Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE) || (gcb === Group2D.GROUPCACHEBEHAVIOR_CACHEINPARENTGROUP)) {
+                    this._isRenderableGroup = gcb === Group2D.GROUPCACHEBEHAVIOR_DONTCACHEOVERRIDE;
+                    this._isCachedGroup = false;
+                }
+
+                if (gcb === Group2D.GROUPCACHEBEHAVIOR_FOLLOWCACHESTRATEGY) {
+                    this._isRenderableGroup = true;
+                    this._isCachedGroup = true;
+                }
+            }
+
+            // If the group is tagged as renderable we add it to the renderable tree
+            if (this._isCachedGroup) {
+                let cur = this.parent;
+                while (cur) {
+                    if (cur instanceof Group2D && cur._isRenderableGroup) {
+                        cur._childrenRenderableGroups.push(this);
+                        break;
+                    }
+                    cur = cur.parent;
+                }
+            }
+        }
+
+        protected _isRenderableGroup: boolean;
+        protected _isCachedGroup: boolean;
+        private _cacheGroupDirty: boolean;
+        protected _childrenRenderableGroups: Array<Group2D>;
+        private _size: Size;
+        private _cacheBehavior: number;
+        private _primDirtyList: Array<Prim2DBase>;
+        private _cacheNode: PackedRect;
+        private _cacheTexture: MapTexture;
+        private _cacheRenderSprite: Sprite2D;
+        private _viewportPosition: Vector2;
+        private _viewportSize: Size;
+
+        groupRenderInfo: StringDictionary<GroupInstanceInfo>;
+    }
+
+}

+ 54 - 0
src/Canvas2d/babylon.modelRenderCache.js

@@ -0,0 +1,54 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var BABYLON;
+(function (BABYLON) {
+    var GroupInstanceInfo = (function () {
+        function GroupInstanceInfo(owner, classTreeInfo, cache) {
+            this._owner = owner;
+            this._classTreeInfo = classTreeInfo;
+            this._modelCache = cache;
+        }
+        return GroupInstanceInfo;
+    }());
+    BABYLON.GroupInstanceInfo = GroupInstanceInfo;
+    var ModelRenderCacheBase = (function () {
+        function ModelRenderCacheBase() {
+        }
+        /**
+         * Render the model instances
+         * @param instanceInfo
+         * @param context
+         * @return must return true is the rendering succeed, false if the rendering couldn't be done (asset's not yet ready, like Effect)
+         */
+        ModelRenderCacheBase.prototype.render = function (instanceInfo, context) {
+            return true;
+        };
+        return ModelRenderCacheBase;
+    }());
+    BABYLON.ModelRenderCacheBase = ModelRenderCacheBase;
+    var ModelRenderCache = (function (_super) {
+        __extends(ModelRenderCache, _super);
+        function ModelRenderCache() {
+            _super.call(this);
+            this._nextKey = 1;
+            this._instancesData = new BABYLON.StringDictionary();
+        }
+        ModelRenderCache.prototype.addInstanceData = function (data) {
+            var key = this._nextKey.toString();
+            if (!this._instancesData.add(key, data)) {
+                throw Error("Key: " + key + " is already allocated");
+            }
+            ++this._nextKey;
+            return key;
+        };
+        ModelRenderCache.prototype.removeInstanceData = function (key) {
+            this._instancesData.remove(key);
+        };
+        return ModelRenderCache;
+    }(ModelRenderCacheBase));
+    BABYLON.ModelRenderCache = ModelRenderCache;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.modelRenderCache.js.map

+ 166 - 0
src/Canvas2d/babylon.modelRenderCache.ts

@@ -0,0 +1,166 @@
+module BABYLON {
+    export const enum ShaderDataType {
+        Vector2, Vector3, Vector4, Matrix, float, Color3, Color4
+    }
+
+    export class GroupInstanceInfo {
+        constructor(owner: Group2D, cache: ModelRenderCache) {
+            this._owner = owner;
+            this._modelCache = cache;
+            this._instancesPartsData = new Array<DynamicFloatArray>();
+            this._instancesPartsBuffer = new Array<WebGLBuffer>();
+            this._instancesPartsBufferSize = new Array<number>();
+            this._partIndexFromId = new StringDictionary<number>();
+            this._instancesPartsUsedShaderCategories = new Array<string>();
+        }
+
+        _owner: Group2D;
+        _modelCache: ModelRenderCache;
+        _partIndexFromId: StringDictionary<number>;
+        _instancesPartsData: DynamicFloatArray[];
+        _dirtyInstancesData: boolean;
+        _instancesPartsBuffer: WebGLBuffer[];
+        _instancesPartsBufferSize: number[];
+        _instancesPartsUsedShaderCategories: string[];
+    }
+
+    export class ModelRenderCache {
+        constructor(modelKey: string, isTransparent: boolean) {
+            this._modelKey = modelKey;
+            this._isTransparent = isTransparent;
+            this._nextKey = 1;
+            this._instancesData = new StringDictionary<InstanceDataBase[]>();
+        }
+
+        /**
+         * Render the model instances
+         * @param instanceInfo
+         * @param context
+         * @return must return true is the rendering succeed, false if the rendering couldn't be done (asset's not yet ready, like Effect)
+         */
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            return true;
+        }
+
+        addInstanceDataParts(data: InstanceDataBase[]): string {
+            let key = this._nextKey.toString();
+
+            if (!this._instancesData.add(key, data)) {
+                throw Error(`Key: ${key} is already allocated`);
+            }
+
+            ++this._nextKey;
+
+            return key;
+        }
+
+        removeInstanceData(key: string) {
+            this._instancesData.remove(key);
+        }
+
+        protected getPartIndexFromId(partId: number) {
+            for (var i = 0; i < this._partIdList.length; i++) {
+                if (this._partIdList[i] === partId) {
+                    return i;
+                }
+            }
+            return null;
+        }
+
+        protected loadInstancingAttributes(partId: number, effect: Effect): InstancingAttributeInfo[] {
+            let i = this.getPartIndexFromId(partId);
+            if (i === null) {
+                return null;
+            }
+
+            var ci = this._partsClassInfo[i];
+            var categories = this._partsUsedCategories[i];
+            let res = ci.classContent.getInstancingAttributeInfos(effect, categories);
+
+            return res;
+        }
+
+        //setupUniformsLocation(effect: Effect, uniforms: string[], partId: number) {
+        //    let i = this.getPartIndexFromId(partId);
+        //    if (i === null) {
+        //        return null;
+        //    }
+
+        //    let pci = this._partsClassInfo[i];
+        //    pci.fullContent.forEach((k, v) => {
+        //        if (uniforms.indexOf(v.attributeName) !== -1) {
+        //            v.uniformLocation = effect.getUniform(v.attributeName);
+        //        }
+        //    });
+        //}
+
+        private static v2 = Vector2.Zero();
+        private static v3 = Vector3.Zero();
+        private static v4 = Vector4.Zero();
+
+        protected setupUniforms(effect: Effect, partIndex: number, data: DynamicFloatArray, elementCount: number) {
+            let offset = (this._partsDataStride[partIndex]/4) * elementCount;
+            let pci = this._partsClassInfo[partIndex];
+
+            let self = this;
+            pci.fullContent.forEach((k, v) => {
+                if (!v.category || self._partsUsedCategories[partIndex].indexOf(v.category)!==1) {
+                    switch (v.dataType) {
+                        case ShaderDataType.float:
+                        {
+                            let attribOffset = v.instanceOffset.get(self._partsJoinedUsedCategories[partIndex]);
+                            effect.setFloat(v.attributeName, data.buffer[offset + attribOffset]);
+                            break;
+                        }
+                        case ShaderDataType.Vector2:
+                        {
+                            let attribOffset = v.instanceOffset.get(self._partsJoinedUsedCategories[partIndex]);
+                            ModelRenderCache.v2.x = data.buffer[offset + attribOffset + 0];
+                            ModelRenderCache.v2.y = data.buffer[offset + attribOffset + 1];
+                            effect.setVector2(v.attributeName, ModelRenderCache.v2);
+                            break;
+                        }
+                        case ShaderDataType.Color3:
+                        case ShaderDataType.Vector3:
+                        {
+                            let attribOffset = v.instanceOffset.get(self._partsJoinedUsedCategories[partIndex]);
+                            ModelRenderCache.v3.x = data.buffer[offset + attribOffset + 0];
+                            ModelRenderCache.v3.y = data.buffer[offset + attribOffset + 1];
+                            ModelRenderCache.v3.z = data.buffer[offset + attribOffset + 2];
+                            effect.setVector3(v.attributeName, ModelRenderCache.v3);
+                            break;
+                        }
+                        case ShaderDataType.Color4:
+                        case ShaderDataType.Vector4:
+                        {
+                            let attribOffset = v.instanceOffset.get(self._partsJoinedUsedCategories[partIndex]);
+                            ModelRenderCache.v4.x = data.buffer[offset + attribOffset + 0];
+                            ModelRenderCache.v4.y = data.buffer[offset + attribOffset + 1];
+                            ModelRenderCache.v4.z = data.buffer[offset + attribOffset + 2];
+                            ModelRenderCache.v4.w = data.buffer[offset + attribOffset + 3];
+                            effect.setVector4(v.attributeName, ModelRenderCache.v4);
+                            break;
+                        }
+                        default:
+                    }
+                }
+            });
+        }
+
+        private _modelKey: string;
+        private _isTransparent: boolean;
+
+        public get isTransparent() {
+            return this._isTransparent;
+        }
+
+        _instancesData: StringDictionary<InstanceDataBase[]>;
+
+        private _nextKey: number;
+        _partIdList: number[];
+        _partsDataStride: number[];
+        _partsUsedCategories: Array<string[]>;
+        _partsJoinedUsedCategories: string[];
+        _partsClassInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>[];
+    }
+}

+ 360 - 0
src/Canvas2d/babylon.prim2dBase.js

@@ -0,0 +1,360 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var Render2DContext = (function () {
+        function Render2DContext() {
+        }
+        return Render2DContext;
+    }());
+    BABYLON.Render2DContext = Render2DContext;
+    var Prim2DBase = (function (_super) {
+        __extends(Prim2DBase, _super);
+        function Prim2DBase() {
+            _super.apply(this, arguments);
+        }
+        Prim2DBase.prototype.setupPrim2DBase = function (owner, parent, id, position, isVisible) {
+            if (isVisible === void 0) { isVisible = true; }
+            if (!(this instanceof BABYLON.Group2D) && !(this instanceof BABYLON.Sprite2D && id !== null && id.indexOf("__cachedSpriteOfGroup__") === 0) && (owner.cachingStrategy === BABYLON.Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) && (parent === owner)) {
+                throw new Error("Can't create a primitive with the canvas as direct parent when the caching strategy is TOPLEVELGROUPS. You need to create a Group below the canvas and use it as the parent for the primitive");
+            }
+            this.setupSmartPropertyPrim();
+            this._boundingInfoDirty = true;
+            this._boundingInfo = new BABYLON.BoundingInfo2D();
+            this._owner = owner;
+            this._parent = parent;
+            if (parent != null) {
+                this._hierarchyDepth = parent._hierarchyDepth + 1;
+                this._renderGroup = this.parent.traverseUp(function (p) { return p instanceof BABYLON.Group2D && p.isRenderableGroup; });
+                parent.addChild(this);
+            }
+            else {
+                this._hierarchyDepth = 0;
+                this._renderGroup = null;
+            }
+            this._id = id;
+            this.propertyChanged = new BABYLON.Observable();
+            this._children = new Array();
+            this._parentTranformStep = 0;
+            this._globalTransformStep = 0;
+            if (this instanceof BABYLON.Group2D) {
+                var group = this;
+                group.detectGroupStates();
+            }
+            this.position = position;
+            this.rotation = 0;
+            this.scale = 1;
+            this.levelVisible = isVisible;
+            this.origin = new BABYLON.Vector2(0.5, 0.5);
+        };
+        Prim2DBase.prototype.traverseUp = function (predicate) {
+            var p = this;
+            while (p != null) {
+                if (predicate(p)) {
+                    return p;
+                }
+                p = p._parent;
+            }
+            return null;
+        };
+        Object.defineProperty(Prim2DBase.prototype, "owner", {
+            get: function () {
+                return this._owner;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "parent", {
+            get: function () {
+                return this._parent;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "id", {
+            get: function () {
+                return this._id;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "position", {
+            get: function () {
+                return this._position;
+            },
+            set: function (value) {
+                this._position = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "rotation", {
+            get: function () {
+                return this._rotation;
+            },
+            set: function (value) {
+                this._rotation = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "scale", {
+            get: function () {
+                return this._scale;
+            },
+            set: function (value) {
+                this._scale = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "origin", {
+            get: function () {
+                return this._origin;
+            },
+            set: function (value) {
+                this._origin = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "levelVisible", {
+            get: function () {
+                return this._levelVisible;
+            },
+            set: function (value) {
+                this._levelVisible = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "isVisible", {
+            get: function () {
+                return this._isVisible;
+            },
+            set: function (value) {
+                this._isVisible = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "zOrder", {
+            get: function () {
+                return this._zOrder;
+            },
+            set: function (value) {
+                this._zOrder = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "hierarchyDepth", {
+            get: function () {
+                return this._hierarchyDepth;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "renderGroup", {
+            get: function () {
+                return this._renderGroup;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "globalTransform", {
+            get: function () {
+                return this._globalTransform;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "invGlobalTransform", {
+            get: function () {
+                return this._invGlobalTransform;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Prim2DBase.prototype, "boundingInfo", {
+            get: function () {
+                if (this._boundingInfoDirty) {
+                    this._boundingInfo = this.levelBoundingInfo.clone();
+                    var bi = this._boundingInfo;
+                    var localTransform = new BABYLON.Matrix();
+                    if (this.parent) {
+                        this.globalTransform.multiplyToRef(BABYLON.Matrix.Invert(this.parent.globalTransform), localTransform);
+                    }
+                    else {
+                        localTransform = this.globalTransform;
+                    }
+                    var invLocalTransform = BABYLON.Matrix.Invert(localTransform);
+                    this.levelBoundingInfo.transformToRef(localTransform, bi);
+                    var tps = new BABYLON.BoundingInfo2D();
+                    for (var _i = 0, _a = this._children; _i < _a.length; _i++) {
+                        var curChild = _a[_i];
+                        curChild.boundingInfo.transformToRef(curChild.globalTransform.multiply(invLocalTransform), tps);
+                        bi.unionToRef(tps, bi);
+                    }
+                    this._boundingInfoDirty = false;
+                }
+                return this._boundingInfo;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Prim2DBase.prototype.moveChild = function (child, previous) {
+            if (child.parent !== this) {
+                return false;
+            }
+            var prevOffset, nextOffset;
+            var childIndex = this._children.indexOf(child);
+            var prevIndex = previous ? this._children.indexOf(previous) : -1;
+            // Move to first position
+            if (!previous) {
+                prevOffset = 0;
+                nextOffset = this._children[1]._siblingDepthOffset;
+            }
+            else {
+                prevOffset = this._children[prevIndex]._siblingDepthOffset;
+                nextOffset = this._children[prevIndex + 1]._siblingDepthOffset;
+            }
+            child._siblingDepthOffset = (nextOffset - prevOffset) / 2;
+            this._children.splice(prevIndex + 1, 0, this._children.splice(childIndex, 1)[0]);
+        };
+        Prim2DBase.prototype.addChild = function (child) {
+            child._siblingDepthOffset = (this._children.length + 1) * this.owner.hierarchySiblingZDelta;
+            this._children.push(child);
+        };
+        Prim2DBase.prototype.getActualZOffset = function () {
+            return this._zOrder || this._siblingDepthOffset;
+        };
+        Prim2DBase.prototype.onPrimBecomesDirty = function () {
+            if (this._renderGroup) {
+                //if (this instanceof Group2D) {
+                //    var group: any= this;
+                //    if (group.isRenderableGroup) {
+                //        return;
+                //    }
+                //}
+                this._renderGroup._addPrimToDirtyList(this);
+            }
+        };
+        Prim2DBase.prototype.needPrepare = function () {
+            return this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformPreviousStep !== this._globalTransformStep);
+        };
+        Prim2DBase.prototype._buildChildContext = function (context) {
+            var childContext = new Render2DContext();
+            childContext.camera = context.camera;
+            childContext.parentVisibleState = context.parentVisibleState && this.levelVisible;
+            childContext.parentTransform = this._globalTransform;
+            childContext.parentTransformStep = this._globalTransformStep;
+            childContext.forceRefreshPrimitive = context.forceRefreshPrimitive;
+            return childContext;
+        };
+        Prim2DBase.prototype._prepareRender = function (context) {
+            this._prepareRenderPre(context);
+            this._prepareRenderPost(context);
+        };
+        Prim2DBase.prototype._prepareRenderPre = function (context) {
+        };
+        Prim2DBase.prototype._prepareRenderPost = function (context) {
+            // Don't recurse if it's a renderable group, the content will be processed by the group itself
+            if (this instanceof BABYLON.Group2D) {
+                var self = this;
+                if (self.isRenderableGroup) {
+                    return;
+                }
+            }
+            // Check if we need to recurse the prepare to children primitives
+            //  - must have children
+            //  - the global transform of this level have changed, or
+            //  - the visible state of primitive has changed
+            if (this._children.length > 0 && ((this._globalTransformPreviousStep !== this._globalTransformStep) ||
+                this.checkPropertiesDirty(Prim2DBase.isVisibleProperty.flagId))) {
+                var childContext = this._buildChildContext(context);
+                this._children.forEach(function (c) {
+                    // As usual stop the recursion if we meet a renderable group
+                    if (!(c instanceof BABYLON.Group2D && c.isRenderableGroup)) {
+                        c._prepareRender(childContext);
+                    }
+                });
+            }
+            // Finally reset the dirty flags as we've processed everything
+            this._modelDirty = false;
+            this._instanceDirtyFlags = 0;
+        };
+        Prim2DBase.CheckParent = function (parent) {
+            if (!parent) {
+                throw new Error("A Primitive needs a valid Parent, it can be any kind of Primitives based types, even the Canvas (with the exception that only Group2D can be direct child of a Canvas if the cache strategy used is TOPLEVELGROUPS)");
+            }
+        };
+        Prim2DBase.prototype.updateGlobalTransVisOf = function (list, context, recurse) {
+            for (var _i = 0, list_1 = list; _i < list_1.length; _i++) {
+                var cur = list_1[_i];
+                cur.updateGlobalTransVis(context, recurse);
+            }
+        };
+        Prim2DBase.prototype.updateGlobalTransVis = function (context, recurse) {
+            this._globalTransformPreviousStep = this._globalTransformStep;
+            this.isVisible = context.parentVisibleState && this.levelVisible;
+            // Detect if nothing changed
+            var tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+            if ((context.parentTransformStep === this._parentTranformStep) && !this.checkPropertiesDirty(tflags)) {
+                return;
+            }
+            var rot = BABYLON.Quaternion.RotationAxis(new BABYLON.Vector3(0, 0, 1), this._rotation);
+            var local = BABYLON.Matrix.Compose(new BABYLON.Vector3(this._scale, this._scale, this._scale), rot, new BABYLON.Vector3(this._position.x, this._position.y, 0));
+            this._globalTransform = context.parentTransform.multiply(local);
+            this._invGlobalTransform = BABYLON.Matrix.Invert(this._globalTransform);
+            ++this._globalTransformStep;
+            this._parentTranformStep = context.parentTransformStep;
+            this.clearPropertiesDirty(tflags);
+            if (recurse) {
+                var childrenContext = this._buildChildContext(context);
+                for (var _i = 0, _a = this._children; _i < _a.length; _i++) {
+                    var child = _a[_i];
+                    // Stop the recursion if we meet a renderable group
+                    child.updateGlobalTransVis(childrenContext, !(child instanceof BABYLON.Group2D && child.isRenderableGroup));
+                }
+            }
+        };
+        Prim2DBase.PRIM2DBASE_PROPCOUNT = 10;
+        __decorate([
+            BABYLON.instanceLevelProperty(1, function (pi) { return Prim2DBase.positionProperty = pi; }, false, true)
+        ], Prim2DBase.prototype, "position", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(2, function (pi) { return Prim2DBase.rotationProperty = pi; }, false, true)
+        ], Prim2DBase.prototype, "rotation", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(3, function (pi) { return Prim2DBase.scaleProperty = pi; }, false, true)
+        ], Prim2DBase.prototype, "scale", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(4, function (pi) { return Prim2DBase.originProperty = pi; }, false, true)
+        ], Prim2DBase.prototype, "origin", null);
+        __decorate([
+            BABYLON.dynamicLevelProperty(5, function (pi) { return Prim2DBase.levelVisibleProperty = pi; })
+        ], Prim2DBase.prototype, "levelVisible", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(6, function (pi) { return Prim2DBase.isVisibleProperty = pi; })
+        ], Prim2DBase.prototype, "isVisible", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(7, function (pi) { return Prim2DBase.zOrderProperty = pi; })
+        ], Prim2DBase.prototype, "zOrder", null);
+        Prim2DBase = __decorate([
+            BABYLON.className("Prim2DBase")
+        ], Prim2DBase);
+        return Prim2DBase;
+    }(BABYLON.SmartPropertyPrim));
+    BABYLON.Prim2DBase = Prim2DBase;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.prim2dBase.js.map

+ 344 - 0
src/Canvas2d/babylon.prim2dBase.ts

@@ -0,0 +1,344 @@
+module BABYLON {
+
+    export class Render2DContext {
+        forceRefreshPrimitive: boolean;
+    }
+
+    @className("Prim2DBase")
+    export class Prim2DBase extends SmartPropertyPrim {
+        static PRIM2DBASE_PROPCOUNT: number = 10;
+
+        protected setupPrim2DBase(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, isVisible: boolean = true) {
+            if (!(this instanceof Group2D) && !(this instanceof Sprite2D && id !== null && id.indexOf("__cachedSpriteOfGroup__") === 0) && (owner.cachingStrategy === Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) && (parent === owner)) {
+                throw new Error("Can't create a primitive with the canvas as direct parent when the caching strategy is TOPLEVELGROUPS. You need to create a Group below the canvas and use it as the parent for the primitive");
+            }
+
+            this.setupSmartPropertyPrim();
+            this._boundingInfoDirty = true;
+            this._boundingInfo = new BoundingInfo2D();
+            this._owner = owner;
+            this._parent = parent;
+            if (parent != null) {
+                this._hierarchyDepth = parent._hierarchyDepth + 1;
+                this._renderGroup = <Group2D>this.parent.traverseUp(p => p instanceof Group2D && p.isRenderableGroup);
+                parent.addChild(this);
+            } else {
+                this._hierarchyDepth = 0;
+                this._renderGroup = null;
+            }
+
+            this._id = id;
+            this.propertyChanged = new Observable<PropertyChangedInfo>();
+            this._children = new Array<Prim2DBase>();
+            this._globalTransformProcessStep = 0;
+            this._globalTransformStep = 0;
+
+            if (this instanceof Group2D) {
+                var group: any = this;
+                group.detectGroupStates();
+            }
+
+            this.position = position;
+            this.rotation = 0;
+            this.scale = 1;
+            this.levelVisible = isVisible;
+            this.origin = new Vector2(0.5, 0.5);
+        }
+
+        public traverseUp(predicate: (p: Prim2DBase) => boolean): Prim2DBase {
+            let p: Prim2DBase = this;
+            while (p != null) {
+                if (predicate(p)) {
+                    return p;
+                }
+                p = p._parent;
+            }
+            return null;
+        }
+
+        public get owner(): Canvas2D {
+            return this._owner;
+        }
+
+        public get parent(): Prim2DBase {
+            return this._parent;
+        }
+
+        public get id(): string {
+            return this._id;
+        }
+
+        public static positionProperty: Prim2DPropInfo;
+        public static rotationProperty: Prim2DPropInfo;
+        public static scaleProperty: Prim2DPropInfo;
+        public static originProperty: Prim2DPropInfo;
+        public static levelVisibleProperty: Prim2DPropInfo;
+        public static isVisibleProperty: Prim2DPropInfo;
+        public static zOrderProperty: Prim2DPropInfo;
+
+        @instanceLevelProperty(1, pi => Prim2DBase.positionProperty = pi, false, true)
+        public get position(): Vector2 {
+            return this._position;
+        }
+
+        public set position(value: Vector2) {
+            this._position = value;
+        }
+
+        @instanceLevelProperty(2, pi => Prim2DBase.rotationProperty = pi, false, true)
+        public get rotation(): number {
+            return this._rotation;
+        }
+
+        public set rotation(value: number) {
+            this._rotation = value;
+        }
+
+        @instanceLevelProperty(3, pi => Prim2DBase.scaleProperty = pi, false, true)
+        public set scale(value: number) {
+            this._scale = value;
+        }
+
+        public get scale(): number {
+            return this._scale;
+        }
+        
+        @instanceLevelProperty(4, pi => Prim2DBase.originProperty = pi, false, true)
+        public set origin(value: Vector2) {
+            this._origin = value;
+        }
+
+        /**
+         * The origin defines the normalized coordinate of the center of the primitive, from the top/left corner.
+         * 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,1 means the center is bottom/left
+         * @returns The normalized center.
+         */
+        public get origin(): Vector2 {
+            return this._origin;
+        }
+
+        @dynamicLevelProperty(5, pi => Prim2DBase.levelVisibleProperty = pi)
+        public get levelVisible(): boolean {
+            return this._levelVisible;
+        }
+
+        public set levelVisible(value: boolean) {
+            this._levelVisible = value;
+        }
+
+        @instanceLevelProperty(6, pi => Prim2DBase.isVisibleProperty = pi)
+        public get isVisible(): boolean {
+            return this._isVisible;
+        }
+
+        public set isVisible(value: boolean) {
+            this._isVisible = value;
+        }
+
+        @instanceLevelProperty(7, pi => Prim2DBase.zOrderProperty = pi)
+        public get zOrder(): number {
+            return this._zOrder;
+        }
+
+        public set zOrder(value: number) {
+            this._zOrder = value;
+        }
+
+        public get hierarchyDepth(): number {
+            return this._hierarchyDepth;
+        }
+
+        public get renderGroup(): Group2D {
+            return this._renderGroup;
+        }
+
+        public get globalTransform(): Matrix {
+            return this._globalTransform;
+        }
+
+        public get invGlobalTransform(): Matrix {
+            return this._invGlobalTransform;
+        }
+
+        public get boundingInfo(): BoundingInfo2D {
+            if (this._boundingInfoDirty) {
+                this._boundingInfo = this.levelBoundingInfo.clone();
+                let bi = this._boundingInfo;
+
+                var tps = new BoundingInfo2D();
+                for (let curChild of this._children) {
+                    let t = curChild.globalTransform.multiply(this.invGlobalTransform);
+                    curChild.boundingInfo.transformToRef(t, curChild.origin, tps);
+                    bi.unionToRef(tps, bi);
+                }
+
+                this._boundingInfoDirty = false;
+            }
+            return this._boundingInfo;
+        }
+
+        public moveChild(child: Prim2DBase, previous: Prim2DBase): boolean {
+            if (child.parent !== this) {
+                return false;
+            }
+
+            let prevOffset: number, nextOffset: number;
+            let childIndex = this._children.indexOf(child);
+            let prevIndex = previous ? this._children.indexOf(previous) : -1;
+
+            // Move to first position
+            if (!previous) {
+                prevOffset = 1;
+                nextOffset = this._children[1]._siblingDepthOffset;
+            } else {
+                prevOffset = this._children[prevIndex]._siblingDepthOffset;
+                nextOffset = this._children[prevIndex + 1]._siblingDepthOffset;
+            }
+
+            child._siblingDepthOffset = (nextOffset - prevOffset) / 2;
+
+            this._children.splice(prevIndex + 1, 0, this._children.splice(childIndex, 1)[0]);
+        }
+
+        private addChild(child: Prim2DBase) {
+            child._siblingDepthOffset = (this._children.length + 1) * this.owner.hierarchySiblingZDelta;
+            child._depthLevel = this._depthLevel + 1;
+            child._hierarchyDepthOffset = child._depthLevel * this.owner.hierarchyLevelZFactor;
+            this._children.push(child);
+
+        }
+
+        protected getActualZOffset(): number {
+            return this._zOrder || 1-(this._siblingDepthOffset + this._hierarchyDepthOffset);
+        }
+
+        protected onPrimBecomesDirty() {
+            if (this._renderGroup) {
+                this._renderGroup._addPrimToDirtyList(this);
+            }
+        }
+
+        public needPrepare(): boolean {
+            return this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep);
+        }
+
+        public _prepareRender(context: Render2DContext) {
+            this._prepareRenderPre(context);
+            this._prepareRenderPost(context);
+        }
+
+        public _prepareRenderPre(context: Render2DContext) {
+        }
+
+        public _prepareRenderPost(context: Render2DContext) {
+            // Don't recurse if it's a renderable group, the content will be processed by the group itself
+            if (this instanceof Group2D) {
+                var self: any = this;
+                if (self.isRenderableGroup) {
+                    return;
+                }
+            }
+
+            // Check if we need to recurse the prepare to children primitives
+            //  - must have children
+            //  - the global transform of this level have changed, or
+            //  - the visible state of primitive has changed
+            if (this._children.length > 0 && ((this._globalTransformProcessStep !== this._globalTransformStep) ||
+                this.checkPropertiesDirty(Prim2DBase.isVisibleProperty.flagId))) {
+
+                this._children.forEach(c => {
+                    // As usual stop the recursion if we meet a renderable group
+                    if (!(c instanceof Group2D && c.isRenderableGroup)) {
+                        c._prepareRender(context);
+                    }
+                });
+            }
+
+            // Finally reset the dirty flags as we've processed everything
+            this._modelDirty = false;
+            this._instanceDirtyFlags = 0;
+        }
+
+        protected static CheckParent(parent: Prim2DBase) {
+            if (!parent) {
+                throw new Error("A Primitive needs a valid Parent, it can be any kind of Primitives based types, even the Canvas (with the exception that only Group2D can be direct child of a Canvas if the cache strategy used is TOPLEVELGROUPS)");
+            }
+        }
+
+        protected updateGlobalTransVisOf(list: Prim2DBase[], recurse: boolean) {
+            for (let cur of list) {
+                cur.updateGlobalTransVis(recurse);
+            }
+        }
+
+        protected updateGlobalTransVis(recurse: boolean) {
+            // Check if the parent is synced
+            if (this._parent && this._parent._globalTransformProcessStep !== this.owner._globalTransformProcessStep) {
+                this._parent.updateGlobalTransVis(false);
+            }
+
+            // Check if we must update this prim
+            if (this === <any>this.owner || this._globalTransformProcessStep !== this.owner._globalTransformProcessStep) {
+                this.isVisible = (!this._parent || this._parent.isVisible) && this.levelVisible;
+
+                // Detect if either the parent or this node changed
+                let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+                if ((this._parent && this._parent._globalTransformStep !== this._parentTransformStep) || this.checkPropertiesDirty(tflags)) {
+                    var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
+                    var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
+
+                    this._globalTransform = this._parent ? local.multiply(this._parent._globalTransform) : local;
+                    this._invGlobalTransform = Matrix.Invert(this._globalTransform);
+
+                    this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
+                    this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
+
+                    this.clearPropertiesDirty(tflags);
+                }
+                this._globalTransformProcessStep = this.owner._globalTransformProcessStep;
+            }
+            if (recurse) {
+                for (let child of this._children) {
+                    // Stop the recursion if we meet a renderable group
+                    child.updateGlobalTransVis(!(child instanceof Group2D && child.isRenderableGroup));
+                }
+            }
+        }
+
+        private _owner: Canvas2D;
+        private _parent: Prim2DBase;
+        protected _children: Array<Prim2DBase>;
+        private _renderGroup: Group2D;
+        private _hierarchyDepth: number;
+        protected _depthLevel: number;
+        private _hierarchyDepthOffset: number;
+        private _siblingDepthOffset: number;
+        private _zOrder: number;
+        private _levelVisible: boolean;
+        public _boundingInfoDirty: boolean;
+        private _isVisible: boolean;
+        private _id: string;
+        private _position: Vector2;
+        private _rotation: number;
+        private _scale: number;
+        private _origin: Vector2;
+
+        // Stores the step of the parent for which the current global tranform 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
+        protected _globalTransformStep: number;
+
+        // Stores the previous 
+        protected _globalTransformProcessStep: number;
+        protected _globalTransform: Matrix;
+        protected _invGlobalTransform: Matrix;
+    }
+
+}

+ 174 - 0
src/Canvas2d/babylon.rectangle2d.js

@@ -0,0 +1,174 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var Rectangle2DRenderCache = (function (_super) {
+        __extends(Rectangle2DRenderCache, _super);
+        function Rectangle2DRenderCache() {
+            _super.apply(this, arguments);
+        }
+        Rectangle2DRenderCache.prototype.render = function (instanceInfo, context) {
+            // Do nothing if the shader is still loading/preparing
+            if (!this.effect.isReady()) {
+                return false;
+            }
+            // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+            if (!this.instancingAttributes) {
+                this.instancingAttributes = instanceInfo._classTreeInfo.classContent.getInstancingAttributeInfos(this.effect);
+            }
+            var engine = instanceInfo._owner.owner.engine;
+            engine.enableEffect(this.effect);
+            engine.bindBuffers(this.fillVB, this.fillIB, [1], 4, this.effect);
+            engine.updateAndBindInstancesBuffer(instanceInfo._instancesBuffer, null, this.instancingAttributes);
+            engine.draw(true, 0, Rectangle2D.roundSubdivisions * 4 * 3, instanceInfo._instancesData.usedElementCount);
+            engine.unBindInstancesBuffer(instanceInfo._instancesBuffer, this.instancingAttributes);
+            return true;
+        };
+        return Rectangle2DRenderCache;
+    }(BABYLON.ModelRenderCache));
+    BABYLON.Rectangle2DRenderCache = Rectangle2DRenderCache;
+    var Rectangle2DInstanceData = (function (_super) {
+        __extends(Rectangle2DInstanceData, _super);
+        function Rectangle2DInstanceData() {
+            _super.apply(this, arguments);
+        }
+        Object.defineProperty(Rectangle2DInstanceData.prototype, "properties", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        __decorate([
+            BABYLON.instanceData()
+        ], Rectangle2DInstanceData.prototype, "properties", null);
+        return Rectangle2DInstanceData;
+    }(BABYLON.InstanceDataBase));
+    BABYLON.Rectangle2DInstanceData = Rectangle2DInstanceData;
+    var Rectangle2D = (function (_super) {
+        __extends(Rectangle2D, _super);
+        function Rectangle2D() {
+            _super.apply(this, arguments);
+        }
+        Object.defineProperty(Rectangle2D.prototype, "size", {
+            get: function () {
+                return this._size;
+            },
+            set: function (value) {
+                this._size = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Rectangle2D.prototype, "notRounded", {
+            get: function () {
+                return this._notRounded;
+            },
+            set: function (value) {
+                this._notRounded = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Rectangle2D.prototype, "roundRadius", {
+            get: function () {
+                return this._roundRadius;
+            },
+            set: function (value) {
+                this._roundRadius = value;
+                this.notRounded = value === 0;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Rectangle2D.prototype.updateLevelBoundingInfo = function () {
+            this._levelBoundingInfo.radius = Math.sqrt(this.size.width * this.size.width + this.size.height * this.size.height);
+            this._levelBoundingInfo.extent = this.size.clone();
+        };
+        Rectangle2D.prototype.setupRectangle2D = function (owner, parent, id, position, size, roundRadius, fill, border) {
+            if (roundRadius === void 0) { roundRadius = 0; }
+            this.setupRenderablePrim2D(owner, parent, id, position, true, fill, border);
+            this.size = size;
+            this.notRounded = !roundRadius;
+            this.roundRadius = roundRadius;
+        };
+        Rectangle2D.Create = function (parent, id, x, y, width, height, fill, border) {
+            BABYLON.Prim2DBase.CheckParent(parent);
+            var rect = new Rectangle2D();
+            rect.setupRectangle2D(parent.owner, parent, id, new BABYLON.Vector2(x, y), new BABYLON.Size(width, height), null);
+            rect.fill = fill || BABYLON.Canvas2D.GetSolidColorFillFromHex("#FFFFFFFF");
+            rect.border = border;
+            return rect;
+        };
+        Rectangle2D.CreateRounded = function (parent, id, x, y, width, height, roundRadius, fill, border) {
+            if (roundRadius === void 0) { roundRadius = 0; }
+            BABYLON.Prim2DBase.CheckParent(parent);
+            var rect = new Rectangle2D();
+            rect.setupRectangle2D(parent.owner, parent, id, new BABYLON.Vector2(x, y), new BABYLON.Size(width, height), roundRadius);
+            rect.fill = fill || BABYLON.Canvas2D.GetSolidColorFillFromHex("#FFFFFFFF");
+            rect.border = border;
+            return rect;
+        };
+        Rectangle2D.prototype.createModelRenderCache = function () {
+            var renderCache = new Rectangle2DRenderCache();
+            var engine = this.owner.engine;
+            // Need to create vb/ib for the fill part?
+            if (this.fill) {
+                var vbSize = ((this.notRounded ? 1 : Rectangle2D.roundSubdivisions) * 4) + 1;
+                var vb = new Float32Array(vbSize);
+                for (var i = 0; i < vbSize; i++) {
+                    vb[i] = i;
+                }
+                renderCache.fillVB = engine.createVertexBuffer(vb);
+                var triCount = vbSize - 1;
+                var ib = new Float32Array(triCount * 3);
+                for (var i = 0; i < triCount; i++) {
+                    ib[i * 3 + 0] = 0;
+                    ib[i * 3 + 1] = i + 1;
+                    ib[i * 3 + 2] = i + 2;
+                }
+                ib[triCount * 3 - 1] = 1;
+                renderCache.fillIB = engine.createIndexBuffer(ib);
+                renderCache.effect = engine.createEffect({ vertex: "rect2d", fragment: "rect2d" }, ["index", "zBias", "transformX", "transformY", "origin", "properties"], [], [], "");
+            }
+            return renderCache;
+        };
+        Rectangle2D.prototype.createInstanceData = function () {
+            return new Rectangle2DInstanceData();
+        };
+        Rectangle2D.prototype.refreshInstanceData = function () {
+            if (!_super.prototype.refreshInstanceData.call(this)) {
+                return false;
+            }
+            var d = this._instanceData;
+            var size = this.size;
+            d.properties = new BABYLON.Vector3(size.width, size.height, this.roundRadius || 0);
+            return true;
+        };
+        Rectangle2D.roundSubdivisions = 16;
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, function (pi) { return Rectangle2D.sizeProperty = pi; }, false, true)
+        ], Rectangle2D.prototype, "size", null);
+        __decorate([
+            BABYLON.modelLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, function (pi) { return Rectangle2D.notRoundedProperty = pi; })
+        ], Rectangle2D.prototype, "notRounded", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, function (pi) { return Rectangle2D.roundRadiusProperty = pi; })
+        ], Rectangle2D.prototype, "roundRadius", null);
+        Rectangle2D = __decorate([
+            BABYLON.className("Rectangle2D")
+        ], Rectangle2D);
+        return Rectangle2D;
+    }(BABYLON.RenderablePrim2D));
+    BABYLON.Rectangle2D = Rectangle2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.rectangle2d.js.map

+ 280 - 0
src/Canvas2d/babylon.rectangle2d.ts

@@ -0,0 +1,280 @@
+module BABYLON {
+    export class Rectangle2DRenderCache extends ModelRenderCache {
+        fillVB: WebGLBuffer;
+        fillIB: WebGLBuffer;
+        fillIndicesCount: number;
+        instancingFillAttributes: InstancingAttributeInfo[];
+        effectFill: Effect;
+
+        borderVB: WebGLBuffer;
+        borderIB: WebGLBuffer;
+        borderIndicesCount: number;
+        instancingBorderAttributes: InstancingAttributeInfo[];
+        effectBorder: Effect;
+
+        constructor(modelKey: string, isTransparent: boolean) {
+            super(modelKey, isTransparent);
+        }
+
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            // Do nothing if the shader is still loading/preparing
+            if ((this.effectFill && !this.effectFill.isReady()) || (this.effectBorder && !this.effectBorder.isReady())) {
+                return false;
+            }
+
+            var engine = instanceInfo._owner.owner.engine;
+
+            let depthFunction = 0;
+            if (this.effectFill && this.effectBorder) {
+                depthFunction = engine.getDepthFunction();
+                engine.setDepthFunctionToLessOrEqual();
+            }
+
+            var cur: number;
+            if (this.isTransparent) {
+                cur = engine.getAlphaMode();
+                engine.setAlphaMode(Engine.ALPHA_COMBINE);
+            }
+
+            if (this.effectFill) {
+                let partIndex = instanceInfo._partIndexFromId.get(Shape2D.SHAPE2D_FILLPARTID.toString());
+
+                engine.enableEffect(this.effectFill);
+                engine.bindBuffers(this.fillVB, this.fillIB, [1], 4, this.effectFill);
+                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
+                        this.instancingFillAttributes = this.loadInstancingAttributes(Shape2D.SHAPE2D_FILLPARTID, this.effectFill);
+                    }
+
+                    engine.updateAndBindInstancesBuffer(instanceInfo._instancesPartsBuffer[partIndex], null, this.instancingFillAttributes);
+                    engine.draw(true, 0, this.fillIndicesCount, count);
+                    engine.unBindInstancesBuffer(instanceInfo._instancesPartsBuffer[partIndex], this.instancingFillAttributes);
+                } else {
+                    for (let i = 0; i < count; i++) {
+                        this.setupUniforms(this.effectFill, partIndex, instanceInfo._instancesPartsData[partIndex], i);
+                        engine.draw(true, 0, this.fillIndicesCount);                        
+                    }
+                }
+            }
+
+            if (this.effectBorder) {
+                let partIndex = instanceInfo._partIndexFromId.get(Shape2D.SHAPE2D_BORDERPARTID.toString());
+
+                engine.enableEffect(this.effectBorder);
+                engine.bindBuffers(this.borderVB, this.borderIB, [1], 4, this.effectBorder);
+                let count = instanceInfo._instancesPartsData[partIndex].usedElementCount;
+                if (instanceInfo._owner.owner.supportInstancedArray) {
+                    if (!this.instancingBorderAttributes) {
+                        this.instancingBorderAttributes = this.loadInstancingAttributes(Shape2D.SHAPE2D_BORDERPARTID, this.effectBorder);
+                    }
+
+                    engine.updateAndBindInstancesBuffer(instanceInfo._instancesPartsBuffer[partIndex], null, this.instancingBorderAttributes);
+                    engine.draw(true, 0, this.borderIndicesCount, count);
+                    engine.unBindInstancesBuffer(instanceInfo._instancesPartsBuffer[partIndex], this.instancingBorderAttributes);
+                } else {
+                    for (let i = 0; i < count; i++) {
+                        this.setupUniforms(this.effectBorder, partIndex, instanceInfo._instancesPartsData[partIndex], i);
+                        engine.draw(true, 0, this.borderIndicesCount);
+                    }
+                }
+            }
+
+            if (this.isTransparent) {
+                engine.setAlphaMode(cur);
+            }
+
+            if (this.effectFill && this.effectBorder) {
+                engine.setDepthFunction(depthFunction);
+            }
+            return true;
+        }
+    }
+
+    export class Rectangle2DInstanceData extends Shape2DInstanceData {
+        constructor(partId: number) {
+            super(partId, 1);
+        }
+
+        @instanceData()
+        get properties(): Vector3 {
+            return null;
+        }
+    }
+
+    @className("Rectangle2D")
+    export class Rectangle2D extends Shape2D {
+
+        public static sizeProperty: Prim2DPropInfo;
+        public static notRoundedProperty: Prim2DPropInfo;
+        public static roundRadiusProperty: Prim2DPropInfo;
+
+        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 1, pi => Rectangle2D.sizeProperty = pi, false, true)
+        public get size(): Size {
+            return this._size;
+        }
+
+        public set size(value: Size) {
+            this._size = value;
+        }
+
+        @modelLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 2, pi => Rectangle2D.notRoundedProperty = pi)
+        public get notRounded(): boolean {
+            return this._notRounded;
+        }
+
+        public set notRounded(value: boolean) {
+            this._notRounded = value;
+        }
+
+        @instanceLevelProperty(Shape2D.SHAPE2D_PROPCOUNT + 3, pi => Rectangle2D.roundRadiusProperty = pi)
+        public get roundRadius(): number {
+            return this._roundRadius;
+        }
+
+        public set roundRadius(value: number) {
+            this._roundRadius = value;
+            this.notRounded = value === 0;
+        }
+
+        protected updateLevelBoundingInfo() {
+            BoundingInfo2D.CreateFromSizeToRef(this.size, this._levelBoundingInfo);
+        }
+
+        protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size: Size, roundRadius = 0, fill?: IBrush2D, border?: IBrush2D, borderThickness: number = 1) {
+            this.setupShape2D(owner, parent, id, position, true, fill, border, borderThickness);
+            this.size = size;
+            this.notRounded = !roundRadius;
+            this.roundRadius = roundRadius;
+        }
+
+        public static Create(parent: Prim2DBase, id: string, x: number, y: number, width: number, height: number, fill?: IBrush2D, border?: IBrush2D): Rectangle2D {
+            Prim2DBase.CheckParent(parent);
+
+            let rect = new Rectangle2D();
+            rect.setupRectangle2D(parent.owner, parent, id, new Vector2(x, y), new Size(width, height), null);
+            rect.fill = fill;
+            rect.border = border;
+            return rect;
+        }
+
+        public static CreateRounded(parent: Prim2DBase, id: string, x: number, y: number, width: number, height: number, roundRadius = 0, fill?: IBrush2D, border?: IBrush2D): Rectangle2D {
+            Prim2DBase.CheckParent(parent);
+
+            let rect = new Rectangle2D();
+            rect.setupRectangle2D(parent.owner, parent, id, new Vector2(x, y), new Size(width, height), roundRadius);
+            rect.fill = fill || Canvas2D.GetSolidColorBrushFromHex("#FFFFFFFF");
+            rect.border = border;
+            return rect;
+        }
+
+        public static roundSubdivisions = 16;
+
+        protected createModelRenderCache(modelKey: string, isTransparent: boolean): ModelRenderCache {
+            let renderCache = new Rectangle2DRenderCache(modelKey, isTransparent);
+            return renderCache;
+        }
+
+        protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
+            let renderCache = <Rectangle2DRenderCache>modelRenderCache;
+            let engine = this.owner.engine;
+
+            // 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);
+                for (let i = 0; i < vbSize; i++) {
+                    vb[i] = i;
+                }
+                renderCache.fillVB = engine.createVertexBuffer(vb);
+
+                let triCount = vbSize - 1;
+                let ib = new Float32Array(triCount * 3);
+                for (let i = 0; i < triCount; i++) {
+                    ib[i * 3 + 0] = 0;
+                    ib[i * 3 + 2] = i + 1;
+                    ib[i * 3 + 1] = i + 2;
+                }
+                ib[triCount * 3 - 2] = 1;
+
+                renderCache.fillIB = engine.createIndexBuffer(ib);
+                renderCache.fillIndicesCount = triCount * 3;
+
+                let ei = this.getDataPartEffectInfo(Shape2D.SHAPE2D_FILLPARTID, ["index"]);
+                renderCache.effectFill = engine.createEffect({ vertex: "rect2d", fragment: "rect2d" }, ei.attributes, ei.uniforms, [], ei.defines, null, e => {
+//                    renderCache.setupUniformsLocation(e, ei.uniforms, Shape2D.SHAPE2D_FILLPARTID);
+                });
+            }
+
+            // 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);
+                for (let i = 0; i < vbSize; i++) {
+                    vb[i] = i;
+                }
+                renderCache.borderVB = engine.createVertexBuffer(vb);
+
+                let triCount = vbSize;
+                let rs = triCount / 2;
+                let ib = new Float32Array(triCount * 3);
+                for (let i = 0; i < rs; i++) {
+                    let r0 = i;
+                    let r1 = (i + 1) % rs;
+
+                    ib[i * 6 + 0] = rs + r1;
+                    ib[i * 6 + 1] = rs + r0;
+                    ib[i * 6 + 2] = r0;
+
+                    ib[i * 6 + 3] = r1;
+                    ib[i * 6 + 4] = rs + r1;
+                    ib[i * 6 + 5] = r0;
+                }
+
+                renderCache.borderIB = engine.createIndexBuffer(ib);
+                renderCache.borderIndicesCount = triCount * 3;
+
+                let ei = this.getDataPartEffectInfo(Shape2D.SHAPE2D_BORDERPARTID, ["index"]);
+                renderCache.effectBorder = engine.createEffect({ vertex: "rect2d", fragment: "rect2d" }, ei.attributes, ei.uniforms, [], ei.defines, null, e => {
+//                    renderCache.setupUniformsLocation(e, ei.uniforms, Shape2D.SHAPE2D_BORDERPARTID);
+                });
+            }
+
+            return renderCache;
+        }
+
+
+        protected createInstanceDataParts(): InstanceDataBase[] {
+            var res = new Array<InstanceDataBase>();
+            if (this.border) {
+                res.push(new Rectangle2DInstanceData(Shape2D.SHAPE2D_BORDERPARTID));
+            }
+            if (this.fill) {
+                res.push(new Rectangle2DInstanceData(Shape2D.SHAPE2D_FILLPARTID));
+            }
+            return res;
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
+                return false;
+            }
+            if (part.id === Shape2D.SHAPE2D_BORDERPARTID) {
+                let d = <Rectangle2DInstanceData>part;
+                let size = this.size;
+                d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
+            }
+            else if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
+                let d = <Rectangle2DInstanceData>part;
+                let size = this.size;
+                d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
+            }
+            return true;
+        }
+
+        private _size: Size;
+        private _notRounded: boolean;
+        private _roundRadius: number;
+    }
+}

+ 372 - 0
src/Canvas2d/babylon.renderablePrim2d.js

@@ -0,0 +1,372 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var InstanceClassInfo = (function () {
+        function InstanceClassInfo(base) {
+            this._baseInfo = base;
+            this._nextOffset = 0;
+            this._attributes = new Array();
+        }
+        InstanceClassInfo.prototype.mapProperty = function (propInfo) {
+            propInfo.instanceOffset = (this._baseInfo ? this._baseInfo._nextOffset : 0) + this._nextOffset;
+            this._nextOffset += (propInfo.size / 4);
+            this._attributes.push(propInfo);
+        };
+        InstanceClassInfo.prototype.getInstancingAttributeInfos = function (effect) {
+            var res = new Array();
+            var curInfo = this;
+            while (curInfo) {
+                for (var _i = 0, _a = curInfo._attributes; _i < _a.length; _i++) {
+                    var attrib = _a[_i];
+                    var index = effect.getAttributeLocationByName(attrib.attributeName);
+                    var iai = new BABYLON.InstancingAttributeInfo();
+                    iai.index = index;
+                    iai.attributeSize = attrib.size / 4; // attrib.size is in byte and we need to store in "component" (i.e float is 1, vec3 is 3)
+                    iai.offset = attrib.instanceOffset * 4; // attrub.instanceOffset is in float, iai.offset must be in bytes
+                    res.push(iai);
+                }
+                curInfo = curInfo._baseInfo;
+            }
+            return res;
+        };
+        return InstanceClassInfo;
+    }());
+    BABYLON.InstanceClassInfo = InstanceClassInfo;
+    var InstancePropInfo = (function () {
+        function InstancePropInfo() {
+        }
+        InstancePropInfo.prototype.setSize = function (val) {
+            if (val instanceof BABYLON.Vector2) {
+                this.size = 8;
+                this.dataType = 0 /* Vector2 */;
+                return;
+            }
+            if (val instanceof BABYLON.Vector3) {
+                this.size = 12;
+                this.dataType = 1 /* Vector3 */;
+                return;
+            }
+            if (val instanceof BABYLON.Vector4) {
+                this.size = 16;
+                this.dataType = 2 /* Vector4 */;
+                return;
+            }
+            if (val instanceof BABYLON.Matrix) {
+                throw new Error("Matrix type is not supported by WebGL Instance Buffer, you have to use four Vector4 properties instead");
+            }
+            if (typeof (val) === "number") {
+                this.size = 4;
+                this.dataType = 4 /* float */;
+                return;
+            }
+            if (val instanceof BABYLON.Color3) {
+                this.size = 12;
+                this.dataType = 5 /* Color3 */;
+                return;
+            }
+            if (val instanceof BABYLON.Color4) {
+                this.size = 16;
+                this.dataType = 6 /* Color4 */;
+                return;
+            }
+        };
+        InstancePropInfo.prototype.writeData = function (array, offset, val) {
+            switch (this.dataType) {
+                case 0 /* Vector2 */:
+                    {
+                        var v = val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        break;
+                    }
+                case 1 /* Vector3 */:
+                    {
+                        var v = val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        array[offset + 2] = v.z;
+                        break;
+                    }
+                case 2 /* Vector4 */:
+                    {
+                        var v = val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        array[offset + 2] = v.z;
+                        array[offset + 3] = v.w;
+                        break;
+                    }
+                case 5 /* Color3 */:
+                    {
+                        var v = val;
+                        array[offset + 0] = v.r;
+                        array[offset + 1] = v.g;
+                        array[offset + 2] = v.b;
+                        break;
+                    }
+                case 6 /* Color4 */:
+                    {
+                        var v = val;
+                        array[offset + 0] = v.r;
+                        array[offset + 1] = v.g;
+                        array[offset + 2] = v.b;
+                        array[offset + 3] = v.a;
+                        break;
+                    }
+                case 4 /* float */:
+                    {
+                        var v = val;
+                        array[offset] = v;
+                        break;
+                    }
+                case 3 /* Matrix */:
+                    {
+                        var v = val;
+                        for (var i = 0; i < 16; i++) {
+                            array[offset + i] = v.m[i];
+                        }
+                        break;
+                    }
+            }
+        };
+        return InstancePropInfo;
+    }());
+    BABYLON.InstancePropInfo = InstancePropInfo;
+    function instanceData(name) {
+        return function (target, propName, descriptor) {
+            var dic = BABYLON.ClassTreeInfo.getOrRegister(target, function (base) { return new InstanceClassInfo(base); });
+            var node = dic.getLevelOf(target);
+            name = name || propName;
+            var info = node.levelContent.get(name);
+            if (info) {
+                throw new Error("The ID " + name + " is already taken by another instance data");
+            }
+            info = new InstancePropInfo();
+            info.attributeName = name;
+            node.levelContent.add(name, info);
+            descriptor.get = function () {
+                return null;
+            };
+            descriptor.set = function (val) {
+                if (!info.size) {
+                    info.setSize(val);
+                    node.classContent.mapProperty(info);
+                }
+                var obj = this;
+                if (obj._dataBuffer) {
+                    info.writeData(obj._dataBuffer.buffer, obj._dataElement.offset + info.instanceOffset, val);
+                }
+            };
+        };
+    }
+    BABYLON.instanceData = instanceData;
+    var InstanceDataBase = (function () {
+        function InstanceDataBase() {
+        }
+        Object.defineProperty(InstanceDataBase.prototype, "zBias", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(InstanceDataBase.prototype, "transformX", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(InstanceDataBase.prototype, "transformY", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(InstanceDataBase.prototype, "origin", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        InstanceDataBase.prototype.getClassTreeInfo = function () {
+            if (!this._typeInfo) {
+                this._typeInfo = BABYLON.ClassTreeInfo.get(Object.getPrototypeOf(this));
+            }
+            return this._typeInfo;
+        };
+        __decorate([
+            instanceData()
+        ], InstanceDataBase.prototype, "zBias", null);
+        __decorate([
+            instanceData()
+        ], InstanceDataBase.prototype, "transformX", null);
+        __decorate([
+            instanceData()
+        ], InstanceDataBase.prototype, "transformY", null);
+        __decorate([
+            instanceData()
+        ], InstanceDataBase.prototype, "origin", null);
+        return InstanceDataBase;
+    }());
+    BABYLON.InstanceDataBase = InstanceDataBase;
+    var RenderablePrim2D = (function (_super) {
+        __extends(RenderablePrim2D, _super);
+        function RenderablePrim2D() {
+            _super.apply(this, arguments);
+        }
+        RenderablePrim2D.prototype.setupRenderablePrim2D = function (owner, parent, id, position, isVisible, fill, border) {
+            this.setupPrim2DBase(owner, parent, id, position);
+            this._isTransparent = false;
+        };
+        Object.defineProperty(RenderablePrim2D.prototype, "border", {
+            get: function () {
+                return this._border;
+            },
+            set: function (value) {
+                if (value === this._border) {
+                    return;
+                }
+                this._border = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(RenderablePrim2D.prototype, "fill", {
+            get: function () {
+                return this._fill;
+            },
+            set: function (value) {
+                if (value === this._fill) {
+                    return;
+                }
+                this._fill = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        RenderablePrim2D.prototype._prepareRenderPre = function (context) {
+            var _this = this;
+            _super.prototype._prepareRenderPre.call(this, context);
+            // If the model changed and we have already an instance, we must remove this instance from the obsolete model
+            if (this._modelDirty && this._modelRenderInstanceID) {
+                this._modelRenderCache.removeInstanceData(this._modelRenderInstanceID);
+                this._modelRenderInstanceID = null;
+            }
+            // Need to create the model?
+            if (!this._modelRenderCache || this._modelDirty) {
+                this._modelRenderCache = BABYLON.SmartPropertyPrim.GetOrAddModelCache(this.modelKey, function (key) { return _this.createModelRenderCache(); });
+                this._modelDirty = false;
+            }
+            // Need to create the instance?
+            var gii;
+            var newInstance = false;
+            if (!this._modelRenderInstanceID) {
+                newInstance = true;
+                var id = this.createInstanceData();
+                this._instanceData = id;
+                var cti_1 = id.getClassTreeInfo();
+                if (!cti_1.classContent.instanceDataStride) {
+                    // Make sure the instance is visible other the properties won't be set and their size/offset wont be computed
+                    var curVisible = this.isVisible;
+                    this.isVisible = true;
+                    // We manually trigger refreshInstanceData for the only sake of evaluating each isntance property size and offset in the instance data, this can only be made at runtime. Once it's done we have all the information to create the instance data buffer.
+                    this.refreshInstanceData();
+                    this.isVisible = curVisible;
+                    var size = 0;
+                    cti_1.fullContent.forEach(function (k, v) {
+                        if (!v.size) {
+                            console.log("ERROR: Couldn't detect the size of the Property " + v.attributeName + " from type " + BABYLON.Tools.getClassName(cti_1.type) + ". Property is ignored.");
+                        }
+                        else {
+                            size += v.size;
+                        }
+                    });
+                    cti_1.classContent.instanceDataStride = size;
+                }
+                gii = this.renderGroup.groupRenderInfo.getOrAddWithFactory(this.modelKey, function (k) { return new BABYLON.GroupInstanceInfo(_this.renderGroup, cti_1, _this._modelRenderCache); });
+                if (!gii._instancesData) {
+                    // instanceDataStride's unit is byte but DynamicFloatArray is float32, so div by four to get the correct number
+                    gii._instancesData = new BABYLON.DynamicFloatArray(cti_1.classContent.instanceDataStride / 4, 50);
+                }
+                id._dataBuffer = gii._instancesData;
+                id._dataElement = id._dataBuffer.allocElement();
+                this._modelRenderInstanceID = this._modelRenderCache.addInstanceData(this._instanceData);
+            }
+            if (context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformPreviousStep !== this._globalTransformStep)) {
+                // Will return false if the instance should not be rendered (not visible or other any reasons)
+                if (!this.refreshInstanceData()) {
+                    // Free the data element
+                    if (this._instanceData._dataElement) {
+                        this._instanceData._dataBuffer.freeElement(this._instanceData._dataElement);
+                        this._instanceData._dataElement = null;
+                    }
+                }
+                this._instanceDirtyFlags = 0;
+                if (!gii) {
+                    gii = this.renderGroup.groupRenderInfo.get(this.modelKey);
+                }
+                gii._dirtyInstancesData = true;
+            }
+        };
+        RenderablePrim2D.prototype.createModelRenderCache = function () {
+            return null;
+        };
+        RenderablePrim2D.prototype.createInstanceData = function () {
+            return null;
+        };
+        RenderablePrim2D.prototype.refreshInstanceData = function () {
+            var d = this._instanceData;
+            if (!this.isVisible) {
+                return false;
+            }
+            d.isVisible = this.isVisible;
+            var t = this.renderGroup.invGlobalTransform.multiply(this._globalTransform);
+            var size = this.renderGroup.viewportSize;
+            var zBias = this.getActualZOffset();
+            // Have to convert the coordinates to clip space which is ranged between [-1;1] on X and Y axis, with 0,0 being the left/bottom corner
+            // Current coordinates are expressed in renderGroup coordinates ([0, renderGroup.actualSize.width|height]) with 0,0 being at the left/top corner
+            // RenderGroup Width and Height are multiplied by zBias because the VertexShader will multiply X and Y by W, which is 1/zBias. Has we divide our coordinate by these Width/Height, we will also divide by the zBias to compensate the operation made by the VertexShader.
+            // So for X: 
+            //  - tx.x = value * 2 / width: is to switch from [0, renderGroup.width] to [0, 2]
+            //  - tx.w = (value * 2 / width) - 1: w stores the translation in renderGroup coordinates so (value * 2 / width) to switch to a clip space translation value. - 1 is to offset the overall [0;2] to [-1;1]. Don't forget it's -(1/zBias) and not -1 because everything need to be scaled by 1/zBias.
+            // Same thing for Y, except the "* -2" instead of "* 2" to switch the origin from top to bottom (has expected by the clip space)
+            var w = size.width * zBias;
+            var h = size.height * zBias;
+            var invZBias = 1 / zBias;
+            var tx = new BABYLON.Vector4(t.m[0] * 2 / w, t.m[4] * 2 / w, t.m[8], (t.m[12] * 2 / w) - (invZBias));
+            var ty = new BABYLON.Vector4(t.m[1] * -2 / h, t.m[5] * -2 / h, t.m[9], ((t.m[13] * 2 / h) - (invZBias)) * -1);
+            d.transformX = tx;
+            d.transformY = ty;
+            d.origin = this.origin;
+            // Stores zBias and it's inverse value because that's needed to compute the clip space W coordinate (which is 1/Z, so 1/zBias)
+            d.zBias = new BABYLON.Vector2(zBias, invZBias);
+            return true;
+        };
+        RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT = BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 10;
+        __decorate([
+            BABYLON.modelLevelProperty(BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 1, function (pi) { return RenderablePrim2D.borderProperty = pi; }, true)
+        ], RenderablePrim2D.prototype, "border", null);
+        __decorate([
+            BABYLON.modelLevelProperty(BABYLON.Prim2DBase.PRIM2DBASE_PROPCOUNT + 2, function (pi) { return RenderablePrim2D.fillProperty = pi; }, true)
+        ], RenderablePrim2D.prototype, "fill", null);
+        RenderablePrim2D = __decorate([
+            BABYLON.className("RenderablePrim2D")
+        ], RenderablePrim2D);
+        return RenderablePrim2D;
+    }(BABYLON.Prim2DBase));
+    BABYLON.RenderablePrim2D = RenderablePrim2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.renderablePrim2d.js.map

+ 523 - 0
src/Canvas2d/babylon.renderablePrim2d.ts

@@ -0,0 +1,523 @@
+module BABYLON {
+    export class InstanceClassInfo {
+        constructor(base: InstanceClassInfo) {
+            this._baseInfo = base;
+            this._nextOffset = new StringDictionary<number>();
+            this._attributes = new Array<InstancePropInfo>();
+        }
+
+        mapProperty(propInfo: InstancePropInfo, push: boolean) {
+            let curOff = this._nextOffset.getOrAdd(InstanceClassInfo._CurCategories, 0);
+            propInfo.instanceOffset.add(InstanceClassInfo._CurCategories, this._getBaseOffset(InstanceClassInfo._CurCategories) + curOff);
+            //console.log(`[${InstanceClassInfo._CurCategories}] New PropInfo. Category: ${propInfo.category}, Name: ${propInfo.attributeName}, Offset: ${propInfo.instanceOffset.get(InstanceClassInfo._CurCategories)}, Size: ${propInfo.size / 4}`);
+
+            this._nextOffset.set(InstanceClassInfo._CurCategories, curOff + (propInfo.size / 4));
+
+            if (push) {
+                this._attributes.push(propInfo);
+            }
+        }
+
+        getInstancingAttributeInfos(effect: Effect, categories: string[]): InstancingAttributeInfo[] {
+            let catInline = categories.join(";");
+            let res = new Array<InstancingAttributeInfo>();
+            let curInfo: InstanceClassInfo = this;
+            while (curInfo) {
+                for (let attrib of curInfo._attributes) {
+                    // Only map if there's no category assigned to the instance data or if there's a category and it's in the given list
+                    if (!attrib.category || categories.indexOf(attrib.category) !== -1) {
+                        let index = effect.getAttributeLocationByName(attrib.attributeName);
+                        let iai = new InstancingAttributeInfo();
+                        iai.index = index;
+                        iai.attributeSize = attrib.size / 4; // attrib.size is in byte and we need to store in "component" (i.e float is 1, vec3 is 3)
+                        iai.offset = attrib.instanceOffset.get(catInline) * 4; // attrib.instanceOffset is in float, iai.offset must be in bytes
+                        iai.attributeName = attrib.attributeName;
+                        res.push(iai);
+                    }
+                }
+
+                curInfo = curInfo._baseInfo;
+            }
+            return res;
+        }
+
+        getShaderAttributes(categories: string[]): string[] {
+            let res = new Array<string>();
+            let curInfo: InstanceClassInfo = this;
+            while (curInfo) {
+                for (let attrib of curInfo._attributes) {
+                    // Only map if there's no category assigned to the instance data or if there's a category and it's in the given list
+                    if (!attrib.category || categories.indexOf(attrib.category) !== -1) {
+                        res.push(attrib.attributeName);
+                    }
+                }
+
+                curInfo = curInfo._baseInfo;
+            }
+            return res;
+        }
+
+        private _getBaseOffset(categories: string): number {
+            let curOffset = 0;
+            let curBase = this._baseInfo;
+            while (curBase) {
+                curOffset += curBase._nextOffset.getOrAdd(categories, 0);
+                curBase = curBase._baseInfo;
+            }
+            return curOffset;
+        }
+
+        static _CurCategories: string;
+        private _baseInfo: InstanceClassInfo;
+        private _nextOffset: StringDictionary<number>;
+        private _attributes: Array<InstancePropInfo>;
+    }
+
+    export class InstancePropInfo {
+        attributeName: string;
+        category: string;
+        size: number;
+        shaderOffset: number;
+        instanceOffset: StringDictionary<number>;
+        dataType: ShaderDataType;
+        //uniformLocation: WebGLUniformLocation;
+
+        constructor() {
+            this.instanceOffset = new StringDictionary<number>();
+        }
+
+        setSize(val) {
+            if (val instanceof Vector2) {
+                this.size = 8;
+                this.dataType = ShaderDataType.Vector2;
+                return;
+            }
+            if (val instanceof Vector3) {
+                this.size = 12;
+                this.dataType = ShaderDataType.Vector3;
+                return;
+            }
+            if (val instanceof Vector4) {
+                this.size = 16;
+                this.dataType = ShaderDataType.Vector4;
+                return;
+            }
+            if (val instanceof Matrix) {
+                throw new Error("Matrix type is not supported by WebGL Instance Buffer, you have to use four Vector4 properties instead");
+            }
+            if (typeof (val) === "number") {
+                this.size = 4;
+                this.dataType = ShaderDataType.float;
+                return;
+            }
+            if (val instanceof Color3) {
+                this.size = 12;
+                this.dataType = ShaderDataType.Color3;
+                return;
+            }
+            if (val instanceof Color4) {
+                this.size = 16;
+                this.dataType = ShaderDataType.Color4;
+                return;
+            }
+            return;
+        }
+
+        writeData(array: Float32Array, offset: number, val) {
+            switch (this.dataType) {
+                case ShaderDataType.Vector2:
+                    {
+                        let v = <Vector2>val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        break;
+                    }
+                case ShaderDataType.Vector3:
+                    {
+                        let v = <Vector3>val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        array[offset + 2] = v.z;
+                        break;
+                    }
+                case ShaderDataType.Vector4:
+                    {
+                        let v = <Vector4>val;
+                        array[offset + 0] = v.x;
+                        array[offset + 1] = v.y;
+                        array[offset + 2] = v.z;
+                        array[offset + 3] = v.w;
+                        break;
+                    }
+                case ShaderDataType.Color3:
+                    {
+                        let v = <Color3>val;
+                        array[offset + 0] = v.r;
+                        array[offset + 1] = v.g;
+                        array[offset + 2] = v.b;
+                        break;
+                    }
+                case ShaderDataType.Color4:
+                    {
+                        let v = <Color4>val;
+                        array[offset + 0] = v.r;
+                        array[offset + 1] = v.g;
+                        array[offset + 2] = v.b;
+                        array[offset + 3] = v.a;
+                        break;
+                    }
+                case ShaderDataType.float:
+                    {
+                        let v = <number>val;
+                        array[offset] = v;
+                        break;
+                    }
+                case ShaderDataType.Matrix:
+                    {
+                        let v = <Matrix>val;
+                        for (let i = 0; i < 16; i++) {
+                            array[offset + i] = v.m[i];
+                        }
+                        break;
+                    }
+            }
+        }
+    }
+
+    export function instanceData<T>(category?: string, shaderAttributeName?: string): (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => void {
+        return (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => {
+
+            let dic = ClassTreeInfo.getOrRegister<InstanceClassInfo, InstancePropInfo>(target, (base) => new InstanceClassInfo(base));
+            let node = dic.getLevelOf(target);
+            let instanceDataName = <string>propName;
+            shaderAttributeName = shaderAttributeName || instanceDataName;
+
+
+            let info = node.levelContent.get(instanceDataName);
+            if (info) {
+                throw new Error(`The ID ${instanceDataName} is already taken by another instance data`);
+            }
+
+            info = new InstancePropInfo();
+            info.attributeName = shaderAttributeName;
+            info.category = category || null;
+
+            node.levelContent.add(instanceDataName, info);
+
+            descriptor.get = function () {
+                return null;
+            }
+
+            descriptor.set = function (val) {
+                if (!info.size) {
+                    info.setSize(val);
+                    node.classContent.mapProperty(info, true);
+                } else if (!info.instanceOffset.contains(InstanceClassInfo._CurCategories)) {
+                    node.classContent.mapProperty(info, false);
+                }
+
+                let obj: InstanceDataBase = this;
+                if (obj.dataBuffer && obj.dataElements) {
+                    let offset = obj.dataElements[obj.curElement].offset + info.instanceOffset.get(InstanceClassInfo._CurCategories);
+                    info.writeData(obj.dataBuffer.buffer, offset, val);
+                }
+            }
+
+        }
+    }
+
+    export class InstanceDataBase {
+        constructor(partId: number, dataElementCount: number) {
+            this.id = partId;
+            this.curElement = 0;
+            this.dataElementCount = dataElementCount;
+        }
+
+        id: number;
+        isVisible: boolean;
+
+        @instanceData()
+        get zBias(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get transformX(): Vector4 {
+            return null;
+        }
+
+        @instanceData()
+        get transformY(): Vector4 {
+            return null;
+        }
+
+        @instanceData()
+        get origin(): Vector2 {
+            return null;
+        }
+
+        getClassTreeInfo(): ClassTreeInfo<InstanceClassInfo, InstancePropInfo> {
+            if (!this.typeInfo) {
+                this.typeInfo = ClassTreeInfo.get<InstanceClassInfo, InstancePropInfo>(Object.getPrototypeOf(this));
+            }
+            return this.typeInfo;
+        }
+
+        allocElements() {
+            let res = new Array<DynamicFloatArrayElementInfo>(this.dataElementCount);
+            for (let i = 0; i < this.dataElementCount; i++) {
+                res[i] = this.dataBuffer.allocElement();
+            }
+            this.dataElements = res;
+        }
+
+        freeElements() {
+            for (let ei of this.dataElements) {
+                this.dataBuffer.freeElement(ei);
+            }
+            this.dataElements = null;
+        }
+
+        curElement: number;
+        dataElementCount: number;
+        dataElements: DynamicFloatArrayElementInfo[];
+        dataBuffer: DynamicFloatArray;
+        typeInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>;
+
+    }
+
+    @className("RenderablePrim2D")
+    export class RenderablePrim2D extends Prim2DBase {
+        static RENDERABLEPRIM2D_PROPCOUNT: number = Prim2DBase.PRIM2DBASE_PROPCOUNT + 5;
+
+        public static isTransparentProperty: Prim2DPropInfo;
+
+        @modelLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 1, pi => RenderablePrim2D.isTransparentProperty = pi)
+        public get isTransparent(): boolean {
+            return this._isTransparent;
+        }
+
+        public set isTransparent(value: boolean) {
+            this._isTransparent = value;
+        }
+
+        setupRenderablePrim2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, isVisible: boolean) {
+            this.setupPrim2DBase(owner, parent, id, position);
+            this._isTransparent = false;
+        }
+
+        public _prepareRenderPre(context: Render2DContext) {
+            super._prepareRenderPre(context);
+
+            // If the model changed and we have already an instance, we must remove this instance from the obsolete model
+            if (this._modelDirty && this._modelRenderInstanceID) {
+                this._modelRenderCache.removeInstanceData(this._modelRenderInstanceID);
+                this._modelRenderInstanceID = null;
+            }
+
+            // Need to create the model?
+            let setupModelRenderCache = false;
+            if (!this._modelRenderCache || this._modelDirty) {
+                this._modelRenderCache = SmartPropertyPrim.GetOrAddModelCache(this.modelKey, (key: string) => {
+                    let mrc = this.createModelRenderCache(key, this.isTransparent);
+                    setupModelRenderCache = true;
+                    return mrc;
+                });
+                this._modelDirty = false;
+            }
+
+            // Need to create the instance?
+            let gii: GroupInstanceInfo;
+            let newInstance = false;
+            if (!this._modelRenderInstanceID) {
+                newInstance = true;
+                let parts = this.createInstanceDataParts();
+                this._instanceDataParts = parts;
+
+                if (!this._modelRenderCache._partsDataStride) {
+                    let ctiArray = new Array<ClassTreeInfo<InstanceClassInfo, InstancePropInfo>>();
+                    let dataStrides = new Array<number>();
+                    let usedCatList = new Array<string[]>();
+                    let partIdList = new Array<number>();
+                    let joinedUsedCatList = new Array<string>();
+
+                    for (let dataPart of parts) {
+                        let cat = this.getUsedShaderCategories(dataPart);
+                        let cti = dataPart.getClassTreeInfo();
+                        // Make sure the instance is visible other the properties won't be set and their size/offset wont be computed
+                        let curVisible = this.isVisible;
+                        this.isVisible = true;
+                        // We manually trigger refreshInstanceData for the only sake of evaluating each isntance property size and offset in the instance data, this can only be made at runtime. Once it's done we have all the information to create the instance data buffer.
+                        //console.log("Build Prop Layout for " + Tools.getClassName(this._instanceDataParts[0]));
+                        let joinCat = cat.join(";");
+                        joinedUsedCatList.push(joinCat);
+                        InstanceClassInfo._CurCategories = joinCat;
+                        this.refreshInstanceDataPart(dataPart);
+                        this.isVisible = curVisible;
+
+                        let size = 0;
+                        cti.fullContent.forEach((k, v) => {
+                            if (!v.category || cat.indexOf(v.category) !== -1) {
+                                if (!v.size) {
+                                    console.log(`ERROR: Couldn't detect the size of the Property ${v.attributeName} from type ${Tools.getClassName(cti.type)}. Property is ignored.`);
+                                } else {
+                                    size += v.size;
+                                }
+                            }
+                        });
+                        dataStrides.push(size);
+                        usedCatList.push(cat);
+                        ctiArray.push(cti);
+                        partIdList.push(dataPart.id);
+                    }
+                    this._modelRenderCache._partsDataStride = dataStrides;
+                    this._modelRenderCache._partsUsedCategories = usedCatList;
+                    this._modelRenderCache._partsJoinedUsedCategories = joinedUsedCatList;
+                    this._modelRenderCache._partsClassInfo = ctiArray;
+                    this._modelRenderCache._partIdList = partIdList;
+                }
+
+                gii = this.renderGroup.groupRenderInfo.getOrAddWithFactory(this.modelKey, k => new GroupInstanceInfo(this.renderGroup, this._modelRenderCache));
+
+                // First time init of the GroupInstanceInfo
+                if (gii._instancesPartsData.length === 0) {
+                    for (let j = 0; j < this._modelRenderCache._partsDataStride.length; j++) {
+                        let stride = this._modelRenderCache._partsDataStride[j];
+                        gii._instancesPartsData.push(new DynamicFloatArray(stride / 4, 50));
+                        gii._partIndexFromId.add(this._modelRenderCache._partIdList[j].toString(), j);
+
+                        for (let part of this._instanceDataParts) {
+                            gii._instancesPartsUsedShaderCategories[gii._partIndexFromId.get(part.id.toString())] = this.getUsedShaderCategories(part).join(";");
+                        }
+                    }
+                }
+
+                for (let i = 0; i < parts.length; i++) {
+                    let part = parts[i];
+                    part.dataBuffer = gii._instancesPartsData[i];
+                    part.allocElements();
+                }
+
+                this._modelRenderInstanceID = this._modelRenderCache.addInstanceDataParts(this._instanceDataParts);
+            }
+
+            if (setupModelRenderCache) {
+                this.setupModelRenderCache(this._modelRenderCache);
+            }
+
+            if (context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep)) {
+                if (!gii) {
+                    gii = this.renderGroup.groupRenderInfo.get(this.modelKey);
+                }
+
+                for (let part of this._instanceDataParts) {
+                    let cat = this.getUsedShaderCategories(part);
+                    InstanceClassInfo._CurCategories = gii._instancesPartsUsedShaderCategories[gii._partIndexFromId.get(part.id.toString())];
+
+                    // Will return false if the instance should not be rendered (not visible or other any reasons)
+                    if (!this.refreshInstanceDataPart(part)) {
+                        // Free the data element
+                        if (part.dataElements) {
+                            part.freeElements();
+                        }
+                    }
+                }
+                this._instanceDirtyFlags = 0;
+
+                gii._dirtyInstancesData = true;
+            }
+        }
+
+        protected getDataPartEffectInfo(dataPartId: number, vertexBufferAttributes: string[]): { attributes: string[], uniforms: string[], defines: string} {
+            let dataPart = Tools.first(this._instanceDataParts, i => i.id === dataPartId);
+            if (!dataPart) {
+                return null;
+            }
+
+            let instancedArray = this.owner.supportInstancedArray;
+
+            let cti = dataPart.getClassTreeInfo();
+            let categories = this.getUsedShaderCategories(dataPart);
+            let att = cti.classContent.getShaderAttributes(categories);
+            let defines = "";
+            categories.forEach(c => { defines += `#define ${c}\n`});
+            if (instancedArray) {
+                defines += "#define Instanced\n";
+            }
+
+            return { attributes: instancedArray ? vertexBufferAttributes.concat(att) : vertexBufferAttributes, uniforms: instancedArray ? [] : att, defines: defines };
+        }
+
+        protected get modelRenderCache(): ModelRenderCache {
+            return this._modelRenderCache;
+        }
+
+        protected createModelRenderCache(modelKey: string, isTransparent: boolean): ModelRenderCache {
+            return null;
+        }
+
+        protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
+        }
+
+        protected createInstanceDataParts(): InstanceDataBase[] {
+            return null;
+        }
+
+        protected getUsedShaderCategories(dataPart: InstanceDataBase): string[] {
+            return [];
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!this.isVisible) {
+                return false;
+            }
+            part.isVisible = this.isVisible;
+
+            // Which means, if there's only one data element, we're update it from this method, otherwise it is the responsability of the derived class to call updateInstanceDataPart as many times as needed, properly (look at Text2D's implementation for more information)
+            if (part.dataElementCount === 1) {
+                this.updateInstanceDataPart(part);
+            }
+            return true;
+        }
+
+        protected updateInstanceDataPart(part: InstanceDataBase, positionOffset: Vector2 = null) {
+            let t = this._globalTransform.multiply(this.renderGroup.invGlobalTransform);
+            let size = (<Size>this.renderGroup.viewportSize);
+            let zBias = this.getActualZOffset();
+
+            let offX = 0;
+            let offY = 0;
+            // If there's an offset, apply the global transformation matrix on it to get a global offset
+            if (positionOffset) {
+                offX = positionOffset.x * t.m[0] + positionOffset.y * t.m[4];
+                offY = positionOffset.x * t.m[1] + positionOffset.y * t.m[5];
+            }
+
+            // Have to convert the coordinates to clip space which is ranged between [-1;1] on X and Y axis, with 0,0 being the left/bottom corner
+            // Current coordinates are expressed in renderGroup coordinates ([0, renderGroup.actualSize.width|height]) with 0,0 being at the left/top corner
+            // RenderGroup Width and Height are multiplied by zBias because the VertexShader will multiply X and Y by W, which is 1/zBias. As we divide our coordinate by these Width/Height, we will also divide by the zBias to compensate the operation made by the VertexShader.
+            // So for X: 
+            //  - tx.x = value * 2 / width: is to switch from [0, renderGroup.width] to [0, 2]
+            //  - tx.w = (value * 2 / width) - 1: w stores the translation in renderGroup coordinates so (value * 2 / width) to switch to a clip space translation value. - 1 is to offset the overall [0;2] to [-1;1]. Don't forget it's -(1/zBias) and not -1 because everything need to be scaled by 1/zBias.
+            let w = size.width * zBias;
+            let h = size.height * zBias;
+            let invZBias = 1 / zBias;
+            let tx = new Vector4(t.m[0] * 2 / w, t.m[4] * 2 / w, 0/*t.m[8]*/, ((t.m[12]+offX) * 2 / w) - (invZBias));
+            let ty = new Vector4(t.m[1] * 2 / h, t.m[5] * 2 / h, 0/*t.m[9]*/, ((t.m[13]+offY) * 2 / h) - (invZBias));
+            part.transformX = tx;
+            part.transformY = ty;
+            part.origin = this.origin;
+
+            // Stores zBias and it's inverse value because that's needed to compute the clip space W coordinate (which is 1/Z, so 1/zBias)
+            part.zBias = new Vector2(zBias, invZBias);
+        }
+
+        private _modelRenderCache: ModelRenderCache;
+        private _modelRenderInstanceID: string;
+
+        protected _instanceDataParts: InstanceDataBase[];
+        protected _isTransparent: boolean;
+    }
+
+
+}

+ 193 - 0
src/Canvas2d/babylon.shape2d.ts

@@ -0,0 +1,193 @@
+module BABYLON {
+
+    @className("Shape2D")
+    export class Shape2D extends RenderablePrim2D {
+        static SHAPE2D_BORDERPARTID            = 1;
+        static SHAPE2D_FILLPARTID              = 2;
+        static SHAPE2D_CATEGORY_BORDER         = "Border";
+        static SHAPE2D_CATEGORY_BORDERSOLID    = "BorderSolid";
+        static SHAPE2D_CATEGORY_BORDERGRADIENT = "BorderGradient";
+        static SHAPE2D_CATEGORY_FILLSOLID      = "FillSolid";
+        static SHAPE2D_CATEGORY_FILLGRADIENT   = "FillGradient";
+
+        static SHAPE2D_PROPCOUNT: number = RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 5;
+        public static borderProperty: Prim2DPropInfo;
+        public static fillProperty: Prim2DPropInfo;
+        public static borderThicknessProperty: Prim2DPropInfo;
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Shape2D.borderProperty = pi, true)
+        public get border(): IBrush2D {
+            return this._border;
+        }
+
+        public set border(value: IBrush2D) {
+            this._border = value;
+            this._updateTransparencyStatus();
+        }
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Shape2D.fillProperty = pi, true)
+        public get fill(): IBrush2D {
+            return this._fill;
+        }
+
+        public set fill(value: IBrush2D) {
+            this._fill = value;
+            this._updateTransparencyStatus();
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, pi => Shape2D.borderThicknessProperty = pi)
+        public get borderThickness(): number {
+            return this._borderThickness;
+        }
+
+        public set borderThickness(value: number) {
+            this._borderThickness = value;
+        }
+
+        setupShape2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, isVisible: boolean, fill: IBrush2D, border: IBrush2D, borderThickness: number = 1.0) {
+            this.setupRenderablePrim2D(owner, parent, id, position, isVisible);
+            this.border = border;
+            this.fill = fill;
+            this.borderThickness = borderThickness;
+        }
+
+        protected getUsedShaderCategories(dataPart: InstanceDataBase): string[] {
+            var cat = super.getUsedShaderCategories(dataPart);
+
+            // Fill Part
+            if (dataPart.id === Shape2D.SHAPE2D_FILLPARTID) {
+                let fill = this.fill;
+                if (fill instanceof SolidColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_FILLSOLID);
+                }
+                if (fill instanceof GradientColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT);
+                }
+            }
+
+            // Border Part
+            if (dataPart.id === Shape2D.SHAPE2D_BORDERPARTID) {
+                cat.push(Shape2D.SHAPE2D_CATEGORY_BORDER);
+
+                let border = this.border;
+                if (border instanceof SolidColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_BORDERSOLID);
+                }
+                if (border instanceof GradientColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_BORDERGRADIENT);
+                }
+            }
+
+            return cat;
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
+                return false;
+            }
+
+            // Fill Part
+            if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
+                let d = <Shape2DInstanceData>part;
+
+                if (this.fill) {
+                    let fill = this.fill;
+                    if (fill instanceof SolidColorBrush2D) {
+                        d.fillSolidColor = fill.color;
+                    } else if (fill instanceof GradientColorBrush2D) {
+                        d.fillGradientColor1 = fill.color1;
+                        d.fillGradientColor2 = fill.color2;
+                        var t = Matrix.Compose(new Vector3(fill.scale, fill.scale, fill.scale), Quaternion.RotationAxis(new Vector3(0, 0, 1), fill.rotation), new Vector3(fill.translation.x, fill.translation.y, 0));
+
+                        let ty = new Vector4(t.m[1], t.m[5], t.m[9], t.m[13]);
+                        d.fillGradientTY = ty;
+                    }
+                }
+            }
+
+            else if (part.id === Shape2D.SHAPE2D_BORDERPARTID) {
+                let d = <Shape2DInstanceData>part;
+
+                if (this.border) {
+                    d.borderThickness = this.borderThickness;
+
+                    let border = this.border;
+                    if (border instanceof SolidColorBrush2D) {
+                        d.borderSolidColor = border.color;
+                    } else if (border instanceof GradientColorBrush2D) {
+                        d.borderGradientColor1 = border.color1;
+                        d.borderGradientColor2 = border.color2;
+                        var t = Matrix.Compose(new Vector3(border.scale, border.scale, border.scale), Quaternion.RotationAxis(new Vector3(0, 0, 1), border.rotation), new Vector3(border.translation.x, border.translation.y, 0));
+
+                        let ty = new Vector4(t.m[1], t.m[5], t.m[9], t.m[13]);
+                        d.borderGradientTY = ty;
+                    }
+                }
+
+
+            }
+
+            return true;
+        }
+
+        private _updateTransparencyStatus() {
+            this.isTransparent = (this._border && this._border.isTransparent()) || (this._fill && this._fill.isTransparent());
+        }
+
+        private _border: IBrush2D;
+        private _borderThickness: number;
+        private _fill: IBrush2D;
+
+    }
+
+    export class Shape2DInstanceData extends InstanceDataBase {
+        // FILL ATTRIBUTES
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLSOLID)
+        get fillSolidColor(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientColor1(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientColor2(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientTY(): Vector4 {
+            return null;
+        }
+
+        // BORDER ATTRIBUTES
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_BORDER)
+        get borderThickness(): number {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_BORDERSOLID)
+        get borderSolidColor(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_BORDERGRADIENT)
+        get borderGradientColor1(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_BORDERGRADIENT)
+        get borderGradientColor2(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_BORDERGRADIENT)
+        get borderGradientTY(): Vector4 {
+            return null;
+        }
+    }
+}

+ 344 - 0
src/Canvas2d/babylon.smartPropertyPrim.js

@@ -0,0 +1,344 @@
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var Prim2DClassInfo = (function () {
+        function Prim2DClassInfo() {
+        }
+        return Prim2DClassInfo;
+    }());
+    BABYLON.Prim2DClassInfo = Prim2DClassInfo;
+    var Prim2DPropInfo = (function () {
+        function Prim2DPropInfo() {
+        }
+        Prim2DPropInfo.PROPKIND_MODEL = 1;
+        Prim2DPropInfo.PROPKIND_INSTANCE = 2;
+        Prim2DPropInfo.PROPKIND_DYNAMIC = 3;
+        return Prim2DPropInfo;
+    }());
+    BABYLON.Prim2DPropInfo = Prim2DPropInfo;
+    var PropertyChangedInfo = (function () {
+        function PropertyChangedInfo() {
+        }
+        return PropertyChangedInfo;
+    }());
+    BABYLON.PropertyChangedInfo = PropertyChangedInfo;
+    var ClassTreeInfo = (function () {
+        function ClassTreeInfo(baseClass, type, classContentFactory) {
+            this._baseClass = baseClass;
+            this._type = type;
+            this._subClasses = new Array();
+            this._levelContent = new BABYLON.StringDictionary();
+            this._classContentFactory = classContentFactory;
+        }
+        Object.defineProperty(ClassTreeInfo.prototype, "classContent", {
+            get: function () {
+                if (!this._classContent) {
+                    this._classContent = this._classContentFactory(this._baseClass ? this._baseClass.classContent : null);
+                }
+                return this._classContent;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(ClassTreeInfo.prototype, "type", {
+            get: function () {
+                return this._type;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(ClassTreeInfo.prototype, "levelContent", {
+            get: function () {
+                return this._levelContent;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(ClassTreeInfo.prototype, "fullContent", {
+            get: function () {
+                if (!this._fullContent) {
+                    var dic_1 = new BABYLON.StringDictionary();
+                    var curLevel = this;
+                    while (curLevel) {
+                        curLevel.levelContent.forEach(function (k, v) { return dic_1.add(k, v); });
+                        curLevel = curLevel._baseClass;
+                    }
+                    this._fullContent = dic_1;
+                }
+                return this._fullContent;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        ClassTreeInfo.prototype.getLevelOf = function (type) {
+            // Are we already there?
+            if (type === this._type) {
+                return this;
+            }
+            var baseProto = Object.getPrototypeOf(type);
+            return this.getOrAddType(baseProto, type);
+            //// If type is a class, this will get the base class proto, if type is an instance of a class, this will get the proto of the class
+            //let baseTypeName = Tools.getClassName(baseProto);
+            //// If both name are equal we only switch from instance to class, we need to get the next proto in the hierarchy to get the base class
+            //if (baseTypeName === typeName) {
+            //    baseTypeName = Tools.getClassName(Object.getPrototypeOf(baseProto));
+            //}
+            //return this.getOrAddType(baseTypeName, typeName);
+        };
+        ClassTreeInfo.prototype.getOrAddType = function (baseType, type) {
+            // Are we at the level corresponding to the baseType?
+            // If so, get or add the level we're looking for
+            if (baseType === this._type) {
+                for (var _i = 0, _a = this._subClasses; _i < _a.length; _i++) {
+                    var subType = _a[_i];
+                    if (subType.type === type) {
+                        return subType.node;
+                    }
+                }
+                var node = new ClassTreeInfo(this, type, this._classContentFactory);
+                var info = { type: type, node: node };
+                this._subClasses.push(info);
+                return info.node;
+            }
+            // Recurse down to keep looking for the node corresponding to the baseTypeName
+            for (var _b = 0, _c = this._subClasses; _b < _c.length; _b++) {
+                var subType = _c[_b];
+                var info = subType.node.getOrAddType(baseType, type);
+                if (info) {
+                    return info;
+                }
+            }
+            return null;
+        };
+        ClassTreeInfo.get = function (type) {
+            var dic = type["__classTreeInfo"];
+            if (!dic) {
+                return null;
+            }
+            return dic.getLevelOf(type);
+        };
+        ClassTreeInfo.getOrRegister = function (type, classContentFactory) {
+            var dic = type["__classTreeInfo"];
+            if (!dic) {
+                dic = new ClassTreeInfo(null, type, classContentFactory);
+                type["__classTreeInfo"] = dic;
+            }
+            return dic;
+        };
+        return ClassTreeInfo;
+    }());
+    BABYLON.ClassTreeInfo = ClassTreeInfo;
+    var SmartPropertyPrim = (function () {
+        function SmartPropertyPrim() {
+        }
+        SmartPropertyPrim.prototype.setupSmartPropertyPrim = function () {
+            this._modelKey = null;
+            this._modelDirty = false;
+            this._levelBoundingInfoDirty = false;
+            this._instanceDirtyFlags = 0;
+            this._levelBoundingInfo = new BABYLON.BoundingInfo2D();
+        };
+        SmartPropertyPrim.prototype.dispose = function () {
+        };
+        Object.defineProperty(SmartPropertyPrim.prototype, "modelKey", {
+            get: function () {
+                var _this = this;
+                // No need to compute it?
+                if (!this._modelDirty && this._modelKey) {
+                    return this._modelKey;
+                }
+                var modelKey = "Class:" + BABYLON.Tools.getClassName(this) + ";";
+                var propDic = this.propDic;
+                propDic.forEach(function (k, v) {
+                    if (v.kind === Prim2DPropInfo.PROPKIND_MODEL) {
+                        var propVal = _this[v.name];
+                        modelKey += v.name + ":" + ((propVal != null) ? ((v.typeLevelCompare) ? BABYLON.Tools.getClassName(propVal) : propVal.toString()) : "[null]") + ";";
+                    }
+                });
+                this._modelDirty = false;
+                this._modelKey = modelKey;
+                return modelKey;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        SmartPropertyPrim.GetOrAddModelCache = function (key, factory) {
+            return SmartPropertyPrim.ModelCache.getOrAddWithFactory(key, factory);
+        };
+        Object.defineProperty(SmartPropertyPrim.prototype, "propDic", {
+            get: function () {
+                if (!this._propInfo) {
+                    var cti = ClassTreeInfo.get(Object.getPrototypeOf(this));
+                    if (!cti) {
+                        throw new Error("Can't access the propDic member in class definition, is this class SmartPropertyPrim based?");
+                    }
+                    this._propInfo = cti.fullContent;
+                }
+                return this._propInfo;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        SmartPropertyPrim._createPropInfo = function (target, propName, propId, dirtyBoundingInfo, typeLevelCompare, kind) {
+            var dic = ClassTreeInfo.getOrRegister(target, function () { return new Prim2DClassInfo(); });
+            var node = dic.getLevelOf(target);
+            var propInfo = node.levelContent.get(propId.toString());
+            if (propInfo) {
+                throw new Error("The ID " + propId + " is already taken by another property declaration named: " + propInfo.name);
+            }
+            // Create, setup and add the PropInfo object to our prop dictionary
+            propInfo = new Prim2DPropInfo();
+            propInfo.id = propId;
+            propInfo.flagId = Math.pow(2, propId);
+            propInfo.kind = kind;
+            propInfo.name = propName;
+            propInfo.dirtyBoundingInfo = dirtyBoundingInfo;
+            propInfo.typeLevelCompare = typeLevelCompare;
+            node.levelContent.add(propId.toString(), propInfo);
+            return propInfo;
+        };
+        SmartPropertyPrim._checkUnchanged = function (curValue, newValue) {
+            // Nothing to nothing: nothign to do!
+            if ((curValue === null && newValue === null) || (curValue === undefined && newValue === undefined)) {
+                return true;
+            }
+            // Check value unchanged
+            if ((curValue != null) && (newValue != null)) {
+                if (typeof (curValue.equals) == "function") {
+                    if (curValue.equals(newValue)) {
+                        return true;
+                    }
+                }
+                else {
+                    if (curValue === newValue) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        };
+        SmartPropertyPrim.prototype._handlePropChanged = function (curValue, newValue, propName, propInfo, typeLevelCompare) {
+            // Trigger propery changed
+            var info = SmartPropertyPrim.propChangedInfo;
+            info.oldValue = curValue;
+            info.newValue = newValue;
+            info.propertyName = propName;
+            var propMask = propInfo.flagId;
+            this.propertyChanged.notifyObservers(info, propMask);
+            // Check if we need to dirty only if the type change and make the test
+            var skipDirty = false;
+            if (typeLevelCompare && curValue != null && newValue != null) {
+                var cvProto = curValue.__proto__;
+                var nvProto = newValue.__proto__;
+                skipDirty = (cvProto === nvProto);
+            }
+            // Set the dirty flags
+            if (!skipDirty) {
+                if (propInfo.kind === Prim2DPropInfo.PROPKIND_MODEL) {
+                    if ((this._instanceDirtyFlags === 0) && (!this._modelDirty)) {
+                        this.onPrimBecomesDirty();
+                    }
+                    this._modelDirty = true;
+                }
+                else if (propInfo.kind === Prim2DPropInfo.PROPKIND_INSTANCE) {
+                    if ((this._instanceDirtyFlags === 0) && (!this._modelDirty)) {
+                        this.onPrimBecomesDirty();
+                    }
+                    this._instanceDirtyFlags |= propMask;
+                }
+            }
+        };
+        SmartPropertyPrim.prototype.checkPropertiesDirty = function (flags) {
+            return (this._instanceDirtyFlags & flags) !== 0;
+        };
+        SmartPropertyPrim.prototype.clearPropertiesDirty = function (flags) {
+            this._instanceDirtyFlags &= ~flags;
+            return this._instanceDirtyFlags;
+        };
+        Object.defineProperty(SmartPropertyPrim.prototype, "levelBoundingInfo", {
+            get: function () {
+                if (this._levelBoundingInfoDirty) {
+                    this.updateLevelBoundingInfo();
+                    this._levelBoundingInfoDirty = false;
+                }
+                return this._levelBoundingInfo;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        SmartPropertyPrim.prototype.updateLevelBoundingInfo = function () {
+        };
+        SmartPropertyPrim.prototype.onPrimBecomesDirty = function () {
+        };
+        SmartPropertyPrim._hookProperty = function (propId, piStore, typeLevelCompare, dirtyBoundingInfo, kind) {
+            return function (target, propName, descriptor) {
+                var propInfo = SmartPropertyPrim._createPropInfo(target, propName, propId, dirtyBoundingInfo, typeLevelCompare, kind);
+                if (piStore) {
+                    piStore(propInfo);
+                }
+                var getter = descriptor.get, setter = descriptor.set;
+                // Overload the property setter implementation to add our own logic
+                descriptor.set = function (val) {
+                    var curVal = getter.call(this);
+                    if (SmartPropertyPrim._checkUnchanged(curVal, val)) {
+                        return;
+                    }
+                    // Cast the object we're working one
+                    var prim = this;
+                    // Change the value
+                    setter.call(this, val);
+                    // If the property change also dirty the boundingInfo, update the boundingInfo dirty flags
+                    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
+                        if (prim instanceof BABYLON.Prim2DBase) {
+                            var curprim = prim.parent;
+                            while (curprim) {
+                                curprim._boundingInfoDirty = true;
+                                if (curprim instanceof BABYLON.Group2D) {
+                                    if (curprim.isRenderableGroup) {
+                                        break;
+                                    }
+                                }
+                                curprim = curprim.parent;
+                            }
+                        }
+                    }
+                    // Notify change, dirty flags update
+                    prim._handlePropChanged(curVal, val, propName, propInfo, typeLevelCompare);
+                };
+            };
+        };
+        SmartPropertyPrim.ModelCache = new BABYLON.StringDictionary();
+        SmartPropertyPrim.propChangedInfo = new PropertyChangedInfo();
+        SmartPropertyPrim = __decorate([
+            BABYLON.className("SmartPropertyPrim")
+        ], SmartPropertyPrim);
+        return SmartPropertyPrim;
+    }());
+    BABYLON.SmartPropertyPrim = SmartPropertyPrim;
+    function modelLevelProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo) {
+        if (typeLevelCompare === void 0) { typeLevelCompare = false; }
+        if (dirtyBoundingInfo === void 0) { dirtyBoundingInfo = false; }
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_MODEL);
+    }
+    BABYLON.modelLevelProperty = modelLevelProperty;
+    function instanceLevelProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo) {
+        if (typeLevelCompare === void 0) { typeLevelCompare = false; }
+        if (dirtyBoundingInfo === void 0) { dirtyBoundingInfo = false; }
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_INSTANCE);
+    }
+    BABYLON.instanceLevelProperty = instanceLevelProperty;
+    function dynamicLevelProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo) {
+        if (typeLevelCompare === void 0) { typeLevelCompare = false; }
+        if (dirtyBoundingInfo === void 0) { dirtyBoundingInfo = false; }
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_DYNAMIC);
+    }
+    BABYLON.dynamicLevelProperty = dynamicLevelProperty;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.smartPropertyPrim.js.map

+ 391 - 0
src/Canvas2d/babylon.smartPropertyPrim.ts

@@ -0,0 +1,391 @@
+module BABYLON {
+    export class Prim2DClassInfo {
+
+    }
+
+    export class Prim2DPropInfo {
+        static PROPKIND_MODEL: number = 1;
+        static PROPKIND_INSTANCE: number = 2;
+        static PROPKIND_DYNAMIC: number = 3;
+
+        id: number;
+        flagId: number;
+        kind: number;
+        name: string;
+        dirtyBoundingInfo: boolean;
+        typeLevelCompare: boolean;
+    }
+
+    export class PropertyChangedInfo {
+        oldValue: any;
+        newValue: any;
+        propertyName: string;
+    }
+
+    export interface IPropertyChanged {
+        propertyChanged: Observable<PropertyChangedInfo>;
+    }
+
+    export class ClassTreeInfo<TClass, TProp>{
+        constructor(baseClass: ClassTreeInfo<TClass, TProp>, type: Object, classContentFactory: (base: TClass) => TClass) {
+            this._baseClass = baseClass;
+            this._type = type;
+            this._subClasses = new Array<{ type: Object, node: ClassTreeInfo<TClass, TProp> }>();
+            this._levelContent = new StringDictionary<TProp>();
+            this._classContentFactory = classContentFactory;
+        }
+
+        get classContent(): TClass {
+            if (!this._classContent) {
+                this._classContent = this._classContentFactory(this._baseClass ? this._baseClass.classContent : null);
+            }
+            return this._classContent;
+        }
+
+        get type(): Object {
+            return this._type;
+        }
+
+        get levelContent(): StringDictionary<TProp> {
+            return this._levelContent;
+        }
+
+        get fullContent(): StringDictionary<TProp> {
+            if (!this._fullContent) {
+                let dic = new StringDictionary<TProp>();
+                let curLevel: ClassTreeInfo<TClass, TProp> = this;
+                while (curLevel) {
+                    curLevel.levelContent.forEach((k, v) => dic.add(k, v));
+                    curLevel = curLevel._baseClass;
+                }
+
+                this._fullContent = dic;
+            }
+
+            return this._fullContent;
+        }
+
+        getLevelOf(type: Object): ClassTreeInfo<TClass, TProp> {
+            // Are we already there?
+            if (type === this._type) {
+                return this;
+            }
+
+            let baseProto = Object.getPrototypeOf(type);
+            let curProtoContent = this.getOrAddType(Object.getPrototypeOf(baseProto), baseProto);
+            if (!curProtoContent) {
+                this.getLevelOf(baseProto);
+            }
+
+            return this.getOrAddType(baseProto, type);
+        }
+
+        getOrAddType(baseType: Object, type: Object): ClassTreeInfo<TClass, TProp> {
+
+            // Are we at the level corresponding to the baseType?
+            // If so, get or add the level we're looking for
+            if (baseType === this._type) {
+                for (let subType of this._subClasses) {
+                    if (subType.type === type) {
+                        return subType.node;
+                    }
+                }
+                let node = new ClassTreeInfo<TClass, TProp>(this, type, this._classContentFactory);
+                let info = { type: type, node: node};
+                this._subClasses.push(info);
+                return info.node;
+            }
+
+            // Recurse down to keep looking for the node corresponding to the baseTypeName
+            for (let subType of this._subClasses) {
+                let info = subType.node.getOrAddType(baseType, type);
+                if (info) {
+                    return info;
+                }
+            }
+            return null;
+        }
+
+        static get<TClass, TProp>(type: Object): ClassTreeInfo<TClass, TProp> {
+            let dic = <ClassTreeInfo<TClass, TProp>>type["__classTreeInfo"];
+            if (!dic) {
+                return null;
+            }
+            return dic.getLevelOf(type);
+        }
+
+        static getOrRegister<TClass, TProp>(type: Object, classContentFactory: (base: TClass) => TClass): ClassTreeInfo<TClass, TProp> {
+            let dic = <ClassTreeInfo<TClass, TProp>>type["__classTreeInfo"];
+            if (!dic) {
+                dic = new ClassTreeInfo<TClass, TProp>(null, type, classContentFactory);
+                type["__classTreeInfo"] = dic;
+            }
+            return dic;
+        }
+
+        private _type: Object;
+        private _classContent: TClass;
+        private _baseClass: ClassTreeInfo<TClass, TProp>;
+        private _subClasses: Array<{type: Object, node: ClassTreeInfo<TClass, TProp>}>;
+        private _levelContent: StringDictionary<TProp>;
+        private _fullContent: StringDictionary<TProp>;
+        private _classContentFactory: (base: TClass) => TClass;
+    }
+
+    @className("SmartPropertyPrim")
+    export class SmartPropertyPrim implements IPropertyChanged {
+
+        protected setupSmartPropertyPrim() {
+            this._modelKey = null;
+            this._modelDirty = false;
+            this._levelBoundingInfoDirty = false;
+            this._instanceDirtyFlags = 0;
+            this._isDisposed = false;
+            this._levelBoundingInfo = new BoundingInfo2D();
+        }
+
+        public propertyChanged: Observable<PropertyChangedInfo>;
+
+        public get isDisposed(): boolean {
+            return this._isDisposed;
+        }
+
+        public dispose(): boolean {
+            if (this.isDisposed) {
+                return false;
+            }
+
+            this._isDisposed = true;
+            return true;
+        }
+
+        public get modelKey(): string {
+
+            // No need to compute it?
+            if (!this._modelDirty && this._modelKey) {
+                return this._modelKey;
+            }
+
+            let modelKey = `Class:${Tools.getClassName(this)};`;
+            let propDic = this.propDic;
+            propDic.forEach((k, v) => {
+                if (v.kind === Prim2DPropInfo.PROPKIND_MODEL) {
+                    let propVal = this[v.name];
+                    modelKey += v.name + ":" + ((propVal != null) ? ((v.typeLevelCompare) ? Tools.getClassName(propVal) : propVal.toString()) : "[null]") + ";";
+                }
+            });
+
+            this._modelDirty = false;
+            this._modelKey = modelKey;
+
+            return modelKey;
+        }
+
+        public get isDirty(): boolean {
+            return (this._instanceDirtyFlags !== 0) || this._modelDirty;
+        }
+
+        protected static GetOrAddModelCache<TInstData>(key: string, factory: (key: string) => ModelRenderCache): ModelRenderCache {
+            return SmartPropertyPrim.ModelCache.getOrAddWithFactory(key, factory);
+        }
+
+        protected static ModelCache: StringDictionary<ModelRenderCache> = new StringDictionary<ModelRenderCache>();
+
+        private get propDic(): StringDictionary<Prim2DPropInfo> {
+            if (!this._propInfo) {
+                let cti = ClassTreeInfo.get<Prim2DClassInfo, Prim2DPropInfo>(Object.getPrototypeOf(this));
+                if (!cti) {
+                    throw new Error("Can't access the propDic member in class definition, is this class SmartPropertyPrim based?");
+                }
+                this._propInfo = cti.fullContent;
+            }
+
+            return this._propInfo;
+        }
+
+        private static _createPropInfo(target: Object, propName: string, propId: number, dirtyBoundingInfo: boolean, typeLevelCompare: boolean, kind: number): Prim2DPropInfo {
+            let dic = ClassTreeInfo.getOrRegister<Prim2DClassInfo, Prim2DPropInfo>(target, () => new Prim2DClassInfo());
+            var node = dic.getLevelOf(target);
+
+            let propInfo = node.levelContent.get(propId.toString());
+            if (propInfo) {
+                throw new Error(`The ID ${propId} is already taken by another property declaration named: ${propInfo.name}`);
+            }
+
+            // Create, setup and add the PropInfo object to our prop dictionary
+            propInfo = new Prim2DPropInfo();
+            propInfo.id = propId;
+            propInfo.flagId = Math.pow(2, propId);
+            propInfo.kind = kind;
+            propInfo.name = propName;
+            propInfo.dirtyBoundingInfo = dirtyBoundingInfo;
+            propInfo.typeLevelCompare = typeLevelCompare;
+            node.levelContent.add(propId.toString(), propInfo);
+
+            return propInfo;
+        }
+
+        private static _checkUnchanged(curValue, newValue): boolean {
+            // Nothing to nothing: nothign to do!
+            if ((curValue === null && newValue === null) || (curValue === undefined && newValue === undefined)) {
+                return true;
+            }
+
+            // Check value unchanged
+            if ((curValue != null) && (newValue != null)) {
+                if (typeof (curValue.equals) == "function") {
+                    if (curValue.equals(newValue)) {
+                        return true;
+                    }
+                } else {
+                    if (curValue === newValue) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private static propChangedInfo = new PropertyChangedInfo();
+
+        private _handlePropChanged<T>(curValue: T, newValue: T, propName: string, propInfo: Prim2DPropInfo, typeLevelCompare: boolean) {
+            // Trigger propery changed
+            let info = SmartPropertyPrim.propChangedInfo;
+            info.oldValue = curValue;
+            info.newValue = newValue;
+            info.propertyName = propName;
+            let propMask = propInfo.flagId;
+            this.propertyChanged.notifyObservers(info, propMask);
+
+            // If the property belong to a group, check if it's a cached one, and dirty its render sprite accordingly
+            if (this instanceof Group2D) {
+                this.handleGroupChanged(propInfo);
+            }
+
+            // Check if we need to dirty only if the type change and make the test
+            var skipDirty = false;
+            if (typeLevelCompare && curValue != null && newValue != null) {
+                var cvProto = (<any>curValue).__proto__;
+                var nvProto = (<any>newValue).__proto__;
+
+                skipDirty = (cvProto === nvProto);
+            }
+
+            // Set the dirty flags
+            if (!skipDirty) {
+                if (propInfo.kind === Prim2DPropInfo.PROPKIND_MODEL) {
+                    if (!this.isDirty) {
+                        this.onPrimBecomesDirty();
+                    }
+                    this._modelDirty = true;
+                } else if (propInfo.kind === Prim2DPropInfo.PROPKIND_INSTANCE) {
+                    if (!this.isDirty) {
+                        this.onPrimBecomesDirty();
+                    }
+                    this._instanceDirtyFlags |= propMask;
+                }
+            }
+        }
+
+        protected handleGroupChanged(prop: Prim2DPropInfo) {
+
+        }
+
+        public checkPropertiesDirty(flags: number): boolean {
+            return (this._instanceDirtyFlags & flags) !== 0;
+        }
+
+        protected clearPropertiesDirty(flags: number): number {
+            this._instanceDirtyFlags &= ~flags;
+            return this._instanceDirtyFlags;
+        }
+
+        public get levelBoundingInfo(): BoundingInfo2D {
+            if (this._levelBoundingInfoDirty) {
+                this.updateLevelBoundingInfo();
+                this._levelBoundingInfoDirty = false;
+            }
+            return this._levelBoundingInfo;
+        }
+
+        protected updateLevelBoundingInfo() {
+
+        }
+
+        protected onPrimBecomesDirty() {
+
+        }
+
+        static _hookProperty<T>(propId: number, piStore: (pi: Prim2DPropInfo) => void, typeLevelCompare: boolean, dirtyBoundingInfo: boolean, kind: number): (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => void {
+            return (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => {
+
+                var propInfo = SmartPropertyPrim._createPropInfo(target, <string>propName, propId, dirtyBoundingInfo, typeLevelCompare, kind);
+                if (piStore) {
+                    piStore(propInfo);
+                }
+                let getter = descriptor.get, setter = descriptor.set;
+
+                // Overload the property setter implementation to add our own logic
+                descriptor.set = function (val) {
+                    let curVal = getter.call(this);
+
+                    if (SmartPropertyPrim._checkUnchanged(curVal, val)) {
+                        return;
+                    }
+
+                    // Cast the object we're working one
+                    let prim = <SmartPropertyPrim>this;
+
+                    // Change the value
+                    setter.call(this, val);
+
+                    // If the property change also dirty the boundingInfo, update the boundingInfo dirty flags
+                    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
+                        if (prim instanceof Prim2DBase) {
+                            let curprim = prim.parent;
+                            while (curprim) {
+                                curprim._boundingInfoDirty = true;
+
+                                if (curprim instanceof Group2D) {
+                                    if (curprim.isRenderableGroup) {
+                                        break;
+                                    }
+                                }
+
+                                curprim = curprim.parent;
+                            }
+                        }
+                    }
+
+                    // Notify change, dirty flags update
+                    prim._handlePropChanged(curVal, val, <string>propName, propInfo, typeLevelCompare);
+                }
+            }
+        }
+
+        private _modelKey; string;
+        private _propInfo: StringDictionary<Prim2DPropInfo>;
+        private _levelBoundingInfoDirty: boolean;
+        private _isDisposed: boolean;
+        protected _levelBoundingInfo: BoundingInfo2D;
+        protected _boundingInfo: BoundingInfo2D;
+        protected _modelDirty: boolean;
+        protected _instanceDirtyFlags: number;
+    }
+
+    export function modelLevelProperty<T>(propId: number, piStore: (pi: Prim2DPropInfo) => void, typeLevelCompare = false, dirtyBoundingInfo = false): (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => void {
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_MODEL);
+    }
+
+    export function instanceLevelProperty<T>(propId: number, piStore: (pi: Prim2DPropInfo) => void, typeLevelCompare = false, dirtyBoundingInfo = false): (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => void {
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_INSTANCE);
+    }
+
+    export function dynamicLevelProperty<T>(propId: number, piStore: (pi: Prim2DPropInfo) => void, typeLevelCompare = false, dirtyBoundingInfo = false): (target: Object, propName: string | symbol, descriptor: TypedPropertyDescriptor<T>) => void {
+        return SmartPropertyPrim._hookProperty(propId, piStore, typeLevelCompare, dirtyBoundingInfo, Prim2DPropInfo.PROPKIND_DYNAMIC);
+    }
+}

+ 233 - 0
src/Canvas2d/babylon.sprite2d.js

@@ -0,0 +1,233 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+    return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+var BABYLON;
+(function (BABYLON) {
+    var Sprite2DRenderCache = (function (_super) {
+        __extends(Sprite2DRenderCache, _super);
+        function Sprite2DRenderCache() {
+            _super.apply(this, arguments);
+        }
+        Sprite2DRenderCache.prototype.render = function (instanceInfo, context) {
+            // Do nothing if the shader is still loading/preparing
+            if (!this.effect.isReady() || !this.texture.isReady()) {
+                return false;
+            }
+            // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+            if (!this.instancingAttributes) {
+                this.instancingAttributes = instanceInfo._classTreeInfo.classContent.getInstancingAttributeInfos(this.effect);
+            }
+            var engine = instanceInfo._owner.owner.engine;
+            engine.enableEffect(this.effect);
+            this.effect.setTexture("diffuseSampler", this.texture);
+            engine.bindBuffers(this.vb, this.ib, [1], 4, this.effect);
+            engine.updateAndBindInstancesBuffer(instanceInfo._instancesBuffer, null, this.instancingAttributes);
+            engine.draw(true, 0, 6, instanceInfo._instancesData.usedElementCount);
+            engine.unBindInstancesBuffer(instanceInfo._instancesBuffer, this.instancingAttributes);
+            return true;
+        };
+        return Sprite2DRenderCache;
+    }(BABYLON.ModelRenderCache));
+    BABYLON.Sprite2DRenderCache = Sprite2DRenderCache;
+    var Sprite2DInstanceData = (function (_super) {
+        __extends(Sprite2DInstanceData, _super);
+        function Sprite2DInstanceData() {
+            _super.apply(this, arguments);
+        }
+        Object.defineProperty(Sprite2DInstanceData.prototype, "topLeftUV", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2DInstanceData.prototype, "sizeUV", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2DInstanceData.prototype, "textureSize", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2DInstanceData.prototype, "frame", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2DInstanceData.prototype, "invertY", {
+            get: function () {
+                return null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        __decorate([
+            BABYLON.instanceData()
+        ], Sprite2DInstanceData.prototype, "topLeftUV", null);
+        __decorate([
+            BABYLON.instanceData()
+        ], Sprite2DInstanceData.prototype, "sizeUV", null);
+        __decorate([
+            BABYLON.instanceData()
+        ], Sprite2DInstanceData.prototype, "textureSize", null);
+        __decorate([
+            BABYLON.instanceData()
+        ], Sprite2DInstanceData.prototype, "frame", null);
+        __decorate([
+            BABYLON.instanceData()
+        ], Sprite2DInstanceData.prototype, "invertY", null);
+        return Sprite2DInstanceData;
+    }(BABYLON.InstanceDataBase));
+    BABYLON.Sprite2DInstanceData = Sprite2DInstanceData;
+    var Sprite2D = (function (_super) {
+        __extends(Sprite2D, _super);
+        function Sprite2D() {
+            _super.apply(this, arguments);
+        }
+        Object.defineProperty(Sprite2D.prototype, "texture", {
+            get: function () {
+                return this._texture;
+            },
+            set: function (value) {
+                this._texture = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2D.prototype, "spriteSize", {
+            get: function () {
+                return this._size;
+            },
+            set: function (value) {
+                this._size = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2D.prototype, "spriteLocation", {
+            get: function () {
+                return this._location;
+            },
+            set: function (value) {
+                this._location = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2D.prototype, "spriteFrame", {
+            get: function () {
+                return this._spriteFrame;
+            },
+            set: function (value) {
+                this._spriteFrame = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(Sprite2D.prototype, "invertY", {
+            get: function () {
+                return this._invertY;
+            },
+            set: function (value) {
+                this._invertY = value;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Sprite2D.prototype.updateLevelBoundingInfo = function () {
+            this._levelBoundingInfo.radius = Math.sqrt(this.spriteSize.width * this.spriteSize.width + this.spriteSize.height * this.spriteSize.height);
+            this._levelBoundingInfo.extent = this.spriteSize.clone();
+        };
+        Sprite2D.prototype.setupSprite2D = function (owner, parent, id, position, texture, spriteSize, spriteLocation, invertY) {
+            this.setupRenderablePrim2D(owner, parent, id, position, true, null, null);
+            this.texture = texture;
+            this.spriteSize = spriteSize;
+            this.spriteLocation = spriteLocation;
+            this.spriteFrame = 0;
+            this.invertY = invertY;
+        };
+        Sprite2D.Create = function (parent, id, x, y, texture, spriteSize, spriteLocation, invertY) {
+            if (invertY === void 0) { invertY = false; }
+            BABYLON.Prim2DBase.CheckParent(parent);
+            var sprite = new Sprite2D();
+            sprite.setupSprite2D(parent.owner, parent, id, new BABYLON.Vector2(x, y), texture, spriteSize, spriteLocation, invertY);
+            return sprite;
+        };
+        Sprite2D.prototype.createModelRenderCache = function () {
+            var renderCache = new Sprite2DRenderCache();
+            var engine = this.owner.engine;
+            var vb = new Float32Array(4);
+            for (var i = 0; i < 4; i++) {
+                vb[i] = i;
+            }
+            renderCache.vb = engine.createVertexBuffer(vb);
+            var ib = new Float32Array(6);
+            ib[0] = 0;
+            ib[1] = 1;
+            ib[2] = 2;
+            ib[3] = 0;
+            ib[4] = 2;
+            ib[5] = 3;
+            renderCache.ib = engine.createIndexBuffer(ib);
+            renderCache.texture = this.texture;
+            renderCache.effect = engine.createEffect({ vertex: "sprite2d", fragment: "sprite2d" }, ["index", "zBias", "transformX", "transformY", "topLeftUV", "sizeUV", "origin", "textureSize", "frame", "invertY"], [], ["diffuseSampler"], "");
+            return renderCache;
+        };
+        Sprite2D.prototype.createInstanceData = function () {
+            return new Sprite2DInstanceData();
+        };
+        Sprite2D.prototype.refreshInstanceData = function () {
+            if (!_super.prototype.refreshInstanceData.call(this)) {
+                return false;
+            }
+            var d = this._instanceData;
+            var ts = this.texture.getSize();
+            var sl = this.spriteLocation;
+            var ss = this.spriteSize;
+            d.topLeftUV = new BABYLON.Vector2(sl.x / ts.width, sl.y / ts.height);
+            var suv = new BABYLON.Vector2(ss.width / ts.width, ss.height / ts.height);
+            d.sizeUV = suv;
+            d.frame = this.spriteFrame;
+            d.textureSize = new BABYLON.Vector2(ts.width, ts.height);
+            d.invertY = this.invertY ? 1 : 0;
+            return true;
+        };
+        __decorate([
+            BABYLON.modelLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, function (pi) { return Sprite2D.textureProperty = pi; })
+        ], Sprite2D.prototype, "texture", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, function (pi) { return Sprite2D.spriteSizeProperty = pi; }, false, true)
+        ], Sprite2D.prototype, "spriteSize", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, function (pi) { return Sprite2D.spriteLocationProperty = pi; })
+        ], Sprite2D.prototype, "spriteLocation", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, function (pi) { return Sprite2D.spriteFrameProperty = pi; })
+        ], Sprite2D.prototype, "spriteFrame", null);
+        __decorate([
+            BABYLON.instanceLevelProperty(BABYLON.RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 5, function (pi) { return Sprite2D.invertYProperty = pi; })
+        ], Sprite2D.prototype, "invertY", null);
+        Sprite2D = __decorate([
+            BABYLON.className("Sprite2D")
+        ], Sprite2D);
+        return Sprite2D;
+    }(BABYLON.RenderablePrim2D));
+    BABYLON.Sprite2D = Sprite2D;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.sprite2d.js.map

+ 226 - 0
src/Canvas2d/babylon.sprite2d.ts

@@ -0,0 +1,226 @@
+module BABYLON {
+    export class Sprite2DRenderCache extends ModelRenderCache {
+        vb: WebGLBuffer;
+        ib: WebGLBuffer;
+        borderVB: WebGLBuffer;
+        borderIB: WebGLBuffer;
+        instancingAttributes: InstancingAttributeInfo[];
+
+        texture: Texture;
+        effect: Effect;
+
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            // Do nothing if the shader is still loading/preparing
+            if (!this.effect.isReady() || !this.texture.isReady()) {
+                return false;
+            }
+
+            // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+            var engine = instanceInfo._owner.owner.engine;
+
+            engine.enableEffect(this.effect);
+            this.effect.setTexture("diffuseSampler", this.texture);
+            engine.bindBuffers(this.vb, this.ib, [1], 4, this.effect);
+
+            var cur = engine.getAlphaMode();
+            engine.setAlphaMode(Engine.ALPHA_COMBINE);
+            let count = instanceInfo._instancesPartsData[0].usedElementCount;
+            if (instanceInfo._owner.owner.supportInstancedArray) {
+                if (!this.instancingAttributes) {
+                    this.instancingAttributes = this.loadInstancingAttributes(Sprite2D.SPRITE2D_MAINPARTID, this.effect);
+                }
+                engine.updateAndBindInstancesBuffer(instanceInfo._instancesPartsBuffer[0], null, this.instancingAttributes);
+                engine.draw(true, 0, 6, count);
+                engine.unBindInstancesBuffer(instanceInfo._instancesPartsBuffer[0], this.instancingAttributes);
+            } else {
+                for (let i = 0; i < count; i++) {
+                    this.setupUniforms(this.effect, 0, instanceInfo._instancesPartsData[0], i);
+                    engine.draw(true, 0, 6);
+                }
+            }
+
+            engine.setAlphaMode(cur);
+
+
+            return true;
+        }
+    }
+
+    export class Sprite2DInstanceData extends InstanceDataBase {
+        constructor(partId: number) {
+            super(partId, 1);
+        }
+
+        @instanceData()
+        get topLeftUV(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get sizeUV(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get textureSize(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get frame(): number {
+            return null;
+        }
+
+        @instanceData()
+        get invertY(): number {
+            return null;
+        }
+    }
+
+    @className("Sprite2D")
+    export class Sprite2D extends RenderablePrim2D {
+        static SPRITE2D_MAINPARTID = 1;
+
+        public static textureProperty: Prim2DPropInfo;
+        public static spriteSizeProperty: Prim2DPropInfo;
+        public static spriteLocationProperty: Prim2DPropInfo;
+        public static spriteFrameProperty: Prim2DPropInfo;
+        public static invertYProperty: Prim2DPropInfo;
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Sprite2D.textureProperty = pi)
+        public get texture(): Texture {
+            return this._texture;
+        }
+
+        public set texture(value: Texture) {
+            this._texture = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Sprite2D.spriteSizeProperty = pi, false, true)
+        public get spriteSize(): Size {
+            return this._size;
+        }
+
+        public set spriteSize(value: Size) {
+            this._size = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, pi => Sprite2D.spriteLocationProperty = pi)
+        public get spriteLocation(): Vector2 {
+            return this._location;
+        }
+
+        public set spriteLocation(value: Vector2) {
+            this._location = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, pi => Sprite2D.spriteFrameProperty = pi)
+        public get spriteFrame(): number {
+            return this._spriteFrame;
+        }
+
+        public set spriteFrame(value: number) {
+            this._spriteFrame = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 5, pi => Sprite2D.invertYProperty = pi)
+        public get invertY(): boolean {
+            return this._invertY;
+        }
+
+        public set invertY(value: boolean) {
+            this._invertY = value;
+        }
+
+        protected updateLevelBoundingInfo() {
+            BoundingInfo2D.CreateFromSizeToRef(this.spriteSize, this._levelBoundingInfo);
+        }
+
+        protected setupSprite2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean) {
+            this.setupRenderablePrim2D(owner, parent, id, position, true);
+            this.texture = texture;
+            this.texture.wrapU = Texture.CLAMP_ADDRESSMODE;
+            this.texture.wrapV = Texture.CLAMP_ADDRESSMODE;
+            this.spriteSize = spriteSize;
+            this.spriteLocation = spriteLocation;
+            this.spriteFrame = 0;
+            this.invertY = invertY;
+            this._isTransparent = true;
+        }
+
+        public static Create(parent: Prim2DBase, id: string, x: number, y: number, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean = false): Sprite2D {
+            Prim2DBase.CheckParent(parent);
+
+            let sprite = new Sprite2D();
+            sprite.setupSprite2D(parent.owner, parent, id, new Vector2(x, y), texture, spriteSize, spriteLocation, invertY);
+            return sprite;
+        }
+
+        protected createModelRenderCache(modelKey: string, isTransparent: boolean): ModelRenderCache {
+            let renderCache = new Sprite2DRenderCache(modelKey, isTransparent);
+            return renderCache;
+        }
+
+        protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
+            let renderCache = <Sprite2DRenderCache>modelRenderCache;
+            let engine = this.owner.engine;
+
+            let vb = new Float32Array(4);
+            for (let i = 0; i < 4; i++) {
+                vb[i] = i;
+            }
+            renderCache.vb = engine.createVertexBuffer(vb);
+
+            let ib = new Float32Array(6);
+            ib[0] = 0;
+            ib[1] = 2;
+            ib[2] = 1;
+            ib[3] = 0;
+            ib[4] = 3;
+            ib[5] = 2;
+
+            renderCache.ib = engine.createIndexBuffer(ib);
+
+            renderCache.texture = this.texture;
+
+            var ei = this.getDataPartEffectInfo(Sprite2D.SPRITE2D_MAINPARTID, ["index"]);
+            renderCache.effect = engine.createEffect({ vertex: "sprite2d", fragment: "sprite2d" }, ei.attributes, ei.uniforms, ["diffuseSampler"], ei.defines, null, e => {
+//                renderCache.setupUniformsLocation(e, ei.uniforms, Sprite2D.SPRITE2D_MAINPARTID);
+            });
+
+            return renderCache;
+        }
+
+        protected createInstanceDataParts(): InstanceDataBase[] {
+            return [new Sprite2DInstanceData(Sprite2D.SPRITE2D_MAINPARTID)];
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
+                return false;
+            }
+
+            if (part.id === Sprite2D.SPRITE2D_MAINPARTID) {
+                let d = <Sprite2DInstanceData>this._instanceDataParts[0];
+                let ts = this.texture.getSize();
+                let sl = this.spriteLocation;
+                let ss = this.spriteSize;
+                d.topLeftUV = new Vector2(sl.x / ts.width, sl.y / ts.height);
+                let suv = new Vector2(ss.width / ts.width, ss.height / ts.height);
+                d.sizeUV = suv;
+                d.frame = this.spriteFrame;
+                d.textureSize = new Vector2(ts.width, ts.height);
+                d.invertY = this.invertY ? 1 : 0;
+            }
+            return true;
+        }
+
+        private _texture: Texture;
+        private _size: Size;
+        private _location: Vector2;
+        private _spriteFrame: number;
+        private _invertY: boolean;
+    }
+
+
+}

+ 331 - 0
src/Canvas2d/babylon.text2d.ts

@@ -0,0 +1,331 @@
+module BABYLON {
+    export class Text2DRenderCache extends ModelRenderCache {
+        vb: WebGLBuffer;
+        ib: WebGLBuffer;
+        borderVB: WebGLBuffer;
+        borderIB: WebGLBuffer;
+        instancingAttributes: InstancingAttributeInfo[];
+        fontTexture: FontTexture;
+        effect: Effect;
+
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            // Do nothing if the shader is still loading/preparing
+            if (!this.effect.isReady() || !this.fontTexture.isReady()) {
+                return false;
+            }
+
+            // Compute the offset locations of the attributes in the vertexshader that will be mapped to the instance buffer data
+            if (!this.instancingAttributes) {
+                this.instancingAttributes = this.loadInstancingAttributes(Text2D.TEXT2D_MAINPARTID, this.effect);
+            }
+            var engine = instanceInfo._owner.owner.engine;
+
+            this.fontTexture.update();
+
+            engine.enableEffect(this.effect);
+            this.effect.setTexture("diffuseSampler", this.fontTexture);
+            engine.bindBuffers(this.vb, this.ib, [1], 4, this.effect);
+
+            var cur = engine.getAlphaMode();
+            engine.setAlphaMode(Engine.ALPHA_COMBINE);
+            let count = instanceInfo._instancesPartsData[0].usedElementCount;
+            if (instanceInfo._owner.owner.supportInstancedArray) {
+                engine.updateAndBindInstancesBuffer(instanceInfo._instancesPartsBuffer[0], null, this.instancingAttributes);
+                engine.draw(true, 0, 6, count);
+                engine.unBindInstancesBuffer(instanceInfo._instancesPartsBuffer[0], this.instancingAttributes);
+            } else {
+                for (let i = 0; i < count; i++) {
+                    this.setupUniforms(this.effect, 0, instanceInfo._instancesPartsData[0], i);
+                    engine.draw(true, 0, 6);
+                }
+            }
+
+            engine.setAlphaMode(cur);
+
+
+            return true;
+        }
+    }
+
+    export class Text2DInstanceData extends InstanceDataBase {
+        constructor(partId: number, dataElementCount: number) {
+            super(partId, dataElementCount);
+        }
+
+        @instanceData()
+        get topLeftUV(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get sizeUV(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get textureSize(): Vector2 {
+            return null;
+        }
+
+        @instanceData()
+        get color(): Color4 {
+            return null;
+        }
+    }
+
+    @className("Text2D")
+    export class Text2D extends RenderablePrim2D {
+        static TEXT2D_MAINPARTID = 1;
+
+        public static fontProperty: Prim2DPropInfo;
+        public static defaultFontColorProperty: Prim2DPropInfo;
+        public static textProperty: Prim2DPropInfo;
+        public static areaSizeProperty: Prim2DPropInfo;
+        public static vAlignProperty: Prim2DPropInfo;
+        public static hAlignProperty: Prim2DPropInfo;
+
+        public static TEXT2D_VALIGN_TOP = 1;
+        public static TEXT2D_VALIGN_CENTER = 2;
+        public static TEXT2D_VALIGN_BOTTOM = 3;
+        public static TEXT2D_HALIGN_LEFT = 1;
+        public static TEXT2D_HALIGN_CENTER = 2;
+        public static TEXT2D_HALIGN_RIGHT = 3;
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Text2D.fontProperty = pi, false, true)
+        public get fontName(): string {
+            return this._fontName;
+        }
+
+        public set fontName(value: string) {
+            if (this._fontName) {
+                throw new Error("Font Name change is not supported right now.");
+            }
+            this._fontName = value;
+        }
+
+        @dynamicLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Text2D.defaultFontColorProperty = pi)
+        public get defaultFontColor(): Color4 {
+            return this._defaultFontColor;
+        }
+
+        public set defaultFontColor(value: Color4) {
+            this._defaultFontColor = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 3, pi => Text2D.textProperty = pi, false, true)
+        public get text(): string {
+            return this._text;
+        }
+
+        public set text(value: string) {
+            this._text = value;
+            this._actualAreaSize = null;    // A change of text will reset the Actual Area Size which will be recomputed next time it's used
+            this._updateCharCount();
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 4, pi => Text2D.areaSizeProperty = pi)
+        public get areaSize(): Size {
+            return this._areaSize;
+        }
+
+        public set areaSize(value: Size) {
+            this._areaSize = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 5, pi => Text2D.vAlignProperty = pi)
+        public get vAlign(): number {
+            return this._vAlign;
+        }
+
+        public set vAlign(value: number) {
+            this._vAlign = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 6, pi => Text2D.hAlignProperty = pi)
+        public get hAlign(): number {
+            return this._hAlign;
+        }
+
+        public set hAlign(value: number) {
+            this._hAlign = value;
+        }
+
+        public get actualAreaSize(): Size {
+            if (this.areaSize) {
+                return this.areaSize;
+            }
+
+            if (this._actualAreaSize) {
+                return this._actualAreaSize;
+            }
+
+            this._actualAreaSize = this.fontTexture.measureText(this._text, this._tabulationSize);
+
+            return this._actualAreaSize;
+        }
+
+        protected get fontTexture(): FontTexture {
+            if (this._fontTexture) {
+                return this._fontTexture;
+            }
+
+            this._fontTexture = FontTexture.GetCachedFontTexture(this.owner.scene, this.fontName);
+            return this._fontTexture;
+        }
+
+        public dispose(): boolean {
+            if (!super.dispose()) {
+                return false;
+            }
+
+            if (this._fontTexture) {
+                FontTexture.ReleaseCachedFontTexture(this.owner.scene, this.fontName);
+                this._fontTexture = null;
+            }
+
+            return true;
+        }
+
+        protected updateLevelBoundingInfo() {
+            BoundingInfo2D.CreateFromSizeToRef(this.actualAreaSize, this._levelBoundingInfo);
+        }
+
+        protected setupText2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, fontName: string, text: string, areaSize: Size, defaultFontColor: Color4, vAlign, hAlign, tabulationSize: number) {
+            this.setupRenderablePrim2D(owner, parent, id, position, true);
+
+            this.fontName = fontName;
+            this.defaultFontColor = defaultFontColor;
+            this.text = text;
+            this.areaSize = areaSize;
+            this.vAlign = vAlign;
+            this.hAlign = hAlign;
+            this._tabulationSize = tabulationSize;
+            this._isTransparent = true;
+            this.origin = Vector2.Zero();
+        }
+
+        public static Create(parent: Prim2DBase, id: string, x: number, y: number, fontName: string, text: string, defaultFontColor?: Color4, areaSize?: Size, vAlign = Text2D.TEXT2D_VALIGN_TOP, hAlign = Text2D.TEXT2D_HALIGN_LEFT, tabulationSize: number = 4): Text2D {
+            Prim2DBase.CheckParent(parent);
+
+            let text2d = new Text2D();
+            text2d.setupText2D(parent.owner, parent, id, new Vector2(x, y), fontName, text, areaSize, defaultFontColor || new Color4(0,0,0,1), vAlign, hAlign, tabulationSize);
+            return text2d;
+        }
+
+        protected createModelRenderCache(modelKey: string, isTransparent: boolean): ModelRenderCache {
+            let renderCache = new Text2DRenderCache(modelKey, isTransparent);
+            return renderCache;
+        }
+
+        protected setupModelRenderCache(modelRenderCache: ModelRenderCache) {
+            let renderCache = <Text2DRenderCache>modelRenderCache;
+            let engine = this.owner.engine;
+
+            renderCache.fontTexture = this.fontTexture;
+
+            let vb = new Float32Array(4);
+            for (let i = 0; i < 4; i++) {
+                vb[i] = i;
+            }
+            renderCache.vb = engine.createVertexBuffer(vb);
+
+            let ib = new Float32Array(6);
+            ib[0] = 0;
+            ib[1] = 2;
+            ib[2] = 1;
+            ib[3] = 0;
+            ib[4] = 3;
+            ib[5] = 2;
+
+            renderCache.ib = engine.createIndexBuffer(ib);
+
+            // Effects
+            let ei = this.getDataPartEffectInfo(Text2D.TEXT2D_MAINPARTID, ["index"]);
+            renderCache.effect = engine.createEffect("text2d", ei.attributes, ei.uniforms, ["diffuseSampler"], ei.defines, null, e => {
+//                renderCache.setupUniformsLocation(e, ei.uniforms, Text2D.TEXT2D_MAINPARTID);
+            });
+
+            return renderCache;
+        }
+
+        protected createInstanceDataParts(): InstanceDataBase[] {
+            return [new Text2DInstanceData(Text2D.TEXT2D_MAINPARTID, this._charCount)];
+        }
+
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
+                return false;
+            }
+
+            if (part.id === Text2D.TEXT2D_MAINPARTID) {
+                let d = <Text2DInstanceData>part;
+                let texture = this.fontTexture;
+                let ts = texture.getSize();
+
+                let offset = Vector2.Zero();
+                let charxpos = 0;
+                d.curElement = 0;
+                for (let char of this.text) {
+
+                    // Line feed
+                    if (char === "\n") {
+                        offset.x = 0;
+                        offset.y += texture.lineHeight;
+                    }
+
+                    // Tabulation ?
+                    if (char === "\t") {
+                        let nextPos = charxpos + this._tabulationSize;
+                        nextPos = nextPos - (nextPos % this._tabulationSize);
+
+                        offset.x += (nextPos - charxpos) * texture.spaceWidth;
+                        charxpos = nextPos;
+                        continue;
+                    }
+
+                    if (char < " ") {
+                        continue;
+                    }
+
+                    this.updateInstanceDataPart(d, offset);
+
+                    let ci = texture.getChar(char);
+                    offset.x += ci.charWidth;
+
+                    d.topLeftUV = ci.topLeftUV;
+                    let suv = ci.bottomRightUV.subtract(ci.topLeftUV);
+                    d.sizeUV = suv;
+                    d.textureSize = new Vector2(ts.width, ts.height);
+                    d.color = this.defaultFontColor;
+
+                    ++d.curElement;
+                }
+            }
+            return true;
+        }
+
+        private _updateCharCount() {
+            let count = 0;
+            for (let char of this._text) {
+                if (char === "\r" || char === "\n" || char === "\t" || char < " ") {
+                    continue;
+                }
+                ++count;
+            }
+            this._charCount = count;
+        }
+
+        private _fontTexture: FontTexture;
+        private _tabulationSize: number;
+        private _charCount: number;
+        private _fontName: string;
+        private _defaultFontColor: Color4;
+        private _text: string;
+        private _areaSize: Size;
+        private _actualAreaSize: Size;
+        private _vAlign: number;
+        private _hAlign: number;
+    }
+
+
+}

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

@@ -0,0 +1,11 @@
+module BABYLON {
+    export class WorldSpaceCanvas2d extends Mesh {
+        constructor(name: string, scene: Scene, canvas: Canvas2D) {
+            super(name, scene);
+
+            this._canvas = canvas;
+        }
+        private _canvas: Canvas2D;
+
+    }
+}

+ 4 - 0
src/Materials/Textures/babylon.baseTexture.ts

@@ -33,6 +33,10 @@
         @serialize()
         public isRenderTarget = false;
 
+        public toString(): string {
+            return this.name;
+        }
+
         public animations = new Array<Animation>();
 
         public onDispose: () => void;

+ 289 - 0
src/Materials/Textures/babylon.fontTexture.ts

@@ -0,0 +1,289 @@
+module BABYLON {
+
+    /**
+     * This class given information about a given character.
+     */
+    export class CharInfo {
+        /**
+         * The normalized ([0;1]) top/left position of the character in the texture
+         */
+        topLeftUV: Vector2;
+
+        /**
+         * The normalized ([0;1]) right/bottom position of the character in the texture
+         */
+        bottomRightUV: Vector2;
+
+        charWidth: number;
+    }
+
+    interface ICharInfoMap {
+        [char: string]: CharInfo;
+    }
+
+    export class FontTexture extends Texture {
+        private _canvas: HTMLCanvasElement;
+        private _context: CanvasRenderingContext2D;
+        private _lineHeight: number;
+        private _offset: number;
+        private _currentFreePosition: Vector2;
+        private _charInfos: ICharInfoMap = {};
+        private _curCharCount = 0;
+        private _lastUpdateCharCount = -1;
+        private _spaceWidth;
+        private _usedCounter = 1;
+
+        public get spaceWidth(): number {
+            return this._spaceWidth;
+        }
+
+        public get lineHeight(): number {
+            return this._lineHeight;
+        }
+
+        public static GetCachedFontTexture(scene: Scene, fontName: string) {
+            let s = <any>scene;
+            if (!s.__fontTextureCache__) {
+                s.__fontTextureCache__ = new StringDictionary<FontTexture>();
+            }
+
+            let dic = <StringDictionary<FontTexture>>s.__fontTextureCache__;
+
+            let lfn = fontName.toLocaleLowerCase();
+            let ft = dic.get(lfn);
+            if (ft) {
+                ++ft._usedCounter;
+                return ft;
+            }
+
+            ft = new FontTexture(null, lfn, scene);
+            dic.add(lfn, ft);
+
+            return ft;
+        }
+
+        public static ReleaseCachedFontTexture(scene: Scene, fontName: string) {
+            let s = <any>scene;
+            let dic = <StringDictionary<FontTexture>>s.__fontTextureCache__;
+            if (!dic) {
+                return;
+            }
+
+            let lfn = fontName.toLocaleLowerCase();
+            var font = dic.get(lfn);
+            if (--font._usedCounter === 0) {
+                dic.remove(lfn);
+                font.dispose();
+            }
+        }
+
+        /**
+         * Create a new instance of the FontTexture class
+         * @param name the name of the texture
+         * @param font the font to use, use the W3C CSS notation
+         * @param scene the scene that owns the texture
+         * @param maxCharCount the approximative maximum count of characters that could fit in the texture. This is an approximation because most of the fonts are proportional (each char has its own Width). The 'W' character's width is used to compute the size of the texture based on the given maxCharCount
+         * @param samplingMode the texture sampling mode
+         */
+        constructor(name: string, font: string, scene: Scene, maxCharCount=200, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE) {
+            super(null, scene, true, false, samplingMode);
+
+            this.name = name;
+
+            this.wrapU = Texture.CLAMP_ADDRESSMODE;
+            this.wrapV = Texture.CLAMP_ADDRESSMODE;
+
+            // First canvas creation to determine the size of the texture to create
+            this._canvas = document.createElement("canvas");
+            this._context = this._canvas.getContext("2d");
+            this._context.font = font;
+            this._context.fillStyle = "white";
+
+            var res = this.getFontHeight(font);
+            this._lineHeight = res.height;
+            this._offset = res.offset-1;
+
+            var maxCharWidth = this._context.measureText("W").width;
+            this._spaceWidth = this._context.measureText(" ").width;
+
+            // This is an approximate size, but should always be able to fit at least the maxCharCount
+            var totalEstSurface = this._lineHeight * maxCharWidth * maxCharCount;
+            var edge = Math.sqrt(totalEstSurface);
+            var textSize = Math.pow(2, Math.ceil(Math.log(edge) / Math.log(2)));
+
+            // Create the texture that will store the font characters
+            this._texture = scene.getEngine().createDynamicTexture(textSize, textSize, false, samplingMode);
+            var textureSize = this.getSize();
+
+            // Recreate a new canvas with the final size: the one matching the texture (resizing the previous one doesn't work as one would expect...)
+            this._canvas = document.createElement("canvas");
+            this._canvas.width = textureSize.width;
+            this._canvas.height = textureSize.height;
+            this._context = this._canvas.getContext("2d");
+            this._context.textBaseline = "top";
+            this._context.font = font;
+            this._context.fillStyle = "white";
+            this._context.imageSmoothingEnabled = false;
+
+            this._currentFreePosition = Vector2.Zero();
+
+            // Add the basic ASCII based characters
+            for (let i = 0x20; i < 0x7F; i++) {
+                var c = String.fromCharCode(i);
+                this.getChar(c);
+            }
+            this.update();
+        }
+
+        /**
+         * Make sure the given char is present in the font map.
+         * @param char the character to get or add
+         * @return the CharInfo instance corresponding to the given character
+         */
+        public getChar(char: string): CharInfo {
+            if (char.length !== 1) {
+                return null;
+            }
+
+            var info = this._charInfos[char];
+            if (info) {
+                return info;
+            }
+
+            info = new CharInfo();
+
+            var measure = this._context.measureText(char);
+            var textureSize = this.getSize();
+
+            // we reached the end of the current line?
+            var xMargin = 2;
+            var yMargin = 2;
+            let width = measure.width;
+            if (this._currentFreePosition.x + width + xMargin > textureSize.width) {
+                this._currentFreePosition.x = 0;
+                this._currentFreePosition.y += this._lineHeight + yMargin;      // +2 for safety marging
+
+                // No more room?
+                if (this._currentFreePosition.y > textureSize.height) {
+                    return this.getChar("!");
+                }
+            }
+
+            // Draw the character in the texture
+            this._context.fillText(char, this._currentFreePosition.x - 0.5, this._currentFreePosition.y - this._offset - 0.5);
+
+            // Fill the CharInfo object
+            info.topLeftUV = new Vector2(this._currentFreePosition.x / textureSize.width, this._currentFreePosition.y / textureSize.height);
+            info.bottomRightUV = new Vector2(info.topLeftUV.x + (width / textureSize.width), info.topLeftUV.y + ((this._lineHeight + 1) / textureSize.height));
+            info.charWidth = width;
+
+            // Add the info structure
+            this._charInfos[char] = info;
+            this._curCharCount++;
+
+            // Set the next position
+            this._currentFreePosition.x += width + xMargin;
+
+            return info;
+        }
+
+        public measureText(text: string, tabulationSize: number = 4): Size {
+            let maxWidth: number = 0;
+            let curWidth: number = 0;
+            let lineCount = 1;
+            let charxpos: number = 0;
+
+            // Parse each char of the string
+            for (var char of text) {
+
+                // Next line feed?
+                if (char === "\n") {
+                    maxWidth = Math.max(maxWidth, curWidth);
+                    charxpos = 0;
+                    curWidth = 0;
+                    ++lineCount;
+                    continue;
+                }
+
+                // Tabulation ?
+                if (char === "\t") {
+                    let nextPos = charxpos + tabulationSize;
+                    nextPos = nextPos - (nextPos % tabulationSize);
+
+                    curWidth += (nextPos - charxpos) * this.spaceWidth;
+                    charxpos = nextPos;
+                    continue;
+                }
+
+                if (char < " ") {
+                    continue;
+                }
+
+                curWidth += this.getChar(char).charWidth;
+                ++charxpos;
+            }
+            maxWidth = Math.max(maxWidth, curWidth);
+
+            return new Size(maxWidth, lineCount * this._lineHeight);
+        }
+
+        // More info here: https://videlais.com/2014/03/16/the-many-and-varied-problems-with-measuring-font-height-for-html5-canvas/
+        private getFontHeight(font: string): {height: number, offset: number} {
+            var fontDraw = document.createElement("canvas");
+            var ctx = fontDraw.getContext('2d');
+            ctx.fillRect(0, 0, fontDraw.width, fontDraw.height);
+            ctx.textBaseline = 'top';
+            ctx.fillStyle = 'white';
+            ctx.font = font;
+            ctx.fillText('jH|', 0, 0);
+            var pixels = ctx.getImageData(0, 0, fontDraw.width, fontDraw.height).data;
+            var start = -1;
+            var end = -1;
+            for (var row = 0; row < fontDraw.height; row++) {
+                for (var column = 0; column < fontDraw.width; column++) {
+                    var index = (row * fontDraw.width + column) * 4;
+                    if (pixels[index] === 0) {
+                        if (column === fontDraw.width - 1 && start !== -1) {
+                            end = row;
+                            row = fontDraw.height;
+                            break;
+                        }
+                        continue;
+                    }
+                    else {
+                        if (start === -1) {
+                            start = row;
+                        }
+                        break;
+                    }
+                }
+            }
+            return { height: end - start, offset: start-1}
+        }
+
+        public get canRescale(): boolean {
+            return false;
+        }
+
+        public getContext(): CanvasRenderingContext2D {
+            return this._context;
+        }
+
+        /**
+         * Call this method when you've call getChar() at least one time, this will update the texture if needed.
+         * Don't be afraid to call it, if no new character was added, this method simply does nothing.
+         */
+        public update(): void {
+            // Update only if there's new char added since the previous update
+            if (this._lastUpdateCharCount < this._curCharCount) {
+                this.getScene().getEngine().updateDynamicTexture(this._texture, this._canvas, false, true);
+                this._lastUpdateCharCount = this._curCharCount;
+            }
+        }
+
+        // cloning should be prohibited, there's no point to duplicate this texture at all
+        public clone(): FontTexture {
+            return null;
+        }
+    }
+} 

+ 95 - 0
src/Materials/Textures/babylon.mapTexture.js

@@ -0,0 +1,95 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var BABYLON;
+(function (BABYLON) {
+    var MapTexture = (function (_super) {
+        __extends(MapTexture, _super);
+        function MapTexture(name, scene, size, samplingMode) {
+            if (samplingMode === void 0) { samplingMode = BABYLON.Texture.TRILINEAR_SAMPLINGMODE; }
+            _super.call(this, null, scene, true, false, samplingMode);
+            this.name = name;
+            this._size = size;
+            this.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE;
+            this.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE;
+            // Create the rectPackMap that will allocate portion of the texture
+            this._rectPackingMap = new BABYLON.RectPackingMap(new BABYLON.Size(size.width, size.height));
+            // Create the texture that will store the content
+            this._texture = scene.getEngine().createRenderTargetTexture(size, { generateMipMaps: !this.noMipmap, type: BABYLON.Engine.TEXTURETYPE_UNSIGNED_INT });
+        }
+        /**
+         * Allocate a rectangle of a given size in the texture map
+         * @param size the size of the rectangle to allocation
+         * @return the PackedRect instance corresponding to the allocated rect or null is there was not enough space to allocate it.
+         */
+        MapTexture.prototype.allocateRect = function (size) {
+            return this._rectPackingMap.addRect(size);
+        };
+        /**
+         * Free a given rectangle from the texture map
+         * @param rectInfo the instance corresponding to the rect to free.
+         */
+        MapTexture.prototype.freeRect = function (rectInfo) {
+            if (rectInfo) {
+                rectInfo.freeContent();
+            }
+        };
+        Object.defineProperty(MapTexture.prototype, "freeSpace", {
+            /**
+             * Return the avaible 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 {}
+             */
+            get: function () {
+                return this._rectPackingMap.freeSpace;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+         * Bind the texture to the rendering engine to render in the zone of a given rectangle.
+         * 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
+         */
+        MapTexture.prototype.bindTextureForRect = function (rect) {
+            var engine = this.getScene().getEngine();
+            engine.bindFramebuffer(this._texture);
+            this._replacedViewport = engine.setDirectViewport(rect.pos.x, rect.pos.y, rect.contentSize.width, rect.contentSize.height);
+        };
+        /**
+         * Unbind the texture map from the rendering engine.
+         * Call this method when you're done rendering. A previous call to bindTextureForRect has to be made.
+         * @param dumpForDebug if set to true the content of the texture map will be dumped to a picture file that will be sent to the internet browser.
+         */
+        MapTexture.prototype.unbindTexture = function (dumpForDebug) {
+            // Dump ?
+            if (dumpForDebug) {
+                BABYLON.Tools.DumpFramebuffer(this._size.width, this._size.height, this.getScene().getEngine());
+            }
+            var engine = this.getScene().getEngine();
+            if (this._replacedViewport) {
+                engine.setViewport(this._replacedViewport);
+                this._replacedViewport = null;
+            }
+            engine.unBindFramebuffer(this._texture);
+        };
+        Object.defineProperty(MapTexture.prototype, "canRescale", {
+            get: function () {
+                return false;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        // Note, I don't know what behevior 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
+        MapTexture.prototype.clone = function () {
+            return null;
+        };
+        return MapTexture;
+    }(BABYLON.Texture));
+    BABYLON.MapTexture = MapTexture;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.mapTexture.js.map

+ 98 - 0
src/Materials/Textures/babylon.mapTexture.ts

@@ -0,0 +1,98 @@
+module BABYLON {
+
+    export class MapTexture extends Texture {
+        private _rectPackingMap: RectPackingMap;
+        private _size: ISize;
+
+        private _replacedViewport: Viewport;
+
+        constructor(name: string, scene: Scene, size: ISize, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE) {
+            super(null, scene, true, false, samplingMode);
+
+            this.name = name;
+            this._size = size;
+            this.wrapU = Texture.CLAMP_ADDRESSMODE;
+            this.wrapV = Texture.CLAMP_ADDRESSMODE;
+
+            // Create the rectPackMap that will allocate portion of the texture
+            this._rectPackingMap = new RectPackingMap(new Size(size.width, size.height));
+
+            // Create the texture that will store the content
+            this._texture = scene.getEngine().createRenderTargetTexture(size, { generateMipMaps: !this.noMipmap, type: Engine.TEXTURETYPE_UNSIGNED_INT });
+        }
+
+        /**
+         * Allocate a rectangle of a given size in the texture map
+         * @param size the size of the rectangle to allocation
+         * @return the PackedRect instance corresponding to the allocated rect or null is there was not enough space to allocate it.
+         */
+        public allocateRect(size: Size): PackedRect {
+            return this._rectPackingMap.addRect(size);
+        }
+
+        /**
+         * Free a given rectangle from the texture map
+         * @param rectInfo the instance corresponding to the rect to free.
+         */
+        public freeRect(rectInfo: PackedRect) {
+            if (rectInfo) {
+                rectInfo.freeContent();
+            }
+        }
+
+        /**
+         * Return the avaible 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 {} 
+         */
+        public get freeSpace(): number {
+            return this._rectPackingMap.freeSpace;
+        }
+
+        /**
+         * Bind the texture to the rendering engine to render in the zone of a given rectangle.
+         * 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
+         */
+        public bindTextureForRect(rect: PackedRect, 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);
+            if (clear) {
+                engine.clear(new Color4(0,0,0,0), true, true);
+            }
+        }
+
+        /**
+         * Unbind the texture map from the rendering engine.
+         * Call this method when you're done rendering. A previous call to bindTextureForRect has to be made.
+         * @param dumpForDebug if set to true the content of the texture map will be dumped to a picture file that will be sent to the internet browser.
+         */
+        public unbindTexture(dumpForDebug?:boolean) {
+            // Dump ?
+            if (dumpForDebug) {
+                Tools.DumpFramebuffer(this._size.width, this._size.height, this.getScene().getEngine());
+            }
+
+            let engine = this.getScene().getEngine();
+
+            if (this._replacedViewport) {
+                engine.setViewport(this._replacedViewport);
+                this._replacedViewport = null;
+            }
+
+            engine.unBindFramebuffer(this._texture);
+        }
+
+        public get canRescale(): boolean {
+            return false;
+        }
+
+        // Note, I don't know what behevior 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;
+        }
+    }
+} 

+ 5 - 0
src/Materials/Textures/babylon.texture.ts

@@ -44,6 +44,10 @@
         @serialize()
         public wAng = 0;
 
+        get noMipmap(): boolean {
+            return this._noMipmap;
+        }
+
         private _noMipmap: boolean;
         public _invertY: boolean;
         private _rowGenerationMatrix: Matrix;
@@ -126,6 +130,7 @@
                 return;
             }
 
+            this._samplingMode = samplingMode;
             this.getScene().getEngine().updateTextureSamplingMode(samplingMode, this._texture);
         }
 

+ 8 - 1
src/Math/babylon.math.ts

@@ -605,10 +605,17 @@
         }
 
         public static Transform(vector: Vector2, transformation: Matrix): Vector2 {
+            let r = Vector2.Zero();
+            Vector2.TransformToRef(vector, transformation, r);
+            return r;
+        }
+
+        public static TransformToRef(vector: Vector2, transformation: Matrix, result: Vector2) {
             var x = (vector.x * transformation.m[0]) + (vector.y * transformation.m[4]) + transformation.m[12];
             var y = (vector.x * transformation.m[1]) + (vector.y * transformation.m[5]) + transformation.m[13];
 
-            return new Vector2(x, y);
+            result.x = x;
+            result.y = y;
         }
 
         public static Distance(value1: Vector2, value2: Vector2): number {

+ 5 - 0
src/Shaders/rect2d.fragment.fx

@@ -0,0 +1,5 @@
+varying vec4 vColor;
+
+void main(void) {
+	gl_FragColor = vColor;
+}

+ 177 - 0
src/Shaders/rect2d.vertex.fx

@@ -0,0 +1,177 @@
+// based on if Instanced Array are supported or not, declare the field either as attribute or uniform
+#ifdef Instanced
+#define att attribute
+#else
+#define att uniform
+#endif
+
+attribute float index;
+att vec2 zBias;
+att vec4 transformX;
+att vec4 transformY;
+att vec2 origin;
+
+#ifdef Border
+att float borderThickness;
+#endif
+
+#ifdef FillSolid
+att vec4 fillSolidColor;
+#endif
+
+#ifdef BorderSolid
+att vec4 borderSolidColor;
+#endif
+
+#ifdef FillGradient
+att vec4 fillGradientColor1;
+att vec4 fillGradientColor2;
+att vec4 fillGradientTY;
+#endif
+
+#ifdef BorderGradient
+att vec4 borderGradientColor1;
+att vec4 borderGradientColor2;
+att vec4 borderGradientTY;
+#endif
+
+// xyzw are: width, height, roundRadius (0.0 for simple rectangle with four vertices)
+att vec3 properties;
+
+// First index is the center, then there's four sections of 16 subdivisions
+
+#define rsub0 17.0
+#define rsub1 33.0
+#define rsub2 49.0
+#define rsub3 65.0
+#define rsub 64.0
+#define TWOPI 6.28318530
+
+// Output
+varying vec2 vUV;
+varying vec4 vColor;
+
+void main(void) {
+
+	vec2 pos2;
+
+	// notRound case, only five vertices, 0 is center, then the 4 other for perimeter
+	if (properties.z == 0.0) {
+		if (index == 0.0) {
+			pos2 = vec2(0.5, 0.5);
+		} 
+		else if (index == 1.0) {
+			pos2 = vec2(1.0, 1.0);
+		} 
+		else if (index == 2.0) {
+			pos2 = vec2(1.0, 0.0);
+		}
+		else if (index == 3.0) {
+			pos2 = vec2(0.0, 0.0);
+		} 
+		else {
+			pos2 = vec2(0.0, 1.0);
+		}
+	}
+	else 
+	{
+#ifdef Border
+		float w = properties.x;
+		float h = properties.y;
+		float r = properties.z;
+		float nru = r / w;
+		float nrv = r / h;
+		vec2 borderOffset = vec2(1.0, 1.0);
+
+		float segi = index;
+		if (index < rsub) {
+			borderOffset = vec2(1.0-(borderThickness*2.0 / w), 1.0-(borderThickness*2.0 / h));
+		}
+		else {
+			segi -= rsub;
+		}
+
+		// right/bottom
+		if (segi < rsub0) {
+			pos2 = vec2(1.0 - nru, nrv);
+		}
+		// left/bottom
+		else if (segi < rsub1) {
+			pos2 = vec2(nru, nrv);
+		}
+		// left/top
+		else if (segi < rsub2) {
+			pos2 = vec2(nru, 1.0 - nrv);
+		}
+		// right/top
+		else {
+			pos2 = vec2(1.0 - nru, 1.0 - nrv);
+		}
+
+		float angle = TWOPI - ((index - 1.0) * TWOPI / (rsub - 0.5));
+		pos2.x += cos(angle) * nru;
+		pos2.y += sin(angle) * nrv;
+
+		pos2.x = ((pos2.x - 0.5) * borderOffset.x) + 0.5;
+		pos2.y = ((pos2.y - 0.5) * borderOffset.y) + 0.5;
+
+#else
+		if (index == 0.0) {
+			pos2 = vec2(0.5, 0.5);
+		}
+		else {
+			float w = properties.x;
+			float h = properties.y;
+			float r = properties.z;
+			float nru = r / w;
+			float nrv = r / h;
+
+			// right/bottom
+			if (index < rsub0) {
+				pos2 = vec2(1.0 - nru, nrv);
+			}
+			// left/bottom
+			else if (index < rsub1) {
+				pos2 = vec2(nru, nrv);
+			}
+			// left/top
+			else if (index < rsub2) {
+				pos2 = vec2(nru, 1.0 - nrv);
+			}
+			// right/top
+			else {
+				pos2 = vec2(1.0 - nru, 1.0 - nrv);
+			}
+
+			float angle = TWOPI - ((index - 1.0) * TWOPI / (rsub - 0.5));
+			pos2.x += cos(angle) * nru;
+			pos2.y += sin(angle) * nrv;
+		}
+#endif
+	}
+
+#ifdef FillSolid
+	vColor = fillSolidColor;
+#endif
+
+#ifdef BorderSolid
+	vColor = borderSolidColor;
+#endif
+
+#ifdef FillGradient
+	float v = dot(vec4(pos2.xy, 1, 1), fillGradientTY);
+	vColor = mix(fillGradientColor2, fillGradientColor1, v);	// As Y is inverted, Color2 first, then Color1
+#endif
+
+#ifdef BorderGradient
+	float v = dot(vec4(pos2.xy, 1, 1), borderGradientTY);
+	vColor = mix(borderGradientColor2, borderGradientColor1, v);	// As Y is inverted, Color2 first, then Color1
+#endif
+
+	vec4 pos;
+	pos.xy = (pos2.xy - origin) * properties.xy;
+	pos.z = 1.0;
+	pos.w = 1.0;
+	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, zBias.y);
+
+}

+ 10 - 0
src/Shaders/sprite2d.fragment.fx

@@ -0,0 +1,10 @@
+varying vec2 vUV;
+uniform sampler2D diffuseSampler;
+
+void main(void) {
+	vec4 color = texture2D(diffuseSampler, vUV);
+	if (color.w == 0.0) {
+		discard;
+	}
+	gl_FragColor = color;
+}

+ 67 - 0
src/Shaders/sprite2d.vertex.fx

@@ -0,0 +1,67 @@
+// based on if Instanced Array are supported or not, declare the field either as attribute or uniform
+#ifdef Instanced
+#define att attribute
+#else
+#define att uniform
+#endif
+
+// Attributes
+attribute float index;
+
+att vec2 topLeftUV;
+att vec2 sizeUV;
+att vec2 origin;
+att vec2 textureSize;
+att float frame;
+att float invertY;
+att vec2 zBias;
+att vec4 transformX;
+att vec4 transformY;
+
+// Uniforms
+
+// Output
+varying vec2 vUV;
+varying vec4 vColor;
+
+void main(void) {
+
+	vec2 pos2;
+
+	//vec2 off = vec2(1.0 / textureSize.x, 1.0 / textureSize.y);
+	vec2 off = vec2(0.0, 0.0);
+
+	// Left/Top
+	if (index == 0.0) {
+		pos2 = vec2(0.0, 0.0);
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x) + off.x, topLeftUV.y - off.y);
+	}
+
+	// Left/Bottom
+	else if (index == 1.0) {
+		pos2 = vec2(0.0,  1.0);
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x) + off.x, (topLeftUV.y + sizeUV.y));
+	}
+
+	// Right/Bottom
+	else if (index == 2.0) {
+		pos2 = vec2( 1.0,  1.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), (topLeftUV.y + sizeUV.y));
+	}
+
+	// Right/Top
+	else if (index == 3.0) {
+		pos2 = vec2( 1.0, 0.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), topLeftUV.y - off.y);
+	}
+
+	if (invertY == 1.0) {
+		vUV.y = 1.0 - vUV.y;
+	}
+
+	vec4 pos;
+	pos.xy = (pos2.xy - origin) * sizeUV * textureSize;
+	pos.z = 1.0;
+	pos.w = 1.0;
+	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, zBias.y);
+}	

+ 10 - 0
src/Shaders/text2d.fragment.fx

@@ -0,0 +1,10 @@
+varying vec4 vColor;
+varying vec2 vUV;
+
+// Samplers
+uniform sampler2D diffuseSampler;
+
+void main(void) {
+	vec4 color = texture2D(diffuseSampler, vUV);
+	gl_FragColor = color*vColor;
+}

+ 59 - 0
src/Shaders/text2d.vertex.fx

@@ -0,0 +1,59 @@
+// based on if Instanced Array are supported or not, declare the field either as attribute or uniform
+#ifdef Instanced
+#define att attribute
+#else
+#define att uniform
+#endif
+
+// Attributes
+attribute float index;
+att vec2 zBias;
+
+att vec4 transformX;
+att vec4 transformY;
+
+att vec2 topLeftUV;
+att vec2 sizeUV;
+att vec2 origin;
+att vec2 textureSize;
+att vec4 color;
+
+// Output
+varying vec2 vUV;
+varying vec4 vColor;
+
+void main(void) {
+
+	vec2 pos2;
+
+	// Bottom/Left
+	if (index == 0.0) {
+		pos2 = vec2(0.0, 0.0);
+		vUV = vec2(topLeftUV.x, topLeftUV.y + sizeUV.y);
+	}
+
+	// Top/Left
+	else if (index == 1.0) {
+		pos2 = vec2(0.0, 1.0);
+		vUV = vec2(topLeftUV.x, topLeftUV.y);
+	}
+	
+	// Top/Right
+	else if (index == 2.0) {
+		pos2 = vec2(1.0, 1.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x, topLeftUV.y);
+	}
+
+	// Bottom/Right
+	else if (index == 3.0) {
+		pos2 = vec2(1.0, 0.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x, topLeftUV.y + sizeUV.y);
+	}
+
+	vColor = color;
+	vec4 pos;
+	pos.xy = (pos2.xy - origin) * sizeUV * textureSize;
+	pos.z = 1.0;
+	pos.w = 1.0;
+	gl_Position = vec4(dot(pos, transformX), dot(pos, transformY), zBias.x, zBias.y);
+}

+ 12 - 3
src/Tools/babylon.stringDictionary.ts

@@ -47,9 +47,9 @@
          * @return the value corresponding to the key
          */
         public getOrAdd(key: string, val: T): T {
-            var val = this.get(key);
-            if (val !== undefined) {
-                return val;
+            var curVal = this.get(key);
+            if (curVal !== undefined) {
+                return curVal;
             }
 
             this.add(key, val);
@@ -80,6 +80,15 @@
             return true;
         }
 
+
+        public set(key: string, value: T): boolean {
+            if (this._data[key] === undefined) {
+                return false;
+            }
+            this._data[key] = value;
+            return true;
+        }
+
         /**
          * Remove a key/value from the dictionary.
          * @param key the key to remove

+ 40 - 6
src/Tools/babylon.tools.ts

@@ -693,12 +693,6 @@
             return false;
         }
 
-        public static getClassName(obj): string {
-            var funcNameRegex = /function (.{1,})\(/;
-            var results = (funcNameRegex).exec((obj).constructor.toString());
-            return (results && results.length > 1) ? results[1] : "";
-        }
-
         // Logs
         private static _NoneLogLevel = 0;
         private static _MessageLogLevel = 1;
@@ -907,6 +901,46 @@
 
             return new Date().getTime();
         }
+
+        /**
+         * This method will return the name of the class used to create the instance of the given object.
+         * It will works only on Javascript basic data types (number, string, ...) and instance of class declared with the @className decorator.
+         * @param object the object to get the class name from
+         * @return the name of the class, will be "object" for a custom data type not using the @className decorator
+         */
+        public static getClassName(object, isType: boolean = false): string {
+            let name = null;
+            if (object instanceof Object) {
+                let classObj = isType ? object :  Object.getPrototypeOf(object);
+                name = classObj.constructor["__bjsclassName__"];
+            }
+            if (!name) {
+                name = typeof object;
+            }
+
+            return name;
+        }
+
+        public static first<T>(array: Array<T>, predicate: (item) => boolean) {
+            for (let el of array) {
+                if (predicate(el)) {
+                    return el;
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Use this className as a decorator on a given class definition to add it a name.
+     * You can then use the Tools.getClassName(obj) on an instance to retrieve its class name.
+     * This method is the only way to get it done in all cases, even if the .js file declaring the class is minified
+     * @param name
+     */
+    export function className(name: string): (target: Object) => void {
+        return (target: Object) => {
+            target["__bjsclassName__"] = name;
+        }
     }
 
     /**

+ 90 - 15
src/babylon.engine.ts

@@ -123,6 +123,39 @@
         }
     };
 
+    export class InstancingAttributeInfo {
+        /**
+         * Index/offset of the attribute in the vertex shader
+         */
+        index: number;
+
+        /**
+         * size of the attribute, 1, 2, 3 or 4
+         */
+        attributeSize: number;
+
+        /**
+         * type of the attribute, gl.BYTE, gl.UNSIGNED_BYTE, gl.SHORT, gl.UNSIGNED_SHORT, gl.FIXED, gl.FLOAT.
+         * default is FLOAT
+         */
+        attribyteType: number;
+
+        /**
+         * normalization of fixed-point data. behavior unclear, use FALSE, default is FALSE
+         */
+        normalized: boolean;
+
+        /**
+         * Offset of the data in the Vertex Buffer acting as the instancing buffer
+         */
+        offset: number;
+
+        /**
+         * Name of the GLSL attribute, for debugging purpose only
+         */
+        attributeName: string;
+    }
+
     export class EngineCapabilities {
         public maxTexturesImageUnits: number;
         public maxTextureSize: number;
@@ -559,6 +592,14 @@
             this._drawCalls = 0;
         }
 
+        public getDepthFunction(): number {
+            return this._depthCullingState.depthFunc;
+        }
+
+        public setDepthFunction(depthFunc: number) {
+            this._depthCullingState.depthFunc = depthFunc;
+        }
+
         public setDepthFunctionToGreater(): void {
             this._depthCullingState.depthFunc = this._gl.GREATER;
         }
@@ -695,10 +736,18 @@
             this._gl.viewport(x * width, y * height, width * viewport.width, height * viewport.height);
         }
 
-        public setDirectViewport(x: number, y: number, width: number, height: number): void {
+        /**
+         * Directly set the WebGL Viewport
+         * The x, y, width & height are directly passed to the WebGL call
+         * @return the current viewport Object (if any) that is being replaced by this call. You can restore this viewport later on to go back to the original state.
+         */
+        public setDirectViewport(x: number, y: number, width: number, height: number): Viewport {
+            let currentViewport = this._cachedViewport;
             this._cachedViewport = null;
 
             this._gl.viewport(x, y, width, height);
+
+            return currentViewport;
         }
 
         public beginFrame(): void {
@@ -960,25 +1009,48 @@
             this._gl.deleteBuffer(buffer);
         }
 
-
-        public updateAndBindInstancesBuffer(instancesBuffer: WebGLBuffer, data: Float32Array, offsetLocations: number[]): void {
+        public updateAndBindInstancesBuffer(instancesBuffer: WebGLBuffer, data: Float32Array, offsetLocations: number[] | InstancingAttributeInfo[]): void {
             this._gl.bindBuffer(this._gl.ARRAY_BUFFER, instancesBuffer);
-            this._gl.bufferSubData(this._gl.ARRAY_BUFFER, 0, data);
+            if (data) {
+                this._gl.bufferSubData(this._gl.ARRAY_BUFFER, 0, data);
+            }
 
-            for (var index = 0; index < 4; index++) {
-                var offsetLocation = offsetLocations[index];
-                this._gl.enableVertexAttribArray(offsetLocation);
-                this._gl.vertexAttribPointer(offsetLocation, 4, this._gl.FLOAT, false, 64, index * 16);
-                this._caps.instancedArrays.vertexAttribDivisorANGLE(offsetLocation, 1);
+            if ((<any>offsetLocations[0]).index !== undefined) {
+                let stride = 0;
+                for (let i = 0; i < offsetLocations.length; i++) {
+                    let ai = <InstancingAttributeInfo>offsetLocations[i];
+                    stride += ai.attributeSize * 4;
+                }
+                for (let i = 0; i < offsetLocations.length; i++) {
+                    let ai = <InstancingAttributeInfo>offsetLocations[i];
+                    this._gl.enableVertexAttribArray(ai.index);
+                    this._gl.vertexAttribPointer(ai.index, ai.attributeSize, ai.attribyteType || this._gl.FLOAT, ai.normalized || false, stride, ai.offset);
+                    this._caps.instancedArrays.vertexAttribDivisorANGLE(ai.index, 1);
+                }
+            } else {
+                for (let index = 0; index < 4; index++) {
+                    let offsetLocation = <number>offsetLocations[index];
+                    this._gl.enableVertexAttribArray(offsetLocation);
+                    this._gl.vertexAttribPointer(offsetLocation, 4, this._gl.FLOAT, false, 64, index * 16);
+                    this._caps.instancedArrays.vertexAttribDivisorANGLE(offsetLocation, 1);
+                }
             }
         }
 
-        public unBindInstancesBuffer(instancesBuffer: WebGLBuffer, offsetLocations: number[]): void {
+        public unBindInstancesBuffer(instancesBuffer: WebGLBuffer, offsetLocations: number[] | InstancingAttributeInfo[]): void {
             this._gl.bindBuffer(this._gl.ARRAY_BUFFER, instancesBuffer);
-            for (var index = 0; index < 4; index++) {
-                var offsetLocation = offsetLocations[index];
-                this._gl.disableVertexAttribArray(offsetLocation);
-                this._caps.instancedArrays.vertexAttribDivisorANGLE(offsetLocation, 0);
+            if ((<any>offsetLocations[0]).index !== undefined) {
+                for (let i = 0; i < offsetLocations.length; i++) {
+                    let ai = <InstancingAttributeInfo>offsetLocations[i];
+                    this._gl.disableVertexAttribArray(ai.index);
+                    this._caps.instancedArrays.vertexAttribDivisorANGLE(ai.index, 0);
+                }
+            } else {
+                for (let index = 0; index < 4; index++) {
+                    let offsetLocation = <number>offsetLocations[index];
+                    this._gl.disableVertexAttribArray(offsetLocation);
+                    this._caps.instancedArrays.vertexAttribDivisorANGLE(offsetLocation, 0);
+                }
             }
         }
 
@@ -1618,9 +1690,12 @@
             }
         }
 
-        public updateDynamicTexture(texture: WebGLTexture, canvas: HTMLCanvasElement, invertY: boolean): void {
+        public updateDynamicTexture(texture: WebGLTexture, canvas: HTMLCanvasElement, invertY: boolean, premulAlpha: boolean = false): void {
             this._gl.bindTexture(this._gl.TEXTURE_2D, texture);
             this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, invertY ? 1 : 0);
+            if (premulAlpha) {
+                this._gl.pixelStorei(this._gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
+            }
             this._gl.texImage2D(this._gl.TEXTURE_2D, 0, this._gl.RGBA, this._gl.RGBA, this._gl.UNSIGNED_BYTE, canvas);
             if (texture.generateMipMaps) {
                 this._gl.generateMipmap(this._gl.TEXTURE_2D);