0.1.1 • Published 8 years ago

webglstarterkit v0.1.1

Weekly downloads
2
License
ISC
Repository
github
Last release
8 years ago

WebGL Starter Kit

Even though WebGL is awesome, and three.js makes it more so, there is still quite a lot to do to draw polygons on your webpages. At the very least, you need:

  • a scene
  • a renderer
  • a camera
  • some lights
  • an input loop
  • an animation loop
  • a link to the DOM

Beyond that, you might also want to:

  • resize to the DOM
  • convert mouse/touch events
  • handle camera movements
  • raycast mouse clicks
  • draw HUD elements over screen
  • put camera in a nice place

That's before a single triangle is drawn.

To help you get started, the WebGLWidget class wraps all this together with some choice defaults.

So you can concentrate on building awesome three.js graphics.

Override the defaults later, when it's convenient for you.

Install

Download the package: https://github.com/boscoh/WebGLStarterKit/archive/master.zip

Or clone the repository

git clone https://github.com/boscoh/WebGLStarterKit.git

Make sure you have installed NodeJS.

Then in the package:

> npm install

Quick example

The WebGLStarterKit assumes that you want to use ES6. In particular, the builder buildwebgl.js automatically transpiles from ES6 (which is backwards compatible with vanilla javascript).

Let's create a simple WebGL app octahedron.js.

First import the modules:

import THREE from "three.js";
import { WebGlWidget } from "./webglstarterkit.js";

Then sublcass WebGLWidget and add meshes to this.scene in the constructor:

class MyWidget extends WebGLWidget {
        let material = new THREE.MeshLambertMaterial(
            { color: 0xffff00 } );
        let geom = new THREE.OctahedronGeometry();
        this.scene.add( new THREE.Mesh( geom, material ) )
        this.moveCameraToShowAll();
    }
}

Don't forget to call this.moveCameraToShowAll to automatically place the camera.

Then instantiate your class to the DOM:

var myWidget = MyWidget('#widget')

Finally, use the following script to build your HTML page:

> ./buildwebgl octahedron.js

Now open octahedron.html.

Animation Loop

To allow for the possiblity of multiple widgets in one page, all WebGLWidget's are registered through a single animation loop.

The animation loop works through the registerWidgetForAnimation(widget) function, which takes any widget with the interface:

  • property widget.isChanged - indicates if draw should happen
  • method widget.draw() - draws at the right time
  • method widget.animate(timeElapsed) - animates with the given elapsed time since last animate

The WebGLWidget instances will call this automatically, but you can always add your custom objects onto the animation loop.

The animation loop is tied to the browser's internal drawing loop, and thus will draw all widgets at the same time.

Lights

If you want different lights, override the method:

this.setLights() {
    let newLight = /* make your own */
    this.lights.append(newLight);
}

When you rotate the camera and you want the light to follow the camera, make sure you set the parameter isRotatingLights in this.rotateCameraAroundSceneAroundScene. Otherwise, the light is static and you rotate around the shadows.

Resizing

The WebGL canvas needs to be manually resized. As such, if you have a resizable <div>, you need to set the resizing function:

window.addEventListener("resize", () => myWidget.resize());

This will resize the rendering canvas to the size of the surrounding <div>.

Handling Mouse/Pointer Input

Your typical widgets should handle mouse input, and this is quite tricky to do that with the typical DOM event listeners.

Thus the method this.bindCallbacks() will bind a number of convenient methods to the mouse input of this.divDom:

  • this.mousescroll( wheel )
  • this.mouseclick( x, y )
  • this.mousedoubleclick( x, y )
  • this.leftmousedrag( x0, y0, x1, y1 )
  • this.rightmousedrag( x0, y0, x1, y1 )
  • this.gesturedrag( rot, scale )

In particular, the x, y pairs are scaled to the size of your this.divDOM. The width and height are obtained from this.width() and this.height().

calcPointerXY( event )

If you're familiar with event handlers, the widget actually binds the following methods to their namesake handlers in this.divDOM:

  • this.mousedown( event )
  • this.mousemove( event )
  • this.mouseup( event )
  • this.mousewheel( event )
  • this.DOMMouseScroll( event )
  • this.touchstart( event )
  • this.touchmove( event )
  • this.touchend( event )
  • this.touchcancel( event )
  • this.gesturestart( event )
  • this.gesturechange( event )
  • this.gestureend( event )

If you're familiar with event handlers, just override them:

this.mousedown( e ) = /* your function */

Of course, this will override the convenient handlers from above.

Inside these routines, a very important function to calculate the pointer position this.calcPointerXY( event ). this function takes a DOM event that contains pointer positions in a particular webpage state based coordinate system and translates to the coordinate system of the this.divDom, from 0 to this.width(), and 0 to this.height(). This translation is crucial for embedded widgets that can be anywhere on a scrollabel webpage.

You can always call this function to unravel any event object.

Raycasting: Clicking on Meshes

For interactivity, you will want to be able to click on 3D objects in your scenes. The way to do this is, while in the constructor, add any meshes you want to keep track off into the list this.clickableMeshes:

this.clickableMeshes.push( mesh )

Add any identifying information the the mesh, maybe some kind of ID.

Then, during the input methods, call:

this.getClickedMeshes()

This assumes that this.calcPointerXY( event ) has been called at some point already and we have obtained this.pointerX and this.pointerY. The function will set the property this.clickedMesh which will refer to the top-most clicked mesh in this.clickableMeshes.

Heads-up Display

In WebGL apps, it's actually faster to draw text labels and such with HTML elements on top of the WebGL <canvas> rather than to draw text in the WebGL context itself.

In order to link 3D objects and HTML elements you will need to convert an object's 3D position into screen coordinates of your <div>.

Let's say you have three.js mesh, say this.clickedMesh from the last section. To calculate it's screen coordinates:

let screen = this.calcScreenXYOfPos( this.clickedMesh );

You can then instantiate a <div>, and a nice little object wrapper around a movable <div> is PopupText.

  • class PopupText

    • constructor( selector, backgroundColor='white', textColor='black', opacity=0.7 )
    • this.move( x, y )
    • this.hide()
    • this.html( text )
    • this.remove()

So you can then:

this.hover.move( screen.x, screen.y );

the PopupText( this.selector, "lightblue", "blue" );

WebGlWidget class

Constructor:

  • constructor(selector, backgroundColor='black') - creates WebGL context to a <div> referred to by selector, sets the background color, registers it with the main animation loop, that draws the scene.

Properties:

  • this.divDom - DOM element pointed to by selector, this is where the <canvas> is inserted and takes the size from

  • this.backgroundColor - stores the color used to build scene, if you need to change.

  • this.scene - Three.js scene

  • this.scene.fog - Three.js fog added

  • this.lights - holds the lights that is inserted into this.scene

  • this.camera - Three.js camera

  • this.zoom - distance between camera and this.scene

  • this.renderer - Three.js renderer

  • this.renderDom - DOM element bound to renderer

  • this.clickableMeshes - list of all meshes for picking. Add meshes here if you want to keep track of them. You can always add properties to those meshses for further analysis

  • this.clickedMesh - top-most picked mesh nothing picked=null

Methods:

  • this.resize() - resize this.renderDom to fit the this.divDom and sets aspect ratio

  • this.setLights() - overrideable function to set your own lights, just push to this.lights

  • this.draw() - draws the scene to the screen

  • this.animate( elapsedTime ) - this is called every time the animation loop is called, elapsedTime gives the time since last call

  • this.getSceneRadius() - gives the bounding radius from this.scene.position of all the meshes in this.scene

  • this.moveCameraToShowAll() - conveniently moves this.camera to twice the distance of this.getSceneRadius()

  • this.rotateCameraAroundScene ( xRotAngle, yRotAngle, zRotAngle, isRotateLights=true ) - rotates this.camera around this.scene.position with respect to the axes:

    • z - direction of this.camera to center
    • y - direction of this.camera.up
    • x - the other orthonormal direction
  • this.setCameraZoomFromScene ( newZoom ) - moves this.camera to the distance newZoom from this.scene in current direction

  • this.getDepth( pos ) - converts pos into a depth value relative to this.scene.position in the camera direction. Negative are in front of this.scene.position. Positive values are behind.

  • this.getClickedMeshes() - find the this.clickedMesh given the current scne

Methods inherited from Widget:

  • this.mousescroll( wheel ) -
  • this.mouseclick( x, y ) -
  • this.mousedoubleclick( x, y ) -
  • this.leftmousedrag( x0, y0, x1, y1 ) -
  • this.rightmousedrag( x0, y0, x1, y1 ) -
  • this.gesturedrag( rot, scale ) -

  • this.draw() - override this and call super to add extra drawing to your object, e.g. updating pop up windows and heads-up displays

  • this.animate( elapsedTime ) - override this to animate your meshes