Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement segmenting of Line2D for better fidelity when using width curves #95541

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/classes/Line2D.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
[b]Note:[/b] The shape of the closing segment is not guaranteed to be seamless if a [member width_curve] is provided.
[b]Note:[/b] The joint between the closing segment and the first segment is drawn first and it samples the [member gradient] and the [member width_curve] at the beginning. This is an implementation detail that might change in a future version.
</member>
<member name="curve_offset" type="float" setter="set_curve_offset" getter="get_curve_offset" default="0.0">
The horizontal offset value used when sampling from the width curve. Typically ranges from -1.0 to 1.0.
chryan marked this conversation as resolved.
Show resolved Hide resolved
</member>
<member name="default_color" type="Color" setter="set_default_color" getter="get_default_color" default="Color(1, 1, 1, 1)">
The color of the polyline. Will not be used if a gradient is set.
</member>
Expand All @@ -82,6 +85,10 @@
<member name="joint_mode" type="int" setter="set_joint_mode" getter="get_joint_mode" enum="Line2D.LineJointMode" default="0">
The style of the connections between segments of the polyline. Use [enum LineJointMode] constants.
</member>
<member name="min_curve_line_segments" type="int" setter="set_min_curve_line_segments" getter="get_min_curve_line_segments" default="1">
Subdivide the line into the number of at least [member min_curve_line_segments] segments. The resulting segment count varies depending on the use of a fill gradient and the distances between points.
[b]Note:[/b] Using higher values results in more segments (and vertices/indices) that need to be generated which can degrade performance, particularly if you are animating properties on your line.
chryan marked this conversation as resolved.
Show resolved Hide resolved
</member>
<member name="points" type="PackedVector2Array" setter="set_points" getter="get_points" default="PackedVector2Array()">
The points of the polyline, interpreted in local 2D coordinates. Segments are drawn between the adjacent points in this array.
</member>
Expand Down
129 changes: 127 additions & 2 deletions scene/2d/line_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ Ref<Curve> Line2D::get_curve() const {
return _curve;
}

void Line2D::set_min_curve_line_segments(int min_curve_line_segments) {
_min_curve_line_segments = min_curve_line_segments;
queue_redraw();
}

void Line2D::set_curve_offset(float curve_offset) {
_curve_offset = curve_offset;
queue_redraw();
}

Vector<Vector2> Line2D::get_points() const {
return _points;
}
Expand Down Expand Up @@ -276,9 +286,115 @@ void Line2D::_draw() {
return;
}

// TODO Maybe have it as member rather than copying parameters and allocating memory?
// NOTE:
// Sublines refer to the lines between points before curve segmentation.

LineBuilder lb;
lb.points = _points;
LocalVector<Vector2> &generated_draw_points = lb.points;
LocalVector<Vector2> gradient_inclusive_points;
// We insert gradient points that correspond to a position on our normalized line (e.g. gradient point of 0.5 means we add a point in the at the half-way point of our total line).
if (_gradient.is_valid()) {
int gradient_point_count = _gradient->get_point_count();

int subline_count = len - 1;
LocalVector<float> subline_lengths;
LocalVector<Vector2> sublines;
subline_lengths.reserve(subline_count);
sublines.reserve(len + _gradient->get_point_count() - 1);

// Gather the total line length, and distances and directions for each subline
float total_line_length = 0.0f;
for (int i = 0; i < subline_count; ++i) {
Vector2 subline_dir = _points[i + 1] - _points[i];
float subline_length = subline_dir.length();
total_line_length += subline_length;
subline_lengths.push_back(subline_length);
sublines.push_back(subline_dir);
}

// Reserve the right amount of memory so we're not constantly reallocating as we're pushing items in the back.
gradient_inclusive_points.reserve(static_cast<unsigned int>(_points.size()) + static_cast<unsigned int>(gradient_point_count));

int gradient_idx = 0;

float cumulative_line_length = 0.0f;
for (int line_idx = 0; line_idx < subline_count; ++line_idx) {
// All current calculations are based on the current subline.
int wrapped_line_idx = line_idx % static_cast<int>(_points.size());
const Vector2 &curr_subline_start = _points[wrapped_line_idx];
const Vector2 &curr_subline = sublines[wrapped_line_idx];
const float &curr_subline_length = subline_lengths[wrapped_line_idx];

// This is where our current subline points lay on our normalized line scale.
float curr_subline_start_offset = cumulative_line_length / total_line_length;
float curr_subline_end_offset = (cumulative_line_length + curr_subline_length) / total_line_length;

gradient_inclusive_points.push_back(curr_subline_start);
// We check to see if there's a gradient point that has an offset between our current subline.
// For the curve offset, we only care about the X axis that ranges from 0 to 1, corresponding with our line normalization.
while (gradient_idx < _gradient->get_point_count()) {
float curr_gradient_point_offset = _gradient->get_offset(gradient_idx);
float relative_gradient_point_offset = (curr_gradient_point_offset - curr_subline_start_offset) / (curr_subline_end_offset - curr_subline_start_offset);
// Only add a point if the point offset is between our subline start and end.
if (relative_gradient_point_offset >= 1.0f) {
break;
}
if (relative_gradient_point_offset > 0.0f) {
gradient_inclusive_points.push_back(curr_subline_start + curr_subline * relative_gradient_point_offset);
}
++gradient_idx;
}
cumulative_line_length += curr_subline_length;
}
gradient_inclusive_points.push_back(_points[subline_count]);
} else {
gradient_inclusive_points = _points;
}

int subline_count = static_cast<int>(gradient_inclusive_points.size()) - 1;
int num_segments = _curve.is_valid() ? _min_curve_line_segments : -1;
num_segments = MAX(subline_count, num_segments);
generated_draw_points.reserve(num_segments);
generated_draw_points.clear();

// We have less segments than what's defined, so we just use the points.
if (num_segments <= subline_count) {
generated_draw_points = gradient_inclusive_points;
} else {
len = gradient_inclusive_points.size();

// TODO: Cache the memory somewhere so we don't keep allocating memory
int line_count = _closed ? len : len - 1;
LocalVector<float> subline_lengths;
subline_lengths.reserve(line_count);

// Gather the total line length, and distances and directions for each segment
float total_line_length = 0.0f;
for (int line_idx = 0; line_idx < line_count; ++line_idx) {
Vector2 line_dir = gradient_inclusive_points[(line_idx + 1) % len] - gradient_inclusive_points[line_idx];
float subline_length = line_dir.length();
total_line_length += subline_length;
subline_lengths.push_back(subline_length);
}

float segment_length = total_line_length / MAX(1, static_cast<float>(num_segments));
for (int line_idx = 0; line_idx < line_count; ++line_idx) {
float subline_length = subline_lengths[line_idx];
int subline_segments_count = static_cast<int>(Math::ceil(subline_length / segment_length));
Vector2 subline_start = gradient_inclusive_points[line_idx];
Vector2 subline_end = gradient_inclusive_points[(line_idx + 1) % len];
generated_draw_points.push_back(gradient_inclusive_points[line_idx]);
for (int l = 1; l < subline_segments_count; ++l) {
float current_segment_relative_start = segment_length * static_cast<float>(l);
generated_draw_points.push_back(subline_start + (subline_end - subline_start) * (current_segment_relative_start / subline_length));
}
}

// We don't add the last point so that the line builder can properly cap the ends
if (!_closed)
generated_draw_points.push_back(gradient_inclusive_points[line_count % len]);
}

lb.closed = _closed;
lb.default_color = _default_color;
lb.gradient = *_gradient;
Expand All @@ -290,6 +406,7 @@ void Line2D::_draw() {
lb.sharp_limit = _sharp_limit;
lb.width = _width;
lb.curve = *_curve;
lb.curve_offset = _curve_offset;

RID texture_rid;
if (_texture.is_valid()) {
Expand Down Expand Up @@ -358,6 +475,12 @@ void Line2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_curve", "curve"), &Line2D::set_curve);
ClassDB::bind_method(D_METHOD("get_curve"), &Line2D::get_curve);

ClassDB::bind_method(D_METHOD("set_min_curve_line_segments", "curve_offset"), &Line2D::set_min_curve_line_segments);
ClassDB::bind_method(D_METHOD("get_min_curve_line_segments"), &Line2D::get_min_curve_line_segments);

ClassDB::bind_method(D_METHOD("set_curve_offset", "curve_offset"), &Line2D::set_curve_offset);
ClassDB::bind_method(D_METHOD("get_curve_offset"), &Line2D::get_curve_offset);

ClassDB::bind_method(D_METHOD("set_default_color", "color"), &Line2D::set_default_color);
ClassDB::bind_method(D_METHOD("get_default_color"), &Line2D::get_default_color);

Expand Down Expand Up @@ -392,6 +515,8 @@ void Line2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "closed"), "set_closed", "is_closed");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "width", PROPERTY_HINT_NONE, "suffix:px"), "set_width", "get_width");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "width_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_curve", "get_curve");
ADD_PROPERTY(PropertyInfo(Variant::INT, "min_curve_line_segments", PROPERTY_HINT_RANGE, "1,64,1,or_greater"), "set_min_curve_line_segments", "get_min_curve_line_segments");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "curve_offset", PROPERTY_HINT_RANGE, "-1.0,1.0,0.01"), "set_curve_offset", "get_curve_offset");
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "default_color"), "set_default_color", "get_default_color");
ADD_GROUP("Fill", "");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "gradient", PROPERTY_HINT_RESOURCE_TYPE, "Gradient"), "set_gradient", "get_gradient");
Expand Down
8 changes: 8 additions & 0 deletions scene/2d/line_2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class Line2D : public Node2D {
void set_curve(const Ref<Curve> &curve);
Ref<Curve> get_curve() const;

void set_min_curve_line_segments(int min_curve_line_segments);
int get_min_curve_line_segments() const { return _min_curve_line_segments; }

void set_curve_offset(float curve_offset);
float get_curve_offset() const { return _curve_offset; }

void set_default_color(Color color);
Color get_default_color() const;

Expand Down Expand Up @@ -133,6 +139,8 @@ class Line2D : public Node2D {
bool _closed = false;
float _width = 10.0;
Ref<Curve> _curve;
int _min_curve_line_segments = 1;
float _curve_offset = 0.0f;
Color _default_color = Color(1, 1, 1);
Ref<Gradient> _gradient;
Ref<Texture2D> _texture;
Expand Down
11 changes: 6 additions & 5 deletions scene/2d/line_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void LineBuilder::build() {
const float hw = width / 2.f;
const float hw_sq = hw * hw;
const float sharp_limit_sq = sharp_limit * sharp_limit;
const int point_count = points.size();
const int point_count = static_cast<int>(points.size());
const bool wrap_around = closed && point_count > 2;

_interpolate_color = gradient != nullptr;
Expand Down Expand Up @@ -113,7 +113,7 @@ void LineBuilder::build() {
}

if (_interpolate_color) {
color0 = gradient->get_color(0);
color0 = gradient->get_color_at_offset(0.0f);
} else {
colors.push_back(default_color);
}
Expand Down Expand Up @@ -189,7 +189,8 @@ void LineBuilder::build() {
color1 = gradient->get_color_at_offset(current_distance1 / total_distance);
}
if (retrieve_curve) {
width_factor = curve->sample_baked(current_distance1 / total_distance);
float offset = CLAMP((current_distance1 / total_distance) - curve_offset, 0.0f, 1.0f);
width_factor = curve->sample_baked(offset);
modified_hw = hw * width_factor;
}

Expand Down Expand Up @@ -385,7 +386,7 @@ void LineBuilder::build() {
current_distance1 += pos0.distance_to(pos1);
}
if (_interpolate_color) {
color1 = gradient->get_color(gradient->get_point_count() - 1);
color1 = gradient->get_color_at_offset(1.0f);
}
if (retrieve_curve) {
width_factor = curve->sample_baked(1.f);
Expand Down Expand Up @@ -414,7 +415,7 @@ void LineBuilder::build() {
// Custom drawing for a round end cap.
if (end_cap_mode == Line2D::LINE_CAP_ROUND) {
// Note: color is not used in case we don't interpolate.
Color color = _interpolate_color ? gradient->get_color(gradient->get_point_count() - 1) : Color(0, 0, 0);
Color color = _interpolate_color ? gradient->get_color_at_offset(1.0f) : Color(0, 0, 0);
float dist = 0;
if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
dist = width_factor / tile_aspect;
Expand Down
3 changes: 2 additions & 1 deletion scene/2d/line_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ class LineBuilder {
public:
// TODO Move in a struct and reference it
// Input
Vector<Vector2> points;
LocalVector<Vector2> points;
Line2D::LineJointMode joint_mode = Line2D::LINE_JOINT_SHARP;
Line2D::LineCapMode begin_cap_mode = Line2D::LINE_CAP_NONE;
Line2D::LineCapMode end_cap_mode = Line2D::LINE_CAP_NONE;
bool closed = false;
float width = 10.0;
Curve *curve = nullptr;
float curve_offset = 0.0f;
Color default_color = Color(0.4, 0.5, 1);
Gradient *gradient = nullptr;
Line2D::LineTextureMode texture_mode = Line2D::LineTextureMode::LINE_TEXTURE_NONE;
Expand Down
1 change: 1 addition & 0 deletions scene/resources/curve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ void Curve::set_bake_resolution(int p_resolution) {
ERR_FAIL_COND(p_resolution > 1000);
_bake_resolution = p_resolution;
_baked_cache_dirty = true;
emit_changed();
}

real_t Curve::sample_baked(real_t p_offset) const {
Expand Down
Loading