diffusion #12

Merged
ktyl merged 6 commits from diffusion into main 2022-12-09 01:36:48 +01:00
12 changed files with 279 additions and 31 deletions

21
gdd.md
View File

@ -28,6 +28,27 @@ Add a tick counter as a UI element.
Diffusing should use 'pull' operations; tiles should pull values from surrounding tiles. Diffusing should use 'pull' operations; tiles should pull values from surrounding tiles.
This is an attempt to think forwards to parallelising computation of diffusiong - ideally using the GPU. This is an attempt to think forwards to parallelising computation of diffusiong - ideally using the GPU.
#### Diffusion Algorithm
Diffusion is modelled as two-dimensional steady-state heat conduction.
This means that the amount of a value does not change between the start and end of the computation of the diffusion step.
The value at one 2D location is calculated based on the values of itself and its neighbours in the previous step.
A diffusion coefficient controls the rate of diffusion.
[Video explanation](https://www.youtube.com/watch?v=RGbV7T_iWT8)
[Derivation](https://www.tec-science.com/thermodynamics/heat/heat-equation-diffusion-equation/)
Implementation:
```cs
private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
```
### Tile Types ### Tile Types
Tiles start as wild tiles. Tiles start as wild tiles.

View File

@ -4,6 +4,6 @@
[resource] [resource]
shader = ExtResource( 1 ) shader = ExtResource( 1 )
shader_param/baseColor = Color( 0.992157, 1, 0, 1 ) shader_param/lowColor = Color( 1, 0.960784, 0, 1 )
shader_param/highlightColor = Color( 1, 0, 0.984314, 1 ) shader_param/highColor = Color( 0.921569, 0, 1, 1 )
shader_param/isHighlighted = 0 shader_param/t = null

View File

@ -5,6 +5,6 @@
[resource] [resource]
resource_local_to_scene = true resource_local_to_scene = true
shader = ExtResource( 1 ) shader = ExtResource( 1 )
shader_param/baseColor = Color( 0.0823529, 0, 1, 1 ) shader_param/lowColor = Color( 0, 0, 0, 1 )
shader_param/highlightColor = Color( 1, 0, 0, 1 ) shader_param/highColor = Color( 1, 0.560784, 0, 1 )
shader_param/isHighlighted = null shader_param/t = null

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/Clock.cs" type="Script" id=1]
[node name="Clock" type="Node"]
script = ExtResource( 1 )
_tickRate = 10.0

View File

@ -0,0 +1,36 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/DebugUI.cs" type="Script" id=1]
[node name="Debug UI" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
TicksLabelPath = NodePath("Ticks")
PauseLabelPath = NodePath("Paused")
[node name="Ticks" type="Label" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -10.0
margin_bottom = -30.0
grow_horizontal = 0
grow_vertical = 0
text = "ticks: 0"
align = 2
valign = 2
[node name="Paused" type="Label" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -10.0
margin_bottom = -10.0
grow_horizontal = 0
grow_vertical = 0
text = "pause: false"
align = 2
valign = 2

View File

@ -1,11 +1,23 @@
[gd_scene load_steps=3 format=2] [gd_scene load_steps=5 format=2]
[ext_resource path="res://nodes/grid.tscn" type="PackedScene" id=1] [ext_resource path="res://nodes/grid.tscn" type="PackedScene" id=1]
[ext_resource path="res://nodes/grid_cursor.tscn" type="PackedScene" id=2] [ext_resource path="res://nodes/grid_cursor.tscn" type="PackedScene" id=2]
[ext_resource path="res://nodes/debug_ui.tscn" type="PackedScene" id=3]
[ext_resource path="res://nodes/clock.tscn" type="PackedScene" id=4]
[node name="Root" type="Node2D"] [node name="Root" type="Node"]
[node name="Grid" parent="." instance=ExtResource( 1 )] [node name="Grid" parent="." instance=ExtResource( 1 )]
DiffusionCoefficient = 0.1
[node name="Cursor" parent="." instance=ExtResource( 2 )] [node name="Cursor" parent="." instance=ExtResource( 2 )]
Grid = NodePath("../Grid") Grid = NodePath("../Grid")
[node name="Debug UI" parent="." instance=ExtResource( 3 )]
[node name="Clock" parent="." instance=ExtResource( 4 )]
_autoTick = true
[connection signal="OnPauseChanged" from="Clock" to="Debug UI" method="_on_Clock_OnPauseChanged"]
[connection signal="OnTick" from="Clock" to="Grid" method="_on_Clock_OnTick"]
[connection signal="OnTick" from="Clock" to="Debug UI" method="_on_Clock_OnTick"]

View File

@ -0,0 +1,67 @@
using Godot;
using System;
public class Clock : Node
{
[Signal]
delegate void OnTick(int ticks);
[Signal]
delegate void OnPauseChanged(bool paused);
[Export(PropertyHint.None, "Number of ticks per second")]
private float _tickRate;
private float TickPeriod => 1.0f / _tickRate;
[Export]
private bool _autoTick;
private int _ticks = 0;
private bool _paused = false;
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
{
switch ((KeyList)keyEvent.Scancode)
{
case KeyList.Space:
if (_autoTick)
{
_paused = !_paused;
EmitSignal(nameof(OnPauseChanged), _paused);
return;
}
Tick();
break;
}
}
}
private float _secondsSinceLastTick;
public override void _Process(float delta)
{
base._Process(delta);
if (!_autoTick)
return;
if (_paused)
return;
_secondsSinceLastTick += delta;
if (_secondsSinceLastTick < TickPeriod)
return;
_secondsSinceLastTick -= TickPeriod;
Tick();
}
private void Tick()
{
_ticks++;
EmitSignal(nameof(OnTick), _ticks);
}
}

View File

@ -0,0 +1,37 @@
using Godot;
using System;
public class DebugUI : Control
{
[Export]
public NodePath TicksLabelPath { private get; set; }
private Label _ticksLabel;
[Export]
public NodePath PauseLabelPath { private get; set; }
private Label _pauseLabel;
public override void _Ready()
{
_ticksLabel = GetNode<Label>(TicksLabelPath);
_pauseLabel = GetNode<Label>(PauseLabelPath);
SetTicksText(0);
SetPauseText(false);
}
#region Signals
public void _on_Clock_OnPauseChanged(bool paused) => SetPauseText(paused);
public void _on_Clock_OnTick(int ticks) => SetTicksText(ticks);
#endregion
private void SetTicksText(int ticks)
{
_ticksLabel.Text = $"ticks: {ticks}";
}
private void SetPauseText(bool paused)
{
_pauseLabel.Text = $"pause: {paused}";
}
}

View File

@ -8,18 +8,18 @@ public class GridCursor : Sprite
private WorldGrid _grid; private WorldGrid _grid;
private ShaderMaterial _material; private ShaderMaterial _material;
private const string IS_HIGHLIGHTED = "isHighlighted"; private const string T = "t";
public override void _Ready() public override void _Ready()
{ {
_grid = GetNode<WorldGrid>(Grid); _grid = GetNode<WorldGrid>(Grid);
_material = (ShaderMaterial)Material; _material = (ShaderMaterial)Material;
} }
public override void _Input(InputEvent @event) public override void _Input(InputEvent @event)
{ {
base._Input(@event); base._Input(@event);
if (@event is InputEventMouseMotion mouseMoveEvent) if (@event is InputEventMouseMotion mouseMoveEvent)
{ {
var pos = mouseMoveEvent.Position; var pos = mouseMoveEvent.Position;
@ -37,12 +37,12 @@ public class GridCursor : Sprite
{ {
var pos = mouseButtonEvent.Position; var pos = mouseButtonEvent.Position;
_grid.GetGridPos(pos, out var x, out var y); _grid.GetGridPos(pos, out var x, out var y);
_grid.ToggleTileHighlight(x, y); _grid.SetTileValue(x, y, 1.0f);
_material.SetShaderParam(IS_HIGHLIGHTED, 1); _material.SetShaderParam(T, 1f);
} }
else else
{ {
_material.SetShaderParam(IS_HIGHLIGHTED, 0); _material.SetShaderParam(T, 0f);
} }
break; break;
} }

View File

@ -0,0 +1,8 @@
using Godot;
public struct Tile
{
public bool isHighlighted;
public float value;
public ShaderMaterial material;
}

View File

@ -8,16 +8,15 @@ public class WorldGrid : Node2D
[Export] [Export]
public int Size { get; set; } public int Size { get; set; }
[Export] [Export]
public int CellSize { get; set; } public int CellSize { get; set; }
private struct Tile [Export]
{ public float DiffusionCoefficient { get; set; } = 0.01f;
public bool isHighlighted;
public ShaderMaterial material;
}
private Tile[,] _tiles; private Tile[,] _tiles;
private float[,] _nextValues = null;
// Called when the node enters the scene tree for the first time. // Called when the node enters the scene tree for the first time.
public override void _Ready() public override void _Ready()
@ -28,18 +27,18 @@ public class WorldGrid : Node2D
public override void _Process(float delta) public override void _Process(float delta)
{ {
base._Process(delta); base._Process(delta);
for (int x = 0; x < Size; x++) for (int x = 0; x < Size; x++)
{ {
for (int y = 0; y < Size; y++) for (int y = 0; y < Size; y++)
{ {
var tile = _tiles[x, y]; var tile = _tiles[x, y];
var material = tile.material; var material = tile.material;
material.SetShaderParam("isHighlighted", tile.isHighlighted ? 1 : 0); material.SetShaderParam("t", tile.value);
} }
} }
} }
public bool IsInBounds(int x, int y) public bool IsInBounds(int x, int y)
{ {
return x >= 0 && x < Size && y >= 0 && y < Size; return x >= 0 && x < Size && y >= 0 && y < Size;
@ -53,6 +52,11 @@ public class WorldGrid : Node2D
_tiles[x, y].isHighlighted ^= true; _tiles[x, y].isHighlighted ^= true;
} }
public void SetTileValue(int x, int y, float value)
{
_tiles[x, y].value = value;
}
public void GetGridPos(Vector2 position, out int x, out int y) public void GetGridPos(Vector2 position, out int x, out int y)
{ {
x = Mathf.FloorToInt(position.x / CellSize); x = Mathf.FloorToInt(position.x / CellSize);
@ -75,8 +79,9 @@ public class WorldGrid : Node2D
private void GenerateGrid(int size) private void GenerateGrid(int size)
{ {
this.Clear(); this.Clear();
_tiles = new Tile[size, size]; _tiles = new Tile[size, size];
_nextValues = new float[size, size];
for (int x = 0; x < size; x++) for (int x = 0; x < size; x++)
{ {
@ -84,7 +89,8 @@ public class WorldGrid : Node2D
{ {
var tile = new Tile(); var tile = new Tile();
tile.isHighlighted = false; tile.isHighlighted = false;
tile.value = 0.0f;
var node = TileScene.Instance<Node2D>(); var node = TileScene.Instance<Node2D>();
this.AddChild(node); this.AddChild(node);
var position = new Vector2 var position = new Vector2
@ -95,9 +101,63 @@ public class WorldGrid : Node2D
node.Position = position; node.Position = position;
var canvasItem = (CanvasItem)node; var canvasItem = (CanvasItem)node;
tile.material = (ShaderMaterial)canvasItem.Material; tile.material = (ShaderMaterial)canvasItem.Material;
_tiles[x, y] = tile; _tiles[x, y] = tile;
} }
} }
} }
public void _on_Clock_OnTick(int ticks)
{
Diffuse();
}
private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
private void GetNeighbourTemperatures(int x, int y, out float nx, out float ny, out float px, out float py)
{
// default value
var t = _tiles[x, y].value;
nx = x > 0 ? _tiles[x - 1, y].value : t;
px = x < Size - 1 ? _tiles[x + 1, y].value : t;
ny = y > 0 ? _tiles[x, y - 1].value : t;
py = y < Size - 1 ? _tiles[x, y + 1].value : t;
}
private void Diffuse()
{
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
float t = _tiles[x, y].value;
var D = DiffusionCoefficient;
GetNeighbourTemperatures(
x, y,
out var nx,
out var ny,
out var px,
out var py);
// current value
_nextValues[x, y] = TransferHeat(t, D, nx, ny, px, py);
}
}
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
_tiles[x, y].value = _nextValues[x, y];
}
}
}
} }

View File

@ -1,9 +1,9 @@
shader_type canvas_item; shader_type canvas_item;
uniform vec4 baseColor : hint_color; uniform vec4 lowColor : hint_color;
uniform vec4 highlightColor : hint_color; uniform vec4 highColor : hint_color;
uniform int isHighlighted; uniform float t;
void fragment() { void fragment() {
COLOR = mix(baseColor, highlightColor, float(isHighlighted)); COLOR = mix(lowColor, highColor, t);
} }