Got it working on Terrain3D..Path3d height automatic on terrain3d

Godot Version

4.4

Question

Hi all,

I have a script to put objects at a Path3d line. See here:

@tool
extends Path3D

@export var fence_scene1 = preload("res://fence.tscn")
@export var fence_scene2 = preload("res://fence2.tscn")
@export var spacing = 0.3

func _ready():
	# Zorg ervoor dat de node "VisibleItems" bestaat
	if !$VisibleItems3:
		var visible_items = Node3D.new()
		visible_items.name = "VisibleItems3"
		add_child(visible_items)
	spawn_fences()

func spawn_fences():
	# Verwijder bestaande hekken van "VisibleItems"
	for child in $VisibleItems3.get_children():
		child.queue_free()

	var current_distance = 0.0

	while current_distance < curve.get_baked_length():
		var pos = curve.sample_baked(current_distance)
		var up_vector = curve.sample_baked_up_vector(current_distance)

		# Maak een nieuw hek, willekeurig gekozen tussen fence_scene1 en fence_scene2
		var fence_instance
		if randf() < 0.5:
			fence_instance = fence_scene1.instantiate()
		else:
			fence_instance = fence_scene2.instantiate()
			
		fence_instance.global_transform.origin = pos
		fence_instance.look_at(pos + curve.sample_baked_up_vector(current_distance), up_vector)
		
		# Willekeurige horizontale rotatie
		var random_rotation = randf_range(0.0, 2.0 * PI)
		fence_instance.rotate_y(random_rotation)
		
		$VisibleItems3.add_child(fence_instance)

		# Verhoog de afstand met 0.2 meter
		current_distance += spacing

func randf_range(min_val: float, max_val: float) -> float:
	return min_val + (max_val - min_val) * randf()

I added RAYCAST to the script so the objects have to follow the terrain under it when the height is different but then the objects on the Path3d are gone, see the script:

@tool
extends Path3D

@export var fence_scene1 = preload("res://fence.tscn")
@export var fence_scene2 = preload("res://fence2.tscn")
@export var spacing = 0.3
@export var raycast_length = 1000  # Lengte van de raycast
@export var raycast_collision_mask = 1  # Laag waarop de raycast checkt

func _ready():
    # Zorg ervoor dat de node "VisibleItems" bestaat
    if !$VisibleItems3:
        var visible_items = Node3D.new()
        visible_items.name = "VisibleItems3"
        add_child(visible_items)
    spawn_fences()

func spawn_fences():
    # Verwijder bestaande hekken van "VisibleItems"
    for child in $VisibleItems3.get_children():
        child.queue_free()

    var current_distance = 0.0

    while current_distance < curve.get_baked_length():
        var pos = curve.sample_baked(current_distance)
        var up_vector = curve.sample_baked_up_vector(current_distance)

        # Pas raycast toe om positie aan te passen op basis van onderliggende objecten
        var hit_position = perform_raycast(pos)
        if hit_position != null:
            pos = hit_position

        # Maak een nieuw hek, willekeurig gekozen tussen fence_scene1 en fence_scene2
        var fence_instance
        if randf() < 0.5:
            fence_instance = fence_scene1.instantiate()
        else:
            fence_instance = fence_scene2.instantiate()
            
        fence_instance.global_transform.origin = pos
        fence_instance.look_at(pos + curve.sample_baked_up_vector(current_distance), up_vector)
        
        # Willekeurige horizontale rotatie
        var random_rotation = randf_range(0.0, 2.0 * PI)
        fence_instance.rotate_y(random_rotation)
        
        $VisibleItems3.add_child(fence_instance)

        # Verhoog de afstand met de opgegeven spacing
        current_distance += spacing

func perform_raycast(from_position: Vector3) -> Vector3:
    var space_state = get_world_3d().direct_space_state
    var raycast_params = {
        "from": from_position,
        "to": from_position + Vector3.DOWN * raycast_length,
        "collision_mask": raycast_collision_mask
    }
    var result = space_state.intersect_ray(raycast_params)
    if result:
        return result.position  # Positie van de hit teruggeven
    else:
        return from_position  # Retourneer de oorspronkelijke positie als er geen hit is

func randf_range(min_val: float, max_val: float) -> float:
    return min_val + (max_val - min_val) * randf()

Is there somethging Wrong in the script with the Raycast option? The terrain is made with TERRAIN3d

I got it working, for anyone who want to use this for a road or other path3d things, here is the script:

@tool
extends Path3D

@export var fence_scene1 = preload("res://fence.tscn")
@export var fence_scene2 = preload("res://fence2.tscn")
@export var terrain: Node3D  # Koppel je Terrain3D-node
@export var spacing = 0.3
@export var height_offset = -0.1  # Offset om zweven te corrigeren

var is_updating = false  # Voorkomt oneindige loops

func _ready():
	if !$VisibleItems3:
		var visible_items = Node3D.new()
		visible_items.name = "VisibleItems3"
		add_child(visible_items)
	
	curve.connect("changed", Callable(self, "_on_curve_changed"))
	
	adjust_curve_with_terrain_data()
	spawn_fences()

func _on_curve_changed():
	if is_updating:
		return  
	is_updating = true

	adjust_curve_with_terrain_data()
	spawn_fences()

	is_updating = false

func adjust_curve_with_terrain_data():
	if terrain == null or terrain.data == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return
	
	if curve.point_count == 0:
		push_error("❌ Path3D heeft geen punten! Voeg punten toe.")
		return

	print("🔎 Path3D heeft", curve.point_count, "punten. Start hoogte-aanpassing...")

	var adjusted_points = []

	for i in range(curve.point_count):
		var point = curve.get_point_position(i)
		var terrain_height = get_terrain_height(point)

		if !is_nan(terrain_height) and !is_inf(terrain_height):
			terrain_height += height_offset  # Pas offset toe
			adjusted_points.append(Vector3(point.x, terrain_height, point.z))
			print("✅ Nieuw punt toegevoegd:", Vector3(point.x, terrain_height, point.z))
		else:
			push_error("⚠️ Ongeldige hoogte ontvangen voor punt: " + str(point) + ", val terug op oorspronkelijke hoogte.")
			adjusted_points.append(point)

	# Verwijder oude punten PAS na het verzamelen van de nieuwe!
	curve.clear_points()
	for new_point in adjusted_points:
		curve.add_point(new_point)
		print("📌 Toegevoegd aan curve:", new_point)

func get_terrain_height(global_position: Vector3) -> float:
	print("🌍 Controleren Terrain3DRegion voor positie:", global_position)

	if terrain == null or terrain.data == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return global_position.y  # Houd oorspronkelijke hoogte aan bij fout

	# Zet lokale positie om naar globale positie
	global_position = to_global(global_position)
	print("🔎 Global position na conversie:", global_position)

	var height: float = terrain.data.get_height(global_position) * terrain.scale.y
	print("✅ Ontvangen hoogte:", height)

	if is_nan(height) or is_inf(height):
		push_error("⚠️ Ongeldige hoogte ontvangen voor " + str(global_position) + ", val terug op 0.")
		return 0.0

	return height  

func spawn_fences():
	if terrain == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return

	for child in $VisibleItems3.get_children():
		child.queue_free()

	var current_distance = 0.0

	while current_distance < curve.get_baked_length():
		var pos = curve.sample_baked(current_distance)
		var up_vector = curve.sample_baked_up_vector(current_distance)

		# Pas de hoogte van het terrein toe
		pos = to_global(pos)  # Zorg dat positie correct is
		print("🔎 Fence global position:", pos)

		var terrain_height = get_terrain_height(pos)
		if !is_nan(terrain_height) and !is_inf(terrain_height):
			pos.y = terrain_height + height_offset  # Offset correctie

		var fence_instance = (fence_scene1 if randf() < 0.5 else fence_scene2).instantiate()
		fence_instance.global_transform.origin = pos
		fence_instance.look_at(pos + up_vector, up_vector)
		fence_instance.rotate_y(randf_range(0.0, 2.0 * PI))

		$VisibleItems3.add_child(fence_instance)
		current_distance += spacing

func randf_range(min_val: float, max_val: float) -> float:
	return min_val + (max_val - min_val) * randf()

You have to set the terrain3d and the height offset is for if the path3d is not exactly on the terrain. In my case it followed heights but was to high above the terrain, when adjusting this it works fine. After change this setting reload your saved scene and when adding points it automatically changes to the good height.

A new version because it was not fully working yet, this one is stable:

@tool
extends Path3D

@export var fence_scene1 = preload("res://fence.tscn")
@export var fence_scene2 = preload("res://fence2.tscn")
@export var terrain: Node3D
@export var spacing = 0.3:
	set(value):
		spacing = value
		if Engine.is_editor_hint() and is_inside_tree():
			spawn_fences()  # 🔁 Herbouw hekjes bij wijziging in editor

@export var height_offset = 0.0:  # Zet standaard hoogte offset op 0 (geen zweven)
	set(value):
		height_offset = value
		if Engine.is_editor_hint() and is_inside_tree():
			spawn_fences()  # 🔁 Herbouw hekjes bij wijziging in editor

@export var enable_random_rotation := true:
	set(value):
		enable_random_rotation = value
		if Engine.is_editor_hint() and is_inside_tree():
			spawn_fences()  # 🔁 Herbouw hekjes bij wijziging in editor

var is_updating = false

func _ready():
	if !$VisibleItems3:
		var visible_items = Node3D.new()
		visible_items.name = "VisibleItems3"
		add_child(visible_items)

	curve.connect("changed", Callable(self, "_on_curve_changed"))

	adjust_curve_with_terrain_data()
	spawn_fences()

func _on_curve_changed():
	if is_updating:
		return
	is_updating = true

	adjust_curve_with_terrain_data()
	spawn_fences()

	is_updating = false

func adjust_curve_with_terrain_data():
	if terrain == null or terrain.data == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return

	if curve.point_count == 0:
		push_error("❌ Path3D heeft geen punten! Voeg punten toe.")
		return

	print("🔎 Path3D heeft", curve.point_count, "punten. Start hoogte-aanpassing...")

	for i in range(curve.point_count):
		var point = curve.get_point_position(i)
		var terrain_height = get_terrain_height(point)

		if !is_nan(terrain_height) and !is_inf(terrain_height):
			var new_point = Vector3(point.x, terrain_height + height_offset, point.z)
			curve.set_point_position(i, new_point)
			print("✅ Punt aangepast:", new_point)
		else:
			push_error("⚠️ Ongeldige hoogte voor punt " + str(point) + ", behoud originele hoogte.")

func get_terrain_height(global_position: Vector3) -> float:
	print("🌍 Controleren Terrain3DRegion voor positie:", global_position)

	if terrain == null or terrain.data == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return global_position.y  # Als er geen terrein is, behoud de oorspronkelijke hoogte

	global_position = to_global(global_position)
	print("🔎 Global position na conversie:", global_position)

	var height: float = terrain.data.get_height(global_position) * terrain.scale.y
	print("✅ Ontvangen hoogte:", height)

	if is_nan(height) or is_inf(height):
		push_error("⚠️ Ongeldige hoogte ontvangen voor " + str(global_position) + ", val terug op 0.")
		return 0.0

	return height

func spawn_fences():
	if terrain == null:
		push_error("❌ Geen geldig Terrain3D gekoppeld!")
		return

	for child in $VisibleItems3.get_children():
		child.queue_free()

	var current_distance = 0.0

	while current_distance < curve.get_baked_length():
		var pos = curve.sample_baked(current_distance)
		var next_pos = curve.sample_baked(current_distance + 0.1)

		pos = to_global(pos)
		next_pos = to_global(next_pos)

		var terrain_height = get_terrain_height(pos)
		if !is_nan(terrain_height) and !is_inf(terrain_height):
			pos.y = terrain_height + height_offset  # Pas de hoogte van het hek aan op de terrain hoogte

		var direction = (next_pos - pos).normalized()
		direction.y = 0

		var fence_instance = (fence_scene1 if randf() < 0.5 else fence_scene2).instantiate()

		if direction.length() > 0:
			var basis = Basis().looking_at(direction, Vector3.UP)
			var transform = Transform3D(basis, pos)
			fence_instance.transform = transform
		else:
			fence_instance.position = pos

		$VisibleItems3.add_child(fence_instance)

		if enable_random_rotation:
			fence_instance.rotate_y(randf_range(0.0, 2.0 * PI))

		current_distance += spacing

func randf_range(min_val: float, max_val: float) -> float:
	return min_val + (max_val - min_val) * randf()