webglstarterkit
v0.1.1
Published
helper module to setup WebGL apps
Downloads
6
Readme
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 byselector
, this is where the<canvas>
is inserted and takes the size fromthis.backgroundColor
- stores the color used to build scene, if you need to change.this.scene
- Three.js scenethis.scene.fog
- Three.js fog addedthis.lights
- holds the lights that is inserted intothis.scene
this.camera
- Three.js camerathis.zoom
- distance between camera andthis.scene
this.renderer
- Three.js rendererthis.renderDom
- DOM element bound to rendererthis.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 analysisthis.clickedMesh
- top-most picked mesh [nothing picked=null]
Methods:
this.resize()
- resizethis.renderDom
to fit thethis.divDom
and sets aspect ratiothis.setLights()
- overrideable function to set your own lights, just push tothis.lights
this.draw()
- draws the scene to the screenthis.animate( elapsedTime )
- this is called every time the animation loop is called,elapsedTime
gives the time since last callthis.getSceneRadius()
- gives the bounding radius fromthis.scene.position
of all the meshes inthis.scene
this.moveCameraToShowAll()
- conveniently movesthis.camera
to twice the distance ofthis.getSceneRadius()
this.rotateCameraAroundScene ( xRotAngle, yRotAngle, zRotAngle, isRotateLights=true )
- rotatesthis.camera
aroundthis.scene.position
with respect to the axes:z
- direction ofthis.camera
to centery
- direction ofthis.camera.up
x
- the other orthonormal direction
this.setCameraZoomFromScene ( newZoom )
- movesthis.camera
to the distancenewZoom
fromthis.scene
in current directionthis.getDepth( pos )
- convertspos
into a depth value relative tothis.scene.position
in the camera direction. Negative are in front ofthis.scene.position
. Positive values are behind.this.getClickedMeshes()
- find thethis.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 callsuper
to add extra drawing to your object, e.g. updating pop up windows and heads-up displaysthis.animate( elapsedTime )
- override this to animate your meshes