diff --git a/zzio/EnumerableExtensions.cs b/zzio/EnumerableExtensions.cs index d77ef10d..a6fa1e9f 100644 --- a/zzio/EnumerableExtensions.cs +++ b/zzio/EnumerableExtensions.cs @@ -73,4 +73,36 @@ public static IEnumerable SelectMany(this IEnumerable(this IEnumerable set, Func selector, TCompare? defaultValue = default) => set.Any() ? set.Max(selector) : defaultValue; + + public delegate bool ReferencePredicate(in TElement element); + + public static int Count(this ReadOnlySpan span, ReferencePredicate predicate) + { + int count = 0; + foreach (ref readonly var element in span) + { + if (predicate(in element)) + count++; + } + return count; + } + + public static int Count(this Span span, ReferencePredicate predicate) + { + int count = 0; + foreach (ref readonly var element in span) + { + if (predicate(in element)) + count++; + } + return count; + } + + public static Range Sub(this Range full, Range sub, int maxValue = int.MaxValue) + { + var (fullOffset, fullLength) = full.GetOffsetAndLength(maxValue); + var (subOffset, subLength) = sub.GetOffsetAndLength(fullLength); + int newOffset = fullOffset + subOffset; + return newOffset..(newOffset + subLength); + } } diff --git a/zzio/primitives/FColor.cs b/zzio/primitives/FColor.cs index ff2cc12e..a79c233b 100644 --- a/zzio/primitives/FColor.cs +++ b/zzio/primitives/FColor.cs @@ -35,6 +35,8 @@ public void Write(BinaryWriter w) public static FColor operator *(FColor a, FColor b) => new(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); + public static FColor operator *(FColor a, float f) => new(a.r * f, a.g * f, a.b * f, a.a * f); + public static implicit operator IColor(FColor c) => new( (byte)(c.r * 255f), (byte)(c.g * 255f), diff --git a/zzre.core.tests/TestRangeCollection.cs b/zzre.core.tests/TestRangeCollection.cs index 2b3d2082..41251564 100644 --- a/zzre.core.tests/TestRangeCollection.cs +++ b/zzre.core.tests/TestRangeCollection.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace zzre.tests; @@ -39,6 +40,18 @@ public void AddMergeOverlapping() Assert.AreEqual(new[] { 3..10 }, coll); } + [Test] + public void AddMergeExactFit() + { + var coll = new RangeCollection + { + 0..3, + 7..10, + 3..7 + }; + Assert.AreEqual(new[] { 0..10 }, coll); + } + [Test] public void AddMergeComplex() { @@ -53,6 +66,40 @@ public void AddMergeComplex() Assert.AreEqual(new[] { 0..17 }, coll); } + [Test] + public void AddBestFit() + { + // Finds hole in the middle + var coll = new RangeCollection { 0..3, 7..10 }; + Assert.AreEqual(3..5, coll.AddBestFit(2)); + Assert.AreEqual(new[] { 0..5, 7..10 }, coll); + + // Finds hole at the start + coll = new RangeCollection { 7..10 }; + Assert.AreEqual(0..3, coll.AddBestFit(3)); + Assert.AreEqual(new[] { 0..3, 7..10 }, coll); + + // Ignores holes that are too small + coll = new RangeCollection { 2..5, 7..10, 15..20 }; + Assert.AreEqual(10..14, coll.AddBestFit(4)); + Assert.AreEqual(new[] { 2..5, 7..14, 15..20 }, coll); + + // Preferes better fitting holes + coll = new RangeCollection { 5..10, 13..15 }; + Assert.AreEqual(10..12, coll.AddBestFit(2)); + Assert.AreEqual(new[] { 5..12, 13..15 }, coll); + + // Returns null on empty and too small + coll = new RangeCollection(5); + Assert.IsNull(coll.AddBestFit(10)); + Assert.IsEmpty(coll); + + // Returns null on too small + coll = new RangeCollection(10) { 3..8 }; + Assert.IsNull(coll.AddBestFit(9)); + Assert.AreEqual(new[] { 3..8 }, coll); + } + [Test] public void RemoveNonExistant() { diff --git a/zzre.core/GameTime.cs b/zzre.core/GameTime.cs index 55e588d0..51037330 100644 --- a/zzre.core/GameTime.cs +++ b/zzre.core/GameTime.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; namespace zzre; @@ -7,8 +9,8 @@ namespace zzre; public class GameTime { private readonly Stopwatch watch = new(); + private readonly List curFrametimes = new List(60); - private int curFPS = 0; private TimeSpan lastSecond; private TimeSpan frameStart; @@ -18,9 +20,12 @@ public class GameTime public float Delta => Math.Min(MaxDelta, UnclampedDelta); public float UnclampedDelta { get; private set; } = 0.0f; public int Framerate { get; private set; } = 0; + public double FrametimeAvg { get; private set; } = 0f; + public double FrametimeSD { get; private set; } = 0f; public bool HasFramerateChanged { get; private set; } = false; private TimeSpan TargetFrametime => TimeSpan.FromSeconds(1.0 / TargetFramerate); + public string FormattedStats => $"FPS: {Framerate} | FT: {FrametimeAvg:F2}ms"; public GameTime() { @@ -33,19 +38,21 @@ public void BeginFrame() UnclampedDelta = (float)(watch.Elapsed - frameStart).TotalSeconds; frameStart = watch.Elapsed; - curFPS++; HasFramerateChanged = false; if ((frameStart - lastSecond).TotalSeconds >= 1) { - Framerate = (int)(curFPS / (frameStart - lastSecond).TotalSeconds + 0.5); + Framerate = (int)(curFrametimes.Count / (frameStart - lastSecond).TotalSeconds + 0.5); + FrametimeAvg = curFrametimes.Average(); + FrametimeSD = curFrametimes.Sum(f => Math.Pow(f - FrametimeAvg, 2)) / curFrametimes.Count; + curFrametimes.Clear(); lastSecond = frameStart; - curFPS = 0; HasFramerateChanged = true; } } public void EndFrame() { + curFrametimes.Add((watch.Elapsed - frameStart).TotalMilliseconds); int delayMs = (int)(TargetFrametime - (watch.Elapsed - frameStart)).TotalMilliseconds; if (delayMs > 0) Thread.Sleep(delayMs); diff --git a/zzre.core/RangeCollection.cs b/zzre.core/RangeCollection.cs index 2e5f55d8..df55e104 100644 --- a/zzre.core/RangeCollection.cs +++ b/zzre.core/RangeCollection.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using zzio; namespace zzre; @@ -17,6 +18,14 @@ public class RangeCollection : ICollection, IReadOnlyCollection : new Range(Index.Start, Index.Start); public int Area => ranges.Sum(r => r.GetOffsetAndLength(MaxRangeValue).Length); + public int MinValue => ranges.Any() + ? ranges.First().Start.GetOffset(MaxRangeValue) + : -1; + + public int MaxValue => ranges.Any() + ? ranges.Last().End.GetOffset(MaxRangeValue) - 1 + : -1; + private int _maxRangeValue; public int MaxRangeValue { @@ -35,6 +44,13 @@ public RangeCollection(int maxRangeValue = int.MaxValue) => public void Add(Range item) { var (itemOffset, itemLength) = item.GetOffsetAndLength(MaxRangeValue); + if (itemOffset == 0 && itemLength >= MaxRangeValue) + { + ranges.Clear(); + ranges.Add(..MaxRangeValue); + return; + } + var nearItem = new Range( Math.Max(0, itemOffset - 1), itemOffset + itemLength + (itemOffset + itemLength < MaxRangeValue ? 1 : 0)); @@ -53,9 +69,16 @@ public void Add(Range item) public bool Remove(Range remove) { - var intersections = FindIntersections(remove).ToArray(); var removeStart = remove.Start.GetOffset(MaxRangeValue); var removeEnd = remove.End.GetOffset(MaxRangeValue); + if (removeStart == 0 && removeEnd == MaxRangeValue) + { + var wasNotEmpty = ranges.Any(); + Clear(); + return wasNotEmpty; + } + + var intersections = FindIntersections(remove).ToArray(); var result = false; foreach (var i in intersections) { @@ -70,6 +93,56 @@ public bool Remove(Range remove) return result; } + public Range? AddBestFit(int length) + { + if (!ranges.Any()) + { + if (MaxRangeValue < length) + return null; + Add(0..length); + return 0..length; + } + + var lastEnd = 0; + int bestStart = -1; + int bestLength = int.MaxValue; + foreach (var curRange in ranges) + { + var (curStart, curEnd) = curRange.GetOffsetAndLength(MaxRangeValue); + curEnd += curStart; + + int curHoleLength = curStart - lastEnd; + if (curHoleLength >= length && curHoleLength < bestLength) + { + bestStart = lastEnd; + bestLength = curHoleLength; + } + if (bestLength == length) + break; // it does not get better than optimal + + lastEnd = curEnd; + } + if (bestStart < 0) + return null; + var newRange = bestStart..(bestStart + length); + Add(newRange); + return newRange; + } + + public Range? RemoveBestFit(int length) + { + var range = ranges + .Where(r => r.GetLength(MaxRangeValue) >= length) + .OrderBy(r => r.GetLength(MaxRangeValue)) + .FirstOrDefault(); + if (range.Equals(default)) + return null; + ranges.Remove(range); + range = range.Start..range.Start.Offset(length); + ranges.Add(range); + return range; + } + public bool Contains(Range item) => FindIntersections(item) .Any(i => Contains(item, i)); diff --git a/zzre.core/math/NumericsExtensions.cs b/zzre.core/math/NumericsExtensions.cs index 97c8f9bc..def86454 100644 --- a/zzre.core/math/NumericsExtensions.cs +++ b/zzre.core/math/NumericsExtensions.cs @@ -96,6 +96,9 @@ public static Vector3 OnSphere(this Random random) public static int NextSign(this Random random) => random.Next(2) * 2 - 1; + public static uint Next(this Random random, uint exclusiveMax) => + checked((uint)random.Next((int)exclusiveMax)); + public static float Next(this Random random, float min, float max) => min + random.NextFloat() * (max - min); diff --git a/zzre.core/rendering/DynamicGraphicsBuffer.cs b/zzre.core/rendering/DynamicGraphicsBuffer.cs new file mode 100644 index 00000000..c355408c --- /dev/null +++ b/zzre.core/rendering/DynamicGraphicsBuffer.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Veldrid; +using zzio; + +namespace zzre.rendering; + +public class DynamicGraphicsBuffer : BaseDisposable +{ + private const int MaxUploadDistance = 512; + + private readonly GraphicsDevice device; + private readonly BufferUsage usage; + private readonly string bufferName; + private readonly float minGrowFactor; + private readonly RangeCollection dirtyBytes = new(0); + private readonly RangeCollection usedElements = new(0); + private DeviceBuffer? buffer; + private byte[]? bytes; + private int sizePerElement; + + public DeviceBuffer? OptionalBuffer => buffer; + public DeviceBuffer Buffer => buffer ?? + throw new InvalidOperationException("Buffer was not created yet"); + + public int SizePerElement + { + get => sizePerElement; + set + { + if (Count > 0) + throw new InvalidOperationException("Cannot change sizePerElement of a non-empty buffer"); + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value)); + sizePerElement = value; + } + } + public int CommittedCapacity => SizePerElement > 0 + ? (bytes?.Length ?? 0) / SizePerElement + : 0; + public int ReservedCapacity + { + get => usedElements.MaxRangeValue; + private set => usedElements.MaxRangeValue = value; + } + public int Count => usedElements.Area; + public int FreeCount => ReservedCapacity - Count; + private int FreeCountAtEnd => ReservedCapacity - (LastUsedIndex + 1); + private int LastUsedIndex => usedElements.MaxValue; + + public DynamicGraphicsBuffer(GraphicsDevice device, BufferUsage usage, + string bufferName = nameof(DynamicGraphicsBuffer), + float minGrowFactor = 1.5f) + { + if (minGrowFactor <= 1f) + throw new ArgumentOutOfRangeException(nameof(minGrowFactor)); + this.device = device; + this.usage = usage; + this.bufferName = bufferName; + this.minGrowFactor = minGrowFactor; + } + + protected override void DisposeManaged() + { + base.DisposeManaged(); + buffer?.Dispose(); + buffer = null; + bytes = null; + dirtyBytes.MaxRangeValue = 0; + Clear(); + } + + /// Forgets all rentals (commited or reserved) + public void Clear() + { + usedElements.Clear(); + usedElements.MaxRangeValue = CommittedCapacity; + dirtyBytes.Clear(); + } + + public Range Rent(int request, bool fast = false) + { + if (!fast) + { + var bestRange = usedElements.AddBestFit(request); + if (bestRange.HasValue) + return bestRange.Value; + } + int missingElements = request - FreeCountAtEnd; + if (missingElements > 0) + ReservedCapacity = Math.Max(ReservedCapacity, CommittedCapacity + missingElements); + + var firstFree = LastUsedIndex + 1; + var endRange = firstFree..(firstFree + request); + usedElements.Add(endRange); + return endRange; + } + + public void Return(Range range) => usedElements.Remove(range); + + private void Commit() + { + if (sizePerElement == 0) + throw new InvalidOperationException("Cannot commit dynamic graphics buffer without a size per element"); + + int nextCapacity = ReservedCapacity; + if (nextCapacity > CommittedCapacity) + nextCapacity = Math.Max(ReservedCapacity, (int)(CommittedCapacity * minGrowFactor + 0.5f)); + + var nextCapacityInBytes = nextCapacity * sizePerElement; + if (nextCapacityInBytes > (bytes?.Length ?? 0)) + { + Array.Resize(ref bytes, nextCapacityInBytes); + dirtyBytes.MaxRangeValue = nextCapacityInBytes; + } + } + + private Range AsByteRange(Range range) + { + var (offset, length) = range.GetOffsetAndLength(ReservedCapacity); + return (offset * sizePerElement)..((offset + length) * sizePerElement); + } + + public ReadOnlySpan Read(Range range) + { + Commit(); + return bytes!.AsSpan(AsByteRange(range)); + } + + public Span Write(Range range) + { + Commit(); + var byteRange = AsByteRange(range); + dirtyBytes.Add(byteRange); + return bytes!.AsSpan(byteRange); + } + + public void Update(CommandList cl) + { + if (!dirtyBytes.Any() && (buffer != null || CommittedCapacity == 0)) + return; + int capacityInBytes = CommittedCapacity * SizePerElement; + if ((uint)dirtyBytes.MaxValue > (buffer?.SizeInBytes ?? 0)) + { + buffer?.Dispose(); + buffer = device.ResourceFactory.CreateBuffer(new((uint)capacityInBytes, usage)); + buffer.Name = bufferName; + } + + dirtyBytes.MergeNearbyRanges(MaxUploadDistance); + foreach (var range in dirtyBytes) + { + var offset = range.GetOffset(capacityInBytes); + cl.UpdateBuffer(buffer, (uint)offset, bytes.AsSpan(range)); + } + dirtyBytes.Clear(); + } +} diff --git a/zzre.core/rendering/DynamicMesh.cs b/zzre.core/rendering/DynamicMesh.cs index 365b5991..405f8749 100644 --- a/zzre.core/rendering/DynamicMesh.cs +++ b/zzre.core/rendering/DynamicMesh.cs @@ -12,249 +12,193 @@ public class DynamicMesh : BaseDisposable, IVertexAttributeContainer { private const int MaxUploadDistance = 512; - private interface IAttribute + private interface IAttribute : IDisposable { string Name { get; } - uint Offset { get; set; } - uint ElementSize { get; } + DynamicGraphicsBuffer Buffer { get; } } public class Attribute : IAttribute where T : unmanaged { - private readonly DynamicMesh mesh; + private readonly DynamicGraphicsBuffer buffer; public string Name { get; } - uint IAttribute.Offset { get; set; } - unsafe uint IAttribute.ElementSize => (uint)sizeof(T); + DynamicGraphicsBuffer IAttribute.Buffer => buffer; - public Attribute(DynamicMesh mesh, string name) + public unsafe Attribute(GraphicsDevice device, bool dynamic, string meshName, string name, float minGrowFactor) { - this.mesh = mesh; Name = name; + buffer = new(device, + BufferUsage.VertexBuffer | (dynamic ? BufferUsage.Dynamic : default), + $"{meshName} {name}", + minGrowFactor) + { + SizePerElement = sizeof(T) + }; } - public T this[int index] - { - get => ReadSpan[index]; - set => AsWriteSpan(index, 1)[0] = value; - } + void IDisposable.Dispose() => buffer.Dispose(); - public ReadOnlySpan ReadSpan => AsSpanInternal(forWrite: false, 0, -1); - public Span AsWriteSpan(int offset = 0, int length = -1) => AsSpanInternal(forWrite: true, offset, length); + public ReadOnlySpan Read(Range range) => + MemoryMarshal.Cast(buffer.Read(range)); - private Span AsSpanInternal(bool forWrite, int offset, int length) + public Span Write(Range range) => + MemoryMarshal.Cast(buffer.Write(range)); + + public Span Write(int offset, int count) => + Write(offset..(offset + count)); + + public T this[int index] { - mesh.EnsureArray(); - var byteRange = mesh.GetByteRange(this, offset, length); - if (forWrite) - mesh.dirtyVertexBytes.Add(byteRange); - return MemoryMarshal.Cast(mesh.vertices!.AsSpan(byteRange)); + get => Read(index..(index + 1))[0]; + set => Write(index..(index + 1))[0] = value; } } protected readonly GraphicsDevice graphicsDevice; protected readonly ResourceFactory resourceFactory; private readonly bool dynamic; + private readonly string meshName; private readonly float minGrowFactor; private readonly List attributes = new(); - private readonly RangeCollection dirtyVertexBytes = new(); - private DeviceBuffer? vertexBuffer, indexBuffer; - private int uploadedPrimitiveCount; - private ushort[] indexPattern = Array.Empty(); - private int verticesPerPrimitive; - private byte[]? vertices; - private int? nextVertexCapacity; - - private uint BytesPerVertex => attributes.Aggregate(0u, (t, a) => t + a.ElementSize); - public int VertexCapacity { get; private set; } - public int VertexCount { get; private set; } - public int VertexFreeCount => VertexCapacity - VertexCount; - public int PrimitiveCount => verticesPerPrimitive < 1 ? 0 : VertexCount / verticesPerPrimitive; + private readonly DynamicGraphicsBuffer indexBuffer; - public IReadOnlyList IndexPattern - { - get => indexPattern; - set - { - indexPattern = value.ToArray(); - uploadedPrimitiveCount = 0; - verticesPerPrimitive = indexPattern.Max() + 1; - } - } - public int IndexCount => PrimitiveCount * IndexPattern.Count; + public int VertexCapacity => attributes.FirstOrDefault()?.Buffer.ReservedCapacity ?? 0; + public int VertexCount => attributes.FirstOrDefault()?.Buffer.Count ?? 0; + public int IndexCapacity => indexBuffer.ReservedCapacity; + public int IndexCount => indexBuffer.Count; public IndexFormat IndexFormat => IndexFormat.UInt16; - public DeviceBuffer IndexBuffer => indexBuffer ?? - throw new InvalidOperationException("Index buffer was not yet generated"); + public DeviceBuffer IndexBuffer => indexBuffer.Buffer; - public DynamicMesh(ITagContainer diContainer, bool dynamic = true, float minGrowFactor = 1.5f) + public DynamicMesh(ITagContainer diContainer, + bool dynamic = true, + string name = nameof(DynamicMesh), + float minGrowFactor = 1.5f) { if (minGrowFactor <= 1f) throw new ArgumentOutOfRangeException(nameof(minGrowFactor)); graphicsDevice = diContainer.GetTag(); resourceFactory = graphicsDevice.ResourceFactory; this.dynamic = dynamic; + this.meshName = name; this.minGrowFactor = minGrowFactor; + + indexBuffer = new(graphicsDevice, + BufferUsage.IndexBuffer | (dynamic ? BufferUsage.Dynamic : default), + name + " Indices", + minGrowFactor) + { + SizePerElement = sizeof(ushort) + }; } protected override void DisposeManaged() { base.DisposeManaged(); - vertexBuffer?.Dispose(); - indexBuffer?.Dispose(); - vertices = null; + foreach (var attribute in attributes) + attribute.Dispose(); attributes.Clear(); + indexBuffer.Dispose(); } - public void Clear() => VertexCount = 0; + public void Clear() + { + ClearVertices(); + ClearIndices(); + } - public void Reserve(int capacity, bool additive = true) + public void ClearVertices() { - if (attributes.Count == 0) - throw new InvalidOperationException("Cannot resize dynamic mesh without attributes"); - if (capacity < 0) - throw new ArgumentOutOfRangeException(nameof(capacity)); - if (additive) - nextVertexCapacity ??= VertexCapacity; - else - nextVertexCapacity = 0; - nextVertexCapacity += capacity; + foreach (var attribute in attributes) + attribute.Buffer.Clear(); } - public int Add(int count = 1) + public void ClearIndices() => indexBuffer.Clear(); + + public Range RentVertices(int request, bool fast = false) { - if (count < 1) - throw new ArgumentOutOfRangeException(nameof(count)); - if (count == 0) - return VertexCount - 1; - if (count > VertexFreeCount) + if (!attributes.Any()) + throw new InvalidOperationException("Cannot rent any vertices without attributes"); + var range = attributes.First().Buffer.Rent(request, fast); + foreach (var attribute in attributes.Skip(1)) { - if (nextVertexCapacity is not null) - nextVertexCapacity = Math.Max(nextVertexCapacity.Value, VertexCapacity); - Reserve(count - VertexFreeCount, additive: true); + var otherRange = attribute.Buffer.Rent(request, fast); + if (!range.Equals(otherRange)) + throw new InvalidOperationException("Somehow the attribute buffers have gone out of sync"); } - EnsureArray(); - int result = VertexCount; - VertexCount += count; - return result; + return range; } - private void EnsureArray() + public void ReturnVertices(Range range) { - if (attributes.Count == 0) - throw new InvalidOperationException("Cannot resize dynamic mesh without attributes"); + foreach (var attribute in attributes) + attribute.Buffer.Return(range); + } - if ((vertices?.LongLength ?? 0) >= BytesPerVertex * VertexCapacity && - VertexCapacity >= nextVertexCapacity) - { - nextVertexCapacity = null; - return; - } + public Range RentIndices(int request, bool fast = false) => + indexBuffer.Rent(request, fast); - if (nextVertexCapacity is not null) - { - var minNextVertexCapacity = (int)(VertexCapacity * minGrowFactor + 0.5f); - VertexCapacity = Math.Max(minNextVertexCapacity, nextVertexCapacity.Value); - } - nextVertexCapacity = null; + public void ReturnIndices(Range range) => + indexBuffer.Return(range); - var needsCopyFromPrevious = vertices != null && VertexCount > 0; - var newVertices = new byte[VertexCapacity * BytesPerVertex]; - var curOffset = 0u; - for (int i = 0; i < attributes.Count; i++) - { - if (needsCopyFromPrevious && attributes[i].Offset < vertices!.Length) - vertices.AsSpan(GetByteRange(attributes[i])).CopyTo(newVertices.AsSpan((int)curOffset)); - attributes[i].Offset = curOffset; - curOffset += attributes[i].ElementSize * (uint)VertexCapacity; - } - vertices = newVertices; + public ReadOnlySpan ReadIndices(Range range) => + MemoryMarshal.Cast(indexBuffer.Read(range)); - if (needsCopyFromPrevious) - { - dirtyVertexBytes.Clear(); - dirtyVertexBytes.MaxRangeValue = vertices.Length; - dirtyVertexBytes.Add(Range.All); - } - else - dirtyVertexBytes.MaxRangeValue = vertices.Length; - } - - public void Update(CommandList cl) - { - UpdateVertexBuffer(cl); - UpdateIndexBuffer(cl); - } + public Span WriteIndices(Range range) => + MemoryMarshal.Cast(indexBuffer.Write(range)); - private void UpdateVertexBuffer(CommandList cl) + public void SetIndicesFromPattern(IReadOnlyList pattern) { - if (vertices == null) + ClearIndices(); + var verticesPerPrimitive = pattern.Max() + 1; + var primitiveCount = VertexCount / verticesPerPrimitive; + if (primitiveCount <= 0) return; - if ((vertexBuffer?.SizeInBytes ?? 0) < vertices.Length) - { - if (vertexBuffer != null) - { - dirtyVertexBytes.Clear(); - dirtyVertexBytes.Add(Range.All); - } - vertexBuffer?.Dispose(); - var bufferUsage = BufferUsage.VertexBuffer | (dynamic ? BufferUsage.Dynamic : default); - vertexBuffer = resourceFactory.CreateBuffer(new((uint)vertices.Length, bufferUsage)); - vertexBuffer.Name = $"InstanceBuffer {GetHashCode()}"; - } - - dirtyVertexBytes.MergeNearbyRanges(MaxUploadDistance); - foreach (var range in dirtyVertexBytes) - { - var offset = range.GetOffset((int)vertexBuffer!.SizeInBytes); - cl.UpdateBuffer(vertexBuffer, (uint)offset, vertices.AsSpan(range)); - } - dirtyVertexBytes.Clear(); + var indexRange = RentIndices(primitiveCount * pattern.Count); + StaticMesh.GeneratePatternIndices(WriteIndices(indexRange), pattern, primitiveCount, verticesPerPrimitive); } - private void UpdateIndexBuffer(CommandList cl) + public void Update(CommandList cl) { - if (IndexPattern.Count == 0) - return; - var expectedIndexBufferSize = PrimitiveCount * IndexPattern.Count * sizeof(ushort); - var indexBufferTooSmall = (indexBuffer?.SizeInBytes ?? 0) < expectedIndexBufferSize; - - if (indexBufferTooSmall) - { - uploadedPrimitiveCount = 0; - indexBuffer?.Dispose(); - indexBuffer = resourceFactory.CreateBuffer(new((uint)expectedIndexBufferSize, BufferUsage.IndexBuffer)); - indexBuffer.Name = $"InstanceBuffer Indices {GetHashCode()}"; - } - - if (uploadedPrimitiveCount < PrimitiveCount) - { - var indices = StaticMesh.GeneratePatternIndices(indexPattern, uploadedPrimitiveCount, PrimitiveCount, verticesPerPrimitive); - cl.UpdateBuffer(indexBuffer, (uint)(uploadedPrimitiveCount * PrimitiveCount * sizeof(ushort)), indices); - } + foreach (var attribute in attributes) + attribute.Buffer.Update(cl); + indexBuffer.Update(cl); } public bool TryGetBufferByMaterialName(string name, [NotNullWhen(true)] out DeviceBuffer? buffer, out uint offset) { var attribute = attributes.FirstOrDefault(a => a.Name == name); - buffer = vertexBuffer; - offset = attribute?.Offset ?? 0u; - return attribute?.Name != null && buffer != null; + buffer = attribute?.Buffer.OptionalBuffer; + offset = 0u; + return buffer != null; } - public Attribute AddAttribute(string name) where T : unmanaged + public Attribute AddAttribute(string attributeName) where T : unmanaged { - var attribute = new Attribute(this, name); + var attribute = new Attribute(graphicsDevice, dynamic, meshName, attributeName, minGrowFactor); attributes.Add(attribute); return attribute; } - private Range GetByteRange(IAttribute attribute, int offset = 0, int length = -1) + public void Preallocate(int vertices, int indices) { - if (length < 0) - length = VertexCount - offset; - if (offset < 0 || offset + length > VertexCount) - throw new ArgumentOutOfRangeException(nameof(offset)); - var start = attribute.Offset + offset * attribute.ElementSize; - var end = attribute.Offset + (offset + length) * attribute.ElementSize; - return (int)start..(int)end; + if (vertices < 0 || indices < 0) + throw new ArgumentOutOfRangeException(); + if (vertices > 0) + { + if (VertexCount > 0) + throw new InvalidOperationException("Cannot preallocate vertices while buffer is in use"); + var vertexRange = RentVertices(vertices); + foreach (var attribute in attributes) + attribute.Buffer.Write(vertexRange); + ClearVertices(); + } + if (indices > 0) + { + if (IndexCount > 0) + throw new InvalidOperationException("Cannot preallocate indices while buffer is in use"); + var indexRange = RentIndices(indices); + indexBuffer.Write(indexRange); + ClearIndices(); + } } } diff --git a/zzre.core/rendering/IRenderable.cs b/zzre.core/rendering/IRenderable.cs deleted file mode 100644 index 183b690c..00000000 --- a/zzre.core/rendering/IRenderable.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Veldrid; - -namespace zzre.rendering; - -public interface IRenderable -{ - void Render(CommandList cl); -} diff --git a/zzre.core/rendering/MlangMaterial.cs b/zzre.core/rendering/MlangMaterial.cs index 166bca5d..08f9084f 100644 --- a/zzre.core/rendering/MlangMaterial.cs +++ b/zzre.core/rendering/MlangMaterial.cs @@ -25,6 +25,7 @@ public class MlangMaterial : BaseDisposable, IMaterial private ResourceSet[]? resourceSets; public GraphicsDevice Device { get; } + public string DebugName { get; set; } = ""; public IBuiltVariantPipeline Pipeline { diff --git a/zzre.core/rendering/StaticMesh.cs b/zzre.core/rendering/StaticMesh.cs index a892eb99..988fe524 100644 --- a/zzre.core/rendering/StaticMesh.cs +++ b/zzre.core/rendering/StaticMesh.cs @@ -173,28 +173,27 @@ public void SetIndicesFromPattern(IReadOnlyList pattern) var verticesPerPrimitive = pattern.Max() + 1; var primitiveCount = VertexCount / verticesPerPrimitive; var buffer = SetIndexCount(primitiveCount * pattern.Count, IndexFormat.UInt16); - var indices = GeneratePatternIndices(pattern, 0, primitiveCount, verticesPerPrimitive); + var indices = new ushort[IndexCount]; + GeneratePatternIndices(indices, pattern, primitiveCount, verticesPerPrimitive); graphicsDevice.UpdateBuffer(buffer, 0u, indices); } - internal static ushort[] GeneratePatternIndices(IReadOnlyList pattern, - int primitiveStart, + public static void GeneratePatternIndices( + Span indices, + IReadOnlyList pattern, int primitiveCount, - int verticesPerPrimitive) + int verticesPerPrimitive, + int vertexOffset = 0) { if (pattern.Count == 0) throw new ArgumentOutOfRangeException(nameof(pattern)); - if (primitiveStart < 0 || primitiveStart > primitiveCount) - throw new ArgumentOutOfRangeException(nameof(primitiveStart)); - if (primitiveStart == primitiveCount) - return Array.Empty(); - var indices = new ushort[pattern.Count * (primitiveCount - primitiveStart)]; - for (int i = primitiveStart; i < primitiveCount; i++) + if (primitiveCount <= 0) + return; + for (int i = 0; i < primitiveCount; i++) { for (int j = 0; j < pattern.Count; j++) - indices[i * pattern.Count + j] = (ushort)(i * verticesPerPrimitive + pattern[j]); + indices[i * pattern.Count + j] = (ushort)(i * verticesPerPrimitive + pattern[j] + vertexOffset); } - return indices; } public void AddSubMesh(SubMesh subMesh) diff --git a/zzre/Program.cs b/zzre/Program.cs index ae374a92..b95b3d05 100644 --- a/zzre/Program.cs +++ b/zzre/Program.cs @@ -95,6 +95,8 @@ private static void Main(string[] args) #if DEBUG new ZanzarahWindow(diContainer); + + //diContainer.GetTag().OpenWith("resources/effects/e0006.ed"); #endif window.Resized += () => @@ -125,7 +127,7 @@ private static void Main(string[] args) { time.BeginFrame(); if (time.HasFramerateChanged) - window.Title = $"Zanzarah | {graphicsDevice.BackendType} | FPS: {(int)(time.Framerate + 0.5)}"; + window.Title = $"Zanzarah | {graphicsDevice.BackendType} | {time.FormattedStats}"; windowContainer.Render(); graphicsDevice.SwapBuffers(); diff --git a/zzre/debug/DebugIconRenderer.cs b/zzre/debug/DebugIconRenderer.cs index 57acccb3..751d8925 100644 --- a/zzre/debug/DebugIconRenderer.cs +++ b/zzre/debug/DebugIconRenderer.cs @@ -18,8 +18,7 @@ public DebugIcon[] Icons set { instanceBuffer.Clear(); - instanceBuffer.Reserve(value.Length, additive: false); - Array.ForEach(value, instanceBuffer.Add); + instanceBuffer.AddRange(value); } } diff --git a/zzre/debug/DebugLineRenderer.cs b/zzre/debug/DebugLineRenderer.cs index 5baca80c..e96c4875 100644 --- a/zzre/debug/DebugLineRenderer.cs +++ b/zzre/debug/DebugLineRenderer.cs @@ -17,7 +17,7 @@ public enum AxisPlanes YZ = (1 << 2) } -public class DebugLineRenderer : BaseDisposable, IRenderable +public class DebugLineRenderer : BaseDisposable { private readonly DebugDynamicMesh mesh; @@ -49,36 +49,31 @@ public void Render(CommandList cl) public void Clear() => mesh.Clear(); - public void Reserve(int lineCount, bool additive = true) => - mesh.Reserve(lineCount * 2, additive); - public void Add(IColor color, Vector3 start, Vector3 end) => Add(color, new Line(start, end)); public void Add(IColor color, params Line[] lines) => Add(color, lines as IEnumerable); public void Add(IColor color, IEnumerable lines) { + var range = mesh.RentVertices(lines.Count() * 2); + mesh.AttrColor.Write(range).Fill(color); + var index = range.Start.Value; foreach (var line in lines) { - mesh.Add(new(line.Start, color)); - mesh.Add(new(line.End, color)); + mesh.AttrPos[index++] = line.Start; + mesh.AttrPos[index++] = line.End; } } - public void AddTriangles(IReadOnlyList triangles, IReadOnlyList colors) + public void AddTriangles(IColor color, IEnumerable triangles) { - if (triangles.Count != colors.Count) - throw new ArgumentException("Triangle and color count have to match"); - Reserve(triangles.Count * 3, additive: true); - foreach (var (tri, col) in triangles.Zip(colors)) - Add(col, tri.Edges()); + Add(color, triangles.SelectMany(t => t.Edges())); } public void AddOctahedron(IReadOnlyList corners, IColor color) { if (corners.Count != 6) throw new ArgumentException("Expected 6 corners for octahedron"); - Reserve(12); - Add(color, new Line[] - { + Add(color, + [ new(corners[0], corners[1]), new(corners[1], corners[2]), new(corners[2], corners[3]), @@ -93,7 +88,7 @@ public void AddOctahedron(IReadOnlyList corners, IColor color) new(corners[5], corners[1]), new(corners[5], corners[2]), new(corners[5], corners[3]) - }); + ]); } public void AddDiamondSphere(Sphere bounds, IColor color) @@ -116,9 +111,8 @@ public void AddHexahedron(IReadOnlyList corners, IColor color) { if (corners.Count != 8) throw new ArgumentException("Expected 8 corners for octahedron"); - Reserve(12); - Add(color, new Line[] - { + Add(color, + [ new(corners[0], corners[1]), new(corners[0], corners[2]), new(corners[3], corners[1]), @@ -133,7 +127,7 @@ public void AddHexahedron(IReadOnlyList corners, IColor color) new(corners[1], corners[5]), new(corners[2], corners[6]), new(corners[3], corners[7]), - }); + ]); } public void AddBox(OrientedBox box, IColor color) => @@ -160,8 +154,6 @@ IEnumerable GenerateParallelLines(Vector3 dir, Vector3 reach, int count) .Select(center => new Line(center - reach, center + reach)); } - static int PlaneLineCount(int count1, int count2) => (count1 + count2 + 1) * 2; - var lines = Enumerable.Empty(); int lineCount = originSize is null ? 0 : 3; Vector3 xReach = Vector3.UnitX * cellCountX * cellSize.X; @@ -169,27 +161,23 @@ IEnumerable GenerateParallelLines(Vector3 dir, Vector3 reach, int count) Vector3 zReach = Vector3.UnitZ * cellCountZ * cellSize.Z; if (planes.HasFlag(AxisPlanes.XY)) { - lineCount += PlaneLineCount(cellCountX, cellCountY); lines = lines .Concat(GenerateParallelLines(Vector3.UnitX, yReach, cellCountX)) .Concat(GenerateParallelLines(Vector3.UnitY, xReach, cellCountY)); } if (planes.HasFlag(AxisPlanes.XZ)) { - lineCount += PlaneLineCount(cellCountX, cellCountZ); lines = lines .Concat(GenerateParallelLines(Vector3.UnitX, zReach, cellCountX)) .Concat(GenerateParallelLines(Vector3.UnitZ, xReach, cellCountZ)); } if (planes.HasFlag(AxisPlanes.YZ)) { - lineCount += PlaneLineCount(cellCountY, cellCountZ); lines = lines .Concat(GenerateParallelLines(Vector3.UnitY, zReach, cellCountY)) .Concat(GenerateParallelLines(Vector3.UnitZ, yReach, cellCountZ)); } - Reserve(lineCount, additive: true); Add(gridColor, lines); if (originSize is not null) { diff --git a/zzre/debug/DebugPlaneRenderer.cs b/zzre/debug/DebugPlaneRenderer.cs index 03f04e85..596f0866 100644 --- a/zzre/debug/DebugPlaneRenderer.cs +++ b/zzre/debug/DebugPlaneRenderer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Veldrid; @@ -18,6 +19,8 @@ public struct DebugPlane public class DebugPlaneRenderer : BaseDisposable { + private static readonly IReadOnlyList IndexPattern = new ushort[] { 0, 1, 2, 3, 2, 1 }; + private readonly DebugDynamicMesh mesh; public DebugMaterial Material { get; } @@ -26,25 +29,26 @@ public DebugPlane[] Planes set { mesh.Clear(); - mesh.Reserve(value.Length * 4, additive: false); + var index = mesh.RentVertices(4 * value.Length).Start.Value; foreach (var plane in value) { var cameraUp = Vector3.Cross(plane.normal, Vector3.UnitY).LengthSquared() < 0.01f ? Vector3.UnitZ : Vector3.UnitY; var rotation = Matrix4x4.CreateLookAt(Vector3.Zero, plane.normal, cameraUp); var right = Vector3.Transform(Vector3.UnitX, rotation) * plane.size; var up = Vector3.Transform(Vector3.UnitY, rotation) * plane.size; - mesh.Add(new(plane.center - right - up, plane.color)); - mesh.Add(new(plane.center + right - up, plane.color)); - mesh.Add(new(plane.center - right + up, plane.color)); - mesh.Add(new(plane.center + right + up, plane.color)); + mesh.AttrColor.Write(index..(index + 4)).Fill(plane.color); + mesh.AttrPos[index++] = plane.center - right - up; + mesh.AttrPos[index++] = plane.center + right - up; + mesh.AttrPos[index++] = plane.center - right + up; + mesh.AttrPos[index++] = plane.center + right + up; } + mesh.SetIndicesFromPattern(IndexPattern); } } public DebugPlaneRenderer(ITagContainer diContainer) { mesh = new(diContainer, dynamic: false); - mesh.IndexPattern = new ushort[] { 0, 1, 2, 3, 2, 1 }; Material = new DebugMaterial(diContainer) { BothSided = true }; } @@ -57,7 +61,7 @@ protected override void DisposeManaged() public void Render(CommandList cl) { - if (mesh.PrimitiveCount == 0) + if (mesh.IndexCount == 0) return; mesh.Update(cl); (Material as IMaterial).Apply(cl); diff --git a/zzre/debug/DebugSkeletonRenderer.cs b/zzre/debug/DebugSkeletonRenderer.cs index 15c763b5..7e07e142 100644 --- a/zzre/debug/DebugSkeletonRenderer.cs +++ b/zzre/debug/DebugSkeletonRenderer.cs @@ -127,7 +127,6 @@ void LinkTransformsFor(IStandardTransformMaterial m) boneMesh.Add("Bone Indices", "inIndices", skinIndices.ToArray()); boneMesh.SetIndicesFromPattern(RhombusIndices); - lineRenderer.Reserve(3); lineRenderer.Add(Colors[0], Vector3.Zero, Vector3.UnitX * LineLength); lineRenderer.Add(Colors[1], Vector3.Zero, Vector3.UnitY * LineLength); lineRenderer.Add(Colors[2], Vector3.Zero, Vector3.UnitZ * LineLength); diff --git a/zzre/game/Game.cs b/zzre/game/Game.cs index 81b6e8f2..430398cb 100644 --- a/zzre/game/Game.cs +++ b/zzre/game/Game.cs @@ -7,6 +7,7 @@ using zzio; using zzio.scn; using zzio.vfs; +using zzre.materials; using zzre.rendering; namespace zzre.game; @@ -44,12 +45,15 @@ public Game(ITagContainer diContainer, Savegame savegame) AddTag(savegame); AddTag(ecsWorld = new DefaultEcs.World()); AddTag(new LocationBuffer(GetTag(), 4096)); + AddTag(new EffectMesh(this, 4096, 8192)); AddTag(camera = new Camera(this)); AddTag(new resources.Clump(this)); AddTag(new resources.ClumpMaterial(this)); AddTag(new resources.Actor(this)); AddTag(new resources.SkeletalAnimation(this)); + AddTag(new resources.EffectCombiner(this)); + AddTag(new resources.EffectMaterial(this)); ecsWorld.SetMaxCapacity(1); @@ -78,6 +82,11 @@ public Game(ITagContainer diContainer, Savegame savegame) new systems.MoveToLocation(this), new systems.AdvanceAnimation(this), + // Effects + new systems.effect.EffectCombiner(this), + new systems.effect.MovingPlanes(this), + new systems.effect.RandomPlanes(this), + // Animals new systems.Animal(this), new systems.Butterfly(this), @@ -147,11 +156,14 @@ public Game(ITagContainer diContainer, Savegame savegame) new systems.ActorRenderer(this), new systems.ModelRenderer(this, components.RenderOrder.EarlySolid), new systems.ModelRenderer(this, components.RenderOrder.EarlyAdditive), + new systems.effect.EffectRenderer(this, components.RenderOrder.EarlyEffect), new systems.ModelRenderer(this, components.RenderOrder.Solid), new systems.ModelRenderer(this, components.RenderOrder.Additive), new systems.ModelRenderer(this, components.RenderOrder.EnvMap), + new systems.effect.EffectRenderer(this, components.RenderOrder.Effect), new systems.ModelRenderer(this, components.RenderOrder.LateSolid), - new systems.ModelRenderer(this, components.RenderOrder.LateAdditive)); + new systems.ModelRenderer(this, components.RenderOrder.LateAdditive), + new systems.effect.EffectRenderer(this, components.RenderOrder.LateEffect)); var worldLocation = new Location(); camera.Location.Parent = worldLocation; diff --git a/zzre/game/Zanzarah.cs b/zzre/game/Zanzarah.cs index 09b32e28..3447d716 100644 --- a/zzre/game/Zanzarah.cs +++ b/zzre/game/Zanzarah.cs @@ -64,8 +64,10 @@ public void Update() public void Render(CommandList finalCommandList) { + finalCommandList.PushDebugGroup("Zanzarah"); CurrentGame?.Render(finalCommandList); UI.Render(finalCommandList); + finalCommandList.PopDebugGroup(); } private zzio.db.MappedDB LoadDatabase() diff --git a/zzre/game/components/effect/CombinerPlayback.cs b/zzre/game/components/effect/CombinerPlayback.cs new file mode 100644 index 00000000..fc3659be --- /dev/null +++ b/zzre/game/components/effect/CombinerPlayback.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace zzre.game.components.effect; + +public struct CombinerPlayback( + float duration, + bool depthTest = true) +{ + public float + CurTime = 0f, + CurProgress = 100f, + Length = 1f; + public readonly float Duration = duration; // set to infinite to loop + public readonly bool DepthTest = depthTest; + public bool IsFinished => CurTime >= Duration; + public bool IsRunning => !IsFinished && !MathEx.CmpZero(CurProgress); + public bool IsLooping => Duration == float.PositiveInfinity; +} diff --git a/zzre/game/components/effect/MovingPlanesState.cs b/zzre/game/components/effect/MovingPlanesState.cs new file mode 100644 index 00000000..fc8ed8ee --- /dev/null +++ b/zzre/game/components/effect/MovingPlanesState.cs @@ -0,0 +1,18 @@ +using System; + +namespace zzre.game.components.effect; + +public struct MovingPlanesState(Range vertexRange, Range indexRange, Rect texCoords) +{ + public float + CurRotation, + CurTexShift, + CurScale, + CurPhase1, + CurPhase2, + PrevProgress; + + public readonly Range VertexRange = vertexRange; + public readonly Range IndexRange = indexRange; + public readonly Rect TexCoords = texCoords; +} diff --git a/zzre/game/components/effect/RandomPlanesState.cs b/zzre/game/components/effect/RandomPlanesState.cs new file mode 100644 index 00000000..c852f08d --- /dev/null +++ b/zzre/game/components/effect/RandomPlanesState.cs @@ -0,0 +1,38 @@ +using System; +using System.Buffers; +using System.Numerics; + +namespace zzre.game.components.effect; + +public struct RandomPlanesState( + IMemoryOwner planeMemoryOwner, + int maxPlaneCount, + Range vertexRange, Range indexRange) +{ + public struct RandomPlane + { + public float + Life, + Rotation, + RotationSpeed, + Scale, + ScaleSpeed; + public Vector3 Pos; + public Vector4 StartColor, CurColor; + public uint TileI; + public float TileProgress; + } + + public float + CurPhase1, + CurPhase2, + CurTexShift, + SpawnProgress; + + public readonly IMemoryOwner PlaneMemoryOwner = planeMemoryOwner; + public readonly Memory Planes = planeMemoryOwner.Memory.Slice(0, maxPlaneCount); + public readonly Range + VertexRange = vertexRange, + IndexRange = indexRange; +} + diff --git a/zzre/game/components/effect/RenderIndices.cs b/zzre/game/components/effect/RenderIndices.cs new file mode 100644 index 00000000..41986ca1 --- /dev/null +++ b/zzre/game/components/effect/RenderIndices.cs @@ -0,0 +1,6 @@ +using System; + +namespace zzre.game.components.effect; + +// let's see how long we can survive with a single index range +public record struct RenderIndices(Range IndexRange); diff --git a/zzre/game/messages/SpawnEffectCombiner.cs b/zzre/game/messages/SpawnEffectCombiner.cs new file mode 100644 index 00000000..74095b9f --- /dev/null +++ b/zzre/game/messages/SpawnEffectCombiner.cs @@ -0,0 +1,10 @@ +using System; +using System.Numerics; + +namespace zzre.game.messages; + +public readonly record struct SpawnEffectCombiner( + int EffectId, + DefaultEcs.Entity? AsEntity = null, + Vector3? Position = null, + bool DepthTest = true); diff --git a/zzre/game/resources/EffectCombiner.cs b/zzre/game/resources/EffectCombiner.cs new file mode 100644 index 00000000..5bd0697e --- /dev/null +++ b/zzre/game/resources/EffectCombiner.cs @@ -0,0 +1,34 @@ +using DefaultEcs.Resource; +using zzio; +using zzio.effect; +using zzio.vfs; + +namespace zzre.game.resources; + +public class EffectCombiner : AResourceManager +{ + private static readonly FilePath BasePath = new("resources/effects/"); + private const string FileExtension = ".ed"; + private readonly IResourcePool resourcePool; + + public EffectCombiner(ITagContainer diContainer) + { + resourcePool = diContainer.GetTag(); + Manage(diContainer.GetTag()); + } + + protected override zzio.effect.EffectCombiner Load(int info) + { + var path = BasePath.Combine($"e{info}{FileExtension}"); + using var stream = resourcePool.FindAndOpen(path) ?? + throw new System.IO.FileNotFoundException($"Could not find effect combiner: {path}"); + var eff = new zzio.effect.EffectCombiner(); + eff.Read(stream); + return eff; + } + + protected override void OnResourceLoaded(in DefaultEcs.Entity entity, int info, zzio.effect.EffectCombiner resource) + { + entity.Set(resource); + } +} diff --git a/zzre/game/resources/EffectMaterial.cs b/zzre/game/resources/EffectMaterial.cs new file mode 100644 index 00000000..fd03aabe --- /dev/null +++ b/zzre/game/resources/EffectMaterial.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DefaultEcs.Resource; +using Veldrid; +using zzio; +using zzio.effect; +using zzre.rendering; +using static zzre.materials.EffectMaterial; + +namespace zzre.game.resources; + +public readonly record struct EffectMaterialInfo( + bool DepthTest, + BillboardMode BillboardMode, + BlendMode BlendMode, + string TextureName) +{ + public EffectMaterialInfo( + bool depthTest, + BillboardMode billboardMode, + EffectPartRenderMode renderMode, + string textureName) + : this(depthTest, billboardMode, RenderToBlendMode(renderMode), textureName) + { } + + private static BlendMode RenderToBlendMode(EffectPartRenderMode renderMode) => renderMode switch + { + EffectPartRenderMode.Additive => BlendMode.Additive, + EffectPartRenderMode.AdditiveAlpha => BlendMode.AdditiveAlpha, + EffectPartRenderMode.NormalBlend => BlendMode.Alpha, + _ => throw new NotSupportedException($"Unsupported effect part render mode: {renderMode}") + }; +} + +public class EffectMaterial : AResourceManager +{ + private static readonly FilePath[] TextureBasePaths = + { + new("resources/textures/effects"), + new("resources/textures/models") + }; + + private readonly ITagContainer diContainer; + private readonly GraphicsDevice graphicsDevice; + private readonly Camera camera; + private readonly IAssetLoader textureLoader; + + public EffectMaterial(ITagContainer diContainer) + { + this.diContainer = diContainer; + graphicsDevice = diContainer.GetTag(); + camera = diContainer.GetTag(); + textureLoader = diContainer.GetTag>(); + Manage(diContainer.GetTag()); + } + + protected override materials.EffectMaterial Load(EffectMaterialInfo info) + { + var material = new materials.EffectMaterial(diContainer) + { + DepthTest = info.DepthTest, + Billboard = info.BillboardMode, + Blend = info.BlendMode + }; + material.Texture.Texture = textureLoader.LoadTexture(TextureBasePaths, info.TextureName); + material.Sampler.Sampler = graphicsDevice.LinearSampler; + material.Projection.BufferRange = camera.ProjectionRange; + material.View.BufferRange = camera.ViewRange; + material.Factors.Value = new() + { + alphaReference = 0.03f + }; + material.DebugName = $"{info.TextureName} {info.BillboardMode} {info.BlendMode}"; + if (!info.DepthTest) + material.DebugName += " NoDepthTest"; + return material; + } + + protected override void Unload(EffectMaterialInfo info, materials.EffectMaterial resource) + { + if (textureLoader is not CachedAssetLoader) + resource.Texture.Texture?.Dispose(); + resource.Dispose(); + } + + protected override void OnResourceLoaded(in DefaultEcs.Entity entity, EffectMaterialInfo info, materials.EffectMaterial resource) + { + entity.Set(resource); + } +} diff --git a/zzre/game/systems/effect/BaseCombinerPart.cs b/zzre/game/systems/effect/BaseCombinerPart.cs new file mode 100644 index 00000000..ef822030 --- /dev/null +++ b/zzre/game/systems/effect/BaseCombinerPart.cs @@ -0,0 +1,41 @@ +using System; +using DefaultEcs.System; +using zzre.materials; + +namespace zzre.game.systems.effect; + +public abstract partial class BaseCombinerPart : AEntityMultiMapSystem + where TData : zzio.effect.IEffectPart + where TState : struct +{ + protected readonly EffectMesh effectMesh; + private readonly IDisposable addDisposable; + private readonly IDisposable removeDisposable; + + public BaseCombinerPart(ITagContainer diContainer) : + base(diContainer.GetTag(), CreateEntityContainer, useBuffer: false) + { + effectMesh = diContainer.GetTag(); + addDisposable = World.SubscribeEntityComponentAdded(HandleAddedComponent); + removeDisposable = World.SubscribeEntityComponentRemoved(HandleRemovedComponent); + } + + public override void Dispose() + { + base.Dispose(); + addDisposable.Dispose(); + removeDisposable.Dispose(); + } + + protected abstract void HandleRemovedComponent(in DefaultEcs.Entity entity, in TState state); + + protected abstract void HandleAddedComponent(in DefaultEcs.Entity entity, in TData data); + + [Update] + protected abstract void Update( + float elapsedTime, + in components.Parent parent, + ref TState state, + in TData data, + ref components.effect.RenderIndices indices); +} diff --git a/zzre/game/systems/effect/EffectCombiner.cs b/zzre/game/systems/effect/EffectCombiner.cs new file mode 100644 index 00000000..c73c6ad1 --- /dev/null +++ b/zzre/game/systems/effect/EffectCombiner.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using System.Numerics; +using DefaultEcs.Resource; +using DefaultEcs.System; +using zzio; + +namespace zzre.game.systems.effect; + +public partial class EffectCombiner : AEntitySetSystem +{ + private readonly IDisposable spawnEffectDisposable; + + public bool AddIndexAsComponent { get; set; } = false; // used for EffectEditor + + public EffectCombiner(ITagContainer diContainer) : base(diContainer.GetTag(), CreateEntityContainer, useBuffer: false) + { + spawnEffectDisposable = World.Subscribe(HandleSpawnEffect); + } + + public override void Dispose() + { + base.Dispose(); + spawnEffectDisposable.Dispose(); + } + + private void HandleSpawnEffect(in messages.SpawnEffectCombiner msg) + { + var entity = msg.AsEntity ?? World.CreateEntity(); + entity.Set(ManagedResource.Create(msg.EffectId)); + var effect = entity.Get(); + entity.Set(new components.effect.CombinerPlayback( + duration: effect.isLooping ? float.PositiveInfinity : effect.Duration, + depthTest: msg.DepthTest)); + entity.Set(new Location() + { + LocalPosition = msg.Position ?? effect.position, + LocalRotation = Quaternion.CreateFromRotationMatrix( + Matrix4x4.CreateLookTo(Vector3.Zero, effect.forwards, effect.upwards)) + }); + + foreach (var (part, index) in effect.parts.Indexed()) + { + var partEntity = World.CreateEntity(); + if (AddIndexAsComponent) + partEntity.Set(index); + partEntity.Set(components.RenderOrder.LateEffect); + partEntity.Set(components.Visibility.Visible); + partEntity.Set(new components.Parent(entity)); + switch(part) + { + case zzio.effect.parts.BeamStar beamStar: partEntity.Set(beamStar); break; + case zzio.effect.parts.ElectricBolt electricBolt: partEntity.Set(electricBolt); break; + case zzio.effect.parts.Models models: partEntity.Set(models); break; + case zzio.effect.parts.MovingPlanes movingPlanes: partEntity.Set(movingPlanes); break; + case zzio.effect.parts.ParticleBeam particleBeam: partEntity.Set(particleBeam); break; + case zzio.effect.parts.RandomPlanes randomPlanes: partEntity.Set(randomPlanes); break; + case zzio.effect.parts.Sound sound: partEntity.Set(sound); break; + case zzio.effect.parts.Sparks sparks: partEntity.Set(sparks); break; + default: + Console.WriteLine($"Warning: unsupported effect combiner part {part.Name}"); + break; + } + } + } + + [Update] + private void Update( + in DefaultEcs.Entity entity, + float elapsedTime, + ref components.effect.CombinerPlayback playback, + zzio.effect.EffectCombiner effect) + { + if (playback.IsFinished) + { + entity.Set(); + return; + } + + playback.CurTime += elapsedTime; + if (float.IsInfinity(playback.Duration)) + playback.CurTime %= effect.Duration; + } +} diff --git a/zzre/game/systems/effect/EffectRenderer.cs b/zzre/game/systems/effect/EffectRenderer.cs new file mode 100644 index 00000000..317ff9dd --- /dev/null +++ b/zzre/game/systems/effect/EffectRenderer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DefaultEcs.System; +using Veldrid; +using zzre.materials; +using zzre.rendering; + +namespace zzre.game.systems.effect; + +public class EffectRenderer : AEntityMultiMapSystem +{ + private readonly EffectMesh effectMesh; + private readonly RangeCollection indexRanges = new(); + private readonly components.RenderOrder responsibility; + + public EffectRenderer(ITagContainer diContainer, components.RenderOrder responsibility) : + base(diContainer.GetTag(), CreateEntityContainer, useBuffer: false) + { + effectMesh = diContainer.GetTag(); + this.responsibility = responsibility; + } + + private static DefaultEcs.EntityMultiMap CreateEntityContainer(object me, DefaultEcs.World world) => world + .GetEntities() + .With() + .With() + .With(static (in components.Visibility v) => v == components.Visibility.Visible) + .With((in components.RenderOrder o) => o == (me as EffectRenderer)!.responsibility) + .AsMultiMap(); + + protected override void PreUpdate(CommandList cl) + { + indexRanges.MaxRangeValue = effectMesh.IndexCapacity; + cl.PushDebugGroup("EffectRenderer"); + effectMesh.Update(cl); + cl.SetIndexBuffer(effectMesh.IndexBuffer, effectMesh.IndexFormat); + } + + protected override void Update(CommandList cl, in EffectMaterial key, ReadOnlySpan entities) + { + var renderIndicesComponents = World.GetComponents(); + foreach (var entity in entities) + { + var renderIndices = renderIndicesComponents[entity]; + indexRanges.Add(renderIndices.IndexRange); + } + } + + protected override void PostUpdate(CommandList cl, EffectMaterial material) + { + cl.PushDebugGroup($"{material.DebugName}"); + (material as IMaterial).Apply(cl); + material.ApplyAttributes(cl, effectMesh); + foreach (var range in indexRanges) + { + var (indexStart, indexCount) = range.GetOffsetAndLength(effectMesh.IndexCapacity); + cl.DrawIndexed( + indexStart: (uint)indexStart, + indexCount: (uint)indexCount, + instanceCount: 1, + vertexOffset: 0, + instanceStart: 0); + } + indexRanges.Clear(); + cl.PopDebugGroup(); + } + + protected override void PostUpdate(CommandList cl) + { + cl.PopDebugGroup(); + } +} diff --git a/zzre/game/systems/effect/MovingPlanes.cs b/zzre/game/systems/effect/MovingPlanes.cs new file mode 100644 index 00000000..5de457aa --- /dev/null +++ b/zzre/game/systems/effect/MovingPlanes.cs @@ -0,0 +1,150 @@ +using System; +using System.Numerics; +using DefaultEcs.Resource; +using zzre.materials; +using zzre.rendering.effectparts; +using zzio; + +namespace zzre.game.systems.effect; + +public sealed class MovingPlanes : BaseCombinerPart< + zzio.effect.parts.MovingPlanes, + components.effect.MovingPlanesState> +{ + public MovingPlanes(ITagContainer diContainer) : base(diContainer) { } + + protected override void HandleRemovedComponent(in DefaultEcs.Entity entity, in components.effect.MovingPlanesState state) + { + effectMesh.ReturnVertices(state.VertexRange); + effectMesh.ReturnIndices(state.IndexRange); + } + + protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzio.effect.parts.MovingPlanes data) + { + var playback = entity.Get().Entity.Get(); + var vertexRange = effectMesh.RentVertices(data.disableSecondPlane ? 4 : 8); + var indexRange = effectMesh.RentQuadIndices(vertexRange); + entity.Set(new components.effect.MovingPlanesState( + vertexRange, + indexRange, + EffectPartUtility.GetTileUV(data.tileW, data.tileH, data.tileId)) + { + PrevProgress = playback.CurProgress + }); + Reset(ref entity.Get(), data); + + var billboardMode = data.circlesAround || data.useDirection + ? EffectMaterial.BillboardMode.None + : EffectMaterial.BillboardMode.View; + entity.Set(ManagedResource.Create(new resources.EffectMaterialInfo( + playback.DepthTest, + billboardMode, + data.renderMode, + data.texName))); + entity.Set(new components.effect.RenderIndices(indexRange)); + } + + private void Reset(ref components.effect.MovingPlanesState state, zzio.effect.parts.MovingPlanes data) + { + state.CurPhase1 = data.phase1 / 1000f; + state.CurPhase2 = data.phase2 / 1000f; + state.CurRotation = 0f; + state.CurTexShift = 0f; + state.CurScale = 1f; + } + + protected override void Update( + float elapsedTime, + in components.Parent parent, + ref components.effect.MovingPlanesState state, + in zzio.effect.parts.MovingPlanes data, + ref components.effect.RenderIndices indices) + { + ref readonly var playback = ref parent.Entity.Get(); + float progressDelta = playback.CurProgress - state.PrevProgress; + state.PrevProgress = playback.CurProgress; + if (data.minProgress > playback.CurProgress) + { + indices.IndexRange = default; + return; + } + + indices.IndexRange = state.IndexRange; + var curColor = data.color.ToFColor(); + if (data.manualProgress) + { + state.CurRotation += progressDelta; + state.CurTexShift += elapsedTime; + float sizeDelta = (data.targetSize - data.width) / (100f - data.minProgress) * progressDelta; + AddScale(ref state, data, sizeDelta); + } + else if (state.CurPhase1 > 0f) + { + state.CurPhase1 -= elapsedTime; + state.CurRotation += elapsedTime; + state.CurTexShift += elapsedTime; + AddScale(ref state, data, elapsedTime * data.sizeModSpeed); + } + else if (state.CurPhase2 > 0f) + { + state.CurPhase2 -= elapsedTime; + state.CurRotation += elapsedTime; + state.CurTexShift += elapsedTime; + curColor *= Math.Clamp(state.CurPhase2 / (data.phase2 / 1000f), 0f, 1f); + AddScale(ref state, data, elapsedTime * data.sizeModSpeed); + } + else if (playback.IsLooping) + { + Reset(ref state, data); + Update(elapsedTime, parent, ref state, data, ref indices); + return; + } + else + return; + UpdateQuads(parent, ref state, data, curColor); + } + + private void AddScale( + ref components.effect.MovingPlanesState state, + zzio.effect.parts.MovingPlanes data, + float amount) + { + var shouldGrow = data.targetSize > data.width; + var curSize = new Vector2(data.width, data.height) * state.CurScale; + if ((shouldGrow && curSize.MaxComponent() < data.targetSize) || + (!shouldGrow && curSize.MinComponent() > data.targetSize)) + state.CurScale += amount; + } + + private void UpdateQuads( + in components.Parent parent, + ref components.effect.MovingPlanesState state, + zzio.effect.parts.MovingPlanes data, + IColor curColor) + { + var curAngle = state.CurRotation * data.rotation * MathEx.DegToRad; + var rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, curAngle); + var right = Vector3.Transform(Vector3.UnitX * data.width * Math.Max(0f, state.CurScale), rotation); + var up = Vector3.Transform(Vector3.UnitY * data.height * Math.Max(0f, state.CurScale), rotation); + var center = data.circlesAround + ? Vector3.Transform(Vector3.UnitY * data.yOffset, rotation) + : Vector3.Zero; + + var location = parent.Entity.Get(); + var applyCenter = data.circlesAround || data.useDirection; + if (applyCenter) + { + right = Vector3.TransformNormal(right, location.LocalToWorld); + up = Vector3.TransformNormal(up, location.LocalToWorld); + } + center = Vector3.Transform(center, location.LocalToWorld); + + var newTexCoords1 = EffectPartUtility.TexShift(state.TexCoords, 2 * state.CurTexShift, data.texShift); + effectMesh.SetQuad(state.VertexRange, 0, applyCenter, center, right, up, curColor, newTexCoords1); + if (!data.disableSecondPlane) + { + var newTexCoords2 = EffectPartUtility.TexShift(state.TexCoords, 2 * state.CurTexShift, -data.texShift); + effectMesh.SetQuad(state.VertexRange, 4, applyCenter, center, -right, up, curColor, newTexCoords2); + } + } +} diff --git a/zzre/game/systems/effect/RandomPlanes.cs b/zzre/game/systems/effect/RandomPlanes.cs new file mode 100644 index 00000000..3ae5167c --- /dev/null +++ b/zzre/game/systems/effect/RandomPlanes.cs @@ -0,0 +1,203 @@ +using System; +using System.Numerics; +using DefaultEcs.Resource; +using zzre.materials; +using zzre.rendering.effectparts; +using zzio; +using System.Buffers; + +namespace zzre.game.systems.effect; + +public sealed class RandomPlanes : BaseCombinerPart< + zzio.effect.parts.RandomPlanes, + components.effect.RandomPlanesState> +{ + private readonly MemoryPool planeMemoryPool; + private readonly Random random = Random.Shared; + + public RandomPlanes(ITagContainer diContainer) : base(diContainer) + { + planeMemoryPool = MemoryPool.Shared; + } + + public override void Dispose() + { + base.Dispose(); + planeMemoryPool.Dispose(); + } + + protected override void HandleRemovedComponent(in DefaultEcs.Entity entity, in components.effect.RandomPlanesState state) + { + effectMesh.ReturnVertices(state.VertexRange); + effectMesh.ReturnIndices(state.IndexRange); + state.PlaneMemoryOwner.Dispose(); + } + + protected override void HandleAddedComponent(in DefaultEcs.Entity entity, in zzio.effect.parts.RandomPlanes data) + { + var playback = entity.Get().Entity.Get(); + int maxPlaneCount = (int)(data.planeLife * data.spawnRate / 1000f); + var planeMemoryOwner = planeMemoryPool.Rent(maxPlaneCount); + var vertexRange = effectMesh.RentVertices(maxPlaneCount * 4); + var indexRange = effectMesh.RentQuadIndices(vertexRange); + entity.Set(new components.effect.RandomPlanesState( + planeMemoryOwner, + maxPlaneCount, + vertexRange, + indexRange)); + Reset(ref entity.Get(), data); + + var billboardMode = data.circlesAround + ? EffectMaterial.BillboardMode.None + : EffectMaterial.BillboardMode.View; + entity.Set(ManagedResource.Create(new resources.EffectMaterialInfo( + playback.DepthTest, + billboardMode, + data.renderMode, + data.texName))); + entity.Set(new components.effect.RenderIndices(default)); + } + + private void Reset(ref components.effect.RandomPlanesState state, zzio.effect.parts.RandomPlanes data) + { + state.CurPhase1 = data.phase1 / 1000f; + state.CurPhase2 = data.phase2 / 1000f; + state.CurTexShift = 0f; + if (state.CurPhase1 <= 0f) + state.CurPhase1 = 1f; + } + + protected override void Update( + float elapsedTime, + in components.Parent parent, + ref components.effect.RandomPlanesState state, + in zzio.effect.parts.RandomPlanes data, + ref components.effect.RenderIndices indices) + { + foreach (ref var plane in state.Planes.Span) + UpdatePlane(elapsedTime, ref state, data, ref plane); + + ref readonly var playback = ref parent.Entity.Get(); + if (!data.ignorePhases && playback.CurProgress > data.minProgress) + { + if (state.CurPhase1 > 0f) + state.CurPhase1 -= elapsedTime; + else if (state.CurPhase2 > 0f) + state.CurPhase2 -= elapsedTime; + else if (playback.IsLooping) + { + Reset(ref state, data); + Update(elapsedTime, parent, ref state, data, ref indices); + return; + } + } + + if (data.ignorePhases || state.CurPhase1 > 0f || state.CurPhase2 > 0f) + SpawnPlanes(elapsedTime, ref state, data); + UpdateQuads(parent, ref state, data, ref indices); + } + + private void UpdatePlane( + float elapsedTime, + ref components.effect.RandomPlanesState state, + in zzio.effect.parts.RandomPlanes data, + ref components.effect.RandomPlanesState.RandomPlane plane) + { + if (plane.Life <= 0f) + return; + + plane.Life -= elapsedTime; + var normalizedLife = Math.Clamp(plane.Life / (data.planeLife / 1000f), 0f, 1f); + plane.CurColor = plane.StartColor * normalizedLife; + plane.Rotation += plane.RotationSpeed * data.rotationSpeedMult * elapsedTime; + state.CurTexShift += elapsedTime; // one texshift for all planes, that is correct + + var scaleDelta = plane.ScaleSpeed * data.scaleSpeedMult * elapsedTime; + var shouldGrow = data.targetSize > data.amplPosX; + var curSize = new Vector2(data.width, data.height) * plane.Scale; + if (shouldGrow && curSize.MaxComponent() < data.targetSize) + plane.Scale += scaleDelta; + if (!shouldGrow && curSize.MinComponent() > data.targetSize) + plane.Scale -= scaleDelta; + + plane.TileProgress += elapsedTime; + if (plane.TileProgress >= data.tileDuration / 1000f) + { + plane.TileProgress = 0f; + plane.TileI = (plane.TileI + 1) % data.tileCount; + } + } + + private void SpawnPlanes( + float elapsedTime, + ref components.effect.RandomPlanesState state, + in zzio.effect.parts.RandomPlanes data) + { + // yes, spawnRate is integer and planes per second + state.SpawnProgress += data.spawnRate * elapsedTime; + int spawnCount = (int)state.SpawnProgress; + state.SpawnProgress -= spawnCount; + + foreach (ref var plane in state.Planes.Span) + { + if (spawnCount == 0) + break; + if (plane.Life > 0f) + continue; + spawnCount--; + + plane.Life = data.planeLife / 1000f; + plane.Scale = 1f; + plane.Rotation = 0f; + plane.ScaleSpeed = random.Next(data.minScaleSpeed, data.maxScaleSpeed) * random.NextSign(); + plane.RotationSpeed = random.Next(data.minScaleSpeed, data.maxScaleSpeed) * random.NextSign(); + plane.TileI = random.Next(data.tileCount); + plane.TileProgress = random.Next(0f, data.tileDuration / 1000f); + plane.StartColor = plane.CurColor = + new Vector4(random.InPositiveCube() * (data.amplColor / 255f), 0f) + + data.color.ToFColor().ToNumerics(); + + var amplPos = new Vector2(data.amplPosX, data.amplPosY); + var minPos = new Vector2(data.minPosX, data.yOffset); + plane.Pos = new Vector3(random.InSquare() * amplPos / 2 + minPos, 0f); + } + } + + private void UpdateQuads( + in components.Parent parent, + ref components.effect.RandomPlanesState state, + zzio.effect.parts.RandomPlanes data, + ref components.effect.RenderIndices indices) + { + var applyCenter = data.circlesAround; + var planes = state.Planes.Span; + int alivePlanes = 0; + foreach (ref readonly var plane in planes) + { + if (plane.Life <= 0f) + continue; + var rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, plane.Rotation * MathEx.DegToRad); + var right = Vector3.Transform(Vector3.UnitX * plane.Scale * data.width, rotation); + var up = Vector3.Transform(Vector3.UnitY * plane.Scale * data.height, rotation); + var center = data.circlesAround + ? Vector3.Transform(plane.Pos, rotation) + : plane.Pos; + var color = plane.CurColor.ToFColor(); + var texCoords = EffectPartUtility.GetTileUV(data.tileW, data.tileH, data.tileId + plane.TileI); + texCoords = EffectPartUtility.TexShift(texCoords, state.CurTexShift, data.texShift); + + var location = parent.Entity.Get(); + if (applyCenter) + { + right = Vector3.TransformNormal(right, location.LocalToWorld); + up = Vector3.TransformNormal(up, location.LocalToWorld); + } + center = Vector3.Transform(center, location.LocalToWorld); + + effectMesh.SetQuad(state.VertexRange, alivePlanes * 4, applyCenter, center, right, up, color, texCoords); + alivePlanes++; + } + + indices = new(state.IndexRange.Sub(0..(alivePlanes * 6), effectMesh.IndexCapacity)); + } +} diff --git a/zzre/game/systems/gameflow/UnlockDoor.cs b/zzre/game/systems/gameflow/UnlockDoor.cs index c4a33de6..77c26db1 100644 --- a/zzre/game/systems/gameflow/UnlockDoor.cs +++ b/zzre/game/systems/gameflow/UnlockDoor.cs @@ -70,8 +70,9 @@ private void HandleMessage(in messages.UnlockDoor msg) timer = 0f; state = State.ShowNotification; var modelId = lockEntity.Get().ModelId; + var location = lockEntity.Get(); World.Publish(new GSModRemoveModel(modelId)); - // TODO: Spawn effect 4002 on opening lock + World.Publish(new messages.SpawnEffectCombiner(4002, Position: location.GlobalPosition)); // TODO: Play soundeffect on opening lock } break; diff --git a/zzre/game/systems/model/ModelRenderer.cs b/zzre/game/systems/model/ModelRenderer.cs index 7caaa595..a0a3f0b0 100644 --- a/zzre/game/systems/model/ModelRenderer.cs +++ b/zzre/game/systems/model/ModelRenderer.cs @@ -28,11 +28,13 @@ public ClumpCount(ClumpMesh clump, IReadOnlyList materials, uint } private readonly ITagContainer diContainer; + private readonly IDisposable sceneChangingSubscription; private readonly IDisposable sceneLoadedSubscription; private readonly components.RenderOrder responsibility; private readonly ModelInstanceBuffer instanceBuffer; private readonly List clumpCounts = new(); + private ModelInstanceBuffer.InstanceArena instanceArena = null!; public ModelRenderer(ITagContainer diContainer, components.RenderOrder responsibility) : base(diContainer.GetTag(), CreateEntityContainer, useBuffer: true) @@ -40,20 +42,28 @@ public ModelRenderer(ITagContainer diContainer, components.RenderOrder responsib this.diContainer = diContainer; this.responsibility = responsibility; instanceBuffer = new(diContainer); + sceneChangingSubscription = World.Subscribe(HandleSceneChanging); sceneLoadedSubscription = World.Subscribe(HandleSceneLoaded); } public override void Dispose() { base.Dispose(); + sceneChangingSubscription.Dispose(); sceneLoadedSubscription.Dispose(); instanceBuffer.Dispose(); } + private void HandleSceneChanging(in messages.SceneChanging message) + { + instanceArena?.Dispose(); + } + private void HandleSceneLoaded(in messages.SceneLoaded message) { clumpCounts.EnsureCapacity(MultiMap.Keys.Count()); - instanceBuffer.Reserve(MultiMap.Keys.Sum(MultiMap.Count), additive: false); + instanceBuffer.Clear(); + instanceArena = instanceBuffer.RentVertices(MultiMap.Keys.Sum(MultiMap.Count) + 1); } [WithPredicate] @@ -73,7 +83,7 @@ private void Update( else clumpCounts[^1] = clumpCounts[^1].Increment(); - instanceBuffer.Add(new() + instanceArena.Add(new() { tint = materialInfo.Color, world = location.LocalToWorld, @@ -83,7 +93,7 @@ private void Update( protected override void PostUpdate(CommandList cl) { - if (instanceBuffer.VertexCount == 0) + if (instanceArena.InstanceCount == 0) return; cl.PushDebugGroup($"{nameof(ModelRenderer)} {responsibility}"); @@ -122,6 +132,6 @@ protected override void PostUpdate(CommandList cl) for (int i = 0; i < clumpCounts.Count; i++) clumpCounts[i] = default; // remove reference to ClumpBuffer and materials clumpCounts.Clear(); - instanceBuffer.Clear(); + instanceArena.Reset(); } } diff --git a/zzre/game/systems/ui/Batcher.cs b/zzre/game/systems/ui/Batcher.cs index c384cd1b..bfb34c01 100644 --- a/zzre/game/systems/ui/Batcher.cs +++ b/zzre/game/systems/ui/Batcher.cs @@ -20,7 +20,8 @@ public record struct Batch(UIMaterial Material, uint Instances); private readonly Texture emptyTexture; private UIMaterial? lastMaterial; - private uint nextInstanceCount; + private uint nextBatchSize; + private int nextInstanceIndex, maxInstanceCount; public Batcher(ITagContainer diContainer) : base(diContainer.GetTag(), CreateEntityContainer, useBuffer: false) { @@ -49,7 +50,7 @@ public override void Dispose() protected override void PreUpdate(CommandList _) { lastMaterial = null; - nextInstanceCount = 0; + nextBatchSize = 0; batches.Clear(); var tiles = World.GetComponents(); @@ -57,7 +58,8 @@ protected override void PreUpdate(CommandList _) foreach (var entity in SortedSet.GetEntities()) totalRects += tiles[entity].Length; instanceBuffer.Clear(); - instanceBuffer.Reserve(totalRects, additive: false); + maxInstanceCount = instanceBuffer.RentVertices(totalRects).GetLength(instanceBuffer.VertexCapacity); + nextInstanceIndex = 0; } [Update] @@ -83,7 +85,7 @@ private void Update( uvRectangle = tileSheet[tile.TileId]; } - instanceBuffer.Add(new() + AddInstance(new() { pos = offset.Calc(tile.Rect.Min, ui.LogicalScreen), size = tile.Rect.Size, @@ -92,10 +94,23 @@ private void Update( uvPos = uvRectangle.Min, uvSize = uvRectangle.Size }); - nextInstanceCount++; + nextBatchSize++; } } + private void AddInstance(UIInstance i) + { + if (nextInstanceIndex >= maxInstanceCount) + throw new IndexOutOfRangeException("Batcher tried to add too many instances"); + instanceBuffer.AttrPos[nextInstanceIndex] = i.pos; + instanceBuffer.AttrSize[nextInstanceIndex] = i.size; + instanceBuffer.AttrColor[nextInstanceIndex] = i.color; + instanceBuffer.AttrTexWeight[nextInstanceIndex] = i.textureWeight; + instanceBuffer.AttrUVPos[nextInstanceIndex] = i.uvPos; + instanceBuffer.AttrUVSize[nextInstanceIndex] = i.uvSize; + nextInstanceIndex++; + } + protected override void PostUpdate(CommandList cl) { FinishBatch(); @@ -122,10 +137,10 @@ protected override void PostUpdate(CommandList cl) private void FinishBatch() { - if (lastMaterial == null || nextInstanceCount == 0) + if (lastMaterial == null || nextBatchSize == 0) return; - batches.Add(new(lastMaterial, nextInstanceCount)); + batches.Add(new(lastMaterial, nextBatchSize)); lastMaterial = null; - nextInstanceCount = 0; + nextBatchSize = 0; } } diff --git a/zzre/imgui/OrbitControlsTag.cs b/zzre/imgui/OrbitControlsTag.cs index 2abdf9b7..a3092f2f 100644 --- a/zzre/imgui/OrbitControlsTag.cs +++ b/zzre/imgui/OrbitControlsTag.cs @@ -10,7 +10,7 @@ public class OrbitControlsTag : BaseDisposable { private readonly FramebufferArea fbArea; private readonly MouseEventArea mouseArea; - private readonly LocationBuffer locationBuffer; + private readonly LocationBuffer? locationBuffer; private readonly Location target; private readonly Location rotationLoc = new(); private readonly DeviceBufferRange rotationLocRange; @@ -45,8 +45,11 @@ public OrbitControlsTag(Window window, Location target, ITagContainer diContaine mouseArea = window.GetTag(); mouseArea.OnDrag += HandleDrag; mouseArea.OnScroll += HandleScroll; - locationBuffer = diContainer.GetTag(); - rotationLocRange = locationBuffer.Add(rotationLoc); + if (diContainer.TryGetTag(out LocationBuffer locationBuffer)) + { + this.locationBuffer = locationBuffer; + rotationLocRange = locationBuffer.Add(rotationLoc); + } target.Parent = rotationLoc; ResetView(); @@ -55,7 +58,7 @@ public OrbitControlsTag(Window window, Location target, ITagContainer diContaine protected override void DisposeManaged() { base.DisposeManaged(); - locationBuffer.Remove(rotationLocRange); + locationBuffer?.Remove(rotationLocRange); } private void HandleDrag(MouseButton button, Vector2 delta) diff --git a/zzre/materials/DebugMaterial.cs b/zzre/materials/DebugMaterial.cs index 1219f985..f7f83476 100644 --- a/zzre/materials/DebugMaterial.cs +++ b/zzre/materials/DebugMaterial.cs @@ -1,6 +1,6 @@ -using System.Numerics; -using System.Runtime.InteropServices; -using Veldrid; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; using zzio; using zzre.rendering; @@ -63,19 +63,12 @@ public DebugMaterial(ITagContainer diContainer) : base(diContainer, "debug") public class DebugDynamicMesh : DynamicMesh { - private readonly Attribute attrPos; - private readonly Attribute attrColor; + public Attribute AttrPos { get; } + public Attribute AttrColor { get; } public DebugDynamicMesh(ITagContainer diContainer, bool dynamic = true) : base(diContainer, dynamic) { - attrPos = AddAttribute("inPos"); - attrColor = AddAttribute("inColor"); - } - - public void Add(ColoredVertex v) - { - var index = Add(1); - attrPos[index] = v.pos; - attrColor[index] = v.color; + AttrPos = AddAttribute("inPos"); + AttrColor = AddAttribute("inColor"); } } diff --git a/zzre/materials/EffectMaterial.cs b/zzre/materials/EffectMaterial.cs index b2915da5..5d9cdb10 100644 --- a/zzre/materials/EffectMaterial.cs +++ b/zzre/materials/EffectMaterial.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Veldrid; @@ -8,6 +10,117 @@ namespace zzre.materials; +[StructLayout(LayoutKind.Sequential)] +public struct EffectFactors +{ + public float alphaReference; + + public static readonly EffectFactors Default = new() + { + alphaReference = 0.03f + }; +} + +public class EffectMaterial : MlangMaterial +{ + public enum BillboardMode : uint + { + None, + View, + Spark + } + + public enum BlendMode : uint + { + Additive, + AdditiveAlpha, + Alpha + } + + public bool DepthTest { set => SetOption(nameof(DepthTest), value); } + public BillboardMode Billboard { set => SetOption(nameof(Billboard), (uint)value); } + public BlendMode Blend { set => SetOption(nameof(Blend), (uint)value); } + + public TextureBinding Texture { get; } + public SamplerBinding Sampler { get; } + public UniformBinding Projection { get; } + public UniformBinding View { get; } + public UniformBinding Factors { get; } + + public EffectMaterial(ITagContainer diContainer) : base(diContainer, "effect") + { + DepthTest = true; + AddBinding("mainTexture", Texture = new(this)); + AddBinding("mainSampler", Sampler = new(this)); + AddBinding("projection", Projection = new(this)); + AddBinding("view", View = new(this)); + AddBinding("factors", Factors = new(this)); + } +} + +public class EffectMesh : DynamicMesh +{ + public Attribute Pos { get; } + public Attribute UV { get; } + public Attribute Color { get; } + public Attribute Center { get; } + public Attribute Direction { get; } + + public EffectMesh(ITagContainer diContainer, int initialVertices, int initialIndices) : base(diContainer, dynamic: true) + { + Pos = AddAttribute("inVertexPos"); + UV = AddAttribute("inUV"); + Color = AddAttribute("inColor"); + Center = AddAttribute("inCenterPos"); + Direction = AddAttribute("inDirection"); + Preallocate(initialVertices, initialIndices); + } + + public Range RentPatternIndices(Range vertexRange, IReadOnlyList pattern) + { + var (vertexOffset, vertexCount) = vertexRange.GetOffsetAndLength(VertexCapacity); + var verticesPerPrimitive = pattern.Max() + 1; + if (vertexCount % verticesPerPrimitive != 0) + throw new ArgumentException("Vertex range does not align with pattern"); + var primitiveCount = vertexCount / verticesPerPrimitive; + var indexCount = primitiveCount * pattern.Count; + var indexRange = RentIndices(indexCount); + StaticMesh.GeneratePatternIndices( + WriteIndices(indexRange), + pattern, + primitiveCount, + verticesPerPrimitive, + vertexOffset); + return indexRange; + } + + private static readonly ushort[] QuadIndexPattern = [0, 2, 1, 0, 3, 2]; + public Range RentQuadIndices(Range vertexRange) => + RentPatternIndices(vertexRange, QuadIndexPattern); + + public void SetQuad(Range vertexRange, int offset, bool applyCenter, Vector3 center, Vector3 right, Vector3 up, IColor color, Rect texCoords) + { + var (vertexOffset, vertexCount) = vertexRange.GetOffsetAndLength(VertexCapacity); + if (vertexCount < offset + 4) + throw new ArgumentException("Quad does not fit given vertex range with offset"); + vertexOffset += offset; + var centerToApply = applyCenter ? center : Vector3.Zero; + var attrPos = Pos.Write(vertexOffset, 4); + attrPos[0] = centerToApply - right - up; + attrPos[1] = centerToApply - right + up; + attrPos[2] = centerToApply + right + up; + attrPos[3] = centerToApply + right - up; + var attrUV = UV.Write(vertexOffset, 4); + attrUV[0] = new(texCoords.Min.X, texCoords.Min.Y); + attrUV[1] = new(texCoords.Min.X, texCoords.Max.Y); + attrUV[2] = new(texCoords.Max.X, texCoords.Max.Y); + attrUV[3] = new(texCoords.Max.X, texCoords.Min.Y); + Color.Write(vertexOffset, 4).Fill(color); + if (!applyCenter) + Center.Write(vertexOffset, 4).Fill(center); + } +} + [StructLayout(LayoutKind.Sequential)] public struct EffectVertex { @@ -39,9 +152,10 @@ public struct EffectMaterialUniforms }; } -public abstract class EffectMaterial : BaseMaterial, IStandardTransformMaterial + +public abstract class EffectMaterialLEGACY : BaseMaterial, IStandardTransformMaterial { - public static EffectMaterial CreateFor(EffectPartRenderMode mode, ITagContainer diContainer) => mode switch + public static EffectMaterialLEGACY CreateFor(EffectPartRenderMode mode, ITagContainer diContainer) => mode switch { EffectPartRenderMode.NormalBlend => new EffectBlendMaterial(diContainer), EffectPartRenderMode.Additive => new EffectAdditiveMaterial(diContainer), @@ -56,7 +170,7 @@ public abstract class EffectMaterial : BaseMaterial, IStandardTransformMaterial public UniformBinding World { get; } public UniformBinding Uniforms { get; } - protected EffectMaterial(ITagContainer diContainer, IBuiltPipeline pipeline) : base(diContainer.GetTag(), pipeline) + protected EffectMaterialLEGACY(ITagContainer diContainer, IBuiltPipeline pipeline) : base(diContainer.GetTag(), pipeline) { Configure() .Add(MainTexture = new TextureBinding(this)) @@ -86,7 +200,7 @@ protected static IPipelineBuilder BuildBasePipeline(IPipelineBuilder builder) => .With(FrontFace.CounterClockwise); } -public class EffectBlendMaterial : EffectMaterial +public class EffectBlendMaterial : EffectMaterialLEGACY { public EffectBlendMaterial(ITagContainer diContainer) : base(diContainer, GetPipeline(diContainer)) { } @@ -97,7 +211,7 @@ private static IBuiltPipeline GetPipeline(ITagContainer diContainer) => Pipeline } -public class EffectAdditiveMaterial : EffectMaterial +public class EffectAdditiveMaterial : EffectMaterialLEGACY { public EffectAdditiveMaterial(ITagContainer diContainer) : base(diContainer, GetPipeline(diContainer)) { } @@ -107,7 +221,7 @@ private static IBuiltPipeline GetPipeline(ITagContainer diContainer) => Pipeline .Build()); } -public class EffectAdditiveAlphaMaterial : EffectMaterial +public class EffectAdditiveAlphaMaterial : EffectMaterialLEGACY { public EffectAdditiveAlphaMaterial(ITagContainer diContainer) : base(diContainer, GetPipeline(diContainer)) { } diff --git a/zzre/materials/ModelMaterial.cs b/zzre/materials/ModelMaterial.cs index 2122dda5..13459614 100644 --- a/zzre/materials/ModelMaterial.cs +++ b/zzre/materials/ModelMaterial.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System; +using System.Numerics; using System.Runtime.InteropServices; using Veldrid; using zzio; @@ -70,25 +71,63 @@ public ModelMaterial(ITagContainer diContainer) : base(diContainer, "model") } } -public class ModelInstanceBuffer : DynamicMesh +public sealed class ModelInstanceBuffer : DynamicMesh { private readonly Attribute attrWorld; private readonly Attribute attrTexShift; private readonly Attribute attrTint; - public ModelInstanceBuffer(ITagContainer diContainer, bool dynamic = true) : base(diContainer, dynamic) + public ModelInstanceBuffer(ITagContainer diContainer, + bool dynamic = true, + string name = nameof(ModelInstanceBuffer)) + : base(diContainer, dynamic, name) { attrWorld = AddAttribute("world"); attrTexShift = AddAttribute("inTexShift"); attrTint = AddAttribute("inTint"); } - public void Add(ModelInstance i) + public class InstanceArena : IDisposable { - var index = Add(1); - attrWorld[index] = i.world; - attrTexShift[index] = i.texShift; - attrTint[index] = i.tint; + private readonly ModelInstanceBuffer buffer; + private readonly int startIndex, endIndex; + private int nextIndex; + + public uint InstanceStart => (uint)startIndex; + public uint InstanceCount => (uint)(nextIndex - startIndex); + + public InstanceArena(ModelInstanceBuffer buffer, Range range) + { + this.buffer = buffer; + (startIndex, endIndex) = range.GetOffsetAndLength(buffer.VertexCapacity); + endIndex += startIndex; + } + + public void Reset() => nextIndex = startIndex; + + public void Add(ModelInstance i) + { + if (nextIndex < 0) + throw new ObjectDisposedException(nameof(InstanceArena)); + if (nextIndex >= endIndex) + throw new InvalidOperationException("Instance range is full"); + buffer.attrWorld[nextIndex] = i.world; + buffer.attrTexShift[nextIndex] = i.texShift; + buffer.attrTint[nextIndex] = i.tint; + nextIndex++; + } + + public void Dispose() + { + if (nextIndex >= 0) + { + nextIndex = -1; + buffer.ReturnVertices(startIndex..endIndex); + } + } } + + public new InstanceArena RentVertices(int request, bool fast = false) => + new InstanceArena(this, base.RentVertices(request, fast)); } diff --git a/zzre/materials/UIMaterial.cs b/zzre/materials/UIMaterial.cs index 7dab0e0b..bbfcce9d 100644 --- a/zzre/materials/UIMaterial.cs +++ b/zzre/materials/UIMaterial.cs @@ -1,4 +1,7 @@ -using System.Numerics; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Numerics; using System.Runtime.InteropServices; using Veldrid; using zzio; @@ -55,55 +58,57 @@ public UIMaterial(ITagContainer diContainer) : base(diContainer, "ui") } } -public class UIInstanceBufferBase : DynamicMesh where TPosition : unmanaged +public abstract class UIInstanceBufferBase : DynamicMesh where TPosition : unmanaged { - protected readonly Attribute attrPos; - protected readonly Attribute - attrSize, - attrUVPos, - attrUVSize; - protected readonly Attribute attrTexWeight; - protected readonly Attribute attrColor; + public readonly Attribute AttrPos; + public readonly Attribute + AttrSize, + AttrUVPos, + AttrUVSize; + public readonly Attribute AttrTexWeight; + public readonly Attribute AttrColor; - public UIInstanceBufferBase(ITagContainer diContainer, bool dynamic = true) : base(diContainer, dynamic) + public UIInstanceBufferBase(ITagContainer diContainer, + bool dynamic = true, + string name = nameof(UIInstanceBuffer)) + : base(diContainer, dynamic, name) { - attrPos = AddAttribute("inPos"); - attrSize = AddAttribute("inSize"); - attrUVPos = AddAttribute("inUVPos"); - attrUVSize = AddAttribute("inUVSize"); - attrColor = AddAttribute("inColor"); - attrTexWeight = AddAttribute("inTexWeight"); + AttrPos = AddAttribute("inPos"); + AttrSize = AddAttribute("inSize"); + AttrUVPos = AddAttribute("inUVPos"); + AttrUVSize = AddAttribute("inUVSize"); + AttrColor = AddAttribute("inColor"); + AttrTexWeight = AddAttribute("inTexWeight"); } } -public class UIInstanceBuffer : UIInstanceBufferBase +public sealed class UIInstanceBuffer : UIInstanceBufferBase { - public UIInstanceBuffer(ITagContainer diContainer, bool dynamic = true) : base(diContainer, dynamic) { } - - public void Add(UIInstance i) - { - var index = Add(1); - attrPos[index] = i.pos; - attrSize[index] = i.size; - attrUVPos[index] = i.uvPos; - attrUVSize[index] = i.uvSize; - attrTexWeight[index] = i.textureWeight; - attrColor[index] = i.color; - } + public UIInstanceBuffer(ITagContainer diContainer, + bool dynamic = true, + string name = nameof(UIInstanceBuffer)) + : base(diContainer, dynamic, name) { } } -public class DebugIconInstanceBuffer : UIInstanceBufferBase +public sealed class DebugIconInstanceBuffer : UIInstanceBufferBase { - public DebugIconInstanceBuffer(ITagContainer diContainer, bool dynamic = true) : base(diContainer, dynamic) { } + public DebugIconInstanceBuffer(ITagContainer diContainer, + bool dynamic = true, + string name = nameof(DebugIconInstanceBuffer)) + : base(diContainer, dynamic, name) { } - public void Add(DebugIcon i) + public void AddRange(IReadOnlyCollection instances) { - var index = Add(1); - attrPos[index] = i.pos; - attrSize[index] = i.size; - attrUVPos[index] = i.uvPos; - attrUVSize[index] = i.uvSize; - attrTexWeight[index] = i.textureWeight; - attrColor[index] = i.color; + var index = RentVertices(instances.Count).Start.Value; + foreach (var i in instances) + { + AttrPos[index] = i.pos; + AttrSize[index] = i.size; + AttrUVPos[index] = i.uvPos; + AttrUVSize[index] = i.uvSize; + AttrTexWeight[index] = i.textureWeight; + AttrColor[index] = i.color; + index++; + } } } diff --git a/zzre/rendering/CachedAssetLoader.cs b/zzre/rendering/CachedAssetLoader.cs index ea354c62..5200eb2a 100644 --- a/zzre/rendering/CachedAssetLoader.cs +++ b/zzre/rendering/CachedAssetLoader.cs @@ -33,7 +33,10 @@ public virtual void Clear() public virtual bool TryLoad(IResource resource, [NotNullWhen(true)] out TAsset? asset) { if (cache.TryGetValue(resource.Path, out asset)) + { + (parent as IAssetLoaderValidation)?.ValidateAsset(asset); return true; + } if (parent.TryLoad(resource, out asset)) { cache.Add(resource.Path, asset); diff --git a/zzre/rendering/IAssetLoader.cs b/zzre/rendering/IAssetLoader.cs index c12c0fc6..b0541fca 100644 --- a/zzre/rendering/IAssetLoader.cs +++ b/zzre/rendering/IAssetLoader.cs @@ -28,3 +28,8 @@ TAsset Load(FilePath path) return Load(resource); } } + +public interface IAssetLoaderValidation where TAsset : class, IDisposable +{ + void ValidateAsset(TAsset asset); +} diff --git a/zzre/rendering/TextureAssetLoader.cs b/zzre/rendering/TextureAssetLoader.cs index 4b5bc772..fbb3b0f6 100644 --- a/zzre/rendering/TextureAssetLoader.cs +++ b/zzre/rendering/TextureAssetLoader.cs @@ -8,7 +8,7 @@ namespace zzre; -public class TextureAssetLoader : IAssetLoader +public class TextureAssetLoader : IAssetLoader, IAssetLoaderValidation { public ITagContainer DIContainer { get; } private readonly IResourcePool resourcePool; @@ -119,4 +119,10 @@ public void Clear() { } Pfim.ImageFormat.Rgba32 => PixelFormat.B8_G8_R8_A8_UNorm, _ => null }; + + public void ValidateAsset(Texture asset) + { + if (asset.IsDisposed) + throw new InvalidOperationException($"Texture {asset.Name} was disposed"); + } } diff --git a/zzre/rendering/effectparts/BeamStarRenderer.cs b/zzre/rendering/effectparts/BeamStarRenderer.cs index 335e8248..88bd066c 100644 --- a/zzre/rendering/effectparts/BeamStarRenderer.cs +++ b/zzre/rendering/effectparts/BeamStarRenderer.cs @@ -10,7 +10,7 @@ namespace zzre.rendering.effectparts; public class BeamStarRenderer : ListDisposable, IEffectPartBeamRenderer { private readonly IQuadMeshBuffer quadMeshBuffer; - private readonly EffectMaterial material; + private readonly EffectMaterialLEGACY material; private readonly BeamStar data; private readonly Range quadRange; @@ -35,7 +35,7 @@ public BeamStarRenderer(ITagContainer diContainer, DeviceBufferRange locationRan var textureLoader = diContainer.GetTag>(); var camera = diContainer.GetTag(); quadMeshBuffer = diContainer.GetTag>(); - material = EffectMaterial.CreateFor(data.renderMode, diContainer); + material = EffectMaterialLEGACY.CreateFor(data.renderMode, diContainer); material.LinkTransformsTo(camera); material.World.BufferRange = locationRange; material.Uniforms.Value = EffectMaterialUniforms.Default; diff --git a/zzre/rendering/effectparts/MovingPlanesRenderer.cs b/zzre/rendering/effectparts/MovingPlanesRenderer.cs index c7d8c608..2706a993 100644 --- a/zzre/rendering/effectparts/MovingPlanesRenderer.cs +++ b/zzre/rendering/effectparts/MovingPlanesRenderer.cs @@ -10,7 +10,7 @@ namespace zzre.rendering.effectparts; public class MovingPlanesRenderer : ListDisposable, IEffectPartRenderer { private readonly IQuadMeshBuffer quadMeshBuffer; - private readonly EffectMaterial material; + private readonly EffectMaterialLEGACY material; private readonly MovingPlanes data; private readonly Range quadRange; private readonly Rect texCoords; @@ -30,7 +30,7 @@ public MovingPlanesRenderer(ITagContainer diContainer, DeviceBufferRange locatio var textureLoader = diContainer.GetTag>(); var camera = diContainer.GetTag(); quadMeshBuffer = diContainer.GetTag>(); - material = EffectMaterial.CreateFor(data.renderMode, diContainer); + material = EffectMaterialLEGACY.CreateFor(data.renderMode, diContainer); material.LinkTransformsTo(camera); material.World.BufferRange = locationRange; material.Uniforms.Value = EffectMaterialUniforms.Default; diff --git a/zzre/rendering/effectparts/ParticleBehaviourModel.cs b/zzre/rendering/effectparts/ParticleBehaviourModel.cs index e938e087..a68807f2 100644 --- a/zzre/rendering/effectparts/ParticleBehaviourModel.cs +++ b/zzre/rendering/effectparts/ParticleBehaviourModel.cs @@ -44,6 +44,7 @@ public void Spawn(Random random, Vector3 pos, in ParticleEmitter data) private readonly ParticleEmitter data; private readonly Model[] models; private readonly ModelInstanceBuffer instanceBuffer; + private readonly ModelInstanceBuffer.InstanceArena instanceArena; public float SpawnRate { get; set; } public int CurrentParticles => instanceBuffer.VertexCount; @@ -96,7 +97,7 @@ public ParticleBehaviourModel(ITagContainer diContainer, Location location, Part models = new Model[(int)(data.spawnRate * data.life.value)]; instanceBuffer = new(diContainer); - instanceBuffer.Reserve(models.Length); + instanceArena = instanceBuffer.RentVertices(models.Length); AddDisposable(instanceBuffer); } @@ -132,12 +133,12 @@ public void Render(CommandList cl) if (areInstancesDirty) { areInstancesDirty = false; - instanceBuffer.Clear(); + instanceArena.Reset(); foreach (ref readonly var model in models.AsSpan()) { if (model.basic.life < 0f) continue; - instanceBuffer.Add(new() + instanceArena.Add(new() { world = Matrix4x4.CreateFromAxisAngle(model.rotationAxis, model.rotation * MathF.PI / 180f) * diff --git a/zzre/rendering/effectparts/ParticleBehaviourParticle.cs b/zzre/rendering/effectparts/ParticleBehaviourParticle.cs index dcd16ee3..b9789dc9 100644 --- a/zzre/rendering/effectparts/ParticleBehaviourParticle.cs +++ b/zzre/rendering/effectparts/ParticleBehaviourParticle.cs @@ -46,7 +46,7 @@ public void Spawn(Random random, Vector3 pos, in ParticleEmitter data) private readonly Random random = new(); private readonly Location location; private readonly IQuadMeshBuffer quadMeshBuffer; - private readonly EffectMaterial material; + private readonly EffectMaterialLEGACY material; private readonly ParticleEmitter data; private readonly Range quadRange; private readonly Rect[] tileTexCoords; @@ -67,7 +67,7 @@ public ParticleBehaviourParticle(ITagContainer diContainer, Location location, P var textureLoader = diContainer.GetTag>(); var camera = diContainer.GetTag(); quadMeshBuffer = diContainer.GetTag>(); - material = EffectMaterial.CreateFor(data.renderMode, diContainer); + material = EffectMaterialLEGACY.CreateFor(data.renderMode, diContainer); material.LinkTransformsTo(camera); material.World.Value = Matrix4x4.Identity; // particles are spawned in world-space material.Uniforms.Value = EffectMaterialUniforms.Default; diff --git a/zzre/rendering/effectparts/RandomPlanesRenderer.cs b/zzre/rendering/effectparts/RandomPlanesRenderer.cs index 4b0e6cbe..d85a3d3c 100644 --- a/zzre/rendering/effectparts/RandomPlanesRenderer.cs +++ b/zzre/rendering/effectparts/RandomPlanesRenderer.cs @@ -26,7 +26,7 @@ private struct RandomPlane private readonly Random random = new(); private readonly IQuadMeshBuffer quadMeshBuffer; - private readonly EffectMaterial material; + private readonly EffectMaterialLEGACY material; private readonly RandomPlanes data; private readonly Range quadRange; private readonly Rect[] tileTexCoords; @@ -45,7 +45,7 @@ public RandomPlanesRenderer(ITagContainer diContainer, DeviceBufferRange locatio var textureLoader = diContainer.GetTag>(); var camera = diContainer.GetTag(); quadMeshBuffer = diContainer.GetTag>(); - material = EffectMaterial.CreateFor(data.renderMode, diContainer); + material = EffectMaterialLEGACY.CreateFor(data.renderMode, diContainer); material.LinkTransformsTo(camera); material.World.BufferRange = locationRange; material.Uniforms.Value = EffectMaterialUniforms.Default; diff --git a/zzre/shaders/effect.mlang b/zzre/shaders/effect.mlang new file mode 100644 index 00000000..f2d6a563 --- /dev/null +++ b/zzre/shaders/effect.mlang @@ -0,0 +1,87 @@ +option DepthTest; +option Billboard = IsNoBillboard, IsViewBillboard, IsSparkBillboard; +option Blend = IsAdditiveBlend, IsAdditiveAlphaBlend, IsAlphaBlend; + +attributes +{ + float3 inVertexPos; + float2 inUV; + byte4_norm inColor; +} +attributes if (Billboard != IsNoBillboard) float3 inCenterPos; +attributes if (Billboard == IsSparkBillboard) float3 inDirection; + +uniform texture2D mainTexture; +uniform sampler mainSampler; +uniform mat4 projection; +uniform view +{ + mat4 viewMatrix; + float4 camPos; + float4 camDir; +} +uniform factors +{ + float inAlphaReference; +} + +varying +{ + float2 varUV; + float4 varColor; +} + +pipeline +{ + output r8_g8_b8_a8_unorm outColor; + output d24_unorm_s8_uint; + depthwrite off; + depthtest off; +} + +pipeline if (DepthTest) +{ + depthtest on; +} + +pipeline if (Blend == IsAlphaBlend) +{ + blend SrcAlpha + InvSrcAlpha; +} + +pipeline if (Blend == IsAdditiveBlend) +{ + blend One + One; +} + +pipeline if (Blend == IsAdditiveAlphaBlend) +{ + blend SrcAlpha + One; +} + +vertex +{ + vec4 pos; + if (Billboard == IsNoBillboard) + pos = viewMatrix * vec4(inVertexPos, 1); + else if (Billboard == IsViewBillboard) + pos = viewMatrix * vec4(inCenterPos, 1) + vec4(inVertexPos, 0); + else if (Billboard == IsSparkBillboard) + { + vec3 right = vec3(viewMatrix * vec4(inDirection, 0)).xyz; + vec3 up = normalize(cross(inCenterPos - camPos.xyz, right)); + pos += vec4(inVertexPos.x * right + inVertexPos.y * up, 0); + } + gl_Position = projection * pos; + + varUV = inUV; + varColor = inColor; +} + +fragment +{ + vec4 color = texture(sampler2D(mainTexture, mainSampler), varUV) * varColor; + if (color.a < inAlphaReference) + discard; + outColor = color; +} diff --git a/zzre/tools/ECSExplorer/ECSExplorer.EntityNaming.cs b/zzre/tools/ECSExplorer/ECSExplorer.EntityNaming.cs index 5cfa9d44..2db9e76e 100644 --- a/zzre/tools/ECSExplorer/ECSExplorer.EntityNaming.cs +++ b/zzre/tools/ECSExplorer/ECSExplorer.EntityNaming.cs @@ -11,6 +11,7 @@ partial class ECSExplorer { public delegate string? TryGetEntityNameFunc(DefaultEcs.Entity entity); public delegate string? TryGetEntityNameByComponentFunc(in T component); + public delegate string? TryGetEntityNameByComponentAndEntityFunc(DefaultEcs.Entity entity, T component); private class EntityNamer : IComparable { @@ -39,7 +40,10 @@ public static void AddEntityNamerByComponent(int prio, TryGetEntityNameFunc f AddEntityNamer(prio, entity => entity.IsAlive && entity.Has() ? func(entity) : null); public static void AddEntityNamerByComponent(int prio, TryGetEntityNameByComponentFunc func) => - AddEntityNamer(prio, entity => entity.IsAlive && entity.TryGet(out var comp) ? func(comp) : null); + AddEntityNamer(prio, entity => entity.TryGet(out var comp) ? func(comp) : null); + + public static void AddEntityNamerByComponent(int prio, TryGetEntityNameByComponentAndEntityFunc func) => + AddEntityNamer(prio, entity => entity.TryGet(out var comp) ? func(entity, comp) : null); public static void AddEntityNamerByComponent(int prio, string name) => AddEntityNamer(prio, entity => entity.IsAlive && entity.Has() ? name : null); diff --git a/zzre/tools/ECSExplorer/ECSExplorer.Standard.cs b/zzre/tools/ECSExplorer/ECSExplorer.Standard.cs index 19a59590..ce91c19f 100644 --- a/zzre/tools/ECSExplorer/ECSExplorer.Standard.cs +++ b/zzre/tools/ECSExplorer/ECSExplorer.Standard.cs @@ -9,6 +9,8 @@ using zzio.scn; using DefaultEcs.Resource; +using EffectCombinerResource = DefaultEcs.Resource.ManagedResource; + namespace zzre.tools; partial class ECSExplorer @@ -37,6 +39,19 @@ private static void AddStandardEntityNaming() } + entity.Get().idx); AddEntityNamerByComponent(Highest, e => $"CollectionFairy \"{e.Get().name}\" {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"EffectCombiner {c.Info} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"BeamStar {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"ElectricBolt {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"Models {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"MovingPlanes {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"ParticleBeam {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"ParticleCollector {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"ParticleEmitter {c.Type} {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"PlaneBeam {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"RandomPlanes {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"Sound {c.Name} {e}"); + AddEntityNamerByComponent(Highest, (e, c) => $"Sparks {c.Name} {e}"); + AddEntityNamerByComponent(High + 1, e => $"OwnedFairy \"{e.Get().name}\" {e}"); AddEntityNamerByComponent(High, e => $"Fairy \"{e.Get().Name}\" {e}"); AddEntityNamerByComponent(High, e => $"ActorPart {e}"); @@ -75,12 +90,14 @@ private static void AddStandardEntityGrouping() const string NPCs = "NPCs"; const string Animals = "Animals"; const string Preload = "Preload"; + const string Effects = "Effects"; AddEntityGrouperByComponent(1000, NPCs); AddEntityGrouperByComponent(1000, Animals); AddEntityGrouperByComponent(1000, Animals); AddEntityGrouperByComponent(1000, Animals); AddEntityGrouperByComponent(1000, Animals); + AddEntityGrouperByComponent(1000, Effects); AddEntityGrouperByComponent(0, Models); AddEntityGrouperByComponent(-1, Triggers); diff --git a/zzre/tools/ECSExplorer/ECSExplorer.cs b/zzre/tools/ECSExplorer/ECSExplorer.cs index 9e3e2574..0fef0efe 100644 --- a/zzre/tools/ECSExplorer/ECSExplorer.cs +++ b/zzre/tools/ECSExplorer/ECSExplorer.cs @@ -9,13 +9,18 @@ using static ImGuiNET.ImGui; +internal interface IECSWindow +{ + Window Window { get; } + IEnumerable<(string name, DefaultEcs.World)> GetWorlds(); +} + internal partial class ECSExplorer { private readonly ITagContainer diContainer; - private readonly ZanzarahWindow zzWindow; + private readonly IECSWindow ecsWindow; public Window Window { get; } - private Zanzarah Zanzarah => zzWindow.Zanzarah; static ECSExplorer() { @@ -23,14 +28,14 @@ static ECSExplorer() AddStandardEntityGrouping(); } - public ECSExplorer(ITagContainer diContainer, ZanzarahWindow zzWindow) + public ECSExplorer(ITagContainer diContainer, IECSWindow ecsWindow) { this.diContainer = diContainer; - this.zzWindow = zzWindow; + this.ecsWindow = ecsWindow; Window = diContainer.GetTag().NewWindow("ECS Explorer"); Window.AddTag(this); Window.InitialBounds = new Rect(float.NaN, float.NaN, 400, 800); - zzWindow.Window.OnClose += Window.Dispose; + ecsWindow.Window.OnClose += Window.Dispose; Window.OnContent += HandleContent; } @@ -38,24 +43,19 @@ public ECSExplorer(ITagContainer diContainer, ZanzarahWindow zzWindow) private void HandleContent() { BeginTabBar("Worlds", ImGuiTabBarFlags.AutoSelectNewTabs); - if (BeginTabItem("UI")) - { - HandleContentFor(Zanzarah.UI.GetTag()); - EndTabItem(); - } - if (Zanzarah.CurrentGame != null && BeginTabItem("Game")) + foreach (var (name, world) in ecsWindow.GetWorlds()) { - HandleContentFor(Zanzarah.CurrentGame.GetTag()); - EndTabItem(); + if (BeginTabItem(name)) + { + HandleContentFor(world); + EndTabItem(); + } } EndTabBar(); } private void HandleContentFor(DefaultEcs.World world) { - if (Zanzarah.CurrentGame == null) - return; - var entityContentRenderer = new EntityContentRenderer(); var children = world.ToLookup(e => e.TryGet().GetValueOrDefault().Entity); diff --git a/zzre/tools/ModelViewer.cs b/zzre/tools/ModelViewer.cs index 3285ce27..e459bb99 100644 --- a/zzre/tools/ModelViewer.cs +++ b/zzre/tools/ModelViewer.cs @@ -240,7 +240,6 @@ private void GenerateNormals() return; var morphTarget = mesh.Geometry.morphTargets.First(); normalRenderer.Clear(); - normalRenderer.Reserve(mesh.VertexCount, additive: false); normalRenderer.Add(IColor.Red, morphTarget.vertices.Zip(morphTarget.normals, (pos, normal) => new Line(pos, pos + Vector3.Normalize(normal) * 0.05f))); } @@ -296,21 +295,17 @@ private void HighlightSplit(int splitI) }; SetPlanes(mesh.BoundingBox, normal, split.left.value, split.right.value, centerValue: null); - triangleRenderer.AddTriangles( - triangles: SplitTriangles(split).ToArray(), - colors: Enumerable.Repeat(IColor.Red, SectorTriangles(split.left).Count()) - .Concat(Enumerable - .Repeat(IColor.Blue, SectorTriangles(split.right).Count())) - .ToArray()); + triangleRenderer.AddTriangles(IColor.Red, SectorTriangles(split.left).ToArray()); + triangleRenderer.AddTriangles(IColor.Blue, SectorTriangles(split.right).ToArray()); fbArea.IsDirty = true; - IEnumerable SplitTriangles(zzio.rwbs.CollisionSplit split) => + IEnumerable SplitTriangles(CollisionSplit split) => SectorTriangles(split.left).Concat(SectorTriangles(split.right)); - IEnumerable SectorTriangles(zzio.rwbs.CollisionSector sector) => sector.count == zzio.rwbs.RWCollision.SplitCount - ? SplitTriangles(collider.Collision.splits[sector.index]) - : collider.Collision.map + IEnumerable SectorTriangles(CollisionSector sector) => sector.count == RWCollision.SplitCount + ? SplitTriangles(collider!.Collision.splits[sector.index]) + : collider!.Collision.map .Skip(sector.index) .Take(sector.count) .Select(collider.GetTriangle) diff --git a/zzre/tools/WorldViewer.cs b/zzre/tools/WorldViewer.cs index d815bf19..3c8986e0 100644 --- a/zzre/tools/WorldViewer.cs +++ b/zzre/tools/WorldViewer.cs @@ -375,12 +375,8 @@ private void HighlightSplit(int splitI) }; SetPlanes(worldMesh.Sections[highlightedSectionI].Bounds, normal, split.left.value, split.right.value, centerValue: null); - triangleRenderer.AddTriangles( - triangles: SplitTriangles(split).ToArray(), - colors: Enumerable.Repeat(IColor.Red, SectorTriangles(split.left).Count()) - .Concat(Enumerable - .Repeat(IColor.Blue, SectorTriangles(split.right).Count())) - .ToArray()); + triangleRenderer.AddTriangles(IColor.Red, SectorTriangles(split.left).ToArray()); + triangleRenderer.AddTriangles(IColor.Blue, SectorTriangles(split.right).ToArray()); fbArea.IsDirty = true; @@ -388,13 +384,13 @@ IEnumerable SplitTriangles(CollisionSplit split) => SectorTriangles(split.left).Concat(SectorTriangles(split.right)); IEnumerable SectorTriangles(CollisionSector sector) => sector.count == RWCollision.SplitCount - ? SplitTriangles(sectionCollision.splits[sector.index]) - : sectionCollision.map + ? SplitTriangles(sectionCollision!.splits[sector.index]) + : sectionCollision!.map .Skip(sector.index) .Take(sector.count) - .Select(i => sectionAtomic.triangles[i]) + .Select(i => sectionAtomic!.triangles[i]) .Select(t => new Triangle( - sectionAtomic.vertices[t.v1], + sectionAtomic!.vertices[t.v1], sectionAtomic.vertices[t.v2], sectionAtomic.vertices[t.v3])) .ToArray(); @@ -529,7 +525,6 @@ private void ShootRay() var ray = new Ray(camera.Location.GlobalPosition, -camera.Location.GlobalForward); var cast = worldCollider.Cast(ray); rayRenderer.Clear(); - rayRenderer.Reserve(5, additive: false); rayRenderer.Add(IColor.Green, ray.Start, ray.Start + ray.Direction * (cast?.Distance ?? 100f)); if (cast.HasValue) { diff --git a/zzre/tools/ZanzarahWindow.cs b/zzre/tools/ZanzarahWindow.cs index efdb00b3..1e31d948 100644 --- a/zzre/tools/ZanzarahWindow.cs +++ b/zzre/tools/ZanzarahWindow.cs @@ -8,7 +8,7 @@ namespace zzre.tools; -public class ZanzarahWindow : IZanzarahContainer +public class ZanzarahWindow : IZanzarahContainer, IECSWindow { private readonly ITagContainer diContainer; private readonly FramebufferArea fbArea; @@ -188,4 +188,11 @@ private void TeleportToScene(IResource resource) return; game.LoadScene(resource.Name.Replace(".scn", ""), () => game.FindEntryTrigger(-1)); } + + public IEnumerable<(string, DefaultEcs.World)> GetWorlds() + { + yield return ("UI", Zanzarah.UI.GetTag()); + if (Zanzarah.CurrentGame != null) + yield return ("Overworld", Zanzarah.CurrentGame.GetTag()); + } } diff --git a/zzre/tools/effecteditor/EffectEditor.BeamStar.cs b/zzre/tools/effecteditor/EffectEditor.BeamStar.cs index 092d1bed..9d34a5c4 100644 --- a/zzre/tools/effecteditor/EffectEditor.BeamStar.cs +++ b/zzre/tools/effecteditor/EffectEditor.BeamStar.cs @@ -7,7 +7,7 @@ namespace zzre.tools; public partial class EffectEditor { - private void HandlePart(BeamStar data, BeamStarRenderer ren) + private void HandlePart(BeamStar data) { InputText("Name", ref data.name, 128); NewLine(); diff --git a/zzre/tools/effecteditor/EffectEditor.MovingPlanes.cs b/zzre/tools/effecteditor/EffectEditor.MovingPlanes.cs index 7b317d03..ee40b8c6 100644 --- a/zzre/tools/effecteditor/EffectEditor.MovingPlanes.cs +++ b/zzre/tools/effecteditor/EffectEditor.MovingPlanes.cs @@ -7,7 +7,7 @@ namespace zzre.tools; public partial class EffectEditor { - private void HandlePart(MovingPlanes data, MovingPlanesRenderer ren) + private void HandlePart(MovingPlanes data) { InputText("Name", ref data.name, 128); NewLine(); diff --git a/zzre/tools/effecteditor/EffectEditor.ParticleEmitter.cs b/zzre/tools/effecteditor/EffectEditor.ParticleEmitter.cs index 8eca44df..f69e9536 100644 --- a/zzre/tools/effecteditor/EffectEditor.ParticleEmitter.cs +++ b/zzre/tools/effecteditor/EffectEditor.ParticleEmitter.cs @@ -7,9 +7,10 @@ namespace zzre.tools; public partial class EffectEditor { - private void HandlePart(ParticleEmitter data, ParticleEmitterRenderer ren) + private void HandlePart(ParticleEmitter data) { - Text($"Current: {ren.CurrentParticles} / {ren.MaxParticles}"); + //Text($"Current: {ren.CurrentParticles} / {ren.MaxParticles}"); + Text($"Current: ? / ?"); NewLine(); InputText("Name", ref data.name, 128); diff --git a/zzre/tools/effecteditor/EffectEditor.RandomPlanes.cs b/zzre/tools/effecteditor/EffectEditor.RandomPlanes.cs index 73cf7d0f..b75ee68b 100644 --- a/zzre/tools/effecteditor/EffectEditor.RandomPlanes.cs +++ b/zzre/tools/effecteditor/EffectEditor.RandomPlanes.cs @@ -7,7 +7,7 @@ namespace zzre.tools; public partial class EffectEditor { - private void HandlePart(RandomPlanes data, RandomPlanesRenderer ren) + private void HandlePart(RandomPlanes data) { InputText("Name", ref data.name, 128); NewLine(); diff --git a/zzre/tools/effecteditor/EffectEditor.cs b/zzre/tools/effecteditor/EffectEditor.cs index d139eea8..cbdac9fb 100644 --- a/zzre/tools/effecteditor/EffectEditor.cs +++ b/zzre/tools/effecteditor/EffectEditor.cs @@ -1,8 +1,10 @@ -using ImGuiNET; -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using DefaultEcs.System; +using ImGuiNET; using Veldrid; using zzio; using zzio.effect; @@ -11,11 +13,10 @@ using zzre.imgui; using zzre.materials; using zzre.rendering; -using zzre.rendering.effectparts; namespace zzre.tools; -public partial class EffectEditor : ListDisposable, IDocumentEditor +public partial class EffectEditor : ListDisposable, IDocumentEditor, IECSWindow { private readonly ITagContainer diContainer; private readonly TwoColumnEditorTag editor; @@ -26,23 +27,28 @@ public partial class EffectEditor : ListDisposable, IDocumentEditor private readonly IResourcePool resourcePool; private readonly DebugLineRenderer gridRenderer; private readonly OpenFileModal openFileModal; - private readonly LocationBuffer locationBuffer; private readonly GameTime gameTime; private readonly CachedAssetLoader textureLoader; private readonly CachedAssetLoader clumpLoader; + private readonly EffectCombiner emptyEffect = new(); + private readonly DefaultEcs.World ecsWorld = new(); + private readonly SequentialSystem updateSystems = new(); + private readonly SequentialSystem renderSystems = new(); + private ECSExplorer? ecsExplorer; + private bool isPlaying = true; - private EffectCombinerRenderer? effectRenderer; - private EffectCombiner Effect => effectRenderer?.Effect ?? emptyEffect; - private EffectCombiner emptyEffect = new(); - private bool[] isVisible = Array.Empty(); - private bool isPlaying = false; - private float timeScale = 1f, progressSpeed = 0f; + private EffectCombiner Effect => loadedEffect ?? emptyEffect; + private EffectCombiner? loadedEffect = null; + private DefaultEcs.Entity effectEntity; + private DefaultEcs.Entity[] partEntities = Array.Empty(); public Window Window { get; } public IResource? CurrentResource { get; private set; } - public EffectEditor(ITagContainer diContainer) + public EffectEditor(ITagContainer diContainer_) { + diContainer = diContainer_.ExtendedWith(ecsWorld); + AddDisposable(diContainer); device = diContainer.GetTag(); resourcePool = diContainer.GetTag(); gameTime = diContainer.GetTag(); @@ -56,6 +62,7 @@ public EffectEditor(ITagContainer diContainer) var menuBar = new MenuBarWindowTag(Window); menuBar.AddButton("Open", HandleMenuOpen); fbArea = Window.GetTag(); + fbArea.ClearColor = new(0.18f, 0.11f, 0.035f, 1f); fbArea.OnResize += HandleResize; fbArea.OnRender += HandleRender; diContainer.GetTag().AddEditor(this); @@ -67,15 +74,12 @@ public EffectEditor(ITagContainer diContainer) }; openFileModal.OnOpenedResource += Load; - locationBuffer = new LocationBuffer(device); - this.diContainer = diContainer.ExtendedWith(locationBuffer); - AddDisposable(this.diContainer); - this.diContainer.AddTag(camera = new Camera(this.diContainer)); - this.diContainer.AddTag>(new DynamicQuadMeshBuffer(device.ResourceFactory, 1024)); - this.diContainer.AddTag>(new DynamicQuadMeshBuffer(device.ResourceFactory, 256)); - controls = new OrbitControlsTag(Window, camera.Location, this.diContainer); + diContainer.AddTag(new EffectMesh(diContainer, 1024, 2048)); + diContainer.AddTag(camera = new Camera(diContainer)); + controls = new OrbitControlsTag(Window, camera.Location, diContainer); AddDisposable(controls); - gridRenderer = new DebugLineRenderer(this.diContainer); + + gridRenderer = new DebugLineRenderer(diContainer); gridRenderer.Material.LinkTransformsTo(camera); gridRenderer.Material.World.Ref = Matrix4x4.Identity; gridRenderer.AddGrid(); @@ -83,17 +87,32 @@ public EffectEditor(ITagContainer diContainer) AddDisposable(textureLoader = new CachedAssetLoader(new TextureAssetLoader(diContainer))); AddDisposable(clumpLoader = new CachedClumpMeshLoader(diContainer)); - this.diContainer.AddTag>(textureLoader); - this.diContainer.AddTag>(clumpLoader); + diContainer.AddTag>(textureLoader); + diContainer.AddTag>(clumpLoader); + + diContainer.AddTag(new game.resources.EffectMaterial(diContainer)); + + updateSystems = new SequentialSystem( + new game.systems.effect.EffectCombiner(diContainer) { AddIndexAsComponent = true }, + new game.systems.effect.MovingPlanes(diContainer), + new game.systems.effect.RandomPlanes(diContainer)); + AddDisposable(updateSystems); + + renderSystems = new SequentialSystem( + new game.systems.effect.EffectRenderer(diContainer, game.components.RenderOrder.EarlyEffect), + new game.systems.effect.EffectRenderer(diContainer, game.components.RenderOrder.Effect), + new game.systems.effect.EffectRenderer(diContainer, game.components.RenderOrder.LateEffect)); + AddDisposable(renderSystems); editor.AddInfoSection("Info", HandleInfoContent); editor.AddInfoSection("Playback", HandlePlaybackContent); - } - protected override void DisposeManaged() - { - base.DisposeManaged(); - effectRenderer?.Dispose(); + menuBar.AddButton("View/Show all parts", () => SetAllVisibilities(true)); + menuBar.AddButton("View/Hide all parts", () => SetAllVisibilities(false)); + +#if DEBUG + menuBar.AddButton("Debug/ECS Explorer", HandleOpenECSExplorer); +#endif } public void Load(string pathText) @@ -112,32 +131,35 @@ private void LoadEffectNow(IResource resource) if (resource.Equals(CurrentResource)) return; CurrentResource = null; - emptyEffect = new EffectCombiner(); + KillEffect(); + textureLoader.Clear(); + clumpLoader.Clear(); + + using (var stream = resource.OpenContent()) + { + if (stream == null) + throw new FileNotFoundException($"Failed to open {resource.Path}"); + loadedEffect = new(); + loadedEffect.Read(stream); + } - effectRenderer?.Dispose(); - effectRenderer = new EffectCombinerRenderer(diContainer, resource); - effectRenderer.Location.LocalPosition = Vector3.Zero; - effectRenderer.Location.LocalRotation = Quaternion.Identity; + SpawnEffect(); + isPlaying = true; editor.ClearInfoSections(); editor.AddInfoSection("Info", HandleInfoContent); editor.AddInfoSection("Playback", HandlePlaybackContent); - foreach (var (partRenderer, i) in effectRenderer.Parts.Indexed()) + foreach (var (part, i) in Effect.parts.Indexed()) { - var part = Effect.parts[i]; editor.AddInfoSection($"{part.Type} \"{part.Name}\"", part switch { - MovingPlanes mp => () => HandlePart(mp, (MovingPlanesRenderer)partRenderer), - RandomPlanes rp => () => HandlePart(rp, (RandomPlanesRenderer)partRenderer), - ParticleEmitter pe => () => HandlePart(pe, (ParticleEmitterRenderer)partRenderer), - BeamStar bs => () => HandlePart(bs, (BeamStarRenderer)partRenderer), + MovingPlanes mp => () => HandlePart(mp), + RandomPlanes rp => () => HandlePart(rp), + ParticleEmitter pe => () => HandlePart(pe), + BeamStar bs => () => HandlePart(bs), _ => () => { } // ignore for now }, defaultOpen: false, () => HandlePartPreContent(i)); } - isVisible = Enumerable.Repeat(true, effectRenderer.Parts.Count).ToArray(); - isPlaying = true; - timeScale = 1f; - progressSpeed = 0f; controls.ResetView(); controls.CameraAngle = new Vector2(45f, -45f) * MathF.PI / 180f; @@ -146,31 +168,68 @@ private void LoadEffectNow(IResource resource) Window.Title = $"Effect Editor - {resource.Path.ToPOSIXString()}"; } + private void KillEffect() + { + effectEntity.Dispose(); + foreach (var entity in partEntities) + entity.Dispose(); + partEntities = Array.Empty(); + } + + private void SpawnEffect() + { + effectEntity = ecsWorld.CreateEntity(); + effectEntity.Set(loadedEffect); + ecsWorld.Publish(new game.messages.SpawnEffectCombiner( + 0, // we do not have the EffectCombiner resource manager, the value here does not matter + AsEntity: effectEntity, + Position: Vector3.Zero)); + partEntities = ecsWorld.GetEntities() + .With() + .AsEnumerable() + .OrderBy(e => e.Get()) + .ToArray(); + } + + private void ResetEffect() + { + var visibilities = partEntities + .Select(e => e.Get()) + .ToArray(); + KillEffect(); + SpawnEffect(); + foreach (var (entity, visibility) in partEntities.Zip(visibilities)) + entity.Set(visibility); + } + + private void SetAllVisibilities(bool isVisible) + { + foreach (var entity in partEntities) + entity.Set(isVisible ? game.components.Visibility.Visible : game.components.Visibility.Invisible); + } + private void HandleResize() => camera.Aspect = fbArea.Ratio; private void HandleRender(CommandList cl) { - locationBuffer.Update(cl); + if (isPlaying && effectEntity.IsAlive) + updateSystems.Update(gameTime.Delta); + + cl.PushDebugGroup(Window.Title); camera.Update(cl); gridRenderer.Render(cl); + renderSystems.Update(cl); + cl.PopDebugGroup(); - if (effectRenderer == null) - return; - foreach (var part in effectRenderer.Parts.Where((p, i) => isVisible[i])) - part.Render(cl); + fbArea.IsDirty = true; } private void HandleInfoContent() { ImGui.InputText("Description", ref Effect.description, 512); - - var pos = effectRenderer?.Location.LocalPosition ?? Vector3.Zero; - var forwards = Effect.forwards; - var upwards = Effect.upwards; - if (ImGui.DragFloat3("Position", ref pos) && effectRenderer != null) - effectRenderer.Location.LocalPosition = pos; - ImGui.DragFloat3("Forwards", ref forwards); - ImGui.DragFloat3("Upwards", ref upwards); + ImGui.DragFloat3("Position", ref Effect.position); + ImGui.DragFloat3("Forwards", ref Effect.forwards); + ImGui.DragFloat3("Upwards", ref Effect.upwards); } private void HandlePlaybackContent() @@ -181,49 +240,38 @@ static void UndoSlider(string label, ref float value, float min, float max, floa if (ImGui.IsItemClicked(ImGuiMouseButton.Right) || ImGui.IsItemClicked(ImGuiMouseButton.Middle)) value = defaultValue; } + game.components.effect.CombinerPlayback dummyPlayback = new(); + var optPlayback = effectEntity.TryGet(); + ref var playback = ref (optPlayback.HasValue ? ref optPlayback.Value : ref dummyPlayback); - ImGui.Checkbox("Looping", ref Effect.isLooping); + var backgroundColor = fbArea.ClearColor.ToVector4(); + ImGui.ColorEdit4("Background", ref backgroundColor); + fbArea.ClearColor = backgroundColor.ToRgbaFloat(); + ImGui.NewLine(); - float curTime = effectRenderer?.CurTime ?? 0f; - ImGui.SliderFloat("Time", ref curTime, 0f, Effect.Duration, $"%.3f / {Effect.Duration}", ImGuiSliderFlags.NoInput); - UndoSlider("Time Scale", ref timeScale, 0f, 5f, 1f); - float progress = effectRenderer?.CurProgress ?? 0f; - if (ImGui.SliderFloat("Progress", ref progress, 0f, 100f)) - { - effectRenderer?.AddTime(0f, progress); - fbArea.IsDirty = true; - } - UndoSlider("Progress Speed", ref progressSpeed, -2f, 2f, 0f); + if (!effectEntity.IsAlive) + ImGui.BeginDisabled(); - float length = effectRenderer?.Length ?? 0f; - UndoSlider("Length", ref length, 0f, 5f, 1f); - if (effectRenderer != null) - effectRenderer.Length = length; + ImGui.Checkbox("Looping", ref Effect.isLooping); + var normalizedTime = playback.CurTime / Effect.Duration; + if (playback.IsLooping) + normalizedTime -= MathF.Truncate(normalizedTime); + ImGui.ProgressBar(normalizedTime, new Vector2(0f, 0f), $"{playback.CurTime:F2} / {Effect.Duration}"); + ImGui.SameLine(); + ImGui.Text("Time"); + ImGui.SliderFloat("Progress", ref playback.CurProgress, 0f, 100f); + UndoSlider("Length", ref playback.Length, 0f, 5f, 1f); if (ImGui.Button(IconFonts.ForkAwesome.FastBackward)) - { - effectRenderer?.Reset(); - fbArea.IsDirty = true; - } + ResetEffect(); ImGui.SameLine(); if (isPlaying && ImGui.Button(IconFonts.ForkAwesome.Pause)) isPlaying = false; - else if (!isPlaying && ImGui.Button(IconFonts.ForkAwesome.Play) && effectRenderer != null) + else if (!isPlaying && ImGui.Button(IconFonts.ForkAwesome.Play)) isPlaying = true; - if (isPlaying && effectRenderer != null) - { - var newProgress = effectRenderer.CurProgress + progressSpeed * 100f * gameTime.Delta; - newProgress = Effect.isLooping - ? newProgress < 0f ? 100f - newProgress - : newProgress > 100f ? newProgress - 100f - : newProgress - : Math.Clamp(newProgress, 0f, 100f); - effectRenderer.AddTime(gameTime.Delta * timeScale, newProgress); - if (effectRenderer.IsDone) - isPlaying = false; - fbArea.IsDirty = true; - } + if (!effectEntity.IsAlive) + ImGui.EndDisabled(); } private void HandleMenuOpen() @@ -234,10 +282,32 @@ private void HandleMenuOpen() private void HandlePartPreContent(int i) { - if (ImGui.Button(isVisible[i] ? IconFonts.ForkAwesome.Eye : IconFonts.ForkAwesome.EyeSlash)) + var isVisible = partEntities[i].Get() == game.components.Visibility.Visible; + var (on, off) = Effect.parts[i] is Sound // that is some unnecessary detail... + ? (IconFonts.ForkAwesome.VolumeUp, IconFonts.ForkAwesome.VolumeOff) + : (IconFonts.ForkAwesome.Eye, IconFonts.ForkAwesome.EyeSlash); + if (ImGui.Button(isVisible ? on : off, new Vector2(24f, 0f))) { - isVisible[i] = !isVisible[i]; + partEntities[i].Set(isVisible + ? game.components.Visibility.Invisible + : game.components.Visibility.Visible); fbArea.IsDirty = true; } } + + private void HandleOpenECSExplorer() + { + if (ecsExplorer == null) + { + ecsExplorer = new ECSExplorer(diContainer, this); + ecsExplorer.Window.OnClose += () => ecsExplorer = null; + } + else + diContainer.GetTag().SetNextFocusedWindow(ecsExplorer.Window); + } + + public IEnumerable<(string, DefaultEcs.World)> GetWorlds() + { + yield return ("Effect", ecsWorld); + } } diff --git a/zzre/zzre.csproj b/zzre/zzre.csproj index f1fefdda..8cde7c1e 100644 --- a/zzre/zzre.csproj +++ b/zzre/zzre.csproj @@ -37,6 +37,7 @@ +