# Copyright © 2022 Kasper Arnklit Frandsen - MIT License # See `LICENSE.md` included in the source distribution for details. @tool extends Node3D const WaterHelperMethods = preload("./water_helper_methods.gd") const FILTER_RENDERER_PATH = "res://addons/waterways/filter_renderer.tscn" const FLOW_OFFSET_NOISE_TEXTURE_PATH = "res://addons/waterways/textures/flow_offset_noise.png" const FOAM_NOISE_PATH = "res://addons/waterways/textures/foam_noise.png" const MATERIAL_CATEGORIES = { albedo_ = "Albedo", emission_ = "Emission", transparency_ = "Transparency", flow_ = "Flow", foam_ = "Foam", custom_ = "Custom" } enum SHADER_TYPES {WATER, LAVA, CUSTOM} const BUILTIN_SHADERS = [ { name = "Water", shader_path = "res://addons/waterways/shaders/river.gdshader", texture_paths = [ { name = "normal_bump_texture", path = "res://addons/waterways/textures/water1_normal_bump.png" } ] }, { name = "Lava", shader_path = "res://addons/waterways/shaders/lava.gdshader", texture_paths = [ { name = "normal_bump_texture", path = "res://addons/waterways/textures/lava_normal_bump.png" }, { name = "emission_texture", path = "res://addons/waterways/textures/lava_emission.png" } ] } ] const DEBUG_SHADER = { name = "Debug", shader_path = "res://addons/waterways/shaders/river_debug.gdshader", texture_paths = [ { name = "debug_pattern", path = "res://addons/waterways/textures/debug_pattern.png" }, { name = "debug_arrow", path = "res://addons/waterways/textures/debug_arrow.svg" } ] } const DEFAULT_PARAMETERS = { shape_step_length_divs = 1, shape_step_width_divs = 1, shape_smoothness = 0.5, mat_shader_type = 0, mat_custom_shader = null, baking_resolution = 2, baking_raycast_distance = 10.0, baking_raycast_layers = 1, baking_dilate = 0.6, baking_flowmap_blur = 0.04, baking_foam_cutoff = 0.9, baking_foam_offset = 0.1, baking_foam_blur = 0.02, lod_lod0_distance = 50.0, } # Shape Properties var shape_step_length_divs : int = 1: set = set_step_length_divs var shape_step_width_divs : int = 1: set = set_step_width_divs var shape_smoothness : float = 0.5: set = set_smoothness # Material Properties that not handled in shader var mat_shader_type : SHADER_TYPES: set = set_shader_type var mat_custom_shader : Shader: set = set_custom_shader # LOD Properties var lod_lod0_distance : float = 50.0: set = set_lod0_distance # Bake Properties var baking_resolution : int = 2 var baking_raycast_distance : float = 10.0 var baking_raycast_layers : int = 1 var baking_dilate : float = 0.6 var baking_flowmap_blur : float = 0.04 var baking_foam_cutoff : float = 0.9 var baking_foam_offset : float = 0.1 var baking_foam_blur : float = 0.02 # Public variables var curve : Curve3D var widths := [1.0, 1.0]: set = set_widths var valid_flowmap := false var debug_view : int = 0: set = set_debug_view var mesh_instance : MeshInstance3D var flow_foam_noise : Texture2D var dist_pressure : Texture2D # Private variables var _steps : int = 2 var _st : SurfaceTool var _mdt : MeshDataTool var _debug_material : ShaderMaterial var _first_enter_tree := true var _filter_renderer : PackedScene # Serialised private variables var _material : ShaderMaterial var _selected_shader : int = SHADER_TYPES.WATER var _uv2_sides : int # river_changed used to update handles when values are changed on script side # progress_notified used to up progress bar when baking maps # albedo_set is needed since the gradient is a custom inspector that needs a signal to update from script side signal river_changed signal progress_notified #signal albedo_set # Internal Methods func _get_property_list() -> Array: var props = [ { name = "Shape", type = TYPE_NIL, hint_string = "shape_", usage = PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "shape_step_length_divs", type = TYPE_INT, hint = PROPERTY_HINT_RANGE, hint_string = "1, 8", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "shape_step_width_divs", type = TYPE_INT, hint = PROPERTY_HINT_RANGE, hint_string = "1, 8", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "shape_smoothness", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.1, 5.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "Material", type = TYPE_NIL, hint_string = "mat_", usage = PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "mat_shader_type", type = TYPE_INT, hint = PROPERTY_HINT_ENUM, hint_string = "Water, Lava, Custom", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "mat_custom_shader", type = TYPE_OBJECT, hint = PROPERTY_HINT_RESOURCE_TYPE, usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE, hint_string = "Shader" }, ] var props2 = [] var mat_categories = MATERIAL_CATEGORIES.duplicate(true) if _material.shader != null: var shader_params := RenderingServer.get_shader_parameter_list(_material.shader.get_rid()) for p in shader_params: if p.name.begins_with("i_"): continue var hit_category = null for category in mat_categories: if p.name.begins_with(category): props2.append({ name = str("Material/", mat_categories[category]), type = TYPE_NIL, hint_string = str("mat_", category), usage = PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SCRIPT_VARIABLE }) hit_category = category break if hit_category != null: mat_categories.erase(hit_category) var cp := {} for k in p: cp[k] = p[k] cp.name = str("mat_", p.name) if "curve" in cp.name: cp.hint = PROPERTY_HINT_EXP_EASING cp.hint_string = "EASE" props2.append(cp) var props3 = [ { name = "Lod", type = TYPE_NIL, hint_string = "lod_", usage = PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "lod_lod0_distance", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "5.0, 200.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "Baking", type = TYPE_NIL, hint_string = "baking_", usage = PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_resolution", type = TYPE_INT, hint = PROPERTY_HINT_ENUM, hint_string = "64, 128, 256, 512, 1024", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_raycast_distance", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 100.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_raycast_layers", type = TYPE_INT, hint = PROPERTY_HINT_LAYERS_3D_PHYSICS, usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_dilate", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_flowmap_blur", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_foam_cutoff", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_foam_offset", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, { name = "baking_foam_blur", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0", usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE }, # Serialize these values without exposing it in the inspector { name = "curve", type = TYPE_OBJECT, usage = PROPERTY_USAGE_STORAGE }, { name = "widths", type = TYPE_ARRAY, usage = PROPERTY_USAGE_STORAGE }, { name = "valid_flowmap", type = TYPE_BOOL, usage = PROPERTY_USAGE_STORAGE }, { name = "flow_foam_noise", type = TYPE_OBJECT, usage = PROPERTY_USAGE_STORAGE }, { name = "dist_pressure", type = TYPE_OBJECT, usage = PROPERTY_USAGE_STORAGE }, { name = "_material", type = TYPE_OBJECT, hint = PROPERTY_HINT_RESOURCE_TYPE, hint_string = "ShaderMaterial", usage = PROPERTY_USAGE_STORAGE }, { name = "_selected_shader", type = TYPE_INT, usage = PROPERTY_USAGE_STORAGE }, { name = "_uv2_sides", type = TYPE_INT, usage = PROPERTY_USAGE_STORAGE } ] var combined_props = props + props2 + props3 # TODO, remember to remove this # print(var2str(combined_props)) return combined_props func _set(property: StringName, value) -> bool: if str(property).begins_with("mat_"): # TODO, is there a better way to do this, now that right() has changed? var param_name = str(property).replace("mat_", "") _material.set_shader_parameter(param_name, value) return true return false func _get(property : StringName): #print("in _get(), property: ", property) if str(property).begins_with("mat_"): var param_name = str(property).replace("mat_", "") #print("property name in _get is: ", property) #print("param name in _get is: ", param_name) #print ("_material.get_shader_parameter(param_name): ", _material.get_shader_parameter(param_name)) return _material.get_shader_parameter(param_name) # TODO - This doesn't currently work in Godot 4. https://github.com/godotengine/godot/issues/69335 #func _property_can_revert(property : StringName) -> bool: # if str(property).begins_with("mat_"): ## if "color" in property: ## # TODO - we are disabling revert for color parameters due to this ## # bug: https://github.com/godotengine/godot/issues/45388 ## return false # var param_name = str(property).replace("mat_", "") # return _material._property_can_revert(str("shader_param/", param_name)) # # if not DEFAULT_PARAMETERS.has(property): # return false # if get(property) != DEFAULT_PARAMETERS[property]: # return true # return false # # #func _property_get_revert(property : StringName): # if str(property).begins_with("mat_"): # var param_name = str(property).replace("mat_", "") # var revert_value = _material._property_get_revert(str("shader_param/", param_name)) # return revert_value func _init() -> void: _st = SurfaceTool.new() _mdt = MeshDataTool.new() _filter_renderer = load(FILTER_RENDERER_PATH) _debug_material = ShaderMaterial.new() _debug_material.shader = load(DEBUG_SHADER.shader_path) as Shader for texture in DEBUG_SHADER.texture_paths: _debug_material.set_shader_parameter(texture.name, load(texture.path) as Texture2D) _material = ShaderMaterial.new() _material.shader = load(BUILTIN_SHADERS[mat_shader_type].shader_path) as Shader for texture in BUILTIN_SHADERS[mat_shader_type].texture_paths: _material.set_shader_parameter(texture.name, load(texture.path) as Texture2D) # Have to manually set the color or it does not default right. Not sure how to work around this _material.set_shader_parameter("albedo_color", Transform3D(Vector3(0.0, 0.8, 1.0), Vector3(0.15, 0.2, 0.5), Vector3.ZERO, Vector3.ZERO)) func _enter_tree() -> void: if Engine.is_editor_hint() and _first_enter_tree: _first_enter_tree = false if not curve: curve = Curve3D.new() curve.bake_interval = 0.05 curve.add_point(Vector3(0.0, 0.0, 0.0), Vector3(0.0, 0.0, -0.25), Vector3(0.0, 0.0, 0.25)) curve.add_point(Vector3(0.0, 0.0, 1.0), Vector3(0.0, 0.0, -0.25), Vector3(0.0, 0.0, 0.25)) if get_child_count() <= 0: ## This is what happens on creating a new river var new_mesh_instance := MeshInstance3D.new() new_mesh_instance.name = "RiverMeshInstance" add_child(new_mesh_instance) mesh_instance = get_child(0) as MeshInstance3D _generate_river() else: mesh_instance = get_child(0) as MeshInstance3D _material = mesh_instance.mesh.surface_get_material(0) as ShaderMaterial set_materials("i_valid_flowmap", valid_flowmap) set_materials("i_uv2_sides", _uv2_sides) set_materials("i_distmap", dist_pressure) set_materials("i_flowmap", flow_foam_noise) set_materials("i_texture_foam_noise", load(FOAM_NOISE_PATH) as Texture2D) func _get_configuration_warning() -> String: if valid_flowmap: return "" else: return "No flowmap is set. Select River -> Generate Flow & Foam Map to generate and assign one." func get_transformed_aabb() -> AABB: return global_transform * mesh_instance.get_aabb() # Public Methods - These should all be good to use as API from other scripts func add_point(position : Vector3, index : int, dir : Vector3 = Vector3.ZERO, width : float = 0.0) -> void: if index == -1: var last_index := curve.get_point_count() - 1 var dist = position.distance_to(curve.get_point_position(last_index)) var new_dir = dir if dir != Vector3.ZERO else (position - curve.get_point_position(last_index) - curve.get_point_out(last_index) ).normalized() * 0.25 * dist curve.add_point(position, -new_dir, new_dir, -1) widths.append(widths[widths.size() - 1]) # If this is a new point at the end, add a width that's the same as last else: var dist = curve.get_point_position(index).distance_to(curve.get_point_position(index + 1)) var new_dir = dir if dir != Vector3.ZERO else (curve.get_point_position(index + 1) - curve.get_point_position(index)).normalized() * 0.25 * dist curve.add_point(position, -new_dir, new_dir, index + 1) var new_width = width if width != 0.0 else (widths[index] + widths[index + 1]) / 2.0 widths.insert(index + 1, new_width) # We set the width to the average of the two surrounding widths emit_signal("river_changed") _generate_river() func remove_point(index : int) -> void: # We don't allow rivers shorter than 2 points if curve.get_point_count() <= 2: return curve.remove_point(index) widths.remove_at(index) emit_signal("river_changed") _generate_river() func bake_texture() -> void: _generate_river() _generate_flowmap(pow(2, 6 + baking_resolution)) func set_curve_point_position(index : int, position : Vector3) -> void: curve.set_point_position(index, position) _generate_river() func set_curve_point_in(index : int, position : Vector3) -> void: curve.set_point_in(index, position) _generate_river() func set_curve_point_out(index : int, position : Vector3) -> void: curve.set_point_out(index, position) _generate_river() func set_widths(new_widths) -> void: widths = new_widths if _first_enter_tree: return _generate_river() func set_materials(param : String, value) -> void: _material.set_shader_parameter(param, value) _debug_material.set_shader_parameter(param, value) func set_debug_view(index : int) -> void: debug_view = index if index == 0: mesh_instance.material_override = null else: _debug_material.set_shader_parameter("mode", index) mesh_instance.material_override =_debug_material func spawn_mesh() -> void: if owner == null: push_warning("Cannot create MeshInstance3D sibling when River is root.") return var sibling_mesh := mesh_instance.duplicate(true) get_parent().add_child(sibling_mesh) sibling_mesh.set_owner(get_tree().get_edited_scene_root()) sibling_mesh.position = position sibling_mesh.material_override = null; func get_curve_points() -> PackedVector3Array: var points : PackedVector3Array for p in curve.get_point_count(): points.append(curve.get_point_position(p)) return points func get_closest_point_to(point : Vector3) -> int: var points = [] var closest_distance := 4096.0 var closest_index for p in curve.get_point_count(): var dist := point.distance_to(curve.get_point_position(p)) if dist < closest_distance: closest_distance = dist closest_index = p return closest_index func get_shader_parameter(param : String): return _material.get_shader_parameter(param) # Parameter Setters func set_step_length_divs(value : int) -> void: shape_step_length_divs = value if _first_enter_tree: return valid_flowmap = false set_materials("i_valid_flowmap", valid_flowmap) _generate_river() emit_signal("river_changed") func set_step_width_divs(value : int) -> void: shape_step_width_divs = value if _first_enter_tree: return valid_flowmap = false set_materials("i_valid_flowmap", valid_flowmap) _generate_river() emit_signal("river_changed") func set_smoothness(value : float) -> void: shape_smoothness = value if _first_enter_tree: return valid_flowmap = false set_materials("i_valid_flowmap", valid_flowmap) _generate_river() emit_signal("river_changed") func set_shader_type(type: int): if type == mat_shader_type: return mat_shader_type = type if mat_shader_type == SHADER_TYPES.CUSTOM: _material.shader = mat_custom_shader else: _material.shader = load(BUILTIN_SHADERS[mat_shader_type].shader_path) for texture in BUILTIN_SHADERS[mat_shader_type].texture_paths: _material.set_shader_parameter(texture.name, load(texture.path) as Texture) notify_property_list_changed() func set_custom_shader(shader : Shader) -> void: if mat_custom_shader == shader: return mat_custom_shader = shader if mat_custom_shader != null: _material.shader = mat_custom_shader if Engine.is_editor_hint: # Ability to fork default shader if shader.code == "": var selected_shader = load(BUILTIN_SHADERS[mat_shader_type].shader_path) as Shader shader.code = selected_shader.code if shader != null: print("shader != null - set shader type to custom") print(shader) set_shader_type(SHADER_TYPES.CUSTOM) else: set_shader_type(SHADER_TYPES.WATER) func set_lod0_distance(value : float) -> void: lod_lod0_distance = value set_materials("i_lod0_distance", value) # Private Methods func _generate_river() -> void: var average_width := WaterHelperMethods.sum_array(widths) / float(widths.size() / 2) _steps = int( max(1.0, round(curve.get_baked_length() / average_width)) ) var river_width_values := WaterHelperMethods.generate_river_width_values(curve, _steps, shape_step_length_divs, shape_step_width_divs, widths) mesh_instance.mesh = WaterHelperMethods.generate_river_mesh(curve, _steps, shape_step_length_divs, shape_step_width_divs, shape_smoothness, river_width_values) mesh_instance.mesh.surface_set_material(0, _material) func _generate_flowmap(flowmap_resolution : float) -> void: WaterHelperMethods.reset_all_colliders(get_tree().root) var image := Image.create(flowmap_resolution, flowmap_resolution, true, Image.FORMAT_RGB8) image.fill(Color(0.0, 0.0, 0.0)) emit_signal("progress_notified", 0.0, "Calculating Collisions (" + str(flowmap_resolution) + "x" + str(flowmap_resolution) + ")") await get_tree().process_frame image = await WaterHelperMethods.generate_collisionmap(image, mesh_instance, baking_raycast_distance, baking_raycast_layers, _steps, shape_step_length_divs, shape_step_width_divs, self) emit_signal("progress_notified", 0.95, "Applying filters (" + str(flowmap_resolution) + "x" + str(flowmap_resolution) + ")") await get_tree().process_frame # Calculate how many colums are in UV2 _uv2_sides = WaterHelperMethods.calculate_side(_steps) var margin := int(round(float(flowmap_resolution) / float(_uv2_sides))) image = WaterHelperMethods.add_margins(image, flowmap_resolution, margin) var collision_with_margins := ImageTexture.create_from_image(image) # Create correctly tiling noise for A channel var noise_texture := load(FLOW_OFFSET_NOISE_TEXTURE_PATH) as Texture2D var noise_with_margin_size := float(_uv2_sides + 2) * (float(noise_texture.get_width()) / float(_uv2_sides)) var noise_with_tiling := Image.create(noise_with_margin_size, noise_with_margin_size, false, Image.FORMAT_RGB8) var slice_width := float(noise_texture.get_width()) / float(_uv2_sides) for x in _uv2_sides: noise_with_tiling.blend_rect(noise_texture.get_image(), Rect2(0.0, 0.0, slice_width, noise_texture.get_height()), Vector2(slice_width + float(x) * slice_width, slice_width - (noise_texture.get_width() / 2.0))) noise_with_tiling.blend_rect(noise_texture.get_image(), Rect2(0.0, 0.0, slice_width, noise_texture.get_height()), Vector2(slice_width + float(x) * slice_width, slice_width + (noise_texture.get_width() / 2.0))) var tiled_noise := ImageTexture.new() tiled_noise.create_from_image(noise_with_tiling) # Create renderer var renderer_instance = _filter_renderer.instantiate() self.add_child(renderer_instance) var flow_pressure_blur_amount = 0.04 / float(_uv2_sides) * flowmap_resolution var dilate_amount = baking_dilate / float(_uv2_sides) var flowmap_blur_amount = baking_flowmap_blur / float(_uv2_sides) * flowmap_resolution var foam_offset_amount = baking_foam_offset / float(_uv2_sides) var foam_blur_amount = baking_foam_blur / float(_uv2_sides) * flowmap_resolution var flow_pressure_map = await renderer_instance.apply_flow_pressure(collision_with_margins, flowmap_resolution, _uv2_sides + 2.0) var blurred_flow_pressure_map = await renderer_instance.apply_vertical_blur(flow_pressure_map, flow_pressure_blur_amount, flowmap_resolution + margin * 2) var dilated_texture = await renderer_instance.apply_dilate(collision_with_margins, dilate_amount, 0.0, flowmap_resolution + margin * 2) var normal_map = await renderer_instance.apply_normal(dilated_texture, flowmap_resolution + margin * 2) var flow_map = await renderer_instance.apply_normal_to_flow(normal_map, flowmap_resolution + margin * 2) var blurred_flow_map = await renderer_instance.apply_blur(flow_map, flowmap_blur_amount, flowmap_resolution + margin * 2) var foam_map = await renderer_instance.apply_foam(dilated_texture, foam_offset_amount, baking_foam_cutoff, flowmap_resolution + margin * 2) var blurred_foam_map = await renderer_instance.apply_blur(foam_map, foam_blur_amount, flowmap_resolution + margin * 2) var flow_foam_noise_img = await renderer_instance.apply_combine(blurred_flow_map, blurred_flow_map, blurred_foam_map, tiled_noise) var dist_pressure_img = await renderer_instance.apply_combine(dilated_texture, blurred_flow_pressure_map) # Debug texture gen # flow_pressure_map.get_image().save_png("res://test_assets/baked_pressure_map.png") # blurred_flow_pressure_map.get_image().save_png("res://test_assets/baked_pressure_map_blurred.png") # dilated_texture.get_image().save_png("res://test_assets/dilated_texture.png") # normal_map.get_image().save_png("res://test_assets/normal_map.png") # flow_map.get_image().save_png("res://test_assets/flow_map.png") # blurred_flow_map.get_image().save_png("res://test_assets/blurred_flow_map.png") remove_child(renderer_instance) # cleanup var flow_foam_noise_result = flow_foam_noise_img.get_image().get_region(Rect2(margin, margin, flowmap_resolution, flowmap_resolution)) var dist_pressure_result = dist_pressure_img.get_image().get_region(Rect2(margin, margin, flowmap_resolution, flowmap_resolution)) flow_foam_noise = flow_foam_noise_img dist_pressure = dist_pressure_img set_materials("i_flowmap", flow_foam_noise) set_materials("i_distmap", dist_pressure) set_materials("i_valid_flowmap", true) set_materials("i_uv2_sides", _uv2_sides) valid_flowmap = true; emit_signal("progress_notified", 100.0, "finished") update_configuration_warnings() # Signal Methods func properties_changed() -> void: emit_signal("river_changed")