How to make a procedural 5 point star with terrain

I have no clue why you would want to do this but if you do this is going to be a long tutorial.

To start with you are expected to know a bit of math and a decent amount of scripting knowledge

If you can’t be bothered to read it all here is the file for the code

File.rbxm (4.9 KB)

Big thanks to @EgoMoose, his github repository and scripts on triangles is what makes this thing work.

I also recommend watching this video:

because it goes into what you need to do for a 5 point star.

Originally this was meant to be for parts. Which is possible with the same method and like 10 extra lines but is a bit clunky as it uses unions and negates.

Getting Started

So first things first let’s create a module script. You can put this anywhere but I recommend in a place easy to get to

Alright so now you want to go to the github repository above read it all through then copy the code near the bottom and just tweak it slightly to return the wedges

local Star = {}

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;

function Star.draw3dTriangle(a, b, c, parent)
	local edges = {
		{longest = (c - a), other = (b - a), origin = a},
		{longest = (a - b), other = (c - b), origin = b},
		{longest = (b - c), other = (a - c), origin = c}
	};

	local edge = edges[1];
	for i = 2, #edges do
		if (edges[i].longest.magnitude > edge.longest.magnitude) then
			edge = edges[i];
		end
	end

	local theta = math.acos(edge.longest.unit:Dot(edge.other.unit));
	local w1 = math.cos(theta) * edge.other.magnitude;
	local w2 = edge.longest.magnitude - w1;
	local h = math.sin(theta) * edge.other.magnitude;

	local p1 = edge.origin + edge.other * 0.5;
	local p2 = edge.origin + edge.longest + (edge.other - edge.longest) * 0.5;

	local right = edge.longest:Cross(edge.other).unit;
	local up = right:Cross(edge.longest).unit;
	local back = edge.longest.unit;

	local cf1 = CFrame.new(
		p1.x, p1.y, p1.z,
		-right.x, up.x, back.x,
		-right.y, up.y, back.y,
		-right.z, up.z, back.z
	);

	local cf2 = CFrame.new(
		p2.x, p2.y, p2.z,
		right.x, up.x, -back.x,
		right.y, up.y, -back.y,
		right.z, up.z, -back.z
	);

	-- put it all together by creating the wedges

	

	local wedge1 = wedge:Clone();
	wedge1.Size = Vector3.new(0.2, h, w1);
	wedge1.CFrame = cf1;
	wedge1.Parent = parent;


	local wedge2 = wedge:Clone();
	wedge2.Size = Vector3.new(0.2 , h, w2);
	wedge2.CFrame = cf2;
	wedge2.Parent = parent;

	return wedge1,wedge2
end


return Star


I think that’s all you need to copy and paste from other places.

Now that the long one is done your going to need 2 more short ones.

local function AbsVector(vector)
	return Vector3.new(math.abs(vector.X),math.abs(vector.Y),math.abs(vector.Z))
end

and

The function just makes all the vectors positive.

Drawing the points

So we know a 5 point star has 5 points because it’s in the name and it is symmetrical

Knowing this we can get the exterior angle by doing 360 (degrees in a circle) / 5 --number of sides

This gives us 72

with this we can create a little script which rotates a part around a point from any given angle:

local function rotatePartAroundPoint(part, rotationPoint, angleInDegrees)
	local axis = Vector3.new(0, 1, 0) -- You can change the axis of rotation as needed
	local rotation = CFrame.Angles(0, math.rad(angleInDegrees), 0)
	local translation = CFrame.new(rotationPoint)
	part.CFrame = CFrame.new(translation * rotation * translation:PointToWorldSpace(part.Position))
	part.Position = Vector3.new(part.Position.X,rotationPoint.Y,part.Position.Z)
end

And then create a for i loop where we increment by 72 every time while also storing the parts in a table for later:


local Parts = {}

for i = 1,5 do

		local myPart = Instance.new("Part")
		myPart.Size = Vector3.new(1, 1, 1)
		myPart.Position = Vector3.new(1 , 0, 0)
		myPart.Parent = Model
		myPart.Anchored = true

		angleToRotate += 72
		myPart.Name = i
        table.insert(Parts,myPart)
		rotatePartAroundPoint(myPart, rotationPoint, angleToRotate)
	end	

So now it creates a star shape cool. Time to connect it

Connecting the points

So I think this is probably the simplest bit as it requires the least math but all you need to do is find the distance between the 2 points and get the magnitude. Then you need to multiply that by the lookvector and then make them all positive with the Abs function from earlier before finally multiplying the CFrame of it by the distance / 2.

Got that?

If not here is the script:

function Star.ConnectParts(a,b,name)
	local Distance = (a - b).Magnitude
	
	local Connector = Instance.new("Part")
	local Size = Vector3.new(Distance,Distance,Distance) * Connector.CFrame.LookVector
	Connector.Size = Vector3.new(5,1,5)
	Connector.Position = b
	Connector.CFrame = CFrame.lookAt(b,a)
	Connector.Material = Enum.Material.SmoothPlastic
	Connector.Size = AbsVector(Size)
	Connector.Size = Vector3.new(1,Connector.Size.Y,Connector.Size.Z)
	Connector.Size = Vector3.new(Connector.Size.X,0.5,Connector.Size.Z)
	Connector.CFrame *= CFrame.new(0,0,-Distance/2)
	Connector.Anchored = true
	Connector.Parent = game.Workspace
	Connector.Name = name or "Connector"
	Connector.Transparency = 1
	return Connector
end

Now following the video from earlier we can just look in the parts table and do

local Connector1 = Star.ConnectParts(Parts[4].Position,Parts[1].Position,"Connector1")
local Connector2 = Star.ConnectParts(Parts[1].Position,Parts[3].Position,"Connector2")
local Connector3 = Star.ConnectParts(Parts[2].Position,Parts[4].Position,"Connector3")

Alright cool the parts are connected. Now let’s fill it in

Filling in the star

To do this were going to need to find the intersection of where the points meet and I can’t be bothered to explain this function as it is just a simple ray cast script

function Idk.FindIntersection(Part,lookFor,genpart,parent,workspace)
	
	local Params = RaycastParams.new()
	Params.FilterType = Enum.RaycastFilterType.Include
	Params.FilterDescendantsInstances = {lookFor}
	
	for i = 0,100 do
		local ray = workspace:Raycast((Part.Position - Vector3.new(0,5,0)) + (Part.CFrame.LookVector * i),Vector3.new(0,15,0),Params)
		
		
		
		if ray then
			
			if ray.Instance then
				
				if genpart == true then
					local NewP = Instance.new("Part")
					NewP.Anchored = true
					NewP.Position = ray.Position
					NewP.Size = Vector3.new(1,1,1)
					NewP.Material = Enum.Material.Neon
					NewP.Color = Color3.fromRGB(255,0,0)
					NewP.Parent = parent
					return ray,NewP
				end
				
				return ray
			end
		end
	end
	
	for i = 1,100 do
		local ray = workspace:Raycast((Part.Position - Vector3.new(0,5,0)) + (-Part.CFrame.LookVector * i),Vector3.new(0,15,0),Params)
		
		
		if ray then

			if ray.Instance then
				
				
				if genpart == true then
					local NewP = Instance.new("Part")
					NewP.Anchored = true
					NewP.Position = ray.Position
					NewP.Size = Vector3.new(1,1,1)
					NewP.Material = Enum.Material.Neon
					NewP.Color = Color3.fromRGB(255,0,0)
					NewP.Parent = parent
					return ray,NewP
				end
				
				return ray
			end
		end
	end
end

Cool so now with this we can find the intersections and create parts where they meet:
But before that we need to create the main triangle so we can use that

local ray,P1= Star.FindIntersection(Connector3,{Connector2},true,Model,Model)

local w1,w2 = Star.draw3dTriangle(Parts[2].Position,Parts[5].Position,Parts[3].Position,Model,scale)
	w1.Name = "w1"
	w2.Name = "w2"

--

local ray1,Mid = Star.FindIntersection(Connector1,{w1,w2},true,Model,Model)
local ray2,LeftP = Star.FindIntersection(Connector2,{w1,w2},true,Model,Model)
local ray3,RightP = Star.FindIntersection(Connector3,{w1,w2},true,Model,Model)

local w3,w4 = Idk.draw3dTriangle(Parts[2].Position,NewP.Position,Parts[3].Position,Model,scale)


	w3.Name = "w3"
	w4.Name = "w4"
	

local w5,w6 = Idk.draw3dTriangle(Mid.Position,Parts[4].Position,RightP.Position,Model,scale)
	w5.Name = "w5"
	w6.Name = "w6"

local w7,w8 = Idk.draw3dTriangle(Mid.Position,Parts[1].Position,RightP.Position,Model,scale)
	w7.Name = "w7"
	w8.Name = "w8"

After that the triangle is filled in with parts and may look a bit weird with one side being to big

But that is fine they get turned into air when we make the terrain

Finalizing

for i,v in pairs(StarM:GetChildren()) do
		
		
		
		if v:IsA('BasePart') then
			v.Position = Vector3.new(v.Position.X,Position.Y,v.Position.Z)
		end
		
		if v:IsA('WedgePart') then
			
			if v.Orientation.Z < 0 then
			
				v.Orientation = Vector3.new(0,v.Orientation.Y,-90)
			elseif v.Orientation.Z > 0 then
				v.Orientation = Vector3.new(0,v.Orientation.Y,90)
			end
		end
		
	end

Here I am just adjusting the orientation’s of the star so it is mostly flat

So now all we need to do is create the terrain. We can do this by using the game.Workspace.Terrain:FillWedge() function with the other wedges as our size and position

You can change the material as grass is just a placeholder

game.Workspace.Terrain:FillWedge(w1.CFrame,w1.Size ,Enum.Material.Grass)
game.Workspace.Terrain:FillWedge(w2.CFrame,w2.Size ,Enum.Material.Grass)
game.Workspace.Terrain:FillWedge(w5.CFrame,w5.Size ,Enum.Material.Grass)
game.Workspace.Terrain:FillWedge(w6.CFrame,w6.Size ,Enum.Material.Grass)
game.Workspace.Terrain:FillWedge(w7.CFrame,w7.Size,Enum.Material.Grass)
game.Workspace.Terrain:FillWedge(w8.CFrame,w8.Size ,Enum.Material.Grass)
	

	
game.Workspace.Terrain:FillWedge(w3.CFrame,w3.Size ,Enum.Material.Air)
game.Workspace.Terrain:FillWedge(w4.CFrame,w4.Size ,Enum.Material.Air)
game.Workspace.Terrain:FillWedge(w3.CFrame,w3.Size ,Enum.Material.Air)
game.Workspace.Terrain:FillWedge(w4.CFrame,w4.Size ,Enum.Material.Air)

For some reason you need to make an area air twice otherwise you get left with these tiny bits. But idk maybe that’s just me

This is probably by no means the best, fastest and easiest way. But it works

If we add in a scale property to it and put it in a world model so the parts don’t show then we can scale it up

That would look like as a full module script:

local Star = {}

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;

local function AbsVector(vector)
	return Vector3.new(math.abs(vector.X),math.abs(vector.Y),math.abs(vector.Z))
end



function Star.ConnectParts(a,b,name)
	local Distance = (a - b).Magnitude

	local Connector = Instance.new("Part")
	local Size = Vector3.new(Distance,Distance,Distance) * Connector.CFrame.LookVector
	Connector.Size = Vector3.new(5,1,5)
	Connector.Position = b
	Connector.CFrame = CFrame.lookAt(b,a)
	Connector.Material = Enum.Material.SmoothPlastic
	Connector.Size = AbsVector(Size)
	Connector.Size = Vector3.new(1,Connector.Size.Y,Connector.Size.Z)
	
	Connector.Size = Vector3.new(Connector.Size.X,0.5,Connector.Size.Z)
	Connector.CFrame *= CFrame.new(0,0,-Distance/2)
	Connector.Anchored = true
	Connector.Parent = game.Workspace
	Connector.Name = name or "Connector"
	Connector.Transparency = 1
	return Connector
end

function Star.FindIntersection(Part,lookFor,genpart,parent,workspace,scale)

	local Params = RaycastParams.new()
	Params.FilterType = Enum.RaycastFilterType.Include
	Params.FilterDescendantsInstances = {lookFor}

	for i = 0,100 do
		local ray = workspace:Raycast((Part.Position - Vector3.new(0,5 * scale,0)) + (Part.CFrame.LookVector * i),Vector3.new(0,15 * scale,0),Params)



		



		if ray then

		
			if ray.Instance then
				

				if genpart == true then
					local NewP = Instance.new("Part")
					NewP.Anchored = true
					NewP.Position = ray.Position
					
					NewP.Size = Vector3.new(1,1,1)

					NewP.Material = Enum.Material.Neon
					NewP.Color = Color3.fromRGB(255,0,0)
					NewP.Parent = parent
					return ray,NewP
				end

				return ray
			end
		end
	end

	for i = 1,100 do
		local ray = workspace:Raycast((Part.Position - Vector3.new(0,5 * scale,0)) + (-Part.CFrame.LookVector * i),Vector3.new(0,15 * scale,0),Params)


		if ray then

			
			if ray.Instance then
				
				if genpart == true then
					local NewP = Instance.new("Part")
					NewP.Anchored = true
					NewP.Position = ray.Position
					
					NewP.Size = Vector3.new(1,1,1)
	
					NewP.Material = Enum.Material.Neon
					NewP.Color = Color3.fromRGB(255,0,0)
					NewP.Parent = parent
					return ray,NewP
				end

				return ray
			end
		end
	end
end


function Star.draw3dTriangle(a, b, c, parent,scale)
	local edges = {
		{longest = (c - a), other = (b - a), origin = a},
		{longest = (a - b), other = (c - b), origin = b},
		{longest = (b - c), other = (a - c), origin = c}
	};

	local edge = edges[1];
	for i = 2, #edges do
		if (edges[i].longest.magnitude > edge.longest.magnitude) then
			edge = edges[i];
		end
	end

	local theta = math.acos(edge.longest.unit:Dot(edge.other.unit));
	local w1 = math.cos(theta) * edge.other.magnitude;
	local w2 = edge.longest.magnitude - w1;
	local h = math.sin(theta) * edge.other.magnitude;

	local p1 = edge.origin + edge.other * 0.5;
	local p2 = edge.origin + edge.longest + (edge.other - edge.longest) * 0.5;

	local right = edge.longest:Cross(edge.other).unit;
	local up = right:Cross(edge.longest).unit;
	local back = edge.longest.unit;

	local cf1 = CFrame.new(
		p1.x, p1.y, p1.z,
		-right.x, up.x, back.x,
		-right.y, up.y, back.y,
		-right.z, up.z, back.z
	);

	local cf2 = CFrame.new(
		p2.x, p2.y, p2.z,
		right.x, up.x, -back.x,
		right.y, up.y, -back.y,
		right.z, up.z, -back.z
	);

	-- put it all together by creating the wedges

	scale = scale / 2.5

	local wedge1 = wedge:Clone();
	wedge1.Size = Vector3.new(0.2 * scale, h, w1);
	wedge1.CFrame = cf1;
	wedge1.Parent = parent;


	local wedge2 = wedge:Clone();
	wedge2.Size = Vector3.new(0.2 * scale, h, w2);
	wedge2.CFrame = cf2;
	wedge2.Parent = parent;

	return wedge1,wedge2
end

local function rotatePartAroundPoint(part, rotationPoint, angleInDegrees)
	local axis = Vector3.new(0, 1, 0) -- You can change the axis of rotation as needed
	local rotation = CFrame.Angles(0, math.rad(angleInDegrees), 0)
	local translation = CFrame.new(rotationPoint)
	part.CFrame = CFrame.new(translation * rotation * translation:PointToWorldSpace(part.Position))
	part.Position = Vector3.new(part.Position.X,rotationPoint.Y,part.Position.Z)
end


function Star.Create(Parent,Position,scale)
	-- Function to rotate a part around a specified point
	local Model = Instance.new("WorldModel")
	Model.Parent = Parent


	
	local rotationPoint = Position
	local angleToRotate = 0


	local Parts = {}

	for i = 1,5 do

		local myPart = Instance.new("Part")
		myPart.Size = Vector3.new(1, 1, 1)
		myPart.Position = Vector3.new(1 * scale, 0, 0)
		myPart.Parent = Model
		myPart.Anchored = true

		angleToRotate += 72
		table.insert(Parts,myPart)
		myPart.Name = i
		
		rotatePartAroundPoint(myPart, rotationPoint, angleToRotate)
	end	



	local Connector1 = Star.ConnectParts(Parts[4].Position,Parts[1].Position,"Connector1")
	local Connector2 = Star.ConnectParts(Parts[1].Position,Parts[3].Position,"Connector2")
	local Connector3 = Star.ConnectParts(Parts[2].Position,Parts[4].Position,"Connector3")

	Connector1.Parent = Model
	Connector2.Parent = Model
	Connector3.Parent = Model


	local ray,NewP = Star.FindIntersection(Connector3,{Connector2},true,Model,Model,scale)

	local w1,w2 = Star.draw3dTriangle(Parts[2].Position,Parts[5].Position,Parts[3].Position,Model,scale)
	w1.Name = "w1"
	w2.Name = "w2"


	local ray1,Mid = Star.FindIntersection(Connector1,{w1,w2},true,Model,Model,scale)
	local ray2,LeftP = Star.FindIntersection(Connector2,{w1,w2},true,Model,Model,scale)
	local ray3,RightP = Star.FindIntersection(Connector3,{w1,w2},true,Model,Model,scale)

	local w3,w4 = Star.draw3dTriangle(Parts[2].Position,NewP.Position,Parts[3].Position,Model,scale)


	w3.Name = "w3"
	w4.Name = "w4"


	local w5,w6 = Star.draw3dTriangle(Mid.Position,Parts[4].Position,RightP.Position,Model,scale)
	w5.Name = "w5"
	w6.Name = "w6"

	local w7,w8 = Star.draw3dTriangle(Mid.Position,Parts[1].Position,RightP.Position,Model,scale)
	w7.Name = "w7"
	w8.Name = "w8"


	for i,v in pairs(Model:GetChildren()) do



		if v:IsA('BasePart') then
			v.Position = Vector3.new(v.Position.X,Position.Y,v.Position.Z)
		end

		if v:IsA('WedgePart') then

			if v.Orientation.Z < 0 then

				v.Orientation = Vector3.new(0,v.Orientation.Y,-90)
			elseif v.Orientation.Z > 0 then
				v.Orientation = Vector3.new(0,v.Orientation.Y,90)
			end
		end

	end
	Model:PivotTo(CFrame.new(Position) * Model:GetPivot().Rotation)

	

	game.Workspace.Terrain:FillWedge(w1.CFrame,w1.Size ,Enum.Material.Grass)
	game.Workspace.Terrain:FillWedge(w2.CFrame,w2.Size ,Enum.Material.Grass)
	game.Workspace.Terrain:FillWedge(w5.CFrame,w5.Size ,Enum.Material.Grass)
	game.Workspace.Terrain:FillWedge(w6.CFrame,w6.Size ,Enum.Material.Grass)
	game.Workspace.Terrain:FillWedge(w7.CFrame,w7.Size,Enum.Material.Grass)
	game.Workspace.Terrain:FillWedge(w8.CFrame,w8.Size ,Enum.Material.Grass)


	game.Workspace.Terrain:FillWedge(w3.CFrame,w3.Size ,Enum.Material.Air)
	game.Workspace.Terrain:FillWedge(w4.CFrame,w4.Size ,Enum.Material.Air)
	game.Workspace.Terrain:FillWedge(w3.CFrame,w3.Size ,Enum.Material.Air)
	game.Workspace.Terrain:FillWedge(w4.CFrame,w4.Size ,Enum.Material.Air)


end


return Star

Um thanks for reading I hope this was ok. I am not very good at explaining really so I hope you understood it. If not I will try to answer any questions and yeh. Enjoy :slight_smile:

2 Likes