Skip to content

Instantly share code, notes, and snippets.

@MarcoLizza
Created October 8, 2018 14:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarcoLizza/95ac9b017f76dcc516d387c3f5157889 to your computer and use it in GitHub Desktop.
Save MarcoLizza/95ac9b017f76dcc516d387c3f5157889 to your computer and use it in GitHub Desktop.
Bézier benchmark for LÖVE
--[[
Copyright (c) 2018 by Marco Lizza (marco.lizza@gmail.com)
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgement in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
]] --
local unpack = unpack or table.unpack
local function compile_bezier_love2d(control_points)
local points = {}
for _, v in ipairs(control_points) do
points[#points + 1] = v[1]
points[#points + 1] = v[2]
end
local bezier = love.math.newBezierCurve(points)
return function(t)
local x, y = bezier:evaluate(t)
return x, y
end
end
-- https://www.math.ubc.ca/~cass/graphics/manual/pdf/a6.pdf
local function compile_bezier_horner(control_points)
local n = #control_points
return function(t)
local s = 1 - t
local C = n * t
local Px, Py = unpack(control_points[1])
for k = 1, n do
local ykx, yky = unpack(control_points[k])
Px = Px * s + C * ykx
Py = Py * s + C * yky
C = C * t * (n - k) / (k + 1)
end
return Px, Py
end
end
-- https://www.gamedev.net/articles/programming/math-and-physics/practical-guide-to-bezier-curves-r3166/
local function compile_bezier_optimized_horner(control_points)
local n = #control_points
return function(t)
local u = 1 - t
local bc = 1
local tn = 1
local x, y = unpack(control_points[1])
x = x * u
y = y * u
for i = 2, n - 1 do
tn = tn * t -- Incremental powers
bc = bc * (n + 1 - i) / i -- Multiplicative formula for binomial calulation
local tn_bc = tn * bc
local px, py = unpack(control_points[i])
x = (x + tn_bc * px) * u
y = (y + tn_bc * py) * u
end
local tn_t = tn * t
local px, py = unpack(control_points[n])
x = x + tn_t * px
y = y + tn_t * py
return x, y
end
end
local function compile_bezier_decasteljau(control_points)
local lerp = function(a, b, t)
return (1 - t) * a + t * b
end
local n = #control_points
local p0, p1, p2, p3 = unpack(control_points)
if n == 4 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
local p3x, p3y = unpack(p3)
return function(t)
local p01x, p01y = lerp(p0x, p1x, t), lerp(p0y, p1y, t)
local p12x, p12y = lerp(p1x, p2x, t), lerp(p1y, p2y, t)
local p23x, p23y = lerp(p2x, p3x, t), lerp(p2y, p3y, t)
local p012x, p012y = lerp(p01x, p12x, t), lerp(p01y, p12y, t)
local p123x, p123y = lerp(p12x, p23x, t), lerp(p12y, p23y, t)
local p0123x, p0123y = lerp(p012x, p123x, t), lerp(p012y, p123y, t)
return p0123x, p0123y
end
elseif n == 3 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
return function(t)
local p01x, p01y = lerp(p0x, p1x, t), lerp(p0y, p1y, t)
local p12x, p12y = lerp(p1x, p2x, t), lerp(p1y, p2y, t)
local p012x, p012y = lerp(p01x, p12x, t), lerp(p01y, p12y, t)
return p012x, p012y
end
elseif n == 2 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
return function(t)
local p01x, p01y = lerp(p0x, p1x, t), lerp(p0y, p1y, t)
return p01x, p01y
end
else
error('Beziér curves are supported from 2nd up to 3rd order.')
end
end
local function compile_bezier_optimized_decasteljau(control_points)
local n = #control_points
local p0, p1, p2, p3 = unpack(control_points)
if n == 4 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
local p3x, p3y = unpack(p3)
return function(t)
local u = 1 - t
local p01x, p01y = u * p0x + t * p1x, u * p0y + t * p1y
local p12x, p12y = u * p1x + t * p2x, u * p1y + t * p2y
local p23x, p23y = u * p2x + t * p3x, u * p2y + t * p3y
local p012x, p012y = u * p01x + t * p12x, u * p01y + t * p12y
local p123x, p123y = u * p12x + t * p23x, u * p12y + t * p23y
local p0123x, p0123y = u * p012x + t * p123x, u * p012y + t * p123y
return p0123x, p0123y
end
elseif n == 3 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
return function(t)
local u = 1 - t
local p01x, p01y = u * p0x + t * p1x, u * p0y + t * p1y
local p12x, p12y = u * p1x + t * p2x, u * p1y + t * p2y
local p012x, p012y = u * p01x + t * p12x, u * p01y + t * p12y
return p012x, p012y
end
elseif n == 2 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
return function(t)
local u = 1 - t
local p01x, p01y = u * p0x + t * p1x, u * p0y + t * p1y
return p01x, p01y
end
else
error('Beziér curves are supported from 2nd up to 3rd order.')
end
end
local function compile_bezier_iterative_decasteljau(control_points)
local n = #control_points
return function(t)
local points = { }
for i = 1, n do
local p = control_points[i]
points[i] = { p[1], p[2] }
end
for step = 1, n - 1 do
for i = 1, n - step do
local p0 = points[i]
local p1 = points[i + 1]
points[i][1] = p0[1] * (1 - t) + p1[1] * t
points[i][2] = p0[2] * (1 - t) + p1[2] * t
end
end
return unpack(points[1])
end
end
local function compile_bezier_bernstein(control_points)
local n = #control_points
local p0, p1, p2, p3 = unpack(control_points)
if n == 4 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
local p3x, p3y = unpack(p3)
return function(t)
local u = 1 - t
local uu = u * u -- Precalculate, to avoid two multiplications.
local tt = t * t
local a = uu * u
local b = 3 * uu * t
local c = 3 * u * tt
local d = t * tt
local x = a * p0x + b * p1x + c * p2x + d * p3x
local y = a * p0y + b * p1y + c * p2y + d * p3y
return x, y
end
elseif n == 3 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
local p2x, p2y = unpack(p2)
return function(t)
local u = 1 - t
local a = u * u
local b = 2 * t * u
local c = t * t
local x = a * p0x + b * p1x + c * p2x
local y = a * p0y + b * p1y + c * p2y
return x, y
end
elseif n == 2 then
local p0x, p0y = unpack(p0)
local p1x, p1y = unpack(p1)
return function(t)
local u = 1 - t
local x = u * p0x + t * p1x
local y = u * p0y + t * p1y
return x, y
end
else
error('Bézier curves are supported from 2nd up to 4th order.')
end
end
local function test_profile()
local COUNT = 10000000
for n = 2, 4 do
local p = { }
for _ = 1, n do
p[#p + 1] = { math.random(), math.random() }
end
local beziers = {
{ id = 'love2d', evaluator = compile_bezier_love2d(p), time = nil },
{ id = 'decasteljau', evaluator = compile_bezier_decasteljau(p), time = nil },
{ id = 'iterative-decasteljau', evaluator = compile_bezier_iterative_decasteljau(p), time = nil },
{ id = 'optimized-decasteljau', evaluator = compile_bezier_optimized_decasteljau(p), time = nil },
{ id = 'bernstein', evaluator = compile_bezier_bernstein(p), time = nil },
{ id = 'horner', evaluator = compile_bezier_horner(p), time = nil },
{ id = 'optimized-horner', evaluator = compile_bezier_optimized_horner(p), time = nil },
}
for _, bezier in ipairs(beziers) do
local s = os.clock()
for j = 0, COUNT do
local t = j / COUNT
bezier.evaluator(t)
end
bezier.time = os.clock() - s
print(string.format('%d order %s took %.3fs', n, bezier.id, bezier.time))
end
end
end
local function test_error()
local function error(ax, ay, bx, by)
local dx, dy = ax - bx, ay - by
return math.sqrt(dx * dx + dy * dy)
end
local points = { { 0, 0 }, { 1, 0 }, { 1, 1 }, { 0, 1 } }
local b_b = compile_bezier_bernstein(points)
local b_d = compile_bezier_love2d(points)
for i = 0, 100 do
local t = i / 100
local ax, ay = b_b(t)
local bx, by = b_d(t)
local e = error(ax, ay, bx, by)
if e > 0.000001 then
print(string.format('%.2f %.2f %.2f %.2f | %f', ax, bx, ay, by, e))
end
end
end
function love.load(_)
test_profile()
test_error()
end
function love.update(_)
end
function love.draw()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment