Building a Cylinder Primitive

Cylinders are represented as a stack of circles, with each circle’s vertices connected with those above and below it. The radius of each circle does not have to be equivalent. In the case where they are not, the object is considered a cone. The vertices are found by dividing the 360 degrees of the circle by n slices to find the angle between each vertex. Then using this and the length of the radius, the vertices are placed. Then, for the top and bottom circles, triangles are generated by connecting each vertex to its next neighbor, then those two points to the midpoint.

Then, the height of the circle is divided into n stacks. Each stack is a circle that is generated “above” the next. For denser cylinders, there are many “inner” circles. Because the height of the cylinder and the number of circles are variable, the radii of the inner circles has to be calculated. This is found by using the ratio of the difference between the top and bottom circles and the height.

To calculate the normal at each vertex, we need to know the angle of the outer wall of the cylinder. This is found by creating a triangle out of the height of the triangle and the difference between the top and bottom radii.

generateAngle(radBase, radTop, height) {
    let biggerRad = radBase > radTop ? radBase : radTop
    let smallerRad =  radBase > radTop ? radTop : radBase

    let triangleBase = biggerRad - smallerRad

    let angle = Math.atan(height / triangleBase)

    if (radTop > radBase) {
        return angle * -1
    } else {
        return angle
    }

}

After calculating the positions and normals, the last calculation is finding the indices of points on these 2D circles to be grouped together during triangle generation. The index array holds the indices of points in the position array rather than copying the points to save space. Every 3 points in the array are considered a grouping which will later be turned into a triangle. For 2D circles, each grouping contains the midpoint of the circle followed by the two outer vertices it connects to.

generateCircle(radius, numPoints, y, offset, centerNormalY) {

        var ret = {}
        
        var positions = []
        var indices = []
        var normals = []
    
        //init origin at the start
        positions.push(0)
        positions.push(y)
        positions.push(0)

        normals.push(0)
        normals.push(centerNormalY != null ? centerNormalY : 0)
        normals.push(0)


        //init circle points
        for(let i = 0; i < numPoints; i++) {

            let rad = i * ((2 * (Math.PI)) / (numPoints)) //in radians

            let x = radius * Math.cos(rad)
            let z = radius * Math.sin(rad)

            let dist = Math.sqrt(x * x + z * z)
            let yOffset = dist * Math.tan(this.angle) //this.angle calculated previously using generateAngle()


            positions.push(x) //x
            positions.push(y) //y
            positions.push(z) //z

            normals.push(x)
            normals.push(centerNormalY != null ? centerNormalY + y : y + yOffset)
            normals.push(z)


        }

        if (offset != -1) {
            for (let i = 1; i < numPoints; i++) {
                indices.push(0) //midpoint
                indices.push(0 + i) 
                indices.push(0 + i + 1)

            }

            //last triangle
            indices.push(0)
            indices.push(0 + numPoints)
            indices.push(0 + 1)
        }
    
        ret.positions = positions
        ret.indices = indices
        ret.normals = normals

        return ret


    }

The perimiter of the circles are then connected to those above and below it in a similar way to how the circles were generated. If we wanted to make a cone, we can adjust the radii of each stack accordingly (e.g. larger radius on the bottom and smaller radius on top).

    connectCirclesTriangle(bottomPos, topPos, len) {
        var ret = [];

        for (let i = 1; i < len - 1; i++) {
            let triangleOne = [bottomPos + i, topPos + i, topPos + i + 1]
            let triangleTwo = [bottomPos + i, bottomPos + i + 1, topPos + i + 1]

            ret = ret.concat(triangleOne)
            ret = ret.concat(triangleTwo)

        }

        //last triangle
        let triangleOne = [bottomPos + len - 1, topPos + len - 1, topPos + 1]
        let triangleTwo = [bottomPos + len - 1, bottomPos + 1, topPos + 1]

        ret = ret.concat(triangleOne)
        ret = ret.concat(triangleTwo)


        return ret

    }

class cylinder {

    constructor(radBase, radTop, height, numSlices, numStacks, color) {
        this.angle = this.generateAngle(radBase, radTop, height)
        let init = this.generateCylinderData(radBase, radTop, height, numSlices, numStacks, color)
        this.combinedVertexIndices = init.combinedVertexIndices
        this.combinedVertexPositions = init.combinedVertexPositions
        this.combinedVertexNormals = init.combinedVertexNormals 
    }

    get indices() {
        return this.combinedVertexIndices
    }

    get positions() {
        return this.combinedVertexPositions
    }

    get normals() {
        return this.combinedVertexNormals
    }

    generateCircle(radius, numPoints, y, offset, centerNormalY) {

        var ret = {}
        
        var positions = []
        var indices = []
        var normals = []
    
        //init origin at the start
        positions.push(0)
        positions.push(y)
        positions.push(0)

        normals.push(0)
        normals.push(centerNormalY != null ? centerNormalY : 0)
        normals.push(0)


        //init circle points
        for(let i = 0; i < numPoints; i++) {

            let rad = i * ((2 * (Math.PI)) / (numPoints)) //in radians

            let x = radius * Math.cos(rad)
            let z = radius * Math.sin(rad)

            let dist = Math.sqrt(x * x + z * z)
            let yOffset = dist * Math.tan(this.angle)


            positions.push(x) //x
            positions.push(y) //y
            positions.push(z) //z

            normals.push(x)
            normals.push(centerNormalY != null ? centerNormalY + y : y + yOffset)
            normals.push(z)


        }

        if (offset != -1) {
            for (let i = 1; i < numPoints; i++) {
                indices.push(0) //midpoint
                indices.push(0 + i) 
                indices.push(0 + i + 1)

            }

            //last triangle
            indices.push(0)
            indices.push(0 + numPoints)
            indices.push(0 + 1)
        }
    
        ret.positions = positions
        ret.indices = indices
        ret.normals = normals

        return ret


    }

    //bottomPos and topPos are the index in the combined array that each respective element starts at
    //len is the length of each list
    //list is the combined list with all the positions
    connectCirclesTriangle(bottomPos, topPos, len) {
        var ret = [];

        for (let i = 1; i < len - 1; i++) {
            let triangleOne = [bottomPos + i, topPos + i, topPos + i + 1]
            let triangleTwo = [bottomPos + i, bottomPos + i + 1, topPos + i + 1]

            ret = ret.concat(triangleOne)
            ret = ret.concat(triangleTwo)

        }

        //last triangle
        let triangleOne = [bottomPos + len - 1, topPos + len - 1, topPos + 1]
        let triangleTwo = [bottomPos + len - 1, bottomPos + 1, topPos + 1]

        ret = ret.concat(triangleOne)
        ret = ret.concat(triangleTwo)


        return ret

    }


    generateAngle(radBase, radTop, height) {
        let biggerRad = radBase > radTop ? radBase : radTop
        let smallerRad =  radBase > radTop ? radTop : radBase

        let triangleBase = biggerRad - smallerRad

        let angle = Math.atan(height / triangleBase)

        if (radTop > radBase) {
            return angle * -1
        } else {
            return angle
        }

    }


    generateCylinderData(radBase, radTop, height, numSlices, numStacks) {

        //prime deltas
        var radDelta= (radBase - radTop) / (numStacks - 1)
        var workingRad = radBase
        var heightDelta = height / (numStacks - 1) 
        var workingHeight = 0 


        //create the base
        var bottomCircle = this.generateCircle(radBase, numSlices, 0, 0)
        var bottomCircleDuplicate = this.generateCircle(radBase, numSlices, 0, 0, -1)

        var combinedVertexPositions = bottomCircle.positions
        var combinedVertexNormals = bottomCircle.normals

        //create the top
        var topCircle = this.generateCircle(radTop, numSlices, height, bottomCircle.indices.length / 3)
        var topCircleDuplicate = this.generateCircle(radTop, numSlices, height, bottomCircle.indices.length / 3, 1)


        //create stacks
        for (let i = 1; i < numStacks - 1; i++) {
            workingRad -= radDelta
            workingHeight += heightDelta

            let tempCircle = this.generateCircle(workingRad, numSlices, workingHeight, -1)
            combinedVertexPositions = combinedVertexPositions.concat(tempCircle.positions)
            combinedVertexNormals = combinedVertexNormals.concat(tempCircle.normals)
        }

        combinedVertexPositions = combinedVertexPositions.concat(topCircle.positions)
        combinedVertexNormals = combinedVertexNormals.concat(topCircle.normals)

        var start = combinedVertexPositions.length / 3

        combinedVertexPositions = combinedVertexPositions.concat(topCircleDuplicate.positions)
        combinedVertexNormals = combinedVertexNormals.concat(topCircleDuplicate.normals)

        var nextStart = combinedVertexPositions.length / 3

        combinedVertexPositions = combinedVertexPositions.concat(bottomCircleDuplicate.positions)
        combinedVertexNormals = combinedVertexNormals.concat(bottomCircleDuplicate.normals)

        /////////////////////////////////////////////////////////

        for (let i = 0; i < topCircleDuplicate.indices.length; i++) {
            topCircleDuplicate.indices[i] += start
            bottomCircleDuplicate.indices[i] += nextStart
        }

        //create top and bottom face triangles
        var combinedVertexIndices = bottomCircleDuplicate.indices.concat(topCircleDuplicate.indices)

        //Connect all of the slices together
        for (let i = 0; i < numStacks; i++) {
            var offset = i * (numSlices - 1)
            var len = numSlices + 1
            let result = this.connectCirclesTriangle(offset, offset + len, len)
            combinedVertexIndices = combinedVertexIndices.concat(result)
        }

        var ret = {}
        ret.combinedVertexIndices = combinedVertexIndices
        ret.combinedVertexPositions = combinedVertexPositions
        ret.combinedVertexNormals = combinedVertexNormals

        return ret
    }
}


Back