How to Use PathRibbonMesh2D (Adding Tentacle Texture)

Overview

PathRibbonMesh2D generates a ribbon-like 2D mesh along a sequence of points baked from a specified Path2D.curve. Using this script, you can create effects such as vines or tentacles growing along a path, or attack trajectory effects. By changing the z_index at intermediate points, you can adjust the rendering order to achieve expressions like piercing into or wrapping around objects.

Videotogif
Videotogif (1)
tentacle_thighten

path_ribbon_mesh_2d.gd (54.9 KB)

tentacle_texture.zip (570.3 KB)

tentacle_project.zip (2.2 MB)

Features

Display is handled by an automatically generated MeshInstance2D as a child of this node. It supports specifying the display range, width variation curves, UV scaling and rotation, miter processing at sharp angles, and stepwise z_index changes based on position.

This script manages the generated MeshInstance2D as a script-controlled object, regenerating or destroying it in response to configuration changes or segmentation conditions.

Prerequisites and Requirements

Assumes Godot 4’s 2D node structure.

The following conditions are required for operation:

  • path_node must be assigned a Path2D.
  • path_node.curve must be set as a Curve2D.
  • The baked point sequence must contain at least two points.

The visual appearance depends on ribbon_texture and blend_material. Even if no texture is set, the mesh itself will still be generated, but the display result will depend on the material implementation.

Setup

  1. Place a Path2D and edit its Curve2D to create the desired shape.
  2. Attach this script to a Node2D.
  3. Assign the Path2D from step 1 to path_node.
  4. Set the width.
  5. If you want to add appearance, set ribbon_texture and blend_material.
  6. Optionally, set the display range, width curve, UV settings, sharp angle processing, and Z keyframes.

Generated Node Specifications

An automatically generated MeshInstance2D is created as a child of this node. The generated node carries metadata _path_ribbon_mesh2d_generated and is managed by the script.

When use_center_split = true, two meshes are generated: RibbonUpper_### and RibbonLower_###. They share the centerline, with the upper mesh extending upward and the lower mesh extending downward from the centerline to form triangles.

When use_center_split = false, a single mesh is generated: RibbonSingle_###. The ribbon surface is constructed by connecting the top and bottom edges.

At locations where z_index changes due to z_keyframes_percent_and_z, segments are split, potentially generating multiple MeshInstance2D nodes. Shared boundary points suppress visible seams.

Usage Examples

Purpose (Use Case) Settings Result Notes
Display the entire path start_percent=0, progress_percent=100, slide_offset_percent=0 Ribbon displayed along the entire path If width_curve is constant at 1.0, the width remains constant
Create a growing ribbon effect Fix start_percent and vary progress_percent from 0 to 100 The end of the segment grows If progress_percent < start_percent, it aligns with start_percent, and no reverse segments are created
Maintain fixed display length while moving position Set display length via progress_percent-start_percent and vary slide_offset_percent The segment slides while maintaining fixed length The start position is controlled to stay within 0..(100-display length)
Fix width variation to path position Set thickness_domain_mode=FullPathAbsolute and configure width_curve Width remains the same at the same distance position regardless of display range Width phase remains fixed during partial display or sliding
Make width variation follow display range Set thickness_domain_mode=VisibleRangeNormalized and configure width_curve Width is determined by normalizing the display range to 0..1 Width phase moves together with the display range
Vary thickness ratio between top and bottom Set use_center_split=true and use different shapes for upper_width_curve and lower_width_curve Different expansion amounts for upper and lower sides relative to the centerline Can create shapes where the upper side is thin and the lower side is thick
Repeat texture in the direction of travel Set ribbon_texture and increase uv_scale.x Number of U-direction tiles increases Appearance depends on material and texture settings
Change texture angle Set texture_rotation_degrees and optionally use uv_rotation_mode=TileLocalWrapRotation UV rotates, changing the pattern angle Tile rotation may be advantageous when used with repeated display
Switch front/back relationship by position Set multiple Vector2(percent,z) in z_keyframes_percent_and_z z_index changes stepwise based on position along the path Segments are split at Z change points, stabilizing rendering order
Adjust smoothness and load Use smaller bake_interval for smoothness priority, larger for load priority Balances curve tracking and update cost Reduces vertex generation load when many duplicate points exist via bake_point_dedup_enabled

Property List

Property Type / Default Description (Behavior / Calculation / Notes)
path_node Path2D / (not set) Source for ribbon generation. If path_node == null, all generated meshes are hidden. If path_node.curve == null, an error is raised (push_error) and the meshes are hidden. Monitors curve reference changes (e.g., replacing Curve2D with a different instance) and discards bake cache to schedule mesh updates.
width float / 40.0 Base width (pixels). The “width factor” (e.g., width_curve) calculated at each point is multiplied by this value, halved, and offset along the normal direction to create vertices. When using center split, separate offsets are created for “center → upper” and “center → lower”. Negative values are not clamped, so operations should assume values ≥ 0.
uv_scale Vector2 / (1,1) Scale applied to UV (texture coordinates). The application method depends on uv_scale_apply_mode. U corresponds to the direction of travel along the line (0..1 within the display range), and V corresponds to the vertical direction of the ribbon (0..1). When using center split, the center point’s V is set to 0.5 to create the “centerline”.
bake_interval float / 1.0 Sets Curve2D.bake_interval and retrieves points via curve.get_baked_points(). Changes in value or curve replacement regenerate the bake cache (point sequence and cumulative distance). Smaller values increase point count, improving curve tracking smoothness, but increase update cost (point processing + mesh generation).
start_percent int / 0 Start of display range (0–100). Clamped to 0..100 within _process. In actual display calculations, adjustments are made to ensure “start > end” does not occur (see progress_percent below).
progress_percent int / 100 End of display range (0–100). Clamped to 0..100, and if progress_percent < start_percent, it aligns with start_percent. Thus, “reverse direction” ranges are not created, resulting in a “length 0” range at minimum.
slide_offset_percent int / 0 Moves the start position while maintaining the difference between start_percent and progress_percent (display length). Internally, first calculates span_p = progress-start (0..100), then adjusts s_p = start + slide_offset to stay within 0..(100-span_p). The end is determined by e_p = s_p + span_p. This ensures “fixed display length, only position moves”.
sharp_bend_mode bool / false Switches the handling method for sharp bends. When false, the center direction is created by combining forward and backward vectors, and the ribbon is expanded using the normal of this direction (smooth corner extension). When true, the front and back normals are combined to create a miter direction, and join_scale (extension multiplier) is calculated. The ribbon extends significantly at sharp angles, so miter_limit acts as a practical safety valve.
ribbon_texture Texture2D / (not set) Set to MeshInstance2D.texture of each generated instance. Additionally, if shader_texture_param_name is not empty and the material is ShaderMaterial, the same texture is set as a shader parameter (see below). Even without a texture, the mesh itself is generated, but visibility depends on material settings.
blend_material Material / null Applies to MeshInstance2D.material of each generated instance. If null, no material is set. In the editor, it monitors the changed signal (when the referenced material is edited) and schedules reapplication if changed.
material_unique_per_instance bool / true If true, creates a local material by executing blend_material.duplicate(false) and sets it to the instance. If false, shares the blend_material reference directly. Shared materials are affected by changes in other objects. Duplicating isolates the impact but increases material count.
shader_texture_param_name StringName / "" Used only when the locally applied material is ShaderMaterial. Only if not empty, set_shader_parameter(shader_texture_param_name, ribbon_texture) is executed. If the shader does not have the corresponding uniform, the setting is meaningless (or causes a warning), so the shader implementation and name must match.
texture_rotation_degrees float / 0.0 Rotates the UV. Skips rotation processing if 0. The rotation center is (0.5, 0.5), and the application method depends on uv_rotation_mode. The appearance changes with tiled UVs (U ≥ 1), so choose uv_rotation_mode based on use case.
uv_rotation_mode enum / LegacyWholeUVRotation UV rotation method. LegacyWholeUVRotation rotates the entire UV (may break appearance when crossing tile boundaries). TileLocalWrapRotation extracts the fractional part (0..1) using fposmod for rotation and restores the integer part (tile number). This results in rotation within each tile, improving compatibility with repeated display.
uv_scale_apply_mode enum / LegacyMultiplyVectorByUVScale UV scaling method. LegacyMultiplyVectorByUVScale multiplies as Vector2(u, v) * uv_scale. SeparateXYScale scales U and V separately using uv_scale.x and uv_scale.y. The centerline (V=0.5) in center split is also scaled similarly.
miter_limit float / 2.5 When sharp_bend_mode = true, sets the upper limit for join_scale (offset multiplier) at sharp bends. Computationally, the multiplier is derived from the dot product with the previous normal, but it approaches infinity at sharp angles, so this limit is set. Increasing the value makes the corners sharper and extends more; decreasing it suppresses corner extension.
width_curve Curve / Curve.new() Base curve for width factor. At each point, t_width is calculated, and width_curve.sample(t_width) is used as a coefficient multiplied by width. Sample values less than zero are clamped to 0. If points are empty at _ready, automatically adds (0,1) and (1,1) to initialize as “constant width”.
upper_width_curve Curve / Curve.new() Coefficient curve for the upper side in center split (use_center_split = true). The upper offset distance is calculated as base_half * upper_factor * join_scale. If empty, initializes to constant 1.0 at _ready.
lower_width_curve Curve / Curve.new() Coefficient curve for the lower side in center split. The lower offset distance is calculated as base_half * lower_factor * join_scale. If empty, initializes to constant 1.0 at _ready.
thickness_domain_mode enum / VisibleRangeNormalized Switches the reference for curve sampling position t_width. When VisibleRangeNormalized, the current display distance range [start_len, end_len] is normalized to 0..1 to calculate t_width (changing the display range changes t_width at the same location). When FullPathAbsolute, the total path length is used as 0..1 to calculate t_width = dist / total_len (changing the display range does not change t_width at the same distance, fixing width variation).
use_center_split bool / true Mesh generation method. When true, shares the centerline and generates two meshes (Upper/Lower). The upper mesh forms triangles from “center → upper”, and the lower mesh from “center → lower”. When false, generates one mesh (Single) connecting “upper → lower” to form a ribbon surface. Switching reconnects the watcher (curve change monitoring) and recreates the generated node array.
bake_point_dedup_enabled bool / true Enables deduplication of baked points. Removes consecutive points from Curve2D.get_baked_points() if their distance is below bake_point_dedup_epsilon. Aims to reduce unnecessary processing (vertex and index generation) on curves with many tiny duplicate points.
bake_point_dedup_epsilon float / 0.0005 Duplication detection distance. Implementation uses squared distance comparison, accepting only if (p - last).length_squared() > eps^2. If 0 or below, it is effectively disabled. Setting too high roughens the curve shape.
profiling_enabled bool / false Enables profiling measurement. Internally uses Time.get_ticks_usec() to accumulate total time and count per label.
profiling_print_each_event bool / true When profiling_enabled = true, prints measurement results for each event. Set to false if event-level output is unnecessary.
profiling_print_threshold_usec int / 0 Event output threshold (usec). ≤ 0 outputs all; ≥ 1 outputs only events exceeding the threshold. Useful for extracting heavy sections.
profiling_dump_after_first_flush bool / true Outputs summary (total, count, average) after the first _flush_updates completes. Use when only the first output is needed.
debug_print_range_stats bool / true Prints display range (%), distance range (start/end/total), U range, t_width range, and thickness_domain_mode each time the mesh updates. For behavior verification.
z_keyframes_percent_and_z Array[Vector2] / [(0,0),(100,0)] Z keyframes. Elements are treated as Vector2(percent, z). Processing is as follows: ① Clamp percent to 0..100 and create array ② Sort by percent ascending (z ascending for equal values) ③ For any location percent, adopt the last key below or equal to percent (step) ④ Round z to int and set to MeshInstance2D.z_index. At Z change points, segments are split, and shared boundary points suppress seams.

Typical Usage Patterns

  • Fixed ribbon displaying entire path
    Set start_percent = 0, progress_percent = 100, slide_offset_percent = 0, and width_curve to constant 1.0 for a ribbon of constant width.

  • Expand display range
    Fix start_percent and vary progress_percent from 0 to 100 to create a growing segment behavior. progress_percent < start_percent is not allowed and aligns with start_percent.

  • Maintain fixed display length while moving position
    The difference between start_percent and progress_percent is the display length. slide_offset_percent moves the start position while maintaining the difference. The movable range is controlled to stay within 0..(100 - display length).

  • Make width variation follow display range or fix it
    thickness_domain_mode = VisibleRangeNormalized samples by normalizing the display range to 0..1, so changing the display range changes the phase of width variation. FullPathAbsolute samples the entire path as 0..1, so changing the display range does not change the width at the same distance position.

1 Like