diff --git a/doc/docs/shader_design.md b/doc/docs/shader_design.md index 4732e6c43..c313e7982 100644 --- a/doc/docs/shader_design.md +++ b/doc/docs/shader_design.md @@ -24,35 +24,43 @@ These notable [Terrain3DData](../api/class_terrain3ddata.rst) variables are pass First are `get_region_uv/_uv2()` which take in UV coordinates and return region coordinates, either absolute or normalized. It also returns the region ID, which is used in the map texture arrays above. -Optionally, world noise is inserted here, which generates fractal brownian noise to be used for background hills outside of your regions. It's an expensive visual gimmick only and does not generate collision. +Optionally, world noise is inserted here, which generates fractal brownian noise to be used for background hills outside of your regions. It's a visual only effect, can be costly at high octaves, and does not generate collision. -`get_height()` returns the value of the heightmap at the given location. If world noise is enabled, it is blended into the height here. +The controlmap is read to determine holes, the heightmap is read if valid, and if enabled world noise is calculated. The values are accumulated along with offsets to determine the final height, and vertex normal. -Finally `vertex()` sets the UV and UV2 coordinates, and the height of the mesh vertex. Elsewhere the CPU creates flat mesh components and a collision mesh with heights. Here is where the flat mesh vertices have their heights set to match the collision mesh. +Elsewhere the CPU creates flat mesh components and a collision mesh with heights. + +As render_mode skip_vertex_transform is used, we apply the required matrix transforms to set the final `VERTEX` position to match the collision mesh. ## Fragment() `fragment()` is run per terrain pixel drawn on the screen. -### Normal calculation - -The first step is calculating the terrain normals. This shared between the `vertex()` and `fragment()` functions. Clipmap terrain vertices spread out at lower LODs causing certain things like normals look strange when you look in the distance as the vertices used for calculation suddenly separate at further LODs. So we switch from normals calculated per vertex to per pixel when the pixel is farther than `vertex_normal_distance`. - -The exact distance that the transition from per vertex to per pixel normal calculations occurs can be adjusted from the default of 192m via the `vertex_normals_distance` uniform. +### Grid offsets, weights and derivatives. -Generating normals in the shader works fine and modern GPUs can handle the load of 2 - 3 additional height lookups and the on-the-fly calculations. Doing this saves 3-4MB VRAM per region. +Due to rotation/scale/detiling/projection requireing breaks in UV continuity, we must use `textureGrad()` and provide derivatives for it. We take 1 set of `dfdx(uv)` and `dFdy(uv)` saved in `base_derivatives` and then scale them as needed. -### Grid creation +The lookup grid and blend weights are initially calculated here, as they are used for both normals, and material lookups. -We create a grid 1 unit wide using the `mirror` and `index00UV`-`index11UV` variables. This defines 4 fixed points around the current pixel. On LOD0 this grid aligns with both the mesh vertices and the control map pixels. However, they don't align further out on lower LODs or beyond the regions (sculpted areas) where the vertices are spread out. Pixel processing out there still occurs based on this 1-unit grid. - -This is `ALBEDO = vec3(mirror.xy, 0.)`, showing horizontal stripes in red, and vertical stripes in green, with the vertices highlighted. The inverse is stored in `mirror.zw`, so where horizontal stripes alternate red, black, red, they are now black, red, black. +To see the grid, apply at the end of the shader `ALBEDO = vec3(round(weight), 0.0);` showing horizontal stripes in red, and vertical stripes in green, with the vertices highlighted. The inverse is stored in `inverse`, so where horizontal stripes alternate red, black, red, they are now black, red, black. ```{image} images/sh_mirror.png :target: ../_images/sh_mirror.png ``` -Next, the control maps are queried for each of the 4 grid points and stored in `control00`-`control11`. The control map bit packed format is defined in [Controlmap Format](controlmap_format.md). +A determination is made with the base derivatives, of whether it is reasonable to skip all additional lookups required to do the bilinear blend. This skip can save a significant amount of bandwidth and processing for the GPU depening on how much of the screen is occupied by distant terrain. It's worth noting that as this is calculated from screen space derivatives, it is independant of screen resolution. + +### Normal calculation + +The next step is calculating the terrain normals. Clipmap terrain vertices spread out at lower LODs causing certain things like normals look strange when you look in the distance as the vertices used for calculation suddenly separate at further LODs. Due to this, we calculate normals by taking derivatives from the heightmap, in `fragment()`. + +Rather than defaulting to the standard sampler, texelfetch is used to aquire all nessecary height values, which are then processed to generate a set of normals per-index, and an interpolated value for smooth normals. + +Generating normals in the shader works fine and modern GPUs can handle the load of the additional height lookups and the on-the-fly calculations. Doing this saves 3-4MB VRAM per region. + +### Material creation + +Next, the control maps are queried for each of the 4 grid points and stored in `control[0]`-`control[3]`. The control map bit packed format is defined in [Controlmap Format](controlmap_format.md). The textures at each point are looked up and stored in an array of structs. If there is an overlay texture, the two are height blended here. Then the pixel position within the 4 grid points is bilinear interpolated to get the weighting for the final pixel values. @@ -74,6 +82,8 @@ All splat maps are sampled, and of the 16-32 values, the 4 strongest are blended *Side note:* Storing blend values in 3-bits is possible, where each of the 8 numbers represents an index in a array of 0-1 values: `{ 0.0f, .125f, .25f, .334f, .5f, .667f, .8f, 1.0f }`. In the future, this may be baked at runtime. However, editing using a 3-bit array of fixed values was exceedingly difficult and unsuccessful. +*Side note 2:* The bilinear blend can be skipped for distant terrain, allowing only 1/4 of the samples normally required. + The position of the pixel within its grid square is used to bilinear interpolate the values of the 4 surrounding samples. We disable the default GPU interpolation on texture lookups and interpolate ourselves here. **Comparing the two methods:** diff --git a/doc/docs/tips.md b/doc/docs/tips.md index 6ce7bb88c..b3f5f7b13 100644 --- a/doc/docs/tips.md +++ b/doc/docs/tips.md @@ -77,14 +77,14 @@ out_mat = Material(vec4(0.), vec4(0.), 0, 0, 0.0, vec3(0.)); // normal_rg = ... vec4 emissive = vec4(0.); if(out_mat.base == emissive_id) { - emissive = texture(emissive_tex, matUV); + emissive = textureGrad(emissive_tex, matUV, dd1.xy, dd1.zw); } // Immediately after albedo_ht2 and normal_rg2 get assigned: // albedo_ht2 = ... // normal_rg2 = ... vec4 emissive2 = vec4(0.); -emissive2 = texture(emissive_tex, matUV2) * float(out_mat.over == emissive_id); +emissive2 = textureGrad(emissive_tex, matUV2, dd2.xy, dd2.zw) * float(out_mat.over == emissive_id); // Immediately after the calls to height_blend: // albedo_ht = height_blend(... @@ -97,11 +97,11 @@ out_mat.emissive = emissive.rgb; // Then at the very bottom of `fragment()`, before the final }, apply the weighting and send it to the GPU. ```glsl -vec3 emissive = weight_inv * ( +vec3 emissive = mat[0].emissive * weights.x + mat[1].emissive * weights.y + mat[2].emissive * weights.z + - mat[3].emissive * weights.w ); + mat[3].emissive * weights.w ; EMISSION = emissive * emissive_strength; ``` diff --git a/project/addons/terrain_3d/src/editor_plugin.gd b/project/addons/terrain_3d/src/editor_plugin.gd index a97b5434a..e9eea3681 100644 --- a/project/addons/terrain_3d/src/editor_plugin.gd +++ b/project/addons/terrain_3d/src/editor_plugin.gd @@ -157,36 +157,39 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> _read_input(p_event) + ## Setup active camera & viewport + # Always update this for all inputs, as the mouse position can move without + # necessarily being a InputEventMouseMotion object. get_intersection() also + # returns the last frame position, and should be updated more frequently. + + # Snap terrain to current camera + terrain.set_camera(p_viewport_camera) + + # Detect if viewport is set to half_resolution + # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D + var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent() + var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true + + ## Get mouse location on terrain + # Project 2D mouse position to 3D position and direction + var vp_mouse_pos: Vector2 = editor_vpc.get_local_mouse_position() + var mouse_pos: Vector2 = vp_mouse_pos if full_resolution else vp_mouse_pos / 2 + var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos) + var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos) + + # If region tool, grab mouse position without considering height + if editor.get_tool() == Terrain3DEditor.REGION: + var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir) + mouse_global_position = (camera_pos + t * camera_dir) + else: + #Else look for intersection with terrain + var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir, true) + if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan + return AFTER_GUI_INPUT_STOP + mouse_global_position = intersection_point + ## Handle mouse movement if p_event is InputEventMouseMotion: - ## Setup active camera & viewport - - # Snap terrain to current camera - terrain.set_camera(p_viewport_camera) - - # Detect if viewport is set to half_resolution - # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D - var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent() - var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true - - ## Get mouse location on terrain - - # Project 2D mouse position to 3D position and direction - var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2 - var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos) - var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos) - - # If region tool, grab mouse position without considering height - if editor.get_tool() == Terrain3DEditor.REGION: - var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir) - mouse_global_position = (camera_pos + t * camera_dir) - else: - # Else look for intersection with terrain - var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir, true) - if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan - return AFTER_GUI_INPUT_STOP - mouse_global_position = intersection_point - ui.update_decal() if _input_mode != -1: # Not cam rotation @@ -206,8 +209,6 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> return AFTER_GUI_INPUT_PASS - ui.update_decal() - if p_event is InputEventMouseButton and _input_mode > 0: if p_event.is_pressed(): # If picking diff --git a/project/addons/terrain_3d/src/tool_settings.gd b/project/addons/terrain_3d/src/tool_settings.gd index ddea9b398..d07525794 100644 --- a/project/addons/terrain_3d/src/tool_settings.gd +++ b/project/addons/terrain_3d/src/tool_settings.gd @@ -173,6 +173,8 @@ func _ready() -> void: "unit":"γ", "range":Vector3(0.1, 2.0, 0.01) }) add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50, "unit":"%", "range":Vector3(0, 100, 1) }) + add_setting({ "name":"crosshair_threshold", "type":SettingType.SLIDER, "list":advanced_list, "default":16., + "unit":"m", "range":Vector3(0, 200, 1) }) func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container: diff --git a/project/addons/terrain_3d/src/ui.gd b/project/addons/terrain_3d/src/ui.gd index 222e32487..48fe2d045 100644 --- a/project/addons/terrain_3d/src/ui.gd +++ b/project/addons/terrain_3d/src/ui.gd @@ -42,21 +42,32 @@ var setting_has_changed: bool = false var visible: bool = false var picking: int = Terrain3DEditor.TOOL_MAX var picking_callback: Callable -var decal: Decal -var decal_timer: Timer -var gradient_decals: Array[Decal] var brush_data: Dictionary var operation_builder: OperationBuilder var last_tool: Terrain3DEditor.Tool var last_operation: Terrain3DEditor.Operation var last_rmb_time: int = 0 # Set in editor.gd -# Compatibility decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B -var editor_decal_position: Array[Vector2] -var editor_decal_rotation: Array[float] -var editor_decal_size: Array[float] -var editor_decal_color: Array[Color] -var editor_decal_visible: Array[bool] +# Editor decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B +var mat_rid: RID +var editor_decal_position: Array[Vector2] = [Vector2(), Vector2(), Vector2()] +var editor_decal_rotation: Array[float] = [float(), float(), float()] +var editor_decal_size: Array[float] = [float(), float(), float()] +var editor_decal_color: Array[Color] = [Color(), Color(), Color()] +var editor_decal_visible: Array[bool] = [bool(), bool(), bool()] +var editor_brush_texture_rid: RID = RID() +var editor_decal_timer: Timer +var editor_decal_fade: float : + set(value): + editor_decal_fade = value + editor_decal_color[0] = Color( + editor_decal_color[0].r, + editor_decal_color[0].g, + editor_decal_color[0].b, + value + ) + RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color) +@onready var editor_ring_texture_rid: RID = ring_texture.get_rid() func _enter_tree() -> void: @@ -80,15 +91,12 @@ func _enter_tree() -> void: _on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD) - decal = Decal.new() - add_child(decal) - decal_timer = Timer.new() - decal_timer.wait_time = .5 - decal_timer.one_shot = true - decal_timer.timeout.connect(Callable(func(node): - if node: - get_tree().create_tween().tween_property(node, "albedo_mix", 0.0, 0.15)).bind(decal)) - add_child(decal_timer) + editor_decal_timer = Timer.new() + editor_decal_timer.wait_time = .5 + editor_decal_timer.one_shot = true + editor_decal_timer.timeout.connect(func(): + get_tree().create_tween().tween_property(self, "editor_decal_fade", 0.0, 0.15)) + add_child(editor_decal_timer) func _exit_tree() -> void: @@ -97,12 +105,7 @@ func _exit_tree() -> void: toolbar.queue_free() tool_settings.queue_free() terrain_menu.queue_free() - decal.queue_free() - decal_timer.queue_free() - for gradient_decal in gradient_decals: - gradient_decal.queue_free() - gradient_decals.clear() - + editor_decal_timer.queue_free() func set_visible(p_visible: bool, p_menu_only: bool = false) -> void: terrain_menu.set_visible(p_visible) @@ -237,6 +240,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor to_show.push_back("show_cursor_while_painting") to_show.push_back("gamma") to_show.push_back("jitter") + to_show.push_back("crosshair_threshold") tool_settings.show_settings(to_show) operation_builder = null @@ -259,10 +263,9 @@ func _on_setting_changed() -> void: return brush_data = tool_settings.get_settings() brush_data["asset_id"] = plugin.asset_dock.get_current_list().get_selected_id() - update_decal() plugin.editor.set_brush_data(brush_data) plugin.editor.set_operation(_modify_operation(plugin.editor.get_operation())) - + update_decal() func update_modifiers() -> void: toolbar.show_add_buttons(not plugin.modifier_ctrl) @@ -300,9 +303,13 @@ func _invert_operation(p_operation: Terrain3DEditor.Operation, flags: int = OP_N func update_decal() -> void: + if not plugin.terrain: + return + mat_rid = plugin.terrain.material.get_material_rid() + editor_decal_timer.start() + # If not a state that should show the decal, hide everything and return if not visible or \ - not plugin.terrain or \ plugin._input_mode < 0 or \ # Wait for cursor to recenter after moving camera before revealing # See https://github.com/godotengine/godot/issues/70098 @@ -310,185 +317,129 @@ func update_decal() -> void: brush_data.is_empty() or \ plugin.editor.get_tool() == Terrain3DEditor.REGION or \ (plugin._input_mode > 0 and not brush_data["show_cursor_while_painting"]): - decal.visible = false - for gradient_decal in gradient_decals: - gradient_decal.visible = false + editor_decal_visible[0] = false + editor_decal_visible[1] = false + editor_decal_visible[2] = false + RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) return - - decal.position = plugin.mouse_global_position - decal.visible = true - decal.size = Vector3.ONE * maxf(brush_data["size"], .5) + + editor_decal_position[0] = Vector2(plugin.mouse_global_position.x, plugin.mouse_global_position.z) + editor_decal_visible[0] = true + editor_decal_size[0] = maxf(brush_data["size"], .5) if brush_data["align_to_view"]: var cam: Camera3D = plugin.terrain.get_camera(); if (cam): - decal.rotation.y = cam.rotation.y + editor_decal_rotation[0] = cam.rotation.y else: - decal.rotation.y = 0 - + editor_decal_rotation[0] = 0. + # Set texture and color if picking != Terrain3DEditor.TOOL_MAX: - decal.texture_albedo = ring_texture - decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing() + editor_brush_texture_rid = ring_texture.get_rid() + editor_decal_size[0] = 10. * plugin.terrain.get_vertex_spacing() match picking: Terrain3DEditor.HEIGHT: - decal.modulate = COLOR_PICK_HEIGHT + editor_decal_color[0] = COLOR_PICK_HEIGHT Terrain3DEditor.COLOR: - decal.modulate = COLOR_PICK_COLOR + editor_decal_color[0] = COLOR_PICK_COLOR Terrain3DEditor.ROUGHNESS: - decal.modulate = COLOR_PICK_ROUGH - decal.modulate.a = 1.0 + editor_decal_color[0] = COLOR_PICK_ROUGH + editor_decal_color[0].a = 1.0 else: - decal.texture_albedo = brush_data["brush"][1] + editor_brush_texture_rid = brush_data["brush"][1].get_rid() match plugin.editor.get_tool(): Terrain3DEditor.SCULPT: match plugin.editor.get_operation(): Terrain3DEditor.ADD: if plugin.modifier_alt: - decal.modulate = COLOR_LIFT - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_LIFT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) else: - decal.modulate = COLOR_RAISE - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_RAISE + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.SUBTRACT: if plugin.modifier_alt: - decal.modulate = COLOR_FLATTEN - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_FLATTEN + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) else: - decal.modulate = COLOR_LOWER - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .5 + editor_decal_color[0] = COLOR_LOWER + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .5 Terrain3DEditor.AVERAGE: - decal.modulate = COLOR_SMOOTH - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .2 + editor_decal_color[0] = COLOR_SMOOTH + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .2 Terrain3DEditor.GRADIENT: - decal.modulate = COLOR_SLOPE - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_SLOPE + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.HEIGHT: - decal.modulate = COLOR_HEIGHT - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_HEIGHT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.TEXTURE: match plugin.editor.get_operation(): Terrain3DEditor.REPLACE: - decal.modulate = COLOR_PAINT - decal.modulate.a = .7 + editor_decal_color[0] = COLOR_PAINT + editor_decal_color[0].a = .7 Terrain3DEditor.SUBTRACT: - decal.modulate = COLOR_PAINT - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_PAINT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.ADD: - decal.modulate = COLOR_SPRAY - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_SPRAY + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.COLOR: - decal.modulate = brush_data["color"].srgb_to_linear() - decal.modulate.a *= clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = brush_data["color"].srgb_to_linear() + editor_decal_color[0].a *= clamp(brush_data["strength"], .2, .5) Terrain3DEditor.ROUGHNESS: - decal.modulate = COLOR_ROUGHNESS - decal.modulate.a = clamp(brush_data["strength"], .2, .5) + editor_decal_color[0] = COLOR_ROUGHNESS + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) Terrain3DEditor.AUTOSHADER: - decal.modulate = COLOR_AUTOSHADER - decal.modulate.a = .7 + editor_decal_color[0] = COLOR_AUTOSHADER + editor_decal_color[0].a = .7 Terrain3DEditor.HOLES: - decal.modulate = COLOR_HOLES - decal.modulate.a = .85 + editor_decal_color[0] = COLOR_HOLES + editor_decal_color[0].a = .85 Terrain3DEditor.NAVIGATION: - decal.modulate = COLOR_NAVIGATION - decal.modulate.a = .85 + editor_decal_color[0] = COLOR_NAVIGATION + editor_decal_color[0].a = .85 Terrain3DEditor.INSTANCER: - decal.texture_albedo = ring_texture - decal.modulate = COLOR_INSTANCER - decal.modulate.a = 1.0 - decal.size.y = max(1000, decal.size.y) - decal.albedo_mix = 1.0 - decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 ) - decal_timer.start() - - for gradient_decal in gradient_decals: - gradient_decal.visible = false + editor_brush_texture_rid = ring_texture.get_rid() + editor_decal_color[0] = COLOR_INSTANCER + editor_decal_color[0].a = 1.0 + + editor_decal_visible[1] = false + editor_decal_visible[2] = false if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT: - var index := 0 - for point in brush_data["gradient_points"]: - if point != Vector3.ZERO: - var point_decal: Decal = _get_gradient_decal(index) - point_decal.visible = true - point_decal.position = point - index += 1 - - update_compatibility_decal() - - -func _get_gradient_decal(index: int) -> Decal: - if gradient_decals.size() > index: - return gradient_decals[index] + var point1: Vector3 = brush_data["gradient_points"][0] + if point1 != Vector3.ZERO: + editor_decal_color[1] = COLOR_SLOPE + editor_decal_size[1] = 10. * plugin.terrain.get_vertex_spacing() + editor_decal_visible[1] = true + editor_decal_position[1] = Vector2(point1.x, point1.z) + var point2: Vector3 = brush_data["gradient_points"][1] + if point2 != Vector3.ZERO: + editor_decal_color[2] = COLOR_SLOPE + editor_decal_size[2] = 10. * plugin.terrain.get_vertex_spacing() + editor_decal_visible[2] = true + editor_decal_position[2] = Vector2(point2.x, point2.z) - var gradient_decal := Decal.new() - gradient_decal = Decal.new() - gradient_decal.texture_albedo = ring_texture - gradient_decal.modulate = COLOR_SLOPE - gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing() - gradient_decal.size.y = 1000. - gradient_decal.cull_mask = decal.cull_mask - add_child(gradient_decal) + if plugin.terrain.is_compatibility_mode(): + for i in editor_decal_color.size(): + editor_decal_color[i] = editor_decal_color[i].darkened(0.2) - gradient_decals.push_back(gradient_decal) - return gradient_decal - - -func update_compatibility_decal() -> void: - if not plugin.terrain.is_compatibility_mode(): - return - - # Verify setup - if editor_decal_position.size() != 3: - editor_decal_position.resize(3) - editor_decal_rotation.resize(3) - editor_decal_size.resize(3) - editor_decal_color.resize(3) - editor_decal_visible.resize(3) - decal_timer.timeout.connect(func(): - var mat_rid: RID = plugin.terrain.material.get_material_rid() - editor_decal_visible[0] = false - RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) - ) - - # Update compatibility decal - var mat_rid: RID = plugin.terrain.material.get_material_rid() - if decal.visible: - editor_decal_position[0] = Vector2(decal.global_position.x, decal.global_position.z) - editor_decal_rotation[0] = decal.rotation.y - editor_decal_size[0] = brush_data.get("size") - editor_decal_color[0] = decal.modulate - editor_decal_visible[0] = decal.visible - RenderingServer.material_set_param( - mat_rid, "_editor_decal_0", decal.texture_albedo.get_rid() - ) - if gradient_decals.size() >= 1: - editor_decal_position[1] = Vector2(gradient_decals[0].global_position.x, - gradient_decals[0].global_position.z) - editor_decal_rotation[1] = gradient_decals[0].rotation.y - editor_decal_size[1] = 10.0 - editor_decal_color[1] = gradient_decals[0].modulate - editor_decal_visible[1] = gradient_decals[0].visible - RenderingServer.material_set_param( - mat_rid, "_editor_decal_1", gradient_decals[0].texture_albedo.get_rid() - ) - if gradient_decals.size() >= 2: - editor_decal_position[2] = Vector2(gradient_decals[1].global_position.x, - gradient_decals[1].global_position.z) - editor_decal_rotation[2] = gradient_decals[1].rotation.y - editor_decal_size[2] = 10.0 - editor_decal_color[2] = gradient_decals[1].modulate - editor_decal_visible[2] = gradient_decals[1].visible - RenderingServer.material_set_param( - mat_rid, "_editor_decal_2", gradient_decals[1].texture_albedo.get_rid() - ) + editor_decal_fade = editor_decal_color[0].a + # Update Shader params + RenderingServer.material_set_param(mat_rid, "_editor_brush_texture", editor_brush_texture_rid) + RenderingServer.material_set_param(mat_rid, "_editor_ring_texture", editor_ring_texture_rid) RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position) RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation) RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size) RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color) RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) - + RenderingServer.material_set_param(mat_rid, "_editor_crosshair_threshold", brush_data["crosshair_threshold"] + 0.1) func set_decal_rotation(p_rot: float) -> void: - decal.rotation.y = p_rot + editor_decal_rotation[0] = p_rot + var mat_rid: RID = plugin.terrain.material.get_material_rid() + RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation) func _on_picking(p_type: int, p_callback: Callable) -> void: diff --git a/project/demo/assets/textures/ground037_nrm_rgh.png b/project/demo/assets/textures/ground037_nrm_rgh.png index 0b83c14cc..d3708665e 100644 Binary files a/project/demo/assets/textures/ground037_nrm_rgh.png and b/project/demo/assets/textures/ground037_nrm_rgh.png differ diff --git a/project/demo/assets/textures/rock030_nrm_rgh.png b/project/demo/assets/textures/rock030_nrm_rgh.png index b2cb71064..b5efd03d8 100644 Binary files a/project/demo/assets/textures/rock030_nrm_rgh.png and b/project/demo/assets/textures/rock030_nrm_rgh.png differ diff --git a/project/demo/data/assets.tres b/project/demo/data/assets.tres index d3f02a1ff..3507a97c2 100644 --- a/project/demo/data/assets.tres +++ b/project/demo/data/assets.tres @@ -26,6 +26,7 @@ name = "Rock" albedo_color = Color(1.596, 1.56, 1.5, 1) albedo_texture = ExtResource("1_v81ad") normal_texture = ExtResource("2_72dup") +detiling = 0.17 [sub_resource type="Terrain3DTextureAsset" id="Terrain3DTextureAsset_od0q7"] name = "Grass" @@ -33,6 +34,8 @@ id = 1 albedo_color = Color(0.67451, 0.74902, 0.686275, 1) albedo_texture = ExtResource("3_g8f2m") normal_texture = ExtResource("4_aw5y1") +uv_scale = 0.2 +detiling = 0.161 [resource] mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_2qf8x")]) diff --git a/src/shaders/dual_scaling.glsl b/src/shaders/dual_scaling.glsl index 92bebc608..04fac8e90 100644 --- a/src/shaders/dual_scaling.glsl +++ b/src/shaders/dual_scaling.glsl @@ -16,27 +16,31 @@ uniform float dual_scale_near : hint_range(0,1000) = 100.0; } //each time we change scale, recalculate antitiling from baseline to maintain continuity. matUV = detiling(base_uv * mat_scale, uv_center * mat_scale, out_mat.base, normal_angle); + dd1.xy = rotate_plane(ddxy.xy, -normal_angle); + dd1.zw = rotate_plane(ddxy.zw, -normal_angle); dd1 *= mat_scale; albedo_ht = textureGrad(_texture_array_albedo, vec3(matUV, float(out_mat.base)), dd1.xy, dd1.zw); normal_rg = textureGrad(_texture_array_normal, vec3(matUV, float(out_mat.base)), dd1.xy, dd1.zw); // Unpack & rotate base normal for blending normal_rg.xz = unpack_normal(normal_rg).xz; - normal_rg.xz = rotate_normal(normal_rg.xz, -normal_angle); + normal_rg.xz = rotate_plane(normal_rg.xz, -normal_angle); float far_factor = clamp(smoothstep(dual_scale_near, dual_scale_far, length(v_vertex - v_camera_pos)), 0.0, 1.0); if (far_factor > 0.f && (out_mat.base == dual_scale_texture || out_mat.over == dual_scale_texture)) { mat_scale *= dual_scale_reduction; - dd1 *= dual_scale_reduction; float dual_scale_normal = uv_rotation; //do not add near & far rotations // Do not apply detiling if tri-scale reduction occurs. matUV = region < 0 ? base_uv * mat_scale : detiling(base_uv * mat_scale, uv_center * mat_scale, dual_scale_texture, dual_scale_normal); + dd1.xy = rotate_plane(ddxy.xy, -dual_scale_normal); + dd1.zw = rotate_plane(ddxy.zw, -dual_scale_normal); + dd1 *= mat_scale; albedo_far = textureGrad(_texture_array_albedo, vec3(matUV, float(dual_scale_texture)), dd1.xy, dd1.zw); normal_far = textureGrad(_texture_array_normal, vec3(matUV, float(dual_scale_texture)), dd1.xy, dd1.zw); // Unpack & rotate dual scale normal for blending normal_far.xz = unpack_normal(normal_far).xz; - normal_far.xz = rotate_normal(normal_far.xz, -dual_scale_normal); + normal_far.xz = rotate_plane(normal_far.xz, -dual_scale_normal); } if(out_mat.base == dual_scale_texture) { @@ -46,13 +50,15 @@ uniform float dual_scale_near : hint_range(0,1000) = 100.0; //INSERT: UNI_SCALING_BASE matUV = detiling(base_uv * mat_scale, uv_center * mat_scale, out_mat.base, normal_angle); + dd1.xy = rotate_plane(ddxy.xy, -normal_angle); + dd1.zw = rotate_plane(ddxy.zw, -normal_angle); dd1 *= mat_scale; albedo_ht = textureGrad(_texture_array_albedo, vec3(matUV, float(out_mat.base)), dd1.xy, dd1.zw); normal_rg = textureGrad(_texture_array_normal, vec3(matUV, float(out_mat.base)), dd1.xy, dd1.zw); // Unpack & rotate base normal for blending normal_rg.xz = unpack_normal(normal_rg).xz; - normal_rg.xz = rotate_normal(normal_rg.xz, -normal_angle); + normal_rg.xz = rotate_plane(normal_rg.xz, -normal_angle); //INSERT: DUAL_SCALING_OVERLAY // If dual scaling, apply to overlay texture diff --git a/src/shaders/editor_functions.glsl b/src/shaders/editor_functions.glsl index ae8514473..9ef6684bb 100644 --- a/src/shaders/editor_functions.glsl +++ b/src/shaders/editor_functions.glsl @@ -29,14 +29,14 @@ R"( // END_COMPAT_DEFINES //INSERT: EDITOR_SETUP_DECAL -uniform highp sampler2D _editor_decal_0 : source_color, filter_linear, repeat_disable; -uniform highp sampler2D _editor_decal_1 : source_color, filter_linear, repeat_disable; -uniform highp sampler2D _editor_decal_2 : source_color, filter_linear, repeat_disable; +uniform highp sampler2D _editor_brush_texture : source_color, filter_linear, repeat_disable; +uniform highp sampler2D _editor_ring_texture : source_color, filter_linear, repeat_disable; uniform vec2 _editor_decal_position[3]; uniform float _editor_decal_size[3]; uniform float _editor_decal_rotation[3]; uniform vec4 _editor_decal_color[3] : source_color; uniform bool _editor_decal_visible[3]; +uniform float _editor_crosshair_threshold = 16.0; // expects uv (Texture/world space 0 to +/- inf 1m units). vec3 get_decal(vec3 albedo, vec2 uv) { @@ -54,23 +54,29 @@ vec3 get_decal(vec3 albedo, vec2 uv) { continue; } float decal = 0.0; - // For webGL we cannot use sampler2D[], sampler2DArray requires all textures be the same size, - // which might not be the case - so use a switch to read the correct uniform. switch (i) { case 0 : - decal = texture(_editor_decal_0, decal_uv + 0.5).r; + decal = texture(_editor_brush_texture, decal_uv + 0.5).r; break; case 1: - decal = texture(_editor_decal_1, decal_uv + 0.5).r; - break; case 2: - decal = texture(_editor_decal_2, decal_uv + 0.5).r; + decal = texture(_editor_ring_texture, decal_uv + 0.5).r; break; } - // Blend in decal; reduce opacity 55% to account for differences in Opengl/Vulkan and/or decals - albedo = mix(albedo, _editor_decal_color[i].rgb, clamp(decal * _editor_decal_color[i].a * .55, 0., .55)); + // Blend in decal; square for better visual blend + albedo = mix(albedo, _editor_decal_color[i].rgb, decal * decal * _editor_decal_color[i].a); + } + // Crosshair + if (_editor_decal_visible[0] && _editor_decal_size[0] <= _editor_crosshair_threshold) { + vec2 cross_uv = ((uv - _editor_decal_position[0] * _vertex_density) * _vertex_spacing) * 16.0; + cross_uv /= sqrt(length(v_camera_pos - v_vertex)); + float line_thickness = 0.5; + float line_start = _editor_decal_size[0] * 0.5 + 8.; + float line_end = _editor_decal_size[0] * 0.5 + 16.; + bool h = abs(cross_uv.y) < line_thickness && abs(cross_uv.x) < line_end && abs(cross_uv.x) > line_start; + bool v = abs(cross_uv.x) < line_thickness && abs(cross_uv.y) < line_end && abs(cross_uv.y) > line_start; + albedo = (h || v) ? mix(albedo, _editor_decal_color[0].rgb, _editor_decal_color[0].a) : albedo; } - return albedo; } diff --git a/src/shaders/main.glsl b/src/shaders/main.glsl index b58d0430a..e34908689 100644 --- a/src/shaders/main.glsl +++ b/src/shaders/main.glsl @@ -215,14 +215,14 @@ vec2 detiling(vec2 uv, vec2 uv_center, int mat_id, inout float normal_rotation){ return uv; } -vec2 rotate_normal(vec2 normal, float angle) { - float new_x = dot(vec2(cos(angle), sin(angle)), normal); +vec2 rotate_plane(vec2 plane, float angle) { + float new_x = dot(vec2(cos(angle), sin(angle)), plane); angle = fma(PI, 0.5, angle); - float new_y = dot(vec2(cos(angle), sin(angle)), normal); + float new_y = dot(vec2(cos(angle), sin(angle)), plane); return vec2(new_x, new_y); } -// 2-4 lookups +// 2-4 lookups ( 2-6 with dual scaling ) void get_material(vec2 base_uv, vec4 ddxy, uint control, ivec3 iuv_center, vec3 normal, out Material out_mat) { out_mat = Material(vec4(0.), vec4(0.), 0, 0, 0.0); vec2 uv_center = vec2(iuv_center.xy); @@ -264,6 +264,8 @@ void get_material(vec2 base_uv, vec4 ddxy, uint control, ivec3 iuv_center, vec3 float normal_angle2 = uv_rotation; vec2 matUV2 = detiling(base_uv * mat_scale2, uv_center * mat_scale2, out_mat.over, normal_angle2); vec4 dd2 = ddxy * mat_scale2; + dd2.xy = rotate_plane(dd2.xy, -normal_angle2); + dd2.zw = rotate_plane(dd2.zw, -normal_angle2); vec4 albedo_ht2 = textureGrad(_texture_array_albedo, vec3(matUV2, float(out_mat.over)), dd2.xy, dd2.zw); vec4 normal_rg2 = textureGrad(_texture_array_normal, vec3(matUV2, float(out_mat.over)), dd2.xy, dd2.zw); @@ -273,7 +275,7 @@ void get_material(vec2 base_uv, vec4 ddxy, uint control, ivec3 iuv_center, vec3 if (out_mat.blend > 0.f) { // Unpack & rotate overlay normal for blending normal_rg2.xz = unpack_normal(normal_rg2).xz; - normal_rg2.xz = rotate_normal(normal_rg2.xz, -normal_angle2); + normal_rg2.xz = rotate_plane(normal_rg2.xz, -normal_angle2); //INSERT: DUAL_SCALING_OVERLAY // Apply color to overlay @@ -342,7 +344,7 @@ void fragment() { indexUV[2] = get_region_uv(index_id + offsets.yx); indexUV[3] = get_region_uv(index_id + offsets.xx); - // Terrain normals + // Terrain normals 3-8 lookups vec3 index_normal[4]; float h[8]; // allows additional derivatives, eg world noise, brush previews etc. @@ -359,7 +361,14 @@ void fragment() { // Set flat world normal - overriden if bilerp is true. vec3 w_normal = index_normal[3]; + // Setting this here, instead of after the branch is appears to be 10%~ faster. + // Likley as flat derivatives seem more cache friendly for texture lookups. + if (enable_projection) { + base_derivatives *= 1.0 + (1.0 - w_normal.y); + } + // Branching smooth normals must be done seperatley for correct normals at all 4 index ids. + // +5 lookups if (bilerp) { // Fetch the additional required height values for smooth normals h[3] = texelFetch(_height_maps, indexUV[1], 0).r; // 3 (1,1) @@ -388,11 +397,7 @@ void fragment() { TANGENT = mat3(VIEW_MATRIX) * w_tangent; BINORMAL = mat3(VIEW_MATRIX) * w_binormal; - if (enable_projection) { - base_derivatives *= 1.0 + (1.0 - w_normal.y); - } - - // Minimum amount of lookups for sub fragment sized index domains. + // 5 lookups for sub fragment sized index domains. uint control[4]; control[3] = texelFetch(_control_maps, indexUV[3], 0).r; @@ -403,6 +408,7 @@ void fragment() { vec4 normal_rough = mat[3].nrm_rg; // Otherwise do full bilinear interpolation + // +15 lookups + 1 noise lookup if (bilerp) { control[0] = texelFetch(_control_maps, indexUV[0], 0).r; control[1] = texelFetch(_control_maps, indexUV[1], 0).r; diff --git a/src/terrain_3d_material.cpp b/src/terrain_3d_material.cpp index 006ac78e6..65d135f97 100644 --- a/src/terrain_3d_material.cpp +++ b/src/terrain_3d_material.cpp @@ -184,7 +184,7 @@ String Terrain3DMaterial::_inject_editor_code(const String &p_shader) const { return shader; } insert_names.clear(); - if (_compatibility) { + if (IS_EDITOR && _terrain && _terrain->get_editor()) { insert_names.push_back("EDITOR_SETUP_DECAL"); } for (int i = 0; i < insert_names.size(); i++) { @@ -251,7 +251,7 @@ String Terrain3DMaterial::_inject_editor_code(const String &p_shader) const { if (_show_navigation || (IS_EDITOR && _terrain && _terrain->get_editor() && _terrain->get_editor()->get_tool() == Terrain3DEditor::NAVIGATION)) { insert_names.push_back("EDITOR_NAVIGATION"); } - if (_compatibility) { + if (IS_EDITOR && _terrain && _terrain->get_editor()) { insert_names.push_back("EDITOR_RENDER_DECAL"); } for (int i = 0; i < insert_names.size(); i++) {