Correctly Create Bezier Curve

Hello good. I’m using Editable Mesh to make a road system that I had already designed months ago, but due to the changes in the API it was obsolete for a long time, I already solved it, and I saw that my curves were very bad, until I discovered the stock of the Bezier curves, then, since I don’t know how to calculate them or how to put them into practice, I took help from ChatGPT 4; And, I have a very good result, but not the best, I was wondering if anyone knows how to solve this or how I can improve.

Code:

local AssetService = game:GetService("AssetService")

function bezier(t, P0, P1, P2, P3)
	local u = 1 - t
	local tt = t * t
	local uu = u * u
	local p = (uu * u * P0) + (3 * uu * t * P1) + (3 * u * tt * P2) + (tt * t * P3)
	return p
end

function rotateUV(uv, rotationAngle, scale)
	local radians = math.rad(rotationAngle)

	local cosAngle = math.cos(radians)
	local sinAngle = math.sin(radians)

	local scaledX = uv.X * scale.X
	local scaledY = uv.Y * scale.Y

	local rotatedX = cosAngle * (scaledX - 0.5) - sinAngle * (scaledY - 0.5) + 0.5
	local rotatedY = sinAngle * (scaledX - 0.5) + cosAngle * (scaledY - 0.5) + 0.5

	return Vector2.new(rotatedX, rotatedY)
end

function generateBezierRoadWithUV(partA:BasePart, partB:BasePart, segments, textureRotation, scale)
	local editableMesh = AssetService:CreateEditableMesh({FixedSize = false})
	local P0 = partA.Position
	local P3 = partB.Position

	local directionA = partA.CFrame.LookVector
	local directionB = partB.CFrame.LookVector

	local distance = (P3 - P0).Magnitude
	local controlOffset = distance * 1

	local P1 = P0 + directionA * controlOffset + partA.CFrame.UpVector * (partA.Size.Y / 2)
	local P2 = P3 - directionB * controlOffset + partB.CFrame.UpVector * (partB.Size.Y / 2)

	local roadWidth = partA.Size.X
	local points = {}

	for i = 0, segments do
		local t = i / segments
		table.insert(points, bezier(t, P0, P1, P2, P3))
	end

	local triangles = {}
	local uvs = {}

	for i = 1, segments do
		local p1 = points[i]
		local p2 = points[i + 1]

		local direction = (p2 - p1).Unit
		local perpendicular = Vector3.new(-direction.Z, 0, direction.X)
		local right1 = p1 + perpendicular * (roadWidth / 2)
		local left1 = p1 - perpendicular * (roadWidth / 2)
		local right2 = p2 + perpendicular * (roadWidth / 2)
		local left2 = p2 - perpendicular * (roadWidth / 2)


		table.insert(triangles, {left1, right1, left2})
		table.insert(triangles, {right1, right2, left2})

		local u1 = (i - 1) / segments
		local u2 = i / segments

		local uv1 = Vector2.new(0, u1)
		local uv2 = Vector2.new(1, u1)
		local uv3 = Vector2.new(0, u2)
		local uv4 = Vector2.new(1, u2)

		uv1 = rotateUV(uv1, textureRotation, scale)
		uv2 = rotateUV(uv2, textureRotation, scale)
		uv3 = rotateUV(uv3, textureRotation, scale)
		uv4 = rotateUV(uv4, textureRotation, scale)

		table.insert(uvs, {uv1, uv2, uv3})
		table.insert(uvs, {uv2, uv4, uv3})
	end

	for faceIndex, tri in pairs(triangles) do
		local v1 = editableMesh:AddVertex(tri[1])
		local v2 = editableMesh:AddVertex(tri[2])
		local v3 = editableMesh:AddVertex(tri[3])
		local faceId = editableMesh:AddTriangle(v1, v2, v3)
		local facesUvs = editableMesh:GetFaceUVs(faceId)
		local uv = uvs[faceIndex]
		for i, uvPoint in pairs(uv) do
			editableMesh:SetUV(facesUvs[i], uvPoint)
		end
	end

	return editableMesh
end

local Hijos = workspace:WaitForChild("Folder"):GetChildren()--Nodes
local segments = 50
local textureRotation = 90
local textureScale = Vector2.new(1, 10)
repeat
	Hijos = workspace:WaitForChild("Folder"):GetChildren()
	task.wait(0)
until #Hijos >= 3

for i, Parts in pairs(Hijos) do
	local PartA = Parts
	local PartB = Hijos[i + 1]
	if PartB then
		local editableMesh = generateBezierRoadWithUV(PartA, PartB, segments, textureRotation, textureScale)
		local finalMesh = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
		finalMesh.PivotOffset = CFrame.new(finalMesh.CenterOfMass)
		finalMesh.Anchored = true
		finalMesh.Parent = workspace
		local Road = game.ReplicatedStorage.SurfaceAppearance:Clone()
		Road.Parent = finalMesh
		PartA.Transparency = 1
		PartB.Transparency = 1
	end
end

Results:


This post by Egomoose shows how to make bezier curves with beams. I think you can apply the formula used with parts as well

1 Like

The complicated thing is that I am managing a Mesh, not pieces, I am creating points (vertices) to later be connected. I read a little, and I don’t think it gives me an answer. Thanks, I’ll keep waiting.

Your problem is here. Each segment of road should be a quad that shares its trailing edge with the previous quad, and its leading edge with the next one. But that’s not what your code is doing, your code makes each segment of the road a rectangle, whose trailing and leading edges are parallel and set by your perpendicular. This is not what you want, you should have two perpendiculars, one each for p1 and p2, and they will not be parallel unless the road is straight in that area.

For example, you could make left1 and right1 use a perpendular that you compute from (p2-p1).Unit rotated, just as you are doing, but left2 and right2 could use a different perpendicular derived from (p3-p2).Unit. You still need to special case the last edge, since there is no sample point points[i+2] right now. Just extrapolate one more data point that’s in the same direction as the last segment.

Alternatively, you could make the whole thing symmetric front to back by making the perpendicular at each point based on the direction from the previous point to the next point (central difference), rather than the current point to the next point as you’re doing now (called a forward difference). With a central difference, your direction and perp would come from p[i+1] - p[i-1] for each point, rather than from p[i+1] - p[i]. With this arrangement, the first and last segments are both special cased symmetrically, using extrapolated points just past the first and last point respectively.

Lastly, note that because adjacent quads share an edge, you don’t need to redundantly compute both perpendiculars for every quad, you can just re-use the perpendicular of the previous quad as the start of the current one. Otherwise you’re computing every pair of points twice (except for the first and last edges of course).

1 Like

Better results, but it still doesn’t work for me, or I didn’t understand you correctly, or there is still work ahead. Thanks for this, I really needed answers.
New Code:

local AssetService = game:GetService("AssetService")
function bezier(t, P0, P1, P2, P3)
	local u = 1 - t
	local tt = t * t
	local uu = u * u
	local p = (uu * u * P0) + (3 * uu * t * P1) + (3 * u * tt * P2) + (tt * t * P3)
	return p
end
function rotateUV(uv, rotationAngle, scale)
	local radians = math.rad(rotationAngle)

	local cosAngle = math.cos(radians)
	local sinAngle = math.sin(radians)

	local scaledX = uv.X * scale.X
	local scaledY = uv.Y * scale.Y

	local rotatedX = cosAngle * (scaledX - 0.5) - sinAngle * (scaledY - 0.5) + 0.5
	local rotatedY = sinAngle * (scaledX - 0.5) + cosAngle * (scaledY - 0.5) + 0.5

	return Vector2.new(rotatedX, rotatedY)
end
function generateBezierRoadWithUV(partA:BasePart, partB:BasePart, segments, textureRotation, scale)
	local editableMesh = AssetService:CreateEditableMesh({FixedSize = false})
	local P0 = partA.Position
	local P3 = partB.Position

	local directionA = partA.CFrame.LookVector
	local directionB = partB.CFrame.LookVector

	local distance = (P3 - P0).Magnitude
	local controlOffset = distance * 0.3

	local P1 = P0 + directionA * controlOffset + partA.CFrame.UpVector * (partA.Size.Y / 2)
	local P2 = P3 - directionB * controlOffset + partB.CFrame.UpVector * (partB.Size.Y / 2)

	local roadWidth = partA.Size.X
	local points = {}

	for i = 0, segments do
		local t = i / segments
		table.insert(points, bezier(t, P0, P1, P2, P3))
	end

	local triangles = {}
	local uvs = {}

	for i = 2, segments do
		local p1 = points[i+1]
		local p2 = points[i-1]

		local direction = (p2 - p1).Unit
		local perpendicular = Vector3.new(-direction.Z, 0, direction.X)
		local right1 = p1 + perpendicular * (roadWidth / 2)
		local left1 = p1 - perpendicular * (roadWidth / 2)
		local right2 = p2 + perpendicular * (roadWidth / 2)
		local left2 = p2 - perpendicular * (roadWidth / 2)

		table.insert(triangles, {left1, right1, left2})
		table.insert(triangles, {right1, right2, left2})

		local u1 = (i - 1) / segments
		local u2 = i / segments

		local uv1 = Vector2.new(0, u1)
		local uv2 = Vector2.new(1, u1)
		local uv3 = Vector2.new(0, u2)
		local uv4 = Vector2.new(1, u2)

		uv1 = rotateUV(uv1, textureRotation, scale)
		uv2 = rotateUV(uv2, textureRotation, scale)
		uv3 = rotateUV(uv3, textureRotation, scale)
		uv4 = rotateUV(uv4, textureRotation, scale)

		table.insert(uvs, {uv1, uv2, uv3})
		table.insert(uvs, {uv2, uv4, uv3})
	end

	for faceIndex, tri in pairs(triangles) do
		local v1 = editableMesh:AddVertex(tri[1])
		local v2 = editableMesh:AddVertex(tri[2])
		local v3 = editableMesh:AddVertex(tri[3])
		local faceId = editableMesh:AddTriangle(v1, v2, v3)
		local facesUvs = editableMesh:GetFaceUVs(faceId)
		local uv = uvs[faceIndex]
		for i, uvPoint in pairs(uv) do
			editableMesh:SetUV(facesUvs[i], uvPoint)
		end
	end
	return editableMesh
end

local Hijos = workspace:WaitForChild("Roads"):GetChildren()
local segments = 50
local textureRotation = 90
local textureScale = Vector2.new(1, 10)

for i, Parts in pairs(Hijos) do
	local PartA = Parts
	local PartB = Hijos[i + 1]
	if PartB then
		local editableMesh = generateBezierRoadWithUV(PartA, PartB, segments, textureRotation, textureScale)
		local finalMesh = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
		finalMesh.PivotOffset = CFrame.new(finalMesh.CenterOfMass)
		finalMesh.Anchored = true
		finalMesh.Parent = workspace
		local Road = game.ReplicatedStorage.SurfaceAppearance:Clone()
		Road.Parent = finalMesh
	end
end

Results:

Now the problem is that there are triangles that overlap others, generating flickering.


See how it looks when you give it transparency

Perhaps try the merge duplicate method for editable meshes?

Excuse my ignorance, but I have no idea what it is.

This thing? I’ve only ever worked with editable meshes once and was to convert each vertex to a sphere and have parts connect between each sphere so I don’t really know how to use it

1 Like

For a moment I thought it worked for me, but, no… Thanks anyway, this way I learn more about this API:

Do me a favor, send a screenshot without the texture, it may be something to do wih the texture stuff over the mesh.

1 Like

No, because look, I already got the result, Well, halfway.
But it doesn’t work well for aggressive cornering.

New Code:

local AssetService = game:GetService("AssetService")
function bezier(t, P0, P1, P2, P3)
	local u = 1 - t
	local tt = t * t
	local uu = u * u
	local p = (uu * u * P0) + (3 * uu * t * P1) + (3 * u * tt * P2) + (tt * t * P3)
	return p
end
function rotateUV(uv, rotationAngle, scale)
	local radians = math.rad(rotationAngle)

	local cosAngle = math.cos(radians)
	local sinAngle = math.sin(radians)

	local scaledX = uv.X * scale.X
	local scaledY = uv.Y * scale.Y

	local rotatedX = cosAngle * (scaledX - 0.5) - sinAngle * (scaledY - 0.5) + 0.5
	local rotatedY = sinAngle * (scaledX - 0.5) + cosAngle * (scaledY - 0.5) + 0.5

	return Vector2.new(rotatedX, rotatedY)
end
function generateBezierRoadWithUV(partA:BasePart, partB:BasePart, segments, textureRotation, scale)
	local editableMesh = AssetService:CreateEditableMesh({FixedSize = false})
	local P0 = partA.Position
	local P3 = partB.Position

	local directionA = partA.CFrame.LookVector
	local directionB = partB.CFrame.LookVector

	local distance = (P3 - P0).Magnitude
	local controlOffset = distance * 0.3

	local P1 = P0 + directionA * controlOffset + partA.CFrame.UpVector * (partA.Size.Y / 2)
	local P2 = P3 - directionB * controlOffset + partB.CFrame.UpVector * (partB.Size.Y / 2)

	local roadWidth = partA.Size.X
	local points = {}

	for i = 0, segments do
		local t = i / segments
		table.insert(points, bezier(t, P0, P1, P2, P3))
	end

	local triangles = {}
	local uvs = {}

	for i = 2, segments do
		local p1 = points[i+1]
		local p2 = points[i-1]

		local direction = (p2 - p1).Unit
		local perpendicular = Vector3.new(-direction.Z, 0, direction.X)
		local right1 = p1 + perpendicular * (roadWidth / 2)
		local left1 = p1 - perpendicular * (roadWidth / 2)
		local right2 = p2 + perpendicular * (roadWidth / 2)
		local left2 = p2 - perpendicular * (roadWidth / 2)

		table.insert(triangles, {left1, right1, left2})
		table.insert(triangles, {right1, right2, left2})

		local u1 = (i - 1) / segments
		local u2 = i / segments

		local uv1 = Vector2.new(0, u1)
		local uv2 = Vector2.new(1, u1)
		local uv3 = Vector2.new(0, u2)
		local uv4 = Vector2.new(1, u2)

		uv1 = rotateUV(uv1, textureRotation, scale)
		uv2 = rotateUV(uv2, textureRotation, scale)
		uv3 = rotateUV(uv3, textureRotation, scale)
		uv4 = rotateUV(uv4, textureRotation, scale)

		table.insert(uvs, {uv1, uv2, uv3})
		table.insert(uvs, {uv2, uv4, uv3})
	end

	for faceIndex, tri in pairs(triangles) do
		local v1 = editableMesh:AddVertex(tri[1])
		local v2 = editableMesh:AddVertex(tri[2])
		local v3 = editableMesh:AddVertex(tri[3])
		local faceId = editableMesh:AddTriangle(v1, v2, v3)
		local facesUvs = editableMesh:GetFaceUVs(faceId)
		local uv = uvs[faceIndex]
		for i, uvPoint in pairs(uv) do
			editableMesh:SetUV(facesUvs[i], uvPoint)
		end
	end
	return editableMesh
end

local Hijos = workspace:WaitForChild("Roads"):GetChildren()
local segments = 50
local textureRotation = 90
local textureScale = Vector2.new(1, 10)

for i, Parts in pairs(Hijos) do
	local PartA = Parts
	local PartB = Hijos[i + 1]
	if PartB then
		local editableMesh = generateBezierRoadWithUV(PartA, PartB, segments, textureRotation, textureScale)
		local finalMesh = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
		finalMesh.PivotOffset = CFrame.new(finalMesh.CenterOfMass)
		--editableMesh:MergeVertices(5)
		finalMesh.Anchored = true
		finalMesh.Parent = workspace
		local Road = game.ReplicatedStorage.SurfaceAppearance:Clone()
		Road.Parent = finalMesh
	end
end

Just comment out the code that does the surface apperance so you get the same color and material as a boring part I wanna see how it looks, or try using material service over surface apperance. I think surface apperance is made specificly for a mesh its designed for not any random mesh you choose.

Unfortunately the SurfaceAppearance and TextureID are the only ones that are affected by the UV maps of the vertices. Also, TextureID allows the mesh to blink (API issues)

So would this be why the road looks funny? Is the mesh itself alright looking without the textures?

Yes, but it falsely hides in certain points of the camera, I don’t know why it happens, but it happens to many people.
Anyway… I don’t know what to do anymore, I still don’t know what calculations are failing me

I’m not sure then, sorry I couldn’t help.

1 Like