forked from Revolutionary-Games/Thrive
/
Membrane.cs
727 lines (594 loc) · 22.8 KB
/
Membrane.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
using System;
using System.Collections.Generic;
using Godot;
using Array = Godot.Collections.Array;
using System.Linq;
/// <summary>
/// Membrane for microbes
/// </summary>
public class Membrane : MeshInstance, IComputedMembraneData
{
/// <summary>
/// This must be big enough that no organelle can be at this position.
/// TODO: maybe switching to nullable float would be a good alternative?
/// </summary>
public const float INVALID_FOUND_ORGANELLE = -999999.0f;
[Export]
public ShaderMaterial? MaterialToEdit;
private static readonly List<Vector2> PreviewMembraneOrganellePositions = new() { new Vector2(0, 0) };
/// <summary>
/// Stores the generated 2-Dimensional membrane. Needed for contains calculations
/// </summary>
private readonly List<Vector2> vertices2D = new();
// Work buffers used when generating membrane data
private List<Vector2> previousWorkBuffer = new();
private List<Vector2> nextWorkBuffer = new();
private float healthFraction = 1.0f;
private float wigglyNess = 1.0f;
private float sizeWigglyNessDampeningFactor = 0.22f;
private float movementWigglyNess = 1.0f;
private float sizeMovementWigglyNessDampeningFactor = 0.22f;
private Color tint = Colors.White;
private float dissolveEffectValue;
private MembraneType? type;
private Texture? albedoTexture;
private Texture noiseTexture = null!;
private string? currentlyLoadedAlbedoTexture;
private bool dirty = true;
private bool radiusIsDirty = true;
private float cachedRadius;
/// <summary>
/// Amount of segments on one side of the above described
/// square. The amount of points on the side of the membrane.
/// </summary>
private int membraneResolution = Constants.MEMBRANE_RESOLUTION;
/// <summary>
/// When true the mesh needs to be regenerated and material properties applied
/// </summary>
public bool Dirty
{
get => dirty;
set
{
if (value)
radiusIsDirty = true;
dirty = value;
}
}
/// <summary>
/// Organelle positions of the microbe, needs to be set for the membrane to appear
/// </summary>
/// <remarks>
/// <para>
/// The contents in this list should not be modified, a new list should be assigned.
/// TODO: change the type here to be a readonly list
/// </para>
/// </remarks>
public List<Vector2> OrganellePositions { get; set; } = PreviewMembraneOrganellePositions;
/// <summary>
/// The type of the membrane.
/// </summary>
/// <exception cref="InvalidOperationException">When trying to read before this is initialized</exception>
/// <exception cref="ArgumentNullException">If value is attempted to be set to null</exception>
public MembraneType Type
{
get => type ?? throw new InvalidOperationException("Membrane type has not been set yet");
set
{
if (value == null)
throw new ArgumentNullException();
if (type == value)
return;
type = value;
dirty = true;
}
}
/// <summary>
/// How healthy the cell is, mixes in a damaged texture. Range 0.0f - 1.0f
/// </summary>
public float HealthFraction
{
get => healthFraction;
set
{
value = value.Clamp(0.0f, 1.0f);
if (value == HealthFraction)
return;
healthFraction = value;
ApplyHealth();
}
}
/// <summary>
/// How much the membrane wiggles. Used values are 0 and 1
/// </summary>
public float WigglyNess
{
get => wigglyNess;
set
{
wigglyNess = Mathf.Clamp(value, 0.0f, 1.0f);
ApplyWiggly();
}
}
public float MovementWigglyNess
{
get => movementWigglyNess;
set
{
movementWigglyNess = Mathf.Clamp(value, 0.0f, 1.0f);
ApplyMovementWiggly();
}
}
public Color Tint
{
get => tint;
set
{
// Desaturate it here so it looks nicer (could implement as method that
// could be called i suppose)
// According to stack overflow HSV and HSB are the same thing
value.ToHsv(out var hue, out var saturation, out var brightness);
value = Color.FromHsv(hue, saturation * 0.75f, brightness);
if (tint == value)
return;
tint = value;
// If we already have created a material we need to re-apply it
ApplyTint();
}
}
/// <summary>
/// Quick radius value for the membrane size
/// </summary>
public float EncompassingCircleRadius
{
get
{
if (radiusIsDirty)
{
cachedRadius = CalculateEncompassingCircleRadius();
radiusIsDirty = false;
}
return cachedRadius;
}
}
public float DissolveEffectValue
{
get => dissolveEffectValue;
set
{
dissolveEffectValue = value;
ApplyDissolveEffect();
}
}
public override void _Ready()
{
type ??= SimulationParameters.Instance.GetMembrane("single");
if (MaterialToEdit == null)
GD.PrintErr("MaterialToEdit on Membrane is not set");
noiseTexture = GD.Load<Texture>("res://assets/textures/dissolve_noise.tres");
Dirty = true;
}
public override void _Process(float delta)
{
if (!Dirty)
return;
Update();
}
/// <summary>
/// Sees if the given point is inside the membrane.
/// </summary>
/// <remarks>
/// <para>
/// This is quite an expensive method as this loops all the vertices
/// </para>
/// </remarks>
public bool Contains(float x, float y)
{
bool crosses = false;
int n = vertices2D.Count;
for (int i = 0; i < n - 1; i++)
{
if ((vertices2D[i].y <= y && y < vertices2D[i + 1].y) ||
(vertices2D[i + 1].y <= y && y < vertices2D[i].y))
{
if (x < (vertices2D[i + 1].x - vertices2D[i].x) *
(y - vertices2D[i].y) /
(vertices2D[i + 1].y - vertices2D[i].y) +
vertices2D[i].x)
{
crosses = !crosses;
}
}
}
return crosses;
}
/// <summary>
/// Finds the point on the membrane nearest to the given point.
/// </summary>
/// <remarks>
/// <para>
/// Used for finding out where to put an external organelle.
/// </para>
/// <para>
/// The returned Vector is in world coordinates (x, 0, z) and
/// not in internal membrane coordinates (x, y, 0). This is so
/// that gameplay code doesn't have to do the conversion
/// everywhere this is used.
/// </para>
/// </remarks>
public Vector3 GetVectorTowardsNearestPointOfMembrane(float x, float y)
{
// Calculate now if dirty to make flagella positioning only have to be done once
// NOTE: that flagella position should only be read once all organelles that are
// going to be added / removed on this game update are done.
if (Dirty)
Update();
float organelleAngle = Mathf.Atan2(y, x);
Vector2 closestSoFar = new Vector2(0, 0);
float angleToClosest = Mathf.Pi * 2;
foreach (var vertex in vertices2D)
{
if (Mathf.Abs(Mathf.Atan2(vertex.y, vertex.x) - organelleAngle) <
angleToClosest)
{
closestSoFar = new Vector2(vertex.x, vertex.y);
angleToClosest =
Mathf.Abs(Mathf.Atan2(vertex.y, vertex.x) - organelleAngle);
}
}
return new Vector3(closestSoFar.x, 0, closestSoFar.y);
}
/// <summary>
/// Return the position of the closest organelle to the target point if it is less then a certain threshold away.
/// </summary>
public Vector2 FindClosestOrganelleInRange(Vector2 origin, float range)
{
float closestSoFar = range;
Vector2 closest = new Vector2(INVALID_FOUND_ORGANELLE, INVALID_FOUND_ORGANELLE);
foreach (var pos in OrganellePositions)
{
float lenToObject = (origin - pos).LengthSquared();
if (lenToObject < closestSoFar)
{
closestSoFar = lenToObject;
closest = pos;
}
}
return closest;
}
public Vector2 FindCenterOfOrganellesInRange(Vector2 origin, float range)
{
List<Vector2> points = new List<Vector2>() { origin };
points.AddRange(OrganellePositions.Where(x => (origin - x).LengthSquared() < range));
return new Vector2(points.Sum(x => x.x) / points.Count(), points.Sum(x => x.y) / points.Count());
}
public bool MatchesCacheParameters(ICacheableData cacheData)
{
if (cacheData is IComputedMembraneData data)
return this.MembraneDataFieldsEqual(data);
return false;
}
public long ComputeCacheHash()
{
return this.ComputeMembraneDataHash();
}
/// <summary>
/// Decides where the point needs to move based on the position of the closest organelle.
/// </summary>
private Vector2 GetMovement(Vector2 target, Vector2 closestOrganelle)
{
float power = Mathf.Pow(2.7f, -(target - closestOrganelle).Length() / 10) / 250;
return (closestOrganelle - target) * power;
}
private Vector2 GetMovementForCellWall(Vector2 target, Vector2 closestOrganelle)
{
float power = Mathf.Pow(3.1f, -(target - closestOrganelle).Length() / 3) / 250;
return (closestOrganelle - target) * power;
}
// Vector2 GetMovementForCellWall(Vector2 target, Vector2 closestOrganelle);
/// <summary>
/// Cheaper version of contains for absorbing stuff.Calculates a
/// circle radius that contains all the points (when it is
/// placed at 0,0 local coordinate).
/// </summary>
private float CalculateEncompassingCircleRadius()
{
if (Dirty)
Update();
float distanceSquared = 0;
foreach (var vertex in vertices2D)
{
var currentDistance = vertex.LengthSquared();
if (currentDistance > distanceSquared)
distanceSquared = currentDistance;
}
return Mathf.Sqrt(distanceSquared);
}
/// <summary>
/// Updates things and marks as not dirty
/// </summary>
private void Update()
{
Dirty = false;
InitializeMesh();
ApplyAllMaterialParameters();
}
private void ApplyAllMaterialParameters()
{
ApplyWiggly();
ApplyMovementWiggly();
ApplyHealth();
ApplyTint();
ApplyTextures();
ApplyDissolveEffect();
}
private void ApplyWiggly()
{
if (MaterialToEdit == null)
return;
// Don't apply wigglyness too early if this is dirty as getting the circle radius forces membrane position
// calculation, which we don't want to do twice when initializing a microbe
if (Dirty)
return;
float wigglyNessToApply =
WigglyNess / (EncompassingCircleRadius * sizeWigglyNessDampeningFactor);
MaterialToEdit.SetShaderParam("wigglyNess", Mathf.Min(WigglyNess, wigglyNessToApply));
}
private void ApplyMovementWiggly()
{
if (MaterialToEdit == null)
return;
// See comment in ApplyWiggly
if (Dirty)
return;
float wigglyNessToApply =
MovementWigglyNess / (EncompassingCircleRadius * sizeMovementWigglyNessDampeningFactor);
MaterialToEdit.SetShaderParam("movementWigglyNess", Mathf.Min(MovementWigglyNess, wigglyNessToApply));
}
private void ApplyHealth()
{
MaterialToEdit?.SetShaderParam("healthFraction", HealthFraction);
}
private void ApplyTint()
{
MaterialToEdit?.SetShaderParam("tint", Tint);
}
private void ApplyTextures()
{
// We must update the texture on already-existing membranes, due to the membrane texture changing
// for the player microbe.
if (albedoTexture != null && currentlyLoadedAlbedoTexture == Type.AlbedoTexture)
return;
albedoTexture = Type.LoadedAlbedoTexture;
MaterialToEdit!.SetShaderParam("albedoTexture", albedoTexture);
MaterialToEdit.SetShaderParam("normalTexture", Type.LoadedNormalTexture);
MaterialToEdit.SetShaderParam("damagedTexture", Type.LoadedDamagedTexture);
MaterialToEdit.SetShaderParam("dissolveTexture", noiseTexture);
currentlyLoadedAlbedoTexture = Type.AlbedoTexture;
}
private void ApplyDissolveEffect()
{
MaterialToEdit?.SetShaderParam("dissolveValue", DissolveEffectValue);
}
/// <summary>
/// First generates the 2D vertices and then builds the 3D mesh
/// </summary>
private void InitializeMesh()
{
// First try to get from cache as it's very expensive to generate the membrane
var cached = this.FetchDataFromCache(ProceduralDataCache.Instance.ReadMembraneData);
if (cached != null)
{
CopyMeshFromCache(cached);
return;
}
// 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.
int cellDimensions = 10;
var nodeLength = cellDimensions / membraneResolution;
foreach (var pos in OrganellePositions)
{
if (Mathf.Abs(pos.x) + 1 > cellDimensions)
{
cellDimensions = (int)Mathf.Abs(pos.x) + 1;
}
if (Mathf.Abs(pos.y) + 1 > cellDimensions)
{
cellDimensions = (int)Mathf.Abs(pos.y) + 1;
}
}
previousWorkBuffer.Capacity = vertices2D.Capacity;
nextWorkBuffer.Capacity = previousWorkBuffer.Capacity;
// left wall of square
for (int i = membraneResolution; i > 0; i--)
{
previousWorkBuffer.Add(new Vector2(-cellDimensions,
cellDimensions - 2 * nodeLength * i));
}
// bottom wall of square
for (int i = membraneResolution; i > 0; i--)
{
previousWorkBuffer.Add(new Vector2(
cellDimensions - 2 * nodeLength * i,
cellDimensions));
}
// right wall of square
for (int i = membraneResolution; i > 0; i--)
{
previousWorkBuffer.Add(new Vector2(cellDimensions,
-cellDimensions + 2 * nodeLength * i));
}
// bottom wall of square
for (int i = membraneResolution; i > 0; i--)
{
previousWorkBuffer.Add(new Vector2(
-cellDimensions + 2 * nodeLength * i,
-cellDimensions));
}
// This needs to actually run a bunch of times as the points moving towards the organelles is iterative.
// We use rotating work buffers to save time on skipping useless copies
for (int i = 0; i < 60 * cellDimensions; i++)
{
DrawMembrane(cellDimensions, previousWorkBuffer, nextWorkBuffer, Type.CellWall ? GetMovementForCellWall : GetMovement);
(previousWorkBuffer, nextWorkBuffer) = (nextWorkBuffer, previousWorkBuffer);
}
// Copy final vertex data from the work buffer
vertices2D.Clear();
// The work buffer not being pointed to as the next, is the one we should read the result from
vertices2D.AddRange(previousWorkBuffer);
previousWorkBuffer.Clear();
nextWorkBuffer.Clear();
BuildMesh();
}
private void CopyMeshFromCache(ComputedMembraneData cached)
{
// TODO: check if it would be better for us to just keep readonly data in the membrane cache so we could
// just copy a reference here
vertices2D.Clear();
vertices2D.AddRange(cached.Vertices2D);
// Apply the mesh to us
Mesh = cached.GeneratedMesh;
SetSurfaceMaterial(cached.SurfaceIndex, MaterialToEdit);
}
/// <summary>
/// Creates the actual mesh object. Call InitializeMesh instead of this directly
/// </summary>
private void BuildMesh()
{
// This is actually a triangle list, but the index buffer is used to build
// the indices (to emulate a triangle fan)
var bufferSize = vertices2D.Count + 2;
var indexSize = vertices2D.Count * 3;
var arrays = new Array();
arrays.Resize((int)Mesh.ArrayType.Max);
// Build vertex, index, and uv lists
// Index mapping to build all triangles
var indices = new int[indexSize];
int currentVertexIndex = 1;
for (int i = 0; i < indexSize; i += 3)
{
indices[i] = 0;
indices[i + 1] = currentVertexIndex + 1;
indices[i + 2] = currentVertexIndex;
++currentVertexIndex;
}
// Write mesh data //
var vertices = new Vector3[bufferSize];
var uvs = new Vector2[bufferSize];
int writeIndex = 0;
writeIndex = InitializeCorrectMembrane(writeIndex, vertices, uvs);
if (writeIndex != bufferSize)
throw new Exception("Membrane buffer write ended up at wrong index");
// Godot might do this automatically
// // Set the bounds to get frustum culling and LOD to work correctly.
// // TODO: make this more accurate by calculating the actual extents
// m_mesh->_setBounds(Ogre::Aabb(Float3::ZERO, Float3::UNIT_SCALE * 50)
// /*, false*/);
// m_mesh->_setBoundingSphereRadius(50);
arrays[(int)Mesh.ArrayType.Vertex] = vertices;
arrays[(int)Mesh.ArrayType.Index] = indices;
arrays[(int)Mesh.ArrayType.TexUv] = uvs;
// Create the mesh
var generatedMesh = new ArrayMesh();
var surfaceIndex = generatedMesh.GetSurfaceCount();
generatedMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
// Apply the mesh to us
Mesh = generatedMesh;
SetSurfaceMaterial(surfaceIndex, MaterialToEdit);
ProceduralDataCache.Instance.WriteMembraneData(CreateDataForCache(generatedMesh, surfaceIndex));
}
private int InitializeCorrectMembrane(int writeIndex, Vector3[] vertices,
Vector2[] uvs)
{
// common variables
float height = 0.1f;
float multiplier = 2.0f * Mathf.Pi;
var center = new Vector2(0.5f, 0.5f);
// cell walls need obvious inner/outer membranes (we can worry
// about chitin later)
if (Type.CellWall)
{
height = 0.05f;
multiplier = Mathf.Pi;
}
vertices[writeIndex] = new Vector3(0, height / 2, 0);
uvs[writeIndex] = center;
++writeIndex;
for (int i = 0, end = vertices2D.Count; i < end + 1; i++)
{
// Finds the UV coordinates be projecting onto a plane and
// stretching to fit a circle.
float currentRadians = multiplier * i / end;
vertices[writeIndex] = new Vector3(vertices2D[i % end].x, height / 2,
vertices2D[i % end].y);
uvs[writeIndex] = center +
new Vector2(Mathf.Cos(currentRadians), Mathf.Sin(currentRadians)) / 2;
++writeIndex;
}
return writeIndex;
}
private void DrawMembrane(float cellDimensions, List<Vector2> sourceBuffer, List<Vector2> targetBuffer,
Func<Vector2, Vector2, Vector2> movementFunc)
{
while (targetBuffer.Count < sourceBuffer.Count)
targetBuffer.Add(new Vector2(0, 0));
// TODO: check that this is actually needed, and if triggered does the right thing
while (targetBuffer.Count > sourceBuffer.Count)
targetBuffer.RemoveAt(targetBuffer.Count - 1);
// Loops through all the points in the membrane and relocates them as necessary.
for (int i = 0, end = sourceBuffer.Count; i < end; i++)
{
var closestOrganelle = FindClosestOrganelleInRange(sourceBuffer[i], 3f);
if (closestOrganelle ==
new Vector2(INVALID_FOUND_ORGANELLE, INVALID_FOUND_ORGANELLE))
{
var distantOrganelle = FindCenterOfOrganellesInRange(sourceBuffer[i], 5f);
var midpoint = (sourceBuffer[(end + i - 1) % end] + sourceBuffer[(i + 1) % end]) / 2;
if (distantOrganelle == sourceBuffer[i])
{
targetBuffer[i] = midpoint;
}
else
{
var movementDirection = movementFunc(sourceBuffer[i], distantOrganelle + distantOrganelle + midpoint / 3);
targetBuffer[i] = new Vector2(sourceBuffer[i].x - movementDirection.x,
sourceBuffer[i].y - movementDirection.y);
}
}
else
{
var movementDirection = movementFunc(sourceBuffer[i], closestOrganelle);
targetBuffer[i] = new Vector2(sourceBuffer[i].x - movementDirection.x,
sourceBuffer[i].y - movementDirection.y);
}
}
// Allows for the addition and deletion of points in the membrane.
for (int i = 0; i < targetBuffer.Count - 1; ++i)
{
// Check to see if the gap between two points in the membrane is too big.
if ((targetBuffer[i] - targetBuffer[(i + 1) % targetBuffer.Count]).Length() >
cellDimensions / membraneResolution)
{
// Add an element after the ith term that is the average of the
// i and i+1 term.
var tempPoint = (targetBuffer[(i + 1) % targetBuffer.Count] + targetBuffer[i]) / 2;
targetBuffer.Insert(i + 1, tempPoint);
++i;
}
// Check to see if the gap between two points in the membrane is too small.
if ((targetBuffer[(i + 1) % targetBuffer.Count] -
targetBuffer[(i + targetBuffer.Count - 1) % targetBuffer.Count]).Length() <
cellDimensions / membraneResolution)
{
// Delete the ith term.
targetBuffer.RemoveAt(i);
}
}
}
private ComputedMembraneData CreateDataForCache(ArrayMesh mesh, int surfaceIndex)
{
// Need to copy our data here when caching it as if we get new organelles and change we would pollute the
// cache entry
return new ComputedMembraneData(OrganellePositions, Type, new List<Vector2>(vertices2D), mesh, surfaceIndex);
}
}