@tool
extends MeshInstance3D
# ==============================================================================
# PROPRIEDADES DO INSPECTOR
# ==============================================================================
@export_category("Configurações do Terreno")
@export var terrain_size: Vector2 = Vector2(40, 40):
set(value):
terrain_size = value
if is_node_ready(): _generate_terrain()
@export var subdivision: int = 40:
set(value):
subdivision = clamp(value, 2, 250)
if is_node_ready(): _generate_terrain()
@export var height_scale: float = 8.0:
set(value):
height_scale = value
if is_node_ready(): _generate_terrain()
@export var generate_collision: bool = true:
set(value):
generate_collision = value
if is_node_ready(): _update_collision()
@export_category("Configuração das Bordas (Suavização e Profundidade)")
@export var enable_edge_falloff: bool = true:
set(value):
enable_edge_falloff = value
if is_node_ready(): _generate_terrain()
@export_range(0.05, 0.5, 0.01) var edge_falloff_margin: float = 0.2:
set(value):
edge_falloff_margin = value
if is_node_ready(): _generate_terrain()
@export var edge_target_height: float = -2.0: # Altura/Profundidade final das bordas (Valores negativos afundam o terreno)
set(value):
edge_target_height = value
if is_node_ready(): _generate_terrain()
@export var edge_falloff_radial: bool = false:
set(value):
edge_falloff_radial = value
if is_node_ready(): _generate_terrain()
@export_category("Configurações do Ruído Base")
@export var noise_seed: int = 1337:
set(value):
noise_seed = value
_update_noise_seeds()
if is_node_ready(): _generate_terrain()
@export var noise_frequency: float = 0.04:
set(value):
noise_frequency = value
if noise: noise.frequency = value
if is_node_ready(): _generate_terrain()
@export var gerar_novo_terreno_aleatorio: bool = false:
set(value):
if value:
noise_seed = randi()
_update_noise_seeds()
if is_node_ready(): _generate_terrain()
gerar_novo_terreno_aleatorio = false
@export_category("Texturas dos Biomas")
@export var texture_sand: Texture2D:
set(value): texture_sand = value; _update_shader_parameters()
@export var texture_grass: Texture2D:
set(value): texture_grass = value; _update_shader_parameters()
@export var texture_rock: Texture2D:
set(value): texture_rock = value; _update_shader_parameters()
@export var texture_snow: Texture2D:
set(value): texture_snow = value; _update_shader_parameters()
@export var texture_trail: Texture2D:
set(value): texture_trail = value; _update_shader_parameters()
@export var texture_scale: float = 10.0:
set(value): texture_scale = value; _update_shader_parameters()
@export_category("Configuração de Trilhas (Caminhos Conectados)")
@export var enable_trails: bool = true:
set(value):
enable_trails = value
if is_node_ready(): _generate_terrain()
@export var trail_frequency: float = 0.03:
set(value):
trail_frequency = value
if trail_noise: trail_noise.frequency = value
if is_node_ready(): _generate_terrain()
@export var trail_width: float = 0.06:
set(value):
trail_width = value
if is_node_ready(): _generate_terrain()
@export var trail_warp_strength: float = 12.0:
set(value):
trail_warp_strength = value
if is_node_ready(): _generate_terrain()
@export_category("Configuração de Árvores (Bioma Grama)")
@export var spawn_trees: bool = true:
set(value):
spawn_trees = value
if is_node_ready(): _generate_trees()
@export var tree_mesh: Mesh:
set(value):
tree_mesh = value
_update_tree_mesh()
@export var tree_density: float = 0.2:
set(value):
tree_density = value
if is_node_ready(): _generate_trees()
# ==============================================================================
# VARIÁVEIS INTERNAS E RUÍDOS
# ==============================================================================
var noise: FastNoiseLite = FastNoiseLite.new()
var trail_noise: FastNoiseLite = FastNoiseLite.new()
var warp_noise: FastNoiseLite = FastNoiseLite.new()
var tree_multimesh_inst: MultiMeshInstance3D
var static_body: StaticBody3D
var collision_shape: CollisionShape3D
# ==============================================================================
# CICLO DE VIDA E INICIALIZAÇÃO
# ==============================================================================
func _ready() -> void:
_init_noises()
_setup_tree_multimesh()
_generate_terrain()
func _init_noises() -> void:
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
trail_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
warp_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
warp_noise.frequency = 0.015
_update_noise_seeds()
func _update_noise_seeds() -> void:
if noise:
noise.seed = noise_seed
noise.frequency = noise_frequency
if trail_noise:
trail_noise.seed = noise_seed + 123
trail_noise.frequency = trail_frequency
if warp_noise:
warp_noise.seed = noise_seed + 456
# ==============================================================================
# CÁLCULOS PROCEDURAIS (ALTURA, BORDAS E TRILHAS)
# ==============================================================================
func _get_trail_weight(x: float, z: float) -> float:
if not enable_trails: return 0.0
var warp_x = warp_noise.get_noise_2d(x, z) * trail_warp_strength
var warp_z = warp_noise.get_noise_2d(x + 500.0, z + 500.0) * trail_warp_strength
var raw_trail = abs(trail_noise.get_noise_2d(x + warp_x, z + warp_z))
if raw_trail < trail_width:
return smoothstep(trail_width, 0.0, raw_trail)
return 0.0
func _get_height_at(x: float, z: float, trail_weight: float) -> float:
var n_val = noise.get_noise_2d(x, z)
var h = n_val * height_scale
# Aplicação da suavização e profundidade nas bordas
if enable_edge_falloff:
var half_x = terrain_size.x * 0.5
var half_z = terrain_size.y * 0.5
var nx = abs(x) / half_x
var nz = abs(z) / half_z
var dist: float
if edge_falloff_radial:
dist = sqrt(nx * nx + nz * nz)
else:
dist = max(nx, nz)
var start_margin = clamp(1.0 - edge_falloff_margin, 0.0, 0.99)
# falloff varia de 1.0 (centro) até 0.0 (extremidade da borda)
var falloff = 1.0 - smoothstep(start_margin, 1.0, dist)
# Interpola da altura original até a altura alvo da borda
h = lerp(edge_target_height, h, falloff)
return h - (trail_weight * 0.4)
# ==============================================================================
# GERAÇÃO DA MALHA DO TERRENO
# ==============================================================================
func _generate_terrain() -> void:
_init_noises()
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var half_x = terrain_size.x / 2.0
var half_z = terrain_size.y / 2.0
var step_x = terrain_size.x / float(subdivision)
var step_z = terrain_size.y / float(subdivision)
for z in range(subdivision):
for x in range(subdivision):
var x0 = -half_x + x * step_x
var z0 = -half_z + z * step_z
var x1 = x0 + step_x
var z1 = z0 + step_z
var tw0 = _get_trail_weight(x0, z0)
var tw1 = _get_trail_weight(x1, z0)
var tw2 = _get_trail_weight(x0, z1)
var tw3 = _get_trail_weight(x1, z1)
var v0 = Vector3(x0, _get_height_at(x0, z0, tw0), z0)
var v1 = Vector3(x1, _get_height_at(x1, z0, tw1), z0)
var v2 = Vector3(x0, _get_height_at(x0, z1, tw2), z1)
var v3 = Vector3(x1, _get_height_at(x1, z1, tw3), z1)
var u0 = float(x) / subdivision
var u1 = float(x + 1) / subdivision
var w0 = float(z) / subdivision
var w1 = float(z + 1) / subdivision
# Triângulo 1
st.set_color(Color(tw0, 0, 0, 1)); st.set_uv(Vector2(u0, w0)); st.add_vertex(v0)
st.set_color(Color(tw1, 0, 0, 1)); st.set_uv(Vector2(u1, w0)); st.add_vertex(v1)
st.set_color(Color(tw2, 0, 0, 1)); st.set_uv(Vector2(u0, w1)); st.add_vertex(v2)
# Triângulo 2
st.set_color(Color(tw1, 0, 0, 1)); st.set_uv(Vector2(u1, w0)); st.add_vertex(v1)
st.set_color(Color(tw3, 0, 0, 1)); st.set_uv(Vector2(u1, w1)); st.add_vertex(v3)
st.set_color(Color(tw2, 0, 0, 1)); st.set_uv(Vector2(u0, w1)); st.add_vertex(v2)
st.generate_normals()
self.mesh = st.commit()
_setup_terrain_shader()
_generate_trees()
_update_collision()
# ==============================================================================
# COLISÃO FÍSICA
# ==============================================================================
func _update_collision() -> void:
static_body = get_node_or_null("TerrainCollider") as StaticBody3D
if not generate_collision or not self.mesh:
if static_body: static_body.queue_free()
return
if not static_body:
static_body = StaticBody3D.new()
static_body.name = "TerrainCollider"
add_child(static_body)
collision_shape = CollisionShape3D.new()
collision_shape.name = "Shape"
static_body.add_child(collision_shape)
else:
collision_shape = static_body.get_node_or_null("Shape") as CollisionShape3D
if collision_shape and self.mesh:
collision_shape.shape = self.mesh.create_trimesh_shape()
# ==============================================================================
# VEGETAÇÃO (MULTIMESH)
# ==============================================================================
func _setup_tree_multimesh() -> void:
tree_multimesh_inst = get_node_or_null("TreeMultiMesh") as MultiMeshInstance3D
if not tree_multimesh_inst:
tree_multimesh_inst = MultiMeshInstance3D.new()
tree_multimesh_inst.name = "TreeMultiMesh"
add_child(tree_multimesh_inst)
if Engine.is_editor_hint() and get_tree():
tree_multimesh_inst.owner = get_tree().edited_scene_root
if not tree_multimesh_inst.multimesh:
var mm = MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
tree_multimesh_inst.multimesh = mm
_update_tree_mesh()
func _update_tree_mesh() -> void:
if not tree_multimesh_inst or not tree_multimesh_inst.multimesh:
return
if tree_mesh:
tree_multimesh_inst.multimesh.mesh = tree_mesh
else:
var default_cyl = CylinderMesh.new()
default_cyl.top_radius = 0.1
default_cyl.bottom_radius = 0.3
default_cyl.height = 1.5
tree_multimesh_inst.multimesh.mesh = default_cyl
func _generate_trees() -> void:
_setup_tree_multimesh()
if not spawn_trees or not tree_multimesh_inst.multimesh:
if tree_multimesh_inst and tree_multimesh_inst.multimesh:
tree_multimesh_inst.multimesh.instance_count = 0
return
var valid_transforms: Array[Transform3D] = []
var step_x = terrain_size.x / float(subdivision)
var step_z = terrain_size.y / float(subdivision)
var rng = RandomNumberGenerator.new()
rng.seed = noise_seed + 999
var start_x = -terrain_size.x / 2.0
var start_z = -terrain_size.y / 2.0
for x in range(subdivision + 1):
for z in range(subdivision + 1):
var px = start_x + (x * step_x) + rng.randf_range(-step_x * 0.3, step_x * 0.3)
var pz = start_z + (z * step_z) + rng.randf_range(-step_z * 0.3, step_z * 0.3)
var tw = _get_trail_weight(px, pz)
var py = _get_height_at(px, pz, tw)
var norm_val = (py / height_scale) * 0.5 + 0.5
# Só instancia se estiver fora do caminho e na altitude da grama
if tw == 0.0 and norm_val >= 0.25 and norm_val <= 0.55:
if rng.randf() <= tree_density:
var t = Transform3D()
t = t.rotated_local(Vector3.UP, rng.randf_range(0, TAU))
var sc = rng.randf_range(0.7, 1.3)
t = t.scaled_local(Vector3(sc, sc, sc))
t.origin = Vector3(px, py, pz)
valid_transforms.append(t)
var mm = tree_multimesh_inst.multimesh
mm.instance_count = valid_transforms.size()
for i in range(valid_transforms.size()):
mm.set_instance_transform(i, valid_transforms[i])
# ==============================================================================
# SHADER DO TERRENO
# ==============================================================================
func _setup_terrain_shader() -> void:
var mat = self.material_override as ShaderMaterial
if not mat:
mat = ShaderMaterial.new()
var shader = Shader.new()
shader.code = """
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
uniform sampler2D tex_sand : source_color, filter_linear_mipmap, hint_default_white;
uniform sampler2D tex_grass : source_color, filter_linear_mipmap, hint_default_white;
uniform sampler2D tex_rock : source_color, filter_linear_mipmap, hint_default_white;
uniform sampler2D tex_snow : source_color, filter_linear_mipmap, hint_default_white;
uniform sampler2D tex_trail : source_color, filter_linear_mipmap, hint_default_white;
uniform float uv_scale = 10.0;
uniform float max_height = 8.0;
uniform bool enable_trails = true;
varying float height_factor;
varying float slope;
void vertex() {
height_factor = (VERTEX.y / max_height) * 0.5 + 0.5;
slope = NORMAL.y;
}
void fragment() {
vec2 base_uv = UV * uv_scale;
vec4 col_sand = texture(tex_sand, base_uv);
vec4 col_grass = texture(tex_grass, base_uv);
vec4 col_rock = texture(tex_rock, base_uv);
vec4 col_snow = texture(tex_snow, base_uv);
vec4 col_trail = texture(tex_trail, base_uv);
vec4 final_color = col_sand;
float blend_grass = smoothstep(0.2, 0.35, height_factor);
final_color = mix(final_color, col_grass, blend_grass);
float blend_snow = smoothstep(0.75, 0.85, height_factor);
final_color = mix(final_color, col_snow, blend_snow);
float rock_blend = 1.0 - smoothstep(0.65, 0.85, slope);
final_color = mix(final_color, col_rock, rock_blend);
if (enable_trails && COLOR.r > 0.0) {
if (height_factor < 0.75 && height_factor > 0.15) {
final_color = mix(final_color, col_trail, COLOR.r);
}
}
ALBEDO = final_color.rgb;
ROUGHNESS = 0.8;
}
"""
mat.shader = shader
self.material_override = mat
_update_shader_parameters()
func _update_shader_parameters() -> void:
var mat = self.material_override as ShaderMaterial
if not mat: return
if texture_sand: mat.set_shader_parameter("tex_sand", texture_sand)
if texture_grass: mat.set_shader_parameter("tex_grass", texture_grass)
if texture_rock: mat.set_shader_parameter("tex_rock", texture_rock)
if texture_snow: mat.set_shader_parameter("tex_snow", texture_snow)
if texture_trail: mat.set_shader_parameter("tex_trail", texture_trail)
mat.set_shader_parameter("uv_scale", texture_scale)
mat.set_shader_parameter("max_height", height_scale)
mat.set_shader_parameter("enable_trails", enable_trails)