diff options
author | Marc Gilleron <marc.gilleron@gmail.com> | 2016-12-09 02:52:40 +0100 |
---|---|---|
committer | Marc Gilleron <marc.gilleron@gmail.com> | 2017-01-15 00:44:46 +0100 |
commit | e2fba10b952f694f604e20b9e5f220023a5f8fd2 (patch) | |
tree | 39b17083e2e7b8b830bce929c45109a4928064d5 /scene/2d | |
parent | dab73c701a9785be443977a613e57600d1e136c8 (diff) |
Added Line2D node that draws a polygon-based line
It supports unlimited width, color gradient, texture and some geometry
options for joints and caps. Also transparency without artifacts
(provided that line joints aren't too sharp).
Diffstat (limited to 'scene/2d')
-rw-r--r-- | scene/2d/line_2d.cpp | 307 | ||||
-rw-r--r-- | scene/2d/line_2d.h | 80 | ||||
-rw-r--r-- | scene/2d/line_builder.cpp | 563 | ||||
-rw-r--r-- | scene/2d/line_builder.h | 76 |
4 files changed, 1026 insertions, 0 deletions
diff --git a/scene/2d/line_2d.cpp b/scene/2d/line_2d.cpp new file mode 100644 index 0000000000..9db3659133 --- /dev/null +++ b/scene/2d/line_2d.cpp @@ -0,0 +1,307 @@ +#include "line_2d.h" +#include "core_string_names.h" + + +// Needed so we can bind functions +VARIANT_ENUM_CAST(LineJointMode) +VARIANT_ENUM_CAST(LineCapMode) +VARIANT_ENUM_CAST(LineTextureMode) + + +Line2D::Line2D() : Node2D() { + _joint_mode = LINE_JOINT_SHARP; + _begin_cap_mode = LINE_CAP_NONE; + _end_cap_mode = LINE_CAP_NONE; + _width = 10; + _default_color = Color(0.4,0.5,1); + _sharp_limit = 2.f; + _round_precision = 8; +} + +void Line2D::set_points(const PoolVector<Vector2> &p_points) { + _points = p_points; + update(); +} + +void Line2D::set_width(float width) { + if(width < 0.0) + width = 0.0; + _width = width; + update(); +} + +float Line2D::get_width() const { + return _width; +} + +PoolVector<Vector2> Line2D::get_points() const { + return _points; +} + +void Line2D::set_point_pos(int i, Vector2 pos) { + _points.set(i, pos); + update(); +} + +Vector2 Line2D::get_point_pos(int i) const { + return _points.get(i); +} + +int Line2D::get_point_count() const { + return _points.size(); +} + +void Line2D::add_point(Vector2 pos) { + _points.append(pos); + update(); +} + +void Line2D::remove_point(int i) { + _points.remove(i); + update(); +} + +void Line2D::set_default_color(Color color) { + _default_color = color; + update(); +} + +Color Line2D::get_default_color() const { + return _default_color; +} + +void Line2D::set_gradient(const Ref<ColorRamp>& gradient) { + + // Cleanup previous connection if any + if(_gradient.is_valid()) { + (**_gradient).disconnect(CoreStringNames::get_singleton()->changed, this, "_gradient_changed"); + } + + _gradient = gradient; + + // Connect to the gradient so the line will update when the ColorRamp is changed + if(_gradient.is_valid()) { + (**_gradient).connect(CoreStringNames::get_singleton()->changed, this, "_gradient_changed"); + } + + update(); +} + +Ref<ColorRamp> Line2D::get_gradient() const { + return _gradient; +} + +void Line2D::set_texture(const Ref<Texture>& texture) { + _texture = texture; + update(); +} + +Ref<Texture> Line2D::get_texture() const { + return _texture; +} + +void Line2D::set_texture_mode(const LineTextureMode mode) { + _texture_mode = mode; + update(); +} + +LineTextureMode Line2D::get_texture_mode() const { + return _texture_mode; +} + +void Line2D::set_joint_mode(LineJointMode mode) { + _joint_mode = mode; + update(); +} + +LineJointMode Line2D::get_joint_mode() const { + return _joint_mode; +} + +void Line2D::set_begin_cap_mode(LineCapMode mode) { + _begin_cap_mode = mode; + update(); +} + +LineCapMode Line2D::get_begin_cap_mode() const { + return _begin_cap_mode; +} + +void Line2D::set_end_cap_mode(LineCapMode mode) { + _end_cap_mode = mode; + update(); +} + +LineCapMode Line2D::get_end_cap_mode() const { + return _end_cap_mode; +} + +void Line2D::_notification(int p_what) { + switch(p_what) { + case NOTIFICATION_DRAW: + _draw(); + break; + } +} + +void Line2D::set_sharp_limit(float limit) { + if(limit < 0.f) + limit = 0.f; + _sharp_limit = limit; + update(); +} + +float Line2D::get_sharp_limit() const { + return _sharp_limit; +} + +void Line2D::set_round_precision(int precision) { + if(precision < 1) + precision = 1; + _round_precision = precision; + update(); +} + +int Line2D::get_round_precision() const { + return _round_precision; +} + +void Line2D::_draw() { + if(_points.size() <= 1 || _width == 0.f) + return; + + // TODO Is this really needed? + // Copy points for faster access + Vector<Vector2> points; + points.resize(_points.size()); + int len = points.size(); { + PoolVector<Vector2>::Read points_read = _points.read(); + for(int i = 0; i < len; ++i) { + points[i] = points_read[i]; + } + } + + // TODO Maybe have it as member rather than copying parameters and allocating memory? + LineBuilder lb; + lb.points = points; + lb.default_color = _default_color; + lb.gradient = *_gradient; + lb.texture_mode = _texture_mode; + lb.joint_mode = _joint_mode; + lb.begin_cap_mode = _begin_cap_mode; + lb.end_cap_mode = _end_cap_mode; + lb.round_precision = _round_precision; + lb.sharp_limit = _sharp_limit; + lb.width = _width; + + lb.build(); + + RID texture_rid; + if(_texture.is_valid()) + texture_rid = (**_texture).get_rid(); + + VS::get_singleton()->canvas_item_add_triangle_array( + get_canvas_item(), + lb.indices, + lb.vertices, + lb.colors, + lb.uvs, + texture_rid); + + // DEBUG + // Draw wireframe +// if(lb.indices.size() % 3 == 0) { +// Color col(0,0,0); +// for(int i = 0; i < lb.indices.size(); i += 3) { +// int vi = lb.indices[i]; +// int lbvsize = lb.vertices.size(); +// Vector2 a = lb.vertices[lb.indices[i]]; +// Vector2 b = lb.vertices[lb.indices[i+1]]; +// Vector2 c = lb.vertices[lb.indices[i+2]]; +// draw_line(a, b, col); +// draw_line(b, c, col); +// draw_line(c, a, col); +// } +// for(int i = 0; i < lb.vertices.size(); ++i) { +// Vector2 p = lb.vertices[i]; +// draw_rect(Rect2(p.x-1, p.y-1, 2, 2), Color(0,0,0,0.5)); +// } +// } +} + +void Line2D::_gradient_changed() { + update(); +} + +// static +void Line2D::_bind_methods() { + + ClassDB::bind_method(_MD("set_points","points"), &Line2D::set_points); + ClassDB::bind_method(_MD("get_points"), &Line2D::get_points); + + ClassDB::bind_method(_MD("set_point_pos","i", "pos"), &Line2D::set_point_pos); + ClassDB::bind_method(_MD("get_point_pos", "i"), &Line2D::get_point_pos); + + ClassDB::bind_method(_MD("get_point_count"), &Line2D::get_point_count); + + ClassDB::bind_method(_MD("add_point", "pos"), &Line2D::add_point); + ClassDB::bind_method(_MD("remove_point", "i"), &Line2D::remove_point); + + ClassDB::bind_method(_MD("set_width","width"), &Line2D::set_width); + ClassDB::bind_method(_MD("get_width"), &Line2D::get_width); + + ClassDB::bind_method(_MD("set_default_color", "color"), &Line2D::set_default_color); + ClassDB::bind_method(_MD("get_default_color"), &Line2D::get_default_color); + + ClassDB::bind_method(_MD("set_gradient", "color"), &Line2D::set_gradient); + ClassDB::bind_method(_MD("get_gradient"), &Line2D::get_gradient); + + ClassDB::bind_method(_MD("set_texture", "texture"), &Line2D::set_texture); + ClassDB::bind_method(_MD("get_texture"), &Line2D::get_texture); + + ClassDB::bind_method(_MD("set_texture_mode", "mode"), &Line2D::set_texture_mode); + ClassDB::bind_method(_MD("get_texture_mode"), &Line2D::get_texture_mode); + + ClassDB::bind_method(_MD("set_joint_mode", "mode"), &Line2D::set_joint_mode); + ClassDB::bind_method(_MD("get_joint_mode"), &Line2D::get_joint_mode); + + ClassDB::bind_method(_MD("set_begin_cap_mode", "mode"), &Line2D::set_begin_cap_mode); + ClassDB::bind_method(_MD("get_begin_cap_mode"), &Line2D::get_begin_cap_mode); + + ClassDB::bind_method(_MD("set_end_cap_mode", "mode"), &Line2D::set_end_cap_mode); + ClassDB::bind_method(_MD("get_end_cap_mode"), &Line2D::get_end_cap_mode); + + ClassDB::bind_method(_MD("set_sharp_limit", "limit"), &Line2D::set_sharp_limit); + ClassDB::bind_method(_MD("get_sharp_limit"), &Line2D::get_sharp_limit); + + ClassDB::bind_method(_MD("set_round_precision", "precision"), &Line2D::set_round_precision); + ClassDB::bind_method(_MD("get_round_precision"), &Line2D::get_round_precision); + + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "points"), _SCS("set_points"), _SCS("get_points")); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "width"), _SCS("set_width"), _SCS("get_width")); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "default_color"), _SCS("set_default_color"), _SCS("get_default_color")); + ADD_PROPERTYNZ( PropertyInfo( Variant::OBJECT, "gradient", PROPERTY_HINT_RESOURCE_TYPE, "ColorRamp"), _SCS("set_gradient"), _SCS("get_gradient" ) ); + ADD_PROPERTYNZ( PropertyInfo( Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), _SCS("set_texture"), _SCS("get_texture" ) ); + ADD_PROPERTYNZ( PropertyInfo( Variant::INT, "texture_mode", PROPERTY_HINT_ENUM, "None,Tile" ), _SCS("set_texture_mode"),_SCS("get_texture_mode" ) ); + ADD_PROPERTYNZ( PropertyInfo( Variant::INT, "joint_mode", PROPERTY_HINT_ENUM, "Sharp,Bevel,Round" ), _SCS("set_joint_mode"),_SCS("get_joint_mode" ) ); + ADD_PROPERTYNZ( PropertyInfo( Variant::INT, "begin_cap_mode", PROPERTY_HINT_ENUM, "None,Box,Round" ), _SCS("set_begin_cap_mode"),_SCS("get_begin_cap_mode" ) ); + ADD_PROPERTYNZ( PropertyInfo( Variant::INT, "end_cap_mode", PROPERTY_HINT_ENUM, "None,Box,Round" ), _SCS("set_end_cap_mode"),_SCS("get_end_cap_mode" ) ); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "sharp_limit"), _SCS("set_sharp_limit"), _SCS("get_sharp_limit")); + ADD_PROPERTY(PropertyInfo(Variant::INT, "round_precision"), _SCS("set_round_precision"), _SCS("get_round_precision")); + + BIND_CONSTANT(LINE_JOINT_SHARP); + BIND_CONSTANT(LINE_JOINT_BEVEL); + BIND_CONSTANT(LINE_JOINT_ROUND); + + BIND_CONSTANT(LINE_CAP_NONE); + BIND_CONSTANT(LINE_CAP_BOX); + BIND_CONSTANT(LINE_CAP_ROUND); + + BIND_CONSTANT(LINE_TEXTURE_NONE); + BIND_CONSTANT(LINE_TEXTURE_TILE); + + ClassDB::bind_method(_MD("_gradient_changed"), &Line2D::_gradient_changed); + +} + + diff --git a/scene/2d/line_2d.h b/scene/2d/line_2d.h new file mode 100644 index 0000000000..f4e2eb7a55 --- /dev/null +++ b/scene/2d/line_2d.h @@ -0,0 +1,80 @@ +#ifndef LINE2D_H +#define LINE2D_H + +#include "node_2d.h" +#include "line_builder.h" + + +class Line2D : public Node2D { + + GDCLASS(Line2D, Node2D) + +public: + Line2D(); + + void set_points(const PoolVector<Vector2> & p_points); + PoolVector<Vector2> get_points() const; + + void set_point_pos(int i, Vector2 pos); + Vector2 get_point_pos(int i) const; + + int get_point_count() const; + + void add_point(Vector2 pos); + void remove_point(int i); + + void set_width(float width); + float get_width() const; + + void set_default_color(Color color); + Color get_default_color() const; + + void set_gradient(const Ref<ColorRamp>& gradient); + Ref<ColorRamp> get_gradient() const; + + void set_texture(const Ref<Texture>& texture); + Ref<Texture> get_texture() const; + + void set_texture_mode(const LineTextureMode mode); + LineTextureMode get_texture_mode() const; + + void set_joint_mode(LineJointMode mode); + LineJointMode get_joint_mode() const; + + void set_begin_cap_mode(LineCapMode mode); + LineCapMode get_begin_cap_mode() const; + + void set_end_cap_mode(LineCapMode mode); + LineCapMode get_end_cap_mode() const; + + void set_sharp_limit(float limit); + float get_sharp_limit() const; + + void set_round_precision(int precision); + int get_round_precision() const; + +protected: + void _notification(int p_what); + void _draw(); + + static void _bind_methods(); + +private: + void _gradient_changed(); + +private: + PoolVector<Vector2> _points; + LineJointMode _joint_mode; + LineCapMode _begin_cap_mode; + LineCapMode _end_cap_mode; + float _width; + Color _default_color; + Ref<ColorRamp> _gradient; + Ref<Texture> _texture; + LineTextureMode _texture_mode; + float _sharp_limit; + int _round_precision; + +}; + +#endif // LINE2D_H diff --git a/scene/2d/line_builder.cpp b/scene/2d/line_builder.cpp new file mode 100644 index 0000000000..230f72cccd --- /dev/null +++ b/scene/2d/line_builder.cpp @@ -0,0 +1,563 @@ +#include "line_builder.h" + +//---------------------------------------------------------------------------- +// Util +//---------------------------------------------------------------------------- + +enum SegmentIntersectionResult { + SEGMENT_PARALLEL = 0, + SEGMENT_NO_INTERSECT = 1, + SEGMENT_INTERSECT = 2 +}; + +static SegmentIntersectionResult segment_intersection( + Vector2 a, Vector2 b, Vector2 c, Vector2 d, + Vector2 * out_intersection) +{ + // http://paulbourke.net/geometry/pointlineplane/ <-- Good stuff + Vector2 cd = d - c; + Vector2 ab = b - a; + float div = cd.y*ab.x - cd.x*ab.y; + + if(Math::abs(div) > 0.001f) { + float ua = (cd.x * (a.y-c.y) - cd.y * (a.x-c.x)) / div; + float ub = (ab.x * (a.y-c.y) - ab.y * (a.x-c.x)) / div; + *out_intersection = a + ua * ab; + if(ua >= 0.f && ua <= 1.f && + ub >= 0.f && ub <= 1.f) + return SEGMENT_INTERSECT; + return SEGMENT_NO_INTERSECT; + } + + return SEGMENT_PARALLEL; +} + +// TODO I'm pretty sure there is an even faster way to swap things +template <typename T> +static inline void swap(T & a, T & b) { + T tmp = a; + a = b; + b = tmp; +} + +static float calculate_total_distance(const Vector<Vector2> & points) { + float d = 0.f; + for(int i = 1; i < points.size(); ++i) { + d += points[i].distance_to(points[i-1]); + } + return d; +} + +static inline Vector2 rotate90(const Vector2 & v) { + // Note: the 2D referential is X-right, Y-down + return Vector2(v.y, -v.x); +} + +static inline Vector2 interpolate(const Rect2 & r, const Vector2 & v) { + return Vector2( + Math::lerp(r.get_pos().x, r.get_pos().x + r.get_size().x, v.x), + Math::lerp(r.get_pos().y, r.get_pos().y + r.get_size().y, v.y) + ); +} + +//---------------------------------------------------------------------------- +// LineBuilder +//---------------------------------------------------------------------------- + +LineBuilder::LineBuilder() { + joint_mode = LINE_JOINT_SHARP; + width = 10; + default_color = Color(0.4,0.5,1); + gradient = NULL; + sharp_limit = 2.f; + round_precision = 8; + begin_cap_mode = LINE_CAP_NONE; + end_cap_mode = LINE_CAP_NONE; + + _interpolate_color = false; + _last_index[0] = 0; + _last_index[1] = 0; +} + + +void LineBuilder::clear_output() { + vertices.clear(); + colors.clear(); + indices.clear(); +} + +void LineBuilder::build() { + + // Need at least 2 points to draw a line + if(points.size() < 2) { + clear_output(); + return; + } + + const float hw = width / 2.f; + const float hw_sq = hw*hw; + const float sharp_limit_sq = sharp_limit * sharp_limit; + const int len = points.size(); + + // Initial values + + Vector2 pos0 = points[0]; + Vector2 pos1 = points[1]; + Vector2 f0 = (pos1 - pos0).normalized(); + Vector2 u0 = rotate90(f0); + Vector2 pos_up0 = pos0 + u0 * hw; + Vector2 pos_down0 = pos0 - u0 * hw; + + Color color0; + Color color1; + + float current_distance0 = 0.f; + float current_distance1 = 0.f; + float total_distance; + _interpolate_color = gradient != NULL; + bool distance_required = _interpolate_color || texture_mode == LINE_TEXTURE_TILE; + if(distance_required) + total_distance = calculate_total_distance(points); + if(_interpolate_color) + color0 = gradient->get_color(0); + else + colors.push_back(default_color); + + float uvx0 = 0.f; + float uvx1 = 0.f; + + // Begin cap + if(begin_cap_mode == LINE_CAP_BOX) { + // Push back first vertices a little bit + pos_up0 -= f0 * hw; + pos_down0 -= f0 * hw; + // The line's outer length will be a little higher due to begin and end caps + total_distance += width; + current_distance0 += hw; + current_distance1 = current_distance0; + } + else if(begin_cap_mode == LINE_CAP_ROUND) { + if(texture_mode == LINE_TEXTURE_TILE) { + uvx0 = 0.5f; + } + new_arc(pos0, pos_up0 - pos0, -Math_PI, color0, Rect2(0.f, 0.f, 1.f, 1.f)); + total_distance += width; + current_distance0 += hw; + current_distance1 = current_distance0; + } + + strip_begin(pos_up0, pos_down0, color0, uvx0); + + // pos_up0 ------------- pos_up1 -------------------- + // | | + // pos0 - - - - - - - - - pos1 - - - - - - - - - pos2 + // | | + // pos_down0 ------------ pos_down1 ------------------ + // + // i-1 i i+1 + + // http://labs.hyperandroid.com/tag/opengl-lines + // (not the same implementation but visuals help a lot) + + // For each additional segment + for(int i = 1; i < len-1; ++i) { + + pos1 = points[i]; + Vector2 pos2 = points[i+1]; + + Vector2 f1 = (pos2 - pos1).normalized(); + Vector2 u1 = rotate90(f1); + + // Determine joint orientation + const float dp = u0.dot(f1); + const Orientation orientation = (dp > 0.f ? UP : DOWN); + + Vector2 inner_normal0, inner_normal1; + if(orientation == UP) { + inner_normal0 = u0 * hw; + inner_normal1 = u1 * hw; + } + else { + inner_normal0 = -u0 * hw; + inner_normal1 = -u1 * hw; + } + + // --------------------------- + // / + // 0 / 1 + // / / + // --------------------x------ / + // / / (here shown with orientation == DOWN) + // / / + // / / + // / / + // 2 / + // / + + // Find inner intersection at the joint + Vector2 corner_pos_in, corner_pos_out; + SegmentIntersectionResult intersection_result = segment_intersection( + pos0 + inner_normal0, pos1 + inner_normal0, + pos1 + inner_normal1, pos2 + inner_normal1, + &corner_pos_in); + + if(intersection_result == SEGMENT_INTERSECT) + // Inner parts of the segments intersect + corner_pos_out = 2.f * pos1 - corner_pos_in; + else { + // No intersection, segments are either parallel or too sharp + corner_pos_in = pos1 + inner_normal0; + corner_pos_out = pos1 - inner_normal0; + } + + Vector2 corner_pos_up, corner_pos_down; + if(orientation == UP) { + corner_pos_up = corner_pos_in; + corner_pos_down = corner_pos_out; + } + else { + corner_pos_up = corner_pos_out; + corner_pos_down = corner_pos_in; + } + + LineJointMode current_joint_mode = joint_mode; + + Vector2 pos_up1, pos_down1; + if(intersection_result == SEGMENT_INTERSECT) { + // Fallback on bevel if sharp angle is too high (because it would produce very long miters) + if(current_joint_mode == LINE_JOINT_SHARP && corner_pos_out.distance_squared_to(pos1) / hw_sq > sharp_limit_sq) { + current_joint_mode = LINE_JOINT_BEVEL; + } + if(current_joint_mode == LINE_JOINT_SHARP) { + // In this case, we won't create joint geometry, + // The previous and next line quads will directly share an edge. + pos_up1 = corner_pos_up; + pos_down1 = corner_pos_down; + } + else { + // Bevel or round + if(orientation == UP) { + pos_up1 = corner_pos_up; + pos_down1 = pos1 - u0 * hw; + } + else { + pos_up1 = pos1 + u0 * hw; + pos_down1 = corner_pos_down; + } + } + } + else { + // No intersection: fallback + pos_up1 = corner_pos_up; + pos_down1 = corner_pos_down; + } + + // Add current line body quad + // Triangles are clockwise + if(distance_required) { + current_distance1 += pos0.distance_to(pos1); + } + if(_interpolate_color) { + color1 = gradient->get_color_at_offset(current_distance1 / total_distance); + } + if(texture_mode == LINE_TEXTURE_TILE) { + uvx0 = current_distance0 / width; + uvx1 = current_distance1 / width; + } + + strip_add_quad(pos_up1, pos_down1, color1, uvx1); + + // Swap vars for use in the next line + color0 = color1; + u0 = u1; + f0 = f1; + pos0 = pos1; + current_distance0 = current_distance1; + if(intersection_result == SEGMENT_INTERSECT) { + if(current_joint_mode == LINE_JOINT_SHARP) { + pos_up0 = pos_up1; + pos_down0 = pos_down1; + } + else { + if(orientation == UP) { + pos_up0 = corner_pos_up; + pos_down0 = pos1 - u1 * hw; + } + else { + pos_up0 = pos1 + u1 * hw; + pos_down0 = corner_pos_down; + } + } + } + else { + pos_up0 = pos1 + u1 * hw; + pos_down0 = pos1 - u1 * hw; + } + // From this point, bu0 and bd0 concern the next segment + + // Add joint geometry + if(current_joint_mode != LINE_JOINT_SHARP) { + + // ________________ cbegin + // / \ + // / \ + // ____________/_ _ _\ cend + // | | + // | | + // | | + + Vector2 cbegin, cend; + if(orientation == UP) { + cbegin = pos_down1; + cend = pos_down0; + } + else { + cbegin = pos_up1; + cend = pos_up0; + } + + if(current_joint_mode == LINE_JOINT_BEVEL) { + strip_add_tri(cend, orientation); + } + else if(current_joint_mode == LINE_JOINT_ROUND) { + Vector2 vbegin = cbegin - pos1; + Vector2 vend = cend - pos1; + strip_add_arc(pos1, vend.angle_to(vbegin), orientation); + } + + if(intersection_result != SEGMENT_INTERSECT) + // In this case the joint is too fucked up to be re-used, + // start again the strip with fallback points + strip_begin(pos_up0, pos_down0, color1, uvx1); + } + } + + // Last (or only) segment + + pos1 = points[points.size()-1]; + + Vector2 pos_up1 = pos1 + u0 * hw; + Vector2 pos_down1 = pos1 - u0 * hw; + + // End cap (box) + if(end_cap_mode == LINE_CAP_BOX) { + pos_up1 += f0 * hw; + pos_down1 += f0 * hw; + } + + if(distance_required) { + current_distance1 += pos0.distance_to(pos1); + } + if(_interpolate_color) { + color1 = gradient->get_color(gradient->get_points_count()-1); + } + if(texture_mode == LINE_TEXTURE_TILE) { + uvx1 = current_distance1 / width; + } + + strip_add_quad(pos_up1, pos_down1, color1, uvx1); + + // End cap (round) + if(end_cap_mode == LINE_CAP_ROUND) { + // Note: color is not used in case we don't interpolate... + Color color = _interpolate_color ? gradient->get_color(gradient->get_points_count()-1) : Color(0,0,0); + new_arc(pos1, pos_up1 - pos1, Math_PI, color, Rect2(uvx1-0.5f, 0.f, 1.f, 1.f)); + } +} + +void LineBuilder::strip_begin(Vector2 up, Vector2 down, Color color, float uvx) { + int vi = vertices.size(); + + vertices.push_back(up); + vertices.push_back(down); + + if(_interpolate_color) { + colors.push_back(color); + colors.push_back(color); + } + + if(texture_mode != LINE_TEXTURE_NONE) { + uvs.push_back(Vector2(uvx, 0.f)); + uvs.push_back(Vector2(uvx, 1.f)); + } + + _last_index[UP] = vi; + _last_index[DOWN] = vi+1; +} + +void LineBuilder::strip_new_quad(Vector2 up, Vector2 down, Color color, float uvx) { + int vi = vertices.size(); + + vertices.push_back(vertices[_last_index[UP]]); + vertices.push_back(vertices[_last_index[DOWN]]); + vertices.push_back(up); + vertices.push_back(down); + + if(_interpolate_color) { + colors.push_back(color); + colors.push_back(color); + colors.push_back(color); + colors.push_back(color); + } + + if(texture_mode != LINE_TEXTURE_NONE) { + uvs.push_back(uvs[_last_index[UP]]); + uvs.push_back(uvs[_last_index[DOWN]]); + uvs.push_back(Vector2(uvx, UP)); + uvs.push_back(Vector2(uvx, DOWN)); + } + + indices.push_back(vi); + indices.push_back(vi+3); + indices.push_back(vi+1); + indices.push_back(vi); + indices.push_back(vi+2); + indices.push_back(vi+3); + + _last_index[UP] = vi+2; + _last_index[DOWN] = vi+3; +} + +void LineBuilder::strip_add_quad(Vector2 up, Vector2 down, Color color, float uvx) { + int vi = vertices.size(); + + vertices.push_back(up); + vertices.push_back(down); + + if(_interpolate_color) { + colors.push_back(color); + colors.push_back(color); + } + + if(texture_mode != LINE_TEXTURE_NONE) { + uvs.push_back(Vector2(uvx, 0.f)); + uvs.push_back(Vector2(uvx, 1.f)); + } + + indices.push_back(_last_index[UP]); + indices.push_back(vi+1); + indices.push_back(_last_index[DOWN]); + indices.push_back(_last_index[UP]); + indices.push_back(vi); + indices.push_back(vi+1); + + _last_index[UP] = vi; + _last_index[DOWN] = vi+1; +} + +void LineBuilder::strip_add_tri(Vector2 up, Orientation orientation) { + int vi = vertices.size(); + + vertices.push_back(up); + + if(_interpolate_color) { + colors.push_back(colors[colors.size()-1]); + } + + Orientation opposite_orientation = orientation == UP ? DOWN : UP; + + if(texture_mode != LINE_TEXTURE_NONE) { + // UVs are just one slice of the texture all along + // (otherwise we can't share the bottom vertice) + uvs.push_back(uvs[_last_index[opposite_orientation]]); + } + + indices.push_back(_last_index[opposite_orientation]); + indices.push_back(vi); + indices.push_back(_last_index[orientation]); + + _last_index[opposite_orientation] = vi; +} + +void LineBuilder::strip_add_arc(Vector2 center, float angle_delta, Orientation orientation) { + + // Take the two last vertices and extrude an arc made of triangles + // that all share one of the initial vertices + + Orientation opposite_orientation = orientation == UP ? DOWN : UP; + Vector2 vbegin = vertices[_last_index[opposite_orientation]] - center; + float radius = vbegin.length(); + float angle_step = Math_PI / static_cast<float>(round_precision); + float steps = Math::abs(angle_delta) / angle_step; + + if(angle_delta < 0.f) + angle_step = -angle_step; + + float t = vbegin.angle_to(Vector2(1, 0)); + float end_angle = t + angle_delta; + Vector2 rpos(0,0); + + // Arc vertices + for(int ti = 0; ti < steps; ++ti, t += angle_step) { + rpos = center + Vector2(Math::cos(t), Math::sin(t)) * radius; + strip_add_tri(rpos, orientation); + } + + // Last arc vertice + rpos = center + Vector2(Math::cos(end_angle), Math::sin(end_angle)) * radius; + strip_add_tri(rpos, orientation); +} + +void LineBuilder::new_arc(Vector2 center, Vector2 vbegin, float angle_delta, Color color, Rect2 uv_rect) { + + // Make a standalone arc that doesn't use existing vertices, + // with undistorted UVs from withing a square section + + float radius = vbegin.length(); + float angle_step = Math_PI / static_cast<float>(round_precision); + float steps = Math::abs(angle_delta) / angle_step; + + if(angle_delta < 0.f) + angle_step = -angle_step; + + float t = vbegin.angle_to(Vector2(1, 0)); + float end_angle = t + angle_delta; + Vector2 rpos(0,0); + float tt_begin = -Math_PI / 2.f; + float tt = tt_begin; + + // Center vertice + int vi = vertices.size(); + vertices.push_back(center); + if(_interpolate_color) + colors.push_back(color); + if(texture_mode != LINE_TEXTURE_NONE) + uvs.push_back(interpolate(uv_rect, Vector2(0.5f, 0.5f))); + + // Arc vertices + for(int ti = 0; ti < steps; ++ti, t += angle_step) { + Vector2 sc = Vector2(Math::cos(t), Math::sin(t)); + rpos = center + sc * radius; + + vertices.push_back(rpos); + if(_interpolate_color) + colors.push_back(color); + if(texture_mode != LINE_TEXTURE_NONE) { + Vector2 tsc = Vector2(Math::cos(tt), Math::sin(tt)); + uvs.push_back(interpolate(uv_rect, 0.5f*(tsc+Vector2(1.f,1.f)))); + tt += angle_step; + } + } + + // Last arc vertice + Vector2 sc = Vector2(Math::cos(end_angle), Math::sin(end_angle)); + rpos = center + sc * radius; + vertices.push_back(rpos); + if(_interpolate_color) + colors.push_back(color); + if(texture_mode != LINE_TEXTURE_NONE) { + tt = tt_begin + angle_delta; + Vector2 tsc = Vector2(Math::cos(tt), Math::sin(tt)); + uvs.push_back(interpolate(uv_rect, 0.5f*(tsc+Vector2(1.f,1.f)))); + } + + // Make up triangles + int vi0 = vi; + for(int ti = 0; ti < steps; ++ti) { + indices.push_back(vi0); + indices.push_back(++vi); + indices.push_back(vi+1); + } +} + + diff --git a/scene/2d/line_builder.h b/scene/2d/line_builder.h new file mode 100644 index 0000000000..73419f88d8 --- /dev/null +++ b/scene/2d/line_builder.h @@ -0,0 +1,76 @@ +#ifndef LINE_BUILDER_H +#define LINE_BUILDER_H + +#include "math_2d.h" +#include "color.h" +#include "scene/resources/color_ramp.h" + +enum LineJointMode { + LINE_JOINT_SHARP = 0, + LINE_JOINT_BEVEL, + LINE_JOINT_ROUND +}; + +enum LineCapMode { + LINE_CAP_NONE = 0, + LINE_CAP_BOX, + LINE_CAP_ROUND +}; + +enum LineTextureMode { + LINE_TEXTURE_NONE = 0, + LINE_TEXTURE_TILE + // TODO STRETCH mode +}; + +class LineBuilder { +public: + // TODO Move in a struct and reference it + // Input + Vector<Vector2> points; + LineJointMode joint_mode; + LineCapMode begin_cap_mode; + LineCapMode end_cap_mode; + float width; + Color default_color; + ColorRamp* gradient; + LineTextureMode texture_mode; + float sharp_limit; + int round_precision; + // TODO offset_joints option (offers alternative implementation of round joints) + + // TODO Move in a struct and reference it + // Output + Vector<Vector2> vertices; + Vector<Color> colors; + Vector<Vector2> uvs; + Vector<int> indices; + + LineBuilder(); + + void build(); + void clear_output(); + +private: + enum Orientation { + UP = 0, + DOWN = 1 + }; + + // Triangle-strip methods + void strip_begin(Vector2 up, Vector2 down, Color color, float uvx); + void strip_new_quad(Vector2 up, Vector2 down, Color color, float uvx); + void strip_add_quad(Vector2 up, Vector2 down, Color color, float uvx); + void strip_add_tri(Vector2 up, Orientation orientation); + void strip_add_arc(Vector2 center, float angle_delta, Orientation orientation); + + void new_arc(Vector2 center, Vector2 vbegin, float angle_delta, Color color, Rect2 uv_rect); + +private: + bool _interpolate_color; + int _last_index[2]; // Index of last up and down vertices of the strip + +}; + + +#endif // LINE_BUILDER_H |