Compare commits
9 Commits
develop
...
proc-terra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24053f2d51 | ||
| fe11763496 | |||
|
|
dd1ba0fa9a | ||
|
|
c1ce3b06f4 | ||
|
|
40cadd2f73 | ||
| 571605de8a | |||
| 7e502d30c1 | |||
| 54ef79eb82 | |||
| 8ed46a2ab9 |
461
.agents/skills/godot-gdscript/SKILL.md
Normal file
461
.agents/skills/godot-gdscript/SKILL.md
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
---
|
||||||
|
name: godot-gdscript
|
||||||
|
description: "Develops GDScript code for Godot 4.6 projects. Use when writing, editing, or debugging GDScript files, creating nodes, signals, exports, or any Godot game development task."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 4.6 GDScript Development
|
||||||
|
|
||||||
|
Write idiomatic GDScript for Godot 4.6 following these conventions and language features.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
@tool # Optional: runs in editor
|
||||||
|
@icon("res://path/to/icon.svg") # Optional: custom icon
|
||||||
|
class_name MyClass # Optional: registers global class
|
||||||
|
extends BaseClass # Required (defaults to RefCounted)
|
||||||
|
|
||||||
|
## Signals
|
||||||
|
signal health_changed(old_value, new_value)
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
enum State { IDLE, RUNNING, JUMPING }
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
const MAX_SPEED = 100.0
|
||||||
|
|
||||||
|
## Exports (inspector properties)
|
||||||
|
@export var speed: float = 50.0
|
||||||
|
@export_range(0, 100) var health: int = 100
|
||||||
|
|
||||||
|
## Public variables
|
||||||
|
var velocity: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
## Private variables (convention: prefix with _)
|
||||||
|
var _internal_state: int = 0
|
||||||
|
|
||||||
|
## @onready variables (initialized in _ready)
|
||||||
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
|
|
||||||
|
## Static variables
|
||||||
|
static var instance_count: int = 0
|
||||||
|
|
||||||
|
## Built-in virtual methods
|
||||||
|
func _init():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
## Public methods
|
||||||
|
func take_damage(amount: int) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
## Private methods
|
||||||
|
func _calculate_something() -> float:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
## Static methods
|
||||||
|
static func get_instance_count() -> int:
|
||||||
|
return instance_count
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type System
|
||||||
|
|
||||||
|
### Type Hints
|
||||||
|
```gdscript
|
||||||
|
var my_int: int = 5
|
||||||
|
var my_string: String = "hello"
|
||||||
|
var my_node: Node2D = null
|
||||||
|
var inferred := Vector2(1, 2) # Type inferred as Vector2
|
||||||
|
|
||||||
|
func my_func(param: int, optional: String = "default") -> bool:
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Arrays and Dictionaries
|
||||||
|
```gdscript
|
||||||
|
var int_array: Array[int] = [1, 2, 3]
|
||||||
|
var node_array: Array[Node] = []
|
||||||
|
var typed_dict: Dictionary[String, int] = {"a": 1, "b": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Casting
|
||||||
|
```gdscript
|
||||||
|
var node := $Sprite as Sprite2D # Returns null if wrong type
|
||||||
|
if node is Sprite2D:
|
||||||
|
node.texture = some_texture
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
```gdscript
|
||||||
|
signal damage_taken(amount: int)
|
||||||
|
|
||||||
|
func take_damage(amount: int) -> void:
|
||||||
|
health -= amount
|
||||||
|
damage_taken.emit(amount)
|
||||||
|
|
||||||
|
# Connecting signals
|
||||||
|
func _ready():
|
||||||
|
$Button.pressed.connect(_on_button_pressed)
|
||||||
|
# With bound arguments
|
||||||
|
$Enemy.died.connect(_on_enemy_died.bind(enemy_name))
|
||||||
|
|
||||||
|
func _on_button_pressed():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties (Setters/Getters)
|
||||||
|
```gdscript
|
||||||
|
var health: int = 100:
|
||||||
|
get:
|
||||||
|
return health
|
||||||
|
set(value):
|
||||||
|
health = clamp(value, 0, max_health)
|
||||||
|
health_changed.emit(health)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Annotations
|
||||||
|
```gdscript
|
||||||
|
@export var speed: float = 10.0
|
||||||
|
@export_range(0, 100, 1) var health: int = 100
|
||||||
|
@export_enum("Walk", "Run", "Jump") var movement: int
|
||||||
|
@export_file("*.png") var texture_path: String
|
||||||
|
@export_dir var save_directory: String
|
||||||
|
@export_node_path("Sprite2D", "Sprite3D") var sprite_path: NodePath
|
||||||
|
@export_flags("Fire", "Water", "Earth", "Wind") var elements: int
|
||||||
|
@export_group("Movement")
|
||||||
|
@export var walk_speed: float
|
||||||
|
@export var run_speed: float
|
||||||
|
@export_subgroup("Physics")
|
||||||
|
@export var gravity: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node References
|
||||||
|
```gdscript
|
||||||
|
@onready var player := $Player as CharacterBody2D
|
||||||
|
@onready var unique_node := %UniqueNodeName # Scene-unique node
|
||||||
|
|
||||||
|
# Get nodes dynamically
|
||||||
|
var child = get_node("Path/To/Child")
|
||||||
|
var parent = get_parent()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Awaiting Signals/Coroutines
|
||||||
|
```gdscript
|
||||||
|
func async_operation() -> void:
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
print("1 second passed")
|
||||||
|
|
||||||
|
var result = await some_async_func()
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Match Statements
|
||||||
|
```gdscript
|
||||||
|
match state:
|
||||||
|
State.IDLE:
|
||||||
|
play_idle_animation()
|
||||||
|
State.RUNNING:
|
||||||
|
play_run_animation()
|
||||||
|
_:
|
||||||
|
push_warning("Unknown state")
|
||||||
|
|
||||||
|
# Pattern matching with guards
|
||||||
|
match point:
|
||||||
|
[var x, var y] when x == y:
|
||||||
|
print("On diagonal")
|
||||||
|
[var x, var y]:
|
||||||
|
print("Point: ", x, ", ", y)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lambda Functions
|
||||||
|
```gdscript
|
||||||
|
var double = func(x): return x * 2
|
||||||
|
print(double.call(5)) # 10
|
||||||
|
|
||||||
|
# With type hints
|
||||||
|
var add: Callable = func(a: int, b: int) -> int: return a + b
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variadic Functions (4.5+)
|
||||||
|
```gdscript
|
||||||
|
func log_values(prefix: String, ...values: Array) -> void:
|
||||||
|
for v in values:
|
||||||
|
print(prefix, v)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Types Reference
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `null` | Empty value |
|
||||||
|
| `bool` | `true` or `false` |
|
||||||
|
| `int` | 64-bit integer |
|
||||||
|
| `float` | 64-bit float |
|
||||||
|
| `String` | Unicode string |
|
||||||
|
| `StringName` | Interned string (fast comparison) |
|
||||||
|
| `NodePath` | Path to node/property |
|
||||||
|
| `Vector2/Vector2i` | 2D vector |
|
||||||
|
| `Vector3/Vector3i` | 3D vector |
|
||||||
|
| `Rect2` | 2D rectangle |
|
||||||
|
| `Transform2D/Transform3D` | Transformation matrix |
|
||||||
|
| `Color` | RGBA color |
|
||||||
|
| `Array` | Dynamic array |
|
||||||
|
| `Dictionary` | Key-value map |
|
||||||
|
| `Callable` | Function reference |
|
||||||
|
| `Signal` | Signal reference |
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
- `RefCounted` subclasses: Automatically freed when no references
|
||||||
|
- `Node` subclasses: Must call `queue_free()` or `free()`
|
||||||
|
- Use `weakref()` to avoid reference cycles
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var weak = weakref(some_object)
|
||||||
|
if weak.get_ref():
|
||||||
|
weak.get_ref().do_something()
|
||||||
|
|
||||||
|
if is_instance_valid(node):
|
||||||
|
node.queue_free()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abstract Classes (4.5+)
|
||||||
|
```gdscript
|
||||||
|
@abstract
|
||||||
|
class_name BaseEnemy
|
||||||
|
extends CharacterBody2D
|
||||||
|
|
||||||
|
@abstract func attack() -> void
|
||||||
|
|
||||||
|
class Zombie extends BaseEnemy:
|
||||||
|
func attack() -> void:
|
||||||
|
# Must implement abstract method
|
||||||
|
melee_attack()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Style Guide
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
| Type | Convention | Example |
|
||||||
|
|------|------------|---------|
|
||||||
|
| File names | snake_case | `yaml_parser.gd` |
|
||||||
|
| Class names | PascalCase | `class_name YAMLParser` |
|
||||||
|
| Node names | PascalCase | `Camera3D`, `Player` |
|
||||||
|
| Functions | snake_case | `func load_level():` |
|
||||||
|
| Variables | snake_case | `var particle_effect` |
|
||||||
|
| Signals | snake_case (past tense) | `signal door_opened` |
|
||||||
|
| Constants | CONSTANT_CASE | `const MAX_SPEED = 200` |
|
||||||
|
| Enum names | PascalCase | `enum Element` |
|
||||||
|
| Enum members | CONSTANT_CASE | `{EARTH, WATER, AIR}` |
|
||||||
|
|
||||||
|
### Private Members
|
||||||
|
Prepend underscore for private/virtual methods and variables:
|
||||||
|
```gdscript
|
||||||
|
var _internal_state: int = 0
|
||||||
|
func _calculate_damage() -> int:
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Order
|
||||||
|
Organize scripts in this order:
|
||||||
|
1. `@tool` (if needed)
|
||||||
|
2. `@icon`
|
||||||
|
3. `class_name`
|
||||||
|
4. `extends`
|
||||||
|
5. `## Documentation comments`
|
||||||
|
6. Signals
|
||||||
|
7. Enums
|
||||||
|
8. Constants
|
||||||
|
9. `@export` variables
|
||||||
|
10. Public variables
|
||||||
|
11. Private variables
|
||||||
|
12. `@onready` variables
|
||||||
|
13. Virtual methods (`_init`, `_ready`, `_process`, etc.)
|
||||||
|
14. Public methods
|
||||||
|
15. Private methods
|
||||||
|
16. Inner classes
|
||||||
|
|
||||||
|
### Formatting Rules
|
||||||
|
|
||||||
|
**Indentation**: Use tabs, not spaces.
|
||||||
|
|
||||||
|
**Blank lines**:
|
||||||
|
- Two blank lines between functions/class definitions
|
||||||
|
- One blank line to separate logical sections within functions
|
||||||
|
|
||||||
|
**Line length**: Keep under 100 characters (prefer 80).
|
||||||
|
|
||||||
|
**Comments**:
|
||||||
|
```gdscript
|
||||||
|
# Regular comment (space after #)
|
||||||
|
## Documentation comment
|
||||||
|
#var disabled_code # No space for commented code
|
||||||
|
#region Region Name
|
||||||
|
#endregion
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiline statements** - Use parentheses, put `and`/`or` at line start:
|
||||||
|
```gdscript
|
||||||
|
if (
|
||||||
|
is_valid
|
||||||
|
and has_permission
|
||||||
|
and not is_disabled
|
||||||
|
):
|
||||||
|
do_something()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enums** - One item per line:
|
||||||
|
```gdscript
|
||||||
|
enum State {
|
||||||
|
IDLE,
|
||||||
|
RUNNING,
|
||||||
|
JUMPING,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Typing
|
||||||
|
|
||||||
|
Static typing catches errors at compile time, improves autocompletion, and enables performance optimizations.
|
||||||
|
|
||||||
|
### Basic Syntax
|
||||||
|
```gdscript
|
||||||
|
# Variables
|
||||||
|
var health: int = 100
|
||||||
|
var velocity: Vector2 = Vector2.ZERO
|
||||||
|
var player: CharacterBody2D
|
||||||
|
|
||||||
|
# Type inference with :=
|
||||||
|
var speed := 10.0 # Inferred as float
|
||||||
|
var position := Vector2.ZERO # Inferred as Vector2
|
||||||
|
|
||||||
|
# Constants (type optional but recommended for arrays)
|
||||||
|
const MAX_SPEED: float = 200.0
|
||||||
|
const ITEMS: Array[String] = ["sword", "shield"]
|
||||||
|
|
||||||
|
# Function parameters and return types
|
||||||
|
func calculate_damage(base: int, multiplier: float) -> int:
|
||||||
|
return int(base * multiplier)
|
||||||
|
|
||||||
|
# Void return type
|
||||||
|
func apply_damage(amount: int) -> void:
|
||||||
|
health -= amount
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Collections
|
||||||
|
```gdscript
|
||||||
|
# Typed arrays
|
||||||
|
var enemies: Array[Enemy] = []
|
||||||
|
var scores: Array[int] = [10, 20, 30]
|
||||||
|
|
||||||
|
# Typed dictionaries (Godot 4.4+)
|
||||||
|
var inventory: Dictionary[String, int] = {"gold": 100}
|
||||||
|
var stats: Dictionary[StringName, float] = {}
|
||||||
|
|
||||||
|
# Typed for loops
|
||||||
|
for enemy: Enemy in enemies:
|
||||||
|
enemy.take_damage(10)
|
||||||
|
|
||||||
|
for name: String in ["Alice", "Bob"]:
|
||||||
|
print(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Casting
|
||||||
|
```gdscript
|
||||||
|
# Safe casting with 'as' (returns null if wrong type)
|
||||||
|
var sprite := $Sprite as Sprite2D
|
||||||
|
if sprite:
|
||||||
|
sprite.texture = new_texture
|
||||||
|
|
||||||
|
# Type checking with 'is'
|
||||||
|
if node is CharacterBody2D:
|
||||||
|
var body: CharacterBody2D = node
|
||||||
|
body.move_and_slide()
|
||||||
|
|
||||||
|
# Casting node references for safe lines
|
||||||
|
@onready var timer := $Timer as Timer
|
||||||
|
@onready var player := $Player as CharacterBody2D
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safe vs Unsafe Lines
|
||||||
|
The editor marks lines green (safe) or grey (unsafe):
|
||||||
|
```gdscript
|
||||||
|
# Safe - type is known
|
||||||
|
var timer: Timer = $Timer as Timer
|
||||||
|
|
||||||
|
# Unsafe - type could fail at runtime
|
||||||
|
var timer = $Timer # No type info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Global Scope Methods
|
||||||
|
Use typed versions for better performance:
|
||||||
|
```gdscript
|
||||||
|
# Prefer typed versions
|
||||||
|
var node: Node = instance_from_id(id) as Node # Safe
|
||||||
|
var clamped: int = clampi(value, 0, 100) # Typed int clamp
|
||||||
|
var lerped: float = lerpf(0.0, 1.0, 0.5) # Typed float lerp
|
||||||
|
|
||||||
|
# Typed math functions
|
||||||
|
var abs_val: int = absi(-5)
|
||||||
|
var max_val: float = maxf(a, b)
|
||||||
|
var min_val: int = mini(a, b)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safe Property/Method Access
|
||||||
|
```gdscript
|
||||||
|
# Unsafe - warnings if property doesn't exist on type
|
||||||
|
func process_node(node: Node2D) -> void:
|
||||||
|
if "custom_property" in node:
|
||||||
|
node.custom_property = 10 # UNSAFE_PROPERTY_ACCESS warning
|
||||||
|
|
||||||
|
# Safe - cast first, then access
|
||||||
|
func process_node_safe(node: Node2D) -> void:
|
||||||
|
if node is MyScript:
|
||||||
|
var typed: MyScript = node as MyScript
|
||||||
|
typed.custom_property = 10 # Safe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
```gdscript
|
||||||
|
# Cannot type individual array/dict elements inline
|
||||||
|
var arr = [1, "two", 3.0] # Mixed types - can't specify
|
||||||
|
|
||||||
|
# Nested typed collections not supported
|
||||||
|
var nested: Array[Array[int]] # ERROR - not allowed
|
||||||
|
|
||||||
|
# Use this instead
|
||||||
|
var nested: Array[Array] = [[1, 2], [3, 4]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Editor Settings
|
||||||
|
Enable in **Project Settings > Debug > GDScript**:
|
||||||
|
- `UNTYPED_DECLARATION` - Warn on untyped variables
|
||||||
|
- `INFERRED_DECLARATION` - Prefer explicit over inferred types
|
||||||
|
- `UNSAFE_PROPERTY_ACCESS` - Warn on unsafe property access
|
||||||
|
- `UNSAFE_METHOD_ACCESS` - Warn on unsafe method calls
|
||||||
|
|
||||||
|
Enable in **Editor Settings > Text Editor > Completion**:
|
||||||
|
- **Add Type Hints** - Auto-add types on completion
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use static typing** - Improves performance and catches errors
|
||||||
|
2. **Prefer `@onready`** - For node references instead of `_ready()` assignments
|
||||||
|
3. **Use signals** - For decoupled communication between nodes
|
||||||
|
4. **Avoid `get_node()` in `_process()`** - Cache references with `@onready`
|
||||||
|
5. **Use `StringName` for frequent comparisons** - `&"my_string"`
|
||||||
|
6. **Group exports** - Use `@export_group` and `@export_subgroup`
|
||||||
|
7. **Document with `##`** - Double hash for documentation comments
|
||||||
|
8. **Declare locals near first use** - Don't declare at top of function
|
||||||
|
9. **Use past tense for signals** - `health_depleted`, `door_opened`
|
||||||
|
10. **Match file name to class** - `YAMLParser` → `yaml_parser.gd`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[folding]
|
||||||
|
|
||||||
|
sections_unfolded=PackedStringArray()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[folding]
|
||||||
|
|
||||||
|
sections_unfolded=PackedStringArray()
|
||||||
BIN
proc-terrain/Dirt_baseColor.tga
Normal file
BIN
proc-terrain/Dirt_baseColor.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
BIN
proc-terrain/Grass_baseColor.tga
Normal file
BIN
proc-terrain/Grass_baseColor.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
100
proc-terrain/TERRAIN_CONTROL_SPEC.md
Normal file
100
proc-terrain/TERRAIN_CONTROL_SPEC.md
Normal 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
BIN
proc-terrain/biomes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
4
proc-terrain/default_noise.tres
Normal file
4
proc-terrain/default_noise.tres
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="FastNoiseLite" format=3 uid="uid://clpxlmv87brc7"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
seed = 42
|
||||||
16
proc-terrain/default_terrain_material.tres
Normal file
16
proc-terrain/default_terrain_material.tres
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[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://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
|
||||||
|
shader = ExtResource("1_shader")
|
||||||
|
shader_parameter/texture_top = ExtResource("3_8s6t3")
|
||||||
|
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)
|
||||||
4
proc-terrain/preview_material.tres
Normal file
4
proc-terrain/preview_material.tres
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://40g5lvx2hw5k"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
vertex_color_use_as_albedo = true
|
||||||
BIN
proc-terrain/slop_limit_map.png
Normal file
BIN
proc-terrain/slop_limit_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
proc-terrain/slop_limit_map_1.png
Normal file
BIN
proc-terrain/slop_limit_map_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
16
proc-terrain/terrain_control_layer.tres
Normal file
16
proc-terrain/terrain_control_layer.tres
Normal 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")
|
||||||
110
proc-terrain/terrain_control_layers.gd
Normal file
110
proc-terrain/terrain_control_layers.gd
Normal 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]
|
||||||
1
proc-terrain/terrain_control_layers.gd.uid
Normal file
1
proc-terrain/terrain_control_layers.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dn0dmf6vj6ybp
|
||||||
507
proc-terrain/terrain_generator.gd
Normal file
507
proc-terrain/terrain_generator.gd
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
@tool
|
||||||
|
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):
|
||||||
|
noise_setting.changed.disconnect(_on_settings_changed)
|
||||||
|
noise_setting = value
|
||||||
|
if noise_setting:
|
||||||
|
noise_setting.changed.connect(_on_settings_changed)
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export var material_setting: Material:
|
||||||
|
set(value):
|
||||||
|
material_setting = value
|
||||||
|
if mesh:
|
||||||
|
mesh.surface_set_material(0, material_setting)
|
||||||
|
|
||||||
|
@export_group("Terrain Size")
|
||||||
|
@export var terrain_size: Vector2 = Vector2(200, 200):
|
||||||
|
set(value):
|
||||||
|
terrain_size = value
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export_range(2, 512) var subdivision: int = 128:
|
||||||
|
set(value):
|
||||||
|
subdivision = value
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export var max_height: float = 50.0:
|
||||||
|
set(value):
|
||||||
|
max_height = value
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export_group("Island Mask")
|
||||||
|
@export var island_mask: bool = true:
|
||||||
|
set(value):
|
||||||
|
island_mask = value
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export_range(0.0, 1.0) var mask_falloff_start: float = 0.5:
|
||||||
|
set(value):
|
||||||
|
mask_falloff_start = value
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
@export_range(0.1, 2.0) var mask_power: float = 1.5:
|
||||||
|
set(value):
|
||||||
|
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):
|
||||||
|
generate_terrain()
|
||||||
|
|
||||||
|
@export var bake_collision: bool = false:
|
||||||
|
set(_value):
|
||||||
|
_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:
|
||||||
|
if noise_setting:
|
||||||
|
if not noise_setting.changed.is_connected(_on_settings_changed):
|
||||||
|
noise_setting.changed.connect(_on_settings_changed)
|
||||||
|
generate_terrain()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_settings_changed() -> void:
|
||||||
|
_request_update()
|
||||||
|
|
||||||
|
|
||||||
|
func _request_update() -> void:
|
||||||
|
if _update_queued:
|
||||||
|
return
|
||||||
|
_update_queued = true
|
||||||
|
call_deferred("_do_update")
|
||||||
|
|
||||||
|
|
||||||
|
func _do_update() -> void:
|
||||||
|
_update_queued = false
|
||||||
|
generate_terrain()
|
||||||
|
|
||||||
|
|
||||||
|
func generate_terrain() -> void:
|
||||||
|
if not noise_setting:
|
||||||
|
return
|
||||||
|
|
||||||
|
_cache_constraints()
|
||||||
|
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
|
||||||
|
var step_x: float = terrain_size.x / float(subdivision - 1)
|
||||||
|
var step_z: float = terrain_size.y / float(subdivision - 1)
|
||||||
|
var half_x: float = terrain_size.x / 2.0
|
||||||
|
var half_z: float = terrain_size.y / 2.0
|
||||||
|
|
||||||
|
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] = []
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
var idx10: int = z * subdivision + (x + 1)
|
||||||
|
var idx01: int = (z + 1) * subdivision + x
|
||||||
|
var idx11: int = (z + 1) * subdivision + (x + 1)
|
||||||
|
|
||||||
|
var x0: float = x * step_x - half_x
|
||||||
|
var x1: float = (x + 1) * step_x - half_x
|
||||||
|
var z0: float = z * step_z - half_z
|
||||||
|
var z1: float = (z + 1) * step_z - half_z
|
||||||
|
|
||||||
|
var v00 := Vector3(x0, heights[idx00], z0)
|
||||||
|
var v10 := Vector3(x1, heights[idx10], z0)
|
||||||
|
var v01 := Vector3(x0, heights[idx01], z1)
|
||||||
|
var v11 := Vector3(x1, heights[idx11], z1)
|
||||||
|
|
||||||
|
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_color(c00)
|
||||||
|
st.set_uv(uv00)
|
||||||
|
st.add_vertex(v00)
|
||||||
|
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, v11, v01 (CCW winding, normal pointing up)
|
||||||
|
var normal2: Vector3 = (v01 - v10).cross(v11 - v10).normalized()
|
||||||
|
st.set_normal(normal2)
|
||||||
|
st.set_color(c10)
|
||||||
|
st.set_uv(uv10)
|
||||||
|
st.add_vertex(v10)
|
||||||
|
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:
|
||||||
|
var nx: float = x / half_x
|
||||||
|
var nz: float = z / half_z
|
||||||
|
var dist: float = sqrt(nx * nx + nz * nz)
|
||||||
|
|
||||||
|
if dist < mask_falloff_start:
|
||||||
|
return 1.0
|
||||||
|
elif dist > 1.0:
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
var t: float = (dist - mask_falloff_start) / (1.0 - mask_falloff_start)
|
||||||
|
return pow(1.0 - t, mask_power)
|
||||||
|
|
||||||
|
|
||||||
|
func _bake_collision() -> void:
|
||||||
|
if not mesh:
|
||||||
|
push_warning("TerrainGenerator: No mesh to bake collision from")
|
||||||
|
return
|
||||||
|
|
||||||
|
var static_body: StaticBody3D
|
||||||
|
var collision_shape: CollisionShape3D
|
||||||
|
|
||||||
|
for child in get_children():
|
||||||
|
if child is StaticBody3D:
|
||||||
|
static_body = child
|
||||||
|
break
|
||||||
|
|
||||||
|
if not static_body:
|
||||||
|
static_body = StaticBody3D.new()
|
||||||
|
static_body.name = "TerrainCollision"
|
||||||
|
add_child(static_body)
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
static_body.owner = get_tree().edited_scene_root
|
||||||
|
|
||||||
|
for child in static_body.get_children():
|
||||||
|
if child is CollisionShape3D:
|
||||||
|
collision_shape = child
|
||||||
|
break
|
||||||
|
|
||||||
|
if not collision_shape:
|
||||||
|
collision_shape = CollisionShape3D.new()
|
||||||
|
collision_shape.name = "CollisionShape"
|
||||||
|
static_body.add_child(collision_shape)
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
collision_shape.owner = get_tree().edited_scene_root
|
||||||
|
|
||||||
|
collision_shape.shape = mesh.create_trimesh_shape()
|
||||||
|
print("TerrainGenerator: Collision baked successfully")
|
||||||
1
proc-terrain/terrain_generator.gd.uid
Normal file
1
proc-terrain/terrain_generator.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://brr6het2uwnp5
|
||||||
153
proc-terrain/terrain_props_data.gd
Normal file
153
proc-terrain/terrain_props_data.gd
Normal 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
|
||||||
1
proc-terrain/terrain_props_data.gd.uid
Normal file
1
proc-terrain/terrain_props_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://csau51iw3gxam
|
||||||
106
proc-terrain/terrain_toy.tscn
Normal file
106
proc-terrain/terrain_toy.tscn
Normal file
File diff suppressed because one or more lines are too long
50
proc-terrain/triplanar_slope.gdshader
Normal file
50
proc-terrain/triplanar_slope.gdshader
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
render_mode cull_back;
|
||||||
|
|
||||||
|
group_uniforms textures;
|
||||||
|
uniform sampler2D texture_top : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
|
uniform sampler2D texture_side : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
|
|
||||||
|
group_uniforms blending;
|
||||||
|
uniform float blend_sharpness : hint_range(0.1, 20.0) = 5.0;
|
||||||
|
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;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec3 abs_normal = abs(world_normal);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
vec4 tex_xz = texture(texture_top, uv_xz);
|
||||||
|
vec4 tex_xy = texture(texture_side, uv_xy);
|
||||||
|
vec4 tex_zy = texture(texture_side, uv_zy);
|
||||||
|
|
||||||
|
vec4 triplanar_color = tex_xz * weights.y + tex_xy * weights.z + tex_zy * weights.x;
|
||||||
|
|
||||||
|
float up_factor = dot(world_normal, vec3(0.0, 1.0, 0.0));
|
||||||
|
float slope_blend = smoothstep(slope_threshold - 0.1, slope_threshold + 0.1, up_factor);
|
||||||
|
|
||||||
|
vec4 top_color = texture(texture_top, uv_xz);
|
||||||
|
vec4 side_color = (tex_xy * weights.z + tex_zy * weights.x) / max(weights.z + weights.x, 0.001);
|
||||||
|
|
||||||
|
vec4 final_color = mix(side_color, top_color, slope_blend);
|
||||||
|
|
||||||
|
ALBEDO = final_color.rgb;
|
||||||
|
}
|
||||||
1
proc-terrain/triplanar_slope.gdshader.uid
Normal file
1
proc-terrain/triplanar_slope.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d4b4lfd2r51iu
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
[ext_resource type="ArrayMesh" uid="uid://cm8un80nubjbi" path="res://resources/train kit/train-diesel-a.obj" id="3_jy5c4"]
|
[ext_resource type="ArrayMesh" uid="uid://cm8un80nubjbi" path="res://resources/train kit/train-diesel-a.obj" id="3_jy5c4"]
|
||||||
[ext_resource type="Script" uid="uid://b65vwus8xxmun" path="res://scripts/traincontrol_camera.gd" id="4_jy5c4"]
|
[ext_resource type="Script" uid="uid://b65vwus8xxmun" path="res://scripts/traincontrol_camera.gd" id="4_jy5c4"]
|
||||||
[ext_resource type="Script" uid="uid://df2oi5vyeak81" path="res://scripts/train_movement.gd" id="6_1lqs8"]
|
[ext_resource type="Script" uid="uid://df2oi5vyeak81" path="res://scripts/train_movement.gd" id="6_1lqs8"]
|
||||||
|
[ext_resource type="Script" uid="uid://b5x80gpkn1swt" path="res://scripts/rail_swtich.gd" id="7_cenvf"]
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="PlaneMesh_23vqf"]
|
[sub_resource type="PlaneMesh" id="PlaneMesh_23vqf"]
|
||||||
size = Vector2(100, 100)
|
size = Vector2(100, 100)
|
||||||
@@ -17,10 +18,10 @@ albedo_color = Color(2.153851e-06, 0.25305444, 0.03645257, 1)
|
|||||||
[sub_resource type="Curve3D" id="Curve3D_0b1nv"]
|
[sub_resource type="Curve3D" id="Curve3D_0b1nv"]
|
||||||
closed = true
|
closed = true
|
||||||
_data = {
|
_data = {
|
||||||
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, -41.962486, 0.03566742, -44.78871, 0, 0, 0, 0, 0, 0, -7.760188, 0.036315918, -45.60305, 0, 0, 0, 0, 0, 0, 21.603975, 0.03604126, -45.26774, 0, 0, 0, 0, 0, 0, 41.148148, 0.03127289, -39.27994, 0, 0, 0, 0, 0, 0, 44.2618, 0.02154541, -27.064838, 0, 0, 0, 0, 0, 0, 44.118095, 0.0033874512, -4.2633114, 0, 0, 0, 0, 0, 0, 26.96904, 0.003112793, -3.9279928, 0, 0, 0, 0, 0, 0, 3.4968712, 0.0079574585, -10.011598, 0, 0, 0, 0, 0, 0, -3.8321874, 0.024543762, -30.849133, 0, 0, 0, 0, 0, 0, -25.627771, 0.026069641, -32.765224, 0, 0, 0, 0, 0, 0, -31.28027, 0.015075684, -18.969343, 0, 0, 0, 0, 0, 0, -23.472174, 0.003479004, -4.4070177, 0, 0, 0, 0, 0, 0, 12.167218, -0.011207581, 14.035401, 0, 0, 0, 0, 0, 0, 34.681347, -0.013763428, 17.244862, 0, 0, 0, 0, 0, 0, 46.99225, -0.019981384, 25.052942, 0, 0, 0, 0, 0, 0, 42.72894, -0.03504944, 43.97439, 0, 0, 0, 0, 0, 0, 14.897652, -0.036231995, 45.459354, 0, 0, 0, 0, 0, 0, -23.088955, -0.034858704, 43.73488, 0, 0, 0, 0, 0, 0, -44.357616, -0.034965515, 43.878586, 0, 0, 0, 0, 0, 0, -45.603073, -0.010246277, 12.837832, 0, 0, 0, 0, 0, 0, -34.585533, 0.0079422, -10.0116005, 0, 0, 0, 0, 0, 0, -38.321926, 0.022087097, -27.78338),
|
"points": PackedVector3Array(-2.1678085, -0.043792725, 1.658886, 2.1678085, 0.043792725, -1.658886, -41.962486, 0.03566742, -44.78871, 0, 0, 0, 0, 0, 0, -7.760188, 0.036315918, -45.60305, -6.559948, 0.007423401, -0.16356277, 6.559948, -0.007423401, 0.16356277, 21.603975, 0.03604126, -45.26774, -3.4513588, 0.124176025, -4.544277, 3.4513588, -0.124176025, 4.544277, 40.931072, 0.032211304, -40.35701, 0.5563507, 0.09750366, -3.6268482, -0.5563507, -0.09750366, 3.6268482, 45.43544, 0.021453857, -26.947992, 3.438774, 0.09018707, -3.405755, -3.438774, -0.09018707, 3.405755, 38.412643, 0.47789, -9.150471, 6.6514378, -0.022911072, 0.7331085, -6.6514378, 0.022911072, -0.7331085, 25.856459, 0.10243797, -3.7440395, 5.315757, -0.10071564, 3.6390858, -5.315757, 0.10071564, -3.6390858, 3.4968712, 0.0079574585, -10.011598, 4.9409637, -0.0754776, 2.709568, -4.9409637, 0.0754776, -2.709568, -3.8321874, 0.024543762, -30.849133, 6.360344, 0.0488739, -1.9223251, -6.360344, -0.0488739, 1.9223251, -22.311077, 0.005493164, -31.887383, -0.9378357, 0.12989807, -4.797656, 0.9378357, -0.12989807, 4.797656, -31.28027, 0.015075684, -18.969343, -7.12364, 0.096969604, -3.4738579, 7.12364, -0.096969604, 3.4738579, -23.472174, 0.003479004, -4.4070177, -9.224047, 0.08418274, -2.961853, 9.224047, -0.08418274, 2.961853, 12.167218, -0.011207581, 14.035401, -8.825874, 0.03265381, -1.0564928, 8.825874, -0.03265381, 1.0564928, 32.632145, -0.011894226, 14.993831, 0, 0, 0, 0, 0, 0, 39.98447, -0.019104004, 24.146927, 4.9879036, 0.122177124, -4.6163826, -4.9879036, -0.122177124, 4.6163826, 35.724655, 0.047676086, 33.444454, 8.1661625, 0.014076233, -0.6628723, -8.1661625, -0.014076233, 0.6628723, 11.495622, 0.0951767, 40.647045, 4.9570847, -0.058029175, 2.0650558, -4.9570847, 0.058029175, -2.0650558, -22.318954, 0.051948547, 40.504288, 4.9869003, -0.18069458, 6.610588, -4.9869003, 0.18069458, -6.610588, -37.494194, 0.080329895, 26.992659, -1.5943985, -0.19984436, 7.435757, 1.5943985, 0.19984436, -7.435757, -46.58226, 0.10346222, 8.63745, -0.4270401, -0.16287994, 6.045038, 0.4270401, 0.16287994, -6.045038, -39.930115, 0.050865173, -11.514841, 4.8086166, -0.20992279, 7.687052, -4.8086166, 0.20992279, -7.687052, -35.56163, 0.15759277, -32.8561, -0.73628235, -0.15232849, 5.656826, 0.73628235, 0.15232849, -5.656826, -45.566254, 0.602231, -39.59991),
|
||||||
"tilts": PackedFloat32Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
"tilts": PackedFloat32Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
point_count = 22
|
point_count = 23
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ke0jj"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ke0jj"]
|
||||||
albedo_texture = ExtResource("1_ihlg3")
|
albedo_texture = ExtResource("1_ihlg3")
|
||||||
@@ -31,6 +32,14 @@ uv1_offset = Vector3(0, -0.61, 0)
|
|||||||
metallic = 1.0
|
metallic = 1.0
|
||||||
roughness = 0.25
|
roughness = 0.25
|
||||||
|
|
||||||
|
[sub_resource type="Curve3D" id="Curve3D_1lqs8"]
|
||||||
|
closed = true
|
||||||
|
_data = {
|
||||||
|
"points": PackedVector3Array(-6.2964935, 0.06638336, -2.3570595, 6.2964935, -0.06638336, 2.3570595, 24, 0, -36, 0.40251923, 0.1432724, -5.316846, -0.40251923, -0.1432724, 5.316846, 30.535202, 0.016418457, -19.69652, -3.0211945, 0.21754456, -8.010609, 3.0211945, -0.21754456, 8.010609, 37.792625, 0.0013275146, -0.5123644, 7.037945, 0.16593933, -6.2738285, -7.037945, -0.16593933, 6.2738285, 33.177887, 0.0013427734, 21.921793, 12.049584, -0.0362854, 1.1334801, -12.049584, 0.0362854, -1.1334801, 7.185522, 0.0013427734, 33.84686, 3.006013, -0.23966217, 8.824167, -3.006013, 0.23966217, -8.824167, -5.633688, 0.0127334595, 16.86435, 0.09129143, -0.14064789, 5.2124147, -0.09129143, 0.14064789, -5.2124147, -20.520624, 0.0013427734, -3.9991686, -6.1595325, -0.18273163, 6.8687496, 6.1595325, 0.18273163, -6.8687496, -10.095118, 0.0013427734, -27.90497),
|
||||||
|
"tilts": PackedFloat32Array(0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
point_count = 8
|
||||||
|
|
||||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_jgp6u"]
|
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_jgp6u"]
|
||||||
sky_top_color = Color(0.6262097, 0.76737565, 0.9309246, 1)
|
sky_top_color = Color(0.6262097, 0.76737565, 0.9309246, 1)
|
||||||
sky_horizon_color = Color(0.82665706, 0.8808251, 0.95468724, 1)
|
sky_horizon_color = Color(0.82665706, 0.8808251, 0.95468724, 1)
|
||||||
@@ -63,10 +72,10 @@ main_scene = ExtResource("2_0b1nv")
|
|||||||
mesh = SubResource("PlaneMesh_23vqf")
|
mesh = SubResource("PlaneMesh_23vqf")
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_23vqf")
|
surface_material_override/0 = SubResource("StandardMaterial3D_23vqf")
|
||||||
|
|
||||||
[node name="Rails" type="Path3D" parent="Ground" unique_id=1160818899]
|
[node name="Rails1" type="Path3D" parent="Ground" unique_id=1160818899]
|
||||||
curve = SubResource("Curve3D_0b1nv")
|
curve = SubResource("Curve3D_0b1nv")
|
||||||
|
|
||||||
[node name="Ballast" type="CSGPolygon3D" parent="Ground/Rails" unique_id=259312931]
|
[node name="Ballast" type="CSGPolygon3D" parent="Ground/Rails1" unique_id=259312931]
|
||||||
polygon = PackedVector2Array(-1, 0, -1, 0.1, 1, 0.1, 1, 0)
|
polygon = PackedVector2Array(-1, 0, -1, 0.1, 1, 0.1, 1, 0)
|
||||||
mode = 2
|
mode = 2
|
||||||
path_node = NodePath("..")
|
path_node = NodePath("..")
|
||||||
@@ -81,7 +90,7 @@ path_u_distance = 1.0
|
|||||||
path_joined = true
|
path_joined = true
|
||||||
material = SubResource("StandardMaterial3D_ke0jj")
|
material = SubResource("StandardMaterial3D_ke0jj")
|
||||||
|
|
||||||
[node name="LRail" type="CSGPolygon3D" parent="Ground/Rails" unique_id=477776682]
|
[node name="LRail" type="CSGPolygon3D" parent="Ground/Rails1" unique_id=477776682]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
||||||
polygon = PackedVector2Array(-0.5, 0, -0.5, 0.2, -0.4, 0.2, -0.4, 0)
|
polygon = PackedVector2Array(-0.5, 0, -0.5, 0.2, -0.4, 0.2, -0.4, 0)
|
||||||
mode = 2
|
mode = 2
|
||||||
@@ -97,7 +106,7 @@ path_u_distance = 1.0
|
|||||||
path_joined = true
|
path_joined = true
|
||||||
material = SubResource("StandardMaterial3D_ihlg3")
|
material = SubResource("StandardMaterial3D_ihlg3")
|
||||||
|
|
||||||
[node name="RRail" type="CSGPolygon3D" parent="Ground/Rails" unique_id=965437995]
|
[node name="RRail" type="CSGPolygon3D" parent="Ground/Rails1" unique_id=965437995]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
||||||
polygon = PackedVector2Array(0.5, 0, 0.5, 0.2, 0.4, 0.2, 0.5, 0)
|
polygon = PackedVector2Array(0.5, 0, 0.5, 0.2, 0.4, 0.2, 0.5, 0)
|
||||||
mode = 2
|
mode = 2
|
||||||
@@ -113,15 +122,78 @@ path_u_distance = 1.0
|
|||||||
path_joined = true
|
path_joined = true
|
||||||
material = SubResource("StandardMaterial3D_ihlg3")
|
material = SubResource("StandardMaterial3D_ihlg3")
|
||||||
|
|
||||||
[node name="TrainEngine" type="PathFollow3D" parent="Ground/Rails" unique_id=973990755]
|
[node name="TrainEngine" type="PathFollow3D" parent="Ground/Rails1" unique_id=973990755]
|
||||||
transform = Transform3D(0.026897233, -2.1416474e-05, -1.1296921, 0, 1.130161, -2.141972e-05, 1.1296905, 5.0991304e-07, 0.02689727, -41.962486, 0.03566742, -44.78871)
|
transform = Transform3D(0.6867339, -0.014397825, -0.8973005, 0, 1.130071, -0.018126711, 0.8974141, 0.01101774, 0.68664694, -41.962486, 0.03566742, -44.78871)
|
||||||
rotation_mode = 4
|
rotation_mode = 4
|
||||||
script = ExtResource("2_jgp6u")
|
script = ExtResource("2_jgp6u")
|
||||||
|
|
||||||
[node name="Train" type="MeshInstance3D" parent="Ground/Rails/TrainEngine" unique_id=65189673]
|
[node name="Rails2" type="Path3D" parent="Ground" unique_id=1426938039]
|
||||||
|
curve = SubResource("Curve3D_1lqs8")
|
||||||
|
|
||||||
|
[node name="Ballast" type="CSGPolygon3D" parent="Ground/Rails2" unique_id=608449521]
|
||||||
|
polygon = PackedVector2Array(-1, 0, -1, 0.1, 1, 0.1, 1, 0)
|
||||||
|
mode = 2
|
||||||
|
path_node = NodePath("..")
|
||||||
|
path_interval_type = 0
|
||||||
|
path_interval = 1.0
|
||||||
|
path_simplify_angle = 0.0
|
||||||
|
path_rotation = 2
|
||||||
|
path_rotation_accurate = false
|
||||||
|
path_local = false
|
||||||
|
path_continuous_u = true
|
||||||
|
path_u_distance = 1.0
|
||||||
|
path_joined = true
|
||||||
|
material = SubResource("StandardMaterial3D_ke0jj")
|
||||||
|
|
||||||
|
[node name="RRail" type="CSGPolygon3D" parent="Ground/Rails2" unique_id=469196308]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
||||||
|
polygon = PackedVector2Array(0.5, 0, 0.5, 0.2, 0.4, 0.2, 0.5, 0)
|
||||||
|
mode = 2
|
||||||
|
path_node = NodePath("..")
|
||||||
|
path_interval_type = 0
|
||||||
|
path_interval = 1.0
|
||||||
|
path_simplify_angle = 0.0
|
||||||
|
path_rotation = 2
|
||||||
|
path_rotation_accurate = false
|
||||||
|
path_local = false
|
||||||
|
path_continuous_u = true
|
||||||
|
path_u_distance = 1.0
|
||||||
|
path_joined = true
|
||||||
|
material = SubResource("StandardMaterial3D_ihlg3")
|
||||||
|
|
||||||
|
[node name="LRail" type="CSGPolygon3D" parent="Ground/Rails2" unique_id=1887208549]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.24889, -4.7985353, 2.2341003)
|
||||||
|
polygon = PackedVector2Array(-0.5, 0, -0.5, 0.2, -0.4, 0.2, -0.4, 0)
|
||||||
|
mode = 2
|
||||||
|
path_node = NodePath("..")
|
||||||
|
path_interval_type = 0
|
||||||
|
path_interval = 1.0
|
||||||
|
path_simplify_angle = 0.0
|
||||||
|
path_rotation = 2
|
||||||
|
path_rotation_accurate = false
|
||||||
|
path_local = false
|
||||||
|
path_continuous_u = true
|
||||||
|
path_u_distance = 1.0
|
||||||
|
path_joined = true
|
||||||
|
material = SubResource("StandardMaterial3D_ihlg3")
|
||||||
|
|
||||||
|
[node name="TrainEngine" type="PathFollow3D" parent="Ground/Rails2" unique_id=1618605131]
|
||||||
|
transform = Transform3D(-0.39616987, 0.010450672, -1.0582525, 0, 1.1301593, 0.01115706, 1.0583019, 0.003912155, -0.3961513, 24, 0, -36)
|
||||||
|
rotation_mode = 4
|
||||||
|
script = ExtResource("2_jgp6u")
|
||||||
|
|
||||||
|
[node name="Train" type="MeshInstance3D" parent="Ground" unique_id=65189673]
|
||||||
|
transform = Transform3D(0.6867333, -0.014397784, -0.89729905, 0, 1.1300678, -0.018126683, 0.8974134, 0.011017708, 0.68664587, -41.962486, 0.03566742, -44.78871)
|
||||||
mesh = ExtResource("3_jy5c4")
|
mesh = ExtResource("3_jy5c4")
|
||||||
|
skeleton = NodePath("../Rails1/TrainEngine")
|
||||||
script = ExtResource("6_1lqs8")
|
script = ExtResource("6_1lqs8")
|
||||||
|
|
||||||
|
[node name="RailSwtich" type="Area3D" parent="Ground" unique_id=1897237029 node_paths=PackedStringArray("rail_a", "rail_b", "switch_to_rail")]
|
||||||
|
script = ExtResource("7_cenvf")
|
||||||
|
rail_a = NodePath("../Rails1")
|
||||||
|
rail_b = NodePath("../Rails2")
|
||||||
|
switch_to_rail = NodePath("../Rails1")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1729725358]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1729725358]
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
shadow_bias = 0.04
|
shadow_bias = 0.04
|
||||||
@@ -135,7 +207,8 @@ current = true
|
|||||||
fov = 11.9
|
fov = 11.9
|
||||||
near = 0.001
|
near = 0.001
|
||||||
script = ExtResource("4_jy5c4")
|
script = ExtResource("4_jy5c4")
|
||||||
train = NodePath("../Ground/Rails/TrainEngine/Train")
|
train = NodePath("../Ground/Train")
|
||||||
|
follow_distance = 30.0
|
||||||
|
|
||||||
[node name="HUD" type="Control" parent="." unique_id=563257159]
|
[node name="HUD" type="Control" parent="." unique_id=563257159]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
@@ -155,9 +228,9 @@ label_settings = SubResource("LabelSettings_1lqs8")
|
|||||||
|
|
||||||
[node name="camera_label" type="Label" parent="HUD" unique_id=1719494020]
|
[node name="camera_label" type="Label" parent="HUD" unique_id=1719494020]
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 20.0
|
offset_left = 19.0
|
||||||
offset_top = 43.0
|
offset_top = 43.0
|
||||||
offset_right = 1280.0
|
offset_right = 1279.0
|
||||||
offset_bottom = 66.0
|
offset_bottom = 66.0
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
text = "Camera: "
|
text = "Camera: "
|
||||||
@@ -165,15 +238,25 @@ label_settings = SubResource("LabelSettings_1lqs8")
|
|||||||
|
|
||||||
[node name="controls_label" type="Label" parent="HUD" unique_id=1262113688]
|
[node name="controls_label" type="Label" parent="HUD" unique_id=1262113688]
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 20.0
|
offset_left = 19.0
|
||||||
offset_top = 64.0
|
offset_top = 83.0
|
||||||
offset_right = 1280.0
|
offset_right = 1279.0
|
||||||
offset_bottom = 217.0
|
offset_bottom = 236.0
|
||||||
size_flags_vertical = 0
|
size_flags_vertical = 0
|
||||||
text = "Comandi:
|
text = "Comandi:
|
||||||
\"W / S - Accelera / Rallenta\",
|
\"W / S - Accelera / Rallenta\",
|
||||||
\"A / D - Ruota camera\",
|
\"A / D - Ruota camera\",
|
||||||
\"SPACE - Cambia camera\",
|
\"SPACE - Cambia camera\",
|
||||||
\"TAB - Cambia treno\",
|
\"TAB\" - Cambia binario,
|
||||||
\"ESC - Esci\""
|
\"ESC - Esci\""
|
||||||
label_settings = SubResource("LabelSettings_1lqs8")
|
label_settings = SubResource("LabelSettings_1lqs8")
|
||||||
|
|
||||||
|
[node name="switch_label" type="Label" parent="HUD" unique_id=462391845]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 19.0
|
||||||
|
offset_top = 64.0
|
||||||
|
offset_right = 1279.0
|
||||||
|
offset_bottom = 87.0
|
||||||
|
size_flags_vertical = 0
|
||||||
|
text = "Binario: "
|
||||||
|
label_settings = SubResource("LabelSettings_1lqs8")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
extends PathFollow3D
|
extends PathFollow3D
|
||||||
class_name TrainEngine
|
#class_name TrainEngine
|
||||||
|
#
|
||||||
@onready var train: MeshInstance3D = $Train
|
#@onready var train: MeshInstance3D = $"../../Train"
|
||||||
|
#
|
||||||
|
#
|
||||||
func _physics_process(delta: float) -> void:
|
#func _physics_process(delta: float) -> void:
|
||||||
if train:
|
#if train:
|
||||||
progress += train.current_speed * delta
|
#progress += train.current_speed * delta
|
||||||
|
|||||||
17
scripts/rail_switch.gd
Normal file
17
scripts/rail_switch.gd
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
extends Area3D
|
||||||
|
class_name RailSwitch
|
||||||
|
|
||||||
|
@export var rail_a: Path3D
|
||||||
|
@export var rail_b: Path3D
|
||||||
|
@export var switch_to_rail: Path3D # Il binario verso cui deviare
|
||||||
|
|
||||||
|
# Cambia direzione dello scambio
|
||||||
|
func toggle_switch() -> void:
|
||||||
|
if switch_to_rail == rail_a:
|
||||||
|
switch_to_rail = rail_b
|
||||||
|
else:
|
||||||
|
switch_to_rail = rail_a
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
if body is Train:
|
||||||
|
body.switch_to_rail(switch_to_rail)
|
||||||
1
scripts/rail_switch.gd.uid
Normal file
1
scripts/rail_switch.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bhan6tvt4sow
|
||||||
17
scripts/rail_swtich.gd
Normal file
17
scripts/rail_swtich.gd
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
extends Area3D
|
||||||
|
|
||||||
|
@export var rail_a: Path3D
|
||||||
|
@export var rail_b: Path3D
|
||||||
|
@export var switch_to_rail: Path3D # Il binario verso cui deviare
|
||||||
|
|
||||||
|
|
||||||
|
# Cambia direzione dello scambio
|
||||||
|
func toggle_switch() -> void:
|
||||||
|
if switch_to_rail == rail_a:
|
||||||
|
switch_to_rail = rail_b
|
||||||
|
else:
|
||||||
|
switch_to_rail = rail_a
|
||||||
|
|
||||||
|
|
||||||
|
#func get_state_name() -> String:
|
||||||
|
#return "BIANRIO 1" if current_state == SwitchState.LEFT else "BINARIO 2"
|
||||||
1
scripts/rail_swtich.gd.uid
Normal file
1
scripts/rail_swtich.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b5x80gpkn1swt
|
||||||
@@ -1,21 +1,30 @@
|
|||||||
extends MeshInstance3D
|
extends MeshInstance3D
|
||||||
|
class_name TrainBody
|
||||||
|
|
||||||
signal speed_changed(new_speed: float)
|
signal speed_changed(new_speed: float)
|
||||||
|
signal rail_switched(old_rail: Path3D, new_rail: Path3D)
|
||||||
|
|
||||||
@export_group("Train")
|
@export_group("Train")
|
||||||
@export var base_speed: float = 15.0
|
@export var base_speed: float = 15.0
|
||||||
@export var max_speed: float = 50.0
|
@export var max_speed: float = 50.0
|
||||||
@export var min_speed: float = 3.0
|
@export var min_speed: float = 3.0
|
||||||
@export var acceleration: float = 12.0
|
@export var acceleration: float = 12.0
|
||||||
|
@onready var rail_a: Path3D = $"../Rails1"
|
||||||
|
@onready var rail_b: Path3D = $"../Rails2"
|
||||||
|
|
||||||
|
var current_follower: PathFollow3D
|
||||||
|
var current_rail: Path3D
|
||||||
var current_speed: float = 15.0
|
var current_speed: float = 15.0
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
current_speed = base_speed
|
current_speed = base_speed
|
||||||
|
switch_to_rail(rail_a)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
_handle_input(delta)
|
_handle_input(delta)
|
||||||
|
current_follower.progress += current_speed * delta
|
||||||
|
global_transform = current_follower.global_transform
|
||||||
|
|
||||||
|
|
||||||
func _handle_input(delta: float) -> void:
|
func _handle_input(delta: float) -> void:
|
||||||
@@ -29,3 +38,35 @@ func _handle_input(delta: float) -> void:
|
|||||||
|
|
||||||
func get_speed() -> float:
|
func get_speed() -> float:
|
||||||
return current_speed
|
return current_speed
|
||||||
|
|
||||||
|
|
||||||
|
func switch_to_rail(new_rail: Path3D) -> void:
|
||||||
|
if new_rail == current_rail:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_rail = new_rail
|
||||||
|
current_follower = new_rail.get_node("TrainEngine") as PathFollow3D
|
||||||
|
|
||||||
|
var closest_offset = find_closest_point_on_path(new_rail, global_position)
|
||||||
|
current_follower.progress = closest_offset
|
||||||
|
|
||||||
|
|
||||||
|
func find_closest_point_on_path(path: Path3D, point: Vector3) -> float:
|
||||||
|
var curve = path.curve
|
||||||
|
var closest_offset = 0.0
|
||||||
|
var min_distance = INF
|
||||||
|
|
||||||
|
var length = curve.get_baked_length()
|
||||||
|
var steps = 100 # Più alto = più preciso ma più lento
|
||||||
|
|
||||||
|
for i in range(steps + 1):
|
||||||
|
var offset = (float(i) / steps) * length
|
||||||
|
# Trasforma il punto locale della curva in coordinate globali
|
||||||
|
var path_point = path.global_transform * curve.sample_baked(offset)
|
||||||
|
var dist = point.distance_to(path_point)
|
||||||
|
|
||||||
|
if dist < min_distance:
|
||||||
|
min_distance = dist
|
||||||
|
closest_offset = offset
|
||||||
|
|
||||||
|
return closest_offset
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ extends Node3D
|
|||||||
|
|
||||||
@onready var speed_label : Label = $HUD/speed_label
|
@onready var speed_label : Label = $HUD/speed_label
|
||||||
@onready var camera_label : Label = $HUD/camera_label
|
@onready var camera_label : Label = $HUD/camera_label
|
||||||
@onready var train: MeshInstance3D = $Ground/Rails/TrainEngine/Train
|
@onready var train: MeshInstance3D = $Ground/Train
|
||||||
@onready var camera: TrainControlCamera = $TrainCamera
|
@onready var camera: TrainControlCamera = $TrainCamera
|
||||||
|
@onready var rail_switch: Area3D = $Ground/RailSwtich
|
||||||
|
@onready var switch_label : Label = $HUD/switch_label
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
@@ -13,8 +15,10 @@ func _process(_delta: float) -> void:
|
|||||||
if Input.is_action_just_pressed("ui_cancel"):
|
if Input.is_action_just_pressed("ui_cancel"):
|
||||||
SceneSwitcher.switch_scene(main_scene.resource_path)
|
SceneSwitcher.switch_scene(main_scene.resource_path)
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("change_train"):
|
||||||
|
_toggle_rail()
|
||||||
|
|
||||||
|
|
||||||
func _update_ui() -> void:
|
func _update_ui() -> void:
|
||||||
if speed_label and train:
|
if speed_label and train:
|
||||||
var kmh = train.get_speed() * 3.6
|
var kmh = train.get_speed() * 3.6
|
||||||
@@ -22,3 +26,22 @@ func _update_ui() -> void:
|
|||||||
|
|
||||||
if camera_label and camera:
|
if camera_label and camera:
|
||||||
camera_label.text = "Camera: %s" % camera.get_mode_name()
|
camera_label.text = "Camera: %s" % camera.get_mode_name()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_rail() -> void:
|
||||||
|
if train and rail_switch:
|
||||||
|
rail_switch.toggle_switch()
|
||||||
|
train.switch_to_rail(rail_switch.switch_to_rail)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_switch_status():
|
||||||
|
if not switch_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
#if rail_switch:
|
||||||
|
#var state_name = rail_switch.get_state_name()
|
||||||
|
#var color = Color.YELLOW if rail_switch.current_state == rail_switch.SwitchState.LEFT else Color.CYAN
|
||||||
|
#switch_label.text = "Scambio: %s" % state_name
|
||||||
|
#switch_label.add_theme_color_override("font_color", color)
|
||||||
|
#else:
|
||||||
|
#switch_label.text = "Scambio: N/A"
|
||||||
|
|||||||
Reference in New Issue
Block a user