Browse Source

RectPackingMap class + misc

* Moved the ISize interface to Maths.ts, added a Size class based from it.
Added the RectPackingMap class to pack several rectangle into a single map.

* RectPackingMap: added some doc.
Loïc Baumann 9 năm trước cách đây
mục cha
commit
b8aedec714

+ 1 - 0
Tools/Gulp/config.json

@@ -22,6 +22,7 @@
       "../../src/Tools/babylon.tools.tga.js",
       "../../src/Tools/babylon.smartArray.js",
       "../../src/Tools/babylon.stringDictionary.js",
+	  "../../src/Tools/babylon.rectPackingMap.js",
       "../../src/Tools/babylon.tools.js",      
       "../../src/States/babylon.alphaCullingState.js",
       "../../src/States/babylon.depthCullingState.js",

+ 2 - 0
dist/preview release/what's new.md

@@ -31,6 +31,8 @@
     - LinesMesh class now supports Intersection. Added the intersectionThreshold property to set a tolerance margin during intersection with wire lines. ([nockawa](https://github.com/nockawa))
     - Geometry.boundingBias property to enlarge the boundingInfo objects ([nockawa](https://github.com/nockawa))
     - Tools.ExtractMinAndMax & ExtractMinAndMaxIndexed now supports an optional Bias for Extent computation.
+	- Added StringDictionary<T> class to implement an efficient generic typed string dictionary based on Javascript associative array. ([nockawa](https://github.com/nockawa))
+	- Added RectanglePackingMap class to fix several rectangles in a big map in the most optimal way.  ([nockawa](https://github.com/nockawa))
 	- Scene.onPointerObservable property added to enable a unique Observable event for user input (see ArcRotateCamera inputs for examples) ([nockawa](https://github.com/nockawa))
 
   - **API doc**

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 2965 - 2937
src/Math/babylon.math.js


+ 36 - 0
src/Math/babylon.math.ts

@@ -1558,6 +1558,42 @@
         }
     }
 
+    export interface ISize {
+        width: number;
+        height: number;
+    }
+
+    export class Size implements ISize {
+        width: number;
+        height: number;
+
+        public constructor(width: number, height: number) {
+            this.width = width;
+            this.height = height;
+        }
+
+        public clone(): Size {
+            return new Size(this.width, this.height);
+        }
+
+        public equals(other: Size): boolean {
+            if (!other) {
+                return false;
+            }
+
+            return (this.width === other.width) && (this.height === other.height);
+        }
+
+        public get surface(): number {
+            return this.width * this.height;
+        }
+
+        public static Zero(): Size {
+            return new Size(0, 0);
+        }
+    }
+
+
     export class Quaternion {
         constructor(public x: number = 0, public y: number = 0, public z: number = 0, public w: number = 1) {
         }

+ 209 - 0
src/Tools/babylon.rectPackingMap.js

@@ -0,0 +1,209 @@
+var __extends = (this && this.__extends) || function (d, b) {
+    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+var BABYLON;
+(function (BABYLON) {
+    /**
+     * The purpose of this class is to pack several Rectangles into a big map, while trying to fit everything as optimaly as possible.
+     * This class is typically used to build lightmaps, sprite map or to pack several little textures into a big one.
+     * Note that this class allows allocated Rectangles to be freed: that is the map is dynamically maintained so you can add/remove rectangle based on their lifecycle.
+     */
+    var RectPackingMap = (function (_super) {
+        __extends(RectPackingMap, _super);
+        /**
+         * Create an instance of the object with a dimension using the given size
+         * @param size The dimension of the rectangle that will contain all the sub ones.
+         */
+        function RectPackingMap(size) {
+            _super.call(this, null, null, BABYLON.Vector2.Zero(), size);
+            this._root = this;
+        }
+        /**
+         * Add a rectangle, finding the best location to store it into the map
+         * @param size the dimension of the rectangle to store
+         * @return the Node containing the rectangle information, or null if we couldn't find a free spot
+         */
+        RectPackingMap.prototype.addRect = function (size) {
+            var node = this.findAndSplitNode(size);
+            return node;
+        };
+        Object.defineProperty(RectPackingMap.prototype, "freeSpace", {
+            /**
+             * Return the current space free normalized between [0;1]
+             * @returns {}
+             */
+            get: function () {
+                var freeSize = 0;
+                freeSize = this.evalFreeSize(freeSize);
+                return freeSize / (this._size.width * this._size.height);
+            },
+            enumerable: true,
+            configurable: true
+        });
+        return RectPackingMap;
+    }(PackedRect));
+    BABYLON.RectPackingMap = RectPackingMap;
+    /**
+     * This class describe a rectangle that were added to the map.
+     * You have access to its coordinates either in pixel or normalized (UV)
+     */
+    var PackedRect = (function () {
+        function PackedRect(root, parent, pos, size) {
+            this._pos = pos;
+            this._size = size;
+            this._root = root;
+            this._parent = parent;
+        }
+        Object.defineProperty(PackedRect.prototype, "pos", {
+            /**
+             * @returns the position of this node into the map
+             */
+            get: function () {
+                return this._pos;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(PackedRect.prototype, "contentSize", {
+            /**
+             * @returns the size of the rectangle this node handles
+             */
+            get: function () {
+                return this._contentSize;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        Object.defineProperty(PackedRect.prototype, "UVs", {
+            /**
+             * Compute the UV of the top/left, top/right, bottom/right, bottom/left points of the rectangle this node handles into the map
+             * @returns And array of 4 Vector2, containing UV coordinates for the four corners of the Rectangle into the map
+             */
+            get: function () {
+                var mainWidth = this._root._size.width;
+                var mainHeight = this._root._size.height;
+                var topLeft = new BABYLON.Vector2(this._pos.x / mainWidth, this._pos.y / mainHeight);
+                var rightBottom = new BABYLON.Vector2((this._pos.x + this._contentSize.width - 1) / mainWidth, (this._pos.y + this._contentSize.height - 1) / mainHeight);
+                var uvs = new Array();
+                uvs.push(topLeft);
+                uvs.push(new BABYLON.Vector2(rightBottom.x, topLeft.y));
+                uvs.push(rightBottom);
+                uvs.push(new BABYLON.Vector2(topLeft.x, rightBottom.y));
+                return uvs;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        /**
+         * Free this rectangle from the map.
+         * Call this method when you no longer need the rectangle to be in the map.
+         */
+        PackedRect.prototype.freeContent = function () {
+            if (!this.contentSize) {
+                return;
+            }
+            this._contentSize = null;
+            // If everything below is also free, reset the whole node, and attempt to reset parents if they also become free
+            this.attemptDefrag();
+        };
+        Object.defineProperty(PackedRect.prototype, "isUsed", {
+            get: function () {
+                return this._contentSize != null || this._leftNode != null;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        PackedRect.prototype.findAndSplitNode = function (contentSize) {
+            var node = this.findNode(contentSize);
+            // Not enought space...
+            if (!node) {
+                return null;
+            }
+            node.splitNode(contentSize);
+            return node;
+        };
+        PackedRect.prototype.findNode = function (size) {
+            var resNode = null;
+            // If this node is used, recurse to each of his subNodes to find an available one in its branch
+            if (this.isUsed) {
+                if (this._leftNode) {
+                    resNode = this._leftNode.findNode(size);
+                }
+                if (!resNode && this._rightNode) {
+                    resNode = this._rightNode.findNode(size);
+                }
+                if (!resNode && this._bottomNode) {
+                    resNode = this._bottomNode.findNode(size);
+                }
+            }
+            else if (this._initialSize && (size.width <= this._initialSize.width) && (size.height <= this._initialSize.height)) {
+                resNode = this;
+            }
+            else if ((size.width <= this._size.width) && (size.height <= this._size.height)) {
+                resNode = this;
+            }
+            return resNode;
+        };
+        PackedRect.prototype.splitNode = function (contentSize) {
+            // If there's no contentSize but an initialSize it means this node were previously allocated, but freed, we need to create a _leftNode as subNode and use to allocate the space we need (and this node will have a right/bottom subNode for the space left as this._initialSize may be greater than contentSize)
+            if (!this._contentSize && this._initialSize) {
+                this._leftNode = new PackedRect(this._root, this, new BABYLON.Vector2(this._pos.x, this._pos.y), new BABYLON.Size(this._initialSize.width, this._initialSize.height));
+                return this._leftNode.splitNode(contentSize);
+            }
+            else {
+                this._contentSize = contentSize.clone();
+                this._initialSize = contentSize.clone();
+                if (contentSize.width !== this._size.width) {
+                    this._rightNode = new PackedRect(this._root, this, new BABYLON.Vector2(this._pos.x + contentSize.width, this._pos.y), new BABYLON.Size(this._size.width - contentSize.width, contentSize.height));
+                }
+                if (contentSize.height !== this._size.height) {
+                    this._bottomNode = new PackedRect(this._root, this, new BABYLON.Vector2(this._pos.x, this._pos.y + contentSize.height), new BABYLON.Size(this._size.width, this._size.height - contentSize.height));
+                }
+                return this;
+            }
+        };
+        PackedRect.prototype.attemptDefrag = function () {
+            if (!this.isUsed && this.isRecursiveFree) {
+                this.clearNode();
+                if (this._parent) {
+                    this._parent.attemptDefrag();
+                }
+            }
+        };
+        PackedRect.prototype.clearNode = function () {
+            this._initialSize = null;
+            this._rightNode = null;
+            this._bottomNode = null;
+        };
+        Object.defineProperty(PackedRect.prototype, "isRecursiveFree", {
+            get: function () {
+                return !this.contentSize && (!this._leftNode || this._leftNode.isRecursiveFree) && (!this._rightNode || this._rightNode.isRecursiveFree) && (!this._bottomNode || this._bottomNode.isRecursiveFree);
+            },
+            enumerable: true,
+            configurable: true
+        });
+        PackedRect.prototype.evalFreeSize = function (size) {
+            var levelSize = 0;
+            if (!this.isUsed) {
+                if (this._initialSize) {
+                    levelSize = this._initialSize.surface;
+                }
+                else {
+                    levelSize = this._size.surface;
+                }
+            }
+            if (this._rightNode) {
+                levelSize += this._rightNode.evalFreeSize(0);
+            }
+            if (this._bottomNode) {
+                levelSize += this._bottomNode.evalFreeSize(0);
+            }
+            return levelSize + size;
+        };
+        return PackedRect;
+    }());
+    BABYLON.PackedRect = PackedRect;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.rectPackingMap.js.map

+ 220 - 0
src/Tools/babylon.rectPackingMap.ts

@@ -0,0 +1,220 @@
+module BABYLON {
+    /**
+     * The purpose of this class is to pack several Rectangles into a big map, while trying to fit everything as optimaly as possible.
+     * This class is typically used to build lightmaps, sprite map or to pack several little textures into a big one.
+     * Note that this class allows allocated Rectangles to be freed: that is the map is dynamically maintained so you can add/remove rectangle based on their lifecycle.
+     */
+    export class RectPackingMap extends PackedRect {
+        /**
+         * Create an instance of the object with a dimension using the given size
+         * @param size The dimension of the rectangle that will contain all the sub ones.
+         */
+        constructor(size: Size) {
+            super(null, null, Vector2.Zero(), size);
+
+            this._root = this;
+        }
+
+        /**
+         * Add a rectangle, finding the best location to store it into the map
+         * @param size the dimension of the rectangle to store
+         * @return the Node containing the rectangle information, or null if we couldn't find a free spot
+         */
+        public addRect(size: Size): PackedRect {
+            var node = this.findAndSplitNode(size);
+            return node;
+        }
+
+        /**
+         * Return the current space free normalized between [0;1]
+         * @returns {} 
+         */
+        public get freeSpace(): number {
+            var freeSize = 0;
+            freeSize = this.evalFreeSize(freeSize);
+
+            return freeSize / (this._size.width * this._size.height);
+        }
+    }
+
+    /**
+     * This class describe a rectangle that were added to the map.
+     * You have access to its coordinates either in pixel or normalized (UV)
+     */
+    export class PackedRect {
+        constructor(root: PackedRect, parent: PackedRect, pos: Vector2, size: Size) {
+            this._pos = pos;
+            this._size = size;
+            this._root = root;
+            this._parent = parent;
+        }
+
+        /**
+         * @returns the position of this node into the map
+         */
+        public get pos() {
+            return this._pos;
+        }
+
+        /**
+         * @returns the size of the rectangle this node handles
+         */
+        public get contentSize(): Size {
+            return this._contentSize;
+        }
+
+        /**
+         * Compute the UV of the top/left, top/right, bottom/right, bottom/left points of the rectangle this node handles into the map
+         * @returns And array of 4 Vector2, containing UV coordinates for the four corners of the Rectangle into the map
+         */
+        public get UVs(): Vector2[] {
+            var mainWidth = this._root._size.width;
+            var mainHeight = this._root._size.height;
+
+            var topLeft = new Vector2(this._pos.x / mainWidth, this._pos.y / mainHeight);
+            var rightBottom = new Vector2((this._pos.x + this._contentSize.width - 1) / mainWidth, (this._pos.y + this._contentSize.height - 1) / mainHeight);
+            var uvs = new Array<Vector2>();
+            uvs.push(topLeft);
+            uvs.push(new Vector2(rightBottom.x, topLeft.y));
+            uvs.push(rightBottom);
+            uvs.push(new Vector2(topLeft.x, rightBottom.y));
+
+            return uvs;
+        }
+
+        /**
+         * Free this rectangle from the map.
+         * Call this method when you no longer need the rectangle to be in the map.
+         */
+        public freeContent() {
+            if (!this.contentSize) {
+                return;
+            }
+
+            this._contentSize = null;
+
+            // If everything below is also free, reset the whole node, and attempt to reset parents if they also become free
+            this.attemptDefrag();
+        }
+
+
+        protected get isUsed(): boolean {
+            return this._contentSize != null || this._leftNode != null;
+        }
+
+        protected findAndSplitNode(contentSize: Size): PackedRect {
+            var node = this.findNode(contentSize);
+
+            // Not enought space...
+            if (!node) {
+                return null;
+            }
+
+            node.splitNode(contentSize);
+            return node;
+        }
+
+        private findNode(size: Size): PackedRect {
+            var resNode: PackedRect = null;
+
+            // If this node is used, recurse to each of his subNodes to find an available one in its branch
+            if (this.isUsed) {
+                if (this._leftNode) {
+                    resNode = this._leftNode.findNode(size);
+                }
+                if (!resNode && this._rightNode) {
+                    resNode = this._rightNode.findNode(size);
+                }
+                if (!resNode && this._bottomNode) {
+                    resNode = this._bottomNode.findNode(size);
+                }
+            }
+
+            // The node is free, but was previously allocated (_initialSize is set), rely on initialSize to make the test as it's the space we have
+            else if (this._initialSize && (size.width <= this._initialSize.width) && (size.height <= this._initialSize.height)) {
+                resNode = this;
+            }
+
+            // The node is free and empty, rely on its size for the test
+            else if ((size.width <= this._size.width) && (size.height <= this._size.height)) {
+                resNode = this;
+            }
+            return resNode;
+        }
+
+        private splitNode(contentSize: Size): PackedRect {
+            // If there's no contentSize but an initialSize it means this node were previously allocated, but freed, we need to create a _leftNode as subNode and use to allocate the space we need (and this node will have a right/bottom subNode for the space left as this._initialSize may be greater than contentSize)
+            if (!this._contentSize && this._initialSize) {
+                this._leftNode = new PackedRect(this._root, this, new Vector2(this._pos.x, this._pos.y), new Size(this._initialSize.width, this._initialSize.height));
+                return this._leftNode.splitNode(contentSize);
+            } else {
+                this._contentSize = contentSize.clone();
+                this._initialSize = contentSize.clone();
+
+                if (contentSize.width !== this._size.width) {
+                    this._rightNode = new PackedRect(this._root, this, new Vector2(this._pos.x + contentSize.width, this._pos.y), new Size(this._size.width - contentSize.width, contentSize.height));
+                }
+
+                if (contentSize.height !== this._size.height) {
+                    this._bottomNode = new PackedRect(this._root, this, new Vector2(this._pos.x, this._pos.y + contentSize.height), new Size(this._size.width, this._size.height - contentSize.height));
+                }
+                return this;
+            }
+        }
+
+        private attemptDefrag() {
+            if (!this.isUsed && this.isRecursiveFree) {
+                this.clearNode();
+
+                if (this._parent) {
+                    this._parent.attemptDefrag();
+                }
+            }
+        }
+
+        private clearNode() {
+            this._initialSize = null;
+            this._rightNode = null;
+            this._bottomNode = null;
+        }
+
+        private get isRecursiveFree() {
+            return !this.contentSize && (!this._leftNode || this._leftNode.isRecursiveFree) && (!this._rightNode || this._rightNode.isRecursiveFree) && (!this._bottomNode || this._bottomNode.isRecursiveFree);
+        }
+
+        protected evalFreeSize(size: number): number {
+            var levelSize = 0;
+
+            if (!this.isUsed) {
+                if (this._initialSize) {
+                    levelSize = this._initialSize.surface;
+                } else {
+                    levelSize = this._size.surface;
+                }
+            }
+
+            if (this._rightNode) {
+                levelSize += this._rightNode.evalFreeSize(0);
+            }
+
+            if (this._bottomNode) {
+                levelSize += this._bottomNode.evalFreeSize(0);
+            }
+
+            return levelSize + size;
+        }
+
+        protected _root: PackedRect;
+        protected _parent: PackedRect;
+        private _contentSize: Size;
+        private _initialSize: Size;
+        private _leftNode: PackedRect;
+        private _rightNode: PackedRect;
+        private _bottomNode: PackedRect;
+
+        private _pos: Vector2;
+        protected _size: Size;
+    }
+
+
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 901 - 900
src/Tools/babylon.tools.js


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

@@ -3,11 +3,6 @@
         animations: Array<Animation>;
     }
 
-    export interface ISize {
-        width: number;
-        height: number;
-    }
-
     // Screenshots
     var screenshotCanvas: HTMLCanvasElement;