Pārlūkot izejas kodu

Merge pull request #1108 from nockawa/master

DynamicFloatArray class
David Catuhe 9 gadi atpakaļ
vecāks
revīzija
138532ba83

+ 1 - 0
Tools/Gulp/config.json

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

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

@@ -32,7 +32,8 @@
     - Geometry.boundingBias property to enlarge the boundingInfo objects ([nockawa](https://github.com/nockawa))
     - Tools.ExtractMinAndMax & ExtractMinAndMaxIndexed now supports an optional Bias for Extent computation.
 	- Added StringDictionary<T> class to implement an efficient generic typed string dictionary based on Javascript associative array. ([nockawa](https://github.com/nockawa))
-	- Added RectanglePackingMap class to fix several rectangles in a big map in the most optimal way.  ([nockawa](https://github.com/nockawa))
+	- Added RectanglePackingMap class to fix several rectangles in a big map in the most optimal way. ([nockawa](https://github.com/nockawa))
+	- Added DynamicFloatArray class to store float32 based elements of a given size (stride) into one big Float32Array, with allocation/free/pack operations to then access an optimal buffer that can be used to update a WebGLBuffer dynamically. ([nockawa](https://github.com/nockawa))
 	- Scene.onPointerObservable property added to enable a unique Observable event for user input (see ArcRotateCamera inputs for examples) ([nockawa](https://github.com/nockawa))
 
   - **API doc**

+ 143 - 0
src/Tools/babylon.dynamicFloatArray.js

@@ -0,0 +1,143 @@
+var BABYLON;
+(function (BABYLON) {
+    /**
+    * The purpose of this class is to store float32 based elements of a given size (defined by the stride argument) in a dynamic fashion, that is, you can add/free elements. You can then access to a defragmented/packed version of the underlying Float32Array by calling the pack() method.
+    * The intent is to maintain through time data that will be bound to a WebGlBuffer with the ability to change add/remove elements.
+    * It was first built to effiently maintain the WebGlBuffer that contain instancing based data.
+    * Allocating an Element will return a instance of DynamicFloatArrayEntry which contains the offset into the Float32Array of where the element starts, you are then responsible to copy your data using this offset.
+    * Beware, calling pack() may change the offset of some Entries because this method will defrag the Float32Array to replace empty elements by moving allocated ones at their location.
+     * This method will retrun an ArrayBufferView on the existing Float32Array that describes the occupied elements. Use this View to update the WebGLBuffer and NOT the "buffer" field of the class. The pack() method won't shrink/reallocate the buffer to keep it GC friendly, all the empty space will be put at the end of the buffer, the method just ensure there're no "free holes".
+    */
+    var DynamicFloatArray = (function () {
+        /**
+         * Construct an instance of the dynamic float array
+         * @param stride size of one entry in float (i.e. not bytes!)
+         * @param initialEntryCount the number of available entries at construction
+         */
+        function DynamicFloatArray(stride, initialEntryCount) {
+            this.stride = stride;
+            this.entryCount = initialEntryCount;
+            this.buffer = new Float32Array(stride * initialEntryCount);
+            this.allEntries = new Array(initialEntryCount);
+            this.freeEntries = new Array(initialEntryCount);
+            for (var i = 0; i < initialEntryCount; i++) {
+                var entry = new DynamicFloatArrayEntry();
+                entry.offset = i * stride;
+                this.allEntries[i] = entry;
+                this.freeEntries[initialEntryCount - i - 1] = entry;
+            }
+        }
+        DynamicFloatArray.prototype.allocElement = function () {
+            if (this.freeEntries.length === 0) {
+                this._growBuffer();
+            }
+            var entry = this.freeEntries.pop();
+            return entry;
+        };
+        DynamicFloatArray.prototype.freeElement = function (entry) {
+            this.freeEntries.push(entry);
+        };
+        /**
+         * This method will pack all the occupied elements into a linear sequence and free the rest.
+         * Instances of DynamicFloatArrayEntry may have their 'offset' member changed as data could be copied from one location to another, so be sure to read/write your data based on the value inside this member after you called pack().
+         */
+        DynamicFloatArray.prototype.pack = function () {
+            // no free slot? no need to pack
+            if (this.freeEntries.length === 0) {
+                return this.buffer;
+            }
+            var s = this.stride;
+            var sortedFree = this.freeEntries.sort(function (a, b) { return a.offset - b.offset; });
+            var sortedAll = this.allEntries.sort(function (a, b) { return a.offset - b.offset; });
+            // Make sure there's a free element at the very end, we need it to create a range where we'll move the occupied elements that may appear before
+            var lastFree = new DynamicFloatArrayEntry();
+            lastFree.offset = this.entryCount * s;
+            sortedFree.push(lastFree);
+            var firstFreeSlotOffset = sortedFree[0].offset;
+            var freeZoneSize = 1;
+            var prevOffset = sortedFree[0].offset;
+            for (var i = 1; i < sortedFree.length; i++) {
+                var curFree = sortedFree[i];
+                var curOffset = curFree.offset;
+                // Compute the distance between this offset and the previous
+                var distance = curOffset - prevOffset;
+                // If the distance is the stride size, they are adjacents, it good, move to the next
+                if (distance === s) {
+                    // Free zone is one element bigger
+                    ++freeZoneSize;
+                    // as we're about to iterate to the next, the cur becomes the prev...
+                    prevOffset = curOffset;
+                    continue;
+                }
+                // Distance is bigger, which means there's x element between the previous free and this one
+                var occupiedRange = (distance / s) - 1;
+                // Two cases the free zone is smaller than the data to move or bigger
+                // Copy what can fit in the free zone
+                var curMoveOffset = curOffset - s;
+                var copyCount = Math.min(freeZoneSize, occupiedRange);
+                for (var j = 0; j < copyCount; j++) {
+                    var freeI = firstFreeSlotOffset / s;
+                    var curI = curMoveOffset / s;
+                    var moveEntry = sortedAll[curI];
+                    this._moveEntry(moveEntry, firstFreeSlotOffset);
+                    var replacedEntry = sortedAll[freeI];
+                    // set the offset of the element entry we replace with a value that will make it discard at the end of the method
+                    replacedEntry.offset = curMoveOffset;
+                    // Swap the entry we moved and the one it replaced in the sorted array to reflect the action we've made
+                    sortedAll[freeI] = moveEntry;
+                    sortedAll[curI] = replacedEntry;
+                    curMoveOffset -= s;
+                    firstFreeSlotOffset += s;
+                }
+                // Free Zone is smaller or equal so it's no longer a free zone, set the new one to the current location
+                if (freeZoneSize <= occupiedRange) {
+                    firstFreeSlotOffset = curOffset;
+                    freeZoneSize = 1;
+                }
+                else {
+                    freeZoneSize = ((curOffset - firstFreeSlotOffset) / s) + 1;
+                }
+                // as we're about to iterate to the next, the cur becomes the prev...
+                prevOffset = curOffset;
+            }
+            // Allocate a new buffer with the perfect size, copy the content, update free data
+            this.entryCount = firstFreeSlotOffset / s;
+            var optimizedBuffer = this.buffer.subarray(0, firstFreeSlotOffset);
+            this.freeEntries.splice(0);
+            this.allEntries = sortedAll.slice(0, this.entryCount);
+            return optimizedBuffer;
+        };
+        DynamicFloatArray.prototype._moveEntry = function (entry, destOffset) {
+            for (var i = 0; i < this.stride; i++) {
+                this.buffer[destOffset + i] = this.buffer[entry.offset + i];
+            }
+            entry.offset = destOffset;
+        };
+        DynamicFloatArray.prototype._growBuffer = function () {
+            // Allocate the new buffer with 50% more entries, copy the content of the current one
+            var newEntryCount = this.entryCount * 1.5;
+            var newBuffer = new Float32Array(newEntryCount * this.stride);
+            newBuffer.set(this.buffer);
+            var addedCount = newEntryCount - this.entryCount;
+            this.allEntries.length += addedCount;
+            this.freeEntries.length += addedCount;
+            for (var i = this.entryCount; i < newEntryCount; i++) {
+                var entry = new DynamicFloatArrayEntry();
+                entry.offset = i * this.stride;
+                this.allEntries[i] = entry;
+                this.freeEntries[i] = entry;
+            }
+            this.buffer = newBuffer;
+            this.entryCount = newEntryCount;
+        };
+        return DynamicFloatArray;
+    }());
+    BABYLON.DynamicFloatArray = DynamicFloatArray;
+    var DynamicFloatArrayEntry = (function () {
+        function DynamicFloatArrayEntry() {
+        }
+        return DynamicFloatArrayEntry;
+    }());
+    BABYLON.DynamicFloatArrayEntry = DynamicFloatArrayEntry;
+})(BABYLON || (BABYLON = {}));
+//# sourceMappingURL=babylon.dynamicFloatArray.js.map

+ 177 - 0
src/Tools/babylon.dynamicFloatArray.ts

@@ -0,0 +1,177 @@
+module BABYLON {
+    /**
+    * The purpose of this class is to store float32 based elements of a given size (defined by the stride argument) in a dynamic fashion, that is, you can add/free elements. You can then access to a defragmented/packed version of the underlying Float32Array by calling the pack() method.
+    * The intent is to maintain through time data that will be bound to a WebGlBuffer with the ability to change add/remove elements.
+    * It was first built to effiently maintain the WebGlBuffer that contain instancing based data.
+    * Allocating an Element will return a instance of DynamicFloatArrayEntry which contains the offset into the Float32Array of where the element starts, you are then responsible to copy your data using this offset.
+    * Beware, calling pack() may change the offset of some Entries because this method will defrag the Float32Array to replace empty elements by moving allocated ones at their location.
+     * This method will retrun an ArrayBufferView on the existing Float32Array that describes the occupied elements. Use this View to update the WebGLBuffer and NOT the "buffer" field of the class. The pack() method won't shrink/reallocate the buffer to keep it GC friendly, all the empty space will be put at the end of the buffer, the method just ensure there're no "free holes". 
+    */
+    export class DynamicFloatArray {
+        /**
+         * Construct an instance of the dynamic float array
+         * @param stride size of one entry in float (i.e. not bytes!)
+         * @param initialEntryCount the number of available entries at construction
+         */
+        constructor(stride: number, initialEntryCount: number) {
+            this.stride = stride;
+            this.entryCount = initialEntryCount;
+            this.buffer = new Float32Array(stride * initialEntryCount);
+
+            this.allEntries = new Array<DynamicFloatArrayEntry>(initialEntryCount);
+            this.freeEntries = new Array<DynamicFloatArrayEntry>(initialEntryCount);
+
+            for (let i = 0; i < initialEntryCount; i++) {
+                let entry = new DynamicFloatArrayEntry();
+                entry.offset = i * stride;
+
+                this.allEntries[i] = entry;
+                this.freeEntries[initialEntryCount - i - 1] = entry;
+            }
+        }
+
+        allocElement(): DynamicFloatArrayEntry {
+            if (this.freeEntries.length === 0) {
+                this._growBuffer();
+            }
+
+            var entry = this.freeEntries.pop();
+            return entry;
+        }
+
+        freeElement(entry: DynamicFloatArrayEntry) {
+            this.freeEntries.push(entry);
+        }
+
+        /**
+         * This method will pack all the occupied elements into a linear sequence and free the rest.
+         * Instances of DynamicFloatArrayEntry may have their 'offset' member changed as data could be copied from one location to another, so be sure to read/write your data based on the value inside this member after you called pack().
+         */
+        pack(): Float32Array {
+            // no free slot? no need to pack
+            if (this.freeEntries.length === 0) {
+                return this.buffer;
+            }
+
+            let s = this.stride;
+            let sortedFree = this.freeEntries.sort((a, b) => a.offset - b.offset);
+            let sortedAll = this.allEntries.sort((a, b) => a.offset - b.offset);
+
+            // Make sure there's a free element at the very end, we need it to create a range where we'll move the occupied elements that may appear before
+            let lastFree = new DynamicFloatArrayEntry();
+            lastFree.offset = this.entryCount * s;
+            sortedFree.push(lastFree);
+
+            let firstFreeSlotOffset = sortedFree[0].offset;
+            let freeZoneSize = 1;
+
+            let prevOffset = sortedFree[0].offset;
+            for (let i = 1; i < sortedFree.length; i++) {
+                let curFree = sortedFree[i];
+                let curOffset = curFree.offset;
+
+                // Compute the distance between this offset and the previous
+                let distance = curOffset - prevOffset;
+
+                // If the distance is the stride size, they are adjacents, it good, move to the next
+                if (distance === s) {
+                    // Free zone is one element bigger
+                    ++freeZoneSize;
+
+                    // as we're about to iterate to the next, the cur becomes the prev...
+                    prevOffset = curOffset;
+
+                    continue;
+                }
+
+                // Distance is bigger, which means there's x element between the previous free and this one
+                let occupiedRange = (distance / s) - 1;
+
+                // Two cases the free zone is smaller than the data to move or bigger
+
+                // Copy what can fit in the free zone
+                let curMoveOffset = curOffset - s;
+                let copyCount = Math.min(freeZoneSize, occupiedRange);
+                for (let j = 0; j < copyCount; j++) {
+                    let freeI = firstFreeSlotOffset / s;
+                    let curI = curMoveOffset / s;
+
+                    let moveEntry = sortedAll[curI];
+                    this._moveEntry(moveEntry, firstFreeSlotOffset);
+                    let replacedEntry = sortedAll[freeI];
+
+                    // set the offset of the element entry we replace with a value that will make it discard at the end of the method
+                    replacedEntry.offset = curMoveOffset;
+
+                    // Swap the entry we moved and the one it replaced in the sorted array to reflect the action we've made
+                    sortedAll[freeI] = moveEntry;
+                    sortedAll[curI] = replacedEntry;
+
+                    curMoveOffset -= s;
+                    firstFreeSlotOffset += s;
+                }
+
+                // Free Zone is smaller or equal so it's no longer a free zone, set the new one to the current location
+                if (freeZoneSize <= occupiedRange) {
+                    firstFreeSlotOffset = curOffset;
+                    freeZoneSize = 1;
+                }
+
+                // Free Zone was bigger, the firstFreeSlotOffset is already up to date, but we need to update the its size
+                else {
+                    freeZoneSize = ((curOffset - firstFreeSlotOffset) / s) + 1;
+                }
+
+                // as we're about to iterate to the next, the cur becomes the prev...
+                prevOffset = curOffset;
+            }
+
+            // Allocate a new buffer with the perfect size, copy the content, update free data
+            this.entryCount = firstFreeSlotOffset / s;
+            let optimizedBuffer = this.buffer.subarray(0, firstFreeSlotOffset);
+            this.freeEntries.splice(0);
+            this.allEntries = sortedAll.slice(0, this.entryCount);
+            return optimizedBuffer;
+        }
+
+        private _moveEntry(entry: DynamicFloatArrayEntry, destOffset: number) {
+            for (let i = 0; i < this.stride; i++) {
+                this.buffer[destOffset + i] = this.buffer[entry.offset + i];
+            }
+
+            entry.offset = destOffset;
+        }
+
+        private _growBuffer() {
+            // Allocate the new buffer with 50% more entries, copy the content of the current one
+            let newEntryCount = this.entryCount * 1.5;
+            let newBuffer = new Float32Array(newEntryCount * this.stride);
+            newBuffer.set(this.buffer);
+
+            let addedCount = newEntryCount - this.entryCount;
+            this.allEntries.length += addedCount;
+            this.freeEntries.length += addedCount;
+
+            for (let i = this.entryCount; i < newEntryCount; i++) {
+                let entry = new DynamicFloatArrayEntry();
+                entry.offset = i * this.stride;
+
+                this.allEntries[i] = entry;
+                this.freeEntries[i] = entry;
+            }
+
+            this.buffer = newBuffer;
+            this.entryCount = newEntryCount;
+        }
+
+        buffer: Float32Array;
+        entryCount: number;
+        stride: number;
+        allEntries: Array<DynamicFloatArrayEntry>;
+        freeEntries: Array<DynamicFloatArrayEntry>;
+    }
+
+    export class DynamicFloatArrayEntry {
+        offset: number;
+    }
+}