diff --git a/src/microbe_stage/IMembraneDataSource.cs b/src/microbe_stage/IMembraneDataSource.cs index 1a160cda79..9cca39bb36 100644 --- a/src/microbe_stage/IMembraneDataSource.cs +++ b/src/microbe_stage/IMembraneDataSource.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using Godot; /// @@ -9,6 +10,9 @@ public interface IMembraneDataSource { public Vector2[] HexPositions { get; } + + public Vector2[]? MulticellularPositions { get; } + public int HexPositionCount { get; } public MembraneType Type { get; } } @@ -18,14 +22,18 @@ public interface IMembraneDataSource /// public struct MembraneGenerationParameters : IMembraneDataSource { - public MembraneGenerationParameters(Vector2[] hexPositions, int hexPositionCount, MembraneType type) + public MembraneGenerationParameters(Vector2[] hexPositions, int hexPositionCount, MembraneType type, Vector2[]? multicellularPositions, Vector2? thisCellPosition) { HexPositions = hexPositions; + MulticellularPositions = multicellularPositions; + CellPositionInMulticellular = thisCellPosition; HexPositionCount = hexPositionCount; Type = type; } public Vector2[] HexPositions { get; } + public Vector2[]? MulticellularPositions { get; } + public Vector2? CellPositionInMulticellular { get; } public int HexPositionCount { get; } public MembraneType Type { get; } @@ -89,7 +97,7 @@ public static MembranePointData GetOrComputeMembraneShape(IReadOnlyList 0); } public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource, IMembraneDataSource other) { - return dataSource.MembraneDataFieldsEqual(other.HexPositions, other.HexPositionCount, other.Type); + return dataSource.MembraneDataFieldsEqual(other.HexPositions, other.HexPositionCount, other.Type, other.MulticellularPositions); } public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource, Vector2[] otherPoints, - int otherPointCount, MembraneType otherType) + int otherPointCount, MembraneType otherType, Vector2[]? multicellularPositions) { if (!dataSource.Type.Equals(otherType)) return false; @@ -162,6 +172,25 @@ public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource, var sourcePoints = dataSource.HexPositions; + if (dataSource.MulticellularPositions != null) + { + if (multicellularPositions == null) + return false; + + if (!dataSource.MulticellularPositions.SequenceEqual(multicellularPositions)) + { + return false; + } + } + else + { + if (multicellularPositions != null) + { + return false; + + } + } + for (int i = 0; i < count; ++i) { if (sourcePoints[i] != otherPoints[i]) diff --git a/src/microbe_stage/MembranePointData.cs b/src/microbe_stage/MembranePointData.cs index 65da4bad70..238176bb14 100644 --- a/src/microbe_stage/MembranePointData.cs +++ b/src/microbe_stage/MembranePointData.cs @@ -23,11 +23,12 @@ public sealed class MembranePointData : IMembraneDataSource, ICacheableData private bool disposed; public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneType type, - IReadOnlyList verticesToCopy) + IReadOnlyList verticesToCopy, Vector2[]? multicellularPositions) { HexPositions = hexPositions; Type = type; HexPositionCount = hexPositionCount; + MulticellularPositions = multicellularPositions; // Setup mesh to be generated (on the main thread) only when required finalMesh = new Lazy<(ArrayMesh Mesh, int SurfaceIndex)>(() => @@ -82,6 +83,11 @@ public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneT /// public Vector2[] HexPositions { get; } + /// + /// Positions of other cells in multicellular organism + /// + public Vector2[]? MulticellularPositions { get; } + public int HexPositionCount { get; } public MembraneType Type { get; } diff --git a/src/microbe_stage/MembraneShapeGenerator.cs b/src/microbe_stage/MembraneShapeGenerator.cs index e70a6ccd2e..6087a7c663 100644 --- a/src/microbe_stage/MembraneShapeGenerator.cs +++ b/src/microbe_stage/MembraneShapeGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Godot; using Array = Godot.Collections.Array; @@ -54,7 +55,8 @@ public static MembraneShapeGenerator GetThreadSpecificGenerator() /// Computed data in a cache entry format (to be used with , which should be /// checked for existing data before computing new data) /// - public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, MembraneType membraneType) + public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, MembraneType membraneType, + Vector2[]? cellPositions, Vector2? thisCellPosition) { // The length in pixels (probably not accurate?) of a side of the square that bounds the membrane. // Half the side length of the original square that is compressed to make the membrane. @@ -109,20 +111,23 @@ public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, Mem // ReSharper restore PossibleLossOfFraction // Get new membrane points for vertices2D - GenerateMembranePoints(hexPositions, hexCount, membraneType); + GenerateMembranePoints(hexPositions, hexCount, membraneType, cellPositions, thisCellPosition); + + GD.Print(cellPositions == null); // This makes a copy of the vertices so the data is safe to modify in further calls to this method - return new MembranePointData(hexPositions, hexCount, membraneType, vertices2D); + return new MembranePointData(hexPositions, hexCount, membraneType, vertices2D, cellPositions); } public MembranePointData GenerateShape(ref MembraneGenerationParameters parameters) { - return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type); + return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type, + parameters.MulticellularPositions, parameters.CellPositionInMulticellular); } public MembranePointData GenerateShape(IMembraneDataSource parameters) { - return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type); + return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type, null, null); } /// @@ -464,7 +469,8 @@ private static ArrayMesh BuildEngulfMesh(Vector2[] vertices2D, int vertexCount, return generatedMesh; } - private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, MembraneType membraneType) + private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, MembraneType membraneType, + Vector2[]? cellPositions, Vector2? thisCellPosition) { // Move all the points in the source buffer close to organelles // This operation used to be iterative but this is now a much faster version that moves things all the way in @@ -550,5 +556,75 @@ private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, Membra vertices2D[i] = point + movement; } + + var average = Vector2.Zero; + + foreach (var vertex in vertices2D) + { + average += vertex; + } + + average /= vertices2D.Count; + + // Multicellular matrix + if (thisCellPosition != null && cellPositions != null) + { + // Make into constant unless you will forget + var verticeDistanceMultiplier = 0.8f; + + for (int i = 0; i < vertices2D.Count; ++i) + { + var relativeVertex = vertices2D[i] - average; + + bool facesAnEdge = false; + float minMultiplier = float.MaxValue; + + foreach (var cellPos in cellPositions) + { + if (cellPos == thisCellPosition) + continue; + + // Coordinates of such a point on the Voronoi edge, that a line from the cell's center to this + // point is perpendicular to the edge. + var edge = (cellPos + thisCellPosition.Value) * 0.5f; + + var relativeEdge = (edge - thisCellPosition.Value) * 2.0f; + + float dotProduct = relativeVertex.Dot(relativeEdge); + + // If the dotproduct is less that this value, then this edge faces a direction different + // than that of the vertex, so it doesn't need to be considered + if (dotProduct <= 0.0f) + continue; + + // The vertex pos, when multiplied by this, should be placed at the edge + float multiplier = relativeEdge.LengthSquared() / dotProduct; + + if (multiplier < minMultiplier) + { + minMultiplier = multiplier; + facesAnEdge = true; + } + } + + if (!facesAnEdge) + { + minMultiplier = 1.0f; + } + else if (minMultiplier > 3.0f) + { + minMultiplier = 1.0f; + } + + vertices2D[i] = average + relativeVertex * minMultiplier * verticeDistanceMultiplier; + } + + for (int i = 0, end = startingBuffer.Count; i < end; ++i) + { + var movement = thisCellPosition.Value; + + startingBuffer[i] = startingBuffer[i] + movement; + } + } } } diff --git a/src/microbe_stage/systems/MicrobeVisualsSystem.cs b/src/microbe_stage/systems/MicrobeVisualsSystem.cs index 1e80e9b86d..932a968a60 100644 --- a/src/microbe_stage/systems/MicrobeVisualsSystem.cs +++ b/src/microbe_stage/systems/MicrobeVisualsSystem.cs @@ -11,6 +11,7 @@ using DefaultEcs; using DefaultEcs.System; using Godot; +using HarmonyLib; using World = DefaultEcs.World; /// @@ -136,8 +137,17 @@ protected override void Update(float delta, in Entity entity) ref var materialStorage = ref entity.Get(); + MembranePointData? data = null; + // Background thread membrane generation - var data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer); + if (entity.Has()) + { + data = GetMulticellularMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer, ref entity.Get()); + } + else + { + data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer); + } if (data == null) { @@ -222,7 +232,70 @@ protected override void PostUpdate(float state) var hexes = MembraneComputationHelpers.PrepareHexPositionsForMembraneCalculations( organelleContainer.Organelles!.Organelles, out var hexCount); - var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType); + var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType, false); + + var cachedMembrane = ProceduralDataCache.Instance.ReadMembraneData(hash); + + if (cachedMembrane != null) + { + // TODO: hopefully this can't get into a permanent loop where 2 conflicting membranes want to + // re-generate on each game update cycle + if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType, null)) + { + CacheableDataExtensions.OnCacheHashCollision(hash); + cachedMembrane = null; + } + } + + if (cachedMembrane != null) + { + // Membrane was ready now + return cachedMembrane; + } + + // Need to generate a new membrane + + lock (pendingGenerationsOfMembraneHashes) + { + if (!pendingGenerationsOfMembraneHashes.Add(hash)) + { + // Already queued, don't need to queue again + + // Return the unnecessary array that there won't be a cache entry to hold to the pool + ArrayPool.Shared.Return(hexes); + + return null; + } + } + + membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType, null, null)); + + // Immediately start some jobs to give background threads something to do while the main thread is busy + // potentially setting up other visuals + StartMembraneGenerationJobs(); + + return null; + } + + private MembranePointData? GetMulticellularMembraneDataIfReadyOrStartGenerating(ref CellProperties cellProperties, + ref OrganelleContainer organelleContainer, ref EarlyMulticellularSpeciesMember multicellular) + { + // TODO: should we consider the situation where a membrane was requested on the previous update but is not + // ready yet? This causes extra memory usage here in those cases. + var hexes = MembraneComputationHelpers.PrepareHexPositionsForMembraneCalculations( + organelleContainer.Organelles!.Organelles, out var hexCount); + + List positions = new List(); + + foreach (var cell in multicellular.Species.Cells) + { + var cartesian = Hex.AxialToCartesian(cell.Position); + positions.Add(new Vector2(cartesian.X, cartesian.Z)); + } + + var positionsArray = positions.ToArray(); + + var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType, true); var cachedMembrane = ProceduralDataCache.Instance.ReadMembraneData(hash); @@ -230,7 +303,7 @@ protected override void PostUpdate(float state) { // TODO: hopefully this can't get into a permanent loop where 2 conflicting membranes want to // re-generate on each game update cycle - if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType)) + if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType, positionsArray)) { CacheableDataExtensions.OnCacheHashCollision(hash); cachedMembrane = null; @@ -258,7 +331,10 @@ protected override void PostUpdate(float state) } } - membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType)); + var thisCartesian = Hex.AxialToCartesian(multicellular.Species.Cells[multicellular.MulticellularBodyPlanPartIndex].Position); + var thisVector2 = new Vector2(thisCartesian.X, thisCartesian.Z); + + membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType, positionsArray, thisVector2)); // Immediately start some jobs to give background threads something to do while the main thread is busy // potentially setting up other visuals