Browse Source

First commit of the Canvas2D feature. Still alpha, but working on a basic scenario.

nockawa 9 năm trước cách đây
mục cha
commit
a093c90189

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

@@ -0,0 +1,84 @@
+module BABYLON {
+    export class BoundingInfo2D {
+        public radius: number;
+        public extent: Size;
+
+        constructor() {
+            this.extent = new Size(0, 0);
+        }
+
+        public clone(): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            r.radius = this.radius;
+            r.extent = this.extent.clone();
+            return r;
+        }
+
+        public transform(matrix: Matrix): BoundingInfo2D {
+            var r = new BoundingInfo2D();
+            this.transformToRef(matrix, r);
+            return r;
+        }
+
+        public union(other: BoundingInfo2D): BoundingInfo2D {
+            var r = new BoundingInfo2D();
+            this.unionToRef(other, r);
+            return r;
+        }
+
+        public transformToRef(matrix: Matrix, result: BoundingInfo2D) {
+            // Extract scale from matrix
+            let xs = MathTools.Sign(matrix.m[0] * matrix.m[1] * matrix.m[2] * matrix.m[3]) < 0 ? -1 : 1;
+            let ys = MathTools.Sign(matrix.m[4] * matrix.m[5] * matrix.m[6] * matrix.m[7]) < 0 ? -1 : 1;
+            let scaleX = xs * Math.sqrt(matrix.m[0] * matrix.m[0] + matrix.m[1] * matrix.m[1] + matrix.m[2] * matrix.m[2]);
+            let scaleY = ys * Math.sqrt(matrix.m[4] * matrix.m[4] + matrix.m[5] * matrix.m[5] + matrix.m[6] * matrix.m[6]);
+
+            // Get translation
+            let trans = matrix.getTranslation();
+            let transLength = trans.length();
+
+            if (transLength < Epsilon) {
+                result.radius = this.radius * Math.max(scaleX, scaleY);
+            } else {
+                // Compute the radius vector by applying the transformation matrix manually
+                let rx = (trans.x / transLength) * (transLength + this.radius) * scaleX;
+                let 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
+            let p = new Array<Vector2>(4);
+            p[0] = new Vector2(this.extent.width, this.extent.height);
+            p[1] = new Vector2(this.extent.width, -this.extent.height);
+            p[2] = new Vector2(-this.extent.width, -this.extent.height);
+            p[3] = new Vector2(-this.extent.width, this.extent.height);
+
+            // Transform the four points of the bounding box with the matrix
+            for (let i = 0; i < 4; i++) {
+                p[i] = Vector2.Transform(p[i], matrix);
+            }
+
+            // Take the first point as reference
+            let 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 (let 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;
+        }
+
+        public unionToRef(other: BoundingInfo2D, result: BoundingInfo2D) {
+            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);
+        }
+
+    }
+}

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

@@ -0,0 +1,92 @@
+module BABYLON {
+    export interface IBorder2D extends ILockable {
+        toString(): string;
+    }
+
+    export interface IFill2D extends ILockable {
+        toString(): string;
+    }
+
+    export interface ILockable {
+        isLocked(): boolean;
+        lock();
+    }
+
+    export class LockableBase implements ILockable {
+        isLocked(): boolean {
+            return this._isLocked;
+        }
+
+        private _isLocked: boolean;
+
+        lock() {
+            if (this._isLocked) {
+                return;
+            }
+
+            this.onLock();
+            this._isLocked = true;
+        }
+
+        protected onLock() {
+
+        }
+    }
+
+    export class SolidColorBorder2D extends LockableBase implements IBorder2D {
+        constructor(color: Color4, lock: boolean = false) {
+            super();
+            this._color = color;
+            if (lock) {
+                this.lock();
+            }
+        }
+
+        public get color(): Color4 {
+            return this._color;
+        }
+
+        public set color(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color = value;
+        }
+
+        public toString(): string {
+            return this._color.toHexString();
+        }
+        private _color: Color4;
+    }
+
+    export class SolidColorFill2D extends LockableBase implements IFill2D {
+        constructor(color: Color4, lock: boolean = false) {
+            super();
+            this._color = color;
+            if (lock) {
+                this.lock();
+            }
+        }
+
+        public get color(): Color4 {
+            return this._color;
+        }
+
+        public set color(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color = value;
+        }
+
+        public toString(): string {
+            return this._color.toHexString();
+        }
+
+        private _color: Color4;
+    }
+
+
+}

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

@@ -0,0 +1,249 @@
+module BABYLON {
+    class GroupsCacheMap {
+        constructor() {
+            this.groupSprites = new Array<{ group: Group2D, sprite: Sprite2D }>();
+        }
+        texture: MapTexture;
+        groupSprites: Array<{ group: Group2D, sprite: Sprite2D }>;
+    }
+
+    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.
+         * @param engine
+         * @param name
+         * @param transform
+         * @param size
+         * @param cachingStrategy
+         */
+        static CreateWorldSpace(scene: Scene, name: string, transform: Matrix, size: Size, cachingStrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS): Canvas2D {
+            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, size, false, cachingStrategy);
+            c._worldTransform = transform;
+
+            return c;
+        }
+
+        protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean = true, cachingstrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
+            this._cachingStrategy = cachingstrategy;
+            this._hierarchyLevelZFactor = 100;
+            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.levelVisible = false;
+            }
+            this._isScreeSpace = isScreenSpace;
+        }
+
+        public get scene(): Scene {
+            return this._scene;
+        }
+
+        public get engine(): Engine {
+            return this._engine;
+        }
+
+        public get cachingStrategy(): number {
+            return this._cachingStrategy;
+        }
+
+        public get backgroundFill(): IFill2D {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.fill;
+        }
+
+        public set backgroundFill(value: IFill2D) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.fill) {
+                return;
+            }
+
+            this._background.fill = value;
+            this._background.isVisible = true;
+        }
+
+        public get border(): IBorder2D {
+            if (!this._background || !this._background.isVisible) {
+                return null;
+            }
+            return this._background.border;
+        }
+
+        public set border(value: IBorder2D) {
+            this.checkBackgroundAvailability();
+
+            if (value === this._background.border) {
+                return;
+            }
+
+            this._background.border = 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");
+            }
+        }
+
+        public get hierarchySiblingZDelta(): number {
+            return this._hierarchySiblingZDelta;
+        }
+        private _mapCounter = 0;
+        private _background: Rectangle2D;
+        private _scene: Scene;
+        private _engine: Engine;
+        private _isScreeSpace: boolean;
+        private _worldTransform: Matrix;
+        private _cachingStrategy: number;
+        private _hierarchyLevelZFactor: number;
+        private _hierarchyLevelMaxSiblingCount: number;
+        private _hierarchySiblingZDelta: number;
+        private _groupCacheMaps: GroupsCacheMap[];
+        public _renderingSize: Size;
+
+        public render(camera: Camera) {
+            this._renderingSize.width = this.engine.getRenderWidth();
+            this._renderingSize.height = this.engine.getRenderHeight();
+
+            var context = new Render2DContext();
+            context.camera = camera;
+            context.parentVisibleState = this.levelVisible;
+            context.parentTransform = Matrix.Identity();
+            context.parentTransformStep = 1;
+            context.forceRefreshPrimitive = false;
+
+            this.updateGlobalTransVis(context, false);
+
+            this._prepareGroupRender(context);
+            this._groupRender(context);
+        }
+
+        public _allocateGroupCache(group: Group2D): { node: PackedRect, texture: MapTexture } {
+            // Determine size
+            let size = group.actualSize;
+            size = new Size(Math.ceil(size.width), Math.ceil(size.height));
+            if (!this._groupCacheMaps) {
+                this._groupCacheMaps = new Array<GroupsCacheMap>();
+            }
+
+            // Try to find a spot in one of the cached texture
+            let res = null;
+            for (var g of this._groupCacheMaps) {
+                let 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) {
+                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)));
+                }
+
+                g = new GroupsCacheMap();
+                let id = `groupsMapChache${this._mapCounter}forCanvas${this.id}`;
+                g.texture = new MapTexture(id, this._scene, mapSize);
+                this._groupCacheMaps.push(g);
+
+                let 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
+            let sprite = Sprite2D.Create(this, `__cachedSpriteOfGroup__${group.id}`, 10, 10, g.texture, res.node.contentSize, res.node.pos, true);
+            sprite.origin = Vector2.Zero();
+            g.groupSprites.push({ group: group, sprite: sprite });
+            return res;
+        }
+
+        private static _groupTextureCacheSize = 1024;
+
+        public static getSolidColorFill(color: Color4): IFill2D {
+            return Canvas2D._solidColorFills.getOrAddWithFactory(color.toHexString(), () => new SolidColorFill2D(color.clone(), true));
+        }
+
+        public static getSolidColorBorder(color: Color4): IBorder2D {
+            return Canvas2D._solidColorBorders.getOrAddWithFactory(color.toHexString(), () => new SolidColorBorder2D(color.clone(), true));
+        }
+
+        public static getSolidColorFillFromHex(hexValue: string): IFill2D {
+            return Canvas2D._solidColorFills.getOrAddWithFactory(hexValue, () => new SolidColorFill2D(Color4.FromHexString(hexValue), true));
+        }
+
+        public static getSolidColorBorderFromHex(hexValue: string): IBorder2D {
+            return Canvas2D._solidColorBorders.getOrAddWithFactory(hexValue, () => new SolidColorBorder2D(Color4.FromHexString(hexValue), true));
+        }
+
+        private static _solidColorFills: StringDictionary<IFill2D> = new StringDictionary<IFill2D>();
+        private static _solidColorBorders: StringDictionary<IBorder2D> = new StringDictionary<IBorder2D>();
+    }
+
+
+}

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

@@ -0,0 +1,365 @@
+module BABYLON {
+    export class Group2D extends Prim2DBase {
+        static GROUP2D_PROPCOUNT: number = Prim2DBase.PRIM2DBASE_PROPCOUNT + 10;
+
+        /**
+         * 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;
+        }
+
+        /**
+         * 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;
+        }
+
+        public static sizeProperty: Prim2DPropInfo;
+        public static actualSizeProperty: Prim2DPropInfo;
+
+
+        @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 size = this.boundingInfo.extent.clone();
+
+            return size;
+        }
+
+        public get cacheBehavior(): number {
+            return this._cacheBehavior;
+        }
+
+        _addPrimToDirtyList(prim: Prim2DBase) {
+            this._primDirtyList.push(prim);
+        }
+
+        protected updateLevelBoundingInfo() {
+            let size: Size;
+
+            // If the size is set by the user, the boundingInfo is compute from this value
+            if (this.size) {
+                size = this.size;
+            }
+            // Otherwise the group's level bouding info is "collapsed"
+            else {
+                size = new 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
+        protected _prepareGroupRender(context: Render2DContext) {
+
+            var childrenContext = this._buildChildContext(context);
+
+            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, 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
+                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(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((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(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(g => {
+                g._prepareGroupRender(childrenContext);
+            });
+        }
+
+        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)
+            var childrenContext = this._buildChildContext(context);
+            for (let childGroup of this._childrenRenderableGroups) {
+                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 {
+                    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) => {
+                    // If the instances of the model was changed, pack the data
+                    let instanceData = v._instancesData.pack();
+
+                    // Compute the size the instance buffer should have
+                    let 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
+                    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();
+                }
+            }
+        }
+
+        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;
+            }
+
+            let n = this._cacheNode;
+            this._cacheTexture.bindTextureForRect(n);
+        }
+
+        private _unbindCacheTarget() {
+            if (this._cacheTexture) {
+                this._cacheTexture.unbindTexture();
+            }
+        }
+
+        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 _viewportPosition: Vector2;
+        private _viewportSize: Size;
+
+        groupRenderInfo: StringDictionary<GroupInstanceInfo>;
+    }
+
+}

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

@@ -0,0 +1,62 @@
+module BABYLON {
+    export const enum ShaderDataType {
+        Vector2, Vector3, Vector4, Matrix, float, Color3, Color4
+    }
+
+    export class GroupInstanceInfo {
+        constructor(owner: Group2D, classTreeInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>, cache: ModelRenderCacheBase) {
+            this._owner = owner;
+            this._classTreeInfo = classTreeInfo;
+            this._modelCache = cache;
+        }
+
+        _owner: Group2D;
+        _classTreeInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>;
+        _modelCache: ModelRenderCacheBase;
+        _instancesData: DynamicFloatArray;
+        _dirtyInstancesData: boolean;
+        _instancesBuffer: WebGLBuffer;
+        _instancesBufferSize: number;
+    }
+
+    export class 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)
+         */
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            return true;
+        }
+    }
+
+    export class ModelRenderCache<TInstData> extends ModelRenderCacheBase {
+
+        constructor() {
+            super();
+            this._nextKey = 1;
+            this._instancesData = new StringDictionary<TInstData>();
+        }
+
+        addInstanceData(data: TInstData): 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);
+        }
+
+        _instancesData: StringDictionary<TInstData>;
+
+        private _nextKey: number;
+    }
+
+}

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

@@ -0,0 +1,352 @@
+module BABYLON {
+    export class Render2DContext {
+        camera: Camera;
+        parentVisibleState: boolean;
+        parentTransform: Matrix;
+        parentTransformStep: number;
+        forceRefreshPrimitive: boolean;
+    }
+
+    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._parentTranformStep = 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;
+        }
+
+        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;
+
+                let localTransform = new Matrix();
+                if (this.parent) {
+                    this.globalTransform.multiplyToRef(Matrix.Invert(this.parent.globalTransform), localTransform);
+                } else {
+                    localTransform = this.globalTransform;
+                }
+                let invLocalTransform = Matrix.Invert(localTransform);
+
+                this.levelBoundingInfo.transformToRef(localTransform, bi);
+
+                var tps = new BoundingInfo2D();
+                for (let curChild of this._children) {
+                    curChild.boundingInfo.transformToRef(curChild.globalTransform.multiply(invLocalTransform), 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 = 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]);
+        }
+
+        private addChild(child: Prim2DBase) {
+            child._siblingDepthOffset = (this._children.length + 1) * this.owner.hierarchySiblingZDelta;
+            this._children.push(child);
+
+        }
+
+        protected getActualZOffset(): number {
+            return this._zOrder || this._siblingDepthOffset;
+        }
+
+        protected onPrimBecomesDirty() {
+            if (this._renderGroup) {
+                //if (this instanceof Group2D) {
+                //    var group: any= this;
+                //    if (group.isRenderableGroup) {
+                //        return;
+                //    }
+                //}
+                this._renderGroup._addPrimToDirtyList(this);
+            }
+        }
+
+        public needPrepare(): boolean {
+            return this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformPreviousStep !== this._globalTransformStep);
+        }
+
+        protected _buildChildContext(context: Render2DContext): Render2DContext {
+            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;
+        }
+
+        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._globalTransformPreviousStep !== this._globalTransformStep) ||
+                this.checkPropertiesDirty(Prim2DBase.isVisibleProperty.flagId))) {
+
+                var childContext = this._buildChildContext(context);
+                this._children.forEach(c => {
+                    // As usual stop the recursion if we meet a renderable group
+                    if (!(c instanceof Group2D && c.isRenderableGroup)) {
+                        c._prepareRender(childContext);
+                    }
+                });
+            }
+
+            // 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[], context: Render2DContext, recurse: boolean) {
+            for (let cur of list) {
+                cur.updateGlobalTransVis(context, recurse);
+            }
+        }
+
+        protected updateGlobalTransVis(context: Render2DContext, recurse: boolean) {
+
+            this._globalTransformPreviousStep = this._globalTransformStep;
+            this.isVisible = context.parentVisibleState && this.levelVisible;
+
+            // Detect if nothing changed
+            let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+            if ((context.parentTransformStep === this._parentTranformStep) && !this.checkPropertiesDirty(tflags)) {
+                return;
+            }
+
+            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 = context.parentTransform.multiply(local);
+            this._invGlobalTransform = Matrix.Invert(this._globalTransform);
+
+            ++this._globalTransformStep;
+            this._parentTranformStep = context.parentTransformStep;
+
+            this.clearPropertiesDirty(tflags);
+
+            if (recurse) {
+                var childrenContext = this._buildChildContext(context);
+
+                for (let child of this._children) {
+                    // Stop the recursion if we meet a renderable group
+                    child.updateGlobalTransVis(childrenContext, !(child instanceof Group2D && child.isRenderableGroup));
+                }
+
+            }
+        }
+
+        private _owner: Canvas2D;
+        private _parent: Prim2DBase;
+        protected _children: Array<Prim2DBase>;
+        private _renderGroup: Group2D;
+        private _hierarchyDepth: 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;
+        protected _parentTranformStep: number;
+        protected _globalTransformStep: number;
+        protected _globalTransformPreviousStep: number;
+        protected _globalTransform: Matrix;
+        protected _invGlobalTransform: Matrix;
+    }
+
+}

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

@@ -0,0 +1,163 @@
+module BABYLON {
+    export class Rectangle2DRenderCache extends ModelRenderCache<Rectangle2DInstanceData> {
+        fillVB: WebGLBuffer;
+        fillIB: WebGLBuffer;
+        borderVB: WebGLBuffer;
+        borderIB: WebGLBuffer;
+        instancingAttributes: InstancingAttributeInfo[];
+
+        effect: Effect;
+
+        render(instanceInfo: GroupInstanceInfo, context: Render2DContext): boolean {
+            // 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;
+        }
+    }
+
+    export class Rectangle2DInstanceData extends InstanceDataBase {
+        @instanceData()
+        get properties(): Vector3 {
+            return null;
+        }
+    }
+
+    export class Rectangle2D extends RenderablePrim2D<Rectangle2DInstanceData> {
+
+        public static sizeProperty: Prim2DPropInfo;
+        public static notRoundedProperty: Prim2DPropInfo;
+        public static roundRadiusProperty: Prim2DPropInfo;
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 1, pi => Rectangle2D.sizeProperty = pi, false, true)
+        public get size(): Size {
+            return this._size;
+        }
+
+        public set size(value: Size) {
+            this._size = value;
+        }
+
+        @modelLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 2, pi => Rectangle2D.notRoundedProperty = pi)
+        public get notRounded(): boolean {
+            return this._notRounded;
+        }
+
+        public set notRounded(value: boolean) {
+            this._notRounded = value;
+        }
+
+        @instanceLevelProperty(RenderablePrim2D.RENDERABLEPRIM2D_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() {
+            this._levelBoundingInfo.radius = Math.sqrt(this.size.width * this.size.width + this.size.height * this.size.height);
+            this._levelBoundingInfo.extent = this.size.clone();
+        }
+
+        protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size: Size, roundRadius = 0, fill?: IFill2D, border?: IBorder2D) {
+            this.setupRenderablePrim2D(owner, parent, id, position, true, fill, border);
+            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?: IFill2D, border?: IBorder2D): 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 || Canvas2D.getSolidColorFillFromHex("#FFFFFFFF");
+            rect.border = border;
+            return rect;
+        }
+
+        public static CreateRounded(parent: Prim2DBase, id: string, x: number, y: number, width: number, height: number, roundRadius = 0, fill?: IFill2D, border?: IBorder2D): 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.getSolidColorFillFromHex("#FFFFFFFF");
+            rect.border = border;
+            return rect;
+        }
+
+        public static roundSubdivisions = 16;
+
+        protected createModelRenderCache(): ModelRenderCache<Rectangle2DInstanceData> {
+            let renderCache = new Rectangle2DRenderCache();
+            let 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;
+                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 + 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;
+        }
+
+
+        protected createInstanceData(): Rectangle2DInstanceData {
+            return new Rectangle2DInstanceData();
+        }
+
+        protected refreshInstanceData(): boolean {
+            if (!super.refreshInstanceData()) {
+                return false;
+            }
+
+            let d = this._instanceData;
+            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;
+    }
+
+
+}

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

@@ -0,0 +1,371 @@
+module BABYLON {
+    export class InstanceClassInfo {
+        constructor(base: InstanceClassInfo) {
+            this._baseInfo = base;
+            this._nextOffset = 0;
+            this._attributes = new Array<InstancePropInfo>();
+        }
+
+        mapProperty(propInfo: InstancePropInfo) {
+            propInfo.instanceOffset = (this._baseInfo ? this._baseInfo._nextOffset : 0) + this._nextOffset;
+            this._nextOffset += (propInfo.size / 4);
+            this._attributes.push(propInfo);
+        }
+
+        getInstancingAttributeInfos(effect: Effect): InstancingAttributeInfo[] {
+            let res = new Array<InstancingAttributeInfo>();
+            let curInfo: InstanceClassInfo = this;
+            while (curInfo) {
+                for (let attrib of curInfo._attributes) {
+                    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 * 4; // attrub.instanceOffset is in float, iai.offset must be in bytes
+                    res.push(iai);
+                }
+
+                curInfo = curInfo._baseInfo;
+            }
+            return res;
+        }
+
+        public instanceDataStride;
+
+        private _baseInfo: InstanceClassInfo;
+        private _nextOffset;
+        private _attributes: Array<InstancePropInfo>;
+    }
+
+    export class InstancePropInfo {
+        attributeName: string;
+        size: number;
+        shaderOffset: number;
+        instanceOffset: number;
+        dataType: ShaderDataType;
+        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;
+            }
+        }
+
+        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>(name?: 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));
+            var node = dic.getLevelOf(target);
+            name = name || <string>propName;
+
+            let 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: InstanceDataBase = this;
+                if (obj._dataBuffer) {
+                    info.writeData(obj._dataBuffer.buffer, obj._dataElement.offset + info.instanceOffset, val);
+                }
+            }
+
+        }
+    }
+
+    export class InstanceDataBase {
+        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> {
+            return ClassTreeInfo.get<InstanceClassInfo, InstancePropInfo>(this);
+        }
+
+        _dataElement: DynamicFloatArrayElementInfo;
+        _dataBuffer: DynamicFloatArray;
+    }
+
+   export class RenderablePrim2D<TInstData extends InstanceDataBase> extends Prim2DBase {
+        static RENDERABLEPRIM2D_PROPCOUNT: number = Prim2DBase.PRIM2DBASE_PROPCOUNT + 10;
+
+        public static borderProperty: Prim2DPropInfo;
+        public static fillProperty: Prim2DPropInfo;
+
+        setupRenderablePrim2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, isVisible: boolean, fill: IFill2D, border: IBorder2D) {
+            this.setupPrim2DBase(owner, parent, id, position);
+            this._isTransparent = false;
+        }
+
+        @modelLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 1, pi => RenderablePrim2D.borderProperty = pi, true)
+        public get border(): IBorder2D {
+            return this._border;
+        }
+
+        public set border(value: IBorder2D) {
+            if (value === this._border) {
+                return;
+            }
+
+            this._border = value;
+        }
+
+        @modelLevelProperty(Prim2DBase.PRIM2DBASE_PROPCOUNT + 2, pi => RenderablePrim2D.fillProperty = pi, true)
+        public get fill(): IFill2D {
+            return this._fill;
+        }
+
+        public set fill(value: IBorder2D) {
+            if (value === this._fill) {
+                return;
+            }
+
+            this._fill = value;
+        }
+
+        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?
+            if (!this._modelRenderCache || this._modelDirty) {
+                this._modelRenderCache = SmartPropertyPrim.GetOrAddModelCache(this.modelKey, (key: string) => this.createModelRenderCache());
+                this._modelDirty = false;
+            }
+
+            // Need to create the instance?
+            let gii: GroupInstanceInfo;
+            let newInstance = false;
+            if (!this._modelRenderInstanceID) {
+                newInstance = true;
+                let id = this.createInstanceData();
+                this._instanceData = id;
+
+                let cti = id.getClassTreeInfo();
+                if (!cti.classContent.instanceDataStride) {
+                    // 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.
+                    this.refreshInstanceData();
+                    this.isVisible = curVisible;
+
+                    var size = 0;
+                    cti.fullContent.forEach((k, v) => {
+                        if (!v.size) {
+                            console.log(`ERROR: Couldn't detect the size of the Property ${v.attributeName} from type ${cti.typeName}. Property is ignored.`);
+                        } else {
+                            size += v.size;
+                        }
+                    });
+                    cti.classContent.instanceDataStride = size;
+                }
+
+                gii = this.renderGroup.groupRenderInfo.getOrAddWithFactory(this.modelKey, k => new GroupInstanceInfo(this.renderGroup, cti, 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 DynamicFloatArray(cti.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;
+            }
+        }
+
+        protected createModelRenderCache(): ModelRenderCache<TInstData> {
+            return null;
+        }
+
+        protected createInstanceData(): TInstData {
+            return null;
+        }
+
+        protected refreshInstanceData(): boolean {
+            var d = this._instanceData;
+            if (!this.isVisible) {
+                return false;
+            }
+
+            d.isVisible = this.isVisible;
+            let t = this.renderGroup.invGlobalTransform.multiply(this._globalTransform);
+            let size = (<Size>this.renderGroup.viewportSize);
+            let 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)
+            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, t.m[8], (t.m[12] * 2 / w) - (invZBias));
+            let ty = new 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 Vector2(zBias, invZBias);
+            return true;
+        }
+
+        private _modelRenderCache: ModelRenderCache<TInstData>;
+        private _modelRenderInstanceID: string;
+
+        protected _instanceData: TInstData;
+        private _border: IBorder2D;
+        private _fill: IFill2D;
+        private _isTransparent: boolean;
+    }
+
+
+}

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

@@ -0,0 +1,351 @@
+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>, typeName: string, classContentFactory: (base: TClass) => TClass) {
+            this._baseClass = baseClass;
+            this._typeName = typeName;
+            this._subClasses = new StringDictionary<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 typeName(): string {
+            return this._typeName;
+        }
+
+        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> {
+            let typeName = Tools.getClassName(type);
+
+            // Are we already there?
+            if (typeName === this._typeName) {
+                return this;
+            }
+
+            let baseProto = Object.getPrototypeOf(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);
+        }
+
+        getOrAddType(baseTypeName, typeName: string): ClassTreeInfo<TClass, TProp> {
+
+            // Are we at the level corresponding to the baseType?
+            // If so, get or add the level we're looking for
+            if (baseTypeName === this._typeName) {
+                return this._subClasses.getOrAddWithFactory(typeName, k => new ClassTreeInfo<TClass, TProp>(this, typeName, this._classContentFactory));
+            }
+
+            // Recurse down to keep looking for the node corresponding to the baseTypeName
+            return this._subClasses.first<ClassTreeInfo<TClass, TProp>>((key, val) => val.getOrAddType(baseTypeName, typeName));
+        }
+
+        static get<TClass, TProp>(target: Object): ClassTreeInfo<TClass, TProp> {
+            let dic = <ClassTreeInfo<TClass, TProp>>target["__classTreeInfo"];
+            if (!dic) {
+                return null;
+            }
+            return dic.getLevelOf(target);
+        }
+
+        static getOrRegister<TClass, TProp>(target: Object, classContentFactory: (base: TClass) => TClass): ClassTreeInfo<TClass, TProp> {
+            let dic = <ClassTreeInfo<TClass, TProp>>target["__classTreeInfo"];
+            if (!dic) {
+                dic = new ClassTreeInfo<TClass, TProp>(null, Tools.getClassName(target), classContentFactory);
+                target["__classTreeInfo"] = dic;
+            }
+            return dic;
+        }
+
+        private _typeName: string;
+        private _classContent: TClass;
+        private _baseClass: ClassTreeInfo<TClass, TProp>;
+        private _subClasses: StringDictionary<ClassTreeInfo<TClass, TProp>>;
+        private _levelContent: StringDictionary<TProp>;
+        private _fullContent: StringDictionary<TProp>;
+        private _classContentFactory: (base: TClass) => TClass;
+    }
+
+    export class SmartPropertyPrim implements IPropertyChanged {
+
+        protected setupSmartPropertyPrim() {
+            this._modelKey = null;
+            this._modelDirty = false;
+            this._levelBoundingInfoDirty = false;
+            this._instanceDirtyFlags = 0;
+            this._levelBoundingInfo = new BoundingInfo2D();
+        }
+
+        public propertyChanged: Observable<PropertyChangedInfo>;
+
+        public dispose() {
+
+        }
+
+        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;
+        }
+
+        protected static GetOrAddModelCache<TInstData>(key: string, factory: (key: string) => ModelRenderCache<TInstData>): ModelRenderCache<TInstData> {
+            return <ModelRenderCache<TInstData>>SmartPropertyPrim.ModelCache.getOrAddWithFactory(key, factory);
+        }
+
+        protected static ModelCache: StringDictionary<ModelRenderCacheBase> = new StringDictionary<ModelRenderCacheBase>();
+
+        private get propDic(): StringDictionary<Prim2DPropInfo> {
+            let cti = ClassTreeInfo.get<Prim2DClassInfo, Prim2DPropInfo>(this);
+            if (!cti) {
+                throw new Error("Can't access the propDic member in class definition, is this class SmartPropertyPrim based?");
+            }
+            return cti.fullContent;
+        }
+
+        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);
+
+            // 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._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;
+                }
+            }
+        }
+
+        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 _levelBoundingInfoDirty: 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);
+    }
+}

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

@@ -0,0 +1,196 @@
+module BABYLON {
+    export class Sprite2DRenderCache extends ModelRenderCache<Sprite2DInstanceData> {
+        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
+            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;
+        }
+    }
+
+    export class Sprite2DInstanceData extends InstanceDataBase {
+        @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;
+        }
+    }
+
+    export class Sprite2D extends RenderablePrim2D<Sprite2DInstanceData> {
+
+        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() {
+            this._levelBoundingInfo.radius = Math.sqrt(this.spriteSize.width * this.spriteSize.width + this.spriteSize.height * this.spriteSize.height);
+            this._levelBoundingInfo.extent = this.spriteSize.clone();
+        }
+
+        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, null, null);
+            this.texture = texture;
+            this.spriteSize = spriteSize;
+            this.spriteLocation = spriteLocation;
+            this.spriteFrame = 0;
+            this.invertY = invertY;
+        }
+
+        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(): ModelRenderCache<Sprite2DInstanceData> {
+            let renderCache = new Sprite2DRenderCache();
+            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] = 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;
+        }
+
+        protected createInstanceData(): Sprite2DInstanceData {
+            return new Sprite2DInstanceData();
+        }
+
+        protected refreshInstanceData(): boolean {
+            if (!super.refreshInstanceData()) {
+                return false;
+            }
+
+            let d = this._instanceData;
+            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;
+    }
+
+
+}

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

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

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

@@ -0,0 +1,58 @@
+// Attributes
+attribute float index;
+attribute vec2 zBias;
+attribute vec4 transformX;
+attribute vec4 transformY;
+attribute vec2 origin;
+
+attribute 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;
+	if (index == 0.0) {
+		pos2 = vec2(0.5, 0.5) * properties.xy;
+	}
+	else {
+		float w = properties.x;
+		float h = properties.y;
+		float r = properties.z;
+
+		if (index < rsub0) {
+			pos2 = vec2(w-r, r);
+		}
+		else if (index < rsub1) {
+			pos2 = vec2(r, r);
+		}
+		else if (index < rsub2) {
+			pos2 = vec2(r, h - r);
+		}
+		else {
+			pos2 = vec2(w - r, h - r);
+		}
+
+		float angle = TWOPI - ((index - 1.0) * TWOPI / (rsub-1.0));
+		pos2.x += cos(angle) * properties.z;
+		pos2.y += sin(angle) * properties.z;
+
+	}
+
+	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);
+}

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

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

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

@@ -0,0 +1,50 @@
+// Attributes
+attribute float index;
+attribute vec2 zBias;
+
+attribute vec4 transformX;
+attribute vec4 transformY;
+
+attribute vec2 topLeftUV;
+attribute vec2 sizeUV;
+attribute vec2 origin;
+attribute vec2 textureSize;
+attribute float frame;
+attribute float invertY;
+
+// Uniforms
+
+// Output
+varying vec2 vUV;
+varying vec4 vColor;
+
+void main(void) {
+
+	vec2 pos2;
+	if (index == 0.0) {
+		pos2 = vec2(0.0, 0.0);
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x), 1.0 - topLeftUV.y);
+	}
+	else if (index == 1.0) {
+		pos2 = vec2(0.0,  1.0);
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x), 1.0 - (topLeftUV.y + sizeUV.y));
+	}
+	else if (index == 2.0) {
+		pos2 = vec2( 1.0,  1.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), 1.0 - (topLeftUV.y + sizeUV.y));
+	}
+	else if (index == 3.0) {
+		pos2 = vec2( 1.0, 0.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), 1.0 - topLeftUV.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);
+}	

+ 14 - 0
src/babylon.scene.ts

@@ -438,6 +438,8 @@
 
         private _debugLayer: DebugLayer;
 
+        private _screenCanvas: Canvas2D;
+
         private _depthRenderer: DepthRenderer;
 
         private _uniqueIdCounter = 0;
@@ -470,6 +472,8 @@
 
             this._debugLayer = new DebugLayer(this);
 
+            this._screenCanvas = Canvas2D.CreateScreenSpace(this, "ScreenCanvas", new Vector2(0, 0), new Size(engine.getRenderWidth(), engine.getRenderHeight()), Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS);
+
             if (SoundTrack) {
                 this.mainSoundTrack = new SoundTrack(this, { mainTrack: true });
             }
@@ -488,6 +492,10 @@
             return this._debugLayer;
         }
 
+        public get screenCanvas(): Canvas2D {
+            return this._screenCanvas;
+        }
+
         public set workerCollisions(enabled: boolean) {
 
             enabled = (enabled && !!Worker);
@@ -1930,6 +1938,9 @@
             this._renderingManager.render(null, null, true, true);
             Tools.EndPerformanceCounter("Main render");
 
+            // Screen Canvas render
+            this._screenCanvas.render(camera);
+
             // Bounding boxes
             this._boundingBoxRenderer.render();
 
@@ -2360,6 +2371,9 @@
             // Debug layer
             this.debugLayer.hide();
 
+            // Screen Canvas
+            this._screenCanvas.dispose();
+
             // Events
             if (this.onDispose) {
                 this.onDispose();