From 0427e9dd2fe9d7b0803894a6f33d6ff0e00dd24c Mon Sep 17 00:00:00 2001 From: unfa Date: Thu, 1 Apr 2021 23:30:10 +0200 Subject: [PATCH] Addd the Godot Trail System Add-on. --- Game/addons/Trail/LICENSE | 21 ++ Game/addons/Trail/plugin.cfg | 7 + Game/addons/Trail/plugin.gd | 12 + Game/addons/Trail/trail2d_icon.svg | 35 +++ Game/addons/Trail/trail2d_icon.svg.import | 34 +++ Game/addons/Trail/trail3d_icon.svg | 83 +++++ Game/addons/Trail/trail3d_icon.svg.import | 34 +++ Game/addons/Trail/trail_2d.gd | 77 +++++ Game/addons/Trail/trail_3d.gd | 349 ++++++++++++++++++++++ Game/addons/Trail/trail_3d_v1.gd | 194 ++++++++++++ 10 files changed, 846 insertions(+) create mode 100644 Game/addons/Trail/LICENSE create mode 100644 Game/addons/Trail/plugin.cfg create mode 100644 Game/addons/Trail/plugin.gd create mode 100644 Game/addons/Trail/trail2d_icon.svg create mode 100644 Game/addons/Trail/trail2d_icon.svg.import create mode 100644 Game/addons/Trail/trail3d_icon.svg create mode 100644 Game/addons/Trail/trail3d_icon.svg.import create mode 100644 Game/addons/Trail/trail_2d.gd create mode 100644 Game/addons/Trail/trail_3d.gd create mode 100644 Game/addons/Trail/trail_3d_v1.gd diff --git a/Game/addons/Trail/LICENSE b/Game/addons/Trail/LICENSE new file mode 100644 index 0000000..bfe8154 --- /dev/null +++ b/Game/addons/Trail/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Oussama + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Game/addons/Trail/plugin.cfg b/Game/addons/Trail/plugin.cfg new file mode 100644 index 0000000..7302aa3 --- /dev/null +++ b/Game/addons/Trail/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Trail System" +description="Advanced 2D/3D Trail system." +author="Oussama BOUKHELF" +version="0.1" +script="plugin.gd" diff --git a/Game/addons/Trail/plugin.gd b/Game/addons/Trail/plugin.gd new file mode 100644 index 0000000..4c96d31 --- /dev/null +++ b/Game/addons/Trail/plugin.gd @@ -0,0 +1,12 @@ +tool +extends EditorPlugin + +func _enter_tree(): + add_custom_type("Trail3D","ImmediateGeometry",preload("res://addons/Trail/trail_3d.gd"),preload("res://addons/Trail/trail3d_icon.svg")) + add_custom_type("Trail2D","Line2D",preload("res://addons/Trail/trail_2d.gd"),preload("res://addons/Trail/trail2d_icon.svg")) + pass + +func _exit_tree(): + remove_custom_type("Trail3D") + remove_custom_type("Trail2D") + pass diff --git a/Game/addons/Trail/trail2d_icon.svg b/Game/addons/Trail/trail2d_icon.svg new file mode 100644 index 0000000..69ba4a2 --- /dev/null +++ b/Game/addons/Trail/trail2d_icon.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Game/addons/Trail/trail2d_icon.svg.import b/Game/addons/Trail/trail2d_icon.svg.import new file mode 100644 index 0000000..e2737cb --- /dev/null +++ b/Game/addons/Trail/trail2d_icon.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/trail2d_icon.svg-607ea772beb499297607579128e70a1c.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Addons/addons/Trail/trail2d_icon.svg" +dest_files=[ "res://.import/trail2d_icon.svg-607ea772beb499297607579128e70a1c.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Game/addons/Trail/trail3d_icon.svg b/Game/addons/Trail/trail3d_icon.svg new file mode 100644 index 0000000..c6ff9ef --- /dev/null +++ b/Game/addons/Trail/trail3d_icon.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Game/addons/Trail/trail3d_icon.svg.import b/Game/addons/Trail/trail3d_icon.svg.import new file mode 100644 index 0000000..05cf7ad --- /dev/null +++ b/Game/addons/Trail/trail3d_icon.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/trail3d_icon.svg-8b210038e0dc69dfd0c78cba261254df.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Addons/addons/Trail/trail3d_icon.svg" +dest_files=[ "res://.import/trail3d_icon.svg-8b210038e0dc69dfd0c78cba261254df.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/Game/addons/Trail/trail_2d.gd b/Game/addons/Trail/trail_2d.gd new file mode 100644 index 0000000..71f931e --- /dev/null +++ b/Game/addons/Trail/trail_2d.gd @@ -0,0 +1,77 @@ +""" +Author: Oussama BOUKHELF +License: MIT +Version: 0.1 +Email: o.boukhelf@gmail.com +Description: Advanced 2D/3D Trail system. +Note: This is a simple implementation, I will update it later on. +""" + +extends Line2D + + +export(bool) var emit := true +export(float) var lifetime := 0.5 +export(float) var distance := 20.0 +export(int) var segments := 20 +var target + +var trail_points := [] +var offset := Vector2() + + +class Point: + var position := Vector2() + var age := 0.0 + + func _init(position :Vector2, age :float) -> void: + self.position = position + self.age = age + + func update(delta :float, points :Array) -> void: + self.age -= delta + if self.age <= 0: + points.erase(self) + + +func _ready(): + offset = position + show_behind_parent = true + target = get_parent() + clear_points() + set_as_toplevel(true) + position = Vector2() + +func _emit(): + var _position :Vector2 = target.global_transform.origin + offset + var point = Point.new(_position, lifetime) + + if trail_points.size() < 1: + trail_points.push_back(point) + return + + if trail_points[-1].position.distance_squared_to(_position) > distance*distance: + trail_points.push_back(point) + + update_points() + +func update_points() -> void: + var delta = get_process_delta_time() + + if trail_points.size() > segments: + trail_points.invert() + trail_points.resize(segments) + trail_points.invert() + + clear_points() + for point in trail_points: + point.update(delta, trail_points) + +# if point: + add_point(point.position) + + +func _process(delta): + if emit: + _emit() + diff --git a/Game/addons/Trail/trail_3d.gd b/Game/addons/Trail/trail_3d.gd new file mode 100644 index 0000000..09e72d0 --- /dev/null +++ b/Game/addons/Trail/trail_3d.gd @@ -0,0 +1,349 @@ +""" +Author: Oussama BOUKHELF +License: MIT +Version: 0.1 +Email: o.boukhelf@gmail.com +Description: Advanced 2D/3D Trail system. +""" + +extends ImmediateGeometry + + +export(bool) var emit := true +export(float) var distance := 0.1 +export(int, 0, 99999) var segments := 20 +export(float) var lifetime := 0.5 +export(float, 0, 99999) var base_width := 0.5 +export(bool) var tiled_texture := false +export(int) var tiling := 0 +export(Curve) var width_profile +export(Gradient) var color_gradient +export(int, 0, 3) var smoothing_iterations := 0 +export(float, 0, 0.5) var smoothing_ratio := 0.25 +export(String, "View", "Normal", "Object") var alignment := "View" +export(String, "X", "Y", "Z") var axe := "Y" +export(bool) var show_wireframe := false +export(Color) var wireframe_color := Color(1, 1, 1, 1) +export(float, 0, 100, 0.1) var wire_line_width := 1.0 + +var points := [] +var color := Color(1, 1, 1, 1) +var always_update = false + +var _target :Spatial +var _wire_obj :ImmediateGeometry = ImmediateGeometry.new() +var _wire_mat :SpatialMaterial = SpatialMaterial.new() +var _A: Point +var _B: Point +var _C: Point +var _temp_segment := [] +var _points := [] + + +class Point: + """ + Class for the 3D point that will be emmited when the object move. + """ + var transform := Transform() + var age := 0.0 + + func _init(transform :Transform, age :float) -> void: + self.transform = transform + self.age = age + + func update(delta :float, points :Array) -> void: + self.age -= delta + if self.age <= 0: + points.erase(self) + + +func add_point(transform :Transform) -> void: + """ + Add a point to the list of points. + This function is called programmatically. + """ + var point = Point.new(transform, lifetime) + points.push_back(point) + + +func clear_points() -> void: + """ + Cleat points list. + This function is called programmatically. + """ + points.clear() + + +func _prepare_geometry(point_prev :Point, point :Point, half_width :float, factor :float) -> Array: + """ + Generate and transform the trail geometry based on the path points that + the target object generated. + """ + var normal := Vector3() + + if alignment == "View": + if get_viewport().get_camera(): + var cam_pos = get_viewport().get_camera().get_global_transform().origin + var path_direction :Vector3 = (point.transform.origin - point_prev.transform.origin).normalized() + normal = (cam_pos - (point.transform.origin + point_prev.transform.origin)/2).cross(path_direction).normalized() + else: + print("There is no camera in the scene") + + elif alignment == "Normal": + if axe == "X": + normal = point.transform.basis.x.normalized() + elif axe == "Y": + normal = point.transform.basis.y.normalized() + else: + normal = point.transform.basis.z.normalized() + + else: + if axe == "X": + normal = _target.global_transform.basis.x.normalized() + elif axe == "Y": + normal = _target.global_transform.basis.y.normalized() + else: + normal = _target.global_transform.basis.z.normalized() + + var width = half_width + if width_profile: + width = half_width * width_profile.interpolate(factor) + + var p1 = point.transform.origin-normal*width + var p2 = point.transform.origin+normal*width + return [p1, p2] + + +func render(update := false) -> void: + """ + Render the points. + This function is called programmatically. + """ + if update: + always_update = update + else: + _render_geometry(points) + + +func _render_realtime() -> void: + """ + Render the points every frame when "emit" is set to True. + """ + var render_points = _points+_temp_segment+[_C] + _render_geometry(render_points) + + +func _render_geometry(source: Array) -> void: + """ + Base function for rendering the generated geometry to the screen. + Renders the trail, and the wireframe if set in parameters. + """ + var points_count = source.size() + if points_count < 2: + return + + # The following section is a hack to make orientation "view" work. + # but it may cause an artifact at the end of the trail. + # You can use transparency in the gradient to hide it for now. + var _d :Vector3 = source[0].transform.origin - source[1].transform.origin + var _t :Transform = source[0].transform + _t.origin = _t.origin + _d + var point = Point.new(_t, source[0].age) + var to_be_rendered = [point]+source + points_count += 1 + + var half_width :float = base_width/2.0 + var wire_points = [] + var u := 0.0 + + clear() + begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, null) + for i in range(1, points_count): + var factor :float = float(i)/(points_count-1) + + var _color = color + if color_gradient: + _color = color * color_gradient.interpolate(1.0-factor) + + var vertices = _prepare_geometry(to_be_rendered[i-1], to_be_rendered[i], half_width, 1.0-factor) + if tiled_texture: + if tiling > 0: + factor *= tiling + else: + var travel = (to_be_rendered[i-1].transform.origin - to_be_rendered[i].transform.origin).length() + u += travel/base_width + factor = u + + set_color(_color) + set_uv(Vector2(factor, 0)) + add_vertex(vertices[0]) + set_uv(Vector2(factor, 1)) + add_vertex(vertices[1]) + + if show_wireframe: + wire_points += vertices + end() + + # For some reason I had to add a second Meshinstance as a child to make the + # wireframe to render, normally you can just draw on top. + if show_wireframe: + _wire_mat.params_line_width = wire_line_width + _wire_obj.clear() + _wire_obj.begin(Mesh.PRIMITIVE_LINE_STRIP, null) + _wire_obj.set_color(wireframe_color) + _wire_obj.set_uv(Vector2(0.5, 0.5)) + for i in range(1, wire_points.size()-2, 2): + ## order: i-1, i+1, i, i+2 + _wire_obj.add_vertex(wire_points[i-1]) + _wire_obj.add_vertex(wire_points[i+1]) + _wire_obj.add_vertex(wire_points[i]) + _wire_obj.add_vertex(wire_points[i+2]) + _wire_obj.end() + + +func _update_points() -> void: + """ + Update ages of the points and remove extra ones. + """ + var delta = get_process_delta_time() + + _A.update(delta, _points) + _B.update(delta, _points) + _C.update(delta, _points) + for point in _points: + point.update(delta, _points) + + var size_multiplier = [1, 2, 4, 6][smoothing_iterations] + var max_points_count :int = segments * size_multiplier + if _points.size() > max_points_count: + _points.invert() + _points.resize(max_points_count) + _points.invert() + + +func smooth() -> void: + """ + Smooth the given path. + This function is called programmatically. + """ + if points.size() < 3: + return + + var output := [points[0]] + for i in range(1, points.size()-1): + output += _chaikin(points[i-1], points[i], points[i+1]) + + output.push_back(points[-1]) + points = output + + +func _chaikin(A, B, C) -> Array: + """ + Chaikin’s smoothing Algorithm + https://www.cs.unc.edu/~dm/UNC/COMP258/LECTURES/Chaikins-Algorithm.pdf + + Ps: I could have avoided a lot of trouble automating this function using FOR loop, + but I opted for a more optimized approach which maybe helpful when dealing with a + large amount of objects. + """ + if smoothing_iterations == 0: + return [B] + + var out := [] + var x :float = smoothing_ratio + + # Pre-calculate some parameters to improve performance + var xi :float = (1-x) + var xpa :float = (x*x-2*x+1) + var xpb :float = (-x*x+2*x) + # transforms + var A1_t :Transform = A.transform.interpolate_with(B.transform, xi) + var B1_t :Transform = B.transform.interpolate_with(C.transform, x) + # ages + var A1_a :float = lerp(A.age, B.age, xi) + var B1_a :float = lerp(B.age, C.age, x) + + if smoothing_iterations == 1: + out = [Point.new(A1_t, A1_a), Point.new(B1_t, B1_a)] + + else: + # transforms + var A2_t :Transform = A.transform.interpolate_with(B.transform, xpa) + var B2_t :Transform = B.transform.interpolate_with(C.transform, xpb) + var A11_t :Transform = A1_t.interpolate_with(B1_t, x) + var B11_t :Transform = A1_t.interpolate_with(B1_t, xi) + # ages + var A2_a :float = lerp(A.age, B.age, xpa) + var B2_a :float = lerp(B.age, C.age, xpb) + var A11_a :float = lerp(A1_a, B1_a, x) + var B11_a :float = lerp(A1_a, B1_a, xi) + + if smoothing_iterations == 2: + out += [Point.new(A2_t, A2_a), Point.new(A11_t, A11_a), + Point.new(B11_t, B11_a), Point.new(B2_t, B2_a)] + elif smoothing_iterations == 3: + # transforms + var A12_t :Transform = A1_t.interpolate_with(B1_t, xpb) + var B12_t :Transform = A1_t.interpolate_with(B1_t, xpa) + var A121_t :Transform = A11_t.interpolate_with(A2_t, x) + var B121_t :Transform = B11_t.interpolate_with(B2_t, x) + # ages + var A12_a :float = lerp(A1_a, B1_a, xpb) + var B12_a :float = lerp(A1_a, B1_a, xpa) + var A121_a :float = lerp(A11_a, A2_a, x) + var B121_a :float = lerp(B11_a, B2_a, x) + out += [Point.new(A2_t, A2_a), Point.new(A121_t, A121_a), Point.new(A12_t, A12_a), + Point.new(B12_t, B12_a), Point.new(B121_t, B121_a), Point.new(B2_t, B2_a)] + + return out + + +func _emit(delta) -> void: + """ + Adding points to be rendered, called every frame when "emit" is set to True. + """ + var _transform :Transform = _target.global_transform + + var point = Point.new(_transform, lifetime) + if not _A: + _A = point + return + elif not _B: + _A.update(delta, _points) + _B = point + return + + if _B.transform.origin.distance_squared_to(_transform.origin) >= distance*distance: + _A = _B + _B = point + _points += _temp_segment + + _C = point + + _update_points() + _temp_segment = _chaikin(_A, _B, _C) + _render_realtime() + + +func _ready() -> void: + _target = get_parent() + + _wire_mat.flags_unshaded = true + _wire_mat.flags_use_point_size = true + _wire_mat.vertex_color_use_as_albedo = true + _wire_mat.params_line_width = 10.0 + _wire_obj.material_override = _wire_mat + add_child(_wire_obj) + + set_as_toplevel(true) + global_transform = Transform() + + +func _process(delta) -> void: + if emit: + _emit(delta) + + elif always_update: + # This is needed for alignment == view, so it can be updated every frame. + _render_geometry(points) + diff --git a/Game/addons/Trail/trail_3d_v1.gd b/Game/addons/Trail/trail_3d_v1.gd new file mode 100644 index 0000000..312e920 --- /dev/null +++ b/Game/addons/Trail/trail_3d_v1.gd @@ -0,0 +1,194 @@ +#tool +extends ImmediateGeometry + +export(bool) var emit = true +export(float) var max_distance = 0.5 +export(int, 0, 99999) var segments = 20 +export(float) var life_time = 5.0 +export(float, 0, 99999) var base_width = 1.0 +export(bool) var tiled_texture = false +export(int) var tiling = 0 +export(Curve) var width_profile +export(Curve) var width_over_time +export(Gradient) var color_gradient +export(float, 0, 0.5) var smoothing_ratio = 0.2 +export(int, 4) var smoothing_iterations = 1 +export(String, "View", "Motion", "Object") var alignment = "View" +export(String, "Idle", "Fixed") var prcess_mode = "Idle" +export(bool) var show_wireframe = false +export(Color) var wireframe_color = Color(1, 1, 1) + +var target +var path_points = [] + + +class Point: + var position = Vector3() + var normal = Vector3() + var age = 0 + + func _init(position, normal, age): + self.position = position + self.normal = normal + self.age = age + + func update(delta): + age -= delta + +func _ready(): + set_as_toplevel(true) + target = get_parent() + global_transform = Transform() + +func _process(delta): + if emit: + add_point() + update_points() + render() + + +func add_point(): + if target: + var pos = target.global_transform.origin + var normal = target.global_transform.basis.y.normalized() + + if emit: + var points_count = path_points.size() + + if points_count < 1: + var point = Point.new(pos, normal, life_time) + path_points.append(point) + else: + var distance = path_points[points_count-2].position.distance_squared_to(pos) + if distance > (max_distance * max_distance): + var point = Point.new(pos, normal, life_time) + path_points.append(point) + + if points_count > 1: + path_points[points_count-1].position = pos + + +func update_points(): + var delta = 0 + if prcess_mode == "Fixed": + delta = get_physics_process_delta_time() + else: + delta = get_process_delta_time() + + var points_count = path_points.size() + if points_count > segments: + path_points.pop_front() + + for i in range(path_points.size()-1): + path_points[i].update(delta) + if path_points[i].age <= 0: + path_points.remove(i) + + +func render(): + if path_points.size() < 2: + clear() + return + +# path_points = [Vector3(-5, 2, 0),Vector3(-5, 2, 0),Vector3(5, 2, 0)] + var to_be_rendered: Array = [] + for point in path_points: + to_be_rendered.append(point.position) + to_be_rendered = chaikin(to_be_rendered, smoothing_iterations) + + var points_to_render: int = to_be_rendered.size() +# var tiling_factor: float = segments*max_distance/base_width if tiled_texture else 0 + var step: float = 1.0/(points_to_render-1) + var factor: float = 0 + var wire_points: Array = [] + var _u = 0 + + clear() + begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, null) + + for i in range(1, to_be_rendered.size()): + var mapped_index = floor(float(i) / points_to_render * path_points.size()) + var normal = Vector3() + if alignment == "Motion": + normal = path_points[mapped_index].normal + + elif alignment == "View": + var path_direction = (to_be_rendered[i] - to_be_rendered[i-1]).normalized() + var cam_pos = get_viewport().get_camera().get_global_transform().origin + normal = (cam_pos - (to_be_rendered[i] + to_be_rendered[i-1])/2).cross(path_direction).normalized() + + else: + normal = target.get_global_transform().basis.y.normalized() + + var rr = 1-factor + var width = base_width + if width_profile: + width = base_width * width_profile.interpolate(rr) + if width_over_time: + var fact = 1 - path_points[mapped_index].age/life_time + width = width * width_over_time.interpolate(fact) + + var color = Color(1, 1, 1) + if color_gradient: + color = color_gradient.interpolate(rr) + + # --------------------------RENDERING---------------------------- + var p1 = to_be_rendered[i] - normal*width/2 + var p2 = to_be_rendered[i] + normal*width/2 + var u: float = factor + + if tiled_texture: + if tiling: + u *= tiling + else: + _u += (to_be_rendered[i] - to_be_rendered[i-1]).length()/base_width + u = _u + + set_color(color) + set_uv(Vector2(u, 0)) + add_vertex(p1) + set_uv(Vector2(u, 1)) + add_vertex(p2) + factor += step + + wire_points += [p1, p2] + end() + + if show_wireframe: + begin(Mesh.PRIMITIVE_LINE_STRIP, null) + set_color(wireframe_color) + for i in range(1, wire_points.size()-2, 2): + ## i-1, i+1, i, i+2 + add_vertex(wire_points[i-1]) + add_vertex(wire_points[i+1]) + add_vertex(wire_points[i]) + add_vertex(wire_points[i+2]) + end() + + +func chaikin(points, iterations): + """ Chaikin’s Algorithms for curves """ + if points.size() > 1: + if (iterations == 0): + return points + + var result = [points[0]] + for i in range(0, points.size()-1): + result += chaikin_cut(points[i], points[i+1]) + result += [points[points.size()-1]] + + return chaikin(result, iterations-1) + return points + +func chaikin_cut(a, b): + """ Cutting one segment """ + var ratio = clamp(smoothing_ratio, 0, 1) + if (ratio > 0.5): ratio = 1 - ratio; + + # Find point at a given ratio going from A to B + var p1 = a.linear_interpolate(b, ratio) + # Find point at a given ratio going from B to A + var p2 = b.linear_interpolate(a, ratio) + + return [p1, p2] +