2025-08-02 21:27:08 -07:00
@ tool
extends CompositorEffect
class_name AutomataCompositorEffect
2025-08-04 19:08:48 -07:00
@ export var pause = false
@ export_range ( 0.001 , 0.5 ) var update_speed = 0.05
@ export var current_seed : int = 0
@ export var random_seed = false
@ export var reseed = true
2025-08-02 21:27:08 -07:00
@ export_group ( " Shader Settings " )
@ export var exposure = Vector4 ( 2 , 1 , 1 , 1 )
var rd : RenderingDevice
var exposure_compute : ACompute
2025-08-03 17:58:17 -07:00
var automaton_texture1 : RID
var automaton_texture2 : RID
2025-08-07 02:26:37 -07:00
var world_texture : RID
2025-08-03 17:58:17 -07:00
var previous_generation : RID
var next_generation : RID
2025-08-07 02:26:37 -07:00
var world_image_texture : ImageTexture
2025-08-04 19:08:48 -07:00
var timer = 0.0
2025-08-03 17:58:17 -07:00
var needs_seeding = true
2025-08-02 21:27:08 -07:00
func _init ( ) :
effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
rd = RenderingServer . get_rendering_device ( )
# To make use of an existing ACompute shader we use its filename to access it, in this case, the example compute shader file is 'exposure_example.acompute'
2025-08-03 17:58:17 -07:00
exposure_compute = ACompute . new ( ' automata ' )
var automaton_resolution = 512
var automaton_format : RDTextureFormat = RDTextureFormat . new ( )
automaton_format . height = automaton_resolution
automaton_format . width = automaton_resolution
automaton_format . format = RenderingDevice . DATA_FORMAT_R16_UNORM
automaton_format . usage_bits = RenderingDevice . TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice . TEXTURE_USAGE_STORAGE_BIT | RenderingDevice . TEXTURE_USAGE_CAN_COPY_FROM_BIT | RenderingDevice . TEXTURE_USAGE_CAN_COPY_TO_BIT
automaton_texture1 = rd . texture_create ( automaton_format , RDTextureView . new ( ) , [ ] )
automaton_texture2 = rd . texture_create ( automaton_format , RDTextureView . new ( ) , [ ] )
previous_generation = automaton_texture1
next_generation = automaton_texture2
2025-08-04 19:08:48 -07:00
needs_seeding = true
2025-08-07 02:26:37 -07:00
var world_format : RDTextureFormat = RDTextureFormat . new ( )
world_format . height = 1024
world_format . width = 1024
world_format . format = RenderingDevice . DATA_FORMAT_R8G8B8A8_UNORM
world_format . usage_bits = RenderingDevice . TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice . TEXTURE_USAGE_STORAGE_BIT | RenderingDevice . TEXTURE_USAGE_CAN_COPY_FROM_BIT | RenderingDevice . TEXTURE_USAGE_CAN_COPY_TO_BIT
world_texture = rd . texture_create ( world_format , RDTextureView . new ( ) , [ ] )
2025-08-04 19:08:48 -07:00
2025-08-02 21:27:08 -07:00
func _notification ( what ) :
if what == NOTIFICATION_PREDELETE :
# ACompute will handle the freeing of any resources attached to it
exposure_compute . free ( )
2025-08-03 17:58:17 -07:00
rd . free_rid ( automaton_texture1 )
rd . free_rid ( automaton_texture2 )
2025-08-07 02:26:37 -07:00
rd . free_rid ( world_texture )
2025-08-02 21:27:08 -07:00
func _render_callback ( p_effect_callback_type , p_render_data ) :
if not enabled : return
if p_effect_callback_type != EFFECT_CALLBACK_TYPE_POST_TRANSPARENT : return
if not rd :
push_error ( " No rendering device " )
return
var render_scene_buffers : RenderSceneBuffersRD = p_render_data . get_render_scene_buffers ( )
if not render_scene_buffers :
push_error ( " No buffer to render to " )
return
var size = render_scene_buffers . get_internal_size ( )
if size . x == 0 and size . y == 0 :
push_error ( " Rendering to 0x0 buffer " )
return
var x_groups = ( size . x - 1 ) / 8 + 1
var y_groups = ( size . y - 1 ) / 8 + 1
var z_groups = 1
2025-08-03 17:58:17 -07:00
2025-08-04 19:08:48 -07:00
if ( reseed ) :
if ( random_seed ) : current_seed = randi ( ) % 10000
print ( current_seed )
2025-08-03 17:58:17 -07:00
2025-08-04 19:08:48 -07:00
needs_seeding = true
reseed = false
2025-08-03 17:58:17 -07:00
2025-08-02 21:27:08 -07:00
# Vulkan has a feature known as push constants which are like uniform sets but for very small amounts of data
2025-08-04 19:08:48 -07:00
var push_constant : PackedFloat32Array = PackedFloat32Array ( [ size . x , size . y , current_seed , 0.0 ] )
2025-08-02 21:27:08 -07:00
for view in range ( render_scene_buffers . get_view_count ( ) ) :
var input_image = render_scene_buffers . get_color_layer ( view )
# Pack the exposure vector into a byte array
var uniform_array = PackedFloat32Array ( [ exposure . x , exposure . y , exposure . z , exposure . w ] ) . to_byte_array ( )
# ACompute handles uniform caching under the hood, as long as the exposure value doesn't change or the render target doesn't change, these functions will only do work once
2025-08-07 02:26:37 -07:00
exposure_compute . set_texture ( 0 , world_texture )
2025-08-03 17:58:17 -07:00
exposure_compute . set_texture ( 2 , previous_generation )
exposure_compute . set_texture ( 3 , next_generation )
2025-08-02 21:27:08 -07:00
exposure_compute . set_uniform_buffer ( 1 , uniform_array )
exposure_compute . set_push_constant ( push_constant . to_byte_array ( ) )
# Dispatch the compute kernel
2025-08-03 17:58:17 -07:00
if ( needs_seeding ) :
exposure_compute . dispatch ( 0 , 512 / 8 , 512 / 8 , 1 )
needs_seeding = false
2025-08-04 19:08:48 -07:00
2025-08-03 17:58:17 -07:00
exposure_compute . dispatch ( 2 , x_groups , y_groups , z_groups )
2025-08-04 19:08:48 -07:00
if ( pause ) : return
if ( timer > update_speed ) :
timer = 0.0
exposure_compute . dispatch ( 1 , 512 / 8 , 512 / 8 , 1 )
var temp : RID = previous_generation
previous_generation = next_generation
next_generation = temp
2025-08-03 17:58:17 -07:00
2025-08-04 19:08:48 -07:00
timer += Engine . get_main_loop ( ) . root . get_process_delta_time ( )
2025-08-07 02:26:37 -07:00
func get_world_texture ( ) - > RID :
return world_texture ;