This talk is called:
Building a Rudimentary 3D Engine with SVG
with this guy:
I am an author:
Wrinklefree jQuery and HTML5
and
Wrinklefree JS for Hipsters
on Leanpub (https://leanpub.com/u/matthiasak)
Firstly:
A confession
I am not really a 3D / graphics developer
When I built
this Rudimentary 3D Engine
It took me
forever
to understand basic 3D engine principles.
What's the story, anyway?
var testBuilding = { width: 800 , length: 800 , rpa: 1 , rpc: 1 , swaEaveHeight: 240 , swcEaveHeight: 240 };
The problem
How do I make it easier to order very detailed buildings
with hundreds of engineering values
without requiring software keyboard interaction on the iPad?
This talk is now:
Things
Matt didn't know about graphics programming
that are
mildly interesting
and relate to
JavaScript and metal buildings
Or,
Nuances
that
SVG fixes
that I
didn't know existed
Thus,
I began my training.
Serious time
Why SVG?
- Widely supported and very portable
- Kept me close to the math (and not too much else)
- SVG makes DOM elements
- I just finished working with Raphael.js
Raphael.js
- Lets you work with IE8 (VML)
- Pretty quick to get up and running
Raphael seems simple at first
// Creates canvas 320 x 200 at 10, 50 var paper = Raphael(10, 50, 320, 200); // Creates circle at x = 50, y = 40, with radius 10 var circle = paper.circle(50, 40, 10); // Sets the fill attribute of the circle to red (#f00) circle.attr("fill", "#f00"); // Sets the stroke attribute of the circle to white circle.attr("stroke", "#fff");
So I went native!
- IE9+
- Just a bunch of paths
- Can modify/persist/mess with the <polygon> elements
Es-Vee-Geeeee
<div id="svg-div"> <svg version="1.1"> <polygon style="fill:rgb(43,244,113);fill-opacity:1" points="241.76134566328196,367 241.76134566328196,551.6185119801582 84.47296084318484,459.4990933923533 84.47296084318484,367 136.974255293421,243.752363252747 241.76134566328196,367"></polygon> </svg> </div>
These points are the path in the 2D space
- (241.76134566328196,367)
- (241.76134566328196,551.6185119801582)
- (84.47296084318484,459.4990933923533)
- (84.47296084318484,367)
- (136.974255293421,243.752363252747)
- (241.76134566328196,367) - close the <polygon>
These points are calculated by the engine from these 3d points
var ewb = [ [0,0,0], [0,0,swcZ], [0,swcEaveY, swcZ], [0, ridgeHeight, swcZ/2], [0,swaEaveY,0] ];
Which I got from
var testBuilding = { width: 800, length: 800, rpa: 1, rpc: 1, swaEaveHeight: 240, swcEaveHeight: 240 };
Make me some polygons
var shapes = createPolygonsFromBuilding(testBuilding); var svgShapes = {};
Make me some polygons
function createPolygonsFromBuilding(building) { ... var ridgeHeight = building.swaEaveHeight+building.swcEaveHeight; var maxDimension = building.width > building.length ? building.width : building.width; ... var opacity = 1; return [ new Polygon('swa', swa, randomColor(), opacity, 0), new Polygon('ewb', ewb, randomColor(), opacity, 0), new Polygon('swc', swc, randomColor(), opacity, 0), new Polygon('ewd', ewd, randomColor(), opacity, 0), new Polygon('rpa', rpa, randomColor(), opacity, 0), new Polygon('rpc', rpc, randomColor(), opacity, 0), createGround(-.5*building.length, 1.5*building.length, -1.5*building.width, .5*building.width) ] }
Erm.. Polygon?
function Polygon(id, coordinates, color, opacity, stroke, forceToBackground){ this.id = id; this.coordinates = coordinates; this.color = color; this.opacity = opacity; this.stroke = stroke; this.forceToBackground = forceToBackground || false; }
Color!!!
/** * Return a random RGB color * @return {[Number]} an RGB array e.g. [200, 30, 47] => [R, G, B] */ function randomColor() { var color = []; for (var j = 0; j < 3; j++) { color[j] = Math.floor(Math.random() * 255); } return color; }
I have a camera
/** * Updates the camera's location */ function setCameraPosition(x, y, z) { camera = [x, y, z]; }
Let's talk about
sets() baby!
sets() all over!
window.addEventListener("load", function() { ... addSVGsFromShapes(shapes, svgShapes, svg); // populate the <svg> with <polygon>s ... }, false);
function addSVGsFromShapes(shapes, svgShapes, svg) { for (var i = 0; i < shapes.length; i++) { var shape = shapes[i]; var poly = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); var rgb = "rgb(" + shape.color[0] + "," + shape.color[1] + "," + shape.color[2] + ")"; poly.setAttribute("style", "fill:" + rgb + ";fill-opacity:" + shape.opacity); svgShapes[shape.id] = poly; svg.appendChild(poly); } }
window.addEventListener("load", function() { ... rearrangePolygonsByCameraDistances(shapes, svgShapes, camera, svg); ... }, false);
function rearrangePolygonsByCameraDistances(shapes, svgShapes, camera, svg){ var cameraDistances = {}; for (var i = 0; i < shapes.length; i++) { cameraDistances[shapes[i].id] = distance(average(shapes[i].coordinates), camera); } shapes.sort(byCameraDistance); var length = shapes.length; for(var i = 0; i < length; i++){ svg.appendChild(svgShapes[shapes[i].id]); } }
window.addEventListener("load", function() { ... requestAnimationFrame(function(){ paint(shapes, svgShapes, camera, width, height, fov); }); }, false);
function paint(shapes, svgShapes, camera, width, height, fov) { var len = shapes.length; for (var i = 0; i < len; i++) { var shape = shapes[i] , coords = shape.coordinates , num_coords = coords.length // Start at the end to close the polygon , point = project(coords[coords.length - 1], camera, width, height, fov) , svgPoints = point[0] + "," + point[1]; for (var j = 0; j < num_coords; j++) { point = project(coords[j], camera, width, height, fov); // The magic! svgPoints += " " + point[0] + "," + point[1]; } svgShapes[shape.id].setAttribute("points", svgPoints); // set the points on } }
Projections (3D space onto the 2D screen)
function project(point, camera, width, height, fov) { // what's greater, the screen width or height? var minDimension = width - height < 0 ? width : height; /* xfov = 70; fov = xfov*0.0174532925; // 1 degree = 0.0174532925 radians; */ var scale = minDimension / fov , z_weighting = point[2] + camera[2] , x = (width/2 + scale * (point[0] + camera[0]) / z_weighting) , y = (height/2 + scale * (point[1] + camera[1]) / z_weighting); return [x,y]; }
Feeling powerful
So what about
mouse and touch events
???
Mousedown / touchstart
window.addEventListener("mousedown", inputStart); window.addEventListener("touchstart", inputStart); function inputStart(e){ mousedown = 1; mouseDownX = e.pageX; mouseDownY = e.pageY; }
Mouseup / touchend
window.addEventListener("mouseup", inputEnd); window.addEventListener("touchend", inputEnd); function inputEnd(e){ mousedown = 0; mouseMoveX = 0; }
Mousemove / touchmove
window.addEventListener("mousemove", inputMove); window.addEventListener("touchmove", inputMove); function inputMove(e){ e.preventDefault(); if(!mousedown) return; var _dX = e.pageX - (mouseMoveX || mouseDownX); mouseMoveX = e.pageX; if((dX > 0 && _dX < 0) || (dX < 0 && _dX > 0)){ mouseDownX = mouseMoveX; } dX = _dX; rotateAllSurfaces("y", (Math.PI * dX * 2 / window_width), shapes, camera); rearrangePolygonsByCameraDistances(shapes, svgShapes, camera, svg); requestAnimationFrame(function(){ paint(shapes, svgShapes, camera, width, height, fov) }); }
Rotating.
rotate ALL TEH THINGS
function rotateAllSurfaces(dimension, degree, shapes, camera){ for (var i = 0; i < shapes.length; i++) { rotate(degree, dimension, shapes[i].coordinates, camera[2]); } }
function rotate(angle, axis, points) { if (angle == 0) return; var d1, d2; switch (axis) { case "x": d1 = 1; d2 = 2; break; ... } var sin = Math.sin(angle), cos = Math.cos(angle); for (var i = 0; i < points.length; i++) { var c1 = points[i][d1], c2 = points[i][d2]; // edit in-memory points[i][d1] = c1 * cos - c2 * sin; // new y = y*Math.cos(angle) - z*Math.sin(angle) points[i][d2] = c1 * sin + c2 * cos; // new z = y*Math.sin(angle) + z*Math.cos(angle) } }
So what about
Mouse scrolling
???
//FF doesn't recognize mousewheel as of FF3.x var mousewheelevt=(/Firefox/i.test(navigator.userAgent))? "DOMMouseScroll" : "mousewheel"; document.addEventListener(mousewheelevt, function(e){ zoom += e.wheelDeltaY; setCameraPosition(0, testBuilding.swaEaveHeight * -1, zoom); requestAnimationFrame(function(){ paint(shapes, svgShapes, camera, width, height, fov) }); }, false);
What about
Performance?
requestAnimationFrame()
(function() { var lastTime = 0, vendors = ['webkit', 'moz']; for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { ... You must be using IE }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());
Why should I use it?
- Single reflow and repaint cycle
- Background tabs don't run, which means less CPU, GPU, and memory usage and longer battery life.
OMG I can brag about having a site with battery-friendly animations?
Yeah, bro. Totes McGoats.
Arrays versus Object literals:
removes some lols
&
makes code faster
&
changes important stuff
Stop the lols!
Currently cannot
- Use the GPU
- Add textures (some browsers may support this)
Areas for improvement
- Caching / currying
- Use HTML5 Web Workers to calculate the projections
- Get contributors
- Modularize the API
- Maybe utilize CSS3D transforms for rotations? (support probably limited)
Learn to
have some fun with it.
Thank You!
Questions?
For more information:
https://github.com/matthiasak/3D-svg-model-viewer
or hit me up on Twitter