How to plot vertices of a 3D shape such as a dodecahedron?

I’m absolutely garbo at math sometimes, and I only tend to learn by teaching myself because I won’t learn jack unless I know why it works.

Today my big curiosity is how to plot the points of a dodecahedron in a 3d space.

I have no clue where to start, I’d like to be able to specify and midpoint and diameter to create the shape. If you could, describe it as if I’m someone who only knows how to +, -, /, *. I’d like to know as much as possible about this, things such as is there a certain formula I need to use? what’s the logic behind using it? etc.

Thank you!

My inspiration to learn this comes from this image:

7 Likes

The vertices for a regular dodecahedron can be found here: Regular dodecahedron - Wikipedia. Look under “Cartesian Coordinates.” I don’t know how these were derived, however. I am curious to know why the golden ratio appears.

This is good, but what does any of this actually mean?

Whats so golden about this ratio?

Why and how is the distance always sqrt of 3 when you could have a any size of diameter?


This is the important information that you need. Only 4 coordinates are shown, but the plus/minus sign indicates that a number’s sign can be flipped to produce a new coordinate. For example, (±1, ±1, ±1) means you can have all these combinations of coordinates:
(1, 1, 1)
(-1, 1, 1)
(1, -1, 1)
(1, 1, -1)
(-1, -1, 1)
(-1, 1, -1)
(1, -1, -1)
(-1, -1, -1)

The circle with a line through it is the Greek symbol phi which is used to represent the golden ratio. The golden ratio is an irrational number that pops up a lot in nature, so it gets its own name.

You’re right that the distance from the origin to a vertex is not always sqrt(3). This is only for a unit dodecahedron, meaning that the orange inner cube has coordinates (±1, ±1, ±1). If it was larger or smaller, the distance would be different.

EDIT: Spelling

2 Likes

Oh man, you’re a huge help, thanks.

I’m gonna mess around with this new knowledge in studio a bit and see what I can do.

My next question is, how could I apply this to making more shapes such as this one with even more sides?

The dodecahedron is one of 5 platonic solids, which follow a specific set of criteria. Platonic solid - Wikipedia
All of their vertex positions are readily available online.

Outside of these simple 5, the classifications of polyhedra get more complex. The shape you’re likely looking for is the “geodesic polyhedron.” I have yet to find an algorithm that would produce a geodesic polyhedron from a given number of triangles, but I hope I’ve provided a starting point for you in your search.

This has proven to be much simpler than I had thought. Thank you for helping me out, I really appreciate it!

1 Like

Oh fun! May I ask what you plan on doing with this? I’ve created an isocohedron and subdivided it to create dodecahedron and further divided it to create geometric spheres which I’ve used as a planet for a hexagonal maze. Check out the video demo here:

Here is the code for it. It includes not only the isocohedron creation (function getShape) and the subdivision algorithm (function divideShape) but also the code to create the triangles for the surface and the walls (createShape), a function to find a path between two tiles (getPath), functions to test if a position is above a cell, and a function to figure out where a position is starting the search from a close cell. Maybe some of them will be useful to you? I’ve had this code just laying around for a good five or six years now.

local phi = (math.sqrt(5) + 1) / 2
local values = {[0] = 0, 1, phi}
local scale = 200

local wallHeight = 30
local columnMesh = game.ServerStorage.ColumnMesh
local floorColor = BrickColor.new("Br. yellowish orange")
local wallColor = BrickColor.new(2, 2, 2)
local floorMaterial = Enum.Material.Granite
local wallMaterial = Enum.Material.Slate

local MazeWorld = {}

function createShape(shape, wallParent, floorParent)
	local function createPart(type)
		local part = Instance.new(type or "Part")
		part.TopSurface = "Smooth"
		part.BottomSurface = "Smooth"
		part.Anchored = true
		part.FormFactor = "Custom"
		return part
	end
	local function createColumn(pos, parent)
		pos = pos + pos.unit * wallHeight/2
		local zAxis = Vector3.new(pos.y, -pos.x, pos.z)
		local yAxis = pos
		local xAxis = yAxis:Cross(zAxis)
		local part = createPart()
		part.Size = Vector3.new(5, wallHeight, 5)
		part.CFrame = CFrame.new(
			pos.x, pos.y, pos.z,
			xAxis.x, yAxis.x, zAxis.x,
			xAxis.y, yAxis.y, zAxis.y,
			xAxis.z, yAxis.z, zAxis.z
		)
		columnMesh:Clone().Parent = part
		part.BrickColor = wallColor
		part.Material = wallMaterial
		part.Parent = parent
	end
	local function createSurface(surface, parent)
		local a = surface[1][1]
		local b = surface[1][2]
		local c = surface[2][1]
		if c == a or c == b then
			c = surface[2][2]
		end
		-- Anaminus's triangle creator used in his terrain generator
		-- split triangle into two right angles on longest edge:
		local len_AB = (b - a).magnitude
		local len_BC = (c - b).magnitude
		local len_CA = (a - c).magnitude
	
		if (len_AB > len_BC) and (len_AB > len_CA) then
			a,c = c,a
			b,c = c,b
		elseif (len_CA > len_AB) and (len_CA > len_BC) then
			a,b = b,a
			b,c = c,b
		end
	
		local dot = (a - b):Dot(c - b)
		local split = b + (c - b).unit*dot/(c - b).magnitude
	
		-- get triangle sizes:
		local xA = 1
		local yA = (split - a).magnitude
		local zA = (split - b).magnitude
	
		local xB = 1
		local yB = (split - a).magnitude
		local zB = (split - c).magnitude
	
		-- get unit directions:
		local diry = (a - split).unit
		local dirz = (c - split).unit
		local dirx = diry:Cross(dirz).unit
	
		-- get triangle centers:
		local posA = split + diry*yA/2 - dirz*zA/2
		local posB = split + diry*yB/2 + dirz*zB/2
	
		-- place parts:
		local partA = createPart("WedgePart")
		partA.Name = "TrianglePart"
		partA.Size = Vector3.new(xA,yA,zA)
		partA.CFrame = CFrame.new(posA.x,posA.y,posA.z, 
			dirx.x,diry.x,dirz.x, 
			dirx.y,diry.y,dirz.y, 
			dirx.z,diry.z,dirz.z
		)
		partA.BrickColor = floorColor
		partA.Material = floorMaterial
		partA.Parent = parent
	
		dirx = dirx * -1
		dirz = dirz * -1
	
		local partB = createPart("WedgePart")
		partB.Name = "TrianglePart"
		partB.Size = Vector3.new(xB,yB,zB)
		partB.CFrame = CFrame.new(posB.x,posB.y,posB.z, 
			dirx.x,diry.x,dirz.x, 
			dirx.y,diry.y,dirz.y, 
			dirx.z,diry.z,dirz.z
		)
		partB.BrickColor = floorColor
		partB.Material = floorMaterial
		partB.Parent = parent
	end
	local function createColumn(pos, parent)
		pos = pos + pos.unit * wallHeight/2
		local zAxis = Vector3.new(pos.y, -pos.x, pos.z)
		local yAxis = pos
		local xAxis = yAxis:Cross(zAxis)
		local part = createPart()
		part.Size = Vector3.new(5, wallHeight, 5)
		part.CFrame = CFrame.new(
			pos.x, pos.y, pos.z,
			xAxis.x, yAxis.x, zAxis.x,
			xAxis.y, yAxis.y, zAxis.y,
			xAxis.z, yAxis.z, zAxis.z
		)
		columnMesh:Clone().Parent = part
		part.BrickColor = wallColor
		part.Material = wallMaterial
		part.Parent = parent
	end
	local function createWall(line, parent)
		local pos = (line[2] + line[1]) / 2
		pos = pos + pos.unit * wallHeight/2
		local zAxis = line[2] - line[1]
		local yAxis = pos
		local xAxis = yAxis:Cross(zAxis)
		local part = createPart()
		part.Size = Vector3.new(3, wallHeight, zAxis.magnitude)
		part.CFrame = CFrame.new(
			pos.x, pos.y, pos.z,
			xAxis.x, yAxis.x, zAxis.x,
			xAxis.y, yAxis.y, zAxis.y,
			xAxis.z, yAxis.z, zAxis.z
		)
		part.BrickColor = wallColor
		part.Material = wallMaterial
		part.Parent = parent
		local part = createPart()
		part.FormFactor = Enum.FormFactor.Custom
		part.Size = Vector3.new(0.2, 2, zAxis.magnitude * 8.5/pos.Magnitude)
		pos = pos.unit * 8.5
		part.CFrame = CFrame.new(
			pos.x, pos.y, pos.z,
			xAxis.x, yAxis.x, zAxis.x,
			xAxis.y, yAxis.y, zAxis.y,
			xAxis.z, yAxis.z, zAxis.z
		)
		part.BrickColor = wallColor
		part.Material = wallMaterial
		part.Parent = parent
	end
	local function getGroup(node)
		if node.group then
			node.group = getGroup(node.group)
			return node.group
		else
			return node
		end
	end

	local walls = {}
	for _, tile in next, shape.tiles do
		for otherTile, line in next, tile.connections do
			walls[#walls + 1] = {line, tile, otherTile}
			tile.connections[otherTile] = nil
			tile.walls[otherTile] = line
			otherTile.connections[tile] = nil
			otherTile.walls[tile] = line
		end
	end
	
	for _, tile in next, shape.tiles do
		local normals = {}
		for key, value in next, tile.walls do
			local normal = value[1]:Cross(value[1] - value[2])
			if normal:Dot(value[1] - tile.center) < 0 then
				normals[key] = {value[1], -normal}
			else
				normals[key] = {value[1], normal}
			end
		end
		tile.normals = normals
	end
	
	local num = #walls
	for i = 1, num do
		local j = math.random(i, num)
		walls[i], walls[j] = walls[j], walls[i]
	end
	for i = 1, num do
		local j = math.random(i, num)
		walls[i], walls[j] = walls[j], walls[i]
	end
	
	local i = 1
	while walls[i] do
		local wall = walls[i]
		local node1 = wall[2]
		local node2 = wall[3]
		if getGroup(node1) ~= getGroup(node2) then
			getGroup(node1).group = getGroup(node2)
			node1.connections[node2] = wall[1]
			node2.connections[node1] = wall[1]
			walls[i] = nil
		else
			createWall(wall[1], wallParent)
			wait()
		end
		i = i + 1
	end

	for _, point in next, shape.points do
		createColumn(point, wallParent)
		wait()
	end
	
	if floorParent then
		for i, surface in next, shape.surfaces do
			createSurface(surface, floorParent)
			wait()
		end
	end
	
	wait()
end

local function newPointFromVector3(vec, final)
	return final and vec or vec.unit * scale
end
local function newPointFromXYZ(x, y, z)
	local point = Vector3.new(x, y, z)
	return point.unit * scale
end
local function newLine(p1, p2)
	return {p1, p2}
end
local function newSurface(l1, l2, l3)
	return {l1, l2, l3}
end

local function getShape()
	local points = {
		newPointFromXYZ( phi,  0  ,  1  ); --1
		newPointFromXYZ( phi,  0  , -1  ); --2
		newPointFromXYZ(-phi,  0  ,  1  ); --3
		newPointFromXYZ(-phi,  0  , -1  ); --4
		newPointFromXYZ( 1  ,  phi,  0  ); --5
		newPointFromXYZ( 1  , -phi,  0  ); --6
		newPointFromXYZ(-1  ,  phi,  0  ); --7
		newPointFromXYZ(-1  , -phi,  0  ); --8
		newPointFromXYZ( 0  ,  1  ,  phi); --9
		newPointFromXYZ( 0  ,  1  , -phi); --10
		newPointFromXYZ( 0  , -1  ,  phi); --11
		newPointFromXYZ( 0  , -1  , -phi); --12
	}
	local lines = {
		newLine(points[1], points[2]);--1
		newLine(points[1], points[5]);--2
		newLine(points[1], points[6]);--3
		newLine(points[1], points[9]);--4
		newLine(points[1], points[11]);--5
		newLine(points[2], points[5]);--6
		newLine(points[2], points[6]);--7
		newLine(points[2], points[10]);--8
		newLine(points[2], points[12]);--9
		newLine(points[3], points[4]);--10
		newLine(points[3], points[7]);--11
		newLine(points[3], points[8]);--12
		newLine(points[3], points[9]);--13
		newLine(points[3], points[11]);--14
		newLine(points[4], points[7]);--15
		newLine(points[4], points[8]);--16
		newLine(points[4], points[10]);--17
		newLine(points[4], points[12]);--18
		
		newLine(points[5], points[7]);--19
		newLine(points[5], points[9]);--20
		newLine(points[5], points[10]);--21
		newLine(points[6], points[8]);--22
		newLine(points[6], points[11]);--23
		newLine(points[6], points[12]);--24
		newLine(points[7], points[9]);--25
		newLine(points[7], points[10]);--26
		newLine(points[8], points[11]);--27
		newLine(points[8], points[12]);--28
		
		newLine(points[9], points[11]);--29
		newLine(points[10], points[12]);--30
	}
	local surfaces = {
		newSurface(lines[1], lines[2], lines[6]);
		newSurface(lines[1], lines[3], lines[7]);
		newSurface(lines[4], lines[2], lines[20]);
		newSurface(lines[4], lines[5], lines[29]);
		newSurface(lines[5], lines[3], lines[23]);
		
		newSurface(lines[6], lines[8], lines[21]);
		newSurface(lines[8], lines[9], lines[30]);
		newSurface(lines[9], lines[7], lines[24]);
		
		newSurface(lines[10], lines[11], lines[15]);
		newSurface(lines[10], lines[12], lines[16]);
		newSurface(lines[13], lines[11], lines[25]);
		newSurface(lines[13], lines[14], lines[29]);
		newSurface(lines[14], lines[12], lines[27]);
		
		newSurface(lines[15], lines[17], lines[26]);
		newSurface(lines[17], lines[18], lines[30]);
		newSurface(lines[18], lines[16], lines[28]);
		
		newSurface(lines[19], lines[20], lines[25]);
		newSurface(lines[19], lines[21], lines[26]);
		
		newSurface(lines[22], lines[23], lines[27]);
		newSurface(lines[22], lines[24], lines[28]);
	}
	return setmetatable({
		points = points;
		lines = lines;
		surfaces = surfaces;
	}, MazeWorld)
end

local function divideShape(shape, final)
	local function divideLine(shape, line, tile)
		if line[6] then
			if tile then
				line[8].connections[tile] = line[4]
				tile.connections[line[8]] = line[4]
			end
			return line[3], line[4], line[5], line[6], line[7]
		else
			local vector = line[2] - line[1]
			local pnts = shape.points
			local p1 = newPointFromVector3(line[1] + vector/3, tile ~= nil)
			local p2 = newPointFromVector3(line[1] + vector * 2/3, tile ~= nil)
			pnts[#pnts + 1] = p1
			pnts[#pnts + 1] = p2
			
			local lns = shape.lines
			local l1 = newLine(line[1], p1)
			local l2 = newLine(p1, p2)
			local l3 = newLine(p2, line[2])
			lns[#lns + 1] = l1
			lns[#lns + 1] = l2
			lns[#lns + 1] = l3
			
			if tile then
				local pentCons = shape.pentagonalConnections
				pentCons[line[1]][#pentCons[line[1]] + 1] = l1
				pentCons[line[2]][#pentCons[line[2]] + 1] = l3
			end
			
			line[3] = l1
			line[4] = l2
			line[5] = l3
			line[6] = p1
			line[7] = p2
			line[8] = tile
			
			return l1, l2, l3, p1, p2
		end
	end
	local function divideSurface(shape, surface, final)
		local pnts = shape.points
		local l1 = surface[1]
		local l2 = surface[2]
		local l3 = surface[3]
		local c1 = l1[1]
		local c2 = l1[2]
		local c3 = l2[1]
		if c3 == c1 or c3 == c2 then
			c3 = l2[2]
		end
		local middle = newPointFromVector3((c1 + c2 + c3)/3, final)
		
		local tile
		if not final then
			pnts[#pnts + 1] = middle
			pnts[#pnts + 1] = c1
			pnts[#pnts + 1] = c2
			pnts[#pnts + 1] = c3
		else
			tile = {connections = {}, center = middle, walls = {}}
		end
		
		local l4, l5, l6, p1, p2 = divideLine(shape, surface[1], tile)
		local l7, l8, l9, p3, p4
		local l10, l11, l12, p5, p6
		if l1[2] == l2[1] then
			l7, l8, l9, p3, p4 = divideLine(shape, l2, tile)
			if l2[2] == l3[2] then
				l12, l11, l10, p6, p5 = divideLine(shape, l3, tile)
			else
				l10, l11, l12, p5, p6 = divideLine(shape, l3, tile)
			end
		elseif l1[2] == l2[2] then
			l9, l8, l7, p4, p3 = divideLine(shape, l2, tile)
			if l2[1] == l3[2] then
				l12, l11, l10, p6, p5 = divideLine(shape, l3, tile)
			else
				l10, l11, l12, p5, p6 = divideLine(shape, l3, tile)
			end
		elseif l1[2] == l3[1] then
			l7, l8, l9, p3, p4 = divideLine(shape, l3, tile)
			if l3[2] == l2[2] then
				l12, l11, l10, p6, p5 = divideLine(shape, l2, tile)
			else
				l10, l11, l12, p5, p6 = divideLine(shape, l2, tile)
			end
		else
			l9, l8, l7, p4, p3 = divideLine(shape, l3, tile)
			if l3[1] == l2[2] then
				l12, l11, l10, p6, p5 = divideLine(shape, l2, tile)
			else
				l10, l11, l12, p5, p6 = divideLine(shape, l2, tile)
			end
		end
		
		local lns = shape.lines
		local l13 = newLine(p1, p6)
		local l14 = newLine(p2, p3)
		local l15 = newLine(p4, p5)
		local l16 = newLine(p1, middle)
		local l17 = newLine(p2, middle)
		local l18 = newLine(p3, middle)
		local l19 = newLine(p4, middle)
		local l20 = newLine(p5, middle)
		local l21 = newLine(p6, middle)
		lns[#lns + 1] = l13
		lns[#lns + 1] = l14
		lns[#lns + 1] = l15
		lns[#lns + 1] = l16
		lns[#lns + 1] = l17
		lns[#lns + 1] = l18
		lns[#lns + 1] = l19
		lns[#lns + 1] = l20
		lns[#lns + 1] = l21
		
		local srfcs = shape.surfaces
		local s1 = newSurface(l5, l16, l17)
		local s2 = newSurface(l8, l18, l19)
		local s3 = newSurface(l11, l20, l21)
		local s4 = newSurface(l13, l16, l21)
		local s5 = newSurface(l14, l17, l18)
		local s6 = newSurface(l15, l19, l20)
		local s7 = newSurface(l4, l12, l13)
		local s8 = newSurface(l6, l7, l14)
		local s9 = newSurface(l9, l10, l15)
		srfcs[#srfcs + 1] = s1
		srfcs[#srfcs + 1] = s2
		srfcs[#srfcs + 1] = s3
		srfcs[#srfcs + 1] = s4
		srfcs[#srfcs + 1] = s5
		srfcs[#srfcs + 1] = s6
		srfcs[#srfcs + 1] = s7
		srfcs[#srfcs + 1] = s8
		srfcs[#srfcs + 1] = s9
		
		if final then
			local t = shape.tiles
			t[#t + 1] = tile
			t[c1].connections[tile] = l13
			t[c2].connections[tile] = l14
			t[c3].connections[tile] = l15
			tile.connections[t[c1]] = l13
			tile.connections[t[c2]] = l14
			tile.connections[t[c3]] = l15
		end
	end
	
	local srfcs = shape.surfaces
	local pnts = shape.points
	local pentCons
	local tiles
	if final then
		tiles = {}
		pentCons = {}
		for p, point in next, pnts do
			pentCons[point] = {}
			tiles[point] = {
				walls = {};
				lines = {};
				connections = {};
			}
		end
		shape.pentagonalConnections = pentCons
		shape.tiles = tiles
	end
	shape.surfaces = {}
	shape.points = {}
	shape.lines = {}
	for i = 1, #srfcs do
		divideSurface(shape, srfcs[i], final)
	end
	if final then
		for p, lines in next, pentCons do
			local newP = Vector3.new()
			for _, line in next, lines do
				if line[1] == p then
					newP = newP + line[2]
				else
					newP = newP + line[1]
				end
			end
			newP = newP / #lines
			for _, line in next, lines do
				if line[1] == p then
					line[1] = newP
				else
					line[2] = newP
				end
			end
			pnts[#pnts+1] = newP
			tiles[p].center = newP
		end
		shape.pentCons = nil
	end
end


local function getPath(start, finish)
	local curLvl
	local nxtLvl = {finish}
	local parent = {}
	while #nxtLvl > 0 do
		curLvl = nxtLvl
		nxtLvl = {}
		for i = 1, #curLvl do
			local node = curLvl[i]
			if node == start then
				local path = {}
				while node do
					path[#path + 1] = node
					node = parent[node]
				end
				return path
			end
			for child, _ in next, node.connections do
				if child ~= parent[node] then
					parent[child] = node
					nxtLvl[#nxtLvl + 1] = child
				end
			end
		end
	end
	print "No Solution"
	return {}
end

local function insideTile(tile, pos)
	local function insideLine(line, center, pos)
		local a = line[1]
		local normal = a:Cross(line[2]).unit
		if (center - a):Dot(normal) <= 0 then
			return (pos - a):Dot(normal) <= 0
		else
			return (pos - a):Dot(normal) > 0
		end 
	end
	for otherTile, line in next, tile.walls do
		if not insideLine(line, tile.center, pos) then
			return false
		end
	end
	for otherTile, line in next, tile.connections do
		if not insideLine(line, tile.center, pos) then
			return false
		end
	end
	return true
end

local function getTile(self, pos)
	for _, tile in next, self.tiles do
		if insideTile(tile, pos) then
			return tile
		end
	end
end

local function getTileGuess(self, pos, guess)
	local fringe = {guess}
	local closed = {[guess] = true}
	local front = 1
	local back = 2
	while front < back do
		local tile = fringe[front]
		front = front + 1
		if insideTile(tile, pos) then
			return tile
		end
		for child, _ in next, tile.connections do
			if not closed[child] then
				fringe[back] = child
				closed[child] = true
				back = back + 1
			end
		end
	end
end

local function getValidPath(self)
	local tiles = self.tiles
	local num = #tiles
	for i = 1, num do
		local j = math.random(i, num)
		tiles[i], tiles[j] = tiles[j], tiles[i]
	end
	for i = 1, num do
		local j = math.random(i, num)
		tiles[i], tiles[j] = tiles[j], tiles[i]
	end
	for i = 2, num do
		local path = getPath(tiles[1], tiles[i])
		if #path >= 20 then
			return path
		end
	end
end

MazeWorld.__index = MazeWorld
MazeWorld.new = getShape
MazeWorld.subdivide = divideShape
MazeWorld.create = createShape
MazeWorld.isInside = insideTile
MazeWorld.getPath = getPath
MazeWorld.getTile = getTile
MazeWorld.getTileGuess = getTileGuess
MazeWorld.getRandomPath = getValidPath

return MazeWorld

The maze algorithm is Randomized Kruskal’s.

3 Likes

Oh dang thats pretty neat.

I probably wont be using this code though as Id like to be able to write my own and get a full understanding of how it works. If I get stuck Ill probably look back at this though, thanks!

I plan to make some really neat effects with this shape, I might have something to share if I get done with it tonight.

Ended up making this interesting contraption
https://gyazo.com/7f3a1a339d9bb3c910b6f04f3750a29d

7 Likes