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)
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.
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.