diff --git a/Examples/Complete/PointCloudPotree2/Core/PointCloudPotree2Core.cs b/Examples/Complete/PointCloudPotree2/Core/PointCloudPotree2Core.cs index 846af0983..20411b459 100644 --- a/Examples/Complete/PointCloudPotree2/Core/PointCloudPotree2Core.cs +++ b/Examples/Complete/PointCloudPotree2/Core/PointCloudPotree2Core.cs @@ -282,29 +282,17 @@ public void Update(bool allowInput) _camTransform.FpsView(_angleHorz, _angleVert, Input.Keyboard.WSAxis, Input.Keyboard.ADAxis, Time.DeltaTimeUpdate * 20); - if (!_keys && Input.Mouse.RightButton && PointRenderMode == RenderMode.DynamicMesh) { var size = RenderToTexture ? ExternalCanvasSize : new int2(_rc.ViewportWidth, _rc.ViewportHeight); var mousePos = RenderToTexture ? ExternalMousePosition : Input.Mouse.Position; - var result = _picker?.Pick(mousePos, size.x, size.y).Where(x => x is PointCloudPickResult).Cast(); + var result = _picker?.Pick(mousePos, size.x, size.y).Where(x => x is PointCloudPickResult).Cast().OrderBy(res => res.DistanceToRay); if (result != null && result.Any()) { var minElement = result.FirstOrDefault(); - - // get min x/y distance point - foreach (var r in result) - { - if (r.DistanceToRay.x < minElement.DistanceToRay.x && r.DistanceToRay.y < minElement.DistanceToRay.y) - { - minElement = r; - } - } _pickResultTransform.Translation = minElement.Mesh.Vertices[minElement.VertIdx]; } - } - } private void OnThresholdChanged(int newValue) diff --git a/src/Base/Core/MemoryCache.cs b/src/Base/Core/MemoryCache.cs index 58931e329..16cb6d781 100644 --- a/src/Base/Core/MemoryCache.cs +++ b/src/Base/Core/MemoryCache.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Threading; namespace Fusee.Base.Core { @@ -105,6 +106,7 @@ public class MemoryCache : IDisposable private readonly MemoryCache _cache; private bool _disposed = false; + private readonly SemaphoreSlim _cacheLock = new(1); /// /// Creates a new instance and initializes the internal . @@ -135,20 +137,26 @@ public bool TryGetValue(TKey key, out TItem item) /// /// The key of the cache item. /// The cache item. - public void AddOrUpdate(TKey key, TItem cacheEntry) + public async void AddOrUpdate(TKey key, TItem cacheEntry) { - var cacheEntryOptions = new MemoryCacheEntryOptions() - .SetPriority(CacheItemPriority.High) - // Keep in cache for this time, reset time if accessed. - .SetSlidingExpiration(TimeSpan.FromSeconds(SlidingExpiration)); - - cacheEntryOptions.RegisterPostEvictionCallback((subkey, subValue, reason, state) => + try { - HandleEvictedItem?.Invoke(subkey, subValue, reason, state); - }); + await _cacheLock.WaitAsync(); + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + // Keep in cache for this time, reset time if accessed. + .SetSlidingExpiration(TimeSpan.FromSeconds(SlidingExpiration)); + + cacheEntryOptions.RegisterPostEvictionCallback((subkey, subValue, reason, state) => + { + HandleEvictedItem?.Invoke(subkey, subValue, reason, state); + }); + + // Key not in cache, so get data. + _cache.Set(key, cacheEntry, cacheEntryOptions); - // Key not in cache, so get data. - _cache.Set(key, cacheEntry, cacheEntryOptions); + } + finally { _cacheLock.Release(); } } /// diff --git a/src/Engine/Common/IManagedInstanceData.cs b/src/Engine/Common/IManagedInstanceData.cs index 3aa7e5711..8c724137c 100644 --- a/src/Engine/Common/IManagedInstanceData.cs +++ b/src/Engine/Common/IManagedInstanceData.cs @@ -21,7 +21,7 @@ public interface IManagedInstanceData : IDisposable /// /// The unique id of the object. /// - public Suid SessionUniqueId + public Guid UniqueId { get; } diff --git a/src/Engine/Common/IManagedMesh.cs b/src/Engine/Common/IManagedMesh.cs index 3d7727ed8..ffc385957 100644 --- a/src/Engine/Common/IManagedMesh.cs +++ b/src/Engine/Common/IManagedMesh.cs @@ -15,7 +15,7 @@ public interface IManagedMesh : IDisposable /// /// SessionUniqueIdentifier is used to verify a Mesh's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; } + public Guid UniqueIdentifier { get; } /// /// The primitive type this mesh is composed of. diff --git a/src/Engine/Common/IMeshImp.cs b/src/Engine/Common/IMeshImp.cs index 479abf2d4..fb7928687 100644 --- a/src/Engine/Common/IMeshImp.cs +++ b/src/Engine/Common/IMeshImp.cs @@ -1,4 +1,6 @@ using ProtoBuf; +using System; + namespace Fusee.Engine.Common { /// diff --git a/src/Engine/Common/IRenderContextImp.cs b/src/Engine/Common/IRenderContextImp.cs index d3354d00c..034065d75 100644 --- a/src/Engine/Common/IRenderContextImp.cs +++ b/src/Engine/Common/IRenderContextImp.cs @@ -482,7 +482,7 @@ public interface IRenderContextImp /// /// The instance. /// The instance colors. - public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColors); + public void SetInstanceColor(IInstanceDataImp instanceImp, uint[] instanceColors); /// /// Binds the tangents onto the GL render context and assigns an TangentBuffer index to the passed instance. diff --git a/src/Engine/Common/ITextureBase.cs b/src/Engine/Common/ITextureBase.cs index 6be80d9aa..fb0ff6fbe 100644 --- a/src/Engine/Common/ITextureBase.cs +++ b/src/Engine/Common/ITextureBase.cs @@ -130,7 +130,7 @@ public interface ITextureBase : IDisposable /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - Suid SessionUniqueIdentifier { get; } + Guid UniqueIdentifier { get; } /// /// Defines if Mipmaps are generated for this texture. diff --git a/src/Engine/Common/Suid.cs b/src/Engine/Common/Suid.cs deleted file mode 100644 index 7f4c93ef9..000000000 --- a/src/Engine/Common/Suid.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Fusee.Engine.Common -{ - /// - /// Session unique Id. - /// - public readonly struct Suid - { - private static readonly object LockObject = new(); - private static ulong _idCounter = 0; - private readonly ulong _id; - - private Suid(ulong id) - { - _id = id; - } - - /// - /// Generate a new session unique Id. - /// - public static Suid GenerateSuid() - { - //increment in a thread-safe way... idCounter is static -> the lockObject must be static. - lock (LockObject) - { - _idCounter++; - } - return new Suid(_idCounter); - } - - /// - /// An Empty Suid. Can be used to compare whether a Suid is Empty / default. - /// - public static readonly Suid Empty = new(0); - - /// - /// Checks if two suids are equal - /// - public override bool Equals(object obj) => obj is Suid s && s._id == _id; - - /// - /// Gets hash code - /// - public override int GetHashCode() => _id.GetHashCode(); - - /// - /// Checks if two suids are equal - /// - /// - /// - public static bool operator ==(Suid left, Suid right) => left.Equals(right); - - /// - /// Checks if two suids are equal - /// - /// - /// - public static bool operator !=(Suid left, Suid right) => !(left == right); - } -} \ No newline at end of file diff --git a/src/Engine/Core/EffectManager.cs b/src/Engine/Core/EffectManager.cs index 4996a526c..afbe4fe40 100644 --- a/src/Engine/Core/EffectManager.cs +++ b/src/Engine/Core/EffectManager.cs @@ -1,5 +1,4 @@ -using Fusee.Base.Core; -using Fusee.Engine.Common; +using Fusee.Base.Core; using Fusee.Engine.Core.Effects; using System; using System.Collections.Generic; @@ -10,15 +9,13 @@ internal class EffectManager { private readonly RenderContext _rc; private readonly Stack _effectsToBeDeleted = new(); - private readonly Dictionary _allEffects = new(); + private readonly Dictionary _allEffects = new(); private void EffectChanged(object? sender, EffectManagerEventArgs args) { if (args == null || sender == null) return; - var senderSF = sender as Effect; - - if (senderSF == null) + if (sender is not Effect senderSF) { Diagnostics.Warn("Casting changed effect to type Effect failed!"); return; @@ -43,7 +40,7 @@ public void RegisterEffect(Effect ef) // Setup handler to observe changes of the mesh data and dispose event (deallocation) ef.EffectChanged += EffectChanged; - _allEffects.Add(ef.SessionUniqueIdentifier, ef); + _allEffects.Add(ef.UniqueIdentifier, ef); } /// @@ -57,7 +54,7 @@ public EffectManager(RenderContext renderContextImp) public Effect? GetEffect(Effect ef) { - return _allEffects.TryGetValue(ef.SessionUniqueIdentifier, out var effect) ? effect : null; + return _allEffects.TryGetValue(ef.UniqueIdentifier, out var effect) ? effect : null; } /// @@ -69,7 +66,7 @@ public void Cleanup() { var tmPop = _effectsToBeDeleted.Pop(); // Remove one Effect from _allEffects - _allEffects.Remove(tmPop.SessionUniqueIdentifier); + _allEffects.Remove(tmPop.UniqueIdentifier); // Remove one Effect from Memory _rc.RemoveShader(tmPop); } diff --git a/src/Engine/Core/Effects/Effect.cs b/src/Engine/Core/Effects/Effect.cs index 67b3116c9..fd6740ed3 100644 --- a/src/Engine/Core/Effects/Effect.cs +++ b/src/Engine/Core/Effects/Effect.cs @@ -34,7 +34,7 @@ public abstract class Effect : SceneComponent, IDisposable /// /// SessionUniqueIdentifier is used to verify a Mesh's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; } = Suid.GenerateSuid(); + public Guid UniqueIdentifier { get; } = Guid.NewGuid(); private bool _disposed; @@ -108,7 +108,7 @@ public override bool Equals(object? obj) } // Return true if the fields match: - return (SessionUniqueIdentifier == p.SessionUniqueIdentifier); + return (UniqueIdentifier == p.UniqueIdentifier); } /// @@ -124,7 +124,7 @@ public bool Equals(Effect p) } // Return true if the fields match: - return (SessionUniqueIdentifier == p.SessionUniqueIdentifier); + return (UniqueIdentifier == p.UniqueIdentifier); } /// @@ -133,7 +133,7 @@ public bool Equals(Effect p) /// A hash code for the current object. public override int GetHashCode() { - return SessionUniqueIdentifier.GetHashCode(); + return UniqueIdentifier.GetHashCode(); } /// diff --git a/src/Engine/Core/GpuMesh.cs b/src/Engine/Core/GpuMesh.cs index e2b81d7d2..6baa2c9ae 100644 --- a/src/Engine/Core/GpuMesh.cs +++ b/src/Engine/Core/GpuMesh.cs @@ -44,7 +44,7 @@ public class GpuMesh : SceneComponent, IManagedMesh /// /// SessionUniqueIdentifier is used to verify a Mesh's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; } = Suid.GenerateSuid(); + public Guid UniqueIdentifier { get; } = Guid.NewGuid(); /// /// Type of data of this mesh (e.g. Triangles, Points, Lines, etc.) diff --git a/src/Engine/Core/LightResult.cs b/src/Engine/Core/LightResult.cs index d4c84c304..37df355d9 100644 --- a/src/Engine/Core/LightResult.cs +++ b/src/Engine/Core/LightResult.cs @@ -1,6 +1,7 @@ using Fusee.Engine.Common; using Fusee.Engine.Core.Scene; using Fusee.Math.Core; +using System; namespace Fusee.Engine.Core { @@ -50,7 +51,7 @@ public float4x4 Rotation /// /// The session unique identifier of tis LightResult. /// - public Suid Id; + public Guid Id; /// /// Creates a new instance of type LightResult. @@ -61,7 +62,7 @@ public LightResult(Light light) Light = light; WorldSpacePos = float3.Zero; Rotation = float4x4.Identity; - Id = Suid.GenerateSuid(); + Id = Guid.NewGuid(); } /// @@ -72,7 +73,7 @@ public LightResult() Light = new Light(); WorldSpacePos = float3.Zero; Rotation = float4x4.Identity; - Id = Suid.GenerateSuid(); + Id = Guid.NewGuid(); } /// diff --git a/src/Engine/Core/MeshManager.cs b/src/Engine/Core/MeshManager.cs index 2a41f4ebd..420f2e511 100644 --- a/src/Engine/Core/MeshManager.cs +++ b/src/Engine/Core/MeshManager.cs @@ -12,9 +12,9 @@ internal class MeshManager private readonly IRenderContextImp _renderContextImp; private readonly Stack _toBeDeletedMeshImps = new(); private readonly Stack _toBeDeletedInstanceDataImps = new(); - private readonly Dictionary _identifierToMeshImpDictionary = new(); + private readonly Dictionary _identifierToMeshImpDictionary = new(); - private readonly Dictionary _identifierToInstanceDataImpDictionary = new(); + private readonly Dictionary _identifierToInstanceDataImpDictionary = new(); /// /// Creates a new Instance of MeshManager. The instance is handling the memory allocation and deallocation on the GPU by observing Mesh objects. @@ -73,14 +73,14 @@ private void Remove(IInstanceDataImp instanceData) private void DisposeMesh(object? sender, MeshChangedEventArgs meshDataEventArgs) { - if (!_identifierToMeshImpDictionary.TryGetValue(meshDataEventArgs.Mesh.SessionUniqueIdentifier, out var toBeUpdatedMeshImp)) + if (!_identifierToMeshImpDictionary.TryGetValue(meshDataEventArgs.Mesh.UniqueIdentifier, out var toBeUpdatedMeshImp)) throw new KeyNotFoundException("Mesh is not registered."); // Add the meshImp to the toBeDeleted Stack...# _toBeDeletedMeshImps.Push(toBeUpdatedMeshImp.IMeshImp); // remove the meshImp from the dictionary, the meshImp data now only resides inside the gpu and will be cleaned up on bottom of Render(Mesh mesh) - _ = _identifierToMeshImpDictionary.Remove(meshDataEventArgs.Mesh.SessionUniqueIdentifier); + _ = _identifierToMeshImpDictionary.Remove(meshDataEventArgs.Mesh.UniqueIdentifier); } internal void UpdateAllMeshes() @@ -210,19 +210,19 @@ internal void UpdateAllMeshes() private void DisposeInstanceData(object? sender, InstanceDataChangedEventArgs instanceDataEventArgs) { - if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceDataEventArgs.InstanceData.SessionUniqueId, out IInstanceDataImp? instanceDataImp)) + if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceDataEventArgs.InstanceData.UniqueId, out IInstanceDataImp instanceDataImp)) throw new KeyNotFoundException("InstanceData is not registered."); // Add the meshImp to the toBeDeleted Stack... _toBeDeletedInstanceDataImps.Push(instanceDataImp); // remove the meshImp from the dictionary, the meshImp data now only resides inside the gpu and will be cleaned up on bottom of Render(Mesh mesh) - _ = _identifierToInstanceDataImpDictionary.Remove(instanceDataEventArgs.InstanceData.SessionUniqueId); + _ = _identifierToInstanceDataImpDictionary.Remove(instanceDataEventArgs.InstanceData.UniqueId); } private void InstanceDataChanged(object? sender, InstanceDataChangedEventArgs instanceDataEventArgs) { - if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceDataEventArgs.InstanceData.SessionUniqueId, out var instanceImp)) + if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceDataEventArgs.InstanceData.UniqueId, out var instanceImp)) { throw new ArgumentException("InstanceData is not registered yet. Use RegisterInstanceData first."); } @@ -286,7 +286,7 @@ public void RegisterNewMesh(GpuMesh mesh, float3[] vertices, uint[] triangles, f mesh.DisposeData += DisposeMesh; meshImp.MeshType = mesh.MeshType; - _identifierToMeshImpDictionary.Add(mesh.SessionUniqueIdentifier, (meshImp, null)); + _identifierToMeshImpDictionary.Add(mesh.UniqueIdentifier, (meshImp, null)); } // Configure newly created MeshImp to reflect Mesh's properties on GPU (allocate buffers) @@ -340,7 +340,7 @@ private IMeshImp RegisterNewMesh(Mesh mesh) meshImp.MeshType = mesh.MeshType; - _identifierToMeshImpDictionary.Add(mesh.SessionUniqueIdentifier, (meshImp, mesh)); + _identifierToMeshImpDictionary.Add(mesh.UniqueIdentifier, (meshImp, mesh)); return meshImp; @@ -350,7 +350,7 @@ private IMeshImp RegisterNewMesh(Mesh mesh) private IInstanceDataImp RegisterNewInstanceData(Mesh mesh, InstanceData instanceData) { - if (!_identifierToMeshImpDictionary.TryGetValue(mesh.SessionUniqueIdentifier, out var meshImp)) + if (!_identifierToMeshImpDictionary.TryGetValue(mesh.UniqueIdentifier, out var meshImp)) { throw new ArgumentException("Mesh is not registered yet. Use RegisterMesh first."); } @@ -361,7 +361,7 @@ private IInstanceDataImp RegisterNewInstanceData(Mesh mesh, InstanceData instanc var instanceDataImp = _renderContextImp.CreateInstanceDataImp(meshImp.IMeshImp); instanceDataImp.Amount = instanceData.Amount; - _identifierToInstanceDataImpDictionary.Add(instanceData.SessionUniqueId, instanceDataImp); + _identifierToInstanceDataImpDictionary.Add(instanceData.UniqueId, instanceDataImp); _renderContextImp.SetInstanceTransform(instanceDataImp, instanceData.Positions, instanceData.Rotations, instanceData.Scales); _renderContextImp.SetInstanceColor(instanceDataImp, instanceData.Colors); @@ -370,7 +370,7 @@ private IInstanceDataImp RegisterNewInstanceData(Mesh mesh, InstanceData instanc public IMeshImp GetImpFromMesh(Mesh m) { - if (!_identifierToMeshImpDictionary.TryGetValue(m.SessionUniqueIdentifier, out var foundMeshImp)) + if (!_identifierToMeshImpDictionary.TryGetValue(m.UniqueIdentifier, out var foundMeshImp)) { return RegisterNewMesh(m); } @@ -379,7 +379,7 @@ public IMeshImp GetImpFromMesh(Mesh m) public IMeshImp GetImpFromMesh(GpuMesh m) { - if (!_identifierToMeshImpDictionary.TryGetValue(m.SessionUniqueIdentifier, out var foundMeshImp)) + if (!_identifierToMeshImpDictionary.TryGetValue(m.UniqueIdentifier, out var foundMeshImp)) { throw new ArgumentException("GpuMesh not found, make sure you created it first."); } @@ -388,7 +388,7 @@ public IMeshImp GetImpFromMesh(GpuMesh m) public IInstanceDataImp GetImpFromInstanceData(Mesh m, InstanceData instanceData) { - if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceData.SessionUniqueId, out IInstanceDataImp? imp)) + if (!_identifierToInstanceDataImpDictionary.TryGetValue(instanceData.UniqueId, out IInstanceDataImp imp)) { return RegisterNewInstanceData(m, instanceData); } diff --git a/src/Engine/Core/Scene/InstanceData.cs b/src/Engine/Core/Scene/InstanceData.cs index e0efdadc4..1471e0561 100644 --- a/src/Engine/Core/Scene/InstanceData.cs +++ b/src/Engine/Core/Scene/InstanceData.cs @@ -70,7 +70,7 @@ public float3[]? Scales /// /// The color of each instance. This array needs to be as long as . /// - public float4[]? Colors + public uint[]? Colors { get => _colors; set @@ -81,7 +81,7 @@ public float4[]? Colors DataChanged?.Invoke(this, new InstanceDataChangedEventArgs(this, InstanceDataChangedEnum.Colors)); } } - private float4[]? _colors; + private uint[]? _colors; /// /// The amount of instances that will be rendered. @@ -91,7 +91,7 @@ public float4[]? Colors /// /// The unique id of this object. /// - public Suid SessionUniqueId { get; } = Suid.GenerateSuid(); + public Guid UniqueId { get; } = Guid.NewGuid(); /// /// Creates a new instance of type . Will fail if the length of a provided array doesn't match . @@ -102,7 +102,7 @@ public float4[]? Colors /// The scale of each instance. /// The color of each instance. /// - public InstanceData(int amount, float3[] positions, float3[]? rotations = null, float3[]? scales = null, float4[]? colors = null) + public InstanceData(int amount, float3[] positions, float3[]? rotations = null, float3[]? scales = null, uint[]? colors = null) { Amount = amount; Guard.IsEqualTo(positions.Length, Amount); diff --git a/src/Engine/Core/Scene/Mesh.cs b/src/Engine/Core/Scene/Mesh.cs index b7961524a..1c1016a89 100644 --- a/src/Engine/Core/Scene/Mesh.cs +++ b/src/Engine/Core/Scene/Mesh.cs @@ -172,7 +172,7 @@ public class Mesh : SceneComponent, IManagedMesh /// /// SessionUniqueIdentifier is used to verify a Mesh's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; } = Suid.GenerateSuid(); + public Guid UniqueIdentifier { get; } = Guid.NewGuid(); /// /// Update all changed data before each frame? diff --git a/src/Engine/Core/Scene/SceneExtensions.cs b/src/Engine/Core/Scene/SceneExtensions.cs index 6dd626f90..9d694d280 100644 --- a/src/Engine/Core/Scene/SceneExtensions.cs +++ b/src/Engine/Core/Scene/SceneExtensions.cs @@ -528,8 +528,8 @@ public static void FpsView(this Transform tc, float angleHorz, float angleVert, if ((angleVert >= M.TwoPi && angleVert > 0f) || angleVert <= -M.TwoPi) angleVert %= M.TwoPi; - var camForward = float4x4.CreateRotationXY(new float2(angleVert, angleHorz)) * float3.UnitZ; - var camRight = float4x4.CreateRotationXY(new float2(angleVert, angleHorz)) * float3.UnitX; + var camForward = float4x4.CreateRotationXY(angleVert, angleHorz) * float3.UnitZ; + var camRight = float4x4.CreateRotationXY(angleVert, angleHorz) * float3.UnitX; tc.Translation += camForward * inputWSAxis * speed; tc.Translation += camRight * inputADAxis * speed; diff --git a/src/Engine/Core/ScenePicker.cs b/src/Engine/Core/ScenePicker.cs index 88a6644df..2e743e35f 100644 --- a/src/Engine/Core/ScenePicker.cs +++ b/src/Engine/Core/ScenePicker.cs @@ -180,7 +180,7 @@ public PickerState() /// /// The pick position on the screen. /// - public float2 PickPosClip { get; set; } + public float2 PickPosClip { get; private set; } private float4x4 _view; private float4x4 _invView; @@ -278,6 +278,10 @@ protected override void InitState() } } + //Early out for the case that the scene is rendered with more than one canvas and the mouse isn't inside the correct one. + if (pickCam == null || pickCam == default) + return null; + CurrentCameraResult = pickCam; pickPosClip = ((pickPos - new float2(pickCamRect.Left, pickCamRect.Top)) * new float2(2.0f / pickCamRect.Width, -2.0f / pickCamRect.Height)) + new float2(-1, 1); @@ -598,7 +602,6 @@ public void HandleMesh(Mesh mesh) private void PickLineAdjacencyGeometry(Mesh mesh) { - var mvp = _projection * _view * State.Model; var matOfNode = CurrentNode.GetComponent(); if (matOfNode == null) @@ -610,7 +613,7 @@ private void PickLineAdjacencyGeometry(Mesh mesh) if (mesh.Triangles == null) return; if (mesh.Vertices == null) return; - if (CurrentCameraResult == null) + if (CurrentCameraResult.Camera == null) { Diagnostics.Warn("No camera found in SceneGraph, no picking possible!"); return; @@ -698,7 +701,6 @@ private void PickLineAdjacencyGeometry(Mesh mesh) private void PickLineGeometry(Mesh mesh) { - var mvp = _projection * _view * State.Model; var matOfNode = CurrentNode.GetComponent(); @@ -711,7 +713,7 @@ private void PickLineGeometry(Mesh mesh) if (mesh.Triangles == null) return; if (mesh.Vertices == null) return; - if (CurrentCameraResult == null) + if (CurrentCameraResult.Camera == null) { Diagnostics.Warn("No camera found in SceneGraph, no picking possible!"); return; @@ -781,7 +783,8 @@ private void PickTriangleGeometry(Mesh mesh) return; } - var ray = new RayF(PickPosClip, _view, _projection); + if (_currentCameraResult.Camera == null) return; + var ray = new RayF(PickPosClip, _view, _projection, _currentCameraResult.Camera.ProjectionMethod == ProjectionMethod.Orthographic); var box = State.Model * mesh.BoundingBox; if (!box.IntersectRay(ray)) diff --git a/src/Engine/Core/Texture.cs b/src/Engine/Core/Texture.cs index e5827191b..063f7e7a1 100644 --- a/src/Engine/Core/Texture.cs +++ b/src/Engine/Core/Texture.cs @@ -27,7 +27,7 @@ protected Texture() { } /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; protected set; } + public Guid UniqueIdentifier { get; protected set; } #endregion /// @@ -144,7 +144,7 @@ public TextureFilterMode FilterMode /// Defines the wrapping mode . public Texture(byte[] pixelData, int width, int height, ImagePixelFormat colorFormat, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.LinearMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); ImageData = new ImageData(pixelData, width, height, colorFormat); DoGenerateMipMaps = generateMipMaps; FilterMode = filterMode; @@ -160,7 +160,7 @@ public Texture(byte[] pixelData, int width, int height, ImagePixelFormat colorFo /// Defines the wrapping mode . public Texture(IImageData imageData, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.NearestMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); ImageData = imageData; DoGenerateMipMaps = generateMipMaps; diff --git a/src/Engine/Core/Texture1D.cs b/src/Engine/Core/Texture1D.cs index d0d53ad62..db876affd 100644 --- a/src/Engine/Core/Texture1D.cs +++ b/src/Engine/Core/Texture1D.cs @@ -22,7 +22,7 @@ public class Texture1D : Texture /// Defines the wrapping mode . public Texture1D(byte[] pixelData, int width, ImagePixelFormat colorFormat, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.LinearMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); ImageData = new ImageData(pixelData, width, 1, colorFormat); DoGenerateMipMaps = generateMipMaps; FilterMode = filterMode; @@ -41,7 +41,7 @@ public Texture1D(IImageData imageData, bool generateMipMaps = true, TextureFilte if (imageData.Height != 1) throw new ArgumentException("Height of the image data is not 1, use a Texture instead."); - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); ImageData = imageData; DoGenerateMipMaps = generateMipMaps; diff --git a/src/Engine/Core/TextureManager.cs b/src/Engine/Core/TextureManager.cs index 24785bbc3..e2c1d4eef 100644 --- a/src/Engine/Core/TextureManager.cs +++ b/src/Engine/Core/TextureManager.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Diagnostics; +using CommunityToolkit.Diagnostics; using Fusee.Engine.Common; using System; using System.Collections.Generic; @@ -11,7 +11,7 @@ internal class TextureManager private readonly Stack _toBeDeletedTextureHandles = new(); - private readonly Dictionary> _identifierToTextureHandleDictionary = new(); + private readonly Dictionary> _identifierToTextureHandleDictionary = new(); private void Remove(ITextureHandle textureHandle) { @@ -20,8 +20,8 @@ private void Remove(ITextureHandle textureHandle) private void TextureChanged(object? sender, TextureEventArgs textureDataEventArgs) { - if (!_identifierToTextureHandleDictionary.TryGetValue(textureDataEventArgs.Texture.SessionUniqueIdentifier, - out Tuple? toBeUpdatedTextureTuple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(textureDataEventArgs.Texture.UniqueIdentifier, + out Tuple toBeUpdatedTextureTuple)) { throw new KeyNotFoundException("Texture is not registered."); } @@ -34,7 +34,7 @@ private void TextureChanged(object? sender, TextureEventArgs textureDataEventArg // Add the TextureHandle to the toBeDeleted Stack... _toBeDeletedTextureHandles.Push(toBeUpdatedTextureTuple.Item1); // remove the TextureHandle from the dictionary, the TextureHandle data now only resides inside the gpu and will be cleaned up on bottom of Render(Mesh mesh) - _identifierToTextureHandleDictionary.Remove(texture.SessionUniqueIdentifier); + _identifierToTextureHandleDictionary.Remove(texture.UniqueIdentifier); // add the identifier to the reusable identifiers stack //_reusableIdentifiers.Push(textureDataEventArgs.Texture.Identifier); break; @@ -69,7 +69,7 @@ private ITextureHandle RegisterNewTexture(ExposedTexture texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(texture.TextureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(texture.TextureHandle, texture)); return textureHandle; } @@ -86,7 +86,7 @@ private ITextureHandle RegisterNewTexture(WritableMultisampleTexture texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(texture.InternalTextureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(texture.InternalTextureHandle, texture)); return textureHandle; } @@ -100,7 +100,7 @@ private ITextureHandle RegisterNewTexture(WritableCubeMap texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(textureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(textureHandle, texture)); return textureHandle; } @@ -114,7 +114,7 @@ private ITextureHandle RegisterNewTexture(WritableArrayTexture texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(textureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(textureHandle, texture)); return textureHandle; } @@ -128,7 +128,7 @@ private ITextureHandle RegisterNewTexture(WritableTexture texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(textureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(textureHandle, texture)); return textureHandle; } @@ -141,7 +141,7 @@ private ITextureHandle RegisterNewTexture(Texture1D texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(textureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(textureHandle, texture)); return textureHandle; } @@ -154,7 +154,7 @@ private ITextureHandle RegisterNewTexture(Texture texture) // Setup handler to observe changes of the texture data and dispose event (deallocation) texture.TextureChanged += TextureChanged; - _identifierToTextureHandleDictionary.Add(texture.SessionUniqueIdentifier, new Tuple(textureHandle, texture)); + _identifierToTextureHandleDictionary.Add(texture.UniqueIdentifier, new Tuple(textureHandle, texture)); return textureHandle; } @@ -170,7 +170,7 @@ public TextureManager(IRenderContextImp renderContextImp) public ITextureHandle GetTextureHandle(Texture texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureTouple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureTouple)) { return RegisterNewTexture(texture); } @@ -179,7 +179,7 @@ public ITextureHandle GetTextureHandle(Texture texture) public ITextureHandle GetTextureHandle(ExposedTexture texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureTouple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureTouple)) { return RegisterNewTexture(texture); } @@ -188,7 +188,7 @@ public ITextureHandle GetTextureHandle(ExposedTexture texture) public ITextureHandle GetTextureHandle(WritableMultisampleTexture texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureTouple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureTouple)) { return RegisterNewTexture(texture); } @@ -197,7 +197,7 @@ public ITextureHandle GetTextureHandle(WritableMultisampleTexture texture) public ITextureHandle GetTextureHandle(WritableCubeMap texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureTouple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureTouple)) { return RegisterNewTexture(texture); } @@ -206,7 +206,7 @@ public ITextureHandle GetTextureHandle(WritableCubeMap texture) public ITextureHandle GetTextureHandle(WritableArrayTexture texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureTouple)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureTouple)) { return RegisterNewTexture(texture); } @@ -215,7 +215,7 @@ public ITextureHandle GetTextureHandle(WritableArrayTexture texture) public ITextureHandle GetTextureHandle(WritableTexture texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureItem)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureItem)) { return RegisterNewTexture(texture); } @@ -224,7 +224,7 @@ public ITextureHandle GetTextureHandle(WritableTexture texture) public ITextureHandle GetTextureHandle(Texture1D texture) { - if (!_identifierToTextureHandleDictionary.TryGetValue(texture.SessionUniqueIdentifier, out var foundTextureItem)) + if (!_identifierToTextureHandleDictionary.TryGetValue(texture.UniqueIdentifier, out var foundTextureItem)) { return RegisterNewTexture(texture); } diff --git a/src/Engine/Core/WritableArrayTexture.cs b/src/Engine/Core/WritableArrayTexture.cs index c8805943d..ac86d0f38 100644 --- a/src/Engine/Core/WritableArrayTexture.cs +++ b/src/Engine/Core/WritableArrayTexture.cs @@ -17,7 +17,7 @@ public class WritableArrayTexture : IWritableArrayTexture /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; private set; } + public Guid UniqueIdentifier { get; private set; } /// /// Type of the render texture, . @@ -122,7 +122,7 @@ public Compare CompareFunc /// The textures compare function. If uncertain, leaf on LEESS, this is only important for depth (shadow) textures and if the CompareMode isn't NONE () public WritableArrayTexture(int layers, RenderTargetTextureTypes texType, ImagePixelFormat colorFormat, int width, int height, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.NearestMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat, TextureCompareMode compareMode = TextureCompareMode.None, Compare compareFunc = Compare.Less) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); PixelFormat = colorFormat; Width = width; Height = height; diff --git a/src/Engine/Core/WritableCubeMap.cs b/src/Engine/Core/WritableCubeMap.cs index 2484df3c9..de5f653c8 100644 --- a/src/Engine/Core/WritableCubeMap.cs +++ b/src/Engine/Core/WritableCubeMap.cs @@ -17,7 +17,7 @@ public class WritableCubeMap : IWritableCubeMap /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; private set; } + public Guid UniqueIdentifier { get; private set; } /// /// Specifies if mipmaps are created for this texture. @@ -117,7 +117,7 @@ public Compare CompareFunc /// The type of the texture. public WritableCubeMap(RenderTargetTextureTypes textureType, ImagePixelFormat colorFormat, int width, int height, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.LinearMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat, TextureCompareMode compareMode = TextureCompareMode.None, Compare compareFunc = Compare.Less) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); PixelFormat = colorFormat; Width = width; Height = height; diff --git a/src/Engine/Core/WritableMultisampleTexture.cs b/src/Engine/Core/WritableMultisampleTexture.cs index 0f2b30e35..01a123cb0 100644 --- a/src/Engine/Core/WritableMultisampleTexture.cs +++ b/src/Engine/Core/WritableMultisampleTexture.cs @@ -19,7 +19,7 @@ public class WritableMultisampleTexture : IWritableTexture /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; } + public Guid UniqueIdentifier { get; } /// /// Type of the render texture, . @@ -125,7 +125,7 @@ public WritableMultisampleTexture(RenderTargetTextureTypes texType, ImagePixelFo // throw new ArgumentException($"Multisample texture factor {multisampleFactor} is either '0' or too big. GL_MAX_SAMPLES for this ImagePixelFormat is {maxSamples}"); //} - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); PixelFormat = colorFormat; Width = width; Height = height; diff --git a/src/Engine/Core/WritableTexture.cs b/src/Engine/Core/WritableTexture.cs index ec978bdaa..b35f26cc3 100644 --- a/src/Engine/Core/WritableTexture.cs +++ b/src/Engine/Core/WritableTexture.cs @@ -18,7 +18,7 @@ public class WritableTexture : IWritableTexture /// /// SessionUniqueIdentifier is used to verify a Textures's uniqueness in the current session. /// - public Suid SessionUniqueIdentifier { get; private set; } + public Guid UniqueIdentifier { get; private set; } /// /// Type of the render texture, . @@ -123,7 +123,7 @@ public Compare CompareFunc /// The textures compare function. If uncertain, leaf on LEESS, this is only important for depth (shadow) textures and if the CompareMode isn't NONE () public WritableTexture(RenderTargetTextureTypes texType, ImagePixelFormat colorFormat, int width, int height, bool generateMipMaps = true, TextureFilterMode filterMode = TextureFilterMode.NearestMipmapLinear, TextureWrapMode wrapMode = TextureWrapMode.Repeat, TextureCompareMode compareMode = TextureCompareMode.None, Compare compareFunc = Compare.Less) { - SessionUniqueIdentifier = Suid.GenerateSuid(); + UniqueIdentifier = Guid.NewGuid(); PixelFormat = colorFormat; Width = width; Height = height; diff --git a/src/Engine/Imp/Graphics/Android/RenderContextImp.cs b/src/Engine/Imp/Graphics/Android/RenderContextImp.cs index c639857b1..9d63df89b 100644 --- a/src/Engine/Imp/Graphics/Android/RenderContextImp.cs +++ b/src/Engine/Imp/Graphics/Android/RenderContextImp.cs @@ -1322,7 +1322,7 @@ public void SetInstanceTransform(IInstanceDataImp instanceImp, float3[] instance /// /// The instance. /// The instance colors. - public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColors) + public void SetInstanceColor(IInstanceDataImp instanceImp, uint[] instanceColors) { if (instanceColors == null) return; @@ -1334,7 +1334,7 @@ public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColo } //TODO: can we use AttributeLocations.Color? - int sizeOfCol = sizeof(float) * 4; + int sizeOfCol = sizeof(uint); int iColorBytes = instanceColors.Length * sizeOfCol; int instanceColorBo = ((InstanceDataImp)instanceImp).InstanceColorBufferObject; if (instanceColorBo == 0) diff --git a/src/Engine/Imp/Graphics/Blazor/RenderContextImp.cs b/src/Engine/Imp/Graphics/Blazor/RenderContextImp.cs index 6f3115160..067760f96 100644 --- a/src/Engine/Imp/Graphics/Blazor/RenderContextImp.cs +++ b/src/Engine/Imp/Graphics/Blazor/RenderContextImp.cs @@ -1226,34 +1226,23 @@ public void SetInstanceTransform(IInstanceDataImp instanceImp, float3[] instance /// /// The . /// The colors of the instances. - public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColors) + public void SetInstanceColor(IInstanceDataImp instanceImp, uint[] instanceColors) { if (instanceColors == null) return; - float[] colorsFlat = new float[instanceColors.Length * 4]; - int i = 0; - foreach (float4 v in instanceColors) - { - colorsFlat[i] = v.x; - colorsFlat[i + 1] = v.y; - colorsFlat[i + 2] = v.z; - colorsFlat[i + 3] = v.w; - i += 4; - } - int vboBytes; - int colsBytes = instanceColors.Length * 4 * sizeof(float); + int colsBytes = instanceColors.Length * sizeof(uint); if (((InstanceDataImp)instanceImp).InstanceColorBufferObject == null) { ((InstanceDataImp)instanceImp).InstanceColorBufferObject = gl2.CreateBuffer(); gl2.BindBuffer(ARRAY_BUFFER, ((InstanceDataImp)instanceImp).InstanceColorBufferObject); - gl2.BufferData(ARRAY_BUFFER, colorsFlat, DYNAMIC_DRAW); + gl2.BufferData(ARRAY_BUFFER, instanceColors, DYNAMIC_DRAW); } else { gl2.BindBuffer(ARRAY_BUFFER, ((InstanceDataImp)instanceImp).InstanceColorBufferObject); - gl2.BufferSubData(ARRAY_BUFFER, IntPtr.Zero, colorsFlat); + gl2.BufferSubData(ARRAY_BUFFER, IntPtr.Zero, instanceColors); } vboBytes = (int)gl2.GetBufferParameter(ARRAY_BUFFER, BUFFER_SIZE); diff --git a/src/Engine/Imp/Graphics/Desktop/Fusee.Engine.Imp.Graphics.Desktop.csproj b/src/Engine/Imp/Graphics/Desktop/Fusee.Engine.Imp.Graphics.Desktop.csproj index 4ff3c6fd5..ccf941357 100644 --- a/src/Engine/Imp/Graphics/Desktop/Fusee.Engine.Imp.Graphics.Desktop.csproj +++ b/src/Engine/Imp/Graphics/Desktop/Fusee.Engine.Imp.Graphics.Desktop.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -27,7 +27,7 @@ analyzers - + diff --git a/src/Engine/Imp/Graphics/Desktop/RenderContextImp.cs b/src/Engine/Imp/Graphics/Desktop/RenderContextImp.cs index 1d6e3c1e2..21ab9ffca 100644 --- a/src/Engine/Imp/Graphics/Desktop/RenderContextImp.cs +++ b/src/Engine/Imp/Graphics/Desktop/RenderContextImp.cs @@ -1366,7 +1366,7 @@ public void SetInstanceTransform(IInstanceDataImp instanceImp, float3[] instance /// /// The . /// The colors of the instances. - public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColors) + public void SetInstanceColor(IInstanceDataImp instanceImp, uint[] instanceColors) { if (instanceColors == null) return; @@ -1378,7 +1378,7 @@ public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColo } //TODO: can we use AttributeLocations.Color? - int sizeOfCol = sizeof(float) * 4; + int sizeOfCol = sizeof(uint); int iColorBytes = instanceColors.Length * sizeOfCol; int instanceColorBo = ((InstanceDataImp)instanceImp).InstanceColorBufferObject; if (instanceColorBo == 0) @@ -1399,7 +1399,7 @@ public void SetInstanceColor(IInstanceDataImp instanceImp, float4[] instanceColo throw new ApplicationException(string.Format("Problem uploading normal buffer to VBO. Tried to upload {0} bytes, uploaded {1}.", instancedColorBytes, iColorBytes)); // set attribute pointers for matrix (4 times vec4) - GL.VertexArrayAttribFormat(vao, AttributeLocations.InstancedColor, 4, VertexAttribType.Float, false, 0); + GL.VertexArrayAttribFormat(vao, AttributeLocations.InstancedColor, 4, VertexAttribType.UnsignedByte, false, 0); GL.VertexArrayAttribBinding(vao, AttributeLocations.InstancedColor, AttributeLocations.InstancedColorBindingIndex); GL.VertexArrayBindingDivisor(vao, AttributeLocations.InstancedColor, 1); } diff --git a/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFilePicker.cs b/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFilePicker.cs index c4091a388..ef0d54bea 100644 --- a/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFilePicker.cs +++ b/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFilePicker.cs @@ -143,7 +143,7 @@ private bool IsNewFolderNameWindowOpen private Exception? _createFolderException; public FileInfo? SelectedFile { get; protected set; } - public DirectoryInfo RootFolder { get; protected set; } + public DirectoryInfo RootFolder; public int FontSize; public ImFontPtr SymbolsFontPtr = null; diff --git a/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFolderPicker.cs b/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFolderPicker.cs index c275639a2..0b46dac50 100644 --- a/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFolderPicker.cs +++ b/src/ImGui/Desktop/Fusee.ImGui.Desktop/Templates/ImGuiFolderPicker.cs @@ -131,7 +131,7 @@ private bool IsNewFolderNameWindowOpen private Exception? _createFolderException; public DirectoryInfo? SelectedFolder { get; protected set; } - public DirectoryInfo RootFolder { get; protected set; } + public DirectoryInfo RootFolder; public int FontSize; public ImFontPtr SymbolsFontPtr = null; diff --git a/src/Math/Core/AABBf.cs b/src/Math/Core/AABBf.cs index 6eaeb60d4..cd3405a1b 100644 --- a/src/Math/Core/AABBf.cs +++ b/src/Math/Core/AABBf.cs @@ -230,28 +230,41 @@ public bool InsideOrIntersectingPlane(PlaneF plane) /// public bool IntersectRay(RayF ray) { - if (Intersects(ray.Origin)) - return true; + var tmin = 0f; + float tmax = float.MaxValue; - float t1 = (min.x - ray.Origin.x) * ray.Inverse.x; - float t2 = (max.x - ray.Origin.x) * ray.Inverse.x; - - float tmin = M.Min(t1, t2); - float tmax = M.Max(t1, t2); - - for (int i = 1; i < 3; i++) + //For all three slabs (slab = space between two parallel box planes). + for (int i = 0; i < 3; i++) { - t1 = (min[i] - ray.Origin[i]) * ray.Inverse[i]; - t2 = (max[i] - ray.Origin[i]) * ray.Inverse[i]; - - t1 = float.IsNaN(t1) ? 0.0f : t1; - t2 = float.IsNaN(t2) ? 0.0f : t2; - - tmin = M.Max(tmin, M.Min(t1, t2)); - tmax = M.Min(tmax, M.Max(t1, t2)); + //Ray is parallel to slab. No hit if origin not within slab. + if (MathF.Abs(ray.Direction[i]) < float.Epsilon) + { + if (ray.Origin[i] < min[i] || ray.Origin[i] > max[i]) + return false; + } + else + { + //Compute intersection t value of ray within near and far plane of slab + //float ood = 1.0f / ray.Direction[i]; + float t1 = (min[i] - ray.Origin[i]) * ray.Inverse[i]; + float t2 = (max[i] - ray.Origin[i]) * ray.Inverse[i]; + + //Make t1 be intersection with near plane, t2 with far plane + if (t1 > t2) + (t2, t1) = (t1, t2); //Swap + + //Compute intersection of slab intersection intervals + tmin = MathF.Max(tmin, t1); + tmax = MathF.Min(tmax, t2); + //Exit with no collision as soon as slab intersection becomes empty + if (tmin > tmax) return false; + + } } - return tmax >= M.Max(tmin, 0.0); + //Ray intersects all 3 slabs. Return intersection point (q) and intersection value (tmin) + //var q = ray.Origin * ray.Direction * tmin; + return true; } /// diff --git a/src/Math/Core/Rayf.cs b/src/Math/Core/Rayf.cs index b3e335c9c..608a88157 100644 --- a/src/Math/Core/Rayf.cs +++ b/src/Math/Core/Rayf.cs @@ -43,8 +43,7 @@ public float3 Inverse { if (_inverseDirty) { - _inverse = new float3(1 / Direction.x, 1 / Direction.y, 1 / Direction.z); - + _inverse = new float3(1 / Direction.x, 1 / Direction.y, 1 / Direction.z).Normalize(); _inverseDirty = false; } @@ -68,24 +67,40 @@ public RayF(float3 origin_, float3 direction_) } /// - /// Creates a new ray. + /// Creates a new ray in world space using clip coordinates. + /// Origin is the camera's world space position. /// /// A mouse position in Clip Space. /// The View Matrix of the rendered scene. /// The Projection Matrix of the rendered scene. - public RayF(float2 pickPosClip, float4x4 view, float4x4 projection) + /// Is the projection matrix a orthographic one? + public RayF(float2 pickPosClip, float4x4 view, float4x4 projection, bool isOrthographic = false) { - float4x4 invViewProjection = float4x4.Invert(projection * view); - - var pickPosFarWorld = float4x4.TransformPerspective(invViewProjection, new float3(pickPosClip.x, pickPosClip.y, 1)); - var pickPosNearWorld = float4x4.TransformPerspective(invViewProjection, new float3(pickPosClip.x, pickPosClip.y, -1)); - - Origin = pickPosNearWorld; - - _direction = (pickPosFarWorld - pickPosNearWorld).Normalize(); + if (!isOrthographic) + { + //No need for perspective devision here. Ray has no intrinsic depth. + //Doesn't work for ortho projection. + //Numerically more stable because of the missing perspective division. + float4 rayClip = new(pickPosClip.x, pickPosClip.y, 1.0f, 1.0f); + float4 rayEye = float4x4.Invert(projection) * rayClip; + rayEye.z = 1.0f; + rayEye.w = 0.0f; + + var invView = float4x4.Invert(view); + + float3 rayWorld = (invView * rayEye).xyz.Normalize(); + Origin = invView.Column4.xyz; + Direction = rayWorld; + } + else + { + float4x4 invViewProjection = float4x4.Invert(projection * view); + var pickPosFarWorld = invViewProjection * new float3(pickPosClip.x, pickPosClip.y, 1); + var pickPosNearWorld = invViewProjection * new float3(pickPosClip.x, pickPosClip.y, -1); - _inverse = new float3(1 / _direction.x, 1 / _direction.y, 1 / _direction.z); - _inverseDirty = false; + Origin = pickPosNearWorld; + Direction = (pickPosFarWorld - pickPosNearWorld).Normalize(); + } } } } \ No newline at end of file diff --git a/src/PointCloud/Common/IPointCloudImp.cs b/src/PointCloud/Common/IPointCloudImp.cs index 34085ee0c..5c6b1ae41 100644 --- a/src/PointCloud/Common/IPointCloudImp.cs +++ b/src/PointCloud/Common/IPointCloudImp.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.HighPerformance.Buffers; +using CommunityToolkit.HighPerformance.Buffers; using Fusee.Engine.Core; using Fusee.Math.Core; using System.Collections.Generic; @@ -11,17 +11,17 @@ namespace Fusee.PointCloud.Common public interface IPointCloudImpBase { /// - /// Token for invalidating the cached gpu data . + /// Object for handling the invalidation of the gpu data cache. /// public InvalidateGpuDataCache InvalidateGpuDataCache { get; } /// - /// Center of the PointCloud's AABB + /// Center of the PointCloud's AABB (already shifted/offsetted) /// public float3 Center { get; } /// - /// Dimensions of the PointCloud's AABB + /// Dimensions of the PointCloud's AABB ("real" size, not the octree cube) /// public float3 Size { get; } diff --git a/src/PointCloud/Common/IPointCloudOctant.cs b/src/PointCloud/Common/IPointCloudOctant.cs index 5339e7a57..ae57604a7 100644 --- a/src/PointCloud/Common/IPointCloudOctant.cs +++ b/src/PointCloud/Common/IPointCloudOctant.cs @@ -6,7 +6,7 @@ namespace Fusee.PointCloud.Common /// /// Used in . Allows the use in non-generic context, e.g. in s. /// - public interface IPointCloudOctant : IEmptyOctant + public interface IPointCloudOctant : IEmptyOctant { /// /// The number of points that fall into this octant. diff --git a/src/PointCloud/Common/PointRenderEnums.cs b/src/PointCloud/Common/PointRenderEnums.cs index 17e36013c..33f0954e1 100644 --- a/src/PointCloud/Common/PointRenderEnums.cs +++ b/src/PointCloud/Common/PointRenderEnums.cs @@ -1,32 +1,5 @@ namespace Fusee.PointCloud.Common { - /// - /// The gpu data can take different states in its life cycle. - /// Gpu data may need to be handled differently according to its current state. - /// - public enum GpuDataState - { - /// - /// Default, gpu data wasn't created yet and or points havent been loaded yet. - /// - None = -1, - - /// - /// Gpu data was newly created. - /// - New = 0, - - /// - /// Gpu data accessed but hasn't changed. - /// - Unchanged = 1, - - /// - /// Gpu data accessed and has changed. For example if a property of the data was updated. - /// - Changed = 2, - } - /// /// Available render modes. /// diff --git a/src/PointCloud/Core/MeshMaker.cs b/src/PointCloud/Core/MeshMaker.cs index 4324cba61..357b0a588 100644 --- a/src/PointCloud/Core/MeshMaker.cs +++ b/src/PointCloud/Core/MeshMaker.cs @@ -1,13 +1,73 @@ -using CommunityToolkit.HighPerformance.Buffers; +using CommunityToolkit.HighPerformance; +using CommunityToolkit.HighPerformance.Buffers; using Fusee.Engine.Common; using Fusee.Engine.Core; using Fusee.Engine.Core.Scene; using Fusee.Math.Core; -using Fusee.PointCloud.Common; +using System; using System.Collections.Generic; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.InteropServices; namespace Fusee.PointCloud.Core { + /// + /// Delegate for a method that knows how to parse a slice of a point's extra bytes to a valid uint. + /// + /// + /// + public delegate uint HandleReadExtraBytes(Span bytes); + + /// + /// Meta data needed to create a mesh from a point cloud file. + /// + public struct CreateMeshMetaData + { + /// + /// Size of one point in byte. + /// + public int PointSize; + + /// + /// Byte offset in one point to reach the position value. + /// + public int OffsetToPosValues; + + /// + /// Byte offset in one point to reach the color value. + /// -1 means there is no such value. + /// + public int OffsetColor; + + /// + /// Byte offset in one point to reach the intensity value. + /// -1 means there is no such value. + /// + public int OffsetIntensity; + + /// + /// Maximum intensity value. + /// + public double IntensityMax; + + /// + /// Minimum intensity value. + /// + public double IntensityMin; + + /// + /// The x, y, z values used to scale the point positions. + /// + public double3 Scale; + + /// + /// Byte offset in one point to reach the extra bytes. + /// -1 means there is no such value. + /// + public int OffsetToExtraBytes; + } + /// /// Static class that provides generic methods that take point cloud points and return s. /// @@ -16,41 +76,39 @@ public static class MeshMaker /// /// Generic method that creates meshes with 65k points maximum. /// - /// The generic point cloud points. + /// The that contains the points. /// The method that defines how to create a GpuMesh from the point cloud points. - /// The octant identifier. /// - public static IEnumerable CreateMeshes(MemoryOwner points, CreateGpuData createGpuDataHandler, OctantId octantId) + public static IEnumerable CreateMeshes(MemoryOwner points, CreateGpuData createGpuDataHandler) { List meshes; - var ptCnt = points.Length; + var numberOfPointInNode = points.Length; + int maxVertCount = ushort.MaxValue - 1; - var noOfMeshes = (int)System.Math.Ceiling((float)ptCnt / maxVertCount); + var noOfMeshes = (int)System.Math.Ceiling((float)numberOfPointInNode / maxVertCount); meshes = new(noOfMeshes); int meshCnt = 0; - for (int i = 0; i < ptCnt; i += maxVertCount) + + for (int i = 0; i < numberOfPointInNode; i += maxVertCount) { int numberOfPointsInMesh; if (noOfMeshes == 1) - numberOfPointsInMesh = ptCnt; + numberOfPointsInMesh = numberOfPointInNode; else if (noOfMeshes == meshCnt + 1) - numberOfPointsInMesh = (ptCnt - maxVertCount * meshCnt); + numberOfPointsInMesh = (numberOfPointInNode - maxVertCount * meshCnt); else numberOfPointsInMesh = maxVertCount; - MemoryOwner pointsPerMesh; - if (ptCnt > maxVertCount) + if (numberOfPointInNode > maxVertCount) { - pointsPerMesh = MemoryOwner.Allocate(numberOfPointsInMesh); - points.Span.Slice(i, numberOfPointsInMesh).CopyTo(pointsPerMesh.Span[..]); + using MemoryOwner pointsInMesh = MemoryOwner.Allocate(numberOfPointsInMesh); + points.Span.Slice(i, numberOfPointsInMesh).CopyTo(pointsInMesh.Span); + meshes.Add(createGpuDataHandler(pointsInMesh)); } else - { - pointsPerMesh = points; - } + meshes.Add(createGpuDataHandler(points)); - meshes.Add(createGpuDataHandler(pointsPerMesh, octantId)); meshCnt++; } return meshes; @@ -60,82 +118,159 @@ public static IEnumerable CreateMeshes(MemoryOwner /// Can be of type or . The latter is used when rendering instanced. - /// The generic point cloud points. + /// The that contains the points. /// The method that defines how to create a InstanceData from the point cloud points. - /// The octant identifier. /// - public static IEnumerable CreateInstanceData(MemoryOwner points, CreateGpuData createGpuDataHandler, OctantId octantId) + public static IEnumerable CreateInstanceData(MemoryOwner points, CreateGpuData createGpuDataHandler) { return new List { - createGpuDataHandler(points, octantId) + createGpuDataHandler(points) }; } - /// - /// Returns meshes for point clouds of type . - /// - /// The lists of "raw" points. - /// The id of the octant. - public static GpuMesh CreateStaticMesh(MemoryOwner points, OctantId octantId) + public static MemoryOwner CreateVisualizationPoints(MemoryMappedFile mmf, int numberOfPoints, HandleReadExtraBytes handleExtraBytes, CreateMeshMetaData metaData, EventHandler? onPointCloudReadError) { - int numberOfPointsInMesh; - numberOfPointsInMesh = points.Length; + var size = numberOfPoints * metaData.PointSize; + using var accessor = mmf.CreateViewAccessor(); + var rawPoints = new byte[size]; + accessor.ReadArray(0, rawPoints, 0, size); - var firstPos = points.Span[0].Position; - var vertices = new float3[numberOfPointsInMesh]; - var triangles = new uint[numberOfPointsInMesh]; - var colors = new uint[numberOfPointsInMesh]; - var flags = new uint[numberOfPointsInMesh]; - var boundingBox = new AABBf(firstPos, firstPos); + MemoryOwner visPoints = MemoryOwner.Allocate(numberOfPoints); + var pointsSpan = rawPoints.AsSpan(); - for (int i = 0; i < points.Length; i++) + for (int i = 0; i < numberOfPoints; i++) { - var pos = points.Span[i].Position; + var visPoint = new VisualizationPoint(); + var byteCountPos = sizeof(int) * 3; + var posRaw = pointsSpan.Slice(i * metaData.PointSize + metaData.OffsetToPosValues, byteCountPos); - vertices[i] = pos; - boundingBox |= vertices[i]; + var pos = MemoryMarshal.Cast(posRaw); - triangles[i] = (uint)i; - var col = points.Span[i].Color; - colors[i] = ColorToUInt((int)col.r, (int)col.g, (int)col.b, 255); - flags[i] = points.Span[i].Flags; + var x = (float)(pos[0] * metaData.Scale.x); + var y = (float)(pos[1] * metaData.Scale.y); + var z = (float)(pos[2] * metaData.Scale.z); + + visPoint.Position = new float3(x, z, y); + + float4 color; + if (metaData.OffsetColor != -1) + { + var byteCountColor = Marshal.SizeOf() * 3; + var colorRaw = pointsSpan.Slice(i * metaData.PointSize + metaData.OffsetColor, byteCountColor); + var rgb = MemoryMarshal.Cast(colorRaw); + + color = float4.Zero; + + color.r = (byte)(rgb[0] > 255 ? rgb[0] / 256 : rgb[0]); + color.g = (byte)(rgb[1] > 255 ? rgb[1] / 256 : rgb[1]); + color.b = (byte)(rgb[2] > 255 ? rgb[2] / 256 : rgb[2]); + color.a = 1; + } + else if (metaData.OffsetIntensity != -1) + { + var byteCountIntensity = Marshal.SizeOf(); + var intensityRaw = pointsSpan.Slice(i * metaData.PointSize + metaData.OffsetIntensity, byteCountIntensity); + var rgb = MemoryMarshal.Cast(intensityRaw); + color = float4.Zero; + + color.r = (float)((rgb[0] - metaData.IntensityMin) / (metaData.IntensityMax - metaData.IntensityMin) * 1f); + color.g = color.r; + color.b = color.r; + color.a = 1; + } + else + { + color = float4.UnitW; + } + + visPoint.Color = color; + + uint flag = 0; + Span extraBytesRaw = new(); + if (metaData.OffsetToExtraBytes != -1 && metaData.OffsetToExtraBytes != 0) + { + var extraByteSize = metaData.PointSize - metaData.OffsetToExtraBytes; + extraBytesRaw = pointsSpan.Slice(i * metaData.PointSize + metaData.OffsetToExtraBytes, extraByteSize); + } + //handleExtraBytes should also handle the case there aren't any extra bytes -> call in any case. + if (handleExtraBytes != null) + { + try + { + flag = handleExtraBytes(extraBytesRaw); + } + catch (Exception e) + { + onPointCloudReadError?.Invoke(null, new ErrorEventArgs(e)); + } + } + visPoint.Flags = flag; + + visPoints.Span[i] = visPoint; } - var mesh = ModuleExtensionPoint.CreateGpuMesh(PrimitiveType.Points, vertices, triangles, null, colors, null, null, null, null, null, null, null, flags); - mesh.BoundingBox = boundingBox; - return mesh; + + return visPoints; } - /// - /// Returns meshes for point clouds of type . - /// - /// The lists of "raw" points. - /// The id of the octant. - public static Mesh CreateDynamicMesh(MemoryOwner points, OctantId octantId) + private static (float3[], uint[], uint[], uint[], AABBf) GetGpuDataContents(MemoryOwner points) { - int numberOfPointsInMesh; - numberOfPointsInMesh = points.Length; - - var firstPos = points.Span[0].Position; + var numberOfPointsInMesh = points.Length; var vertices = new float3[numberOfPointsInMesh]; var triangles = new uint[numberOfPointsInMesh]; var colors = new uint[numberOfPointsInMesh]; var flags = new uint[numberOfPointsInMesh]; - var boundingBox = new AABBf(firstPos, firstPos); + var boundingBox = new AABBf(); + var pointsSpan = points.Span; - for (int i = 0; i < points.Length; i++) + for (int i = 0; i < numberOfPointsInMesh; i++) { - var pos = points.Span[i].Position; - - vertices[i] = pos; - boundingBox |= vertices[i]; - + vertices[i] = pointsSpan[i].Position; + if (i == 0) + boundingBox = new(vertices[i], vertices[i]); + else + boundingBox |= vertices[i]; triangles[i] = (uint)i; - var col = points.Span[i].Color; + + var col = pointsSpan[i].Color; colors[i] = ColorToUInt((int)col.r, (int)col.g, (int)col.b, 255); - flags[i] = points.Span[i].Flags; + + flags[i] = pointsSpan[i].Flags; } + return (vertices, triangles, colors, flags, boundingBox); + } + + /// + /// Returns meshes for points of type . + /// + /// The that contains the points. + public static GpuMesh CreateStaticMesh(MemoryOwner points) + { + var meshData = GetGpuDataContents(points); + var vertices = meshData.Item1; + var triangles = meshData.Item2; + var colors = meshData.Item3; + var flags = meshData.Item4; + var boundingBox = meshData.Item5; + var mesh = ModuleExtensionPoint.CreateGpuMesh(PrimitiveType.Points, vertices, triangles, null, colors, null, null, null, null, null, null, null, flags); + mesh.BoundingBox = boundingBox; + return mesh; + } + + /// + /// Returns meshes for points of type . + /// + /// The that contains the points. + public static Mesh CreateDynamicMesh(MemoryOwner points) + { + var meshData = GetGpuDataContents(points); + var vertices = meshData.Item1; + var triangles = meshData.Item2; + var colors = meshData.Item3; + var flags = meshData.Item4; + //var boundingBox = meshData.Item5; + return new Mesh(triangles, vertices, null, null, null, null, null, null, colors, null, null, flags) { MeshType = PrimitiveType.Points @@ -145,34 +280,16 @@ public static Mesh CreateDynamicMesh(MemoryOwner points, Oct /// /// Returns meshes for point clouds of type . /// - /// The lists of "raw" points. - /// The id of the octant. - public static InstanceData CreateInstanceData(MemoryOwner points, OctantId octantId) + /// The that contains the points. + public static InstanceData CreateInstanceData(MemoryOwner points) { - int numberOfPointsInMesh; - numberOfPointsInMesh = points.Length; - - var firstPos = points.Span[0].Position; - var vertices = new float3[numberOfPointsInMesh]; - var triangles = new ushort[numberOfPointsInMesh]; - var colors = new float4[numberOfPointsInMesh]; - var boundingBox = new AABBf(firstPos, firstPos); - var flags = new uint[numberOfPointsInMesh]; - - for (int i = 0; i < points.Length; i++) - { - var pos = points.Span[i].Position; - - vertices[i] = pos; - boundingBox |= vertices[i]; - - triangles[i] = (ushort)i; - colors[i] = new float4(points.Span[i].Color.xyz / 265, points.Span[i].Color.w); - flags[i] = points.Span[i].Flags; - } + var meshData = GetGpuDataContents(points); + var vertices = meshData.Item1; + var colors = meshData.Item3; + //var flags = meshData.Item4; // TODO: Add flags to InstanceData - return new InstanceData(points.Length, vertices, null, null, colors) + return new InstanceData(vertices.Length, vertices, null, null, colors) { }; } @@ -186,7 +303,7 @@ public static InstanceData CreateInstanceData(MemoryOwner po /// The blue value. /// The alpha value. /// - private static uint ColorToUInt(int r, int g, int b, int a) + public static uint ColorToUInt(int r, int g, int b, int a) { return (uint)((b << 16) | (g << 8) | (r << 0) | (a << 24)); } diff --git a/src/PointCloud/Core/OctantPicker.cs b/src/PointCloud/Core/OctantPicker.cs deleted file mode 100644 index 4bb128e41..000000000 --- a/src/PointCloud/Core/OctantPicker.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Fusee.Base.Common; -using Fusee.Engine.Core; -using Fusee.Engine.Core.Primitives; -using Fusee.Engine.Core.Scene; -using Fusee.Math.Core; -using System.Collections.Generic; -using System.Linq; - -namespace Fusee.PointCloud.Core -{ - /// - /// Picker for octants. - /// - public class OctantPicker - { - private readonly PointCloudOctree _octree; - private readonly RenderContext _rc; - /// - /// The currently used for rendering the - /// - public Camera Cam; - /// - /// The current position in world coordinates of the used - /// - public float3 CamPosWorld; - - /// - /// Constructor for the octant picker. - /// - /// The octree to pick from. - /// The render context. - /// The camera the calculation is based on. - /// The position of previously given camera. - public OctantPicker(PointCloudOctree octree, RenderContext rc, Camera cam, float3 camPosWorld) - { - _octree = octree; - _rc = rc; - Cam = cam; - CamPosWorld = camPosWorld; - } - - /// - /// Helper method to traverse octree and return octant. Assumes that the node is visible and intersected by given ray. - /// - /// - private List PickOctantRecursively(PointCloudOctant node, RayD ray, List list) - { - list.Add(node); - if (node.Children[0] != null) - { - foreach (var child in node.Children.Cast()) - { - if (child?.IsVisible == true && child.IntersectRay(ray)) - { - PickOctantRecursively(child, ray, list); - } - } - } - return list; - } - - - /// - /// Pick the densest octant under a given mouse position. - /// - /// The mouse position in clip space. - /// Width and height of the viewport. - private List? PickOctantWrapper(float2 pickPosClip, int2 viewportSize) - { - // Create ray to intersect aabb's with. - var ray = new RayD(new double2(pickPosClip.x, pickPosClip.y), (double4x4)_rc.View, (double4x4)Cam.GetProjectionMat(viewportSize.x, viewportSize.y, out _)); - var rootnode = (PointCloudOctant)_octree.Root; - List picked = new(); - - // Check if ray is hitting the octree at all. - if (!rootnode.IsVisible || !rootnode.IntersectRay(ray)) - { - return null; - } - - // Go over each child and check whether they are visible nad intersecting the ray. If thats the case, a smaller octant can be picked. - if (rootnode.Children[0] != null) - { - for (int i = 0; i < 8; i++) - { - var child = (PointCloudOctant)rootnode.Children[i]; - if (child?.IsVisible == true && child.IntersectRay(ray)) - { - PickOctantRecursively(child, ray, picked); - } - } - } - else - { - picked.Add(rootnode); - return picked; - } - if (picked.Count > 0) - { - return picked; - - } - return null; - } - - /// - /// Returns all under given mouse position, without filtering or sorting - /// - /// - /// - /// - public List? PickAllOctants(float2 pickPosClip, int2 viewportSize) - { - return PickOctantWrapper(pickPosClip, viewportSize); - } - - /// - /// Pick the that is densest to the camera under given mouse position. - /// Octants are ignored if the camera is inside or the distance from the camera to the octant is smaller than the octant size. - /// - /// The mouse position in clip space. - /// Width and height of the window. - public PointCloudOctant? PickDensestOctant(float2 pickPosClip, int2 viewportSize) - { - var pickResult = PickOctantWrapper(pickPosClip, viewportSize); - - // No octants picked. - if (pickResult == null) - { - return null; - } - else - { - pickResult = pickResult.OrderBy(pickResult => DistanceToCamera((float3)pickResult.Center)).ToList(); - for (int i = 0; i < pickResult.Count(); i++) - { - // Only pick octant that has a certain distance to the camera. - var dist = DistanceToCamera((float3)pickResult[i].Center); - if (pickResult[i].Intersects((double3)CamPosWorld) || dist < pickResult[i].Size) - { - pickResult.Remove(pickResult[i]); - } - } - // Sort by ratio of number of points and size of octant. - pickResult = pickResult.OrderByDescending(pickResult => pickResult.NumberOfPointsInNode / pickResult.Size).ToList(); - return pickResult[0]; - } - } - - /// - /// Pick the that is closest to the camera under given mouse position. - /// Octants are ignored if the camera is inside or the distance from the camera to the octant is smaller than the octant size. - /// - /// The mouse position in clip space. - /// Width and height of the window. - public PointCloudOctant? PickClosestOctant(float2 pickPosClip, int2 viewportSize) - { - var pickResult = PickOctantWrapper(pickPosClip, viewportSize); - - // No octants picked. - if (pickResult == null) - { - return null; - } - else - { - pickResult = pickResult.OrderBy(pickResult => DistanceToCamera((float3)pickResult.Center)).ToList(); - for (int i = 0; i < pickResult.Count(); i++) - { - // Only pick octant that has a certain distance to the camera. - var dist = DistanceToCamera((float3)pickResult[i].Center); - if (pickResult[i].Intersects((double3)CamPosWorld) || dist < pickResult[i].Size) - { - pickResult.Remove(pickResult[i]); - } - } - return pickResult[0]; - } - } - - /// - /// Helper method to calculate distance between given vector (usually center of an octant) and the cameras position. - /// - /// Point to calculate distance to. - private float DistanceToCamera(float3 point) - { - return (point - CamPosWorld).Length; - } - - /// - /// Generate a cube with the same size and position as given octant. - /// - /// The octant to extract information on size and position from. - public static SceneNode CreateCubeFromNode(PointCloudOctant node) - { - return new SceneNode - { - Components = - { - new Transform - { - Translation = (float3)node.Center, - Scale = new float3((float)node.Size) - }, - MakeEffect.FromUnlit((float4)ColorUint.Blue), - new Cube(), - } - }; - } - } -} \ No newline at end of file diff --git a/src/PointCloud/Core/PointCloudDataHandler.cs b/src/PointCloud/Core/PointCloudDataHandler.cs index 7868b9ac5..ed20ea587 100644 --- a/src/PointCloud/Core/PointCloudDataHandler.cs +++ b/src/PointCloud/Core/PointCloudDataHandler.cs @@ -1,3 +1,4 @@ +using CommunityToolkit.Diagnostics; using CommunityToolkit.HighPerformance.Buffers; using Fusee.Base.Core; using Fusee.Engine.Core; @@ -5,8 +6,10 @@ using Fusee.PointCloud.Common; using Microsoft.Extensions.Caching.Memory; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.MemoryMappedFiles; using System.Linq; using System.Threading.Tasks; @@ -22,7 +25,16 @@ namespace Fusee.PointCloud.Core /// Delegate that allows to inject the loading method of the PointReader - loads the points from file. /// /// Unique ID of an octant. - public delegate MemoryOwner LoadPointsHandler(OctantId guid); + public delegate MemoryMappedFile LoadPointsHandler(OctantId guid); + + /// + /// Delegate that allows to inject method of the PointReader that knows how to return a byte array containing all bytes of one attribute of the point cloud. + /// + /// + /// + /// + /// + public delegate byte[] GetAllBytesForAttributeHandler(string attribName, MemoryMappedFile pointsMmf, OctantId guid); /// /// Delegate for a method that tries to get the mesh(es) of an octant. If they are not cached yet, they should be created an added to the _gpuDataCache. @@ -49,10 +61,8 @@ namespace Fusee.PointCloud.Core /// Generic delegate to inject a method that nows how to actually create a GpuMesh or InstanceData for the given point type. /// /// - /// The point cloud points as generic array. - /// - /// - public delegate TGpuData CreateGpuData(MemoryOwner points, OctantId octantId); + /// The points. + public delegate TGpuData CreateGpuData(MemoryOwner points); /// /// Manages the caching and loading of point and mesh data. @@ -65,173 +75,150 @@ public class PointCloudDataHandler : PointCloudDataHandlerBase public EventHandler? OnLoadingErrorEvent; + /// + /// Information about the point cloud, needed to create meshes. + /// + public CreateMeshMetaData MetaData { get; private set; } + + private readonly HandleReadExtraBytes _handleExtraBytes; - private HashSet _meshesToUpdate = new(); + private HashSet _updateFromInvalidateCache = new(); /// - /// Caches loaded points. + /// Caches loaded raw points. /// - private readonly MemoryCache> _pointCache; + private readonly MemoryCache _rawPointCache; /// - /// Caches loaded points. + /// Caches loaded points that are ready for the visualization. + /// + private readonly MemoryCache> _visPtCache; + + /// + /// Caches loaded gpu data. /// private readonly MemoryCache> _gpuDataCache; - private readonly CreateGpuData _createGpuDataHandler; private readonly LoadPointsHandler _loadPointsHandler; - private const int _maxNumberOfDisposals = 1; - private float? _deltaTimeSinceLastDisposal; + private readonly Func _getNumberOfPointsInNode; + private readonly GetAllBytesForAttributeHandler _getAllBytesForAttribute; + private readonly bool _doRenderInstanced; + private readonly ConcurrentDictionary _loadingPointsTriggeredFor = new(); + private readonly ConcurrentDictionary _creatingMeshesTriggeredFor = new(); + + private readonly ConcurrentQueue> _disposeQueue = new(); + private const int _maxNumberOfDisposals = 10; + private float _deltaTimeSinceLastDisposal = 0.0f; + private bool _disposed; /// /// Creates a new instance. /// /// Method that knows how to create a mesh for the explicit point type (see ). + /// Method that know how to handle the extra bytes when creating the mesh for the points. + /// Information about the point cloud, needed to create meshes. /// The method that is able to load the points from the hard drive/file. + /// + /// /// - public PointCloudDataHandler(CreateGpuData createMeshHandler, LoadPointsHandler loadPointsHandler, bool doRenderInstanced = false) + public PointCloudDataHandler(CreateGpuData createMeshHandler, HandleReadExtraBytes handleExtraBytes, CreateMeshMetaData metaData, LoadPointsHandler loadPointsHandler, Func getNumberOfPointsInNode, GetAllBytesForAttributeHandler getAllBytesForAttribute, bool doRenderInstanced = false) { - _pointCache = new(); + _rawPointCache = new() + { + SlidingExpiration = 15, + ExpirationScanFrequency = 16 + }; + + _visPtCache = new() + { + SlidingExpiration = 60, + ExpirationScanFrequency = 61 + }; + _gpuDataCache = new() { SlidingExpiration = 30, ExpirationScanFrequency = 31 }; - _createGpuDataHandler = createMeshHandler; + CreateGpuDataHandler = createMeshHandler; _loadPointsHandler = loadPointsHandler; - + _getNumberOfPointsInNode = getNumberOfPointsInNode; _doRenderInstanced = doRenderInstanced; + _handleExtraBytes = handleExtraBytes; + _getAllBytesForAttribute = getAllBytesForAttribute; + MetaData = metaData; - LoadingQueue = new((8 ^ 8) / 8); - DisposeQueue = new Dictionary>((8 ^ 8) / 8); - - _gpuDataCache.HandleEvictedItem = OnItemEvictedFromCache; - - _pointCache.HandleEvictedItem += (object key, object? value, EvictionReason reason, object? state) => - { - if (value != null && value is MemoryOwner mo) - { - mo.Dispose(); - } - }; + _gpuDataCache.HandleEvictedItem += OnItemEvictedFromGpuDataCache; + _rawPointCache.HandleEvictedItem += OnItemEvictedFromRawPointCache; + _visPtCache.HandleEvictedItem += OnItemEvictedFromVisPointCache; InvalidateCacheToken.IsDirtyPropertyChanged += (isDirty) => { if (isDirty) { - //_meshesToUpdate = _gpuDataCache.GetKeys.ToHashSet(); - _meshesToUpdate = _pointCache.GetKeys.ToHashSet(); + _updateFromInvalidateCache = _visPtCache.GetKeys.ToHashSet(); } }; - } - private GpuDataState DoUpdateGpuData(OctantId octantId, ref IEnumerable gpuData) - { - if (_pointCache.TryGetValue(octantId, out var points)) - { - if (UpdateGpuDataCache != null) - { - UpdateGpuDataCache.Invoke(ref gpuData, points); - } - else - { - if (!_doRenderInstanced) - gpuData = MeshMaker.CreateMeshes(points, _createGpuDataHandler, octantId); - else - gpuData = MeshMaker.CreateInstanceData(points, _createGpuDataHandler, octantId); - } - return GpuDataState.Changed; - } - - //No points in cache - cannot update (point loading is triggered in VisibilityTester) - return GpuDataState.None; - - } + private HashSet _queuedForUpdate = new HashSet(); /// - /// First looks in the mesh cache, if there are meshes return, - /// else look in the DisposeQueue, if there are meshes return, - /// else look in the point cache, if there are points create a mesh and add to the _meshCache. + /// First looks in the mesh cache, if there isn't pending update for this mesh, return. + /// Else try to trigger the mesh creation. /// /// The unique id of an octant. /// Allows inserting a condition, if true the mesh will be updated. This is an addition to - /// State of the gpu data in it's life cycle. /// - public override IEnumerable? GetGpuData(OctantId octantId, Func? doUpdateIf, out GpuDataState gpuDataState) + public override IEnumerable? GetGpuData(OctantId octantId, Func? doUpdateIf) { - if (_gpuDataCache.TryGetValue(octantId, out var gpuData)) - { - var doUpdate = doUpdateIf is not null && doUpdateIf.Invoke(); - if (_meshesToUpdate.Contains(octantId) || doUpdate) - { - gpuDataState = DoUpdateGpuData(octantId, ref gpuData); + Guard.IsNotNull(CreateGpuDataHandler); + IEnumerable? gpuData; - if (gpuDataState != GpuDataState.None) - { - _gpuDataCache.AddOrUpdate(octantId, gpuData); - _meshesToUpdate.Remove(octantId); - if (_meshesToUpdate.Count == 0) - { - InvalidateCacheToken.IsDirty = false; - } - return gpuData; - } + var doUpdate = doUpdateIf != null && doUpdateIf.Invoke(); - //Mesh remains in the _meshesToUpdate list but couldn't be updated because the points were missing. - _gpuDataCache.Remove(octantId); - return null; - } - else - gpuDataState = GpuDataState.Unchanged; - - return gpuData; + //Queue meshes for update. + if ((doUpdate || _updateFromInvalidateCache.Contains(octantId)) && !_queuedForUpdate.Contains(octantId)) + { + _queuedForUpdate.Add(octantId); + _updateFromInvalidateCache.Remove(octantId); } - else if (DisposeQueue.TryGetValue(octantId, out gpuData)) + + //Update or return meshes. + if (_queuedForUpdate.Contains(octantId)) { - lock (LockDisposeQueue) - { - DisposeQueue.Remove(octantId); - } + _gpuDataCache.TryGetValue(octantId, out gpuData); - gpuDataState = DoUpdateGpuData(octantId, ref gpuData); - if (gpuDataState != GpuDataState.None) + if (gpuData == null) { - _gpuDataCache.AddOrUpdate(octantId, gpuData); - _meshesToUpdate.Remove(octantId); - if (_meshesToUpdate.Count == 0) - { - InvalidateCacheToken.IsDirty = false; - } - return gpuData; + //Octant contents need to be updated / rendered but we haven't a mesh to update. + TriggerMeshCreation(octantId); + return null; } - _gpuDataCache.Remove(octantId); + if (UpdateGpuData(octantId, gpuData)) + _queuedForUpdate.Remove(octantId); } - else if (_pointCache.TryGetValue(octantId, out var points)) + else if (_gpuDataCache.TryGetValue(octantId, out gpuData)) { - if (!_doRenderInstanced) - gpuData = MeshMaker.CreateMeshes(points, _createGpuDataHandler, octantId); - else - gpuData = MeshMaker.CreateInstanceData(points, _createGpuDataHandler, octantId); - - _gpuDataCache.AddOrUpdate(octantId, gpuData); - gpuDataState = GpuDataState.New; return gpuData; } - - gpuDataState = GpuDataState.None; + else + { + TriggerMeshCreation(octantId); + } //no points yet, probably in loading queue return null; } /// - /// Disposes of unused meshes, if needed. Depends on the dispose rate and the expiration frequency of the _meshCache. + /// Disposes of unused meshes, if needed. Depends on the dispose rate and the expiration frequency of the gpu data cache. + /// Make sure to call this on the main thread. /// public override void ProcessDisposeQueue() { @@ -241,69 +228,160 @@ public override void ProcessDisposeQueue() { _deltaTimeSinceLastDisposal = 0; - lock (LockDisposeQueue) + if (_disposeQueue.Count > 0) { - if (DisposeQueue.Count > 0) + var nodesInQueue = _disposeQueue.Count; + var count = nodesInQueue < _maxNumberOfDisposals ? nodesInQueue : _maxNumberOfDisposals; + + for (int i = 0; i < count; i++) { - var nodesInQueue = DisposeQueue.Count; - var count = nodesInQueue < _maxNumberOfDisposals ? nodesInQueue : _maxNumberOfDisposals; - - for (int? i = 0; i < count; i++) - { - var meshes = DisposeQueue.Last(); - var removed = DisposeQueue.Remove(meshes.Key); - foreach (var mesh in meshes.Value) - mesh.Dispose(); - } + _disposeQueue.TryDequeue(out var gpuData); + foreach (var data in gpuData) + data.Dispose(); } } + } } /// - /// Loads points from the hard drive if they are neither in the loading queue nor in the PointCahce. + /// Returns all bytes of one node for a specific attribute. + /// + /// + /// + /// + public override byte[] GetAllBytesForAttribute(string attribName, OctantId guid) + { + //NOTE: Don't add to the caches here! This defies the purpose of the caches, that is to save the "lastly-viewed" nodes. + if (!_rawPointCache.TryGetValue(guid, out var pointsMmf)) + pointsMmf = _loadPointsHandler.Invoke(guid); + + return _getAllBytesForAttribute(attribName, pointsMmf, guid); + } + + private void TriggerMeshCreation(OctantId octantId) + { + Guard.IsNotNull(CreateGpuDataHandler); + + if (!_visPtCache.TryGetValue(octantId, out var points)) return; + if (_creatingMeshesTriggeredFor.ContainsKey(octantId)) return; + + _creatingMeshesTriggeredFor.TryAdd(octantId, octantId); + + IEnumerable gpuData; + + int numberOfPointsInNode = (int)_getNumberOfPointsInNode(octantId); + if (!_doRenderInstanced) + gpuData = MeshMaker.CreateMeshes(points, CreateGpuDataHandler); + else + gpuData = MeshMaker.CreateInstanceData(points, CreateGpuDataHandler); + + foreach (var mesh in gpuData) + { + NewMeshAction?.Invoke(mesh); + } + + _gpuDataCache.AddOrUpdate(octantId, gpuData); + _creatingMeshesTriggeredFor.TryRemove(octantId, out var _); + } + + /// + /// Runs a task that loads points from the hard drive. /// /// The octant for which the points should be loaded. public override void TriggerPointLoading(OctantId guid) { - if (!LoadingQueue.Contains(guid) && LoadingQueue.Count <= MaxNumberOfNodesToLoad) - { - if (_pointCache.TryGetValue(guid, out var points)) return; + if (_loadingPointsTriggeredFor.ContainsKey(guid)) + return; - lock (LockLoadingQueue) + _loadingPointsTriggeredFor.TryAdd(guid, guid); + _ = Task.Run(() => + { + if (!_rawPointCache.TryGetValue(guid, out var pointsMmf)) { - LoadingQueue.Add(guid); + pointsMmf = _loadPointsHandler.Invoke(guid); + _rawPointCache.AddOrUpdate(guid, pointsMmf); } + CreateVisPointCacheEntry(pointsMmf, guid); + _loadingPointsTriggeredFor.TryRemove(guid, out var _); - _ = Task.Run(() => + }).ContinueWith((finishedTask) => + { + // if an exception happened during loading process call the error event for further handling of the situation + if (finishedTask.Exception != null) { - points = _loadPointsHandler.Invoke(guid); - _pointCache.AddOrUpdate(guid, points); + OnLoadingErrorEvent?.Invoke(this, new ErrorEventArgs(finishedTask.Exception)); + } + }); + } - lock (LockLoadingQueue) - { - LoadingQueue.Remove(guid); - } - }).ContinueWith((finishedTask) => - { - // if an exception happened during loading process call the error event for futher handling of the situation - if (finishedTask.Exception != null) - { - OnLoadingErrorEvent?.Invoke(this, new ErrorEventArgs(finishedTask.Exception)); - } + private void CreateVisPointCacheEntry(MemoryMappedFile pointsMmf, OctantId guid) + { + if (_visPtCache.TryGetValue(guid, out var _)) return; - }); + int numberOfPointsInNode = (int)_getNumberOfPointsInNode(guid); + var visPts = MeshMaker.CreateVisualizationPoints(pointsMmf, numberOfPointsInNode, _handleExtraBytes, MetaData, OnLoadingErrorEvent); + _visPtCache.AddOrUpdate(guid, visPts); + } + + private void OnItemEvictedFromRawPointCache(object guid, object? obj, EvictionReason reason, object? state) + { + if (obj != null && obj is MemoryMappedFile mo) + { + mo.Dispose(); + } + } + + private void OnItemEvictedFromVisPointCache(object guid, object? obj, EvictionReason reason, object? state) + { + if (obj != null && obj is MemoryOwner mo) + { + mo.Dispose(); + } + } + + private void OnItemEvictedFromGpuDataCache(object guid, object? meshes, EvictionReason reason, object? state) + { + if (meshes == null) return; + _disposeQueue.Enqueue((IEnumerable)meshes); + } + + private bool UpdateGpuData(OctantId octantId, IEnumerable gpuData) + { + var updateSucceded = UpdateFromVisPoints(octantId, ref gpuData); + if (updateSucceded) + { + foreach (var mesh in gpuData) + { + UpdatedMeshAction?.Invoke(mesh); + } + _gpuDataCache.AddOrUpdate(octantId, gpuData); } + + return updateSucceded; } - private void OnItemEvictedFromCache(object guid, object? meshes, EvictionReason reason, object? state) + private bool UpdateFromVisPoints(OctantId octantId, ref IEnumerable gpuData) { - lock (LockDisposeQueue) + Guard.IsNotNull(CreateGpuDataHandler); + + if (_visPtCache.TryGetValue(octantId, out var points)) { - if (meshes == null) return; - DisposeQueue.TryAdd((OctantId)guid, (IEnumerable)meshes); + if (UpdateGpuDataCache != null) + { + UpdateGpuDataCache.Invoke(ref gpuData, points); + return true; + } + else + { + //Mesh has to be created anew. + TriggerMeshCreation(octantId); + } } + + //No points in cache - cannot update (point loading is triggered in VisibilityTester) + return false; } /// @@ -330,22 +408,19 @@ protected override void Dispose(bool disposing) // Call the appropriate methods to clean up // unmanaged resources here. + _gpuDataCache.Dispose(); + _visPtCache.Dispose(); + _rawPointCache.Dispose(); - lock (LockDisposeQueue) + foreach (var gpuDatas in _disposeQueue) { - foreach (var item in DisposeQueue) + foreach (var gpuData in gpuDatas) { - foreach (var d in item.Value) - { - d.Dispose(); - } + gpuData.Dispose(); } } - _pointCache.Dispose(); - LoadingQueue.Clear(); - // Note disposing has been done. _disposed = true; } diff --git a/src/PointCloud/Core/PointCloudDataHandlerBase.cs b/src/PointCloud/Core/PointCloudDataHandlerBase.cs index caf2a5eda..0ce1d2b86 100644 --- a/src/PointCloud/Core/PointCloudDataHandlerBase.cs +++ b/src/PointCloud/Core/PointCloudDataHandlerBase.cs @@ -12,6 +12,11 @@ namespace Fusee.PointCloud.Core /// public abstract class PointCloudDataHandlerBase : IDisposable where TGpuData : IDisposable { + /// + /// Allows to inject a method that knows how to create a specific type of mesh. + /// + public CreateGpuData? CreateGpuDataHandler; + /// /// Token, that allows to invalidate the complete GpuData cache. /// @@ -28,25 +33,6 @@ public abstract class PointCloudDataHandlerBase : IDisposable where TG /// protected int MaxNumberOfNodesToLoad = 5; - /// - /// Contains nodes that are queued for loading in the background. - /// - protected List LoadingQueue = new(); - - /// - /// Contains meshes that are marked for disposal. - /// - protected Dictionary> DisposeQueue = new(); - - /// - /// Locking object for the loading queue. - /// - protected object LockLoadingQueue = new(); - /// - /// Locking object for the dispose queue. - /// - protected object LockDisposeQueue = new(); - /// /// First looks in the mesh cache, if there are meshes return, /// else look in the DisposeQueue, if there are meshes return, @@ -54,8 +40,7 @@ public abstract class PointCloudDataHandlerBase : IDisposable where TG /// /// The unique id of an octant. /// Allows inserting a condition, if true the mesh will be updated. - /// State of the gpu data in it's life cycle. - public abstract IEnumerable? GetGpuData(OctantId guid, Func? doUpdateIf, out GpuDataState gpuDataState); + public abstract IEnumerable? GetGpuData(OctantId guid, Func? doUpdateIf); /// /// Loads points from the hard drive if they are neither in the loading queue nor in the PointCahce. @@ -64,7 +49,8 @@ public abstract class PointCloudDataHandlerBase : IDisposable where TG public abstract void TriggerPointLoading(OctantId guid); /// - /// Disposes of unused meshes, if needed. Depends on the dispose rate and the expiration frequency of the MeshCache. + /// Disposes of unused meshes, if needed. Depends on the dispose rate and the expiration frequency of the gpu data cache. + /// Make sure to call this on the main thread. /// public abstract void ProcessDisposeQueue(); @@ -73,6 +59,24 @@ public abstract class PointCloudDataHandlerBase : IDisposable where TG /// public UpdateGpuData, MemoryOwner>? UpdateGpuDataCache; + /// + /// Used to inject a application dependent method that processes newly created gpu data. + /// + public Action? NewMeshAction; + + /// + /// Used to inject a application dependent method that processes gpu data after some update. + /// + public Action? UpdatedMeshAction; + + /// + /// Returns all bytes of one node for a specific attribute. + /// + /// + /// + /// + public abstract byte[] GetAllBytesForAttribute(string attribName, OctantId guid); + private bool _disposed = false; /// @@ -110,13 +114,7 @@ protected virtual void Dispose(bool disposing) // Call the appropriate methods to clean up // unmanaged resources here. - foreach (var kvp in DisposeQueue) - { - foreach (var val in kvp.Value) - { - val.Dispose(); - } - } + _disposed = true; } } diff --git a/src/PointCloud/Core/PointCloudOctant.cs b/src/PointCloud/Core/PointCloudOctant.cs index f8cc4dbd5..309b76dfa 100644 --- a/src/PointCloud/Core/PointCloudOctant.cs +++ b/src/PointCloud/Core/PointCloudOctant.cs @@ -1,4 +1,4 @@ -using Fusee.Math.Core; +using Fusee.Math.Core; using Fusee.PointCloud.Common; using Fusee.Structures; @@ -42,7 +42,7 @@ public double3 Center /// /// Length, width and height of this Octant. /// - public double Size + public double3 Size { get { return _size; } set @@ -52,12 +52,12 @@ public double Size Max = _center + 0.5f * _size; } } - private double _size; + private double3 _size; /// /// List of child octants. /// - public IEmptyOctant[] Children { get; private set; } + public IEmptyOctant[] Children { get; private set; } /// /// If true this octant does not have any children. @@ -96,7 +96,7 @@ public double Size /// The size (in all three dimensions) of this octant. /// /// The octants child octants. - public PointCloudOctant(double3 center, double size, OctantId octId, PointCloudOctant[]? children = null) + public PointCloudOctant(double3 center, double3 size, OctantId octId, PointCloudOctant[]? children = null) { Center = center; Size = size; @@ -144,7 +144,7 @@ public void ComputeScreenProjectedSize(double3 camPos, int screenHeight, float f /// Instantiates a child octant at the given position. /// /// The the new child has. - public IEmptyOctant CreateChild(int atPosInParent) + public IEmptyOctant CreateChild(int atPosInParent) { throw new System.NotImplementedException(); } @@ -154,13 +154,14 @@ public IEmptyOctant CreateChild(int atPosInParent) /// Returns true if one of the Frustum planes is intersecting this octant. /// /// The frustum to test against. - /// Translation of octant - /// Scale of octant + /// Additional translation. + /// Additional scale. /// false if fully outside, true if inside or intersecting. public bool InsideOrIntersectingFrustum(FrustumF frustum, float3 translation, float3 scale) { + //var maxSize = (float)System.Math.Max(System.Math.Max(Size.x, Size.y), Size.z); var translatedCenter = new float3(Center) + translation; - var scaledSize = new float3((float)Size) * scale; + var scaledSize = new float3(Size) * scale; return frustum.Near.InsideOrIntersecting(translatedCenter, scaledSize) & frustum.Far.InsideOrIntersecting(translatedCenter, scaledSize) & frustum.Left.InsideOrIntersecting(translatedCenter, scaledSize) & @@ -177,12 +178,13 @@ public bool InsideOrIntersectingFrustum(FrustumF frustum, float3 translation, fl /// false if fully outside, true if inside or intersecting. public bool InsideOrIntersectingFrustum(FrustumF frustum) { - return frustum.Near.InsideOrIntersecting(new float3(Center), (float)Size) & - frustum.Far.InsideOrIntersecting(new float3(Center), (float)Size) & - frustum.Left.InsideOrIntersecting(new float3(Center), (float)Size) & - frustum.Right.InsideOrIntersecting(new float3(Center), (float)Size) & - frustum.Top.InsideOrIntersecting(new float3(Center), (float)Size) & - frustum.Bottom.InsideOrIntersecting(new float3(Center), (float)Size); + var sizeF = (float3)Size; + return frustum.Near.InsideOrIntersecting(new float3(Center), sizeF) & + frustum.Far.InsideOrIntersecting(new float3(Center), sizeF) & + frustum.Left.InsideOrIntersecting(new float3(Center), sizeF) & + frustum.Right.InsideOrIntersecting(new float3(Center), sizeF) & + frustum.Top.InsideOrIntersecting(new float3(Center), sizeF) & + frustum.Bottom.InsideOrIntersecting(new float3(Center), sizeF); } /// diff --git a/src/PointCloud/Core/PointCloudOctree.cs b/src/PointCloud/Core/PointCloudOctree.cs index 90c416ae6..6cd01de7d 100644 --- a/src/PointCloud/Core/PointCloudOctree.cs +++ b/src/PointCloud/Core/PointCloudOctree.cs @@ -21,7 +21,7 @@ public class PointCloudOctree : IPointCloudOctree /// /// Constructor for creating an Octree that is suitable for creating files from it. /// - public PointCloudOctree(double3 center, double size, int maxLvl) + public PointCloudOctree(double3 center, double3 size, int maxLvl) { Root = new PointCloudOctant(center, size, new OctantId("r")); Depth = maxLvl; diff --git a/src/PointCloud/Core/Scene/PointCloudPickerModule.cs b/src/PointCloud/Core/Scene/PointCloudPickerModule.cs index b7fa11884..9a8e55ee7 100644 --- a/src/PointCloud/Core/Scene/PointCloudPickerModule.cs +++ b/src/PointCloud/Core/Scene/PointCloudPickerModule.cs @@ -36,7 +36,7 @@ public class PointCloudPickResult : PickResult /// /// The distance between ray (origin: mouse position) and hit result (point) /// - public float2 DistanceToRay; + public float DistanceToRay; } /// @@ -56,18 +56,18 @@ public class PointCloudPickerModule : IPickerModule internal struct MinPickValue { - internal float2 Distance; + internal float Distance; internal Mesh Mesh; internal int VertIdx; internal OctantId OctantId; } /// - /// Determines visible points of a point cloud (using the components ) and renders them. + /// Picks visible points. /// /// The point cloud component. [VisitMethod] - public void RenderPointCloud(PointCloudComponent pointCloud) + public void PickPointCloud(PointCloudComponent pointCloud) { PickResults.Clear(); if (!pointCloud.Active) return; @@ -88,81 +88,83 @@ public void RenderPointCloud(PointCloudComponent pointCloud) if (allHitBoxes == null || allHitBoxes.Count == 0) return; - var currentRes = new ConcurrentBag(); + var results = new ConcurrentBag(); Parallel.ForEach(_pcImp.GpuDataToRender, (mesh) => { foreach (var box in allHitBoxes) { - if (!mesh.BoundingBox.Intersects(new AABBf((float3)box.Min, (float3)box.Max))) continue; + var minDistBox = float.MaxValue; - var currentMin = new MinPickValue - { - Distance = float2.One * float.MaxValue - }; + if (!mesh.BoundingBox.Intersects(new AABBf((float3)box.Min, (float3)box.Max))) continue; Guard.IsNotNull(mesh.Vertices); + var levelDependentSpacing = (_pointSpacing * MathF.Pow(2, box.Level)) / 2f; for (var i = 0; i < mesh.Vertices.Length; i++) { - var dist = SphereRayIntersection((float3)rayD.Origin, (float3)rayD.Direction, mesh.Vertices[i], _pointSpacing * 0.5f); - if (dist.x < 0 || dist.y < 0) continue; + var dist = CalculateDistance(mesh.Vertices[i], (float3)rayD.Origin, (float3)rayD.Direction); + if (dist < 0) continue; - if (dist.x <= currentMin.Distance.x && dist.y <= currentMin.Distance.y) + if (dist <= levelDependentSpacing && dist < minDistBox) { - currentMin.Distance = dist; - currentMin.Mesh = mesh; - currentMin.VertIdx = i; - currentMin.OctantId = box.OctId; - //break; // <- check if break after first result is enough even for sparse point clouds + minDistBox = dist; + results.Add(new MinPickValue() + { + Distance = dist, + Mesh = mesh, + VertIdx = i, + OctantId = box.OctId + }); } } - - if (currentMin.Mesh == null) continue; - currentRes.Add(currentMin); } - }); - - if (currentRes == null || currentRes.IsEmpty) return; + }); var mvp = proj * view * _state.Model; - foreach (var r in currentRes) + foreach (var res in results) { - Guard.IsNotNull(r.Mesh.Vertices); + Guard.IsNotNull(res.Mesh.Vertices); var pickRes = new PointCloudPickResult { Node = null, Projection = proj, View = view, Model = _state.Model, - ClipPos = float4x4.TransformPerspective(mvp, r.Mesh.Vertices[r.VertIdx]), - DistanceToRay = r.Distance, - Mesh = r.Mesh, - VertIdx = r.VertIdx, - OctantId = r.OctantId + ClipPos = float4x4.TransformPerspective(mvp, res.Mesh.Vertices[res.VertIdx]), + DistanceToRay = res.Distance, + Mesh = res.Mesh, + VertIdx = res.VertIdx, + OctantId = res.OctantId }; PickResults.Add(pickRes); } } + private static float CalculateDistance(float3 point, float3 rayOrigin, float3 rayDirection) + { + if (float3.Dot(point - rayOrigin, rayDirection) < 0) //point is behind the ray's origin + return -1; + return float3.Cross(rayDirection, point - rayOrigin).Length; + } /// /// Calculates the intersection distance between a ray and a sphere. /// - /// - /// - /// Center point of sphere/point - /// Radius of sphere with center point of + /// + /// + /// Center point of sphere/point + /// Radius of sphere with center point of /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - static float2 SphereRayIntersection(float3 ro, float3 rd, float3 ce, float ra) + static float2 SphereRayIntersection(float3 rayOrigin, float3 rayDirection, float3 sphereCenter, float sphereRad) { - var oc = ro - ce; - var b = float3.Dot(oc, rd); - var c = float3.Dot(oc, oc) - ra * ra; + var oc = rayOrigin - sphereCenter; + var b = float3.Dot(oc, rayDirection); + var c = float3.Dot(oc, oc) - sphereRad * sphereRad; var h = b * b - c; if (h < 0.0f) return new float2(-1.0f); // no intersection h = MathF.Sqrt(h); @@ -187,6 +189,32 @@ private List PickOctantRecursively(PointCloudOctant node, RayD return list; } + private List PickOctantIterative(PointCloudOctant node, RayD ray, List list) + { + var stack = new Stack(); + stack.Push(node); + + while (stack.Count > 0) + { + var current = stack.Pop(); + + if (current?.IsVisible == true && current.IntersectRay(ray)) + { + list.Add(current); + } + + if (current == null) + continue; + + foreach (var child in current.Children) + { + if (child != null) + stack.Push((PointCloudOctant)child); + } + } + return list; + } + /// /// Inject this to a to be able to pick s. /// The actual point and data needs to be present a priori, however it's type is polymorph, therefore we need to inject those data, too. diff --git a/src/PointCloud/Core/VisibilityTester.cs b/src/PointCloud/Core/VisibilityTester.cs index 51c66120d..2b6189c15 100644 --- a/src/PointCloud/Core/VisibilityTester.cs +++ b/src/PointCloud/Core/VisibilityTester.cs @@ -90,7 +90,7 @@ public float MinProjSizeModifier /// public float UpdateRate { get; set; } = 1 / 30f; - // Minimal screen projected size of a node. Depends on spacing of the octree. + // Minimal screen projected size of a node. private double _minScreenProjectedSize; // Allows traversal in order of screen projected size. diff --git a/src/PointCloud/Potree/Potree2Cloud.cs b/src/PointCloud/Potree/Potree2Cloud.cs index ea291f174..b7d9f2870 100644 --- a/src/PointCloud/Potree/Potree2Cloud.cs +++ b/src/PointCloud/Potree/Potree2Cloud.cs @@ -1,10 +1,11 @@ -using CommunityToolkit.HighPerformance.Buffers; +using CommunityToolkit.HighPerformance.Buffers; using Fusee.Base.Core; using Fusee.Engine.Core; using Fusee.Math.Core; using Fusee.PointCloud.Common; using Fusee.PointCloud.Core; using System.Collections.Generic; +using System.IO.MemoryMappedFiles; namespace Fusee.PointCloud.Potree { @@ -14,7 +15,7 @@ namespace Fusee.PointCloud.Potree public class Potree2Cloud : IPointCloudImp { /// - /// Is being called when the GpuDataCache is being invalidated + /// Object for handling the invalidation of the gpu data cache. /// public InvalidateGpuDataCache InvalidateGpuDataCache { get; } = new(); @@ -82,32 +83,34 @@ public float UpdateRate /// /// The center of the point clouds AABB / Octree root. /// - public float3 Center => (float3)VisibilityTester.Octree.Root.Center; + public float3 Center { get; private set; } /// /// The size (longest edge) of the point clouds AABB / Octree root. /// - public float3 Size => new((float)VisibilityTester.Octree.Root.Size); + public float3 Size { get; private set; } private bool _doUpdate = true; /// /// Creates a new instance of type /// - public Potree2Cloud(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree) + public Potree2Cloud(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree, float3 size, float3 center) { - GpuDataToRender = new List(); + GpuDataToRender = new(); DataHandler = dataHandler; DataHandler.UpdateGpuDataCache = UpdateGpuDataCache; VisibilityTester = new VisibilityTester(octree, dataHandler.TriggerPointLoading); + Size = size; + Center = center; } /// /// Allows to update meshes with data from the points. /// /// The meshes that have to be updated. - /// The points with the desired values. - public void UpdateGpuDataCache(ref IEnumerable meshes, MemoryOwner points) + /// The for the points. + public void UpdateGpuDataCache(ref IEnumerable meshes, MemoryOwner visPoints) { Diagnostics.Warn("Not implemented. Cache will not be updated."); } @@ -145,7 +148,7 @@ public void Update(float fov, int viewportHeight, FrustumF renderFrustum, float3 { if (!guid.Valid) continue; - var meshes = DataHandler.GetGpuData(guid, null, out _); + var meshes = DataHandler.GetGpuData(guid, null); if (meshes == null) continue; //points for this octant aren't loaded yet. diff --git a/src/PointCloud/Potree/Potree2CloudDynamic.cs b/src/PointCloud/Potree/Potree2CloudDynamic.cs index 74f28d238..05ea9481c 100644 --- a/src/PointCloud/Potree/Potree2CloudDynamic.cs +++ b/src/PointCloud/Potree/Potree2CloudDynamic.cs @@ -1,5 +1,4 @@ -using CommunityToolkit.Diagnostics; -using CommunityToolkit.HighPerformance.Buffers; +using CommunityToolkit.HighPerformance.Buffers; using Fusee.Engine.Core.Scene; using Fusee.Math.Core; using Fusee.PointCloud.Common; @@ -83,34 +82,54 @@ public float UpdateRate /// /// The center of the point clouds AABB / Octree root. /// - public float3 Center => (float3)VisibilityTester.Octree.Root.Center; + public float3 Center { get; private set; } /// /// The size (longest edge) of the point clouds AABB / Octree root. /// - public float3 Size => new((float)VisibilityTester.Octree.Root.Size); + public float3 Size { get; private set; } /// /// Action that is run on every mesh that is determined as newly visible. /// - public Action? NewMeshAction; + public Action? NewMeshAction + { + get => _newMeshAction; + set + { + _newMeshAction = value; + (DataHandler).NewMeshAction = _newMeshAction; + } + } + private Action? _newMeshAction; /// /// Action that is run on every mesh that was updated. /// - public Action? UpdatedMeshAction; + public Action? UpdatedMeshAction + { + get => _updateMeshAction; + set + { + _updateMeshAction = value; + DataHandler.UpdatedMeshAction = _updateMeshAction; + } + } + private Action? _updateMeshAction; private bool _doUpdate = true; /// /// Creates a new instance of type /// - public Potree2CloudDynamic(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree) + public Potree2CloudDynamic(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree, float3 size, float3 center) { - GpuDataToRender = new List(); + GpuDataToRender = new(); DataHandler = dataHandler; DataHandler.UpdateGpuDataCache = UpdateGpuDataCache; VisibilityTester = new VisibilityTester(octree, dataHandler.TriggerPointLoading); + Size = size; + Center = center; } /// @@ -183,39 +202,11 @@ public void Update(float fov, int viewportHeight, FrustumF renderFrustum, float3 { if (!guid.Valid) continue; - var guidMeshes = DataHandler.GetGpuData(guid, () => !_visibleOctantsCache.Contains(guid), out GpuDataState meshStatus); + var guidMeshes = DataHandler.GetGpuData(guid, () => !_visibleOctantsCache.Contains(guid)); - switch (meshStatus) - { - //Octants that are now visible but the points for this octant aren't loaded yet. - //Nothing to do here. - case GpuDataState.None: - continue; - //Octants that are now visible and the meshes are newly created. - //They we have to call "NewMeshAction" when they are loaded. - case GpuDataState.New: - Guard.IsNotNull(guidMeshes); //If this is null we have an internal error in DataHandler.GetMeshes/DoUpdate - foreach (var mesh in guidMeshes) - { - NewMeshAction?.Invoke(mesh); - } - break; - //Octants that are now visible and the existing meshes where updated. - case GpuDataState.Changed: - Guard.IsNotNull(guidMeshes); //If this is null we have an internal error in DataHandler.GetMeshes/DoUpdate - foreach (var mesh in guidMeshes) - { - UpdatedMeshAction?.Invoke(mesh); - } - break; - case GpuDataState.Unchanged: - Guard.IsNotNull(guidMeshes); //If this is null we have an internal error in DataHandler.GetMeshes/DoUpdate - break; - default: - throw new ArgumentException($"Invalid mesh status {meshStatus}."); - } + if (guidMeshes != null) + GpuDataToRender.AddRange(guidMeshes); - GpuDataToRender.AddRange(guidMeshes); currentOctants.Add(guid); } diff --git a/src/PointCloud/Potree/Potree2CloudInstanced.cs b/src/PointCloud/Potree/Potree2CloudInstanced.cs index e2fe246e1..2d33e6911 100644 --- a/src/PointCloud/Potree/Potree2CloudInstanced.cs +++ b/src/PointCloud/Potree/Potree2CloudInstanced.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.HighPerformance.Buffers; +using CommunityToolkit.HighPerformance.Buffers; using Fusee.Base.Core; using Fusee.Engine.Core.Scene; using Fusee.Math.Core; @@ -14,7 +14,7 @@ namespace Fusee.PointCloud.Potree public class Potree2CloudInstanced : IPointCloudImp { /// - /// Called when the GpuDataCache is being invalidated + /// Provides a dirty flag for the gpu data cache and a property changed handle for it. /// public InvalidateGpuDataCache InvalidateGpuDataCache { get; } = new(); @@ -82,24 +82,26 @@ public float UpdateRate /// /// The center of the point clouds AABB / Octree root. /// - public float3 Center => (float3)VisibilityTester.Octree.Root.Center; + public float3 Center { get; private set; } /// /// The size (longest edge) of the point clouds AABB / Octree root. /// - public float3 Size => new((float)VisibilityTester.Octree.Root.Size); + public float3 Size { get; private set; } private bool _doUpdate = true; /// /// Creates a new instance of type /// - public Potree2CloudInstanced(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree) + public Potree2CloudInstanced(PointCloudDataHandlerBase dataHandler, IPointCloudOctree octree, float3 size, float3 center) { - GpuDataToRender = new List(); + GpuDataToRender = new(); DataHandler = dataHandler; DataHandler.UpdateGpuDataCache = UpdateGpuDataCache; VisibilityTester = new VisibilityTester(octree, dataHandler.TriggerPointLoading); + Size = size; + Center = center; } /// @@ -145,7 +147,7 @@ public void Update(float fov, int viewportHeight, FrustumF renderFrustum, float3 { if (!guid.Valid) continue; - var instanceData = DataHandler.GetGpuData(guid, null, out _); + var instanceData = DataHandler.GetGpuData(guid, null); if (instanceData == null) continue; //points for this octant aren't loaded yet. diff --git a/src/PointCloud/Potree/Potree2LAS.cs b/src/PointCloud/Potree/Potree2LAS.cs index 221fa5b90..134db722e 100644 --- a/src/PointCloud/Potree/Potree2LAS.cs +++ b/src/PointCloud/Potree/Potree2LAS.cs @@ -268,7 +268,7 @@ private void ParseAndFillHeader() Guard.IsLessThan(size, ushort.MaxValue); // automatic point guessing - var ptSize = _potreeData.Metadata.OffsetToExtraBytes == - 1 ? size : (_potreeData.Metadata.OffsetToExtraBytes - 1); + var ptSize = _potreeData.Metadata.OffsetToExtraBytes == -1 ? size : (_potreeData.Metadata.OffsetToExtraBytes - 1); LASPointType ptType = ptSize switch { 20 => LASPointType.Zero, @@ -331,8 +331,8 @@ private void ParseAndFillHeader() if (offset >= _potreeData.Metadata.OffsetToExtraBytes) { - var desc = Encoding.ASCII.GetBytes(attribute.Description.Append('\0').ToArray()); - var name = Encoding.ASCII.GetBytes(attribute.Name.Append('\0').ToArray()); + var desc = Encoding.ASCII.GetBytes(attribute.Description.ToArray()); + var name = Encoding.ASCII.GetBytes(attribute.Name.ToArray()); var extraByteType = attribute.Type switch { @@ -356,8 +356,8 @@ private void ParseAndFillHeader() data_type = (byte)extraByteType }; - Guard.IsLessThan(desc.Length, currentExtra.description.Length); - Guard.IsLessThan(name.Length, currentExtra.name.Length); + Guard.IsLessThanOrEqualTo(desc.Length, currentExtra.description.Length); + Guard.IsLessThanOrEqualTo(name.Length, currentExtra.name.Length); Array.Copy(desc, currentExtra.description, desc.Length); Array.Copy(name, currentExtra.name, name.Length); @@ -419,8 +419,8 @@ private void ParseAndFillHeader() var description = Encoding.UTF8.GetBytes("Extra Bytes Record\0"); var userId = Encoding.UTF8.GetBytes("LASF_Spec\0"); - Guard.IsLessThan(description.Length, vlr.Description.Length); - Guard.IsLessThan(userId.Length, vlr.UserId.Length); + Guard.IsLessThanOrEqualTo(description.Length, vlr.Description.Length); + Guard.IsLessThanOrEqualTo(userId.Length, vlr.UserId.Length); Array.Copy(description, vlr.Description, description.Length); Array.Copy(userId, vlr.UserId, userId.Length); diff --git a/src/PointCloud/Potree/V2/Data/PotreeData.cs b/src/PointCloud/Potree/V2/Data/PotreeData.cs index a72e46cee..aa1be7211 100644 --- a/src/PointCloud/Potree/V2/Data/PotreeData.cs +++ b/src/PointCloud/Potree/V2/Data/PotreeData.cs @@ -1,7 +1,6 @@ using CommunityToolkit.Diagnostics; using Fusee.PointCloud.Common; using System; -using System.Diagnostics; using System.IO; using System.IO.MemoryMappedFiles; diff --git a/src/PointCloud/Potree/V2/Potree2AccessBase.cs b/src/PointCloud/Potree/V2/Potree2AccessBase.cs index 894d410f4..2cd0e5c45 100644 --- a/src/PointCloud/Potree/V2/Potree2AccessBase.cs +++ b/src/PointCloud/Potree/V2/Potree2AccessBase.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Diagnostics; using Fusee.PointCloud.Potree.V2.Data; +using System.IO.MemoryMappedFiles; namespace Fusee.PointCloud.Potree.V2 { @@ -32,16 +33,20 @@ protected Potree2AccessBase() { } /// /// /// - internal byte[] ReadRawNodeData(PotreeNode node) + internal MemoryMappedFile ReadRawNodeData(PotreeNode node) { Guard.IsLessThanOrEqualTo(node.NumPoints, int.MaxValue); Guard.IsNotNull(PotreeData); - var potreePointSize = (int)node.NumPoints * PotreeData.Metadata.PointSize; - var pointArray = new byte[potreePointSize]; - PotreeData.ReadViewAccessor.ReadArray(node.ByteOffset, pointArray, 0, potreePointSize); + var nodeSize = (int)node.NumPoints * PotreeData.Metadata.PointSize; + var pointArray = new byte[nodeSize]; + PotreeData.ReadViewAccessor.ReadArray(node.ByteOffset, pointArray, 0, nodeSize); - return pointArray; + var mmf = MemoryMappedFile.CreateNew(null, nodeSize); + using var accessor = mmf.CreateViewAccessor(); + accessor.WriteArray(0, pointArray, 0, pointArray.Length); + + return mmf; } #region Metadata caching diff --git a/src/PointCloud/Potree/V2/Potree2Reader.cs b/src/PointCloud/Potree/V2/Potree2Reader.cs index 034584d3f..beaeb44be 100644 --- a/src/PointCloud/Potree/V2/Potree2Reader.cs +++ b/src/PointCloud/Potree/V2/Potree2Reader.cs @@ -1,6 +1,5 @@ using CommunityToolkit.Diagnostics; using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; using Fusee.Base.Core; using Fusee.Engine.Core; using Fusee.Engine.Core.Scene; @@ -12,20 +11,13 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Diagnostics.Tracing; using System.IO; +using System.IO.MemoryMappedFiles; using System.Linq; using System.Runtime.InteropServices; namespace Fusee.PointCloud.Potree.V2 { - /// - /// Delegate for a method that knows how to parse a slice of a point's extra bytes to a valid uint. - /// - /// - /// - public delegate uint HandleReadExtraBytes(Span bytes); - /// /// Reads Potree V2 files and is able to create a point cloud scene component, that can be rendered. /// @@ -41,11 +33,6 @@ public class Potree2Reader : Potree2AccessBase, IPointReader /// public EventHandler? OnPointCloudReadError; - /// - /// Specify the byte offset for one point until the extra byte data is reached - /// - public int OffsetToExtraBytes = -1; - /// /// Generate a new instance of . /// @@ -70,36 +57,55 @@ public Potree2Reader(PotreeData potreeData) : base(potreeData) /// Determines which is used to display the returned point cloud."/> public IPointCloud GetPointCloudComponent(RenderMode renderMode = RenderMode.StaticMesh) { + var metaData = new CreateMeshMetaData() + { + PointSize = PotreeData.Metadata.PointSize, + Scale = PotreeData.Metadata.Scale, + OffsetColor = offsetColor, + OffsetIntensity = offsetIntensity, + OffsetToPosValues = offsetPosition, + OffsetToExtraBytes = PotreeData.Metadata.OffsetToExtraBytes, + IntensityMax = PotreeData.Metadata.Attributes["intensity"].MaxList[0], + IntensityMin = PotreeData.Metadata.Attributes["intensity"].MinList[0], + + }; + + var shiftedAabbCenter = (float3)(PotreeData.Metadata.AABB.Center - PotreeData.Metadata.Offset); + switch (renderMode) { default: case RenderMode.StaticMesh: { - var dataHandler = new PointCloudDataHandler(MeshMaker.CreateStaticMesh, - LoadVisualizationPointData); + var dataHandler = new PointCloudDataHandler(MeshMaker.CreateStaticMesh, HandleReadExtraBytes, metaData, LoadVisualizationPointData, GetNumberOfPointsInNode, GetAllBytesForAttribute); dataHandler.OnLoadingErrorEvent += OnPointCloudReadError; - var imp = new Potree2Cloud(dataHandler, GetOctree()); + var imp = new Potree2Cloud(dataHandler, GetOctree(), (float3)PotreeData.Metadata.AABB.Size, shiftedAabbCenter); return new PointCloudComponent(imp, renderMode); } case RenderMode.Instanced: { - var dataHandlerInstanced = new PointCloudDataHandler(MeshMaker.CreateInstanceData, - LoadVisualizationPointData, true); + var dataHandlerInstanced = new PointCloudDataHandler(MeshMaker.CreateInstanceData, HandleReadExtraBytes, metaData, LoadVisualizationPointData, GetNumberOfPointsInNode, GetAllBytesForAttribute, true); dataHandlerInstanced.OnLoadingErrorEvent += OnPointCloudReadError; - var imp = new Potree2CloudInstanced(dataHandlerInstanced, GetOctree()); + var imp = new Potree2CloudInstanced(dataHandlerInstanced, GetOctree(), (float3)PotreeData.Metadata.AABB.Size, shiftedAabbCenter); return new PointCloudComponent(imp, renderMode); } case RenderMode.DynamicMesh: { - var dataHandlerDynamic = new PointCloudDataHandler(MeshMaker.CreateDynamicMesh, - LoadVisualizationPointData); + var dataHandlerDynamic = new PointCloudDataHandler(MeshMaker.CreateDynamicMesh, HandleReadExtraBytes, metaData, LoadVisualizationPointData, GetNumberOfPointsInNode, GetAllBytesForAttribute); dataHandlerDynamic.OnLoadingErrorEvent += OnPointCloudReadError; - var imp = new Potree2CloudDynamic(dataHandlerDynamic, GetOctree()); + var imp = new Potree2CloudDynamic(dataHandlerDynamic, GetOctree(), (float3)PotreeData.Metadata.AABB.Size, shiftedAabbCenter); return new PointCloudComponent(imp, renderMode); } } } + private long GetNumberOfPointsInNode(OctantId octantId) + { + Guard.IsNotNull(PotreeData); + var node = PotreeData.GetNode(octantId); + return node == null ? throw new ArgumentException($"Couldn't get node for id {octantId}.") : node.NumPoints; + } + /// /// Reads the Potree file and returns an octree. /// @@ -114,7 +120,7 @@ public IPointCloudOctree GetOctree() Guard.IsNotNull(PotreeData.Metadata.Hierarchy); var center = PotreeData.Hierarchy.Root.Aabb.Center; - var size = PotreeData.Hierarchy.Root.Aabb.Size.y; + var size = PotreeData.Hierarchy.Root.Aabb.Size;//System.Math.Max(System.Math.Max(PotreeData.Hierarchy.Root.Aabb.Size.x, PotreeData.Hierarchy.Root.Aabb.Size.y), PotreeData.Hierarchy.Root.Aabb.Size.z); var maxLvl = PotreeData.Metadata.Hierarchy.Depth; var octree = new PointCloudOctree(center, size, maxLvl); @@ -124,12 +130,42 @@ public IPointCloudOctree GetOctree() return octree; } + /// + /// Returns all bytes of one node for a specific attribute. + /// + /// + /// + /// + /// + public byte[] GetAllBytesForAttribute(string attribName, MemoryMappedFile pointsMmf, OctantId guid) + { + Guard.IsNotNull(PotreeData); + var node = PotreeData.GetNode(guid); + Guard.IsNotNull(node); + var potreePointSize = (int)node.NumPoints * PotreeData.Metadata.PointSize; + + using var accessor = pointsMmf.CreateViewAccessor(); + var pointArray = new byte[potreePointSize]; + accessor.ReadArray(0, pointArray, 0, potreePointSize); + + var memStream = new MemoryStream(); + var attrib = PotreeData.Metadata.Attributes[attribName]; + + for (var i = 0; i < pointArray.Length; i += PotreeData.Metadata.PointSize) + { + var extraBytesSpan = pointArray.AsSpan().Slice(i + attrib.AttributeOffset, attrib.Size); + memStream.Write(extraBytesSpan); + } + + return memStream.ToArray(); + } + /// /// Reads the points for a specific octant of type . /// /// Id of the octant. /// - public MemoryOwner LoadVisualizationPointData(OctantId id) + public MemoryMappedFile LoadVisualizationPointData(OctantId id) { Guard.IsNotNull(PotreeData); var node = PotreeData.GetNode(id); @@ -140,97 +176,12 @@ public MemoryOwner LoadVisualizationPointData(OctantId id) return LoadVisualizationPoint(node); } - private MemoryOwner LoadVisualizationPoint(PotreeNode node) + private MemoryMappedFile LoadVisualizationPoint(PotreeNode node) { Guard.IsLessThanOrEqualTo(node.NumPoints, int.MaxValue); - //if (HandleExtraBytes != null) - // Guard.IsGreaterThan(OffsetToExtraBytes, 0); Guard.IsNotNull(PotreeData); - var pointArray = ReadRawNodeData(node); - - var returnMemory = MemoryOwner.Allocate((int)node.NumPoints); - - var pointCount = 0; - - for (var i = 0; i < pointArray.Length; i += PotreeData.Metadata.PointSize) - { - var posSlice = new Span(pointArray).Slice(i + offsetPosition, Marshal.SizeOf() * 3); - var pos = MemoryMarshal.Cast(posSlice); - - double x = pos[0] * PotreeData.Metadata.Scale.x; - double y = pos[1] * PotreeData.Metadata.Scale.y; - double z = pos[2] * PotreeData.Metadata.Scale.z; - - float3 position = new((float)x, (float)y, (float)z); - position = (float4x4)Potree2Consts.YZflip * position; - - var posSpan = MemoryMarshal.Cast(position.ToArray()); - - Span colorSlice; - Span rgb; - float4 color; - if (offsetColor != -1) - { - colorSlice = new Span(pointArray).Slice(i + offsetColor, Marshal.SizeOf() * 3); - rgb = MemoryMarshal.Cast(colorSlice); - - color = float4.Zero; - - color.r = ((byte)(rgb[0] > 255 ? rgb[0] / 256 : rgb[0])); - color.g = ((byte)(rgb[1] > 255 ? rgb[1] / 256 : rgb[1])); - color.b = ((byte)(rgb[2] > 255 ? rgb[2] / 256 : rgb[2])); - color.a = 1; - } - else if (offsetIntensity != -1) - { - var attrib = PotreeData.Metadata.Attributes["intensity"]; - colorSlice = new Span(pointArray).Slice(i + offsetIntensity, Marshal.SizeOf()); - rgb = MemoryMarshal.Cast(colorSlice); - color = float4.Zero; - - color.r = (float)((rgb[0] - attrib.MinList[0]) / (attrib.MaxList[0] - attrib.MinList[0]) * 1f); - color.g = color.r; - color.b = color.r; - color.a = 1; - } - else - { - color = float4.UnitW; - } - - var colorSpan = MemoryMarshal.Cast(color.ToArray()); - - uint flags = 0; - Span extraBytesSpan = null; - if (PotreeData.Metadata.OffsetToExtraBytes != -1 && PotreeData.Metadata.OffsetToExtraBytes != 0) - { - var extraByteSize = PotreeData.Metadata.PointSize - PotreeData.Metadata.OffsetToExtraBytes; - extraBytesSpan = pointArray.AsSpan().Slice(i + PotreeData.Metadata.OffsetToExtraBytes, extraByteSize); - } - if (HandleReadExtraBytes != null) - { - try - { - flags = HandleReadExtraBytes(extraBytesSpan); - } - catch (Exception e) - { - OnPointCloudReadError?.Invoke(this, new ErrorEventArgs(e)); - } - } - - var flagsSpan = MemoryMarshal.Cast(new uint[] { flags }); - - var currentMemoryPt = MemoryMarshal.Cast(returnMemory.Span.Slice(pointCount, 1)); - posSpan.CopyTo(currentMemoryPt[..]); - colorSpan.CopyTo(currentMemoryPt[posSpan.Length..]); - flagsSpan.CopyTo(currentMemoryPt.Slice(posSpan.Length + colorSpan.Length, Marshal.SizeOf())); - - pointCount++; - } - - return returnMemory; + return ReadRawNodeData(node); } private static void MapChildNodesRecursive(IPointCloudOctant octreeNode, PotreeNode potreeNode) @@ -242,8 +193,8 @@ private static void MapChildNodesRecursive(IPointCloudOctant octreeNode, PotreeN if (potreeNode.Children[i] != null) { var potreeChild = potreeNode.Children[i]; - Guard.IsNotNull(potreeNode.Children[i].Aabb); - var octant = new PointCloudOctant(potreeNode.Children[i].Aabb.Center, potreeNode.Children[i].Aabb.Size.y, new OctantId(potreeChild.Name)); + + var octant = new PointCloudOctant(potreeNode.Children[i].Aabb.Center, potreeNode.Children[i].Aabb.Size, new OctantId(potreeChild.Name)); if (potreeChild.NodeType == NodeType.LEAF) { @@ -279,39 +230,48 @@ public PotreeData ReadNewFile(string path) CacheMetadata(true); - PotreeData.Metadata.PrincipalAxisRotation = CalculatePrincipalAxis(PotreeData); - return PotreeData; } - - /// /// Calculate the principal axis rotation from given data /// - Load root node /// - Convert to covariance matrix /// - Calculate principal axis /// - /// The potree data /// - private float4x4 CalculatePrincipalAxis(PotreeData data) + public float4x4 CalculatePrincipalAxis() { - using var allPoints = LoadVisualizationPoint(data.Hierarchy.Root); - using var positions = MemoryOwner.Allocate(allPoints.Length); + Guard.IsNotNull(PotreeData); + var potreeRootSize = (int)PotreeData.Hierarchy.Root.NumPoints * PotreeData.Metadata.PointSize; + using var mmf = LoadVisualizationPoint(PotreeData.Hierarchy.Root); + using var accessor = mmf.CreateViewAccessor(); - var allPtsBytes = allPoints.Span.AsBytes(); + var potreeRootRaw = new byte[potreeRootSize]; + accessor.ReadArray(0, potreeRootRaw, 0, potreeRootSize); - // convert all points to byte, slice the position value (stride/offset) and copy to position array - for (var i = 0; i < allPoints.Length; i++) + var rootPoints = new float3[(int)PotreeData.Hierarchy.Root.NumPoints]; + var pointCounter = 0; + var sizeOffloat3 = Marshal.SizeOf() * 3; + + for (var i = 0; i < potreeRootRaw.Length; i += PotreeData.Metadata.PointSize) { - positions.Span[i] = allPoints.Span[i].Position; + var posSlice = new Span(potreeRootRaw).Slice(i + offsetPosition, sizeOffloat3); + var pos = MemoryMarshal.Cast(posSlice); + + float x = (float)(pos[0] * PotreeData.Metadata.Scale.x); + float y = (float)(pos[1] * PotreeData.Metadata.Scale.y); + float z = (float)(pos[2] * PotreeData.Metadata.Scale.z); + + float3 position = (float4x4)Potree2Consts.YZflip * new float3(x, y, z); + rootPoints[pointCounter] = position; + pointCounter++; } - // dangerous, undefined behavior -> do not use the array values after this method - // this is irrelevant, as we use only a local variable try { - var eigen = new Eigen(positions.DangerousGetArray().Array); + var eigen = new Eigen(rootPoints); + PotreeData.Metadata.PrincipalAxisRotation = (float4x4)eigen.RotationMatrix; return (float4x4)eigen.RotationMatrix; } catch (ArithmeticException ex) @@ -330,7 +290,6 @@ private float4x4 CalculatePrincipalAxis(PotreeData data) public void ReadFile(PotreeData potreeData) { PotreeData = potreeData; - CacheMetadata(true); } @@ -367,18 +326,41 @@ private static (PotreeMetadata, PotreeHierarchy) LoadHierarchy(string folderPath LoadHierarchyRecursive(ref Hierarchy.Root, ref data, 0, Metadata.Hierarchy.FirstChunkSize); + //TODO: this is needed because the .NET Potree Converter is producing Nodes with 0 points right now. Remove when this is fixed. + CleanupHierarchy(Hierarchy.Root); + Hierarchy.Nodes = new(); Hierarchy.Root.Traverse(n => Hierarchy.Nodes.Add(n)); Potree2Reader.FlipYZAxis(Metadata, Hierarchy); - // adapt the global AABB after conversion, this works with the current LAS writer - Metadata.BoundingBox.MinList = new List(3) { Hierarchy.Root.Aabb.min.x + Metadata.Offset.x, Hierarchy.Root.Aabb.min.z + Metadata.Offset.z, Hierarchy.Root.Aabb.min.y + Metadata.Offset.y }; - Metadata.BoundingBox.MaxList = new List(3) { Hierarchy.Root.Aabb.max.x + Metadata.Offset.x, Hierarchy.Root.Aabb.max.z + Metadata.Offset.z, Hierarchy.Root.Aabb.max.y + Metadata.Offset.y }; - return (Metadata, Hierarchy); } + private static void CleanupHierarchy(PotreeNode root) + { + Stack stack = new(); + stack.Push(root); + + while (stack.Count > 0) + { + PotreeNode node = stack.Pop(); + + for (int i = 0; i < node.Children.Length; i++) + { + var child = node.Children[i]; + + if (child != null) + { + if (child.NumPoints == 0) + node.Children[i] = null; + else + stack.Push(child); + } + } + } + } + private static PotreeMetadata LoadPotreeMetadata(string metadataFilepath) { var settings = new JsonSerializerSettings(); @@ -409,9 +391,6 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer } } - - - private static void LoadHierarchyRecursive(ref PotreeNode root, ref byte[] data, long offset, long size) { int bytesPerNode = 22; @@ -518,6 +497,8 @@ private static void FlipYZAxis(PotreeMetadata potreeMetadata, PotreeHierarchy po } potreeMetadata.OffsetList = new List(3) { potreeMetadata.Offset.x, potreeMetadata.Offset.z, potreeMetadata.Offset.y }; potreeMetadata.ScaleList = new List(3) { potreeMetadata.Scale.x, potreeMetadata.Scale.z, potreeMetadata.Scale.y }; + potreeMetadata.BoundingBox.MaxList = new List(3) { potreeMetadata.BoundingBox.Max.x, potreeMetadata.BoundingBox.Max.z, potreeMetadata.BoundingBox.Max.y }; + potreeMetadata.BoundingBox.MinList = new List(3) { potreeMetadata.BoundingBox.Min.x, potreeMetadata.BoundingBox.Min.z, potreeMetadata.BoundingBox.Min.y }; } private static void CalculateAttributeOffsets(ref PotreeMetadata potreeMetadata) diff --git a/src/PointCloud/Potree/V2/Potree2Writer.cs b/src/PointCloud/Potree/V2/Potree2Writer.cs index 6328d9e40..e950ea04b 100644 --- a/src/PointCloud/Potree/V2/Potree2Writer.cs +++ b/src/PointCloud/Potree/V2/Potree2Writer.cs @@ -4,15 +4,17 @@ using Fusee.PointCloud.Core; using Fusee.PointCloud.Potree.V2.Data; using System; +using System.Collections.Generic; namespace Fusee.PointCloud.Potree.V2 { /// - /// Delegate for a method that knows how to parse a enxtra byte uint back to its byte representation. + /// Delegate for a method that knows how to parse a info from a flag uint back to its byte representation. /// /// + /// The attribute. /// - public delegate Span HandleWriteExtraBytes(uint flag); + public delegate Span HandleWriteExtraBytes(uint flag, PotreeSettingsAttribute attrib); /// /// Writes Potree data @@ -34,8 +36,8 @@ public Potree2Writer(PotreeData potreeData) : base(potreeData) { } /// /// /// - /// - public void WriteVisualizationPoint(OctantId octantId, MemoryOwner visualizationPoints, PotreeSettingsAttribute potreeSettingsAttribute) + /// + public void WriteVisualizationPointForNode(OctantId octantId, MemoryOwner visualizationPoints, List attribs) { Guard.IsNotNull(PotreeData); var node = PotreeData.GetNode(octantId); @@ -43,39 +45,26 @@ public void WriteVisualizationPoint(OctantId octantId, MemoryOwner visualizationPoints, PotreeSettingsAttribute potreeSettingsAttribute) + private void WriteVisualizationPoints(PotreeNode potreeNode, MemoryOwner visualizationPoints, List attribs) { Guard.IsLessThanOrEqualTo(potreeNode.NumPoints, int.MaxValue); Guard.IsNotNull(PotreeData); Guard.IsNotNull(HandleWriteExtraBytes); - var pointArray = ReadRawNodeData(potreeNode); - - var visualizationArray = visualizationPoints.Span; - var visualizationIdx = 0; - - for (int i = 0; i < pointArray.Length; i += PotreeData.Metadata.PointSize) + for (int i = 0; i < visualizationPoints.Length; i++) { - var attributeSlice = new Span(pointArray).Slice(i + potreeSettingsAttribute.AttributeOffset, potreeSettingsAttribute.Size); - - HandleWriteExtraBytes(visualizationArray[visualizationIdx].Flags).CopyTo(attributeSlice); - - visualizationIdx++; + foreach (var attrib in attribs) + { + if (attrib != null) + { + var extraBytesRaw = HandleWriteExtraBytes(visualizationPoints.Span[i].Flags, attrib).ToArray(); + PotreeData.WriteViewAccessor.WriteArray(potreeNode.ByteOffset + (PotreeData.Metadata.PointSize * i) + attrib.AttributeOffset, extraBytesRaw, 0, extraBytesRaw.Length); + } + } } - - WriteRawNodeData(potreeNode, pointArray); - } - - private void WriteRawNodeData(PotreeNode potreeNode, byte[] rawNodeData) - { - Guard.IsNotNull(PotreeData); - Guard.IsNotNull(rawNodeData); - - var potreePointSize = (int)potreeNode.NumPoints * PotreeData.Metadata.PointSize; - PotreeData.WriteViewAccessor.WriteArray(potreeNode.ByteOffset, rawNodeData, 0, potreePointSize); } } } \ No newline at end of file diff --git a/src/Tests/Render/Desktop/References/AdvancedUI.png b/src/Tests/Render/Desktop/References/AdvancedUI.png index fcffcc601..93408daa3 100644 Binary files a/src/Tests/Render/Desktop/References/AdvancedUI.png and b/src/Tests/Render/Desktop/References/AdvancedUI.png differ diff --git a/src/Tests/Render/Desktop/References/Camera.png b/src/Tests/Render/Desktop/References/Camera.png index dc8d08020..8d34d7ff3 100644 Binary files a/src/Tests/Render/Desktop/References/Camera.png and b/src/Tests/Render/Desktop/References/Camera.png differ diff --git a/src/Tests/Render/Desktop/References/Deferred.png b/src/Tests/Render/Desktop/References/Deferred.png index bc109e4f6..e5adf3e7a 100644 Binary files a/src/Tests/Render/Desktop/References/Deferred.png and b/src/Tests/Render/Desktop/References/Deferred.png differ diff --git a/src/Tests/Render/Desktop/References/Fractal.png b/src/Tests/Render/Desktop/References/Fractal.png index 96fc83288..9268213d0 100644 Binary files a/src/Tests/Render/Desktop/References/Fractal.png and b/src/Tests/Render/Desktop/References/Fractal.png differ diff --git a/src/Tests/Render/Desktop/References/Labyrinth.png b/src/Tests/Render/Desktop/References/Labyrinth.png index 3c627c7c4..48b566404 100644 Binary files a/src/Tests/Render/Desktop/References/Labyrinth.png and b/src/Tests/Render/Desktop/References/Labyrinth.png differ diff --git a/src/Tests/Render/Desktop/References/Materials.png b/src/Tests/Render/Desktop/References/Materials.png index e83107a81..f48b0cbe7 100644 Binary files a/src/Tests/Render/Desktop/References/Materials.png and b/src/Tests/Render/Desktop/References/Materials.png differ diff --git a/src/Tests/Render/Desktop/References/Picking.png b/src/Tests/Render/Desktop/References/Picking.png index d15d977dd..9b1a76712 100644 Binary files a/src/Tests/Render/Desktop/References/Picking.png and b/src/Tests/Render/Desktop/References/Picking.png differ diff --git a/src/Tests/Render/Desktop/References/Simple.png b/src/Tests/Render/Desktop/References/Simple.png index aac80f449..099d90202 100644 Binary files a/src/Tests/Render/Desktop/References/Simple.png and b/src/Tests/Render/Desktop/References/Simple.png differ diff --git a/src/Tests/Render/Desktop/References/ThreeDFont.png b/src/Tests/Render/Desktop/References/ThreeDFont.png index bcb975e7e..5b383af4a 100644 Binary files a/src/Tests/Render/Desktop/References/ThreeDFont.png and b/src/Tests/Render/Desktop/References/ThreeDFont.png differ