water_plane.gd 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. @tool
  2. extends Area3D
  3. ############################################################################
  4. # Water ripple effect shader - Bastiaan Olij
  5. #
  6. # This is an example of how to implement a more complex compute shader
  7. # in Godot and making use of the new Custom Texture RD API added to
  8. # the RenderingServer.
  9. #
  10. # If thread model is set to Multi-Threaded the code related to compute will
  11. # run on the render thread. This is needed as we want to add our logic to
  12. # the normal rendering pipeline for this thread.
  13. #
  14. # The effect itself is an implementation of the classic ripple effect
  15. # that has been around since the 90ies but in a compute shader.
  16. # If someone knows if the original author ever published a paper I could
  17. # quote, please let me know :)
  18. @export var rain_size : float = 3.0
  19. @export var mouse_size : float = 5.0
  20. @export var texture_size : Vector2i = Vector2i(512, 512)
  21. @export_range(1.0, 10.0, 0.1) var damp : float = 1.0
  22. var t = 0.0
  23. var max_t = 0.1
  24. var texture : Texture2DRD
  25. var next_texture : int = 0
  26. var add_wave_point : Vector4
  27. var mouse_pos : Vector2
  28. var mouse_pressed : bool = false
  29. # Called when the node enters the scene tree for the first time.
  30. func _ready():
  31. # In case we're running stuff on the rendering thread
  32. # we need to do our initialisation on that thread.
  33. RenderingServer.call_on_render_thread(_initialize_compute_code.bind(texture_size))
  34. # Get our texture from our material so we set our RID.
  35. var material : ShaderMaterial = $MeshInstance3D.material_override
  36. if material:
  37. material.set_shader_parameter("effect_texture_size", texture_size)
  38. # Get our texture object.
  39. texture = material.get_shader_parameter("effect_texture")
  40. func _exit_tree():
  41. # Make sure we clean up!
  42. if texture:
  43. texture.texture_rd_rid = RID()
  44. RenderingServer.call_on_render_thread(_free_compute_resources)
  45. func _unhandled_input(event):
  46. # If tool enabled, we don't want to handle our input in the editor.
  47. if Engine.is_editor_hint():
  48. return
  49. if event is InputEventMouseMotion or event is InputEventMouseButton:
  50. mouse_pos = event.global_position
  51. if event is InputEventMouseButton and event.button_index == MouseButton.MOUSE_BUTTON_LEFT:
  52. mouse_pressed = event.pressed
  53. func _check_mouse_pos():
  54. # This is a mouse event, do a raycast.
  55. var camera = get_viewport().get_camera_3d()
  56. var parameters = PhysicsRayQueryParameters3D.new()
  57. parameters.from = camera.project_ray_origin(mouse_pos)
  58. parameters.to = parameters.from + camera.project_ray_normal(mouse_pos) * 100.0
  59. parameters.collision_mask = 1
  60. parameters.collide_with_bodies = false
  61. parameters.collide_with_areas = true
  62. var result = get_world_3d().direct_space_state.intersect_ray(parameters)
  63. if result.size() > 0:
  64. # Transform our intersection point.
  65. var pos = global_transform.affine_inverse() * result.position
  66. add_wave_point.x = clamp(pos.x / 5.0, -0.5, 0.5) * texture_size.x + 0.5 * texture_size.x
  67. add_wave_point.y = clamp(pos.z / 5.0, -0.5, 0.5) * texture_size.y + 0.5 * texture_size.y
  68. add_wave_point.w = 1.0 # We have w left over so we use it to indicate mouse is over our water plane.
  69. else:
  70. add_wave_point.x = 0.0
  71. add_wave_point.y = 0.0
  72. add_wave_point.w = 0.0
  73. # Called every frame. 'delta' is the elapsed time since the previous frame.
  74. func _process(delta):
  75. # If tool is enabled, ignore mouse input.
  76. if Engine.is_editor_hint():
  77. add_wave_point.w = 0.0
  78. else:
  79. # Check where our mouse intersects our area, can change if things move.
  80. _check_mouse_pos()
  81. # If we're not using the mouse, animate water drops, we (ab)used our W for this.
  82. if add_wave_point.w == 0.0:
  83. t += delta
  84. if t > max_t:
  85. t = 0
  86. add_wave_point.x = randi_range(0, texture_size.x)
  87. add_wave_point.y = randi_range(0, texture_size.y)
  88. add_wave_point.z = rain_size
  89. else:
  90. add_wave_point.z = 0.0
  91. else:
  92. add_wave_point.z = mouse_size if mouse_pressed else 0.0
  93. # Increase our next texture index.
  94. next_texture = (next_texture + 1) % 3
  95. # Update our texture to show our next result (we are about to create).
  96. # Note that `_initialize_compute_code` may not have run yet so the first
  97. # frame this my be an empty RID.
  98. if texture:
  99. texture.texture_rd_rid = texture_rds[next_texture]
  100. # While our render_process may run on the render thread it will run before our texture
  101. # is used and thus our next_rd will be populated with our next result.
  102. # It's probably overkill to sent texture_size and damp as parameters as these are static
  103. # but we sent add_wave_point as it may be modified while process runs in parallel.
  104. RenderingServer.call_on_render_thread(_render_process.bind(next_texture, add_wave_point, texture_size, damp))
  105. ###############################################################################
  106. # Everything after this point is designed to run on our rendering thread.
  107. var rd : RenderingDevice
  108. var shader : RID
  109. var pipeline : RID
  110. # We use 3 textures:
  111. # - One to render into
  112. # - One that contains the last frame rendered
  113. # - One for the frame before that
  114. var texture_rds : Array = [ RID(), RID(), RID() ]
  115. var texture_sets : Array = [ RID(), RID(), RID() ]
  116. func _create_uniform_set(texture_rd : RID) -> RID:
  117. var uniform := RDUniform.new()
  118. uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
  119. uniform.binding = 0
  120. uniform.add_id(texture_rd)
  121. # Even though we're using 3 sets, they are identical, so we're kinda cheating.
  122. return rd.uniform_set_create([uniform], shader, 0)
  123. func _initialize_compute_code(init_with_texture_size):
  124. # As this becomes part of our normal frame rendering,
  125. # we use our main rendering device here.
  126. rd = RenderingServer.get_rendering_device()
  127. # Create our shader.
  128. var shader_file = load("res://water_plane/water_compute.glsl")
  129. var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
  130. shader = rd.shader_create_from_spirv(shader_spirv)
  131. pipeline = rd.compute_pipeline_create(shader)
  132. # Create our textures to manage our wave.
  133. var tf : RDTextureFormat = RDTextureFormat.new()
  134. tf.format = RenderingDevice.DATA_FORMAT_R32_SFLOAT
  135. tf.texture_type = RenderingDevice.TEXTURE_TYPE_2D
  136. tf.width = init_with_texture_size.x
  137. tf.height = init_with_texture_size.y
  138. tf.depth = 1
  139. tf.array_layers = 1
  140. tf.mipmaps = 1
  141. tf.usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT + RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_COPY_TO_BIT
  142. for i in range(3):
  143. # Create our texture.
  144. texture_rds[i] = rd.texture_create(tf, RDTextureView.new(), [])
  145. # Make sure our textures are cleared.
  146. rd.texture_clear(texture_rds[i], Color(0, 0, 0, 0), 0, 1, 0, 1)
  147. # Now create our uniform set so we can use these textures in our shader.
  148. texture_sets[i] = _create_uniform_set(texture_rds[i])
  149. func _render_process(with_next_texture, wave_point, tex_size, damp):
  150. # We don't have structures (yet) so we need to build our push constant
  151. # "the hard way"...
  152. var push_constant : PackedFloat32Array = PackedFloat32Array()
  153. push_constant.push_back(wave_point.x)
  154. push_constant.push_back(wave_point.y)
  155. push_constant.push_back(wave_point.z)
  156. push_constant.push_back(wave_point.w)
  157. push_constant.push_back(tex_size.x)
  158. push_constant.push_back(tex_size.y)
  159. push_constant.push_back(damp)
  160. push_constant.push_back(0.0)
  161. # Calculate our dispatch group size.
  162. # We do `n - 1 / 8 + 1` in case our texture size is not nicely
  163. # divisible by 8.
  164. # In combination with a discard check in the shader this ensures
  165. # we cover the entire texture.
  166. var x_groups = (tex_size.x - 1) / 8 + 1
  167. var y_groups = (tex_size.y - 1) / 8 + 1
  168. var next_set = texture_sets[with_next_texture]
  169. var current_set = texture_sets[(with_next_texture - 1) % 3]
  170. var previous_set = texture_sets[(with_next_texture - 2) % 3]
  171. # Run our compute shader.
  172. var compute_list := rd.compute_list_begin()
  173. rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
  174. rd.compute_list_bind_uniform_set(compute_list, current_set, 0)
  175. rd.compute_list_bind_uniform_set(compute_list, previous_set, 1)
  176. rd.compute_list_bind_uniform_set(compute_list, next_set, 2)
  177. rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
  178. rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1)
  179. rd.compute_list_end()
  180. # We don't need to sync up here, Godots default barriers will do the trick.
  181. # If you want the output of a compute shader to be used as input of
  182. # another computer shader you'll need to add a barrier:
  183. #rd.barrier(RenderingDevice.BARRIER_MASK_COMPUTE)
  184. func _free_compute_resources():
  185. # Note that our sets and pipeline are cleaned up automatically as they are dependencies :P
  186. for i in range(3):
  187. if texture_rds[i]:
  188. rd.free_rid(texture_rds[i])
  189. if shader:
  190. rd.free_rid(shader)