瀏覽代碼

StringDictionary
- getOrAdd fixed: https://trello.com/c/s8BfxNNa
- set method added

Tools.first<T> method added to find the first member in an array based on a given predicate

Primitives rendered as Parts. Conditional InstanceData make the whole InstanceData layout be based on the Used Categories. Offset to the DynamicFloatArray must be computed based on the Used Categories.
InstanceData Refresh is now for a given Part.
https://trello.com/c/kidvE5tG

Gradient Brush
https://trello.com/c/lBkCEWEm

Rectangle2D: now support Gradient Fill Brush
https://trello.com/c/6t4baIDP

nockawa 9 年之前
父節點
當前提交
0deef97d4f

+ 95 - 1
src/Canvas2d/babylon.brushes2d.ts

@@ -63,12 +63,15 @@
     /**
      * This classs implements a Brush that will be drawn with a uniform solid color (i.e. the same color everywhere in the content where the brush is assigned to).
      */
+    @className("SolidColorBrush2D")
     export class SolidColorBrush2D extends LockableBase implements IBrush2D {
         constructor(color: Color4, lock: boolean = false) {
             super();
             this._color = color;
             if (lock) {
-                this.lock();
+                {
+                    this.lock();
+                }
             }
         }
 
@@ -96,4 +99,95 @@
         }
         private _color: Color4;
     }
+
+    @className("GradientColorBrush2D")
+    export class GradientColorBrush2D extends LockableBase implements IBrush2D {
+        constructor(color1: Color4, color2: Color4, translation: Vector2 = Vector2.Zero(), rotation: number = 0, scale: number = 1, lock: boolean = false) {
+            super();
+
+            this._color1 = color1;
+            this._color2 = color2;
+            this._translation = translation;
+            this._rotation = rotation;
+            this._scale = scale;
+
+            if (lock) {
+                this.lock();
+            }
+        }
+        public get color1(): Color4 {
+            return this._color1;
+        }
+
+        public set color1(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color1 = value;
+        }
+
+        public get color2(): Color4 {
+            return this._color2;
+        }
+
+        public set color2(value: Color4) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._color2 = value;
+        }
+
+        public get translation(): Vector2 {
+            return this._translation;
+        }
+
+        public set translation(value: Vector2) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._translation = value;
+        }
+
+        public get rotation(): number {
+            return this._rotation;
+        }
+
+        public set rotation(value: number) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._rotation = value;
+        }
+
+        public get scale(): number {
+            return this._scale;
+        }
+
+        public set scale(value: number) {
+            if (this.isLocked()) {
+                return;
+            }
+
+            this._scale = value;
+        }
+
+        public toString(): string {
+            return `C1:${this._color1};C2:${this._color2};T:${this._translation.toString()};R:${this._rotation};S:${this._scale};`;
+        }
+
+        public static BuildKey(color1: Color4, color2: Color4, translation: Vector2, rotation: number, scale: number) {
+            return `C1:${color1};C2:${color2};T:${translation.toString()};R:${rotation};S:${scale};`;
+        }
+
+        private _color1: Color4;
+        private _color2: Color4;
+        private _translation: Vector2;
+        private _rotation: number;
+        private _scale: number;
+    }
+
 }

+ 6 - 1
src/Canvas2d/babylon.canvas2d.ts

@@ -265,7 +265,7 @@
          * @param color The color to retrieve
          * @return A shared instance of the SolidColorBrush2D class that use the given color
          */
-        public static GetSolidColorFill(color: Color4): IBrush2D {
+        public static GetSolidColorBrush(color: Color4): IBrush2D {
             return Canvas2D._solidColorBrushes.getOrAddWithFactory(color.toHexString(), () => new SolidColorBrush2D(color.clone(), true));
         }
 
@@ -278,6 +278,11 @@
             return Canvas2D._solidColorBrushes.getOrAddWithFactory(hexValue, () => new SolidColorBrush2D(Color4.FromHexString(hexValue), true));
         }
 
+        public static GetGradientColorBrush(color1: Color4, color2: Color4, translation: Vector2 = Vector2.Zero(), rotation: number = 0, scale: number = 1): IBrush2D {
+            return Canvas2D._gradientColorBrushes.getOrAddWithFactory(GradientColorBrush2D.BuildKey(color1, color2, translation, rotation, scale), () => new GradientColorBrush2D(color1, color2, translation, rotation, scale, true));
+        }
+
         private static _solidColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
+        private static _gradientColorBrushes: StringDictionary<IBrush2D> = new StringDictionary<IBrush2D>();
     }
 }

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

@@ -155,14 +155,15 @@
             return [new Rectangle2DInstanceData(Shape2D.SHAPE2D_FILLPARTID)];
         }
 
-        protected refreshInstanceDataParts(): boolean {
-            if (!super.refreshInstanceDataParts()) {
+        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataParts(part)) {
                 return false;
             }
-
-            let d = <Rectangle2DInstanceData>this._instanceDataParts[0];
-            let size = this.size;
-            d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
+            if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
+                let d = <Rectangle2DInstanceData>part;
+                let size = this.size;
+                d.properties = new Vector3(size.width, size.height, this.roundRadius || 0);
+            }
             return true;
         }
 

+ 67 - 49
src/Canvas2d/babylon.renderablePrim2d.ts

@@ -2,18 +2,24 @@
     export class InstanceClassInfo {
         constructor(base: InstanceClassInfo) {
             this._baseInfo = base;
-            this._nextOffset = 0;
+            this._nextOffset = new StringDictionary<number>();
             this._attributes = new Array<InstancePropInfo>();
         }
 
-        mapProperty(propInfo: InstancePropInfo) {
-            propInfo.instanceOffset = this._baseOffset + this._nextOffset;
-            //console.log(`New PropInfo. Category: ${propInfo.category}, Name: ${propInfo.attributeName}, Offset: ${propInfo.instanceOffset}, Size: ${propInfo.size/4}`);
-            this._nextOffset += (propInfo.size / 4);
-            this._attributes.push(propInfo);
+        mapProperty(propInfo: InstancePropInfo, push: boolean) {
+            let curOff = this._nextOffset.getOrAdd(InstanceClassInfo._CurCategories, 0);
+            propInfo.instanceOffset.add(InstanceClassInfo._CurCategories, this._getBaseOffset(InstanceClassInfo._CurCategories) + curOff);
+            //console.log(`[${InstanceClassInfo._CurCategories}] New PropInfo. Category: ${propInfo.category}, Name: ${propInfo.attributeName}, Offset: ${propInfo.instanceOffset.get(InstanceClassInfo._CurCategories)}, Size: ${propInfo.size / 4}`);
+
+            this._nextOffset.set(InstanceClassInfo._CurCategories, curOff + (propInfo.size / 4));
+
+            if (push) {
+                this._attributes.push(propInfo);
+            }
         }
 
         getInstancingAttributeInfos(effect: Effect, categories: string[]): InstancingAttributeInfo[] {
+            let catInline = categories.join(";");
             let res = new Array<InstancingAttributeInfo>();
             let curInfo: InstanceClassInfo = this;
             while (curInfo) {
@@ -24,7 +30,7 @@
                         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
+                        iai.offset = attrib.instanceOffset.get(catInline) * 4; // attrib.instanceOffset is in float, iai.offset must be in bytes
                         res.push(iai);
                     }
                 }
@@ -50,18 +56,19 @@
             return res;
         }
 
-        private get _baseOffset(): number {
+        private _getBaseOffset(categories: string): number {
             var curOffset = 0;
             var curBase = this._baseInfo;
             while (curBase) {
-                curOffset += curBase._nextOffset;
+                curOffset += curBase._nextOffset.getOrAdd(categories, 0);
                 curBase = curBase._baseInfo;
             }
             return curOffset;
         }
 
+        static _CurCategories: string;
         private _baseInfo: InstanceClassInfo;
-        private _nextOffset;
+        private _nextOffset: StringDictionary<number>;
         private _attributes: Array<InstancePropInfo>;
     }
 
@@ -70,8 +77,13 @@
         category: string;
         size: number;
         shaderOffset: number;
-        instanceOffset: number;
+        instanceOffset: StringDictionary<number>;
         dataType: ShaderDataType;
+
+        constructor() {
+            this.instanceOffset = new StringDictionary<number>();
+        }
+
         setSize(val) {
             if (val instanceof Vector2) {
                 this.size = 8;
@@ -197,12 +209,15 @@
             descriptor.set = function (val) {
                 if (!info.size) {
                     info.setSize(val);
-                    node.classContent.mapProperty(info);
+                    node.classContent.mapProperty(info, true);
+                } else if (!info.instanceOffset.contains(InstanceClassInfo._CurCategories)) {
+                    node.classContent.mapProperty(info, false);
                 }
 
                 var obj: InstanceDataBase = this;
                 if (obj._dataBuffer) {
-                    info.writeData(obj._dataBuffer.buffer, obj._dataElement.offset + info.instanceOffset, val);
+                    let offset = obj._dataElement.offset + info.instanceOffset.get(InstanceClassInfo._CurCategories);
+                    info.writeData(obj._dataBuffer.buffer, offset, val);
                 }
             }
 
@@ -286,7 +301,7 @@
                 if (!this._modelRenderCache._partsDataStride) {
                     let ctiArray = new Array<ClassTreeInfo<InstanceClassInfo, InstancePropInfo>>();
                     var dataStrides = new Array<number>();
-                    var usedCat = new Array<string[]>();
+                    var usedCatList = new Array<string[]>();
 
                     for (var dataPart of parts) {
                         let cat = this.getUsedShaderCategories(dataPart);
@@ -295,7 +310,9 @@
                         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.refreshInstanceDataParts();
+                        //console.log("Build Prop Layout for " + Tools.getClassName(this._instanceDataParts[0]));
+                        InstanceClassInfo._CurCategories = cat.join(";");
+                        this.refreshInstanceDataParts(dataPart);
                         this.isVisible = curVisible;
 
                         var size = 0;
@@ -309,11 +326,11 @@
                             }
                         });
                         dataStrides.push(size);
-                        usedCat.push(cat);
+                        usedCatList.push(cat);
                         ctiArray.push(cti);
                     }
                     this._modelRenderCache._partsDataStride = dataStrides;
-                    this._modelRenderCache._partsUsedCategories = usedCat;
+                    this._modelRenderCache._partsUsedCategories = usedCatList;
                     this._modelRenderCache._partsClassInfo = ctiArray;
                 }
 
@@ -340,9 +357,12 @@
             }
 
             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.refreshInstanceDataParts()) {
-                    for (let part of this._instanceDataParts) {
+                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)) {
                         // Free the data element
                         if (part._dataElement) {
                             part._dataBuffer.freeElement(part._dataElement);
@@ -394,37 +414,35 @@
             return [];
         }
 
-        protected refreshInstanceDataParts(): boolean {
-            for (let part of this._instanceDataParts) {
-                if (!this.isVisible) {
-                    return false;
-                }
-
-                part.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);
-                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);
+        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
+            if (!this.isVisible) {
+                return false;
             }
 
+            part.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);
+            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;
         }
 

+ 52 - 18
src/Canvas2d/babylon.shape2d.ts

@@ -2,10 +2,12 @@
 
     @className("Shape2D")
     export class Shape2D extends RenderablePrim2D {
-        static SHAPE2D_BORDERPARTID = 1;
-        static SHAPE2D_FILLPARTID = 1;
-        static SHAPE2D_CATEGORY_BORDERSOLID = "BorderSolid";
-        static SHAPE2D_CATEGORY_FILLSOLID = "FillSolid";
+        static SHAPE2D_BORDERPARTID            = 1;
+        static SHAPE2D_FILLPARTID              = 2;
+        static SHAPE2D_CATEGORY_BORDERSOLID    = "BorderSolid";
+        static SHAPE2D_CATEGORY_BORDERGRADIENT = "BorderGradient";
+        static SHAPE2D_CATEGORY_FILLSOLID      = "FillSolid";
+        static SHAPE2D_CATEGORY_FILLGRADIENT   = "FillGradient";
 
         static SHAPE2D_PROPCOUNT: number = RenderablePrim2D.RENDERABLEPRIM2D_PROPCOUNT + 5;
         public static borderProperty: Prim2DPropInfo;
@@ -38,6 +40,9 @@
                 if (fill instanceof SolidColorBrush2D) {
                     cat.push(Shape2D.SHAPE2D_CATEGORY_FILLSOLID);
                 }
+                if (fill instanceof GradientColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT);
+                }
             }
 
             // Fill Part
@@ -46,32 +51,46 @@
                 if (border instanceof SolidColorBrush2D) {
                     cat.push(Shape2D.SHAPE2D_CATEGORY_BORDERSOLID);
                 }
+                if (border instanceof GradientColorBrush2D) {
+                    cat.push(Shape2D.SHAPE2D_CATEGORY_BORDERGRADIENT);
+                }
             }
 
             return cat;
         }
 
-        protected refreshInstanceDataParts(): boolean {
-            if (!super.refreshInstanceDataParts()) {
+        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataParts(part)) {
                 return false;
             }
 
-            let d = <Shape2DInstanceData>this._instanceDataParts[0];
-
-            if (this.border) {
-                let border = this.border;
-                if (border instanceof SolidColorBrush2D) {
-                    d.borderSolidColor = border.color;
+            // Fill Part
+            if (part.id === Shape2D.SHAPE2D_FILLPARTID) {
+                let d = <Shape2DInstanceData>part;
+
+                if (this.border) {
+                    let border = this.border;
+                    if (border instanceof SolidColorBrush2D) {
+                        d.borderSolidColor = border.color;
+                    }
                 }
-            }
 
-            if (this.fill) {
-                let fill = this.fill;
-                if (fill instanceof SolidColorBrush2D) {
-                    d.fillSolidColor = fill.color;
+                if (this.fill) {
+                    let fill = this.fill;
+                    if (fill instanceof SolidColorBrush2D) {
+                        d.fillSolidColor = fill.color;
+                    }
+
+                    else if (fill instanceof GradientColorBrush2D) {
+                        d.fillGradientColor1 = fill.color1;
+                        d.fillGradientColor2 = fill.color2;
+                        var t = Matrix.Compose(new Vector3(fill.scale, fill.scale, fill.scale), Quaternion.RotationAxis(new Vector3(0, 0, 1), fill.rotation), new Vector3(fill.translation.x, fill.translation.y, 0));
+
+                        let ty = new Vector4(t.m[1], t.m[5], t.m[9], t.m[13]);
+                        d.fillGradientTY = ty;
+                    }
                 }
             }
-
             return true;
         }
 
@@ -90,6 +109,21 @@
             return null;
         }
 
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientColor1(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientColor2(): Color4 {
+            return null;
+        }
+
+        @instanceData(Shape2D.SHAPE2D_CATEGORY_FILLGRADIENT)
+        get fillGradientTY(): Vector4 {
+            return null;
+        }
+
     }
 
 

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

@@ -169,7 +169,7 @@
 
             renderCache.texture = this.texture;
 
-            var ei = this.getDataPartEffectInfo(Shape2D.SHAPE2D_FILLPARTID, ["index"]);
+            var ei = this.getDataPartEffectInfo(Sprite2D.SPRITE2D_MAINPARTID, ["index"]);
             renderCache.effect = engine.createEffect({ vertex: "sprite2d", fragment: "sprite2d" }, ei.attributes, [], ["diffuseSampler"], ei.defines);
 
             return renderCache;
@@ -179,21 +179,23 @@
             return [new Sprite2DInstanceData(Sprite2D.SPRITE2D_MAINPARTID)];
         }
 
-        protected refreshInstanceDataParts(): boolean {
-            if (!super.refreshInstanceDataParts()) {
+        protected refreshInstanceDataParts(part: InstanceDataBase): boolean {
+            if (!super.refreshInstanceDataParts(part)) {
                 return false;
             }
 
-            let d = <Sprite2DInstanceData>this._instanceDataParts[0];
-            let ts = this.texture.getSize();
-            let sl = this.spriteLocation;
-            let ss = this.spriteSize;
-            d.topLeftUV = new Vector2(sl.x / ts.width, sl.y / ts.height);
-            let suv = new Vector2(ss.width / ts.width, ss.height / ts.height);
-            d.sizeUV = suv;
-            d.frame = this.spriteFrame;
-            d.textureSize = new Vector2(ts.width, ts.height);
-            d.invertY = this.invertY ? 1 : 0;
+            if (part.id === Sprite2D.SPRITE2D_MAINPARTID) {
+                let d = <Sprite2DInstanceData>this._instanceDataParts[0];
+                let ts = this.texture.getSize();
+                let sl = this.spriteLocation;
+                let ss = this.spriteSize;
+                d.topLeftUV = new Vector2(sl.x / ts.width, sl.y / ts.height);
+                let suv = new Vector2(ss.width / ts.width, ss.height / ts.height);
+                d.sizeUV = suv;
+                d.frame = this.spriteFrame;
+                d.textureSize = new Vector2(ts.width, ts.height);
+                d.invertY = this.invertY ? 1 : 0;
+            }
             return true;
         }
 

+ 24 - 9
src/Shaders/rect2d.vertex.fx

@@ -5,7 +5,15 @@ attribute vec4 transformX;
 attribute vec4 transformY;
 attribute vec2 origin;
 
+#ifdef FillSolid
 attribute vec4 fillSolidColor;
+#endif
+
+#ifdef FillGradient
+attribute vec4 fillGradientColor1;
+attribute vec4 fillGradientColor2;
+attribute vec4 fillGradientTY;
+#endif
 
 attribute vec3 properties;
 
@@ -26,36 +34,43 @@ void main(void) {
 
 	vec2 pos2;
 	if (index == 0.0) {
-		pos2 = vec2(0.5, 0.5) * properties.xy;
+		pos2 = vec2(0.5, 0.5);
 	}
 	else {
 		float w = properties.x;
 		float h = properties.y;
 		float r = properties.z;
+		float nru = r / w;
+		float nrv = r / h;
 
 		if (index < rsub0) {
-			pos2 = vec2(w-r, r);
+			pos2 = vec2(1.0-nru, nrv);
 		}
 		else if (index < rsub1) {
-			pos2 = vec2(r, r);
+			pos2 = vec2(nru, nrv);
 		}
 		else if (index < rsub2) {
-			pos2 = vec2(r, h - r);
+			pos2 = vec2(nru, 1.0 - nrv);
 		}
 		else {
-			pos2 = vec2(w - r, h - r);
+			pos2 = vec2(1.0 - nru, 1.0 - nrv);
 		}
 
 		float angle = TWOPI - ((index - 1.0) * TWOPI / (rsub-1.0));
-		pos2.x += cos(angle) * properties.z;
-		pos2.y += sin(angle) * properties.z;
-
+		pos2.x += cos(angle) * nru;
+		pos2.y += sin(angle) * nrv;
 	}
 
+#ifdef FillSolid
 	vColor = fillSolidColor;
+#endif
+#ifdef FillGradient
+	float v = dot(vec4(pos2.xy, 1, 1), fillGradientTY);
+	vColor = mix(fillGradientColor2, fillGradientColor1, v);	// As Y is inverted, Color2 first, then Color1
+#endif
 
 	vec4 pos;
-	pos.xy = pos2.xy - (origin * properties.xy);
+	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);

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

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

+ 8 - 0
src/Tools/babylon.tools.ts

@@ -921,6 +921,14 @@
             return name;
         }
 
+        public static first<T>(array: Array<T>, predicate: (item) => boolean) {
+            for (let el of array) {
+                if (predicate(el)) {
+                    return el;
+                }
+            }
+        }
+
     }
 
     /**