water_plane.gd 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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 90s, 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() -> void:
  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() -> void:
  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: InputEvent) -> void:
  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() -> void:
  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 not result.is_empty():
  64. # Transform our intersection point.
  65. var pos: Vector3 = 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. # We have w left over so we use it to indicate mouse is over our water plane.
  69. add_wave_point.w = 1.0
  70. else:
  71. add_wave_point.x = 0.0
  72. add_wave_point.y = 0.0
  73. add_wave_point.w = 0.0
  74. func _process(delta: float) -> void:
  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(), RID()]
  115. var texture_sets: Array[RID] = [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: Vector2i) -> void:
  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 = (
  142. RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT |
  143. RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT |
  144. RenderingDevice.TEXTURE_USAGE_STORAGE_BIT |
  145. RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT |
  146. RenderingDevice.TEXTURE_USAGE_CAN_COPY_TO_BIT
  147. )
  148. for i in 3:
  149. # Create our texture.
  150. texture_rds[i] = rd.texture_create(tf, RDTextureView.new(), [])
  151. # Make sure our textures are cleared.
  152. rd.texture_clear(texture_rds[i], Color(0, 0, 0, 0), 0, 1, 0, 1)
  153. # Now create our uniform set so we can use these textures in our shader.
  154. texture_sets[i] = _create_uniform_set(texture_rds[i])
  155. func _render_process(with_next_texture: int, wave_point: Vector4, tex_size: Vector2i, p_damp: float) -> void:
  156. # We don't have structures (yet) so we need to build our push constant
  157. # "the hard way"...
  158. var push_constant := PackedFloat32Array()
  159. push_constant.push_back(wave_point.x)
  160. push_constant.push_back(wave_point.y)
  161. push_constant.push_back(wave_point.z)
  162. push_constant.push_back(wave_point.w)
  163. push_constant.push_back(tex_size.x)
  164. push_constant.push_back(tex_size.y)
  165. push_constant.push_back(p_damp)
  166. push_constant.push_back(0.0)
  167. # Calculate our dispatch group size.
  168. # We do `n - 1 / 8 + 1` in case our texture size is not nicely
  169. # divisible by 8.
  170. # In combination with a discard check in the shader this ensures
  171. # we cover the entire texture.
  172. @warning_ignore("integer_division")
  173. var x_groups := (tex_size.x - 1) / 8 + 1
  174. @warning_ignore("integer_division")
  175. var y_groups := (tex_size.y - 1) / 8 + 1
  176. var next_set := texture_sets[with_next_texture]
  177. var current_set := texture_sets[(with_next_texture - 1) % 3]
  178. var previous_set := texture_sets[(with_next_texture - 2) % 3]
  179. # Run our compute shader.
  180. var compute_list := rd.compute_list_begin()
  181. rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
  182. rd.compute_list_bind_uniform_set(compute_list, current_set, 0)
  183. rd.compute_list_bind_uniform_set(compute_list, previous_set, 1)
  184. rd.compute_list_bind_uniform_set(compute_list, next_set, 2)
  185. rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
  186. rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1)
  187. rd.compute_list_end()
  188. # We don't need to sync up here, Godots default barriers will do the trick.
  189. # If you want the output of a compute shader to be used as input of
  190. # another computer shader you'll need to add a barrier:
  191. #rd.barrier(RenderingDevice.BARRIER_MASK_COMPUTE)
  192. func _free_compute_resources() -> void:
  193. # Note that our sets and pipeline are cleaned up automatically as they are dependencies :P
  194. for i in 3:
  195. if texture_rds[i]:
  196. rd.free_rid(texture_rds[i])
  197. if shader:
  198. rd.free_rid(shader)