In this MP you will

  1. Import an OBJ (see the OBJ MP)
  2. Render it with flat shading based on polygon geometery
  3. Apply Catmull-Clark subdivision to the imported mesh

This MP is elective, with no core components. It assumes you have already completed the OBJ MP.

Warning

This MP is likely to take more effort and time than its point value suggests. It is intended to be a jumping-off point for students interested in geometry definition and refinement, which is not a topic given much attention in this course.

1 Overview

You will submit a webpage that has

  • One file browsing field
  • One numeric entry field
  • One canvas
  • A 3D view of a lit subdivided model

Submit an HTML file and any number of js, glsl, css, and JSON files. No image files are permitted for this assignment. Do not include spaces in file names as the submission server does not process them well.

You are welcome to use a JavaScript math library, such as the one used in in-class examples or others you might know.

2 Specification

2.1 HTML input setup

Have one file browsing input for uploading an OBJ file.

Have one input numeric field for entering how many rounds of subdivision to do.

HTML input elements, styling, and event handling are beyond the scope of this class. We provide suggested starter code for them:

Suggested starter code

We recommend using the following HTML body:

<body>
<label>OBJ File path: <input id="file" type="file"/></label>
<label>Levels: <input id="levels" type="number" value="0" max="5" min="0" step="1"/></label>
<canvas></canvas>
</body>

and the following event listeners

document.getElementById('file').addEventListener('change', async event=>{
  if (event.target.files.length != 1) {
    console.debug("No file selected")
    return
  }
  const txt = await event.target.files[0].text()
  if (!/^v .*^v .*^v .*^f /gms.test(txt)) {
    console.debug("File not a valid OBJ file")
    return
  }
  
  // TO DO: parse the OBJ file from the string in `txt` and display it
})
document.getElementById('levels').addEventListener('change', async event=>{
  const level = Math.min(document.getElementById('levels').value, 5)|0
  
  // TO DO: subdivide (if level > 0) and display the object
})

2.2 Flat-shaded OBJ

You’ll load the OBJ file similarly to how you did in the OBJ MP. The string is retrieved in a slightly different way, but otherwise is the same.

However, you will need to store the resulting mesh differently. You need to store faces of any number of sides and the interconnections, which a simple triangle index array does not provide. We recommend storing them in a half-edge data structure, either using straightforward object-oriented structures like

class HalfEdge {
    v
    next
    twin
    constructor(v, next, twin) {
        this.v = v;
        this.next = next;
        this.twin = twin;
    }
    sideCount() {
        let ans = 1
        for(let he=this.next; he!=this; he=he.next) ans += 1
        return ans
    }
}

or by using parallel arrays, as is outlined in Dupuy and Vanhoey’s 2021 paper.

You also need to flat-shade the polygons, meaning you have one normal per face, not one per vertex. The easiest way to accomplish this is to use drawArrays instead of drawElements, providing each vertex one per face it touches with a different normal each time. To compute the normals themselves, take the cross product of two edges of a triangle or of the two diagonals of a quad; for 5+-sided shapes, the normal of any triangle or quad inside it will work.

2.3 Catmull-Clark subdivision

If the rounds of subdivision is greater than 0, that many rounds of Catmull-Clark subdivision. Dupuy and Vanhoey’s 2021 paper explains one algorithm for doing this, but there are many other algorithms available online or derivable by yourself from the definition of Catmull-Clark’s scheme.

Store the resulting levels (or at least the level-0 mesh) so that you can go back to a lower level after being at a higher one.

2.4 Render and light

Render the geometry with both diffuse and specular lighting. Have the camera or geometry moving so that the geometry can be seen from multiple viewpoints.

3 Evaluating your results

On both your development machine and when submitted to the submission server and then viewed by clicking the HTML link, the resulting page should initially be blank. Once an OBJ file is loaded, a flat-shaded mesh should appear, filling most of the screen with a moving camera. As the levels are changed the mesh should be subdivided. One example might be the following:

A video of an example result.

Your program should work correctly for at least the following subdivision-friendly meshes:

The teapot and cow models are not designed to subdivide well; your code should run on them, but might give ugly results.

The Suzanne and triangle models have edges that are not connected to anything. We don’t require your code to work on them, but if you want it to then have edges of the mesh act the same they would if they were adjoined by zero-width polygons instead.