Skip to content

Instantly share code, notes, and snippets.

@Decapitated
Last active February 16, 2025 23:06
Show Gist options
  • Select an option

  • Save Decapitated/5d949e36bc375540aacf2f605477ca0a to your computer and use it in GitHub Desktop.

Select an option

Save Decapitated/5d949e36bc375540aacf2f605477ca0a to your computer and use it in GitHub Desktop.
Bendy Tube
@tool
extends Node3D
@export var tube_material: Material
@export var target: Node3D
@export_range(0.0, 10.0, 0.1, "or_greater") var radius: float = 0.5
@export_range(3, 10, 1, "or_greater") var segments: int = 8
@export_range(2, 10, 1, "or_greater") var radials: int = 10
@export_range(0.0, 10.0, 0.1, "or_greater") var control_length: float = 10.0
@export var curve: CurveTexture
@export_range(0.0, 10.0, 0.1, "or_greater") var curve_scale: float = 1.0
@export_tool_button("Generate") var tb_generate: Callable = _generate
var mesh_instance: MeshInstance3D = MeshInstance3D.new()
var prev_target_position: Vector3 = Vector3.INF
var prev_position: Vector3 = Vector3.INF
var prev_basis: Basis = Basis.IDENTITY
func _ready() -> void:
mesh_instance.material_override = tube_material
add_child(mesh_instance)
func _process(_delta: float) -> void:
if target:
# If Target or this node moves/rotates, update.
if target.global_position != prev_target_position || global_position != prev_position || prev_basis != global_basis:
prev_target_position = target.global_position
prev_position = global_position
prev_basis = global_basis
_generate()
func get_bezier(t: float) -> Vector3:
var length := (target.global_position - global_position).length()
return quad_bezier(global_position, global_position + global_basis.z * min(length / 2.0, control_length), target.global_position, t)
static func quad_bezier(a: Vector3, b: Vector3, c: Vector3, t: float) -> Vector3:
return pow(1.0-t, 2.0)*a + 2.0*t*(1.0 - t)*b + pow(t, 2.0)*c
func generate_cylinder_mesh(path_points: Array[Vector3]) -> ArrayMesh:
if path_points.size() < 2:
push_error("Path must have at least 2 points")
return null
var mesh = ArrayMesh.new()
var vertices: PackedVector3Array = PackedVector3Array()
var normals: PackedVector3Array = PackedVector3Array()
var indices: PackedInt32Array = PackedInt32Array()
var uvs: PackedVector2Array = PackedVector2Array()
var previous_up: Vector3
var previous_right: Vector3
for i in path_points.size():
var current_point := path_points[i]
var path_lerp := i / (path_points.size() - 1.0)
var up := Vector3.UP
var right: Vector3
if i < path_points.size() - 1:
var next_point := path_points[i + 1]
# Calculate the direction vector and its perpendicular vectors
var direction := (next_point - current_point).normalized()
if direction.is_equal_approx(up) or direction.is_equal_approx(-up):
up = Vector3.RIGHT
right = direction.cross(up).normalized()
up = right.cross(direction).normalized()
previous_up = up
previous_right = right
else:
up = previous_up
right = previous_right
# Generate circle vertices around current point
for j in segments:
var angle := j * (2.0 * PI / (segments - 1))
var x := cos(angle)
var y := sin(angle)
# Calculate vertex position
var calculated_radius := radius if !curve || !curve.curve else radius + curve.curve.sample_baked(path_lerp) * curve_scale
var offset := right * (x * calculated_radius) + up * (y * calculated_radius)
var vertex := current_point + offset
vertices.push_back(vertex)
# Calculate normal
var normal := offset.normalized()
normals.push_back(normal)
# Calculate UVs
var u := j / float(segments - 1) # U coordinate based on the angle around the cylinder
var v := path_lerp # V coordinate based on the position along the path
uvs.push_back(Vector2(u, v))
# Generate triangles (except for the last segment)
if i < path_points.size() - 1:
var current_ring_start := i * segments
var next_ring_start := (i + 1) * segments
var current_vertex := j
var next_vertex := (j + 1) % segments
# First triangle
indices.push_back(current_ring_start + current_vertex)
indices.push_back(current_ring_start + next_vertex)
indices.push_back(next_ring_start + current_vertex)
# Second triangle
indices.push_back(current_ring_start + next_vertex)
indices.push_back(next_ring_start + next_vertex)
indices.push_back(next_ring_start + current_vertex)
# Create the mesh
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_INDEX] = indices
arrays[Mesh.ARRAY_TEX_UV] = uvs
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
func get_point_path() -> PackedVector3Array:
var path := PackedVector3Array()
for i in radials:
var bezier_lerp := i / (radials - 1.0)
path.append(to_local(get_bezier(bezier_lerp)))
return path
func _generate():
if target:
var cylinder_mesh := generate_cylinder_mesh(get_point_path())
mesh_instance.mesh = cylinder_mesh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment