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.



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_nodemust be assigned aPath2D.path_node.curvemust be set as aCurve2D.- 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
- Place a
Path2Dand edit itsCurve2Dto create the desired shape. - Attach this script to a
Node2D. - Assign the
Path2Dfrom step 1 topath_node. - Set the
width. - If you want to add appearance, set
ribbon_textureandblend_material. - 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
Setstart_percent = 0,progress_percent = 100,slide_offset_percent = 0, andwidth_curveto constant 1.0 for a ribbon of constant width. -
Expand display range
Fixstart_percentand varyprogress_percentfrom 0 to 100 to create a growing segment behavior.progress_percent < start_percentis not allowed and aligns withstart_percent. -
Maintain fixed display length while moving position
The difference betweenstart_percentandprogress_percentis the display length.slide_offset_percentmoves the start position while maintaining the difference. The movable range is controlled to stay within0..(100 - display length). -
Make width variation follow display range or fix it
thickness_domain_mode = VisibleRangeNormalizedsamples by normalizing the display range to 0..1, so changing the display range changes the phase of width variation.FullPathAbsolutesamples the entire path as 0..1, so changing the display range does not change the width at the same distance position.