In this tutorial, we'll create a webgl demo of a dog with a cute animation:
Click and drag to look around.
We'll be creating geometries by hand instead of using a 3d modeling program. This way we can explore the essentials of setting up a tree of matrix calculations to model joints without worrying about the complexities of 3d file formats.
Let's begin by creating a box centered at the origin:
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var box = {
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]]
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(box)
})
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * vec4(position,1);
}
`,
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
To run this code, install node.js and npm, then from a new project directory do:
npm install -g budo && npm install regl regl-camera gl-mat4 gl-vec3
.
Save this source code to main.js
and do budo main.js
. Finally visit
http://localhost:9966
in your browser.
In this example, we have a box with 8 vertices and 12 triangles (2 for each of the 6 sides).
This example uses the standard derivatives extension to give our model a flat shaded coloring scheme calculated from the normal of each face.
Now let's create two more boxes to go with the first box:
one above (+y)
and one below (-y)
:
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var boxes = [
{
positions: [[+0.5,+1.0,+0.5],[+0.5,+1.0,-0.5],[-0.5,+1.0,-0.5],
[-0.5,+1.0,+0.5],[+0.5,+2.0,+0.5],[+0.5,+2.0,-0.5],
[-0.5,+2.0,-0.5],[-0.5,+2.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]]
},
{
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]]
},
{
positions: [[+0.5,-2.0,+0.5],[+0.5,-2.0,-0.5],[-0.5,-2.0,-0.5],
[-0.5,-2.0,+0.5],[+0.5,-1.0,+0.5],[+0.5,-1.0,-0.5],
[-0.5,-1.0,-0.5],[-0.5,-1.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]]
}
]
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(boxes)
})
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * vec4(position,1);
}
`,
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
The box
variable has become an array of boxes
. +1.5
has been added to
the y
coordinate of every vertex in the first box and -1.5
has been added to
every y
coordinate of the last box. The middle box is the same as before.
Now let's create a model matrix for each box. In an update(time)
function called
each frame, we'll set each model matrix to the identity, then we'll rotate the
bottom box about the x axis.
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var mat4 = require('gl-mat4')
var boxes = [
{
positions: [[+0.5,+1.0,+0.5],[+0.5,+1.0,-0.5],[-0.5,+1.0,-0.5],
[-0.5,+1.0,+0.5],[+0.5,+2.0,+0.5],[+0.5,+2.0,-0.5],
[-0.5,+2.0,-0.5],[-0.5,+2.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16)
},
{
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16)
},
{
positions: [[+0.5,-2.0,+0.5],[+0.5,-2.0,-0.5],[-0.5,-2.0,-0.5],
[-0.5,-2.0,+0.5],[+0.5,-1.0,+0.5],[+0.5,-1.0,-0.5],
[-0.5,-1.0,-0.5],[-0.5,-1.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16)
}
]
function update (t) {
mat4.identity(boxes[0].model)
mat4.identity(boxes[1].model)
mat4.identity(boxes[2].model)
mat4.rotateX(boxes[2].model, boxes[2].model, Math.sin(t*5)*0.8)
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(boxes)
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
This looks cool, with the bottom box swinging like a pendulum about the origin,
but to demonstrate rigging, we should move that pivot point closer to each box's
center of mass. For the top box, that's [+0.0,+1.5,+0.0]
and for the bottom
box it's [+0.0,-1.5,+0.0]
. The center box's pivot will be [+0.0,+0.0,+0.0]
.
We'll store the pivot alongside the positions, cells, and model for each box.
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var mat4 = require('gl-mat4')
var vec3 = require('gl-vec3')
var boxes = [
{
positions: [[+0.5,+1.0,+0.5],[+0.5,+1.0,-0.5],[-0.5,+1.0,-0.5],
[-0.5,+1.0,+0.5],[+0.5,+2.0,+0.5],[+0.5,+2.0,-0.5],
[-0.5,+2.0,-0.5],[-0.5,+2.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,+1.5,+0.0]
},
{
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,+0.0,+0.0]
},
{
positions: [[+0.5,-2.0,+0.5],[+0.5,-2.0,-0.5],[-0.5,-2.0,-0.5],
[-0.5,-2.0,+0.5],[+0.5,-1.0,+0.5],[+0.5,-1.0,-0.5],
[-0.5,-1.0,-0.5],[-0.5,-1.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,-1.5,+0.0]
}
]
var tmpv = new Float32Array(3)
function update (t) {
mat4.identity(boxes[0].model)
mat4.identity(boxes[1].model)
mat4.identity(boxes[2].model)
mat4.translate(boxes[0].model,boxes[0].model,boxes[0].pivot)
mat4.translate(boxes[1].model,boxes[1].model,boxes[1].pivot)
mat4.translate(boxes[2].model,boxes[2].model,boxes[2].pivot)
mat4.rotateX(boxes[2].model, boxes[2].model, Math.sin(t*5)*0.8)
mat4.translate(boxes[0].model,boxes[0].model,vec3.negate(tmpv,boxes[0].pivot))
mat4.translate(boxes[1].model,boxes[1].model,vec3.negate(tmpv,boxes[1].pivot))
mat4.translate(boxes[2].model,boxes[2].model,vec3.negate(tmpv,boxes[2].pivot))
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(boxes)
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
To apply the pivots, in the update function we first translate the model matrix by the pivot coordinate, then we perform any rotations, and finally we translate the model matrix back by the negated pivot coordinates.
We reuse a tmpv
variable declared outside of the update()
function for the negation to avoid allocating memory inside the update()
loop
which runs every frame. Avoiding allocations in your code that runs every frame
will keep the garbage collector from activating and introducing jitter into your
demos.
Now that we can apply rotations to each box's pivot point, let's refactor our data structures so that we can establish a tree of joints. This way, when we rotate a parent joint, any child joints attached to that parent joint will also rotate about the parent joint's pivot. And each child joint can further have children that rotate according to this tree of joints.
First let's convert our boxes array into an object so we can give each box a
name. Then we can add a parent
key to each box to describe the tree of joints.
We'll get around to using this parent
key later, but for now let's just get
the existing code working with the new data structure.
We'll call the boxes top
, middle
, and bottom
. The bottom box will be the
root of our joint tree with the middle box a child of the bottom box and the top
box a child of the middle box.
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var mat4 = require('gl-mat4')
var vec3 = require('gl-vec3')
var boxes = {
top: {
positions: [[+0.5,+1.0,+0.5],[+0.5,+1.0,-0.5],[-0.5,+1.0,-0.5],
[-0.5,+1.0,+0.5],[+0.5,+2.0,+0.5],[+0.5,+2.0,-0.5],
[-0.5,+2.0,-0.5],[-0.5,+2.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,+1.5,+0.0],
parent: 'middle'
},
middle: {
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,+0.0,+0.0],
parent: 'bottom'
},
bottom: {
positions: [[+0.5,-2.0,+0.5],[+0.5,-2.0,-0.5],[-0.5,-2.0,-0.5],
[-0.5,-2.0,+0.5],[+0.5,-1.0,+0.5],[+0.5,-1.0,-0.5],
[-0.5,-1.0,-0.5],[-0.5,-1.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pivot: [+0.0,-1.5,+0.0],
parent: null
}
}
var boxProps = Object.values(boxes)
var tmpv = new Float32Array(3)
function update (t) {
mat4.identity(boxes.top.model)
mat4.identity(boxes.middle.model)
mat4.identity(boxes.bottom.model)
mat4.translate(boxes.top.model,boxes.top.model,boxes.top.pivot)
mat4.translate(boxes.middle.model,boxes.middle.model,boxes.middle.pivot)
mat4.translate(boxes.bottom.model,boxes.bottom.model,boxes.bottom.pivot)
mat4.rotateX(boxes.bottom.model, boxes.bottom.model, Math.sin(t*5)*0.8)
mat4.translate(boxes.top.model,boxes.top.model,
vec3.negate(tmpv,boxes.top.pivot))
mat4.translate(boxes.middle.model,boxes.middle.model,
vec3.negate(tmpv,boxes.middle.pivot))
mat4.translate(boxes.bottom.model,boxes.bottom.model,
vec3.negate(tmpv,boxes.bottom.pivot))
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(boxProps)
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
Good so far with the same behavior as before.
We'll next add an extra matrix for every box called pose
. Instead of modifying
the model
matrix, we'll modify the pose
matrix in our update()
function
and create a new function updateModels()
to calculate the model
matrix from
each pose
matrix in a box's joint tree.
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, { phi: 0.4, theta: 0.7 })
var mat4 = require('gl-mat4')
var vec3 = require('gl-vec3')
var boxes = {
top: {
positions: [[+0.5,+1.0,+0.5],[+0.5,+1.0,-0.5],[-0.5,+1.0,-0.5],
[-0.5,+1.0,+0.5],[+0.5,+2.0,+0.5],[+0.5,+2.0,-0.5],
[-0.5,+2.0,-0.5],[-0.5,+2.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pose: new Float32Array(16),
pivot: [+0.0,+1.5,+0.0],
parent: 'middle'
},
middle: {
positions: [[+0.5,-0.5,+0.5],[+0.5,-0.5,-0.5],[-0.5,-0.5,-0.5],
[-0.5,-0.5,+0.5],[+0.5,+0.5,+0.5],[+0.5,+0.5,-0.5],
[-0.5,+0.5,-0.5],[-0.5,+0.5,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pose: new Float32Array(16),
pivot: [+0.0,+0.0,+0.0],
parent: 'bottom'
},
bottom: {
positions: [[+0.5,-2.0,+0.5],[+0.5,-2.0,-0.5],[-0.5,-2.0,-0.5],
[-0.5,-2.0,+0.5],[+0.5,-1.0,+0.5],[+0.5,-1.0,-0.5],
[-0.5,-1.0,-0.5],[-0.5,-1.0,+0.5]],
cells: [[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]],
model: new Float32Array(16),
pose: new Float32Array(16),
pivot: [+0.0,-1.5,+0.0],
parent: null
}
}
var boxProps = Object.values(boxes)
var tmpv = new Float32Array(3)
var tmpm = new Float32Array(16)
function update (t) {
boxProps.forEach(function (box) {
mat4.identity(box.pose)
})
mat4.rotateX(boxes.bottom.pose, boxes.bottom.pose, Math.sin(t*1)*0.8)
mat4.rotateY(boxes.middle.pose, boxes.middle.pose, Math.sin(t*2)*1.5)
mat4.rotateZ(boxes.top.pose, boxes.top.pose, Math.sin(t*8)*0.5)
updateModels()
}
function updateModels () {
boxProps.forEach(function (box) {
mat4.identity(box.model)
var b = box
while (b) {
mat4.identity(tmpm)
mat4.translate(tmpm, tmpm, b.pivot)
mat4.multiply(tmpm, tmpm, b.pose)
mat4.translate(tmpm, tmpm, vec3.negate(tmpv,b.pivot))
mat4.multiply(box.model, tmpm, box.model)
b = boxes[b.parent]
}
})
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(boxProps)
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
In this example all three boxes have associated rotations on separate axes, and as a parent box moves, so do its children. The joint tree is working how we need it to work.
Let's take a closer look at the code that updates the models:
function updateModels () {
boxProps.forEach(function (box) {
mat4.identity(box.model)
var b = box
while (b) {
mat4.identity(tmpm)
mat4.translate(tmpm, tmpm, b.pivot)
mat4.multiply(tmpm, tmpm, b.pose)
mat4.translate(tmpm, tmpm, vec3.negate(tmpv,b.pivot))
mat4.multiply(box.model, tmpm, box.model)
b = boxes[b.parent]
}
})
}
Before, we translated each box by its pivot
point, performed our rotation,
then translated each box by the negated pivot
. In the updated code, we perform
the rotations on the pose
matrix. A translation by the pivot
is first
performed on a temporary matrix tmpm
, then the pose
matrix rotations are
multiplied into tmpm
, and finally tmpm
is translated by the negated
pivot
. These steps are repeated for every pose matrix along the box's joint
tree, from the leaf joint to the root joint. The transformations for the whole
joint tree are collected into the model matrix for each box.
With patience or the assistance of a 3d modeling program, you can build up a more complicated collection of shape geometries to animate.
Here is a the dog model from the title demo that I created by hand:
module.exports = {
chest: {
pivot: [0,0,0],
positions: [
[+0.05,-0.09,+0.15],[+0.10,-0.07,-0.25],[-0.10,-0.07,-0.25],[-0.05,-0.09,+0.15],
[+0.11,+0.18,+0.15],[+0.11,+0.20,-0.22],[-0.11,+0.20,-0.22],[-0.11,+0.18,+0.15]
],
cells: [
[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,7],[7,6,2],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
rear: {
parent: 'chest',
pivot: [+0.00,+0.00,-0.25],
positions: [
[+0.11,-0.06,-0.26],[+0.08,-0.08,-0.40],[-0.08,-0.08,-0.40],[-0.11,-0.06,-0.26],
[+0.11,+0.20,-0.23],[+0.08,+0.15,-0.45],[-0.08,+0.15,-0.45],[-0.11,+0.20,-0.23]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
neck: {
parent: 'chest',
pivot: [+0.00,+0.00,+0.15],
positions: [
[+0.08,+0.15,+0.30],[+0.08,-0.08,+0.16],[-0.08,-0.08,+0.16],[-0.08,+0.15,+0.30],
[+0.06,+0.28,+0.25],[+0.10,+0.17,+0.05],[-0.10,+0.17,+0.05],[-0.06,+0.28,+0.25]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
head: {
parent: 'neck',
pivot: [0,0,+0.20],
positions: [
[+0.08,+0.34,+0.35],[+0.06,+0.20,+0.08],[-0.06,+0.20,+0.08],[-0.08,+0.34,+0.35],
[+0.04,+0.43,+0.30],[+0.03,+0.39,+0.15],[-0.03,+0.39,+0.15],[-0.04,+0.43,+0.30]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
snout: {
parent: 'head',
pivot: [+0.00,+0.36,+0.34],
positions: [
[+0.02,+0.36,+0.47],[+0.06,+0.34,+0.35],[-0.06,+0.34,+0.35],[-0.02,+0.36,+0.47],
[+0.02,+0.40,+0.46],[+0.05,+0.40,+0.32],[-0.05,+0.40,+0.32],[-0.02,+0.40,+0.46]
],
cells: [
[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,5],[6,5,2],
[2,3,7],[7,6,2],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
jaw: {
parent: 'head',
pivot: [+0.00,+0.30,+0.30],
positions: [
[+0.02,+0.30,+0.46],[+0.04,+0.28,+0.30],[-0.04,+0.28,+0.30],[-0.02,+0.30,+0.46],
[+0.02,+0.32,+0.46],[+0.05,+0.35,+0.27],[-0.06,+0.35,+0.27],[-0.02,+0.32,+0.46]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
leftEar: {
parent: 'head',
pivot: [+0.05,+0.40,+0.20],
positions: [
[+0.09,+0.36,+0.25],[+0.09,+0.36,+0.16],[+0.07,+0.36,+0.16],[+0.07,+0.36,+0.25],
[+0.05,+0.43,+0.22],[+0.05,+0.41,+0.18],[+0.03,+0.40,+0.16],[+0.04,+0.42,+0.25]
],
cells: [
[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,6],[6,5,1],
[2,3,7],[7,6,2],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
rightEar: {
parent: 'head',
pivot: [-0.05,+0.40,+0.20],
positions: [
[-0.09,+0.36,+0.25],[-0.09,+0.36,+0.16],[-0.07,+0.36,+0.16],[-0.07,+0.36,+0.25],
[-0.05,+0.43,+0.22],[-0.05,+0.41,+0.18],[-0.03,+0.40,+0.16],[-0.04,+0.42,+0.25]
],
cells: [
[2,1,0],[3,2,0],[0,1,4],[5,4,1],[1,2,6],[6,5,1],
[2,3,7],[7,6,2],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
leftFrontLeg: {
parent: 'chest',
pivot: [+0.10,+0.05,+0.20],
positions: [
[+0.10,-0.40,+0.10],[+0.10,-0.40,+0.05],[+0.05,-0.40,+0.05],[+0.05,-0.40,+0.10],
[+0.13,+0.10,+0.15],[+0.13,+0.10,+0.05],[+0.07,+0.10,+0.05],[+0.07,+0.10,+0.15]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
rightFrontLeg: {
parent: 'chest',
pivot: [-0.10,+0.05,+0.20],
positions: [
[-0.10,-0.40,+0.10],[-0.10,-0.40,+0.05],[-0.05,-0.40,+0.05],[-0.05,-0.40,+0.10],
[-0.13,+0.10,+0.15],[-0.13,+0.10,+0.05],[-0.07,+0.10,+0.05],[-0.07,+0.10,+0.15]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
leftBackLeg: {
parent: 'rear',
pivot: [+0.10,+0.05,-0.30],
positions: [
[+0.10,-0.40,-0.38],[+0.10,-0.40,-0.43],[+0.05,-0.40,-0.43],[+0.05,-0.40,-0.38],
[+0.13,+0.10,-0.33],[+0.13,+0.10,-0.43],[+0.07,+0.10,-0.43],[+0.07,+0.10,-0.33]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
rightBackLeg: {
parent: 'rear',
pivot: [-0.10,+0.05,-0.30],
positions: [
[-0.10,-0.40,-0.38],[-0.10,-0.40,-0.43],[-0.05,-0.40,-0.43],[-0.05,-0.40,-0.38],
[-0.13,+0.10,-0.33],[-0.13,+0.10,-0.43],[-0.07,+0.10,-0.43],[-0.07,+0.10,-0.33]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
},
tail: {
parent: 'rear',
pivot: [0,0,-0.40],
positions: [
[+0.02,+0.38,-0.58],[+0.02,+0.35,-0.61],[-0.02,+0.35,-0.61],[-0.02,+0.38,-0.58],
[+0.02,+0.19,-0.41],[+0.02,+0.16,-0.45],[-0.02,+0.16,-0.45],[-0.02,+0.19,-0.41]
],
cells: [
[2,1,0],[3,2,0],[0,1,5],[5,4,0],[1,2,5],[6,5,2],
[2,3,6],[7,6,3],[7,3,0],[0,4,7],[4,5,6],[4,6,7]
]
}
}
Adapting the previous box rigging code, we can make the dog wag its tail while slowly moving its head side to side:
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl,
{ phi: 0.4, theta: 0.7, distance: 3.2 })
var mat4 = require('gl-mat4')
var vec3 = require('gl-vec3')
var shapes = Object.assign({}, require('./dog.js'))
var pose = {}
Object.keys(shapes).forEach(function (key) {
shapes[key] = Object.assign({
model: new Float32Array(16),
pose: new Float32Array(16)
}, shapes[key])
pose[key] = shapes[key].pose
})
var shapeProps = Object.values(shapes)
var tmpv = new Float32Array(3)
var tmpm = new Float32Array(16)
function update (t) {
shapeProps.forEach(function (shape) {
mat4.identity(shape.pose)
})
mat4.rotateY(pose.tail, pose.tail, Math.sin(t*12))
mat4.rotateY(pose.neck, pose.neck, Math.sin(t)*0.5)
mat4.rotateY(pose.head, pose.head, Math.sin(t)*0.5)
updateModels()
}
function updateModels () {
shapeProps.forEach(function (shape) {
mat4.identity(shape.model)
var s = shape
while (s) {
mat4.identity(tmpm)
mat4.translate(tmpm, tmpm, s.pivot)
mat4.multiply(tmpm, tmpm, s.pose)
mat4.translate(tmpm, tmpm, vec3.negate(tmpv,s.pivot))
mat4.multiply(shape.model, tmpm, shape.model)
s = shapes[s.parent]
}
})
}
var draw = {
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.solid(shapeProps)
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
Adding axes, a moving grid, and a full walking animation, we get:
var regl = require('regl')({
extensions: [ 'oes_standard_derivatives' ] })
var camera = require('regl-camera')(regl, {
phi: 0.4, theta: 0.7, distance: 3.2, center: [0,0.2,0] })
var mat4 = require('gl-mat4')
var vec3 = require('gl-vec3')
var quat = require('gl-quat')
var shapes = Object.assign({}, require('./dog.js'))
var pose = {}
Object.keys(shapes).forEach(function (key) {
shapes[key] = Object.assign({
model: new Float32Array(16),
pose: new Float32Array(16)
}, shapes[key])
pose[key] = shapes[key].pose
})
var shapeProps = Object.values(shapes)
var tmpv = new Float32Array(3)
var tmpm = new Float32Array(16)
var PI = Math.PI
function update (t) {
shapeProps.forEach(function (shape) {
mat4.identity(shape.pose)
})
var w = 12
mat4.rotateY(pose.neck, pose.neck, Math.sin(t)*0.5)
mat4.rotateY(pose.head,pose.head,powSign(Math.sin(t),0.7)*0.5)
mat4.rotateY(pose.tail, pose.tail, Math.sin(t*w))
mat4.rotateX(pose.leftFrontLeg,pose.leftFrontLeg,Math.sin(t*w)*0.2)
mat4.rotateX(pose.rightFrontLeg,pose.rightFrontLeg,Math.sin(t*w+PI*0.5)*0.2)
mat4.rotateX(pose.leftBackLeg,pose.leftBackLeg,Math.sin(t*w+Math.PI*0.5)*0.2)
mat4.rotateX(pose.rightBackLeg,pose.rightBackLeg,Math.sin(t*w)*0.2)
mat4.rotateX(pose.chest,pose.chest,Math.sin(t*w*2)*0.02)
mat4.rotateX(pose.rear,pose.rear,Math.sin(t*w*2)*0.02)
mat4.rotateX(pose.neck,pose.neck,Math.sin(t*0.25)*0.2-Math.sin(t*w*2)*0.015)
mat4.rotateX(pose.jaw,pose.jaw,Math.max(-0.2,Math.sin(t*w)*0.02))
mat4.rotateX(pose.snout,pose.snout,Math.min(-0.2,Math.sin(t*w)*0.01))
mat4.rotateZ(pose.leftEar,pose.leftEar,Math.max(0,Math.sin(t*w*2)*0.2))
mat4.rotateZ(pose.rightEar,pose.rightEar,Math.max(0,-Math.sin(t*w*2)*0.2))
updateModels()
}
function powSign (x,n) { return Math.pow(Math.abs(x),n)*Math.sign(x) }
function updateModels () {
shapeProps.forEach(function (shape) {
mat4.identity(shape.model)
var s = shape
while (s) {
mat4.identity(tmpm)
mat4.translate(tmpm, tmpm, s.pivot)
mat4.multiply(tmpm, tmpm, s.pose)
mat4.translate(tmpm, tmpm, vec3.negate(tmpv,s.pivot))
mat4.multiply(shape.model, tmpm, shape.model)
s = shapes[s.parent]
}
})
}
var axes = [ { axis: [1,0,0] }, { axis: [0,1,0] }, { axis: [0,0,1] } ]
var draw = {
axis: axis(regl,1.3),
grid: grid(regl, 1),
solid: solid(regl)
}
regl.frame(function (context) {
regl.clear({ color: [0.3,0.22,0.22,1], depth: true })
camera(function () {
draw.axis(axes)
draw.solid(shapeProps)
draw.grid()
})
update(context.time)
})
function solid (regl) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
void main () {
vec3 N = normalize(cross(dFdx(vpos),dFdy(vpos)));
gl_FragColor = vec4(N*0.5+0.5,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view
* (model * vec4(position,1) + vec4(0,0.4,0,0));
}
`,
uniforms: {
model: regl.prop('model')
},
attributes: {
position: regl.prop('positions')
},
elements: regl.prop('cells')
})
}
function axis (regl, d) {
var model = new Float32Array(16)
var positions = new Float32Array(3*6)
return regl({
frag: `
precision highp float;
uniform vec3 axis;
void main () {
gl_FragColor = vec4(axis,1);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view, model;
uniform vec3 eye, center;
attribute vec3 position;
void main () {
gl_Position = projection * view * model * vec4(position,1);
}
`,
uniforms: {
axis: regl.prop('axis'),
model: function (context, props) {
mat4.identity(model)
var angle = 0
if (props.axis[0] > 0) {
angle = Math.atan2(context.eye[2],context.eye[1]) + Math.PI/2
} else if (props.axis[1] > 0) {
angle = Math.atan2(context.eye[0],context.eye[2])
} else if (props.axis[2] > 0) {
angle = Math.atan2(context.eye[1],context.eye[0]) + Math.PI/2
}
mat4.rotate(model, model, angle, props.axis)
var tmpm = new Float32Array(16)
var q = [0,0,0,0]
quat.rotationTo(q,[0,1,0],props.axis)
mat4.fromQuat(tmpm,q)
mat4.multiply(model, model, tmpm)
return model
}
},
attributes: {
position: [
[0,0,0],
[0,d-0.2,0],
[+0.05,d-0.2,0],
[0,d,0],
[-0.05,d-0.2,0],
[0,d-0.2,0]
],
axis: regl.prop('axis')
},
count: 6,
primitive: 'line strip'
})
}
function grid (regl, d) {
return regl({
frag: `
precision highp float;
#extension GL_OES_standard_derivatives: enable
varying vec3 vpos;
uniform float time, d;
void main () {
float q = 4.0;
vec2 p = vpos.xz*q + vec2(0,time*3.0);
vec2 m = abs(vec2(1)-mod(p,vec2(1))*2.0)*2.0-1.0;
m = max(m,pow(abs(vpos.xz)/d,vec2(8)));
vec2 fw = fwidth(p);
float fm = min(fw.x,fw.y);
float x = pow(max(0.0,max(m.x,m.y)),1.0/4.0/fm);
//if (x < 0.5) discard;
gl_FragColor = vec4(vec3(x),x);
}
`,
vert: `
precision highp float;
uniform mat4 projection, view;
attribute vec3 position;
varying vec3 vpos;
void main () {
vpos = position;
gl_Position = projection * view * vec4(position,1);
}
`,
uniforms: {
time: regl.context('time'),
d: d
},
attributes: {
position: [[+d,+0,+d],[+d,+0,-d],[-d,+0,-d],[-d,+0,+d]]
},
elements: [[0,1,2],[0,2,3]],
blend: {
enable: true,
func: { src: 'src alpha', dst: 'one minus src alpha' }
},
depth: { mask: false }
})
}
Pretty cool. This should be enough for you to get starting making your own 3d animations. If you want to animate a single 3d surface with a skeletal armature instead of separate boxes, check out chinedufn's article on dual quaternions.