extends Node3D @export_subgroup("Scene") @export var terrain : MeshInstance3D @export var camera : Camera3D @export var target : Node3D @export_subgroup("Terrain", "terrain_") # Noise to generate terrain @export var terrain_noise : Noise # Max height (as noise is normalized in [-1, 1] @export var terrain_height : float = 16. # Sea level : Terrain height will be clamped below this level @export var terrain_sea_level : float = -8. # Terrain size @export var terrain_size : Vector2i = Vector2i(128, 128) @export_subgroup("Camera") # Camera rotation speed around the Y axis. @export var y_angle_speed : float = PI # Camera rotation speed around the X axis. @export var other_value_speed : float = PI # Clamping values for camera inclinaison (X axis rotation) @export var min_other_value : float = 0.1 @export var max_other_value : float = PI/2 - PI/5 # Maximum camera distance @export var max_camera_distance : float = 16. # Function between camera inclinaison and distance (for example, the more it faces the floor, the further the camera goes). @export var distance_per_other_value : Curve @export_subgroup("Subject") # Maximum target speed in unit/s @export var max_speed : float = 16. @onready var camera_distance : float = max_camera_distance @onready var y_angle : float = PI/4. @onready var other_value : float = 0.5 @onready var other_value_norm : float = 0.5 @onready var move_speed : float = 0 @onready var move_angle : float = 0 @onready var target_offset_y : float func _ready() -> void: assert(terrain_noise) assert(terrain_size.x > 0 and terrain_size.y > 0) assert(distance_per_other_value) var terrain_tex : ImageTexture = compute_terrain_data(terrain_noise, terrain_height, terrain_sea_level, terrain_size) target_offset_y = (target.mesh as CapsuleMesh).height / 2 # WARNING Only works if target main mesh is a capsule target.position.x = terrain_size.x / 2. target.position.z = terrain_size.y / 2. target.position.y = get_height(Vector2(target.position.x, target.position.z)) + target_offset_y camera_distance = distance_per_other_value.sample_baked(other_value_norm) * max_camera_distance compute_camera_position() terrain.position = Vector3(camera.position.x, 0., camera.position.z) var shader : ShaderMaterial = (terrain.material_override as ShaderMaterial) shader.set_shader_parameter("terrain_size", Vector2(terrain_size)) shader.set_shader_parameter("data_tex", terrain_tex) shader.set_shader_parameter("translation", Vector2(terrain.position.x - 0.5, terrain.position.z - 0.5)) shader.set_shader_parameter("rotation", PI - camera.rotation.y) func manage_stick_entries(left_stick : Vector2, right_stick : Vector2, delta : float) -> void: # Left stick => Player movement # Right stick => Camera movement y_angle += right_stick.x * y_angle_speed * delta if y_angle < 0: # Value didn't vary enough to justify the use of a loop. y_angle = 2 * PI + y_angle elif y_angle > 2 * PI: y_angle -= 2 * PI other_value = clampf(other_value - right_stick.y * other_value_speed * delta, min_other_value, max_other_value) other_value_norm = clamp(other_value_norm - right_stick.y * other_value_speed * delta, 0., 1.) camera_distance = distance_per_other_value.sample_baked(other_value_norm) * max_camera_distance var transformed_left : Vector2 = left_stick.rotated(y_angle) * 0.1 move_speed = transformed_left.length() if not is_zero_approx(move_speed): move_angle = atan2(transformed_left.x, transformed_left.y) @onready var _coming_from_controller_left : Vector2 = Vector2.ZERO @onready var _coming_from_controller_right : Vector2 = Vector2.ZERO func compute_camera_position() -> void: var camera_ray : Vector3 = Vector3.BACK.rotated(Vector3.LEFT, other_value).rotated(Vector3.DOWN, y_angle) var cam_pos : Vector3 = target.position + camera_ray * camera_distance camera.look_at_from_position(cam_pos, target.position) func _process(delta : float) -> void: # Left stick, Character move # Right stick, Camera move. _coming_from_controller_left.x = Input.get_axis("player_left", "player_right") _coming_from_controller_left.y = Input.get_axis("player_forward", "player_backward") _coming_from_controller_right.x = Input.get_axis("camera_left", "camera_right") _coming_from_controller_right.y = Input.get_axis("camera_zoom_in", "camera_zoom_out") manage_stick_entries(_coming_from_controller_left, _coming_from_controller_right, delta) target.rotation.y = move_angle target.translate(Vector3(0, 0, move_speed * delta * max_speed)) var logical_position : Vector2 = Vector2(target.position.x, target.position.z) target.position.y = get_height(logical_position) + target_offset_y compute_camera_position() var shader : ShaderMaterial = (terrain.material_override as ShaderMaterial) terrain.position = Vector3(camera.position.x, 0., camera.position.z) shader.set_shader_parameter("translation", Vector2(terrain.position.x - 0.5, terrain.position.z - 0.5)) shader.set_shader_parameter("rotation", PI - camera.rotation.y) @onready var terrain_data : PackedVector3Array = PackedVector3Array() func compute_terrain_data(noise : Noise, height : float, sea_level : float, size : Vector2i) -> ImageTexture: # Generate the terrain. var count : int = size.x * size.y var heights : PackedFloat32Array = PackedFloat32Array() heights.resize(count) terrain_data.resize(count) # Compute Heights -------------------------------------------------------- var idx : int = 0 for y : int in range(size.y): for x : int in range(size.x): heights[idx] = max(sea_level, noise.get_noise_2d(x, y)) * height idx += 1 # Compute Texture Data --------------------------------------------------- idx = 0 for y : int in range(size.y): for x : int in range(size.x): var h : float = heights[idx] var hmx : float = heights[idx - 1] if x > 0 else h var hpx : float = heights[idx + 1] if x < size.x - 1 else h var hmz : float = heights[idx - size.x] if y > 0 else h var hpz : float = heights[idx + size.x] if y < size.y - 1 else h var dX : float = hmx - hpx var dZ : float = hmz - hpz var n : Vector3 = Vector3(dX, 2., dZ).normalized() n.y *= -3. terrain_data[idx] = Vector3(h, n.x / n.y, n.z / n.y) idx += 1 var image : Image = Image.create_from_data(size.x, size.y, false, Image.FORMAT_RGBF, terrain_data.to_byte_array()) return ImageTexture.create_from_image(image) const PATCH_OFFSET : Array[Vector2] = [ Vector2(1., 1.), Vector2(0., 1.), Vector2(1., 0.), Vector2(0., 0.) ] const PATCH_POINTS : Array[Vector4i] = [ Vector4i(0, 1, 3, 4), Vector4i(1, 2, 4, 5), Vector4i(3, 4, 6, 7), Vector4i(4, 5, 7, 8) ] const T_IDX : Array[Vector2] = [ Vector2(-1., -1.), Vector2(0., -1.), Vector2(1., -1.), Vector2(-1., 0.), Vector2(0., 0.), Vector2(1., 0.), Vector2(-1., 1.), Vector2(0., 1.), Vector2(1., 1.) ] const ONE_THIRD : float = 1. / 3. func get_height(pos : Vector2) -> float: var global_uv : Vector2 = floor(pos) var p_uv : Vector2 = pos - global_uv - Vector2(.5, .5) var patch_id : int = (2 if p_uv.y > 0. else 0) + (1 if p_uv.x > 0. else 0) var l_uv : Vector2 = p_uv + PATCH_OFFSET[patch_id] var control_points : Vector4i = PATCH_POINTS[patch_id] var map_size : Vector2 = Vector2(terrain_size) var factor : Vector2 = (map_size - Vector2(1., 1.)) / map_size var uv : Vector2 = global_uv + Vector2(.5, .5) var mp0 : Vector2 = clamp((uv + T_IDX[control_points.x]), Vector2.ZERO, map_size) * factor var mp1 : Vector2 = clamp((uv + T_IDX[control_points.y]), Vector2.ZERO, map_size) * factor var mp2 : Vector2 = clamp((uv + T_IDX[control_points.z]), Vector2.ZERO, map_size) * factor var mp3 : Vector2 = clamp((uv + T_IDX[control_points.w]), Vector2.ZERO, map_size) * factor var idx0 : int = round(mp0.y) * terrain_size.x + round(mp0.x) var idx1 : int = round(mp1.y) * terrain_size.x + round(mp1.x) var idx2 : int = round(mp2.y) * terrain_size.x + round(mp2.x) var idx3 : int = round(mp3.y) * terrain_size.x + round(mp3.x) var p0 : Vector3 = Vector3(T_IDX[control_points.x].x, terrain_data[idx0].x, T_IDX[control_points.x].y) var p1 : Vector3 = Vector3(T_IDX[control_points.y].x, terrain_data[idx1].x, T_IDX[control_points.y].y) var p2 : Vector3 = Vector3(T_IDX[control_points.z].x, terrain_data[idx2].x, T_IDX[control_points.z].y) var p3 : Vector3 = Vector3(T_IDX[control_points.w].x, terrain_data[idx3].x, T_IDX[control_points.w].y) var sl0 : Vector2 = Vector2(terrain_data[idx0].y, terrain_data[idx0].z) var sl1 : Vector2 = Vector2(terrain_data[idx1].y, terrain_data[idx1].z) var sl2 : Vector2 = Vector2(terrain_data[idx2].y, terrain_data[idx2].z) var sl3 : Vector2 = Vector2(terrain_data[idx3].y, terrain_data[idx3].z) # Time to run bezier interpolation var t : float = l_uv.x; var m_t : float = 1. - t; var t2 : float = t * t; var m_t2 : float = m_t * m_t var ktu : Vector4 = Vector4(m_t2 * m_t, 3. * t * m_t2, 3. * t2 * m_t, t2 * t) t = l_uv.y; m_t = 1. - t; t2 = t * t; m_t2 = m_t * m_t var ktv : Vector4 = Vector4(m_t2 * m_t, 3. * t * m_t2, 3. * t2 * m_t, t2 * t) var b0 : Vector3; var b1 : Vector3; var b2 : Vector3; var b3 : Vector3 var u0 : Vector3; var u1 : Vector3; var u2 : Vector3; var u3 : Vector3 # First and second curves slopes var x0_slope : float = sl0.x; var z0_slope : float = sl0.y; var x1_slope : float = sl2.x; var z1_slope : float = sl2.y; var shift_0 : Vector3 = Vector3.ZERO; var shift_1 : Vector3 = Vector3.ZERO shift_0.x = ONE_THIRD; shift_0.y = x0_slope; shift_0.z = 0. shift_1.x = ONE_THIRD; shift_1.y = x1_slope; shift_1.z = 0. # First curve interpolation b0 = Vector3(p0); b1 = Vector3(p0.x, p0.y + z0_slope, p0.z + ONE_THIRD) b2 = Vector3(p2.x, p2.y - z1_slope, p2.z - ONE_THIRD) b3 = Vector3(p2) u0 = (ktv.x * b0) + (ktv.y * b1) + (ktv.z * b2) + (ktv.w * b3) # Second curve interpolation b0 += shift_0; b1 += shift_0; b2 += shift_1; b3 += shift_1 u1 = (ktv.x * b0) + (ktv.y * b1) + (ktv.z * b2) + (ktv.w * b3) # Third and fourth curves slopes x0_slope = sl1.x; z0_slope = sl1.y; x1_slope = sl3.x; z1_slope = sl3.y shift_0.y = x0_slope; shift_1.y = x1_slope # Fourth curve interpolation b0 = Vector3(p1); b1 = Vector3(p1.x, p1.y + z0_slope, p1.z + ONE_THIRD) b2= Vector3(p3.x, p3.y - z1_slope, p3.z - ONE_THIRD); b3 = Vector3(p3) u3 = (ktv.x * b0) + (ktv.y * b1) + (ktv.z * b2) + (ktv.w * b3) # Third curve interpolation b0 -= shift_0; b1 -= shift_0; b2 -= shift_1; b3 -= shift_1 u2 = (ktv.x * b0) + (ktv.y * b1) + (ktv.z * b2) + (ktv.w * b3) # Final point return ((ktu.x * u0) + (ktu.y * u1) + (ktu.z * u2) + (ktu.w * u3)).y
or share this direct link: