Browse Source

Big commit!

Canvas2D:
 - No more cheating: the canvas origin is based on WebGL's one: left/bottom of the viewport.

BoundingInfo2D:
 - Introduce Center property, much needed
 - Transformation/union are accurate (thanks to center)

Group2D: cached groups are now correctly rendered

Primitives:
 - Z is not accurately built from depth+sibling
 - Fixed Transformation computation to work across many depth levels + optimal traversal of graph
 - boundingInfo of Primitive and its children is now accurate
 - A primitive's Part can now create multiple instances of InstanceData objects. For instance the Text2D creates one instance per letter in the text.

Text2D: first version of the primitive, simply render text, no alignment supported right now. https://trello.com/c/WRirxsgG

FontTexture:
 - Introduce charWidth property in CharInfo class
 - FontTexture can now be cached by Scene + fontName using the static methods: Get/ReleaseCachedFontTexture
 - measureText method added

MapTexture: added clear parameter to bindTextureForRect to clear the content of the frame/depth buffers

Math.Vector2: added TransformToRef
nockawa 9 years ago
parent
commit
87d7a6f59f

+ 94 - 49
src/Canvas2d/babylon.bounding2d.ts

@@ -7,18 +7,74 @@
     export class BoundingInfo2D {
 
         /**
-         * The radius of the bounding circle, from the origin of the bounded object
+         * The coordinate of the center of the bounding info
+         */
+        public center: Vector2;
+
+        /**
+         * The radius of the bounding circle, from the center of the bounded object
          */
         public radius: number;
 
         /**
-         * The extent of the bounding rectangle, from the orgini of the bounded object.
+         * The extent of the bounding rectangle, from the center of the bounded object.
          * This is an absolute value in both X and Y of the vector which describe the right/top corner of the rectangle, you can easily reconstruct the whole rectangle by negating X &| Y.
          */
-        public extent: Size;
+        public extent: Vector2;
 
         constructor() {
-            this.extent = new Size(0, 0);
+            this.radius = 0;
+            this.center = Vector2.Zero();
+            this.extent = Vector2.Zero();
+        }
+
+        public static ConstructFromSize(size: Size): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.ConstructFromSizeToRef(size, r);
+            return r;
+        }
+
+        public static ConstructFromRadius(radius: number): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.ConstructFromRadiusToRef(radius, r);
+            return r;
+        }
+
+        public static ConstructFromPoints(points: Vector2[]): BoundingInfo2D {
+            let r = new BoundingInfo2D();
+            BoundingInfo2D.ConstructFromPointsToRef(points, r);
+
+            return r;
+        }
+
+        public static ConstructFromSizeToRef(size: Size, b: BoundingInfo2D) {
+            b.center = new Vector2(size.width / 2, size.height / 2);
+            b.extent = b.center.clone();
+            b.radius = b.extent.length();
+        }
+
+        public static ConstructFromRadiusToRef(radius: number, b: BoundingInfo2D) {
+            b.center = Vector2.Zero();
+            b.extent = new Vector2(radius, radius);
+            b.radius = radius;
+        }
+
+        public static ConstructFromPointsToRef(points: Vector2[], b: BoundingInfo2D) {
+            let xmin = Number.MAX_VALUE, ymin = Number.MAX_VALUE, xmax = Number.MIN_VALUE, ymax = Number.MIN_VALUE;
+            for (let p of points) {
+                xmin = Math.min(p.x, xmin);
+                xmax = Math.max(p.x, xmax);
+                ymin = Math.min(p.y, ymin);
+                ymax = Math.max(p.y, ymax);
+            }
+            BoundingInfo2D.ConstructFromMinMaxToRef(xmin, xmax, ymin, ymax, b);
+        }
+
+
+        public static ConstructFromMinMaxToRef(xmin: number, xmax: number, ymin: number, ymax: number, b: BoundingInfo2D) {
+            b.center = new Vector2(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2);
+            b.extent = new Vector2(xmax - b.center.x, ymax - b.center.y);
+            b.radius = b.extent.length();
         }
 
         /**
@@ -27,19 +83,31 @@
          */
         public clone(): BoundingInfo2D {
             let r = new BoundingInfo2D();
+            r.center = this.center.clone();
             r.radius = this.radius;
             r.extent = this.extent.clone();
             return r;
         }
 
+        public max(): Vector2 {
+            let r = Vector2.Zero();
+            this.maxToRef(r);
+            return r;
+        }
+
+        public maxToRef(result: Vector2) {
+            result.x = this.center.x + this.extent.x;
+            result.y = this.center.y + this.extent.y;
+        }
+
         /**
          * Apply a transformation matrix to this BoundingInfo2D and return a new instance containing the result
          * @param matrix the transformation matrix to apply
          * @return the new instance containing the result of the transformation applied on this BoundingInfo2D
          */
-        public transform(matrix: Matrix): BoundingInfo2D {
+        public transform(matrix: Matrix, origin: Vector2=null): BoundingInfo2D {
             var r = new BoundingInfo2D();
-            this.transformToRef(matrix, r);
+            this.transformToRef(matrix, origin, r);
             return r;
         }
 
@@ -57,55 +125,30 @@
         /**
          * Transform this BoundingInfo2D with a given matrix and store the result in an existing BoundingInfo2D instance.
          * This is a GC friendly version, try to use it as much as possible, specially if your transformation is inside a loop, allocate the result object once for good outside of the loop and use it everytime.
+         * @param origin An optional normalized origin to apply before the transformation. 0;0 is top/left, 0.5;0.5 is center, etc.
          * @param matrix The matrix to use to compute the transformation
          * @param result A VALID (i.e. allocated) BoundingInfo2D object where the result will be stored
          */
-        public transformToRef(matrix: Matrix, 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);
-            }
-
+        public transformToRef(matrix: Matrix, origin: Vector2, result: BoundingInfo2D) {
             // Construct a bounding box based on the extent values
             let p = new Array<Vector2>(4);
-            p[0] = new Vector2(this.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);
+            p[0] = new Vector2(this.center.x + this.extent.x, this.center.y + this.extent.y);
+            p[1] = new Vector2(this.center.x + this.extent.x, this.center.y - this.extent.y);
+            p[2] = new Vector2(this.center.x - this.extent.x, this.center.y - this.extent.y);
+            p[3] = new Vector2(this.center.x - this.extent.x, this.center.y + this.extent.y);
+
+            //if (origin) {
+            //    let off = new Vector2((p[0].x - p[2].x) * origin.x, (p[0].y - p[2].y) * origin.y);
+            //    for (let j = 0; j < 4; j++) {
+            //        p[j].subtractInPlace(off);
+            //    }
+            //}
 
             // Transform the four points of the bounding box with the matrix
             for (let i = 0; i < 4; i++) {
-                p[i] = Vector2.Transform(p[i], matrix);
+                Vector2.TransformToRef(p[i], matrix, p[i]);
             }
-
-            // 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;
+            BoundingInfo2D.ConstructFromPointsToRef(p, result);
         }
 
         /**
@@ -115,9 +158,11 @@
          * @param result a VALID BoundingInfo2D instance (i.e. allocated) where the result will be stored
          */
         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);
+            let xmax = Math.max(this.center.x + this.extent.x, other.center.x + other.extent.x);
+            let ymax = Math.max(this.center.y + this.extent.y, other.center.y + other.extent.y);
+            let xmin = Math.min(this.center.x - this.extent.x, other.center.x - other.extent.x);
+            let ymin = Math.min(this.center.y - this.extent.y, other.center.y - other.extent.y);
+            BoundingInfo2D.ConstructFromMinMaxToRef(xmin, xmax, ymin, ymax, result);
         }
 
     }

+ 12 - 7
src/Canvas2d/babylon.canvas2d.ts

@@ -68,7 +68,9 @@
 
         protected setupCanvas(scene: Scene, name: string, size: Size, isScreenSpace: boolean = true, cachingstrategy: number = Canvas2D.CACHESTRATEGY_TOPLEVELGROUPS) {
             this._cachingStrategy = cachingstrategy;
-            this._hierarchyLevelZFactor = 100;
+            this._depthLevel = 0;
+            this._hierarchyMaxDepth = 100;
+            this._hierarchyLevelZFactor = 1 / this._hierarchyMaxDepth;
             this._hierarchyLevelMaxSiblingCount = 1000;
             this._hierarchySiblingZDelta = this._hierarchyLevelZFactor / this._hierarchyLevelMaxSiblingCount;
 
@@ -171,6 +173,10 @@
             return this._hierarchySiblingZDelta;
         }
 
+        public get hierarchyLevelZFactor(): number {
+            return this._hierarchyLevelZFactor;
+        }
+
         private _mapCounter = 0;
         private _background: Rectangle2D;
         private _scene: Scene;
@@ -178,6 +184,7 @@
         private _isScreeSpace: boolean;
         private _worldTransform: Matrix;
         private _cachingStrategy: number;
+        private _hierarchyMaxDepth: number;
         private _hierarchyLevelZFactor: number;
         private _hierarchyLevelMaxSiblingCount: number;
         private _hierarchySiblingZDelta: number;
@@ -193,13 +200,10 @@
             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._globalTransformProcessStep;
+            this.updateGlobalTransVis(false);
 
             this._prepareGroupRender(context);
             this._groupRender(context);
@@ -248,7 +252,8 @@
             }
 
             // 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}`, group.position.x, group.position.y, map, res.node.contentSize, res.node.pos, true);
+            let node: PackedRect = res.node;
+            let sprite = Sprite2D.Create(this, `__cachedSpriteOfGroup__${group.id}`, group.position.x, group.position.y, map, node.contentSize, node.pos, false);
             sprite.origin = Vector2.Zero();
             res.sprite = sprite;
             return res;

+ 10 - 16
src/Canvas2d/babylon.group2d.ts

@@ -89,9 +89,8 @@
             }
 
             // Otherwise the size is computed based on the boundingInfo
-            let size = this.boundingInfo.extent.clone();
-
-            return size;
+            let m = this.boundingInfo.max();
+            return new Size(m.x, m.y);
         }
 
         public get cacheBehavior(): number {
@@ -105,7 +104,7 @@
         protected updateLevelBoundingInfo() {
             let size: Size;
 
-            // If the size is set by the user, the boundingInfo is compute from this value
+            // If the size is set by the user, the boundingInfo is computed from this value
             if (this.size) {
                 size = this.size;
             }
@@ -114,21 +113,17 @@
                 size = new Size(0, 0);
             }
 
-            this._levelBoundingInfo.radius = Math.sqrt(size.width * size.width + size.height * size.height);
-            this._levelBoundingInfo.extent = size.clone();
+            BoundingInfo2D.ConstructFromSizeToRef(size, this._levelBoundingInfo);
         }
 
         // 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);
+                this.updateGlobalTransVisOf(sortedDirtyList, true);
             }
 
             // Setup the size of the rendering viewport
@@ -169,7 +164,7 @@
                 // If it's a force refresh, prepare all the children
                 if (context.forceRefreshPrimitive) {
                     for (let p of this._children) {
-                        p._prepareRender(childrenContext);
+                        p._prepareRender(context);
                     }
                 } else {
                     // Each primitive that changed at least once was added into the primDirtyList, we have to sort this level using
@@ -183,7 +178,7 @@
                         // 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);
+                            p._prepareRender(context);
                         }
                     });
 
@@ -194,7 +189,7 @@
 
             // 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);
+                g._prepareGroupRender(context);
             });
         }
 
@@ -203,9 +198,8 @@
             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);
+                childGroup._groupRender(context);
             }
 
             // Render the primitives if needed: either if we don't cache the content or if the content is cached but has changed
@@ -276,7 +270,7 @@
             }
 
             let n = this._cacheNode;
-            this._cacheTexture.bindTextureForRect(n);
+            this._cacheTexture.bindTextureForRect(n, true);
         }
 
         private _unbindCacheTarget() {

+ 56 - 57
src/Canvas2d/babylon.prim2dBase.ts

@@ -1,9 +1,6 @@
 module BABYLON {
+
     export class Render2DContext {
-        camera: Camera;
-        parentVisibleState: boolean;
-        parentTransform: Matrix;
-        parentTransformStep: number;
         forceRefreshPrimitive: boolean;
     }
 
@@ -33,7 +30,7 @@
             this._id = id;
             this.propertyChanged = new Observable<PropertyChangedInfo>();
             this._children = new Array<Prim2DBase>();
-            this._parentTranformStep = 0;
+            this._globalTransformProcessStep = 0;
             this._globalTransformStep = 0;
 
             if (this instanceof Group2D) {
@@ -105,12 +102,21 @@
         public get scale(): number {
             return this._scale;
         }
-
+        
         @instanceLevelProperty(4, pi => Prim2DBase.originProperty = pi, false, true)
         public set origin(value: Vector2) {
             this._origin = value;
         }
 
+        /**
+         * The origin defines the normalized coordinate of the center of the primitive, from the top/left corner.
+         * The origin is used only to compute transformation of the primitive, it has no meaning in the primitive local frame of reference
+         * For instance:
+         * 0,0 means the center is top/left
+         * 0.5,0.5 means the center is at the center of the primtive
+         * 0,1 means the center is bottom/left
+         * @returns The normalized center.
+         */
         public get origin(): Vector2 {
             return this._origin;
         }
@@ -163,19 +169,10 @@
                 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);
+                    let t = curChild.globalTransform.multiply(this.invGlobalTransform);
+                    curChild.boundingInfo.transformToRef(t, curChild.origin, tps);
                     bi.unionToRef(tps, bi);
                 }
 
@@ -195,7 +192,7 @@
 
             // Move to first position
             if (!previous) {
-                prevOffset = 0;
+                prevOffset = 1;
                 nextOffset = this._children[1]._siblingDepthOffset;
             } else {
                 prevOffset = this._children[prevIndex]._siblingDepthOffset;
@@ -209,12 +206,14 @@
 
         private addChild(child: Prim2DBase) {
             child._siblingDepthOffset = (this._children.length + 1) * this.owner.hierarchySiblingZDelta;
+            child._depthLevel = this._depthLevel + 1;
+            child._hierarchyDepthOffset = child._depthLevel * this.owner.hierarchyLevelZFactor;
             this._children.push(child);
 
         }
 
         protected getActualZOffset(): number {
-            return this._zOrder || this._siblingDepthOffset;
+            return this._zOrder || 1-(this._siblingDepthOffset + this._hierarchyDepthOffset);
         }
 
         protected onPrimBecomesDirty() {
@@ -224,18 +223,7 @@
         }
 
         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;
+            return this._modelDirty || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep);
         }
 
         public _prepareRender(context: Render2DContext) {
@@ -259,14 +247,13 @@
             //  - 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) ||
+            if (this._children.length > 0 && ((this._globalTransformProcessStep !== 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);
+                        c._prepareRender(context);
                     }
                 });
             }
@@ -282,41 +269,43 @@
             }
         }
 
-        protected updateGlobalTransVisOf(list: Prim2DBase[], context: Render2DContext, recurse: boolean) {
+        protected updateGlobalTransVisOf(list: Prim2DBase[], recurse: boolean) {
             for (let cur of list) {
-                cur.updateGlobalTransVis(context, recurse);
+                cur.updateGlobalTransVis(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;
+        protected updateGlobalTransVis(recurse: boolean) {
+            // Check if the parent is synced
+            if (this._parent && this._parent._globalTransformProcessStep !== this.owner._globalTransformProcessStep) {
+                this._parent.updateGlobalTransVis(false);
             }
 
-            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));
+            // Check if we must update this prim
+            if (this === <any>this.owner || this._globalTransformProcessStep !== this.owner._globalTransformProcessStep) {
+                this.isVisible = (!this._parent || this._parent.isVisible) && this.levelVisible;
 
-            this._globalTransform = context.parentTransform.multiply(local);
-            this._invGlobalTransform = Matrix.Invert(this._globalTransform);
+                // Detect if either the parent or this node changed
+                let tflags = Prim2DBase.positionProperty.flagId | Prim2DBase.rotationProperty.flagId | Prim2DBase.scaleProperty.flagId;
+                if ((this._parent && this._parent._globalTransformStep !== this._parentTransformStep) || this.checkPropertiesDirty(tflags)) {
+                    var rot = Quaternion.RotationAxis(new Vector3(0, 0, 1), this._rotation);
+                    var local = Matrix.Compose(new Vector3(this._scale, this._scale, this._scale), rot, new Vector3(this._position.x, this._position.y, 0));
 
-            ++this._globalTransformStep;
-            this._parentTranformStep = context.parentTransformStep;
+                    this._globalTransform = this._parent ? local.multiply(this._parent._globalTransform) : local;
+                    this._invGlobalTransform = Matrix.Invert(this._globalTransform);
 
-            this.clearPropertiesDirty(tflags);
+                    this._globalTransformStep = this.owner._globalTransformProcessStep + 1;
+                    this._parentTransformStep = this._parent ? this._parent._globalTransformStep : 0;
 
+                    this.clearPropertiesDirty(tflags);
+                }
+                this._globalTransformProcessStep = this.owner._globalTransformProcessStep;
+            }
             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));
+                    child.updateGlobalTransVis(!(child instanceof Group2D && child.isRenderableGroup));
                 }
-
             }
         }
 
@@ -325,6 +314,8 @@
         protected _children: Array<Prim2DBase>;
         private _renderGroup: Group2D;
         private _hierarchyDepth: number;
+        protected _depthLevel: number;
+        private _hierarchyDepthOffset: number;
         private _siblingDepthOffset: number;
         private _zOrder: number;
         private _levelVisible: boolean;
@@ -335,9 +326,17 @@
         private _rotation: number;
         private _scale: number;
         private _origin: Vector2;
-        protected _parentTranformStep: number;
+
+        // Stores the step of the parent for which the current global tranform was computed
+        // If the parent has a new step, it means this prim's global transform must be updated
+        protected _parentTransformStep: number;
+
+        // Stores the step corresponding of the global transform for this prim
+        // If a child prim has an older _parentTransformStep it means the chidl's transform should be updated
         protected _globalTransformStep: number;
-        protected _globalTransformPreviousStep: number;
+
+        // Stores the previous 
+        protected _globalTransformProcessStep: number;
         protected _globalTransform: Matrix;
         protected _invGlobalTransform: Matrix;
     }

+ 7 - 8
src/Canvas2d/babylon.rectangle2d.ts

@@ -61,7 +61,7 @@
 
     export class Rectangle2DInstanceData extends Shape2DInstanceData {
         constructor(partId: number) {
-            super(partId);
+            super(partId, 1);
         }
 
         @instanceData()
@@ -106,8 +106,7 @@
         }
 
         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();
+            BoundingInfo2D.ConstructFromSizeToRef(this.size, this._levelBoundingInfo);
         }
 
         protected setupRectangle2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, size: Size, roundRadius = 0, fill?: IBrush2D, border?: IBrush2D, borderThickness: number = 1) {
@@ -161,10 +160,10 @@
                 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[i * 3 + 2] = i + 1;
+                    ib[i * 3 + 1] = i + 2;
                 }
-                ib[triCount * 3 - 1] = 1;
+                ib[triCount * 3 - 2] = 1;
 
                 renderCache.fillIB = engine.createIndexBuffer(ib);
                 renderCache.fillIndicesCount = triCount * 3;
@@ -220,8 +219,8 @@
             return res;
         }
 
-        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
-            if (!super.refreshInstanceDataParts(part)) {
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
                 return false;
             }
             if (part.id === Shape2D.SHAPE2D_BORDERPARTID) {

+ 63 - 27
src/Canvas2d/babylon.renderablePrim2d.ts

@@ -216,9 +216,9 @@
                 }
 
                 var obj: InstanceDataBase = this;
-                if (obj._dataBuffer) {
-                    let offset = obj._dataElement.offset + info.instanceOffset.get(InstanceClassInfo._CurCategories);
-                    info.writeData(obj._dataBuffer.buffer, offset, val);
+                if (obj.dataBuffer && obj.dataElements) {
+                    let offset = obj.dataElements[obj.curElement].offset + info.instanceOffset.get(InstanceClassInfo._CurCategories);
+                    info.writeData(obj.dataBuffer.buffer, offset, val);
                 }
             }
 
@@ -226,8 +226,10 @@
     }
 
     export class InstanceDataBase {
-        constructor(partId: number) {
+        constructor(partId: number, dataElementCount: number) {
             this.id = partId;
+            this.curElement = 0;
+            this.dataElementCount = dataElementCount;
         }
 
         id: number;
@@ -254,15 +256,33 @@
         }
 
         getClassTreeInfo(): ClassTreeInfo<InstanceClassInfo, InstancePropInfo> {
-            if (!this._typeInfo) {
-                this._typeInfo = ClassTreeInfo.get<InstanceClassInfo, InstancePropInfo>(Object.getPrototypeOf(this));
+            if (!this.typeInfo) {
+                this.typeInfo = ClassTreeInfo.get<InstanceClassInfo, InstancePropInfo>(Object.getPrototypeOf(this));
             }
-            return this._typeInfo;
+            return this.typeInfo;
         }
 
-        _dataElement: DynamicFloatArrayElementInfo;
-        _dataBuffer: DynamicFloatArray;
-        _typeInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>;
+        allocElements() {
+            var res = new Array<DynamicFloatArrayElementInfo>(this.dataElementCount);
+            for (let i = 0; i < this.dataElementCount; i++) {
+                res[i] = this.dataBuffer.allocElement();
+            }
+            this.dataElements = res;
+        }
+
+        freeElements() {
+            for (let ei of this.dataElements) {
+                this.dataBuffer.freeElement(ei);
+            }
+            this.dataElements = null;
+        }
+
+        curElement: number;
+        dataElementCount: number;
+        dataElements: DynamicFloatArrayElementInfo[];
+        dataBuffer: DynamicFloatArray;
+        typeInfo: ClassTreeInfo<InstanceClassInfo, InstancePropInfo>;
+
     }
 
     @className("RenderablePrim2D")
@@ -314,7 +334,7 @@
                         // We manually trigger refreshInstanceData for the only sake of evaluating each isntance property size and offset in the instance data, this can only be made at runtime. Once it's done we have all the information to create the instance data buffer.
                         //console.log("Build Prop Layout for " + Tools.getClassName(this._instanceDataParts[0]));
                         InstanceClassInfo._CurCategories = cat.join(";");
-                        this.refreshInstanceDataParts(dataPart);
+                        this.refreshInstanceDataPart(dataPart);
                         this.isVisible = curVisible;
 
                         var size = 0;
@@ -350,8 +370,8 @@
 
                 for (let i = 0; i < parts.length; i++) {
                     let part = parts[i];
-                    part._dataBuffer = gii._instancesPartsData[i];
-                    part._dataElement = part._dataBuffer.allocElement();
+                    part.dataBuffer = gii._instancesPartsData[i];
+                    part.allocElements();
                 }
 
                 this._modelRenderInstanceID = this._modelRenderCache.addInstanceDataParts(this._instanceDataParts);
@@ -361,17 +381,16 @@
                 this.setupModelRenderCache(this._modelRenderCache);
             }
 
-            if (context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformPreviousStep !== this._globalTransformStep)) {
+            if (context.forceRefreshPrimitive || newInstance || (this._instanceDirtyFlags !== 0) || (this._globalTransformProcessStep !== this._globalTransformStep)) {
                 for (let part of this._instanceDataParts) {
                     let cat = this.getUsedShaderCategories(part);
                     InstanceClassInfo._CurCategories = cat.join(";");
 
                     // Will return false if the instance should not be rendered (not visible or other any reasons)
-                    if (!this.refreshInstanceDataParts(part)) {
+                    if (!this.refreshInstanceDataPart(part)) {
                         // Free the data element
-                        if (part._dataElement) {
-                            part._dataBuffer.freeElement(part._dataElement);
-                            part._dataElement = null;
+                        if (part.dataElements) {
+                            part.freeElements();
                         }
                     }
                 }
@@ -404,6 +423,10 @@
             return this._isTransparent;
         }
 
+        protected get modelRenderCache(): ModelRenderCache {
+            return this._modelRenderCache;
+        }
+
         protected createModelRenderCache(): ModelRenderCache {
             return null;
         }
@@ -419,36 +442,49 @@
             return [];
         }
 
-        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
             if (!this.isVisible) {
                 return false;
             }
-
             part.isVisible = this.isVisible;
-            let t = this.renderGroup.invGlobalTransform.multiply(this._globalTransform);
+
+            // Which means, if there's only one data element, we're update it from this method, otherwise it is the responsability of the derived class to call updateInstanceDataPart as many times as needed, properly (look at Text2D's implementation for more information)
+            if (part.dataElementCount === 1) {
+                this.updateInstanceDataPart(part);
+            }
+            return true;
+        }
+
+        protected updateInstanceDataPart(part: InstanceDataBase, positionOffset: Vector2 = null) {
+            let t = this._globalTransform.multiply(this.renderGroup.invGlobalTransform);
             let size = (<Size>this.renderGroup.viewportSize);
             let zBias = this.getActualZOffset();
 
+            let offX = 0;
+            let offY = 0;
+            // If there's an offset, apply the global transformation matrix on it to get a global offset
+            if (positionOffset) {
+                offX = positionOffset.x * t.m[0] + positionOffset.y * t.m[4];
+                offY = positionOffset.x * t.m[1] + positionOffset.y * t.m[5];
+            }
+
             // Have to convert the coordinates to clip space which is ranged between [-1;1] on X and Y axis, with 0,0 being the left/bottom corner
             // Current coordinates are expressed in renderGroup coordinates ([0, renderGroup.actualSize.width|height]) with 0,0 being at the left/top corner
-            // RenderGroup Width and Height are multiplied by zBias because the VertexShader will multiply X and Y by W, which is 1/zBias. Has we divide our coordinate by these Width/Height, we will also divide by the zBias to compensate the operation made by the VertexShader.
+            // RenderGroup Width and Height are multiplied by zBias because the VertexShader will multiply X and Y by W, which is 1/zBias. As we divide our coordinate by these Width/Height, we will also divide by the zBias to compensate the operation made by the VertexShader.
             // So for X: 
             //  - tx.x = value * 2 / width: is to switch from [0, renderGroup.width] to [0, 2]
             //  - tx.w = (value * 2 / width) - 1: w stores the translation in renderGroup coordinates so (value * 2 / width) to switch to a clip space translation value. - 1 is to offset the overall [0;2] to [-1;1]. Don't forget it's -(1/zBias) and not -1 because everything need to be scaled by 1/zBias.
-            // 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);
+            let tx = new Vector4(t.m[0] * 2 / w, t.m[4] * 2 / w, 0/*t.m[8]*/, ((t.m[12]+offX) * 2 / w) - (invZBias));
+            let ty = new Vector4(t.m[1] * 2 / h, t.m[5] * 2 / h, 0/*t.m[9]*/, ((t.m[13]+offY) * 2 / h) - (invZBias));
             part.transformX = tx;
             part.transformY = ty;
             part.origin = this.origin;
 
             // Stores zBias and it's inverse value because that's needed to compute the clip space W coordinate (which is 1/Z, so 1/zBias)
             part.zBias = new Vector2(zBias, invZBias);
-
-            return true;
         }
 
         private _modelRenderCache: ModelRenderCache;

+ 2 - 2
src/Canvas2d/babylon.shape2d.ts

@@ -79,8 +79,8 @@
             return cat;
         }
 
-        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
-            if (!super.refreshInstanceDataParts(part)) {
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
                 return false;
             }
 

+ 12 - 1
src/Canvas2d/babylon.smartPropertyPrim.ts

@@ -140,13 +140,23 @@
             this._modelDirty = false;
             this._levelBoundingInfoDirty = false;
             this._instanceDirtyFlags = 0;
+            this._isDisposed = false;
             this._levelBoundingInfo = new BoundingInfo2D();
         }
 
         public propertyChanged: Observable<PropertyChangedInfo>;
 
-        public dispose() {
+        public get isDisposed(): boolean {
+            return this._isDisposed;
+        }
+
+        public dispose(): boolean {
+            if (this.isDisposed) {
+                return false;
+            }
 
+            this._isDisposed = true;
+            return true;
         }
 
         public get modelKey(): string {
@@ -360,6 +370,7 @@
         private _modelKey; string;
         private _propInfo: StringDictionary<Prim2DPropInfo>;
         private _levelBoundingInfoDirty: boolean;
+        private _isDisposed: boolean;
         protected _levelBoundingInfo: BoundingInfo2D;
         protected _boundingInfo: BoundingInfo2D;
         protected _modelDirty: boolean;

+ 13 - 8
src/Canvas2d/babylon.sprite2d.ts

@@ -38,6 +38,10 @@
     }
 
     export class Sprite2DInstanceData extends InstanceDataBase {
+        constructor(partId: number) {
+            super(partId, 1);
+        }
+
         @instanceData()
         get topLeftUV(): Vector2 {
             return null;
@@ -120,13 +124,14 @@
         }
 
         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();
+            BoundingInfo2D.ConstructFromSizeToRef(this.spriteSize, this._levelBoundingInfo);
         }
 
         protected setupSprite2D(owner: Canvas2D, parent: Prim2DBase, id: string, position: Vector2, texture: Texture, spriteSize: Size, spriteLocation: Vector2, invertY: boolean) {
             this.setupRenderablePrim2D(owner, parent, id, position, true);
             this.texture = texture;
+            this.texture.wrapU = Texture.CLAMP_ADDRESSMODE;
+            this.texture.wrapV = Texture.CLAMP_ADDRESSMODE;
             this.spriteSize = spriteSize;
             this.spriteLocation = spriteLocation;
             this.spriteFrame = 0;
@@ -159,11 +164,11 @@
 
             let ib = new Float32Array(6);
             ib[0] = 0;
-            ib[1] = 1;
-            ib[2] = 2;
+            ib[1] = 2;
+            ib[2] = 1;
             ib[3] = 0;
-            ib[4] = 2;
-            ib[5] = 3;
+            ib[4] = 3;
+            ib[5] = 2;
 
             renderCache.ib = engine.createIndexBuffer(ib);
 
@@ -179,8 +184,8 @@
             return [new Sprite2DInstanceData(Sprite2D.SPRITE2D_MAINPARTID)];
         }
 
-        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
-            if (!super.refreshInstanceDataParts(part)) {
+        protected refreshInstanceDataPart(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataPart(part)) {
                 return false;
             }
 

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

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

+ 86 - 2
src/Materials/Textures/babylon.fontTexture.ts

@@ -13,6 +13,8 @@
          * The normalized ([0;1]) right/bottom position of the character in the texture
          */
         bottomRightUV: Vector2;
+
+        charWidth: number;
     }
 
     interface ICharInfoMap {
@@ -29,11 +31,52 @@
         private _curCharCount = 0;
         private _lastUpdateCharCount = -1;
         private _spaceWidth;
+        private _usedCounter = 1;
 
-        public get spaceWidth() {
+        public get spaceWidth(): number {
             return this._spaceWidth;
         }
 
+        public get lineHeight(): number {
+            return this._lineHeight;
+        }
+
+        public static GetCachedFontTexture(scene: Scene, fontName: string) {
+            let s = <any>scene;
+            if (!s.__fontTextureCache__) {
+                s.__fontTextureCache__ = new StringDictionary<FontTexture>();
+            }
+
+            let dic = <StringDictionary<FontTexture>>s.__fontTextureCache__;
+
+            let lfn = fontName.toLocaleLowerCase();
+            let ft = dic.get(lfn);
+            if (ft) {
+                ++ft._usedCounter;
+                return ft;
+            }
+
+            ft = new FontTexture(null, lfn, scene);
+            dic.add(lfn, ft);
+
+            return ft;
+        }
+
+        public static ReleaseCachedFontTexture(scene: Scene, fontName: string) {
+            let s = <any>scene;
+            let dic = <StringDictionary<FontTexture>>s.__fontTextureCache__;
+            if (!dic) {
+                return;
+            }
+
+            let lfn = fontName.toLocaleLowerCase();
+            var font = dic.get(lfn);
+            if (--font._usedCounter === 0) {
+                dic.remove(lfn);
+                font.dispose();
+            }
+        }
+
         /**
          * Create a new instance of the FontTexture class
          * @param name the name of the texture
@@ -131,7 +174,8 @@
 
             // Fill the CharInfo object
             info.topLeftUV = new Vector2(this._currentFreePosition.x / textureSize.width, this._currentFreePosition.y / textureSize.height);
-            info.bottomRightUV = new Vector2(info.topLeftUV.x + (width / textureSize.width), info.topLeftUV.y + ((this._lineHeight+1) / textureSize.height));
+            info.bottomRightUV = new Vector2(info.topLeftUV.x + (width / textureSize.width), info.topLeftUV.y + ((this._lineHeight + 1) / textureSize.height));
+            info.charWidth = width;
 
             // Add the info structure
             this._charInfos[char] = info;
@@ -143,6 +187,46 @@
             return info;
         }
 
+        public measureText(text: string, tabulationSize: number = 4): Size {
+            let maxWidth: number = 0;
+            let curWidth: number = 0;
+            let lineCount = 1;
+            let charxpos: number = 0;
+
+            // Parse each char of the string
+            for (var char of text) {
+
+                // Next line feed?
+                if (char === "\n") {
+                    maxWidth = Math.max(maxWidth, curWidth);
+                    charxpos = 0;
+                    curWidth = 0;
+                    ++lineCount;
+                    continue;
+                }
+
+                // Tabulation ?
+                if (char === "\t") {
+                    let nextPos = charxpos + tabulationSize;
+                    nextPos = nextPos - (nextPos % tabulationSize);
+
+                    curWidth += (nextPos - charxpos) * this.spaceWidth;
+                    charxpos = nextPos;
+                    continue;
+                }
+
+                if (char < " ") {
+                    continue;
+                }
+
+                curWidth += this.getChar(char).charWidth;
+                ++charxpos;
+            }
+            maxWidth = Math.max(maxWidth, curWidth);
+
+            return new Size(maxWidth, lineCount * this._lineHeight);
+        }
+
         // More info here: https://videlais.com/2014/03/16/the-many-and-varied-problems-with-measuring-font-height-for-html5-canvas/
         private getFontHeight(font: string): {height: number, offset: number} {
             var fontDraw = document.createElement("canvas");

+ 5 - 1
src/Materials/Textures/babylon.mapTexture.ts

@@ -55,10 +55,14 @@
          * Don't forget to call unbindTexture when you're done rendering
          * @param rect the zone to render to
          */
-        public bindTextureForRect(rect: PackedRect) {
+        public bindTextureForRect(rect: PackedRect, clear: boolean) {
             let engine = this.getScene().getEngine();
             engine.bindFramebuffer(this._texture);
             this._replacedViewport = engine.setDirectViewport(rect.pos.x, rect.pos.y, rect.contentSize.width, rect.contentSize.height);
+            if (clear) {
+                engine.clear(new Color4(0,0,0,0), true, true);
+            }
+//            this._replacedViewport = engine.setDirectViewport(rect.pos.x, rect.pos.y, rect.contentSize.width, rect.contentSize.height);
         }
 
         /**

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

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

+ 13 - 7
src/Shaders/rect2d.vertex.fx

@@ -49,17 +49,23 @@ void main(void) {
 
 	vec2 pos2;
 
-	// notRound case, only four vertices
+	// notRound case, only five vertices, 0 is center, then the 4 other for perimeter
 	if (properties.z == 0.0) {
-		if (index == 1.0) {
+		if (index == 0.0) {
+			pos2 = vec2(0.5, 0.5);
+		} 
+		else if (index == 1.0) {
 			pos2 = vec2(1.0, 1.0);
-		} else if (index == 2.0) {
-			pos2 = vec2(0.0, 1.0);
-		} else if (index == 3.0) {
-			pos2 = vec2(0.0, 0.0);
-		} else {
+		} 
+		else if (index == 2.0) {
 			pos2 = vec2(1.0, 0.0);
 		}
+		else if (index == 3.0) {
+			pos2 = vec2(0.0, 0.0);
+		} 
+		else {
+			pos2 = vec2(0.0, 1.0);
+		}
 	}
 	else 
 	{

+ 15 - 4
src/Shaders/sprite2d.vertex.fx

@@ -21,21 +21,32 @@ varying vec4 vColor;
 void main(void) {
 
 	vec2 pos2;
+
+	//vec2 off = vec2(1.0 / textureSize.x, 1.0 / textureSize.y);
+	vec2 off = vec2(0.0, 0.0);
+
+	// Left/Top
 	if (index == 0.0) {
 		pos2 = vec2(0.0, 0.0);
-		vUV = vec2(topLeftUV.x + (frame*sizeUV.x), 1.0 - topLeftUV.y);
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x) + off.x, topLeftUV.y - off.y);
 	}
+
+	// Left/Bottom
 	else if (index == 1.0) {
 		pos2 = vec2(0.0,  1.0);
-		vUV = vec2(topLeftUV.x + (frame*sizeUV.x), 1.0 - (topLeftUV.y + sizeUV.y));
+		vUV = vec2(topLeftUV.x + (frame*sizeUV.x) + off.x, (topLeftUV.y + sizeUV.y));
 	}
+
+	// Right/Bottom
 	else if (index == 2.0) {
 		pos2 = vec2( 1.0,  1.0);
-		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), 1.0 - (topLeftUV.y + sizeUV.y));
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), (topLeftUV.y + sizeUV.y));
 	}
+
+	// Right/Top
 	else if (index == 3.0) {
 		pos2 = vec2( 1.0, 0.0);
-		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), 1.0 - topLeftUV.y);
+		vUV = vec2(topLeftUV.x + sizeUV.x + (frame*sizeUV.x), topLeftUV.y - off.y);
 	}
 
 	if (invertY == 1.0) {

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

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

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

@@ -0,0 +1,52 @@
+// 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;
+
+// Uniforms
+
+// Output
+varying vec2 vUV;
+varying vec4 vColor;
+
+void main(void) {
+
+	vec2 pos2;
+
+	// Bottom/Left
+	if (index == 0.0) {
+		pos2 = vec2(0.0, 0.0);
+		vUV = vec2(topLeftUV.x, topLeftUV.y + sizeUV.y);
+	}
+
+	// Top/Left
+	else if (index == 1.0) {
+		pos2 = vec2(0.0, 1.0);
+		vUV = vec2(topLeftUV.x, topLeftUV.y);
+	}
+	
+	// Top/Right
+	else if (index == 2.0) {
+		pos2 = vec2(1.0, 1.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x, topLeftUV.y);
+	}
+
+	// Bottom/Right
+	else if (index == 3.0) {
+		pos2 = vec2(1.0, 0.0);
+		vUV = vec2(topLeftUV.x + sizeUV.x, topLeftUV.y + sizeUV.y);
+	}
+
+	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);
+}