In this tutorial you’ll learn how to render an f16 air force airplane 3d .obj model in your browser using small open source modules.

Setting up

Before getting started let’s download everything that we need for the tutorial upfront in case you want to work through it offline later.

# Run this in your command line to create
# a new directory for this tutorial
mkdir webgl-wavefront-obj-tutorial
cd webgl-wavefront-obj-tutorial
npm init -f
# Next we download our 3d model and texture
curl -L http://chinedufn.com/assets/\
f16/f16-model.obj -o f16-model.obj
curl -L http://chinedufn.com/assets/\
f16/f16-texture.bmp -o f16-texture.bmp
# Next we install our code dependencies
# In order to prevent this tutorial from rotting
# as years go by we're grabbing versions that
# we know for sure will work
npm install load-wavefront-obj@0.6.2 \
raf-loop@1.1.3 wavefront-obj-parser@0.3.0 \
browserify@13.1.1 http-server@0.9.0

Alright, we’ve downloaded everything that we need to learn how to render an F16 plane model. You will not need an internet connection from here on.

Parsing our model

Right now we have an f16-model.obj file. This file is a text file that contains data about our model’s vertices. In order to make use of this data, we parse it into more readily accessible structure and store it as JSON. This is the job of the wavefront-obj-parser dependency that we downloaded earlier.

# Convert our .obj file into a JSON file
node_modules/wavefront-obj-parser/bin/obj2json.js \
f16-model.obj > f16-model.json

Creating our HTML file

# Create a new html file
touch index.html

And edit your index.html to contain the following:

<!doctype html>
<html>
  <body>
    F16 Plane!
    <!-- Load our tutorial application -->
    <script src='bundle.js'></script>
  </body>
</html>

And then start a server to serve our files

node_modules/http-server/bin/http-server \
-p 4040

Now if you visit http://localhost:4040 in your browser you should see the text “F16 Plane!”

Drawing a blank canvas

Let’s create a new file for our demo application.

# Create our app's js file
touch f16-demo.js

Now let’s edit this f16-demo.js file to show an empty canvas.

// f16-demo.js

// Create our canvas and WebGL context
var canvas = document.createElement('canvas')
canvas.width = 500
canvas.height = 500
var gl = canvas.getContext('webgl')
// Make our canvas background black
gl.clearColor(0.0, 0.0, 0.0, 1.0)

gl.enable(gl.DEPTH_TEST)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

// Add our canvas to the DOM (browser)
document.body.appendChild(canvas)

The above code will add an all black canvas to our page. We can verify this by running:

# Run this and then refresh your browser
node_modules/browserify/bin/cmd.js \
f16-demo.js > bundle.js

Now if you refresh your browser you should see an all black canvas.

Drawing our model

Alright we have a blank canvas, let’s draw our model onto it.

Add this new code to the bottom of the f16-demo.js file that you created above.

// f16-demo.js

// ...
// ...

// This helps us use our model's JSON and texture
// data to create the command that draws our model
var loadWFObj = require('load-wavefront-obj')
var loaded3dModel

// Here we import our model's JSON
// That we generated earlier.
//  You could also use an xhr request
var modelJSON = require('./f16-model.json')

// Download our model's texture image.
//  You could also pass in a Uint8Array
//  of image data
var image = new window.Image()
image.crossOrigin = 'anonymous'
// Once our image downloads we buffer our
// 3d model data for the GPU
image.onload = loadModel
image.src = 'f16-texture.bmp'

// This prepare our data for the GPU
// so that we can later draw it
function loadModel () {
  loaded3dModel = loadWFObj(gl, modelJSON, {
    textureImage: image
  })
}


// Our model's x-axis rotation in radians
var xRotation = 0
// We render our model every request animation frame
var loop = require('raf-loop')
// dt is the number of milliseconds since
// we last rendered our model
loop(function (dt) {
  gl.viewport(0, 0, canvas.width, canvas.height)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

  // Once we've loaded our model we draw it every frame
  if (loaded3dModel) {
    // Pass in options whenever you want to draw
    // Your model. There are other options that
    // we'll link to below!
    loaded3dModel.draw({
      position: [0, 0, -3.1],
      rotateX: xRotation
    })
  }
  // Rotate our model by a little each frame
  xRotation += dt / 3000
}).start()

The above code first loaded our model’s JSON file and our texture. It then passes those into load-wavefront-obj, a module that gives us a draw command that we can use to render our 3d model onto our canvas.

We use a raf-loop to redraw our model everytime the browser repaints.

Lastly, we call loaded3dModel.draw with our position and rotation. We’re only manipulating the rotation in this tutorial, but load-wavefront-obj documents the other other options that you can pass in.


Now if we view our demo app we should see our f16 ship rotating in our browser.

# Run this and then refresh your browser
node_modules/browserify/bin/cmd.js \
f16-demo.js > bundle.js

Where to go from here

Rapid prototyping

Needing to run our browserify command everytime we make a change can become a bit frustrating, so a live reload server like budo can be very handy. Give budo --open --live f16-demo.js a try.

Camera

In a real application you’ll usually want to render your models in a scene relative to a camera that you have control over.

You can accomplish this by passing your camera’s viewMatrix into your model’s draw command.


What would you like to learn next? Let me know on Twitter!

Til’ next time,

- CFN

Update: Hey HN. Thanks for the feedback! Check out the discussion on Hacker News