From e2f1c8c13afbbc93a8167076380e7af9cf5e1fc8 Mon Sep 17 00:00:00 2001 From: Kasper Sauramo Date: Fri, 1 Aug 2025 19:18:36 +0300 Subject: [PATCH] first it worked, then i broke it and finally remembered to commit --- shaders/agents_cs.glsl | 62 +++++++++++++++++++++++------- shaders/decay_cs.glsl | 34 ++++++++++++----- slime_simulation.gd | 86 ++++++++++++++++++++++++++---------------- slime_simulation.tscn | 10 ++++- 4 files changed, 134 insertions(+), 58 deletions(-) diff --git a/shaders/agents_cs.glsl b/shaders/agents_cs.glsl index 3aa9410..686d56d 100644 --- a/shaders/agents_cs.glsl +++ b/shaders/agents_cs.glsl @@ -4,32 +4,66 @@ // Invocations in the (x, y, z) dimension layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; -layout(r32f, set = 0, binding = 0) uniform restrict readonly image2D current_trail; -layout(r32f, set = 1, binding = 0) uniform restrict writeonly image2D output_trail; -layout(set = 2, binding = 1) restrict buffer AgentBuffer { - vec2 positions[]; +layout(r32f, binding = 0) uniform restrict readonly image2D current_trail; +layout(r32f, binding = 1) uniform restrict writeonly image2D output_trail; +layout(binding = 2) restrict buffer AgentBuffer { + vec2 positions[1024*1024]; + float headings[1024*1024]; } agent_buffer; layout(push_constant, std430) uniform Params { - vec2 texture_size; + ivec2 texture_size; + float sensor_angle; + float sensor_distance; } params; +const float SPEED = 1.0; void main() { ivec2 size = ivec2(params.texture_size.x - 1, params.texture_size.y - 1); - vec2 position = agent_buffer.positions[gl_GlobalInvocationID.x]; - ivec2 uv = ivec2(gl_GlobalInvocationID.xy); + uint id = gl_GlobalInvocationID.x; + vec2 position = agent_buffer.positions[id]; + float heading = agent_buffer.headings[id]; - ivec2 tl = ivec2(0, 0); - float current_v = imageLoad(current_trail, clamp(uv - ivec2(0, 1), tl, size)).r; - float up_v = imageLoad(current_trail, clamp(uv - ivec2(0, 1), tl, size)).r; - float down_v = imageLoad(current_trail, clamp(uv + ivec2(0, 1), tl, size)).r; - float left_v = imageLoad(current_trail, clamp(uv - ivec2(1, 0), tl, size)).r; - float right_v = imageLoad(current_trail, clamp(uv + ivec2(1, 0), tl, size)).r; + float sa = params.sensor_angle; + float left_heading = heading - sa; + float right_heading = heading + sa; - float new_v = current_v + 0.05; + vec2 left_detect_dir = vec2(cos(left_heading), sin(left_heading)); + vec2 right_detect_dir = vec2(cos(right_heading), sin(right_heading)); + vec2 forward_detect_dir = vec2(cos(heading), sin(heading)); + + float sd = params.sensor_distance; + vec2 left_detect_pos = position + left_detect_dir * sd; + vec2 right_detect_pos = position + right_detect_dir * sd; + vec2 forward_detect_pos = position + forward_detect_dir * sd; + + left_detect_pos = mod(left_detect_pos, size); + right_detect_pos = mod(right_detect_pos, size); + forward_detect_pos = mod(forward_detect_pos, size); + + float left = imageLoad(current_trail, ivec2(left_detect_pos)).r; + float right = imageLoad(current_trail, ivec2(right_detect_pos)).r; + float forward = imageLoad(current_trail, ivec2(forward_detect_pos)).r; + + vec2 pos = position; + if (left > forward && left > right) { + pos += left_detect_dir * SPEED; + } else if (right > forward && right > left) { + pos += right_detect_dir * SPEED; + } else { + pos += forward_detect_dir * SPEED; + } + + pos = mod(pos, size); + agent_buffer.positions[id] = pos; + + ivec2 uv = ivec2(pos); + float current_v = imageLoad(current_trail, uv).r; + + float new_v = current_v + 0.3; vec4 result = vec4(new_v, new_v, new_v, 1.0); imageStore(output_trail, uv, result); diff --git a/shaders/decay_cs.glsl b/shaders/decay_cs.glsl index 598c93f..3a5221e 100644 --- a/shaders/decay_cs.glsl +++ b/shaders/decay_cs.glsl @@ -12,8 +12,17 @@ layout(r32f, set = 1, binding = 0) uniform restrict writeonly image2D output_ima layout(push_constant, std430) uniform Params { vec2 texture_size; float decay_factor; + float dissipation_factor; } params; +float get_dissipation_from(ivec2 uv) { + float v = imageLoad(current_image, uv).r; + return v * params.dissipation_factor; +} + +void store_value(ivec2 uv, vec4 v) { + imageStore(output_image, uv, v); +} // The code we want to execute in each invocation void main() { @@ -21,24 +30,31 @@ void main() { ivec2 size = ivec2(params.texture_size.x - 1, params.texture_size.y - 1); ivec2 uv = ivec2(gl_GlobalInvocationID.xy); + ivec2 tl = ivec2(0, 0); + + ivec2 up_uv = clamp(uv - ivec2(0, 1), tl, size); + ivec2 down_uv = clamp(uv + ivec2(0, 1), tl, size); + ivec2 left_uv = clamp(uv - ivec2(1, 0), tl, size); + ivec2 right_uv = clamp(uv + ivec2(1, 0), tl, size); + // Just in case the texture size is not divisable by 8. if ((uv.x > size.x) || (uv.y > size.y)) { return; } - ivec2 tl = ivec2(0, 0); float current_v = imageLoad(current_image, uv).r; - float up_v = imageLoad(current_image, clamp(uv - ivec2(0, 1), tl, size)).r; - float down_v = imageLoad(current_image, clamp(uv + ivec2(0, 1), tl, size)).r; - float left_v = imageLoad(current_image, clamp(uv - ivec2(1, 0), tl, size)).r; - float right_v = imageLoad(current_image, clamp(uv + ivec2(1, 0), tl, size)).r; float new_v = current_v * params.decay_factor; + new_v += get_dissipation_from(up_uv); + new_v += get_dissipation_from(down_uv); + new_v += get_dissipation_from(right_uv); + new_v += get_dissipation_from(left_uv); + + + new_v = clamp(new_v, 0, 1); - if (new_v < 0.0) { - new_v = 0.0; - } vec4 result = vec4(new_v, new_v, new_v, 1.0); - imageStore(output_image, uv, result); + + } diff --git a/slime_simulation.gd b/slime_simulation.gd index 1e66dda..3132c02 100644 --- a/slime_simulation.gd +++ b/slime_simulation.gd @@ -2,9 +2,18 @@ extends Node2D @export var texture_size: Vector2i = Vector2i(512, 512) @export var decay_factor: float = 0.98 +@export var dissipation_factor: float = 0.12 +@export var sensor_angle: float = 32.0: + get(): + return deg_to_rad(sensor_angle) +@export var sensor_distance: float = 3.86 +@export var rng_seed: int = 80085 @onready var mesh := $MeshInstance2D +## Needs to be changed from the shader as well +const AGENTS: int = 4096 + var texture: Texture2DRD var next_texture: int = 0 @@ -22,7 +31,7 @@ func _process(_delta: float) -> void: texture.texture_rd_rid = texture_rds[next_texture] RenderingServer.call_on_render_thread( - _render_process.bind(next_texture, texture_size, decay_factor) + _render_process.bind(next_texture, texture_size, decay_factor, dissipation_factor, sensor_angle, sensor_distance) ) @@ -50,9 +59,8 @@ var agent_pipeline: RID var texture_rds: Array[RID] = [RID(), RID()] var texture_sets: Array[RID] = [RID(), RID()] -var agent_uniform_sets: Array[RID] = [RID(), RID()] var agent_buffer: RID -var agent_set: RID +var agent_sets: Array[RID] = [RID(), RID()] func _load_shader(rd_: RenderingDevice, path: String) -> RID: @@ -69,7 +77,7 @@ func _create_trail_uniform(texture_rd: RID) -> RDUniform: return uniform -func _initialize_compute_code(init_with_texture_size: Vector2i) -> void: +func _initialize_compute_code(with_texture_size: Vector2i) -> void: # As this becomes part of our normal frame rendering, # we use our main rendering device here. rd = RenderingServer.get_rendering_device() @@ -83,26 +91,31 @@ func _initialize_compute_code(init_with_texture_size: Vector2i) -> void: # Create agent buffer var rng := RandomNumberGenerator.new() - rng.seed = 80085 - var agent_data := PackedVector2Array() - for n in 64 * 16: - var new_position: Vector2i = Vector2( - rng.randi_range(0, init_with_texture_size.x), - rng.randi_range(0, init_with_texture_size.y) + rng.seed = rng_seed + var agent_positions := PackedVector2Array() + var agent_angles := PackedFloat32Array() + # This must match what is set in the shader, if fixed size + for n in AGENTS: + var new_position: Vector2 = Vector2( + rng.randf_range(0, with_texture_size.x), + rng.randf_range(0, with_texture_size.y) ) - agent_data.push_back(new_position) + agent_positions.push_back(new_position) + agent_angles.push_back(rng.randf_range(0, TAU)) - var agent_size_in_bytes := 8 + var agent_data := PackedByteArray(agent_positions.to_byte_array()) + agent_data.append_array(agent_angles.to_byte_array()) agent_buffer = rd.storage_buffer_create( - agent_data.size() * agent_size_in_bytes, agent_data.to_byte_array() + agent_data.size(), agent_data, 0, + RenderingDevice.BUFFER_CREATION_AS_STORAGE_BIT ) # Create trail texture var tf: RDTextureFormat = RDTextureFormat.new() tf.format = RenderingDevice.DATA_FORMAT_R32_SFLOAT tf.texture_type = RenderingDevice.TEXTURE_TYPE_2D - tf.width = init_with_texture_size.x - tf.height = init_with_texture_size.y + tf.width = with_texture_size.x + tf.height = with_texture_size.y tf.depth = 1 tf.array_layers = 1 tf.mipmaps = 1 @@ -115,7 +128,7 @@ func _initialize_compute_code(init_with_texture_size: Vector2i) -> void: ) - var agent_uniforms = [] + var agent_uniforms: Array[RDUniform] = [] for i in 2: texture_rds[i] = rd.texture_create(tf, RDTextureView.new(), []) # Make sure our textures are cleared. @@ -123,17 +136,28 @@ func _initialize_compute_code(init_with_texture_size: Vector2i) -> void: var trail_uniform := _create_trail_uniform(texture_rds[i]) texture_sets[i] = rd.uniform_set_create([trail_uniform], decay_shader, 0) - agent_uniform_sets[i] = rd.uniform_set_create([trail_uniform], agent_shader, 0) + var txtr_uniform_1 := RDUniform.new() + txtr_uniform_1.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + txtr_uniform_1.binding = 0 + txtr_uniform_1.add_id(texture_rds[0]) + agent_uniforms.append(txtr_uniform_1) + var txtr_uniform_2 := RDUniform.new() + txtr_uniform_2.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + txtr_uniform_2.binding = 1 + txtr_uniform_2.add_id(texture_rds[1]) + agent_uniforms.append(txtr_uniform_2) + var uniform := RDUniform.new() uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER - uniform.binding = 1 + uniform.binding = 2 uniform.add_id(agent_buffer) - agent_set = rd.uniform_set_create([uniform], agent_shader, 0) + agent_sets[0] = rd.uniform_set_create([txtr_uniform_1, txtr_uniform_2, uniform], agent_shader, 0) + agent_sets[1] = rd.uniform_set_create([txtr_uniform_2, txtr_uniform_1, uniform], agent_shader, 0) -func _render_process(with_next_texture: int, tex_size: Vector2i, decay_fctr: float) -> void: +func _render_process(with_next_texture: int, tex_size: Vector2i, decay_fctr: float, dissipation_fctr: float, snsr_angle: float, snsr_distance: float) -> void: var next_set := texture_sets[with_next_texture] var current_set := texture_sets[(with_next_texture - 1) % 2] @@ -141,19 +165,18 @@ func _render_process(with_next_texture: int, tex_size: Vector2i, decay_fctr: flo var push_constant := PackedFloat32Array() push_constant.push_back(tex_size.x) push_constant.push_back(tex_size.y) - push_constant.push_back(0.0) - push_constant.push_back(0.0) + push_constant.push_back(snsr_angle) + push_constant.push_back(snsr_distance) var agent_compute_list := rd.compute_list_begin() rd.compute_list_bind_compute_pipeline(agent_compute_list, agent_pipeline) - rd.compute_list_bind_uniform_set(agent_compute_list, current_set, 0) - rd.compute_list_bind_uniform_set(agent_compute_list, next_set, 1) - rd.compute_list_bind_uniform_set(agent_compute_list, agent_set, 2) + rd.compute_list_bind_uniform_set(agent_compute_list, agent_sets[with_next_texture], 0) rd.compute_list_set_push_constant( agent_compute_list, push_constant.to_byte_array(), push_constant.size() * 4 ) - rd.compute_list_dispatch(agent_compute_list, 16, 1, 1) + @warning_ignore("integer_division") + rd.compute_list_dispatch(agent_compute_list, 64, 1, 1) rd.compute_list_end() # Decay and dissipate @@ -161,7 +184,7 @@ func _render_process(with_next_texture: int, tex_size: Vector2i, decay_fctr: flo push_constant.push_back(tex_size.x) push_constant.push_back(tex_size.y) push_constant.push_back(decay_fctr) - push_constant.push_back(0.0) + push_constant.push_back(dissipation_fctr) @warning_ignore("integer_division") var x_groups := (tex_size.x - 1) / 8 + 1 @@ -179,18 +202,15 @@ func _render_process(with_next_texture: int, tex_size: Vector2i, decay_fctr: flo rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1) rd.compute_list_end() - # We don't need to sync up here, Godots default barriers will do the trick. - # If you want the output of a compute shader to be used as input of - # another computer shader you'll need to add a barrier: - #rd.barrier(RenderingDevice.BARRIER_MASK_COMPUTE) - func _free_compute_resources() -> void: # Note that our sets and pipeline are cleaned up automatically # as they are dependencies :P - for i in 3: + for i in 2: if texture_rds[i]: rd.free_rid(texture_rds[i]) if decay_shader: rd.free_rid(decay_shader) + if agent_shader: + rd.free_rid(agent_shader) diff --git a/slime_simulation.tscn b/slime_simulation.tscn index 93127b2..a341f8d 100644 --- a/slime_simulation.tscn +++ b/slime_simulation.tscn @@ -13,9 +13,15 @@ shader_parameter/trail_tex = SubResource("Texture2DRD_pxe3a") [node name="SlimeSimulation" type="Node2D"] script = ExtResource("1_pxe3a") +texture_size = Vector2i(1152, 648) +decay_factor = 0.75 +dissipation_factor = 0.04 +sensor_angle = 27.0 +sensor_distance = 5.0 [node name="MeshInstance2D" type="MeshInstance2D" parent="."] +self_modulate = Color(0.581022, 0.701395, 0.890748, 1) material = SubResource("ShaderMaterial_pxe3a") -position = Vector2(256, 256) -scale = Vector2(512, 512) +position = Vector2(576, 324) +scale = Vector2(1152, 648) mesh = SubResource("QuadMesh_pxe3a")