Last active
August 3, 2025 19:14
-
-
Save enbugger/37e7e363e3b93786d748deaf55f9d7a5 to your computer and use it in GitHub Desktop.
Array-based octree #octree #unity #burst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #define LOG_COUNTER | |
| #define LOG_OPS | |
| using System; | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Text; | |
| using Unity.Collections; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using Unity.Jobs; | |
| using UnityEngine; | |
| namespace Game | |
| { | |
| /// <summary> | |
| /// Stateless adapter that must be implemented by user | |
| /// Value must contain or be derivable from value | |
| /// </summary> | |
| public interface IOAAdapter<TKey, TValue> | |
| where TKey : unmanaged, IEquatable<TKey> | |
| where TValue : unmanaged | |
| { | |
| /// <summary> | |
| /// You must implement logic that mark value as deleted | |
| /// The implementation should allow to determine if | |
| /// the element is deleted in IsDeleted() | |
| /// </summary> | |
| public void MarkDeleted(ref TValue value); | |
| /// <summary> | |
| /// Check if element was marked as deleted previously | |
| /// </summary> | |
| public bool IsDeleted(TValue value); | |
| /// <summary> | |
| /// Check if element is 0 or empty | |
| /// </summary> | |
| public bool IsEmpty(TValue value); | |
| /// <summary> | |
| /// Implement (or don't) your hashing here | |
| /// Index must be less that length | |
| /// Index must be different with different attempt value | |
| /// </summary> | |
| public int ConvertToIndex(TKey key, int length, int attempt = 0); | |
| /// <summary> | |
| /// Extract key from value | |
| /// </summary> | |
| public TKey ValueToKey(TValue value); | |
| } | |
| /// <summary> | |
| /// Hash map that uses open addressing instead of chaining for collision resolving | |
| /// Only values are stored | |
| /// NOTE: Key must be stored or be derivable from value data | |
| /// </summary> | |
| public struct OAHashMap<TKey, TValue, TAdapter> | |
| where TKey : unmanaged, IEquatable<TKey> | |
| where TValue : unmanaged | |
| where TAdapter : IOAAdapter<TKey, TValue> | |
| { | |
| private UnsafeList<TValue> _data; | |
| private uint _count; | |
| private readonly TAdapter _adapter; | |
| public OAHashMap(int capacity, Allocator allocator) | |
| { | |
| _data = new UnsafeList<TValue>(capacity, allocator); | |
| _data.Resize(capacity, NativeArrayOptions.ClearMemory); | |
| _count = 0; | |
| _adapter = default; | |
| } | |
| public OAHashMap(UnsafeList<TValue> data, uint count) | |
| { | |
| _data = data; | |
| _count = count; | |
| _adapter = default; | |
| } | |
| /// <summary> | |
| /// Put a new value to map | |
| /// Always returns -1 if load factor is 1.0 | |
| /// </summary> | |
| public int Put(TValue value) | |
| { | |
| #if LOG_OPS | |
| Debug.LogFormat("HashMap::Put() <= {0}", value); | |
| #endif | |
| if (_count >= _data.Length) | |
| return -1; | |
| var inc = 0; | |
| var index = _adapter.ConvertToIndex(_adapter.ValueToKey(value), _data.Length); | |
| var cur = _data[index]; | |
| while (!_adapter.IsEmpty(cur) | |
| && !_adapter.IsDeleted(cur) | |
| && !_adapter.ValueToKey(cur).Equals(_adapter.ValueToKey(value))) | |
| { | |
| inc++; | |
| index = _adapter.ConvertToIndex(_adapter.ValueToKey(value), _data.Length, inc); | |
| cur = _data[index]; | |
| } | |
| _data[index] = value; | |
| if (_adapter.IsEmpty(cur) || _adapter.IsDeleted(cur)) | |
| { | |
| #if LOG_COUNTER | |
| Debug.LogFormat("COUNT: INC {0} => {1} ({2}) ({3})", | |
| _count, _count + 1, _adapter.IsEmpty(cur) ? "Set-Empty" : "Set-Deleted", cur); | |
| #endif | |
| _count++; | |
| } | |
| if ((float)_count / _data.Length > 0.75f) | |
| Resize(); | |
| return index; | |
| } | |
| /// <summary> | |
| /// Retrieve value from map | |
| /// Returns -1 if no such key found otherwise returns index of the element | |
| /// </summary> | |
| public readonly int TryGet(TKey key, out TValue value) | |
| { | |
| var inc = 0; | |
| var index = _adapter.ConvertToIndex(key, _data.Length); | |
| value = _data[index]; | |
| while (!_adapter.IsEmpty(value) | |
| // && !_adapter.IsDeleted(value) | |
| && !_adapter.ValueToKey(value).Equals(key)) | |
| { | |
| // Special case: when we have load factor 1 | |
| // we quit after one full iteration of array | |
| // because we'd never reach the of cluster | |
| if (_count >= _data.Length && inc >= _data.Length) | |
| { | |
| return -1; | |
| } | |
| inc++; | |
| index = _adapter.ConvertToIndex(key, _data.Length, inc); | |
| value = _data[index]; | |
| } | |
| if (_adapter.IsEmpty(value) || _adapter.IsDeleted(value)) | |
| { | |
| value = default; | |
| return -1; | |
| } | |
| return index; | |
| } | |
| /// <summary> | |
| /// Remove (actually mark as removed) value from map | |
| /// Returns -1 if no such key found otherwise returns index of the element | |
| /// </summary> | |
| public int Remove(TKey key) | |
| { | |
| #if LOG_OPS | |
| Debug.LogFormat("HashMap::Remove() <= {0}", key); | |
| #endif | |
| var index = TryGet(key, out var value); | |
| if (index < 0) | |
| { | |
| return index; | |
| } | |
| #if LOG_COUNTER | |
| Debug.LogFormat("MARK DELETED: {0}", value); | |
| #endif | |
| _adapter.MarkDeleted(ref value); | |
| _data[index] = value; | |
| #if LOG_COUNTER | |
| Debug.LogFormat("COUNT: DEC {0} => {1} (Remove)", _count, _count - 1); | |
| #endif | |
| _count--; | |
| return index; | |
| } | |
| private unsafe void Resize() | |
| { | |
| #if LOG_OPS | |
| Debug.LogFormat("HashMap::Resize()"); | |
| #endif | |
| var tmp = new UnsafeList<TValue>(_data.Length, Allocator.Temp); | |
| tmp.AddRange(_data.Ptr, _data.Length); | |
| #if LOG_COUNTER | |
| Debug.LogFormat("COUNT: NULLIFY from {0} (Resize)", _count); | |
| #endif | |
| _count = 0; | |
| _data.Clear(); | |
| _data.Resize(tmp.Length * 2, NativeArrayOptions.ClearMemory); | |
| for (int i = 0; i < tmp.Length; i++) | |
| { | |
| Put(tmp[i]); | |
| } | |
| tmp.Dispose(); | |
| } | |
| public void Clear() | |
| { | |
| #if LOG_OPS | |
| Debug.LogFormat("HashMap::Clear()"); | |
| #endif | |
| var len = _data.Length; | |
| _data.Clear(); | |
| _data.Resize(len, NativeArrayOptions.ClearMemory); | |
| #if LOG_COUNTER | |
| Debug.LogFormat("COUNT: NULLIFY from {0} (Clear)", _count); | |
| #endif | |
| _count = 0; | |
| } | |
| public readonly int Count => (int)_count; | |
| public readonly int Capacity => _data.Length; | |
| public readonly bool IsCreated => _data.IsCreated; | |
| public readonly float LoadFactor => (float)_count / _data.Length; | |
| public readonly UnsafeList<TValue> Data => _data; | |
| public readonly string ToStringDebug() | |
| { | |
| var sb = new StringBuilder(); | |
| sb.AppendFormat("HashMap [size={0}/{1}]\n", _count, _data.Capacity); | |
| for (int i = 0; i < _data.Length; i++) | |
| { | |
| var node = _data[i]; | |
| var index = ""; | |
| if (!_adapter.IsDeleted(node)) | |
| { | |
| index = string.Format("->{0}", _adapter.ConvertToIndex(_adapter.ValueToKey(node), _data.Length)); | |
| } | |
| sb.AppendFormat("#{0}{1}\t{2}\n", i, index, node); | |
| } | |
| return sb.ToString(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #define LOG_OPS | |
| using System.Runtime.CompilerServices; | |
| using System.Text; | |
| using Unity.Collections; | |
| using Unity.Collections.LowLevel.Unsafe; | |
| using Unity.Jobs; | |
| using Unity.Mathematics; | |
| using UnityEngine; | |
| using static Unity.Mathematics.math; | |
| namespace Game | |
| { | |
| // TODO | |
| // Long-term: | |
| // - Use templates to generate non-generic OctreeNode. | |
| // It can be used with ExplicitLayout to save us a byte pre node. | |
| // - Use templates to generate 2 versions: With UnsafeHashMap and NativeHashMap. | |
| // There is no common interface for these two. | |
| // - Use templates for 64-bit-based implementation | |
| // Current 32-bit-based supports only 10 levels | |
| // - [DONE] Research on replacing HashMap with array(s) for passing it in compute shaders | |
| // without copying. Likely, it'll need custom hash implementation | |
| // - Use quadratic probing | |
| // Short-term: | |
| // - How to effectively determine neighbor? | |
| // Implement GetNeighbor() where traverse from parent? | |
| // - Iterator over levels | |
| public struct Octree<TValue> | |
| where TValue : unmanaged | |
| { | |
| private OAHashMap<LocationalCode, OctreeNode<TValue>, OctreeHashMapAdapter<TValue>> _data; | |
| public const int CAPACITY = 128; | |
| public Octree(Allocator allocator) | |
| { | |
| _data = new OAHashMap<LocationalCode, OctreeNode<TValue>, OctreeHashMapAdapter<TValue>>(CAPACITY, allocator); | |
| InternalPut(CreateRoot()); | |
| } | |
| public Octree(UnsafeList<OctreeNode<TValue>> data, uint count) | |
| { | |
| _data = new OAHashMap<LocationalCode, OctreeNode<TValue>, OctreeHashMapAdapter<TValue>>(data, count); | |
| InternalPut(CreateRoot()); | |
| } | |
| private static OctreeNode<TValue> CreateRoot() | |
| { | |
| return new OctreeNode<TValue>() | |
| { | |
| Bits = 0x00000001, // Non-Leaf with level 0 | |
| ChildrenMask = 0 // No children | |
| }; | |
| } | |
| public void Set(uint3 pos, int level, TValue value) | |
| { | |
| var loc = LocationalCode.Encode(pos, level); | |
| Set(loc, value); | |
| } | |
| /// <summary> | |
| /// Set value. Parent nodes will be created automatically | |
| /// </summary> | |
| /// Level - Bounds | |
| /// 0 - 0-0 // 0x0...01 can't have coords | |
| /// 1 - 0-1 | |
| /// 2 - 0-3 | |
| /// 3 - 0-7 | |
| /// 4 - 0-15 | |
| /// 5 - 0-31 | |
| /// 6 - 0-63 | |
| /// 7 - 0-127 | |
| /// 8 - 0-255 | |
| /// 9 - 0-511 | |
| /// 10- 0-1023 -- deprecated | |
| public void Set(LocationalCode loc, TValue value) | |
| { | |
| var node = new OctreeNode<TValue> | |
| { | |
| Bits = loc.Code, | |
| ChildrenMask = 0, | |
| Value = value | |
| }; | |
| node.SetLeaf(); | |
| InternalPut(node); | |
| while (loc.Level > 0) | |
| { | |
| var curCode = loc.Code; | |
| loc.ToParent(); | |
| if (InternalTryGet(loc.Code, out var parentNode) < 0) | |
| { | |
| parentNode = new OctreeNode<TValue> | |
| { | |
| Bits = loc.Code, | |
| }; | |
| } | |
| parentNode.UnsetLeaf(); | |
| parentNode.ChildrenMask |= (byte)(1 << (int)(curCode & 0b111)); | |
| InternalPut(parentNode); | |
| } | |
| } | |
| public void SetBlock(uint3 pos, int level, TValue value) | |
| { | |
| var loc = LocationalCode.Encode(pos, level); | |
| SetBlock(loc, value); | |
| } | |
| public void SetBlock(LocationalCode loc, TValue value) | |
| { | |
| #if LOG_OPS | |
| Debug.LogFormat("Octree::SetBlock() <= ({0}, {1})", loc.ToStringOctal(), value); | |
| #endif | |
| RemoveChildrenRecursive(loc); | |
| Set(loc, value); | |
| } | |
| public bool RemoveBlock(uint3 pos, int level) | |
| { | |
| var loc = LocationalCode.Encode(pos, level); | |
| #if LOG_OPS | |
| Debug.LogFormat("Octree::RemoveBlock() <= {0}", loc.ToStringOctal()); | |
| #endif | |
| for (var i = 1; i < loc.Level; i++) | |
| { | |
| var lc = loc.GetParent(i); | |
| if (InternalTryGet(lc, out var value) >= 0 && value.IsLeaf()) | |
| { | |
| Subdivide(lc); | |
| } | |
| } | |
| return Remove(loc); | |
| } | |
| public bool Subdivide(LocationalCode loc, byte exclude) | |
| { | |
| // Debug.LogFormat("Subdividing {0}", loc.ToStringBinary()); | |
| if (InternalTryGet(loc, out var value) < 0 || !value.IsLeaf()) return false; | |
| for (var i = 0; i < 8; i++) | |
| { | |
| if (exclude == i) continue; | |
| var copy = value; | |
| copy.Bits = loc.GetChild((NRP)i); | |
| copy.SetLeaf(); | |
| InternalPut(copy); | |
| } | |
| value.UnsetLeaf(); | |
| value.ChildrenMask = 0b11111111; | |
| value.UnsetChild(exclude); | |
| InternalPut(value); | |
| return true; | |
| } | |
| public bool Subdivide(LocationalCode loc) | |
| { | |
| if (InternalTryGet(loc, out var value) < 0 || !value.IsLeaf()) return false; | |
| for (var i = 0; i < 8; i++) | |
| { | |
| var copy = value; | |
| copy.Bits = loc.GetChild((NRP)i); | |
| copy.SetLeaf(); | |
| InternalPut(copy); | |
| } | |
| value.UnsetLeaf(); | |
| value.ChildrenMask = 0b11111111; | |
| InternalPut(value); | |
| return true; | |
| } | |
| public bool Subdivide(uint3 pos, int level, byte exclude = 0b1111) | |
| { | |
| var loc = LocationalCode.Encode(pos, level); | |
| return Subdivide(loc, exclude); | |
| } | |
| // TODO Level should be uint | |
| public readonly int TryGet(uint3 pos, int level, out OctreeNode<TValue> value) | |
| { | |
| var loc = LocationalCode.Encode(pos, level); | |
| return InternalTryGet(loc.Code, out value); | |
| } | |
| public readonly int TryGet(LocationalCode code, out OctreeNode<TValue> value) | |
| { | |
| return InternalTryGet(code, out value); | |
| } | |
| public readonly OctreeNode<TValue> GetRoot() | |
| { | |
| InternalTryGet(LocationalCode.ROOT, out var root); | |
| return root; | |
| } | |
| public bool Remove(uint3 pos, int level) | |
| { | |
| if (level == 0) | |
| return false; | |
| var loc = LocationalCode.Encode(pos, level); | |
| return Remove(loc); | |
| } | |
| public bool Remove(LocationalCode loc) | |
| { | |
| if (InternalTryGet(loc.Code, out var _) < 0) | |
| return false; | |
| CleanParents(loc); | |
| RemoveChildrenRecursive(loc); | |
| return InternalRemove(loc); | |
| } | |
| private void CleanParents(LocationalCode code) | |
| { | |
| var prevCode = code; | |
| var curCode = prevCode.GetParent(); | |
| while (InternalTryGet(curCode, out var pValue) >= 0) | |
| { | |
| pValue.UnsetChild(prevCode); | |
| if (curCode.Level > 0 && pValue.ChildrenMask == 0) | |
| { | |
| InternalRemove(curCode); | |
| prevCode = curCode; | |
| curCode = curCode.GetParent(); | |
| continue; | |
| } | |
| InternalPut(pValue); | |
| break; | |
| } | |
| } | |
| private void RemoveChildrenRecursive(LocationalCode parent) | |
| { | |
| var toRemove = new UnsafeList<uint>(0, Allocator.Temp); | |
| var toRetrieve = new UnsafeList<uint>(0, Allocator.Temp); | |
| toRetrieve.Add(parent); | |
| while (!toRetrieve.IsEmpty) | |
| { | |
| var lastIndex = toRetrieve.Length - 1; | |
| var curCode = toRetrieve[lastIndex]; | |
| toRetrieve.RemoveAt(lastIndex); | |
| if (InternalTryGet(curCode, out var curValue) < 0) | |
| break; | |
| for (uint i = 0; i < 8; i++) | |
| { | |
| if (!curValue.ChildIsSet(i)) | |
| continue; | |
| var next = curValue.Code; | |
| next.ToChild((NRP)i); | |
| if (InternalTryGet(next, out var childValue) < 0) | |
| continue; | |
| toRemove.Add(next); | |
| if (!childValue.IsLeaf()) | |
| { | |
| toRetrieve.Add(next); | |
| } | |
| } | |
| } | |
| toRetrieve.Dispose(); | |
| for (int i = 0; i < toRemove.Length; i++) | |
| { | |
| InternalRemove(toRemove[i]); | |
| } | |
| toRemove.Dispose(); | |
| } | |
| public readonly int Count => _data.Count; | |
| public readonly bool IsCreated => _data.IsCreated; | |
| private readonly DebugNode<TValue> Visit(LocationalCode code, StringBuilder sb) | |
| { | |
| InternalTryGet(code, out var val); | |
| for (var i = 0; i < code.Level; i++) | |
| { | |
| sb.Append('+'); | |
| } | |
| sb.Append(val.ToString()); | |
| sb.Append("\n"); | |
| if (val.IsLeaf()) { | |
| return new DebugNode<TValue> {IsLeaf = true, Value = val.Value}; | |
| } else { | |
| var result = new DebugNode<TValue>{Children = new DebugNode<TValue>[8]}; | |
| for (byte i = 0; i < 8; i++) | |
| { | |
| if (!val.ChildIsSet(i)) continue; | |
| result.Children[i] = Visit(code.GetChild((NRP)i), sb); | |
| } | |
| return result; | |
| } | |
| } | |
| public void Clear() | |
| { | |
| _data.Clear(); | |
| InternalPut(CreateRoot()); | |
| } | |
| public readonly string ToStringTree() | |
| { | |
| var sb = new StringBuilder(); | |
| Visit(LocationalCode.ROOT, sb); | |
| return sb.ToString(); | |
| } | |
| public readonly string ToStringData() => _data.ToStringDebug(); | |
| public void InternalPut(OctreeNode<TValue> value) | |
| { | |
| _data.Put(value); | |
| } | |
| public readonly int InternalTryGet(LocationalCode code, out OctreeNode<TValue> value) | |
| { | |
| return _data.TryGet(code, out value); | |
| } | |
| private bool InternalRemove(LocationalCode code) | |
| { | |
| return _data.Remove(code) >= 0; | |
| } | |
| public readonly UnsafeList<OctreeNode<TValue>> Data => _data.Data; | |
| } | |
| public struct OctreeHashMapAdapter<TValue> : IOAAdapter<LocationalCode, OctreeNode<TValue>> | |
| where TValue : unmanaged | |
| { | |
| public int ConvertToIndex(LocationalCode key, int length, int attempt = 0) | |
| => key.ToIndex(length, attempt); | |
| public bool IsDeleted(OctreeNode<TValue> value) => value.IsDeleted(); | |
| public bool IsEmpty(OctreeNode<TValue> value) => value.IsEmpty(); | |
| public void MarkDeleted(ref OctreeNode<TValue> value) => value.SetDeleted(); | |
| public LocationalCode ValueToKey(OctreeNode<TValue> value) => value.Code; | |
| } | |
| /// <summary> | |
| /// Node's relative position to it's parent's center | |
| /// </summary> | |
| /// 011@3----111@7 | |
| /// 010@2-+--110@6 | | |
| /// | | | | Y Z | |
| /// | 001@1--+-101@5 |/ | |
| /// 000@0----100@4 +--X | |
| public enum NRP : byte | |
| { | |
| LEFT_BOTTOM_FRONT = 0b000, | |
| LEFT_BOTTOM_BACK = 0b001, | |
| LEFT_TOP_FRONT = 0b010, | |
| LEFT_TOP_BACK = 0b011, | |
| RIGHT_BOTTOM_FRONT = 0b100, | |
| RIGHT_BOTTOM_BACK = 0b101, | |
| RIGHT_TOP_FRONT = 0b110, | |
| RIGHT_TOP_BACK = 0b111 | |
| } | |
| public struct LocationalCode : System.IEquatable<LocationalCode> | |
| { | |
| public const int MAX_LEVEL = 9; | |
| public const uint ROOT = 0x00000001; // Level 0 | |
| public const uint NULL = 0x00000000; | |
| public uint Code; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static implicit operator uint(LocationalCode c) => c.Code; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static implicit operator LocationalCode(uint v) => new LocationalCode { Code = v }; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void ToChild(NRP pos) | |
| { | |
| Code <<= 3; | |
| Code |= (byte)pos; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void ToParent() | |
| { | |
| Code >>= 3; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly LocationalCode GetParent() | |
| { | |
| return new LocationalCode { Code = Code >> 3 }; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly LocationalCode GetParent(int level) | |
| { | |
| var diff = (int) Level - level; | |
| return new LocationalCode { Code = diff > 0 ? Code >> 3 * diff : Code << 3 * abs(diff) }; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly LocationalCode GetChild(NRP pos) | |
| { | |
| var code = Code << 3; | |
| code |= (byte)pos; | |
| return new LocationalCode { Code = code }; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly LocationalCode GetChild(int pos) | |
| { | |
| return GetChild((NRP) pos); | |
| } | |
| public uint Level | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| readonly get => | |
| MAX_LEVEL - ((uint) lzcnt(Code) - (sizeof(uint) * 8 - (MAX_LEVEL * 3 + 1))) / 3; // Bit scan | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| set => Code |= 1U << (int)(value * 3); | |
| } | |
| // Be careful. This is a duplicate from OctreeNode | |
| private const uint LEAF_BIT = 0x80000000; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly int ToIndex(int length, int inc = 0) | |
| { | |
| var code = Code & (~(LEAF_BIT)); | |
| return (int) ((code * code + inc) % (length - 1)); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static bool CheckInput(uint3 pos, uint level) | |
| { | |
| return level <= MAX_LEVEL && all(pos <= uint3(GetMaxForLevel(level))); | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static uint GetMaxForLevel(uint level) => (uint)(1 << (int)level) - 1; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static LocationalCode Encode(uint3 pos, int level) | |
| { | |
| var code = ROOT; | |
| for (var i = level - 1; i >= 0; i--) | |
| { | |
| code <<= 3; | |
| var t = (pos >> i) & 1; | |
| code |= (t.x << 0) | (t.y << 1) | (t.z << 2); | |
| } | |
| return new LocationalCode { Code = code }; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly uint3 Decode() | |
| { | |
| var pos = uint3(0); | |
| var code = Code; | |
| for (var i = 0; i < Level; i++) | |
| { | |
| var bits = uint3((code >> 0) & 1, (code >> 1) & 1, (code >> 2) & 1); | |
| pos += bits * (1U << i); | |
| code >>= 3; | |
| } | |
| return pos; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly float3 Decode(float sceneSize) | |
| { | |
| var pos = float3(0); | |
| var level = Level; | |
| var code = Code; | |
| for (var i = (int)level; i > 0; i--) | |
| { | |
| var bits = uint3((code >> 0) & 1, (code >> 1) & 1, (code >> 2) & 1); | |
| pos += float3(bits) * sceneSize / (1 << i); | |
| code >>= 3; | |
| } | |
| return pos; | |
| } | |
| public readonly LocationalCode NeighborTop { | |
| get | |
| { | |
| return Code + 4; | |
| } | |
| } | |
| public override string ToString() | |
| { | |
| // return System.Convert.ToString(Code, 16).PadLeft(8, '0'); | |
| // return System.Convert.ToString(Code, 2).PadLeft(32, '0'); | |
| return ToStringOctal(); | |
| } | |
| public readonly string ToStringBinary() | |
| { | |
| return System.Convert.ToString(Code, 2).PadLeft(32, '0'); | |
| } | |
| public readonly string ToStringOctal() | |
| { | |
| return System.Convert.ToString(Code, 8).PadLeft((int)MAX_LEVEL + 1, '0'); | |
| } | |
| public readonly bool Equals(LocationalCode other) | |
| { | |
| return Code.Equals(other.Code); | |
| } | |
| } | |
| // TODO: ExplicitLayout is not compatible with generics | |
| public struct OctreeNode<T> where T : struct | |
| { | |
| /* | |
| * 1 - 27: Locational code | |
| * 1 - 28: Level bit (follows locational code) | |
| * Must be always set | |
| * 29 - 30: Reserved | |
| * 31: Deleted flag bit | |
| * 32: Leaf bit | |
| */ | |
| public uint Bits; | |
| public byte ChildrenMask; | |
| public T Value; | |
| private const uint LEAF_BIT = 0x80000000; | |
| private const uint DELETED_BIT = 0x40000000; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool IsLeaf() => (Bits & LEAF_BIT) > 0; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SetLeaf() => Bits |= LEAF_BIT; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void UnsetLeaf() => Bits &= ~LEAF_BIT; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool IsDeleted() => (Bits & DELETED_BIT) > 0; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SetDeleted() => Bits |= DELETED_BIT; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void UnsetDeleted() => Bits &= ~DELETED_BIT; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void SetChild(uint childCode) => ChildrenMask |= (byte) (1 << (int) (childCode & 0b111)); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void UnsetChild(uint childCode) => ChildrenMask &= (byte) ~(1 << (int) (childCode & 0b111)); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool ChildIsSet(NRP childCode) => | |
| (ChildrenMask & (byte) (1 << ((byte) childCode & 0b111))) > 0; | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool ChildIsSet(int childCode) => ChildIsSet((NRP) childCode); | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool ChildIsSet(uint childCode) => ChildIsSet((NRP) childCode); | |
| // Minimal bits is 1 for root | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public readonly bool IsEmpty() => Bits == 0; | |
| public readonly LocationalCode Code | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| get => Bits & (~LEAF_BIT); | |
| } | |
| public readonly uint Level | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| get => Code.Level; | |
| } | |
| public override string ToString() | |
| { | |
| if (IsEmpty()) | |
| return "Empty"; | |
| var deleted = IsDeleted() ? "[DELETED]" : ""; | |
| var locCodeHex = Code; | |
| if (IsLeaf()) | |
| { | |
| return $"{deleted} {locCodeHex.ToStringOctal()} LEAF \"{Value}\"({locCodeHex.Level})"; | |
| } | |
| var childrenMaskHex = System.Convert.ToString(ChildrenMask, 2).PadLeft(8, '0'); | |
| return $"{deleted} {locCodeHex.ToStringOctal()} PARENT. Children: {childrenMaskHex}({locCodeHex.Level})"; | |
| } | |
| } | |
| public class DebugNode<T> where T : unmanaged | |
| { | |
| public T Value; | |
| public bool IsLeaf; | |
| // public DebugNode<T> _000LBF, _001LBB, _010LTF, _011LTB, _100RBF, _101RBB, _110RTF, _111RTB; | |
| public DebugNode<T>[] Children; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using Unity.Mathematics; | |
| using UnityEngine; | |
| using static Unity.Mathematics.math; | |
| namespace Game | |
| { | |
| // You MUST use "this ref" instead of "this"! | |
| public static partial class OctreeExtensions | |
| { | |
| public static bool SetBlockTryMerge(this ref Octree<BlockNode> octree, LocationalCode code, BlockNode node) | |
| { | |
| octree.SetBlock(code, node); | |
| // Check if we can "merge" children leaves into one parent leaf | |
| var thereWasMerge = false; | |
| var i = 10; // Infinite loop breaker | |
| var currentCode = code.GetParent(); | |
| octree.TryGet(currentCode, out var val); | |
| while (val.Level > 1 && i > 0) | |
| { | |
| i--; | |
| thereWasMerge |= octree.TryMergeBlock(val); | |
| currentCode = currentCode.GetParent(); | |
| octree.TryGet(currentCode, out val); | |
| } | |
| if (i <= 0) | |
| { | |
| Debug.LogError("Infinite loop!!"); | |
| } | |
| return thereWasMerge; | |
| } | |
| public static bool SetBlockTryMerge(this ref Octree<BlockNode> octree, uint3 pos, int level, BlockNode node) | |
| { | |
| var code = LocationalCode.Encode(pos, level); | |
| return octree.SetBlockTryMerge(code, node); | |
| } | |
| public static bool TryMergeBlock(this ref Octree<BlockNode> octree, OctreeNode<BlockNode> node) | |
| { | |
| if (node.ChildrenMask != 0xff) return false; | |
| octree.TryGet(node.Code.GetChild(0b000), out var c000); | |
| octree.TryGet(node.Code.GetChild(0b001), out var c001); | |
| octree.TryGet(node.Code.GetChild(0b010), out var c010); | |
| octree.TryGet(node.Code.GetChild(0b011), out var c011); | |
| octree.TryGet(node.Code.GetChild(0b100), out var c100); | |
| octree.TryGet(node.Code.GetChild(0b101), out var c101); | |
| octree.TryGet(node.Code.GetChild(0b110), out var c110); | |
| octree.TryGet(node.Code.GetChild(0b111), out var c111); | |
| var allAreLeaves = c000.IsLeaf() && c001.IsLeaf() && c010.IsLeaf() && c011.IsLeaf() && c100.IsLeaf() | |
| && c101.IsLeaf() && c110.IsLeaf() && c111.IsLeaf(); | |
| if (!allAreLeaves) return false; | |
| var allAreOfOneType = c000.Value.Type == c001.Value.Type && | |
| c000.Value.Type == c010.Value.Type && | |
| c000.Value.Type == c011.Value.Type && | |
| c000.Value.Type == c100.Value.Type && | |
| c000.Value.Type == c101.Value.Type && | |
| c000.Value.Type == c110.Value.Type && | |
| c000.Value.Type == c111.Value.Type; | |
| if (!allAreOfOneType) return false; | |
| // Check deformation | |
| if (any(c000.Value.Deformation.data != 0)) return false; | |
| if (any(c001.Value.Deformation.data != 0)) return false; | |
| if (any(c010.Value.Deformation.data != 0)) return false; | |
| if (any(c011.Value.Deformation.data != 0)) return false; | |
| if (any(c100.Value.Deformation.data != 0)) return false; | |
| if (any(c101.Value.Deformation.data != 0)) return false; | |
| if (any(c110.Value.Deformation.data != 0)) return false; | |
| if (any(c111.Value.Deformation.data != 0)) return false; | |
| node.SetLeaf(); | |
| node.Value.Type = c000.Value.Type; | |
| node.Value.NeighborMask = NeighborMask.Full; | |
| node.ChildrenMask = 0; | |
| octree.SetBlock(node.Code, node.Value); | |
| return true; | |
| } | |
| } | |
| public static partial class OctreeNodeExtensions | |
| { | |
| public static float ScaleFactor(this OctreeNode<BlockNode> node) | |
| => 1 << (int) (LocationalCode.MAX_LEVEL - node.Level); | |
| public static float3 Offset(this OctreeNode<BlockNode> node) => node.Code.Decode(OctreeConstants.SceneSize); | |
| public static float3 TransformVertex(this OctreeNode<BlockNode> node, float3 point) | |
| => node.Offset() + point * node.ScaleFactor(); | |
| public static float3 Min(this OctreeNode<BlockNode> node) => node.TransformVertex(0); | |
| public static float3 Max(this OctreeNode<BlockNode> node) => node.TransformVertex(1); | |
| public static float3x2 Bounds(this OctreeNode<BlockNode> node) => math.float3x2(node.Min(), node.Max()); | |
| public static float3 Center(this OctreeNode<BlockNode> node) => node.TransformVertex(0.5f); | |
| public static float3 HalfExtents(this OctreeNode<BlockNode> node) => node.Max() - node.Center(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment