@gianmichele I’ve tested my code with vertices for a concave shape. it’s not quite the shape I expected but it is concave haha. I’m still working on getting it to work with control_points that can be moved then it can be regenerated on the fly but with fixed vertices I have this
local spline_geom = require(“game.spline_geometry”)
function regenerate_mesh(self)
– Generate spline from current control points
local spline = spline_geom:generateSpline(self.control_points)
-- Triangulate the spline polygon
local triangles = spline_geom.triangulatePolygon(spline)
-- Flatten triangles into vertices array (x,y,z)
local triangle_vertices = {}
for _, tri in ipairs(triangles) do
for i = 1, 6, 2 do
table.insert(triangle_vertices, tri[i]) -- x
table.insert(triangle_vertices, tri[i+1]) -- y
table.insert(triangle_vertices, 0) -- z
end
end
-- Create buffer and upload data to the mesh
local buf = buffer.create(#triangle_vertices / 3, {
{ name = hash("position"), type = buffer.VALUE_TYPE_FLOAT32, count = 3 }
})
local positions = buffer.get_stream(buf, "position")
for i = 1, #triangle_vertices do
positions[i] = triangle_vertices[i]
end
local res = go.get("#mesh", "vertices")
resource.set_buffer(res, buf)
print("Mesh regenerated with " .. #triangles .. " triangles")
end
function init(self)
msg.post(“.”, “acquire_input_focus”)
– { {0, 0}, {1, 0}, {1, 1}, {0, 1} } curvy cube
self.control_points = { {-0.5, 1}, {-0.25, 0.5}, {-0.5, -1}, {0.25, 0.5} }
regenerate_mesh(self)
– local pos = go.get_position()
– msg.post(“spawner#spawner”, “create_control_point”, {x = pos.x, y = pos.y})
– msg.post(“spawner#spawner”, “create_control_point”, {x = pos.x+30, y = pos.y+30})
end
and the relevant spline_geometry.lua functions are
– ✳ Generates a smooth curve that passes through p1 and p2.
– ✳ This uses a cubic Hermite polynomial interpolation with 4 points.
– ✳ t ∈ [0, 1] defines the fraction along the segment between p1 and p2.
local function catmullRom(p0, p1, p2, p3, t)
local t2, t3 = t * t, t * t * t – ✳ Precompute powers for efficiency
– ✳ The formula below is derived from the Catmull–Rom spline equation:
– ✳ P(t) = 0.5 * ((2P1) + (-P0 + P2)t + (2P0 - 5P1 + 4P2 - P3)t² + (-P0 + 3P1 - 3P2 + P3)t³)
– ✳ Each coordinate is computed independently.
local x = 0.5 * ((2 * p1[1])
+ (-p0[1] + p2[1]) * t
+ (2p0[1] - 5p1[1] + 4p2[1] - p3[1]) * t2
+ (-p0[1] + 3p1[1] - 3p2[1] + p3[1]) * t3)
local y = 0.5 * ((2 * p1[2])
+ (-p0[2] + p2[2]) * t
+ (2p0[2] - 5p1[2] + 4p2[2] - p3[2]) * t2
+ (-p0[2] + 3p1[2] - 3p2[2] + p3[2]) * t3)
return { x, y }
end
function spline_geom:generateSpline(points)
local spline = {}
local n = #points
if n < 3 then return spline end – ✳ Need at least 3 points to define a curve
for i = 1, n do
-- ✳ Wrap indices (mod n) to form a closed loop.
local p0 = points[((i-2)%n)+1]
local p1 = points[i]
local p2 = points[(i%n)+1]
local p3 = points[((i+1)%n)+1]
-- ✳ Sample the curve between p1 and p2 in samplesPerSegment increments.
for s = 0, self.samplesPerSegment-1 do
table.insert(spline, catmullRom(p0,p1,p2,p3,s/self.samplesPerSegment))
end
end
return spline
end
function spline_geom.ensureCounterClockwise(polygon)
– Calculate signed area to determine winding
local area = 0
local n = #polygon
for i = 1, n do
local j = i % n + 1
area = area + (polygon[j][1] - polygon[i][1]) * (polygon[j][2] + polygon[i][2])
end end
-- If area is positive, it's clockwise, so reverse
if area > 0 then
local reversed = {}
for i = n, 1, -1 do
table.insert(reversed, polygon[i])
end
return reversed
end
return polygon
function spline_geom.triangulatePolygon(polygon)
polygon = spline_geom.ensureCounterClockwise(polygon)
– polygon: { {x1,y1}, {x2,y2}, … }
local n = #polygon
if n < 3 then return {} end
if n == 3 then
return { { polygon[1][1], polygon[1][2],
polygon[2][1], polygon[2][2],
polygon[3][1], polygon[3][2] } }
end
-- Copy
vertices and initialize next/prev indices
local vertices = {}
for i, p in ipairs(polygon) do
vertices[i] = {x = p[1], y = p[2], i = i}
end
local function isCCW(a,b,c) -- counter clockwise
return ((b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x)) >= 0
end
local function isEar(a,b,c, others)
if not isCCW(a,b,c) then return false end
for _, p in ipairs(others) do
if p ~= a and p ~= b and p ~= c then
if pointInTriangle(p.x,p.y, a.x,a.y, b.x,b.y, c.x,c.y) then
return false
end
end
end
return true
end
local result = {}
local V = {}
for i = 1, #vertices do
V[i] = vertices[i]
end
while #V > 3 do
local earFound = false
for i=1,#V do
local prev = V[(i-2)%#V+1]
local curr = V[i]
local next = V[i%#V+1]
if isEar(prev,curr,next,V) then
table.insert(result, {prev.x,prev.y, curr.x,curr.y, next.x,next.y})
table.remove(V,i)
earFound = true
break
end
end
if not earFound then
error("Cannot triangulate polygon: possible self-intersection or degenerate polygon")
end
end
-- Last remaining triangle
table.insert(result, {V[1].x,V[1].y, V[2].x,V[2].y, V[3].x,V[3].y})
return result
end
local function pointInTriangle(px, py, x1, y1, x2, y2, x3, y3)
local d1 = (px - x2)(y1 - y2) - (py - y2)(x1 - x2)
local d2 = (px - x3)(y2 - y3) - (py - y3)(x2 - x3)
local d3 = (px - x1)(y3 - y1) - (py - y1)(x3 - x1)
local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
end
that should get you started with creating convex meshes. tbh you probably dont need the triangulation if you’re only looking to draw an outline since defold seems to have a line primitive you can use for the mesh - you might just be able to use the splinepoints from the generatespline function but I haven’t tried that. the control points that generate me a concave mesh are self.control_points = { {-0.5, 1}, {-0.25, 0.5}, {-0.5, -1}, {0.25, 0.5} } the pointInTriangle and triangulation functions are there if you wanna play with em though 
this just requires an initial buffer with a position stream to be set on the mesh.