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.
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
Tiles start as wild tiles.

View File

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

View File

@ -5,6 +5,6 @@
[resource]
resource_local_to_scene = true
shader = ExtResource( 1 )
shader_param/baseColor = Color( 0.0823529, 0, 1, 1 )
shader_param/highlightColor = Color( 1, 0, 0, 1 )
shader_param/isHighlighted = null
shader_param/lowColor = Color( 0, 0, 0, 1 )
shader_param/highColor = Color( 1, 0.560784, 0, 1 )
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_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 )]
DiffusionCoefficient = 0.1
[node name="Cursor" parent="." instance=ExtResource( 2 )]
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 ShaderMaterial _material;
private const string IS_HIGHLIGHTED = "isHighlighted";
private const string T = "t";
public override void _Ready()
{
_grid = GetNode<WorldGrid>(Grid);
_material = (ShaderMaterial)Material;
}
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (@event is InputEventMouseMotion mouseMoveEvent)
{
var pos = mouseMoveEvent.Position;
@ -37,12 +37,12 @@ public class GridCursor : Sprite
{
var pos = mouseButtonEvent.Position;
_grid.GetGridPos(pos, out var x, out var y);
_grid.ToggleTileHighlight(x, y);
_material.SetShaderParam(IS_HIGHLIGHTED, 1);
_grid.SetTileValue(x, y, 1.0f);
_material.SetShaderParam(T, 1f);
}
else
{
_material.SetShaderParam(IS_HIGHLIGHTED, 0);
_material.SetShaderParam(T, 0f);
}
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]
public int Size { get; set; }
[Export]
public int CellSize { get; set; }
private struct Tile
{
public bool isHighlighted;
public ShaderMaterial material;
}
[Export]
public float DiffusionCoefficient { get; set; } = 0.01f;
private Tile[,] _tiles;
private float[,] _nextValues = null;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
@ -28,18 +27,18 @@ public class WorldGrid : Node2D
public override void _Process(float delta)
{
base._Process(delta);
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
var tile = _tiles[x, y];
var material = tile.material;
material.SetShaderParam("isHighlighted", tile.isHighlighted ? 1 : 0);
material.SetShaderParam("t", tile.value);
}
}
}
public bool IsInBounds(int x, int y)
{
return x >= 0 && x < Size && y >= 0 && y < Size;
@ -53,6 +52,11 @@ public class WorldGrid : Node2D
_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)
{
x = Mathf.FloorToInt(position.x / CellSize);
@ -75,8 +79,9 @@ public class WorldGrid : Node2D
private void GenerateGrid(int size)
{
this.Clear();
_tiles = new Tile[size, size];
_nextValues = new float[size, size];
for (int x = 0; x < size; x++)
{
@ -84,7 +89,8 @@ public class WorldGrid : Node2D
{
var tile = new Tile();
tile.isHighlighted = false;
tile.value = 0.0f;
var node = TileScene.Instance<Node2D>();
this.AddChild(node);
var position = new Vector2
@ -95,9 +101,63 @@ public class WorldGrid : Node2D
node.Position = position;
var canvasItem = (CanvasItem)node;
tile.material = (ShaderMaterial)canvasItem.Material;
_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;
uniform vec4 baseColor : hint_color;
uniform vec4 highlightColor : hint_color;
uniform int isHighlighted;
uniform vec4 lowColor : hint_color;
uniform vec4 highColor : hint_color;
uniform float t;
void fragment() {
COLOR = mix(baseColor, highlightColor, float(isHighlighted));
COLOR = mix(lowColor, highColor, t);
}