Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Primitive multicellular matrix attempt #5673

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions src/microbe_stage/IMembraneDataSource.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using Godot;

/// <summary>
Expand All @@ -9,6 +10,9 @@
public interface IMembraneDataSource
{
public Vector2[] HexPositions { get; }

public Vector2[]? MulticellularPositions { get; }

public int HexPositionCount { get; }
public MembraneType Type { get; }
}
Expand All @@ -18,14 +22,18 @@ public interface IMembraneDataSource
/// </summary>
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; }
Expand Down Expand Up @@ -89,7 +97,7 @@ public static MembranePointData GetOrComputeMembraneShape(IReadOnlyList<IPositio

var cache = ProceduralDataCache.Instance;

var hash = ComputeMembraneDataHash(hexes, length, membraneType);
var hash = ComputeMembraneDataHash(hexes, length, membraneType, false);

var result = cache.ReadMembraneData(hash);

Expand All @@ -108,14 +116,14 @@ public static MembranePointData GetOrComputeMembraneShape(IReadOnlyList<IPositio

lock (generator)
{
result = generator.GenerateShape(hexes, length, membraneType);
result = generator.GenerateShape(hexes, length, membraneType, null, null);
}

cache.WriteMembraneData(ref result);
return result;
}

public static long ComputeMembraneDataHash(Vector2[] positions, int count, MembraneType type)
public static long ComputeMembraneDataHash(Vector2[] positions, int count, MembraneType type, bool multicellular)
{
var nameHash = type.InternalName.GetHashCode();

Expand All @@ -129,9 +137,10 @@ public static long ComputeMembraneDataHash(Vector2[] positions, int count, Membr
for (int i = 0; i < count; ++i)
{
var posHash = positions[i].GetHashCode();
var multicellularityAdd = multicellular ? 1 : 0;

// TODO: switch to using rotate left here once we can (after Godot 4)
hash ^= (hashMultiply * posHash) ^ ((5081L * hashMultiply * hashMultiply + posHash) << 32);
hash ^= (hashMultiply * posHash) ^ ((5081L * hashMultiply * hashMultiply + posHash + multicellularityAdd) << 32);
++hashMultiply;
}

Expand All @@ -141,16 +150,17 @@ public static long ComputeMembraneDataHash(Vector2[] positions, int count, Membr

public static long ComputeMembraneDataHash(this IMembraneDataSource dataSource)
{
return ComputeMembraneDataHash(dataSource.HexPositions, dataSource.HexPositionCount, dataSource.Type);
return ComputeMembraneDataHash(dataSource.HexPositions, dataSource.HexPositionCount, dataSource.Type,
dataSource.MulticellularPositions == null ? false : dataSource.MulticellularPositions.Length > 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;
Expand All @@ -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])
Expand Down
8 changes: 7 additions & 1 deletion src/microbe_stage/MembranePointData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ public sealed class MembranePointData : IMembraneDataSource, ICacheableData
private bool disposed;

public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneType type,
IReadOnlyList<Vector2> verticesToCopy)
IReadOnlyList<Vector2> 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)>(() =>
Expand Down Expand Up @@ -82,6 +83,11 @@ public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneT
/// </summary>
public Vector2[] HexPositions { get; }

/// <summary>
/// Positions of other cells in multicellular organism
/// </summary>
public Vector2[]? MulticellularPositions { get; }

public int HexPositionCount { get; }

public MembraneType Type { get; }
Expand Down
88 changes: 82 additions & 6 deletions src/microbe_stage/MembraneShapeGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using Array = Godot.Collections.Array;

Expand Down Expand Up @@ -54,7 +55,8 @@ public static MembraneShapeGenerator GetThreadSpecificGenerator()
/// Computed data in a cache entry format (to be used with <see cref="ProceduralDataCache"/>, which should be
/// checked for existing data before computing new data)
/// </returns>
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.
Expand Down Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it prints false, thus cellPositions is not 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);
}

/// <summary>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
}
}
84 changes: 80 additions & 4 deletions src/microbe_stage/systems/MicrobeVisualsSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using DefaultEcs;
using DefaultEcs.System;
using Godot;
using HarmonyLib;
using World = DefaultEcs.World;

/// <summary>
Expand Down Expand Up @@ -136,8 +137,17 @@ protected override void Update(float delta, in Entity entity)

ref var materialStorage = ref entity.Get<EntityMaterial>();

MembranePointData? data = null;

// Background thread membrane generation
var data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer);
if (entity.Has<EarlyMulticellularSpeciesMember>())
{
data = GetMulticellularMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer, ref entity.Get<EarlyMulticellularSpeciesMember>());
}
else
{
data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer);
}

if (data == null)
{
Expand Down Expand Up @@ -222,15 +232,78 @@ 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<MembranePointData>(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<Vector2>.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<Vector2> positions = new List<Vector2>();

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);

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))
if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType, positionsArray))
{
CacheableDataExtensions.OnCacheHashCollision<MembranePointData>(hash);
cachedMembrane = null;
Expand Down Expand Up @@ -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
Expand Down