Icosphere / Icosahedron Generator

Although resources that can help you create your own icosphere generator already exist, a finished product is not publicly available (that I’m aware of). This is my attempt at providing one.

Examples

Icosahedron

Icospheres (1-4 subdivisions)

Halved icosphere / Dome

Various effects

The code in action (2 subdivisions)

gif

The code

SHOW CODE
-- golden ratio
local TAU = 0.5 + math.sqrt(5) / 2

-- (modified) 3D Triangles by EgoMoose: https://github.com/EgoMoose/Articles/blob/master/3d%20triangles/3D%20triangles.md
local function createTriangle(A: Vector3, B: Vector3, C: Vector3, thickness: number): UnionOperation
	
	local AB, AC, BC = B - A, C - A, C - B
	
	local XVector = AC:Cross(AB).Unit
	local YVector = BC:Cross(XVector).Unit
	local ZVector = BC.Unit
	
	local height = math.abs(AB:Dot(YVector))
	
	local WedgePart1 = Instance.new('WedgePart')
	WedgePart1.BottomSurface = Enum.SurfaceType.Smooth
	WedgePart1.Size = Vector3.new(thickness, height, math.abs(AB:Dot(ZVector)))
	WedgePart1.CFrame = CFrame.fromMatrix((A + B) / 2, XVector, YVector, ZVector)
	
	local WedgePart2 = Instance.new('WedgePart')
	WedgePart2.BottomSurface = Enum.SurfaceType.Smooth
	WedgePart2.Size = Vector3.new(thickness, height, math.abs(AC:Dot(ZVector)))
	WedgePart2.CFrame = CFrame.fromMatrix((A + C) / 2, -XVector, YVector, -ZVector)
	
	WedgePart1.Parent = game
	local Triangle = WedgePart1:UnionAsync({WedgePart2})
	WedgePart1:Destroy(); WedgePart2:Destroy()
	return Triangle
	
end

return function (radius: number, subdivisions: number?, properties: {any}?, thickness: number?, offset: number?, halve: boolean?): nil
	
	-- set default values
	subdivisions, properties, thickness, offset = subdivisions or 0, properties or { Anchored = true, UsePartColor = true }, thickness or 0.001, offset or 0
	
	-- icosahedron data
	local vertices = {{-1, TAU, 0}, {1, TAU, 0}, {-1, -TAU, 0}, {1, -TAU, 0}, {0, -1, TAU}, {0, 1, TAU}, {0, -1, -TAU}, {0, 1, -TAU}, {TAU, 0, -1}, {TAU, 0, 1}, {-TAU, 0, -1}, {-TAU, 0, 1}}
	local faces = {{1, 12, 6}, {1, 6, 2}, {1, 2, 8}, {1, 8, 11}, {1, 11, 12}, {2, 6, 10}, {6, 12, 5}, {12, 11, 3}, {11, 8, 7}, {8, 2, 9}, {4, 10, 5}, {4, 5, 3}, {4, 3, 7}, {4, 7, 9}, {4, 9, 10}, {5, 10, 6}, {3, 5, 12}, {7, 3, 11}, {9, 7, 8}, {10, 9, 2}}
	-- convert vertices tables to Vector3s, and point indices of each face to those Vector3s
	for i, vertex in vertices do vertices[i] = Vector3.new(vertex[1], vertex[2], vertex[3]) end
	for t, triangle in faces do for p, pointIndex in triangle do faces[t][p] = vertices[pointIndex] end end
	
	for _ = 1, subdivisions do
		-- split every face into 4 faces by adding vertices in the middle of its edges
		for _ = 1, #faces do
			local vertices = table.remove(faces, 1)
			local AB = (vertices[1] + vertices[2]) / 2
			local BC = (vertices[2] + vertices[3]) / 2
			local CA = (vertices[3] + vertices[1]) / 2
			table.insert(faces, {vertices[1], AB, CA})
			table.insert(faces, {vertices[2], BC, AB})
			table.insert(faces, {vertices[3], CA, BC})
			table.insert(faces, {AB, BC, CA})
		end
	end
	
	local Model = Instance.new('Model', workspace)
	local Offset = CFrame.new(thickness / 2 - offset, 0, 0)
	
	-- create the triangles, apply thickness, offset and properties
	for i, face in ipairs(faces) do
		-- move vertices to radius distance from the center, there's no need to do it before this point
		local Triangle = createTriangle(face[1].Unit * radius, face[2].Unit * radius, face[3].Unit * radius, thickness)
		Triangle.CFrame *= Offset
		for property, value in properties do Triangle[property] = value end
		Triangle.Parent = Model
		Model.Name = string.format('Icosphere_%i %i%%', subdivisions, i / #faces * 100)
	end
	
	if halve then
		-- rotate sphere so that one of the halving lines is on the XZ plane
		Model.WorldPivot = CFrame.Angles(0, 0, -0.5535746812820435)
		Model:PivotTo(CFrame.new())
		for _, Triangle in Model:GetChildren() do
			-- move pivot to the outward face of the triangle (otherwise this would be inaccurate with some offset values),
			-- and destroy it if it's below the XZ plane
			Triangle.PivotOffset = CFrame.new(-Triangle.Size.X / 2, 0, 0)
			if Triangle:GetPivot().Position.Y < 0 then Triangle:Destroy() end
		end
	end
	
	Model.WorldPivot = CFrame.new()
	Model.Name = 'Icosphere_' .. subdivisions
	
end

As you can see, the code is intended for a ModuleScript, and it returns a function. You could use it in the command bar like so:

require(PATH_TO_MODULE_SCRIPT)(ARGUMENTS)

You could also use this generator at runtime, but I would recommend tweaking it if that is your intention. In its current form, it is most suitable for Studio use.

Currently, every face/triangle (made up of two WedgeParts - thank you @EgoMoose) is unioned and added to the workspace as soon as it is created. This is unnecessary and 100+ times slower than the alternative (not creating unions, and only adding the entire model to workspace once everything is done), but, since UnionAsync() yields, this prevents the lag spike that you would otherwise get when creating spheres with 4+ (or less, depending on your PC) subdivisions.

Arguments

radius: number
subdivisions: number? (Default Value: 0)

CAUTION: Every icosphere will be made up of 20 * 4^subdivisions Unions (triangles). As stated before, doing more subdivisions (within reason) will probably not cause a lag spike with the current setup, but it will take a long time with 5+ subdivisions, and might eventually crash Studio because of the number of Unions. With a high enough number, it might even cause a lag spike, use up your available RAM and/or cause Studio to crash before creating any parts, because all vertices are calculated prior to creating parts. START LOW

properties: {any}? (Default Value: { Anchored = true, UsePartColor = true })

Properties to apply to all Unions. They are not applied to the WedgeParts.

thickness: number? (Default Value: 0.001)

Thickness of the Unions, looking from the surface of the sphere, in the direction of its center. Thickness will not offset the outward-facing surfaces of the Unions from the surface of the sphere, but if it is close to or larger than the diameter of the sphere then the Unions will protrude on the opposite side of the sphere.
CAUTION: As the minimum part size is currently 0.001 and the generator relies on the thickness value for positioning the Unions correctly (relative to the sphere’s surface), using a value less than 0.001 will not make the Unions thinner, but will only cause them to be placed incorrectly, making the surface of the sphere slightly incorrect.

offset: number? (Default Value: 0)

Offset of the Unions, looking from the surface of the sphere, in the opposite direction from its center - positive values will make them move away from the sphere’s center, and negative values will move them towards (and past) it.

halve: boolean? (Default Value: nil)

Whether a half of the sphere should be removed.

Remarks

For some of you, perhaps, this leaves much to be desired. Feel free to tweak it and add onto it. This generator is not intended as an end-all resource, but a starting point, which I might or might not add on to it in the future. If anything, performance could definitely be improved.

Here are some possible additions and changes you could make: Welds, truncating, offset (or thickness) randomization, color randomization, color gradients, saving subdivision vertex data for reuse, or otherwise making things more friendly for runtime generation, not generating half of the faces when using halve (instead of deleting the Unions later), generating different stellations.

If you are big on math unlike me, and see some huge optimization possibilities, and you feel like sharing, then share, I’ll gladly test and edit the post for that purpose, for the sake of whoever uses this in the future.

Finally, I don’t usually comment code, so I hope I did a good enough job, but if something in the code (or in this post) is left unclear feel free to ask below.

:wave:

Edit 1 (Fix)

Unioning WedgeParts with a thickness of 0.001, and then resizing the Unions to the desired thickness afterwards was not the right way to go. This became apparent with larger spheres:

The code was updated to set the desired thickness to the WedgeParts directly instead.

14 Likes

I have only one question, why’d you make that if there’s already resources like blender? Other then that, great job contributing to dev community!

1 Like

Not everyone know how to use blender, not everyone can use blender, not everyone know that blender exists, and creating these spheres using built-in functions is better imo

Great job btw

and not everyone knows how to script though, so blender would be the easier choice for many.

other than that, this is very impressive and looks quite cool!

@ORdenDev Collisions, for me particularly that is. Concave shapes (I wanted a dome) usually don’t play well with collisions. There are other possible reasons to want this as well, most generally speaking: if you import it from Blender you will have a single mesh, and if you build it like this then you have individual faces, and can manipulate them individually: whether that means removing some, recoloring some, randomizing their positions to generate planets, or something else.

Here’s a bonus to the original post:

Recording 2023-06-16 at 22.06.29

And yes, that could’ve been done with Blender spheres as well :stuck_out_tongue:

2 Likes

That looks super cool. I would love to see that in a (space) game.

Reminds me of … warhead? A 3d game with a hostile alien machine entity. The 3d performance back then was super limited, but it had a kind of space-horror vibe.

Warhead it is Warhead (video game) - Wikipedia

why would we need this unperformant thing over blender which is more performant???

@VSCPlays This would be useful if you need to generate an icosphere at runtime. In that case blender doesn’t really apply here.

One usecase example for this would be generating randomized smooth terrain planets. It’d be easy to do this by generating an icosphere then using Terrain:DrawWedge on the resultant wedgeparts

2 Likes

Awesome resource, thank you for sharing and taking the time to write this out, I cannot even image the complex math involed in this!

You’re welcome. It’s actually a very simple process, I’m not big on math at all:

You start with faces of an icosahedron; those are predefined, you don’t calculate them. The way you subdivide is by looping through all faces (triangles) that you have, and dividing them into 4 faces by splitting the edges in half, like so:


In the end, you just change the magnitude of all vertices (points) to match the radius, by doing:

Vertex.Unit * radius

You could even use the Scale property of the final Model instead. In that case you’d need to take thickness and offset into account, they would also be scaled.

2 Likes