Last active
February 16, 2025 23:06
-
-
Save Decapitated/5d949e36bc375540aacf2f605477ca0a to your computer and use it in GitHub Desktop.
Bendy Tube
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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