Make some test with terrain generator

This commit is contained in:
2026-02-01 20:22:20 +01:00
parent 571605de8a
commit fe11763496
22 changed files with 1252 additions and 117 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -0,0 +1,100 @@
# Terrain Control System Specification
## Summary
Provide artists with deterministic, in-editor control over procedural terrain generation by
introducing control layers (biomes, height profiles, masks, slope limits) and hard scene
constraints (bel vedere stations and rail splines). The generator must combine these inputs
to produce a terrain mesh and mask/biome outputs suitable for materials and prop placement.
## Goals
- Allow artists to explicitly control biomes, height profiles, masks, and slope limits.
- Ensure rails and stations act as hard constraints that shape/flatten the terrain.
- Support a procedural rail spline (Path3D + Curve3D) with known points near stations.
- Provide real-time preview and regeneration in-editor.
- Expose data for props placement (density/exclusion by biome/slope/rail distance).
## Non-Goals
- Final art-quality shader/material authoring.
- Runtime streaming or terrain LOD.
- Full track generation algorithm (assumed to exist elsewhere).
## Inputs
### Scene Inputs (Hard Constraints)
- Bel vedere stations: placed as scene nodes (Node3D).
- Rail spline: Path3D with Curve3D (procedural). Only points near stations are known.
### Artist Inputs (Control Layers)
- Biome map (Texture2D): defines biome IDs/weights across terrain.
- Height profile (Curve): maps base noise height to final height per biome.
- Mask layers (Texture2D array): painted/assigned regions (forest, village, rock, etc.).
- Slope limit map (Texture2D): limits maximum slope per region.
## Data Model
### TerrainControlLayers (Resource)
- biome_map: Texture2D
- height_profile: Curve
- slope_limit_map: Texture2D
- mask_layers: Array[Texture2D]
- mask_names: Array[String]
- resolution: Vector2i (shared texture resolution for all layers)
### TerrainGenerator (Node)
- control_layers: TerrainControlLayers
- station_paths: Array[NodePath]
- rail_paths: Array[NodePath]
- rail_influence_radius: float
- station_influence_radius: float
- station_flatten_height: float
- rail_flatten_strength: float
- biome_preview_mode: enum (None, Biome, Slope, MaskIndex)
## Workflow
1. Artist places bel vedere station nodes in the scene.
2. Rail spline is generated as Path3D with Curve3D (known points near stations).
3. Artist assigns/creates a TerrainControlLayers resource.
4. Artist paints biome map and mask layers in-editor and sets height/slope controls.
5. TerrainGenerator regenerates and previews terrain with rail/station constraints.
## Generation Pipeline
1. Base height: sample noise and scale by max_height.
2. Apply height profile: evaluate Curve to remap height.
3. Apply island/edge mask if enabled.
4. Sample biome map and masks to modulate height and material blending.
5. Apply slope limits by clamping local height gradients per pixel.
6. Apply rail constraints:
- Compute distance from vertex to nearest point on rail spline.
- Within rail_influence_radius, flatten height toward rail height.
- Enforce maximum slope along rail direction.
7. Apply station constraints:
- Within station_influence_radius, flatten to station height.
- Optionally expand a platform/yard mask for props and paths.
8. Output: mesh + per-vertex data for materials (biome/mask indices) and props.
## Rail Spline Handling
- Rail spline is Path3D with Curve3D and may be partially defined near stations.
- The generator samples the spline at fixed intervals to build a distance field.
- If only station-adjacent points exist, interpolate curve points between stations.
## Editor Tooling
- Terrain Control Painter:
- Paint biome IDs, slope limits, and masks directly on terrain.
- Toggle view modes (biome/slope/mask overlays).
- Provide brush size, hardness, and opacity.
- Preview toggle:
- Regenerate terrain on parameter changes in the editor.
## Props Integration
- Provide maps to props system:
- biome_id, slope_limit, rail_distance, station_distance, mask values.
- Placement rules can use these to control density and exclusions.
## Definition of Done
- Artists can paint biomes/masks and see results in-editor.
- Terrain respects rail and station constraints.
- Slope limits and height profiles affect terrain as expected.
- Outputs for props placement are available and consistent.
## Open Questions
- How is rail height defined relative to terrain (absolute Y or curve metadata)?
- Should station flattening use station Y or a per-station override value?
- Expected texture resolution for control layers?

BIN
proc-terrain/biomes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -1,7 +1,4 @@
[gd_resource type="FastNoiseLite" format=3 uid="uid://clpxlmv87brc7"]
[resource]
noise_type = 4
seed = 42
frequency = 0.008
fractal_octaves = 4

View File

@@ -1,8 +1,8 @@
[gd_resource type="ShaderMaterial" format=3 uid="uid://4a05tpi1tu14"]
[ext_resource type="Shader" uid="uid://d4b4lfd2r51iu" path="res://proc-terrain/triplanar_slope.gdshader" id="1_shader"]
[ext_resource type="Texture2D" uid="uid://5pfh43bjvef4" path="res://proc-terrain/rock.png" id="2_50hjp"]
[ext_resource type="Texture2D" uid="uid://rc8iqoij2c5w" path="res://proc-terrain/grass.png" id="3_8s6t3"]
[ext_resource type="Texture2D" uid="uid://cfffdsmmgils3" path="res://proc-terrain/Dirt_baseColor.tga" id="2_50hjp"]
[ext_resource type="Texture2D" uid="uid://dq7id4wwphvm6" path="res://proc-terrain/Grass_baseColor.tga" id="3_8s6t3"]
[resource]
render_priority = 0
@@ -12,3 +12,5 @@ shader_parameter/texture_side = ExtResource("2_50hjp")
shader_parameter/blend_sharpness = 5.0
shader_parameter/slope_threshold = 0.7
shader_parameter/uv_scale = Vector3(0.1, 0.1, 0.1)
shader_parameter/texture_top_tiling = Vector2(2, 2)
shader_parameter/texture_side_tiling = Vector2(1, 1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,41 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rc8iqoij2c5w"
path.s3tc="res://.godot/imported/grass.png-288495857c635d40287c0bebe39e1299.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://proc-terrain/grass.png"
dest_files=["res://.godot/imported/grass.png-288495857c635d40287c0bebe39e1299.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View File

@@ -0,0 +1,4 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://40g5lvx2hw5k"]
[resource]
vertex_color_use_as_albedo = true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,41 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://5pfh43bjvef4"
path.s3tc="res://.godot/imported/rock.png-342cc50a309b9d723500126a447b3982.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://proc-terrain/rock.png"
dest_files=["res://.godot/imported/rock.png-342cc50a309b9d723500126a447b3982.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="TerrainControlLayers" format=3 uid="uid://diftpul4fiqwa"]
[ext_resource type="Texture2D" uid="uid://cdx6jsbu8586x" path="res://proc-terrain/biomes.png" id="1_1o45a"]
[ext_resource type="Script" uid="uid://dn0dmf6vj6ybp" path="res://proc-terrain/terrain_control_layers.gd" id="1_2pv2t"]
[ext_resource type="Texture2D" uid="uid://b8dd3au482ax" path="res://proc-terrain/slop_limit_map.png" id="3_3iw73"]
[sub_resource type="Curve" id="Curve_2pv2t"]
_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.48395061, 1), 0.0, 0.0, 0, 0, Vector2(0.99814117, 0.004347801), 0.0, 0.0, 0, 0]
point_count = 3
[resource]
script = ExtResource("1_2pv2t")
resolution = Vector2i(1024, 1024)
biome_map = ExtResource("1_1o45a")
height_profile = SubResource("Curve_2pv2t")
slope_limit_map = ExtResource("3_3iw73")

View File

@@ -0,0 +1,110 @@
@tool
class_name TerrainControlLayers
extends Resource
## Resource containing artist-controlled terrain layers for procedural generation.
## Includes biome maps, height profiles, masks, and slope limits.
signal layers_changed
@export var resolution: Vector2i = Vector2i(512, 512):
set(value):
resolution = value
emit_changed()
layers_changed.emit()
@export_group("Biome Control")
@export var biome_map: Texture2D:
set(value):
biome_map = value
emit_changed()
layers_changed.emit()
@export var height_profile: Curve:
set(value):
if height_profile and height_profile.changed.is_connected(_on_curve_changed):
height_profile.changed.disconnect(_on_curve_changed)
height_profile = value
if height_profile:
height_profile.changed.connect(_on_curve_changed)
emit_changed()
layers_changed.emit()
@export_group("Slope Control")
@export var slope_limit_map: Texture2D:
set(value):
slope_limit_map = value
emit_changed()
layers_changed.emit()
@export_range(0.0, 90.0) var default_slope_limit: float = 45.0:
set(value):
default_slope_limit = value
emit_changed()
layers_changed.emit()
@export_group("Mask Layers")
@export var mask_layers: Array[Texture2D] = []:
set(value):
mask_layers = value
emit_changed()
layers_changed.emit()
@export var mask_names: Array[String] = []:
set(value):
mask_names = value
emit_changed()
layers_changed.emit()
func _on_curve_changed() -> void:
emit_changed()
layers_changed.emit()
func get_biome_value(uv: Vector2) -> Color:
if not biome_map:
return Color.WHITE
var img := biome_map.get_image()
if not img:
return Color.WHITE
var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1)
var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1)
return img.get_pixel(px, py)
func get_slope_limit(uv: Vector2) -> float:
if not slope_limit_map:
return default_slope_limit
var img := slope_limit_map.get_image()
if not img:
return default_slope_limit
var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1)
var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1)
return img.get_pixel(px, py).r * 90.0
func get_height_profile_value(normalized_height: float) -> float:
if not height_profile:
return normalized_height
return height_profile.sample(clampf(normalized_height, 0.0, 1.0))
func get_mask_value(mask_index: int, uv: Vector2) -> float:
if mask_index < 0 or mask_index >= mask_layers.size():
return 0.0
var tex := mask_layers[mask_index]
if not tex:
return 0.0
var img := tex.get_image()
if not img:
return 0.0
var px := clampi(int(uv.x * img.get_width()), 0, img.get_width() - 1)
var py := clampi(int(uv.y * img.get_height()), 0, img.get_height() - 1)
return img.get_pixel(px, py).r
func get_mask_name(mask_index: int) -> String:
if mask_index < 0 or mask_index >= mask_names.size():
return "mask_%d" % mask_index
return mask_names[mask_index]

View File

@@ -0,0 +1 @@
uid://dn0dmf6vj6ybp

View File

@@ -2,6 +2,11 @@
class_name TerrainGenerator
extends MeshInstance3D
enum PreviewMode { NONE, BIOME, SLOPE, MASK_INDEX, RAIL_DISTANCE, STATION_DISTANCE }
signal terrain_generated
signal props_data_ready(props_data: TerrainPropsData)
@export var noise_setting: FastNoiseLite:
set(value):
if noise_setting and noise_setting.changed.is_connected(_on_settings_changed):
@@ -11,7 +16,7 @@ extends MeshInstance3D
noise_setting.changed.connect(_on_settings_changed)
_request_update()
@export var material_setting: ShaderMaterial:
@export var material_setting: Material:
set(value):
material_setting = value
if mesh:
@@ -49,6 +54,70 @@ extends MeshInstance3D
mask_power = value
_request_update()
@export_group("Control Layers")
@export var control_layers: TerrainControlLayers:
set(value):
if control_layers and control_layers.layers_changed.is_connected(_on_settings_changed):
control_layers.layers_changed.disconnect(_on_settings_changed)
control_layers = value
if control_layers:
control_layers.layers_changed.connect(_on_settings_changed)
_request_update()
@export_group("Rail Constraints")
@export var rail_paths: Array[NodePath] = []:
set(value):
rail_paths = value
_request_update()
@export var rail_influence_radius: float = 10.0:
set(value):
rail_influence_radius = value
_request_update()
@export var rail_flatten_strength: float = 1.0:
set(value):
rail_flatten_strength = value
_request_update()
@export_range(0.0, 45.0) var rail_max_slope: float = 5.0:
set(value):
rail_max_slope = value
_request_update()
@export_group("Station Constraints")
@export var station_paths: Array[NodePath] = []:
set(value):
station_paths = value
_request_update()
@export var station_influence_radius: float = 25.0:
set(value):
station_influence_radius = value
_request_update()
@export var station_flatten_height: float = 0.0:
set(value):
station_flatten_height = value
_request_update()
@export var use_station_y_as_height: bool = true:
set(value):
use_station_y_as_height = value
_request_update()
@export_group("Preview")
@export var preview_mode: PreviewMode = PreviewMode.NONE:
set(value):
preview_mode = value
_request_update()
@export var preview_mask_index: int = 0:
set(value):
preview_mask_index = value
if preview_mode == PreviewMode.MASK_INDEX:
_request_update()
@export_group("Actions")
@export var regenerate: bool = false:
set(_value):
@@ -59,6 +128,9 @@ extends MeshInstance3D
_bake_collision()
var _update_queued: bool = false
var _rail_spline_cache: Array[Curve3D] = []
var _station_positions: Array[Vector3] = []
var _props_data: TerrainPropsData
func _ready() -> void:
@@ -88,6 +160,8 @@ func generate_terrain() -> void:
if not noise_setting:
return
_cache_constraints()
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
@@ -97,20 +171,110 @@ func generate_terrain() -> void:
var half_z: float = terrain_size.y / 2.0
var heights: Array[float] = []
heights.resize(subdivision * subdivision)
var biome_ids: Array[int] = []
var slope_values: Array[float] = []
var rail_distances: Array[float] = []
var station_distances: Array[float] = []
var mask_values: Array[Array] = []
heights.resize(subdivision * subdivision)
biome_ids.resize(subdivision * subdivision)
slope_values.resize(subdivision * subdivision)
rail_distances.resize(subdivision * subdivision)
station_distances.resize(subdivision * subdivision)
var mask_count := 0
if control_layers:
mask_count = control_layers.mask_layers.size()
for i in range(mask_count):
var arr: Array[float] = []
arr.resize(subdivision * subdivision)
mask_values.append(arr)
# Pass 1: Generate base heights
for z in range(subdivision):
for x in range(subdivision):
var world_x: float = x * step_x - half_x
var world_z: float = z * step_z - half_z
var idx: int = z * subdivision + x
var uv := Vector2(float(x) / float(subdivision - 1), float(z) / float(subdivision - 1))
var height: float = noise_setting.get_noise_2d(world_x, world_z) * max_height
var base_noise: float = (noise_setting.get_noise_2d(world_x, world_z) + 1.0) * 0.5
var height: float = base_noise * max_height
# Apply height profile from control layers
if control_layers and control_layers.height_profile:
height = control_layers.get_height_profile_value(base_noise) * max_height
# Apply island mask
if island_mask:
height *= _calculate_island_mask(world_x, world_z, half_x, half_z)
heights[z * subdivision + x] = height
# Sample biome
var biome_color := Color.WHITE
if control_layers:
biome_color = control_layers.get_biome_value(uv)
biome_ids[idx] = _color_to_biome_id(biome_color)
# Sample slope limit
var slope_limit := 45.0
if control_layers:
slope_limit = control_layers.get_slope_limit(uv)
slope_values[idx] = slope_limit
# Sample masks
for m in range(mask_count):
mask_values[m][idx] = control_layers.get_mask_value(m, uv)
heights[idx] = height
# Pass 2: Apply rail constraints
for z in range(subdivision):
for x in range(subdivision):
var world_x: float = x * step_x - half_x
var world_z: float = z * step_z - half_z
var idx: int = z * subdivision + x
var world_pos := Vector3(world_x, heights[idx], world_z)
var rail_result := _get_rail_constraint(world_pos)
rail_distances[idx] = rail_result.distance
if rail_result.distance < rail_influence_radius and rail_result.distance >= 0.0:
var t: float = 1.0 - (rail_result.distance / rail_influence_radius)
t = t * t * rail_flatten_strength
heights[idx] = lerpf(heights[idx], rail_result.height, t)
# Pass 3: Apply station constraints
for z in range(subdivision):
for x in range(subdivision):
var world_x: float = x * step_x - half_x
var world_z: float = z * step_z - half_z
var idx: int = z * subdivision + x
var world_pos := Vector3(world_x, heights[idx], world_z)
var station_result := _get_station_constraint(world_pos)
station_distances[idx] = station_result.distance
if station_result.distance < station_influence_radius and station_result.distance >= 0.0:
var t: float = 1.0 - (station_result.distance / station_influence_radius)
t = t * t
heights[idx] = lerpf(heights[idx], station_result.height, t)
# Pass 4: Apply slope limits
_apply_slope_limits(heights, slope_values, step_x, step_z)
# Build props data
_props_data = TerrainPropsData.new()
_props_data.resolution = Vector2i(subdivision, subdivision)
_props_data.terrain_size = terrain_size
_props_data.heights = heights
_props_data.biome_ids = biome_ids
_props_data.slope_values = slope_values
_props_data.rail_distances = rail_distances
_props_data.station_distances = station_distances
_props_data.mask_values = mask_values
# Build mesh
for z in range(subdivision - 1):
for x in range(subdivision - 1):
var idx00: int = z * subdivision + x
@@ -128,30 +292,169 @@ func generate_terrain() -> void:
var v01 := Vector3(x0, heights[idx01], z1)
var v11 := Vector3(x1, heights[idx11], z1)
# Triangle 1: v00, v01, v10
var uv00 := Vector2(x0 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5)
var uv10 := Vector2(x1 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5)
var uv01 := Vector2(x0 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5)
var uv11 := Vector2(x1 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5)
# Preview mode coloring
var c00 := _get_preview_color(idx00, biome_ids, slope_values, rail_distances, station_distances, mask_values)
var c10 := _get_preview_color(idx10, biome_ids, slope_values, rail_distances, station_distances, mask_values)
var c01 := _get_preview_color(idx01, biome_ids, slope_values, rail_distances, station_distances, mask_values)
var c11 := _get_preview_color(idx11, biome_ids, slope_values, rail_distances, station_distances, mask_values)
# Triangle 1: v00, v10, v01 (CCW winding, normal pointing up)
var normal1: Vector3 = (v01 - v00).cross(v10 - v00).normalized()
st.set_normal(normal1)
st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5))
st.set_color(c00)
st.set_uv(uv00)
st.add_vertex(v00)
st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5))
st.add_vertex(v01)
st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5))
st.set_color(c10)
st.set_uv(uv10)
st.add_vertex(v10)
st.set_color(c01)
st.set_uv(uv01)
st.add_vertex(v01)
# Triangle 2: v10, v01, v11
# Triangle 2: v10, v11, v01 (CCW winding, normal pointing up)
var normal2: Vector3 = (v01 - v10).cross(v11 - v10).normalized()
st.set_normal(normal2)
st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z0 / terrain_size.y + 0.5))
st.set_color(c10)
st.set_uv(uv10)
st.add_vertex(v10)
st.set_uv(Vector2(x0 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5))
st.add_vertex(v01)
st.set_uv(Vector2(x1 / terrain_size.x + 0.5, z1 / terrain_size.y + 0.5))
st.set_color(c11)
st.set_uv(uv11)
st.add_vertex(v11)
st.set_color(c01)
st.set_uv(uv01)
st.add_vertex(v01)
mesh = st.commit()
if material_setting:
mesh.surface_set_material(0, material_setting)
terrain_generated.emit()
props_data_ready.emit(_props_data)
func _cache_constraints() -> void:
_rail_spline_cache.clear()
_station_positions.clear()
for path in rail_paths:
if path.is_empty():
continue
var node := get_node_or_null(path)
if node is Path3D:
var path3d := node as Path3D
if path3d.curve:
_rail_spline_cache.append(path3d.curve)
for path in station_paths:
if path.is_empty():
continue
var node := get_node_or_null(path)
if node is Node3D:
_station_positions.append((node as Node3D).global_position)
func _get_rail_constraint(world_pos: Vector3) -> Dictionary:
var min_dist := INF
var rail_height := world_pos.y
for curve in _rail_spline_cache:
var closest := curve.get_closest_point(world_pos)
var dist := Vector2(world_pos.x - closest.x, world_pos.z - closest.z).length()
if dist < min_dist:
min_dist = dist
rail_height = closest.y
return {"distance": min_dist, "height": rail_height}
func _get_station_constraint(world_pos: Vector3) -> Dictionary:
var min_dist := INF
var target_height := station_flatten_height
for station_pos in _station_positions:
var dist := Vector2(world_pos.x - station_pos.x, world_pos.z - station_pos.z).length()
if dist < min_dist:
min_dist = dist
if use_station_y_as_height:
target_height = station_pos.y
return {"distance": min_dist, "height": target_height}
func _apply_slope_limits(heights: Array[float], slope_limits: Array[float], step_x: float, step_z: float) -> void:
var max_iterations := 10
for _iter in range(max_iterations):
var changed := false
for z in range(1, subdivision - 1):
for x in range(1, subdivision - 1):
var idx: int = z * subdivision + x
var slope_limit_rad: float = deg_to_rad(slope_limits[idx])
var max_delta_x: float = tan(slope_limit_rad) * step_x
var max_delta_z: float = tan(slope_limit_rad) * step_z
var h := heights[idx]
var h_xm := heights[idx - 1]
var h_xp := heights[idx + 1]
var h_zm := heights[idx - subdivision]
var h_zp := heights[idx + subdivision]
var target := h
if abs(h - h_xm) > max_delta_x:
target = minf(target, h_xm + max_delta_x) if h > h_xm else maxf(target, h_xm - max_delta_x)
changed = true
if abs(h - h_xp) > max_delta_x:
target = minf(target, h_xp + max_delta_x) if h > h_xp else maxf(target, h_xp - max_delta_x)
changed = true
if abs(h - h_zm) > max_delta_z:
target = minf(target, h_zm + max_delta_z) if h > h_zm else maxf(target, h_zm - max_delta_z)
changed = true
if abs(h - h_zp) > max_delta_z:
target = minf(target, h_zp + max_delta_z) if h > h_zp else maxf(target, h_zp - max_delta_z)
changed = true
heights[idx] = lerpf(h, target, 0.5)
if not changed:
break
func _color_to_biome_id(color: Color) -> int:
return int(color.r * 255.0) + int(color.g * 255.0) * 256
func _get_preview_color(idx: int, biome_ids: Array[int], slope_values: Array[float],
rail_distances: Array[float], station_distances: Array[float], mask_values: Array[Array]) -> Color:
match preview_mode:
PreviewMode.NONE:
return Color.WHITE
PreviewMode.BIOME:
var biome := biome_ids[idx]
return Color.from_hsv(fmod(float(biome) * 0.618033988749, 1.0), 0.7, 0.9)
PreviewMode.SLOPE:
var slope := slope_values[idx] / 90.0
return Color(slope, 1.0 - slope, 0.0)
PreviewMode.MASK_INDEX:
if preview_mask_index < mask_values.size():
var v: float = mask_values[preview_mask_index][idx]
return Color(v, v, v)
return Color.BLACK
PreviewMode.RAIL_DISTANCE:
var d: float = clampf(rail_distances[idx] / (rail_influence_radius * 2.0), 0.0, 1.0)
return Color(1.0 - d, 0.2, d)
PreviewMode.STATION_DISTANCE:
var d: float = clampf(station_distances[idx] / (station_influence_radius * 2.0), 0.0, 1.0)
return Color(0.2, 1.0 - d, d)
return Color.WHITE
func get_props_data() -> TerrainPropsData:
return _props_data
func _calculate_island_mask(x: float, z: float, half_x: float, half_z: float) -> float:

View File

@@ -0,0 +1,153 @@
@tool
class_name TerrainPropsData
extends RefCounted
## Data structure containing terrain information for props placement.
## Generated by TerrainGenerator after terrain generation.
var resolution: Vector2i = Vector2i(128, 128)
var terrain_size: Vector2 = Vector2(200, 200)
var heights: Array[float] = []
var biome_ids: Array[int] = []
var slope_values: Array[float] = []
var rail_distances: Array[float] = []
var station_distances: Array[float] = []
var mask_values: Array[Array] = []
func get_index(x: int, z: int) -> int:
return z * resolution.x + x
func get_uv(x: int, z: int) -> Vector2:
return Vector2(float(x) / float(resolution.x - 1), float(z) / float(resolution.y - 1))
func get_world_position(x: int, z: int) -> Vector3:
var half_x := terrain_size.x / 2.0
var half_z := terrain_size.y / 2.0
var step_x := terrain_size.x / float(resolution.x - 1)
var step_z := terrain_size.y / float(resolution.y - 1)
var idx := get_index(x, z)
return Vector3(
x * step_x - half_x,
heights[idx] if idx < heights.size() else 0.0,
z * step_z - half_z
)
func get_height(x: int, z: int) -> float:
var idx := get_index(x, z)
if idx < 0 or idx >= heights.size():
return 0.0
return heights[idx]
func get_biome_id(x: int, z: int) -> int:
var idx := get_index(x, z)
if idx < 0 or idx >= biome_ids.size():
return 0
return biome_ids[idx]
func get_slope_limit(x: int, z: int) -> float:
var idx := get_index(x, z)
if idx < 0 or idx >= slope_values.size():
return 45.0
return slope_values[idx]
func get_rail_distance(x: int, z: int) -> float:
var idx := get_index(x, z)
if idx < 0 or idx >= rail_distances.size():
return INF
return rail_distances[idx]
func get_station_distance(x: int, z: int) -> float:
var idx := get_index(x, z)
if idx < 0 or idx >= station_distances.size():
return INF
return station_distances[idx]
func get_mask_value(mask_index: int, x: int, z: int) -> float:
if mask_index < 0 or mask_index >= mask_values.size():
return 0.0
var mask_arr: Array = mask_values[mask_index]
var idx := get_index(x, z)
if idx < 0 or idx >= mask_arr.size():
return 0.0
return mask_arr[idx]
func get_calculated_slope(x: int, z: int) -> float:
if x <= 0 or x >= resolution.x - 1 or z <= 0 or z >= resolution.y - 1:
return 0.0
var step_x := terrain_size.x / float(resolution.x - 1)
var step_z := terrain_size.y / float(resolution.y - 1)
var h := get_height(x, z)
var h_xm := get_height(x - 1, z)
var h_xp := get_height(x + 1, z)
var h_zm := get_height(x, z - 1)
var h_zp := get_height(x, z + 1)
var dx := (h_xp - h_xm) / (2.0 * step_x)
var dz := (h_zp - h_zm) / (2.0 * step_z)
return rad_to_deg(atan(sqrt(dx * dx + dz * dz)))
func can_place_prop(x: int, z: int, rules: Dictionary) -> bool:
var slope := get_calculated_slope(x, z)
var rail_dist := get_rail_distance(x, z)
var station_dist := get_station_distance(x, z)
var biome := get_biome_id(x, z)
if rules.has("max_slope") and slope > rules["max_slope"]:
return false
if rules.has("min_rail_distance") and rail_dist < rules["min_rail_distance"]:
return false
if rules.has("min_station_distance") and station_dist < rules["min_station_distance"]:
return false
if rules.has("allowed_biomes") and not biome in rules["allowed_biomes"]:
return false
if rules.has("excluded_biomes") and biome in rules["excluded_biomes"]:
return false
if rules.has("required_mask"):
var mask_idx: int = rules["required_mask"]
var threshold: float = rules.get("mask_threshold", 0.5)
if get_mask_value(mask_idx, x, z) < threshold:
return false
if rules.has("excluded_mask"):
var mask_idx: int = rules["excluded_mask"]
var threshold: float = rules.get("exclude_threshold", 0.5)
if get_mask_value(mask_idx, x, z) >= threshold:
return false
return true
func get_density_multiplier(x: int, z: int, rules: Dictionary) -> float:
var multiplier := 1.0
if rules.has("slope_density_curve"):
var slope := get_calculated_slope(x, z)
var curve: Curve = rules["slope_density_curve"]
multiplier *= curve.sample(clampf(slope / 90.0, 0.0, 1.0))
if rules.has("rail_distance_curve"):
var dist := get_rail_distance(x, z)
var curve: Curve = rules["rail_distance_curve"]
var max_dist: float = rules.get("rail_distance_max", 100.0)
multiplier *= curve.sample(clampf(dist / max_dist, 0.0, 1.0))
if rules.has("density_mask"):
var mask_idx: int = rules["density_mask"]
multiplier *= get_mask_value(mask_idx, x, z)
return multiplier

View File

@@ -0,0 +1 @@
uid://csau51iw3gxam

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
shader_type spatial;
render_mode cull_disabled;
render_mode cull_back;
group_uniforms textures;
uniform sampler2D texture_top : source_color, filter_linear_mipmap, repeat_enable;
@@ -11,6 +11,8 @@ uniform float slope_threshold : hint_range(0.0, 1.0) = 0.7;
group_uniforms uv_scaling;
uniform vec3 uv_scale = vec3(0.1, 0.1, 0.1);
uniform vec2 texture_top_tiling = vec2(1.0, 1.0);
uniform vec2 texture_side_tiling = vec2(1.0, 1.0);
varying vec3 world_pos;
varying vec3 world_normal;
@@ -23,9 +25,9 @@ void vertex() {
void fragment() {
vec3 abs_normal = abs(world_normal);
vec2 uv_xz = world_pos.xz * uv_scale.x;
vec2 uv_xy = world_pos.xy * uv_scale.y;
vec2 uv_zy = world_pos.zy * uv_scale.z;
vec2 uv_xz = world_pos.xz * uv_scale.x * texture_top_tiling;
vec2 uv_xy = world_pos.xy * uv_scale.y * texture_side_tiling;
vec2 uv_zy = world_pos.zy * uv_scale.z * texture_side_tiling;
vec3 weights = pow(abs_normal, vec3(blend_sharpness));
weights /= (weights.x + weights.y + weights.z);