Hello,
I’ve just spent some time writing a few functions that create procedural geodesic “spheres” by recursively subdividng the faces of an octahedron. This gives you a sphere with lots of near-equilateral triangles in it. The most degenerate triangles are right triangles. Contrast this with the latitude-longitude globe method of making spheres, where the “fullest” triangles are right triangles, and the most degenerate triangles look like toothpicks. I had to do this because I am writing a space game, and the performance of the latitude-longitude procedural globe at maximum zoom-out was poor. (Viewed from the direction of the poles, I got weird cross artifacts and moire patterns near the edges while the globe still had some significant size on the screen.) My space game is going to allow the user to zoom out from ship-scale views, out to solar-system and interstellar scales, so good zoom-out and zoom-in performance is important.
As a result of these functions, these spheres give you equivalent smoothness with far fewer triangles. In addition, the triangles are still generated consecutively in horizontal strips, giving short vertex caches in the hardware the chance to optimize.
def bisectPoints(indexA, indexB, vertexList, vertexCache):
if indexA < indexB:
index1 = indexA
index2 = indexB
else:
index1 = indexB
index2 = indexA
key = (index1,index2)
if vertexCache.has_key(key):
return vertexCache[key]
else:
newPoint = vertexList[index1] + vertexList[index2]
newPoint.normalize()
newIndex = len(vertexList)
vertexList.append(newPoint)
vertexCache[key] = newIndex
return newIndex
def uvCoordForSphereVertex(vertex):
x = vertex.getX()
y = vertex.getY()
z = vertex.getZ()
r = math.sqrt((x*x) + (y*y))
radiansLatitude = math.atan2(r,z)
v = 1 - (radiansLatitude / (math.pi))
radiansLongitude = math.atan2(y,x)
u = 1 + (radiansLongitude / (2 * math.pi))
return (u,v)
def splitTriangle(triangle, vertexList, vertexCache):
index0 = triangle[0]
index1 = triangle[1]
index2 = triangle[2]
newLowerTriangleList = []
index0_1 = bisectPoints(index0, index1, vertexList, vertexCache)
index0_2 = bisectPoints(index0, index2, vertexList, vertexCache)
index1_2 = bisectPoints(index1, index2, vertexList, vertexCache)
newUpperTriangle = [index0,index0_1,index0_2]
newLowerTriangle1 = [index0_1,index1,index1_2]
newLowerTriangleList.append(newLowerTriangle1)
newLowerTriangle2 = [index1_2,index0_2,index0_1]
newLowerTriangleList.append(newLowerTriangle2)
newLowerTriangle3 = [index0_2,index1_2,index2]
newLowerTriangleList.append(newLowerTriangle3)
return [newUpperTriangle,newLowerTriangleList]
def splitTriangleCap(listOfTriangles, vertexList, vertexCache, recursionLevel):
if recursionLevel == 0:
return listOfTriangles
else:
newTriangleCap = []
newTriangleBelt = []
for triangle in listOfTriangles:
result = splitTriangle(triangle,vertexList,vertexCache)
newTriangleCap.append(result[0])
newTriangleBelt += result[1]
rval = splitTriangleCap(newTriangleCap, vertexList, vertexCache, (recursionLevel - 1))
rval += splitTriangleBelt(newTriangleBelt, vertexList, vertexCache, (recursionLevel - 1))
return rval
def splitTriangleBelt(listOfTriangles, vertexList, vertexCache, recursionLevel):
if recursionLevel == 0:
return listOfTriangles
else:
upperTriangleBelt = []
lowerTriangleBelt = []
evenFlag = 1
for triangle in listOfTriangles:
result = splitTriangle(triangle,vertexList,vertexCache)
if evenFlag:
upperTriangleBelt.append(result[0])
lowerTriangleBelt += result[1]
else:
result[1].reverse()
upperTriangleBelt += (result[1])
lowerTriangleBelt.append(result[0])
evenFlag = not evenFlag
rval = splitTriangleBelt(upperTriangleBelt, vertexList, vertexCache, (recursionLevel - 1))
rval += splitTriangleBelt(lowerTriangleBelt, vertexList, vertexCache, (recursionLevel - 1))
return rval
def makeGeodesicWireframeDome(recursionLevel, scale, colorValues, name):
vertexList = []
vertexList.append(Point3(0,0,1))
vertexList.append(Point3(-1,0,0))
vertexList.append(Point3(0,1,0))
vertexList.append(Point3(1,0,0))
vertexList.append(Point3(0,-1,0))
initialUpperCap = [[0,1,2],[0,2,3],[0,3,4],[0,4,1]]
triangleList = splitTriangleCap(initialUpperCap, vertexList, recursionLevel)
# Set up the format and vertex data and vertex data writer objects
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData(name, format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
color = GeomVertexWriter(vdata, 'color')
normal = GeomVertexWriter(vdata, 'normal')
# Populate the vertex data and the coordinate index
for v in vertexList:
x = v.getX()
y = v.getY()
z = v.getZ()
vertex.addData3f((x*scale),(y*scale),(z*scale))
normal.addData3f(x,y,z)
color.addData4f(colorValues[0],colorValues[1],colorValues[2],colorValues[3])
#texcoord.addData2f(u,v)
# Now that we have the vertexes, populate the triangles
sphere = Geom(vdata)
for triangleData in triangleList:
triangle = GeomLinestrips(Geom.UHStatic)
triangle.addVertex(triangleData[0])
triangle.addVertex(triangleData[1])
triangle.addVertex(triangleData[2])
triangle.addVertex(triangleData[0])
triangle.closePrimitive()
sphere.addPrimitive(triangle)
gNode = GeomNode(name+'GeomNode')
gNode.addGeom(sphere)
return gNode
def makeGeodesicSphere(recursionLevel, scale, colorValues, name):
vertexList = []
vertexCache = {}
vertexList.append(Point3(0,0,-1))
vertexList.append(Point3(-1,0,0))
vertexList.append(Point3(0,1,0))
vertexList.append(Point3(1,0,0))
vertexList.append(Point3(0,-1,0))
skewPoint = Point3(-1,-0.000001,0)
skewPoint.normalize()
vertexList.append(skewPoint)
initialUpperCap = [[0,1,2],[0,2,3],[0,3,4],[0,4,5]]
vertexList.append(Point3(0,0,1))
initialLowerCap = [[6,5,4],[6,4,3],[6,3,2],[6,2,1]]
triangleList1 = splitTriangleCap(initialUpperCap, vertexList, vertexCache, recursionLevel)
triangleList2 = splitTriangleCap(initialLowerCap, vertexList, vertexCache, recursionLevel)
triangleList = triangleList1 + triangleList2
# Set up the format and vertex data and vertex data writer objects
format = GeomVertexFormat.getV3n3c4t2()
#format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData(name, format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
color = GeomVertexWriter(vdata, 'color')
normal = GeomVertexWriter(vdata, 'normal')
texcoord = GeomVertexWriter(vdata, 'texcoord')
# Populate the vertex data and the coordinate index
for v in vertexList:
x = v.getX()
y = v.getY()
z = v.getZ()
u,v = uvCoordForSphereVertex(v)
vertex.addData3f((x*scale),(y*scale),(z*scale))
normal.addData3f(x,y,z)
color.addData4f(colorValues[0],colorValues[1],colorValues[2],colorValues[3])
texcoord.addData2f(u,v)
# Now that we have the vertexes, populate the triangles
sphere = Geom(vdata)
triangles = GeomTriangles(Geom.UHStatic)
for triangleData in triangleList:
triangles.addVertex(triangleData[0])
triangles.addVertex(triangleData[1])
triangles.addVertex(triangleData[2])
triangles.closePrimitive()
sphere.addPrimitive(triangles)
gNode = GeomNode(name+'GeomNode')
gNode.addGeom(sphere)
return gNode
This code is copyrighted by me, but you may use it provided I am credited in your source code comments.