Geodesic Spheres

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.

I WANNA USE II ! Just don’t know how to… :blush: Can’t type code to save my life. Just got Panda 3D…
Hi by the way :slight_smile:

Here’s a snippet of code where I use my routine:

        sphere = makeGeodesicSphere(4, 500, [1,1,1,1], 'TestSphere')
        testSphereNode = render.attachNewNode('TestSphereNode')
        testSphereNode.attachNewNode(sphere)
        testSphereNode.setPos(0,0,1005)   

I will explain the makeGeodesicSphere call’s arguments:

4 = level of recursion.

500 = scale. This determines the radius of the sphere.

[1,1,1,1] = Color information. RGB + intensity

‘TestSphere’ = name of the Geom object you are getting back.

Also be sure that you are importing the math module, as the routines make use of trig functions.