Skip to content
Browse files

Initial import of pre3d, an immediate mode JavaScript 3d engine.

  - pre3d.js, core math, datastructures, and engine
  - pre3d_shape_utils.js, some tessellation and procedural mesh code
  - colorscube demo
  • Loading branch information...
1 parent e582844 commit 61205882fed684e98830c8802204d474cbd7bc2c @deanm committed Mar 26, 2009
Showing with 2,055 additions and 1 deletion.
  1. +36 −1 README
  2. +54 −0 demos/colorscube.html
  3. +65 −0 demos/colorscube.js
  4. +260 −0 demos/demo_utils.js
  5. +976 −0 pre3d.js
  6. +664 −0 pre3d_shape_utils.js
View
37 README
@@ -1 +1,36 @@
-A JavaScript 3d rendering engine.
+Pre3d is a JavaScript library, which will project a 3d scene into 2d, and draw
+it to a <canvas> element. The API is immediate mode, with the basic primitive
+of a Shape, consisting of QuadFace quad and/or triangle faces. The library is
+designed to be low-level and direct, there is no retrained or scene graph API.
+
+There are currently 2 JavaScript files, the core engine and some mesh utils.
+There are no external dependencies, and the DOM shouldn't be touched outside
+of using the <canvas> element passed to the Renderer.
+
+ pre3d.js - The core math routines, data structures, and rendering code. It
+ does not touch the DOM, except the <canvas> element passed to the Renderer.
+
+ pre3d_shape_utils.js - While pre3d.js defines the basic shape datastructures,
+ it implement much code for working with them. This is a collection of code
+ for creating new Shapes (cube, sphere, etc), and for manipulating Shapes. It
+ implements some basic procedural operators like smooth and subdivide.
+
+There are some demo applications implemented in the demos/ directory. Along
+with the comments in the source code, the demos are the best source of
+documentation. They should give you an idea of how to use the engine, and what
+it is capable of. demos/demo_utils.js implements some UI helpers, like moving
+camera when the canvas element is dragged on, etc.
+
+License:
+ The engine code is free to use under the BSD license. The examples / demos
+ are (c) Dean McNamee, All rights reserved.
+
+Credits:
+ Kragen's torus is the best/simplest/cleanest JS 3d code I've seen, and was
+ a good source of inspiration. http://www.canonical.org/~kragen/sw/torus.html
+
+ The Demoscene has strongly influenced how I think about graphics, and this
+ engine is a joke compared to what is being done there.
+
+ Thatcher Ulrich gave me a bunch of help and ideas, and implemented the
+ textured triangle drawing.
View
54 demos/colorscube.html
@@ -0,0 +1,54 @@
+<html>
+
+<head>
+
+<title>A 3d demo in JavaScript</title>
+
+<style>
+ body * {
+ font-family: sans-serif;
+ font-size: 14px;
+ }
+ body.white {
+ background-color: white;
+ color: black;
+ }
+ body.black {
+ background-color: black;
+ color: white;
+ }
+ span.spaceyspan { margin-right: 20px; }
+ div.centeredDiv { text-align: center; }
+ li { list-style: none; }
+ td { padding-right: 10px; }
+</style>
+
+<script src="../pre3d.js"></script>
+<script src="../pre3d_shape_utils.js"></script>
+<script src="demo_utils.js"></script>
+<script src="colorscube.js"></script>
+</head>
+
+<body class="black">
+
+<div class="centeredDiv">
+<canvas id="canvas" width="800" height="600">
+ Sorry, this demo requires a web browser which supports HTML5 canvas!
+</canvas>
+</div>
+
+<p>
+JavaScript software 3d renderer &nbsp; &copy 2009 Dean McNamee (dean at gmail)
+</p>
+
+<table>
+<tr><td>Press t</td><td>&rarr;</td><td>toggle background color</td></tr>
+<tr><td>Press p</td><td>&rarr;</td><td>pause</td></tr>
+<tr><td>Mouse</td><td>&rarr;</td><td>rotate around origin x and y axis</td></tr>
+<tr><td>Mouse + ctrl</td><td>&rarr;</td><td>pan x / y</td></tr>
+<tr><td>Mouse + shift</td><td>&rarr;</td><td>pan z</td></tr>
+<tr><td>Mouse + ctrl + shift</td><td>&rarr;</td><td>adjust focal length</td></tr>
+</table>
+
+</body>
+</html>
View
65 demos/colorscube.js
@@ -0,0 +1,65 @@
+// (c) Dean McNamee <dean@gmail.com>. All rights reserved.
+
+window.addEventListener('load', function() {
+ var black = new Pre3d.RGBA(0, 0, 0, 1);
+ var white = new Pre3d.RGBA(1, 1, 1, 1);
+
+ var screen_canvas = document.getElementById('canvas');
+ var renderer = new Pre3d.Renderer(screen_canvas);
+
+ var cubes = [ ];
+
+ for (var i = 0; i < 10; ++i) {
+ for (var j = 0; j < 10; ++j) {
+ for (var k = 0; k < 10; ++k) {
+ if (i == 0 || j == 0 || k == 0 ||
+ i == 9 || j == 9 || k == 9) {
+ var cube = Pre3d.ShapeUtils.makeCube(0.5);
+ var transform = new Pre3d.Transform();
+ transform.translate(i - 5, j - 5, k - 5);
+ cubes.push({
+ shape: cube,
+ color: new Pre3d.RGBA(i / 10, j / 10, k / 10, 0.3),
+ trans: transform});
+ }
+ }
+ }
+ }
+
+ var num_cubes = cubes.length;
+
+ function draw() {
+ for (var i = 0; i < num_cubes; ++i) {
+ var cube = cubes[i];
+ renderer.fill_rgba = cube.color;
+ renderer.transform = cube.trans;
+ renderer.bufferShape(cube.shape);
+ }
+ renderer.draw();
+ renderer.emptyBuffer();
+ }
+
+ renderer.camera.focal_length = 2.5;
+ // Have the engine handle mouse / camera movement for us.
+ DemoUtils.autoCamera(renderer, 0, 0, -30, 0.40, -1.06, 0, draw);
+
+ renderer.background_rgba = black;
+
+ cur_white = false;
+ document.addEventListener('keydown', function(e) {
+ if (e.keyCode != 84) // t
+ return;
+
+ if (cur_white) {
+ document.body.className = "black";
+ renderer.background_rgba = black;
+ } else {
+ document.body.className = "white";
+ renderer.background_rgba = white;
+ }
+ cur_white = !cur_white;
+ draw();
+ }, false);
+
+ draw();
+}, false);
View
260 demos/demo_utils.js
@@ -0,0 +1,260 @@
+// (c) Dean McNamee <dean@gmail.com>, Dec 2008.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// This file implements helpers you might want to use when making a demo. It
+// mostly consists of UI helpers, like a toolbar for toggling modes, mouse
+// and camera handling, etc.
+
+DemoUtils = (function() {
+
+ function min(a, b) {
+ if (a < b) return a;
+ return b;
+ }
+
+ function max(a, b) {
+ if (a > b) return a;
+ return b;
+ }
+
+ // Keep c >= a && c <= b.
+ function clamp(a, b, c) {
+ return min(b, max(a, c));
+ }
+
+ // A Ticker helps you keep a beat, calling a callback based on a target
+ // frames-per-second. You can stop and start the ticker, change the step
+ // size, etc. Your callback will be passed the frame number.
+ function Ticker(fps, callback) {
+ this.interval_ms_ = 1000 / fps;
+ this.callback_ = callback;
+ this.t_ = 0;
+ this.step_ = 1;
+ this.interval_handle_ = null;
+ }
+
+ Ticker.prototype.isRunning = function() {
+ return this.interval_handle_ !== null;
+ };
+
+ Ticker.prototype.start = function(fps, callback) {
+ if (this.isRunning())
+ return;
+
+ var self = this;
+ this.interval_handle_ = setInterval(function() {
+ var callback = self.callback_;
+ callback(self.t_);
+ self.t_ += self.step_;
+ }, this.interval_ms_);
+ };
+
+ Ticker.prototype.stop = function() {
+ if (!this.isRunning())
+ return;
+
+ clearInterval(this.interval_handle_);
+ this.interval_handle_ = null;
+ };
+
+ Ticker.prototype.set_t = function(t) {
+ this.t_ = t;
+ };
+
+ Ticker.prototype.set_step = function(step) {
+ this.step_ = step;
+ };
+
+ Ticker.prototype.reverse_step_direction = function() {
+ this.step_ = -this.step_;
+ };
+
+ // Registers some mouse listeners on a <canvas> element, to help you with
+ // things like dragging, clicking, etc. Your callback will get called on
+ // any mouse movement, with info / state about the mouse.
+ function registerMouseListener(canvas, listener) {
+ var state = {
+ first_event: true,
+ is_clicking: false,
+ last_x: 0,
+ last_y: 0,
+ };
+
+ function relXY(e) {
+ if (typeof e.offsetX == 'number')
+ return {x: e.offsetX, y: e.offsetY};
+
+ // TODO this is my offsetX/Y emulation for Firefox. I'm not sure it is
+ // exactly right, but it seems to work ok, including scroll, etc.
+ var off = {x: 0, y: 0};
+ var node = e.target;
+ var pops = node.offsetParent;
+ if (pops) {
+ off.x += node.offsetLeft - pops.offsetLeft;
+ off.y += node.offsetTop - pops.offsetTop;
+ }
+
+ return {x: e.layerX - off.x, y: e.layerY - off.y};
+ };
+
+ canvas.addEventListener('mousedown', function(e) {
+ var rel = relXY(e);
+ state.is_clicking = true;
+ state.last_x = rel.x;
+ state.last_y = rel.y
+ }, false);
+
+ canvas.addEventListener('mouseup', function(e) {
+ state.is_clicking = false;
+ }, false);
+
+ canvas.addEventListener('mouseout', function(e) {
+ state.is_clicking = false;
+ }, false);
+
+ canvas.addEventListener('mousemove', function(e) {
+ var rel = relXY(e);
+ var delta_x = state.last_x - rel.x;
+ var delta_y = state.last_y - rel.y;
+
+ // TODO: I'd like to use offsetX here, but it doesn't exist in Firefox.
+ // I should make a shim, but you have to do some DOM walking...
+ state.last_x = rel.x;
+ state.last_y = rel.y;
+
+ // We need one event to get calibrated.
+ if (state.first_event) {
+ state.first_event = false;
+ return;
+ }
+
+ var info = {
+ is_clicking: state.is_clicking,
+ canvas_x: state.last_x,
+ canvas_y: state.last_y,
+ delta_x: delta_x,
+ delta_y: delta_y,
+ shift: e.shiftKey,
+ ctrl: e.ctrlKey
+ };
+
+ listener(info);
+ }, false);
+ }
+
+ // Register mouse handlers to automatically handle camera:
+ // Mouse -> rotate around origin x and y axis.
+ // Mouse + ctrl -> pan x / y.
+ // Mouse + shift -> pan z.
+ // Mouse + ctrl + shift -> adjust focal length.
+ function autoCamera(renderer, ix, iy, iz, tx, ty, tz, draw_callback) {
+ var camera_state = {
+ rotate_x: tx,
+ rotate_y: ty,
+ rotate_z: tz,
+ x: ix,
+ y: iy,
+ z: iz
+ };
+
+ function set_camera() {
+ var ct = renderer.camera.transform;
+ ct.reset();
+ ct.rotateY(camera_state.rotate_y);
+ ct.rotateX(camera_state.rotate_x);
+ ct.translate(camera_state.x, camera_state.y, camera_state.z);
+ }
+
+ // We debounce fast mouse movements so we don't paint a million times.
+ var cur_pending = null;
+
+ registerMouseListener(renderer.canvas, function(info) {
+ if (!info.is_clicking)
+ return;
+
+ if (info.shift && info.ctrl) {
+ renderer.camera.focal_length = clamp(0.05, 10,
+ renderer.camera.focal_length + (info.delta_y * 0.01));
+ } else if (info.shift) {
+ camera_state.z += info.delta_y * 0.01;
+ } else if (info.ctrl) {
+ camera_state.x -= info.delta_x * 0.01;
+ camera_state.y -= info.delta_y * 0.01;
+ } else {
+ camera_state.rotate_y -= info.delta_x * 0.01;
+ camera_state.rotate_x -= info.delta_y * 0.01;
+ }
+
+ if (cur_pending != null)
+ clearTimeout(cur_pending);
+
+ cur_pending = setTimeout(function() {
+ cur_pending = null;
+ set_camera();
+ draw_callback();
+ }, 0);
+ });
+
+ // Set up the initial camera.
+ set_camera();
+ }
+
+ function ToggleToolbar() {
+ this.options_ = [ ];
+ }
+
+ ToggleToolbar.prototype.addEntry = function(text, initial, callback) {
+ this.options_.push([text, !!initial, callback]);
+ };
+
+ ToggleToolbar.prototype.populateDiv = function(div) {
+ var options = this.options_;
+ for (var i = 0, il = options.length; i < il; ++i) {
+ var option = options[i];
+ var name = option[0];
+ var checked = option[1];
+ var handler = option[2];
+ var span = document.createElement('span');
+ span.style.marginRight = '20px';
+ var cb = document.createElement('input');
+ cb.type = 'checkbox';
+ if (checked)
+ cb.checked = true;
+ cb.addEventListener('change', handler, false);
+ span.appendChild(cb);
+ span.appendChild(document.createTextNode(' ' + name));
+ div.appendChild(span);
+ }
+ };
+
+ ToggleToolbar.prototype.createBefore = function(element) {
+ var div = document.createElement('div');
+ this.populateDiv(div);
+ var pops = element.parentNode;
+ pops.insertBefore(div, pops.firstChild);
+ };
+
+ return {
+ Ticker: Ticker,
+ registerMouseListener: registerMouseListener,
+ autoCamera: autoCamera,
+ ToggleToolbar: ToggleToolbar,
+ };
+})();
View
976 pre3d.js
@@ -0,0 +1,976 @@
+// Pre3d, a JavaScript software 3d renderer.
+// (c) Dean McNamee <dean@gmail.com>, Dec 2008.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// Here are a few notes about what was involved in making this code fast.
+//
+// - Being careful about painting The engine works in quads, 4 vertices per
+// face, no restriction on being coplanar, or on triangles. If we were to
+// work only in triangles, we would have to do twice as many paints and
+// longer sorts, since we would double the polygon count.
+//
+// Depending on the underlying rasterization system, strokes can be pretty
+// slow, slower than fills. This is why overdraw is not a stroke.
+//
+// - Objects over Arrays
+// Because Arrays always go through the key lookup path (a[0] is a['0']), and
+// there is no way to do a named lookup (like a.0), it is faster to use
+// objects than arrays for fixed size storage. You can think of this like
+// the difference between a List and Tuple in languages like python. Modern
+// engines can do a better job accessing named properties, so we represented
+// our data as objects. Profiling showed a huge difference, keyed lookup
+// used to be the most expensive operation in profiling, taking around ~5%.
+//
+// There is also a performance (and convenience) balance betweening object
+// literals and constructor functions. Small and obvious structures like
+// points have no constructor, and are expected to be created as object
+// literals. Objects with many properties are created through a constructor.
+//
+// - Object creation / GC pressure
+// One of the trickiest things about a language like JavaScript is avoiding
+// long GC pauses and object churn. You can do things like cache and reuse
+// objects, avoid creating extra intermediate objects, etc. Right now there
+// has been a little bit of work done here, but there is more to be done.
+//
+// - Flattening
+// It is very tempting as a programmer to write generic routines, for example
+// math functions that could work on either 2d or 3d. This is convenient,
+// but the caller already knows which they should be using, and the extra
+// overhead for generic routines turned out to be substantial. Unrolling
+// specialized code makes a big difference, for example an early profile:
+// before: 2.5% 2.5% Function: subPoints // old general 2d and 3d
+// after: 0.3% 0.3% Function: subPoints2d // fast case 2d
+// after: 0.2% 0.2% Function: subPoints3d // fast case 3d
+//
+// - Don't use new if you don't have to
+// Some profiles showed that new (JSConstructCall) at about ~1%. These were
+// for code like new Array(size); Specifically for the Array constructor, it
+// ignores the object created and passed in via new, and returns a different
+// object anyway. This means 'new Array()' and 'Array()' should be
+// interchangable, and this allows you to avoid the overhead for new.
+//
+// - Local variable caching
+// In most cases it should be faster to look something up in the local frame
+// than to evaluate the expression / lookup more than once. In these cases
+// I generally try to cache the variable in a local var.
+//
+// You might notice that in a few places there is code like:
+// Blah.protype.someMethod = function someMethod() { }
+// someMethod is duplicated on the function so that the name of the function
+// is not anonymous, and it can be easier to debug and profile.
+
+Pre3d = (function() {
+
+ // 2D and 3D point / vector / matrix math. Points and vectors are expected
+ // to have an x, y and z (if 3d) property. It is important to be consistent
+ // when creating these objects to allow the JavaScript engine to properly
+ // optimize the property access. Create this as object literals, ex:
+ // var my_2d_point_or_vector = {x: 0, y: 0};
+ // var my_3d_point_or_vector = {x: 0, y: 0, z: 0};
+ //
+ // There is one convention that might be confusing. In order to avoid extra
+ // object creations, there are some "IP" versions of these functions. This
+ // stands for "in place", and they write the result to one of the arguments.
+
+ function crossProduct(a, b) {
+ // a1b2 - a2b1, a2b0 - a0b2, a0b1 - a1b0
+ return {
+ x: a.y * b.z - a.z * b.y,
+ y: a.z * b.x - a.x * b.z,
+ z: a.x * b.y - a.y * b.x
+ };
+ }
+
+ function dotProduct2d(a, b) {
+ return a.x * b.x + a.y * b.y;
+ }
+ function dotProduct3d(a, b) {
+ return a.x * b.x + a.y * b.y + a.z * b.z;
+ }
+
+ // a - b
+ function subPoints2d(a, b) {
+ return {x: a.x - b.x, y: a.y - b.y};
+ }
+ function subPoints3d(a, b) {
+ return {x: a.x - b.x, y: a.y - b.y, z: a.z - b.z};
+ }
+
+ // c = a - b
+ function subPoints2dIP(c, a, b) {
+ c.x = a.x - b.x;
+ c.y = a.y - b.y;
+ return c;
+ }
+ function subPoints3dIP(c, a, b) {
+ c.x = a.x - b.x;
+ c.y = a.y - b.y;
+ c.z = a.z - b.z;
+ return c;
+ }
+
+ // a + b
+ function addPoints2d(a, b) {
+ return {x: a.x + b.x, y: a.y + b.y};
+ }
+ function addPoints3d(a, b) {
+ return {x: a.x + b.x, y: a.y + b.y, z: a.z + b.z};
+ }
+
+ // c = a + b
+ function addPoints2dIP(c, a, b) {
+ c.x = a.x + b.x;
+ c.y = a.y + b.y;
+ return c;
+ }
+ function addPoints3dIP(c, a, b) {
+ c.x = a.x + b.x;
+ c.y = a.y + b.y;
+ c.z = a.z + b.z;
+ return c;
+ }
+
+ // a * s
+ function mulPoint2d(a, s) {
+ return {x: a.x * s, y: a.y * s};
+ }
+ function mulPoint3d(a, s) {
+ return {x: a.x * s, y: a.y * s, z: a.z * s};
+ }
+
+ // |a|
+ function vecMag2d(a) {
+ var ax = a.x, ay = a.y;
+ return Math.sqrt(ax * ax + ay * ay);
+ }
+ function vecMag3d(a) {
+ var ax = a.x, ay = a.y, az = a.z;
+ return Math.sqrt(ax * ax + ay * ay + az * az);
+ }
+
+ // a / |a|
+ function unitVector2d(a) {
+ return mulPoint2d(a, 1 / vecMag2d(a));
+ }
+ function unitVector3d(a) {
+ return mulPoint3d(a, 1 / vecMag3d(a));
+ }
+
+ // Linear interpolation on the line along points (0, |a|) and (1, |b|). The
+ // position |d| is the x coordinate, where 0 is |a| and 1 is |b|.
+ function linearInterpolate(a, b, d) {
+ return (b-a)*d + a;
+ }
+
+ // Linear interpolation on the line along points |a| and |b|. |d| is the
+ // position, where 0 is |a| and 1 is |b|.
+ function linearInterpolatePoints3d(a, b, d) {
+ return {
+ x: (b.x-a.x)*d + a.x,
+ y: (b.y-a.y)*d + a.y,
+ z: (b.z-a.z)*d + a.z
+ }
+ }
+
+ // This represents an affine 4x4 matrix, stored as a 3x4 matrix with the last
+ // row implied as [0, 0, 0, 1]. This is to avoid generally unneeded work,
+ // skipping part of the homogeneous coordinates calculations and the
+ // homogeneous divide. Unlike points, we use a constructor function instead
+ // of object literals to ensure map sharing. The matrix looks like:
+ // e0 e1 e2 e3
+ // e4 e5 e6 e7
+ // e8 e9 e10 e11
+ // 0 0 0 1
+ function AffineMatrix(e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11) {
+ this.e0 = e0;
+ this.e1 = e1;
+ this.e2 = e2;
+ this.e3 = e3;
+ this.e4 = e4;
+ this.e5 = e5;
+ this.e6 = e6;
+ this.e7 = e7;
+ this.e8 = e8;
+ this.e9 = e9;
+ this.e10 = e10;
+ this.e11 = e11;
+ };
+
+ // Transform the point |p| by the AffineMatrix |t|.
+ function transformPoint(t, p) {
+ return {
+ x: t.e0 * p.x + t.e1 * p.y + t.e2 * p.z + t.e3,
+ y: t.e4 * p.x + t.e5 * p.y + t.e6 * p.z + t.e7,
+ z: t.e8 * p.x + t.e9 * p.y + t.e10 * p.z + t.e11
+ };
+ }
+
+ // Matrix multiplication of AffineMatrix |a| x |b|. This is unrolled,
+ // and includes the calculations with the implied last row.
+ function multiplyAffine(a, b) {
+ // Avoid repeated property lookups by accessing into the local frame.
+ var a0 = a.e0, a1 = a.e1, a2 = a.e2, a3 = a.e3, a4 = a.e4, a5 = a.e5;
+ var a6 = a.e6, a7 = a.e7, a8 = a.e8, a9 = a.e9, a10 = a.e10, a11 = a.e11;
+ var b0 = b.e0, b1 = b.e1, b2 = b.e2, b3 = b.e3, b4 = b.e4, b5 = b.e5;
+ var b6 = b.e6, b7 = b.e7, b8 = b.e8, b9 = b.e9, b10 = b.e10, b11 = b.e11;
+
+ return new AffineMatrix(
+ a0 * b0 + a1 * b4 + a2 * b8,
+ a0 * b1 + a1 * b5 + a2 * b9,
+ a0 * b2 + a1 * b6 + a2 * b10,
+ a0 * b3 + a1 * b7 + a2 * b11 + a3,
+ a4 * b0 + a5 * b4 + a6 * b8,
+ a4 * b1 + a5 * b5 + a6 * b9,
+ a4 * b2 + a5 * b6 + a6 * b10,
+ a4 * b3 + a5 * b7 + a6 * b11 + a7,
+ a8 * b0 + a9 * b4 + a10 * b8,
+ a8 * b1 + a9 * b5 + a10 * b9,
+ a8 * b2 + a9 * b6 + a10 * b10,
+ a8 * b3 + a9 * b7 + a10 * b11 + a11
+ );
+ }
+
+ function makeIdentityAffine() {
+ return new AffineMatrix(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0
+ );
+ }
+
+ // http://en.wikipedia.org/wiki/Rotation_matrix
+ function makeRotateAffineX(theta) {
+ var s = Math.sin(theta);
+ var c = Math.cos(theta);
+ return new AffineMatrix(
+ 1, 0, 0, 0,
+ 0, c, -s, 0,
+ 0, s, c, 0
+ );
+ }
+
+ function makeRotateAffineY(theta) {
+ var s = Math.sin(theta);
+ var c = Math.cos(theta);
+ return new AffineMatrix(
+ c, 0, s, 0,
+ 0, 1, 0, 0,
+ -s, 0, c, 0
+ );
+ }
+
+ function makeRotateAffineZ(theta) {
+ var s = Math.sin(theta);
+ var c = Math.cos(theta);
+ return new AffineMatrix(
+ c, -s, 0, 0,
+ s, c, 0, 0,
+ 0, 0, 1, 0
+ );
+ }
+
+ function makeTranslateAffine(dx, dy, dz) {
+ return new AffineMatrix(
+ 1, 0, 0, dx,
+ 0, 1, 0, dy,
+ 0, 0, 1, dz
+ );
+ }
+
+ function makeScaleAffine(sx, sy, sz) {
+ return new AffineMatrix(
+ sx, 0, 0, 0,
+ 0, sy, 0, 0,
+ 0, 0, sz, 0
+ );
+ }
+
+ // Return the transpose of the inverse done via the classical adjoint. This
+ // skips division by the determinant, so vectors transformed by the resulting
+ // transform will not retain their original length.
+ // Reference: "Transformations of Surface Normal Vectors" by Ken Turkowski.
+ function transAdjoint(a) {
+ var a0 = a.e0, a1 = a.e1, a2 = a.e2, a4 = a.e4, a5 = a.e5;
+ var a6 = a.e6, a8 = a.e8, a9 = a.e9, a10 = a.e10;
+ return new AffineMatrix(
+ a10 * a5 - a6 * a9,
+ a6 * a8 - a4 * a10,
+ a4 * a9 - a8 * a5,
+ 0,
+ a2 * a9 - a10 * a1,
+ a10 * a0 - a2 * a8,
+ a8 * a1 - a0 * a9,
+ 0,
+ a6 * a1 - a2 * a5,
+ a4 * a2 - a6 * a0,
+ a0 * a5 - a4 * a1,
+ 0
+ );
+ }
+
+ // Transform and return a new array of points with transform matrix |t|.
+ function transformPoints(t, ps) {
+ var il = ps.length;
+ var out = Array(il);
+ for (var i = 0; i < il; ++i) {
+ out[i] = transformPoint(t, ps[i]);
+ }
+ return out;
+ }
+
+ // Average a list of points, returning a new "centroid" point.
+ function averagePoints(ps) {
+ var avg = {x: 0, y: 0, z: 0};
+ for (var i = 0, il = ps.length; i < il; ++i) {
+ var p = ps[i];
+ avg.x += p.x;
+ avg.y += p.y;
+ avg.z += p.z;
+ }
+
+ // TODO(deanm): 1 divide and 3 multiplies cheaper than 3 divides?
+ var f = 1 / il;
+
+ avg.x *= f;
+ avg.y *= f;
+ avg.z *= f;
+
+ return avg;
+ }
+
+ // Push a and b away from each other. This means that the distance between
+ // a and be should be greater, by 2 units, 1 in each direction.
+ function pushPoints2dIP(a, b) {
+ var vec = unitVector2d(subPoints2d(b, a));
+ addPoints2dIP(b, b, vec);
+ subPoints2dIP(a, a, vec);
+ }
+
+ // A Transform is a convenient wrapper around a AffineMatrix, and it is what
+ // will be expose for more transforms (camera, etc).
+ function Transform() {
+ this.reset();
+ }
+
+ // Returns a reference (not a copy) of the matrix object.
+ Transform.prototype.getMatrix = function() {
+ return this.matrix_;
+ }
+
+ Transform.prototype.reset = function() {
+ this.matrix_ = makeIdentityAffine();
+ };
+
+ // TODO(deanm): We are creating two extra objects here. What would be most
+ // effecient is something like multiplyAffineByRotateXIP(this.matrix_), etc.
+ Transform.prototype.rotateX = function(theta) {
+ this.matrix_ =
+ multiplyAffine(makeRotateAffineX(theta), this.matrix_);
+ };
+
+ Transform.prototype.rotateY = function(theta) {
+ this.matrix_ =
+ multiplyAffine(makeRotateAffineY(theta), this.matrix_);
+ };
+
+ Transform.prototype.rotateZ = function(theta) {
+ this.matrix_ =
+ multiplyAffine(makeRotateAffineZ(theta), this.matrix_);
+ };
+
+ Transform.prototype.translate = function(dx, dy, dz) {
+ this.matrix_ =
+ multiplyAffine(makeTranslateAffine(dx, dy, dz), this.matrix_);
+ };
+ Transform.prototype.translatePre = function(dx, dy, dz) {
+ this.matrix_ =
+ multiplyAffine(this.matrix_, makeTranslateAffine(dx, dy, dz));
+ };
+
+ Transform.prototype.scale = function(sx, sy, sz) {
+ this.matrix_ =
+ multiplyAffine(makeScaleAffine(sx, sy, sz), this.matrix_);
+ };
+
+ Transform.prototype.scalePre = function(sx, sy, sz) {
+ this.matrix_ =
+ multiplyAffine(this.matrix_, makeScaleAffine(sx, sy, sz));
+ };
+
+ Transform.prototype.transformPoint = function(p) {
+ return transformPoint(this.matrix_, p);
+ };
+
+ // RGBA is our simple representation for colors.
+ function RGBA(r, g, b, a) {
+ this.setRGBA(r, g, b, a);
+ };
+
+ RGBA.prototype.setRGBA = function(r, g, b, a) {
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ this.a = a;
+ };
+
+ RGBA.prototype.setRGB = function(r, g, b) {
+ this.setRGBA(r, g, b, 1);
+ };
+
+ RGBA.prototype.invert = function() {
+ this.r = 1 - this.r;
+ this.g = 1 - this.g;
+ this.b = 1 - this.b;
+ };
+
+ RGBA.prototype.dup = function() {
+ return new RGBA(this.r, this.g, this.b, this.a);
+ };
+
+ // A QuadFace represents a polygon, either a four sided quad, or sort of a
+ // degenerated quad triangle. Passing null as i3 indicates a triangle. The
+ // QuadFace stores indices, which will generally point into some vertex list
+ // that the QuadFace has nothing to do with. At the annoyance of keeping
+ // the data up to date, QuadFace stores a pre-calculated centroid and two
+ // normals (two triangles in a quad). This is an optimization for rendering
+ // and procedural operations, and you must set them correctly.
+ function QuadFace(i0, i1, i2, i3) {
+ this.i0 = i0;
+ this.i1 = i1;
+ this.i2 = i2;
+ this.i3 = i3;
+
+ this.centroid = null;
+ this.normal1 = null;
+ this.normal2 = null;
+ }
+
+ QuadFace.prototype.isTriangle = function() {
+ return (this.i3 === null);
+ };
+
+ QuadFace.prototype.setQuad = function(i0, i1, i2, i3) {
+ this.i0 = i0;
+ this.i1 = i1;
+ this.i2 = i2;
+ this.i3 = i3;
+ };
+
+ QuadFace.prototype.setTriangle = function(i0, i1, i2) {
+ this.i0 = i0;
+ this.i1 = i1;
+ this.i2 = i2;
+ this.i3 = null;
+ };
+
+ // A Shape represents a mesh, a collection of QuadFaces. The Shape stores
+ // a list of all vertices (so they can be shared across QuadFaces), and the
+ // QuadFaces store indices into this list.
+ //
+ // All properties of shapes are meant to be public, so access them directly.
+ function Shape() {
+ // Array of 3d points, our vertices.
+ this.vertices = [ ];
+ // Array of QuadFaces, the indices will point into |vertices|.
+ this.quads = [ ];
+ }
+
+ // A camera is represented by a transform, and a focal length.
+ function Camera() {
+ this.transform = new Transform();
+ this.focal_length = 1;
+ }
+
+ // TextureInfo is used to describe when and how a QuadFace should be
+ // textured. |image| should be something drawable by <canvas>, like a <img>
+ // or another <canvas> element. This also stores the 2d uv coordinates.
+ function TextureInfo() {
+ this.image = null;
+ this.u0 = null;
+ this.v0 = null;
+ this.u1 = null;
+ this.v1 = null;
+ this.u2 = null;
+ this.v2 = null;
+ this.u3 = null;
+ this.v3 = null;
+ };
+
+ // This is the guts, drawing 3d onto a <canvas> element. This class does a
+ // few things:
+ // - Manage the render state, things like colors, transforms, camera, etc.
+ // - Manage a buffer of quads to be drawn. When you add something to be
+ // drawn, it will use the render state at the time it was added. The
+ // pattern is generally to add some things, modify the render state, add
+ // some more things, change some colors, add some more, than draw.
+ // NOTE: The reason for buffering is having to z-sort. We do not perform
+ // the rasterization, so something like a z-buffer isn't applicable.
+ // - Draw the buffer of things to be drawn. This will do a background
+ // color paint, render all of the buffered quads to the screen, etc.
+ //
+ // NOTE: Drawing does not clear the buffered quads, so you can keep drawing
+ // and adding more things and drawing, etc. You must explicitly empty the
+ // things to be drawn when you want to start fresh.
+ //
+ // NOTE: Some things, such as colors, as copied into the buffered state as
+ // a reference. If you want to update the color on the render state, you
+ // should replace it with a new color. Modifying the original will modify
+ // it for objects that have already been buffered. Same holds for textures.
+ function Renderer(canvas_element) {
+ // Should we z-sort for painters back to front.
+ this.perform_z_sorting = true;
+ // Should we inflate quads to visually cover up antialiasing gaps.
+ this.draw_overdraw = true;
+ // Should we skip backface culling.
+ this.draw_backfaces = false;
+
+ // The color we paint as the background.
+ this.background_rgba = new RGBA(1, 1, 1, 1);
+
+ this.texture = null;
+ this.fill_rgba = new RGBA(1, 0, 0, 1);
+
+ this.stroke_rgba = null;
+
+ this.normal1_rgba = null;
+ this.normal2_rgba = null;
+
+ this.canvas = canvas_element;
+ this.ctx = canvas_element.getContext('2d');
+
+ // The camera.
+ this.camera = new Camera();
+
+ // Object to world coordinates transformation.
+ this.transform = new Transform();
+
+ // A callback before a QuadFace is processed during bufferShape. This
+ // allows you to change the render state per-quad, and also to skip a quad
+ // by returning true from the callback. For example:
+ // renderer.quad_callback = function(quad_face, quad_index, shape) {
+ // renderer.fill_rgba.r = quad_index * 40;
+ // return false; // Don't skip this quad.
+ // };
+ this.quad_callback = null;
+
+ // Internals, don't access me.
+ // TODO(deanm): Width and height, currently this is hardcoded as 800x600.
+ // Investigate performance of using a <canvas> transform to map to screen.
+ this.width_ = canvas_element.width;
+ this.height_ = canvas_element.height;
+
+ this.buffered_quads_ = null;
+ this.emptyBuffer();
+
+ // We prefer these functions as they avoid the CSS color parsing path, but
+ // if they're not available (Firefox), then augment the ctx to fall back.
+ if (this.ctx.setStrokeColor == null) {
+ this.ctx.setStrokeColor = function setStrokeColor(r, g, b, a) {
+ var rgba = [
+ Math.floor(r * 255),
+ Math.floor(g * 255),
+ Math.floor(b * 255),
+ a
+ ];
+ this.strokeStyle = 'rgba(' + rgba.join(',') + ')';
+ }
+ }
+ if (this.ctx.setFillColor == null) {
+ this.ctx.setFillColor = function setFillColor(r, g, b, a) {
+ var rgba = [
+ Math.floor(r * 255),
+ Math.floor(g * 255),
+ Math.floor(b * 255),
+ a
+ ];
+ this.fillStyle = 'rgba(' + rgba.join(',') + ')';
+ }
+ }
+ }
+
+ Renderer.prototype.emptyBuffer = function() {
+ this.buffered_quads_ = [ ];
+ };
+
+ // TODO(deanm): Pull the project stuff off the class if possible.
+
+ // http://en.wikipedia.org/wiki/Pinhole_camera_model
+ //
+ // Project the 3d point |p| to a point in 2d.
+ // Takes the current focal_length_ in account.
+ Renderer.prototype.projectPointToCanvas = function projectPointToCanvas(p) {
+ // We're looking down the z-axis in the negative direction...
+ var v = this.camera.focal_length / -p.z;
+ var x = p.x * v;
+ var y = p.y * v;
+ // TODO hardcoded 800x600 for now.
+ return {x: (x + 1) * 300 + 100, y: (-y + 1) * 300};
+ };
+
+ // Project a 3d point onto the 2d canvas surface (pixel coordinates).
+ // Takes the current focal_length in account.
+ // TODO: flatten this calculation so we don't need make a method call.
+ Renderer.prototype.projectPointsToCanvas =
+ function projectPointsToCanvas(ps) {
+ var il = ps.length;
+ var out = Array(il);
+ for (var i = 0; i < il; ++i) {
+ out[i] = this.projectPointToCanvas(ps[i]);
+ }
+ return out;
+ };
+
+ Renderer.prototype.projectQuadFaceToCanvasIP = function(qf) {
+ qf.i0 = this.projectPointToCanvas(qf.i0);
+ qf.i1 = this.projectPointToCanvas(qf.i1);
+ qf.i2 = this.projectPointToCanvas(qf.i2);
+ if (!qf.isTriangle())
+ qf.i3 = this.projectPointToCanvas(qf.i3);
+ return qf;
+ };
+
+ // Path out a canvas path from an array of points.
+ // TODO(deanm): Only two simple uses of this left, should probably just
+ // clean those up and drop this.
+ function makeCanvasPath(ctx, ps) {
+ for (var i = 0, il = ps.length; i < il; ++i) {
+ var p = ps[i];
+ if (i == 0) {
+ ctx.moveTo(p.x, p.y);
+ } else {
+ ctx.lineTo(p.x, p.y);
+ }
+
+ // We don't need to completely close the path, this
+ // will happen for us when we fill it.
+ }
+ }
+
+ // Textured triangle drawing by Thatcher Ulrich. Draw a triangle portion of
+ // an image, with the source (uv coordinates) mapped to screen x/y
+ // coordinates. A transformation matrix for this mapping is calculated, so
+ // that the image |im| is rotated / scaled / etc to map to the x/y dest. A
+ // clipping mask is applied when drawing |im|, so only the triangle is drawn.
+ function drawCanvasTexturedTriangle(ctx, im,
+ x0, y0, x1, y1, x2, y2,
+ sx0, sy0, sx1, sy1, sx2, sy2) {
+ ctx.save();
+
+ // Clip the output to the on-screen triangle boundaries.
+ ctx.beginPath();
+ ctx.moveTo(x0, y0);
+ ctx.lineTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.closePath();
+ ctx.clip();
+
+ var denom =
+ sx0 * (sy2 - sy1) -
+ sx1 * sy2 +
+ sx2 * sy1 +
+ (sx1 - sx2) * sy0;
+
+ var m11 = - (
+ sy0 * (x2 - x1) -
+ sy1 * x2 +
+ sy2 * x1 +
+ (sy1 - sy2) * x0) / denom;
+ var m12 = (
+ sy1 * y2 +
+ sy0 * (y1 - y2) -
+ sy2 * y1 +
+ (sy2 - sy1) * y0) / denom;
+ var m21 = (
+ sx0 * (x2 - x1) -
+ sx1 * x2 +
+ sx2 * x1 +
+ (sx1 - sx2) * x0) / denom;
+ var m22 = - (
+ sx1 * y2 +
+ sx0 * (y1 - y2) -
+ sx2 * y1 +
+ (sx2 - sx1) * y0) / denom;
+ var dx = (
+ sx0 * (sy2 * x1 - sy1 * x2) +
+ sy0 * (sx1 * x2 - sx2 * x1) +
+ (sx2 * sy1 - sx1 * sy2) * x0) / denom;
+ var dy = (
+ sx0 * (sy2 * y1 - sy1 * y2) +
+ sy0 * (sx1 * y2 - sx2 * y1) +
+ (sx2 * sy1 - sx1 * sy2) * y0) / denom;
+
+ ctx.transform(m11, m12, m21, m22, dx, dy);
+
+ // Draw the whole image. Transform and clip will map it onto the
+ // correct output triangle.
+ //
+ // TODO(tulrich): figure out if drawImage goes faster if we specify the
+ // rectangle that bounds the source coords.
+ ctx.drawImage(im, 0, 0);
+ ctx.restore();
+ }
+
+ // A unit vector down the z-axis.
+ var g_z_axis_vector = {x: 0, y: 0, z: 1};
+
+ // Put a shape into the draw buffer, transforming it by the current camera,
+ // applying any current render state, etc.
+ Renderer.prototype.bufferShape = function bufferShape(shape) {
+ var draw_backfaces = this.draw_backfaces;
+ var quad_callback = this.quad_callback;
+
+ // Our vertex transformation matrix.
+ var t = multiplyAffine(this.camera.transform.getMatrix(),
+ this.transform.getMatrix());
+ // Our normal transformation matrix.
+ var tn = transAdjoint(t);
+
+ // We are transforming the points even if we decide it's back facing.
+ // We could just transform the normal, and then only transform the
+ // points if we needed it. But then you need to check to see if the
+ // point was already translated to avoid duplicating work, or just
+ // always calculate it and duplicate the work. Not sure what's best...
+ var world_vertices = transformPoints(t, shape.vertices);
+ var quads = shape.quads;
+
+ for (var j = 0, jl = shape.quads.length; j < jl; ++j) {
+ var qf = quads[j];
+
+ // Call the optional quad callback. This gives a chance to update the
+ // render state per-quad, before we emit into the buffered quads. It
+ // also gives the earliest chance to skip a quad.
+ if (quad_callback !== null && quad_callback(qf, j, shape) === true)
+ continue;
+
+ var centroid = transformPoint(t, qf.centroid);
+
+ // Cull quads that are behind the camera.
+ // TODO(deanm): this should probably involve the focal point?
+ if (centroid.z >= -1)
+ continue;
+
+ // NOTE: The transform tn isn't going to always keep the vectors unit
+ // length, so n1 and n2 should be normalized if needed.
+ // We unit vector n1 (for lighting, etc).
+ var n1 = unitVector3d(transformPoint(tn, qf.normal1));
+ var n2 = transformPoint(tn, qf.normal2);
+
+ // Backface culling. I'm not sure the exact right way to do this, but
+ // this seems to look ok, following the eye from the origin. We look
+ // at the normals of the triangulated quad, and make sure at least one
+ // is point towards the camera...
+ if (!draw_backfaces &&
+ dotProduct3d(centroid, n1) > 0 &&
+ dotProduct3d(centroid, n2) > 0) {
+ continue;
+ }
+
+ // Lighting intensity is just based on just one of the normals pointing
+ // towards the camera. Should do something better here someday...
+ var intensity = dotProduct3d(g_z_axis_vector, n1);
+ if (intensity < 0)
+ intensity = 0;
+
+ // We map the quad into world coordinates, and also replace the indices
+ // with the actual points.
+ var world_qf;
+
+ if (qf.isTriangle()) {
+ world_qf = new QuadFace(
+ world_vertices[qf.i0],
+ world_vertices[qf.i1],
+ world_vertices[qf.i2],
+ null
+ );
+ } else {
+ world_qf = new QuadFace(
+ world_vertices[qf.i0],
+ world_vertices[qf.i1],
+ world_vertices[qf.i2],
+ world_vertices[qf.i3]
+ );
+ }
+
+ world_qf.centroid = centroid;
+ world_qf.normal1 = n1;
+ world_qf.normal2 = n2;
+
+ var obj = {
+ qf: world_qf,
+ intensity: intensity,
+ draw_overdraw: this.draw_overdraw,
+ texture: this.texture,
+ fill_rgba: this.fill_rgba,
+ stroke_rgba: this.stroke_rgba,
+ normal1_rgba: this.normal1_rgba,
+ normal2_rgba: this.normal2_rgba,
+ };
+
+ this.buffered_quads_.push(obj);
+ }
+ };
+
+ // Sort an array of points by z axis.
+ function zSorter(x, y) {
+ return x.qf.centroid.z - y.qf.centroid.z;
+ }
+
+ Renderer.prototype.draw = function draw(options) {
+ var ctx = this.ctx;
+
+ // Paint the background. If there is no background, clear the canvas
+ // so that the background is transparent.
+ var bg_rgba = this.background_rgba;
+ if (bg_rgba !== null) {
+ ctx.setFillColor(bg_rgba.r, bg_rgba.g, bg_rgba.b, bg_rgba.a);
+ ctx.fillRect(0, 0, this.width_, this.height_);
+ } else {
+ ctx.clearRect(0, 0, this.width_, this.height_);
+ }
+
+ var all_quads = this.buffered_quads_;
+ var num_quads = all_quads.length;
+
+ // Sort the quads by z-index for painters algorithm :(
+ // We're looking down the z-axis in the negative direction, so we want
+ // to paint the most negative z quads first.
+ if (this.perform_z_sorting)
+ all_quads.sort(zSorter);
+
+ for (var j = 0; j < num_quads; ++j) {
+ var obj = all_quads[j];
+ var qf = obj.qf;
+
+ this.projectQuadFaceToCanvasIP(qf);
+
+ var is_triangle = qf.isTriangle();
+
+ if (obj.draw_overdraw) {
+ // Unfortunately when we fill with canvas, we can get some gap looking
+ // things on the edges between quads. One possible solution is to
+ // stroke the path, but this turns out to be really expensive. Instead
+ // we try to increase the area of the quad. Each edge pushes its
+ // vertices away from each other. This is sort of similar in concept
+ // to the builtin canvas shadow support (shadowOffsetX, etc). However,
+ // Chrome doesn't support shadows correctly now. It does in trunk, but
+ // using shadows to fill the gaps looks awful, and also seems slower.
+
+ pushPoints2dIP(qf.i0, qf.i1);
+ pushPoints2dIP(qf.i1, qf.i2);
+ if (is_triangle) {
+ pushPoints2dIP(qf.i2, qf.i0);
+ } else { // Quad.
+ pushPoints2dIP(qf.i2, qf.i3);
+ pushPoints2dIP(qf.i3, qf.i0);
+ }
+ }
+
+ // Create our quad as a <canvas> path.
+ ctx.beginPath();
+ ctx.moveTo(qf.i0.x, qf.i0.y);
+ ctx.lineTo(qf.i1.x, qf.i1.y);
+ ctx.lineTo(qf.i2.x, qf.i2.y);
+ if (!is_triangle)
+ ctx.lineTo(qf.i3.x, qf.i3.y);
+ // Don't bother closing it unless we need to.
+
+ // Fill...
+ var frgba = obj.fill_rgba;
+ if (frgba) {
+ var iy = obj.intensity;
+ ctx.setFillColor(frgba.r * iy, frgba.g * iy, frgba.b * iy, frgba.a);
+ ctx.fill();
+ }
+
+ // Texturing...
+ var texture = obj.texture;
+ if (texture !== null) {
+ drawCanvasTexturedTriangle(ctx, texture.image,
+ qf.i0.x, qf.i0.y, qf.i1.x, qf.i1.y, qf.i2.x, qf.i2.y,
+ texture.u0, texture.v0, texture.u1, texture.v1,
+ texture.u2, texture.v2);
+ if (!is_triangle) {
+ drawCanvasTexturedTriangle(ctx, texture.image,
+ qf.i0.x, qf.i0.y, qf.i2.x, qf.i2.y, qf.i3.x, qf.i3.y,
+ texture.u0, texture.v0, texture.u2, texture.v2,
+ texture.u3, texture.v3);
+ }
+ }
+
+ // Stroke...
+ var srgba = obj.stroke_rgba;
+ if (srgba) {
+ ctx.closePath();
+ ctx.setStrokeColor(srgba.r, srgba.g, srgba.b, srgba.a);
+ ctx.stroke();
+ }
+
+ // Normal lines (stroke)...
+ var n1r = obj.normal1_rgba;
+ var n2r = obj.normal2_rgba;
+ if (n1r) {
+ ctx.setStrokeColor(n1r.r, n1r.g, n1r.b, n1r.a);
+ ctx.beginPath();
+ makeCanvasPath(ctx, this.projectPointsToCanvas([
+ qf.centroid,
+ addPoints3d(qf.centroid, unitVector3d(qf.normal2))]));
+ ctx.stroke();
+ }
+ if (n2r) {
+ ctx.setStrokeColor(n2r.r, n2r.g, n2r.b, n2r.a);
+ ctx.beginPath();
+ makeCanvasPath(ctx, this.projectPointsToCanvas([
+ qf.centroid,
+ addPoints3d(qf.centroid, qf.normal1)]));
+ ctx.stroke();
+ }
+ }
+
+ return num_quads;
+ }
+
+ return {
+ RGBA: RGBA,
+ Transform: Transform,
+ QuadFace: QuadFace,
+ Shape: Shape,
+ Camera: Camera,
+ TextureInfo: TextureInfo,
+ Renderer: Renderer,
+ Math: {
+ crossProduct: crossProduct,
+ dotProduct2d: dotProduct2d,
+ dotProduct3d: dotProduct3d,
+ subPoints2d: subPoints2d,
+ subPoints3d: subPoints3d,
+ addPoints2d: addPoints2d,
+ addPoints3d: addPoints3d,
+ mulPoint2d: mulPoint2d,
+ mulPoint3d: mulPoint3d,
+ vecMag2d: vecMag2d,
+ vecMag3d: vecMag3d,
+ unitVector2d: unitVector2d,
+ unitVector3d: unitVector3d,
+ linearInterpolate: linearInterpolate,
+ linearInterpolatePoints3d: linearInterpolatePoints3d,
+ averagePoints: averagePoints,
+ },
+ };
+})();
View
664 pre3d_shape_utils.js
@@ -0,0 +1,664 @@
+// Pre3d, a JavaScript software 3d renderer.
+// (c) Dean McNamee <dean@gmail.com>, Dec 2008.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//
+// This file implements helpers related to creating / modifying Shapes. Some
+// routines exist for basic primitives (box, sphere, etc), along with some
+// routines for procedural shape operations (extrude, subdivide, etc).
+//
+// The procedural operations were inspired from the demoscene. A lot of the
+// ideas are based on similar concepts in Farbrausch's werkkzeug1.
+
+Pre3d.ShapeUtils = (function() {
+
+ // TODO(deanm): Having to import all the math like this is a bummer.
+ var crossProduct = Pre3d.Math.crossProduct;
+ var dotProduct2d = Pre3d.Math.dotProduct2d;
+ var dotProduct3d = Pre3d.Math.dotProduct3d;
+ var subPoints2d = Pre3d.Math.subPoints2d;
+ var subPoints3d = Pre3d.Math.subPoints3d;
+ var addPoints2d = Pre3d.Math.addPoints2d;
+ var addPoints3d = Pre3d.Math.addPoints3d;
+ var mulPoint2d = Pre3d.Math.mulPoint2d;
+ var mulPoint3d = Pre3d.Math.mulPoint3d;
+ var vecMag2d = Pre3d.Math.vecMag2d;
+ var vecMag3d = Pre3d.Math.vecMag3d;
+ var unitVector2d = Pre3d.Math.unitVector2d;
+ var unitVector3d = Pre3d.Math.unitVector3d;
+ var linearInterpolate = Pre3d.Math.linearInterpolate;
+ var linearInterpolatePoints3d = Pre3d.Math.linearInterpolatePoints3d;
+ var averagePoints = Pre3d.Math.averagePoints;
+
+ var k2PI = Math.PI * 2;
+
+ // averagePoints() specialized for averaging 2 points.
+ function averagePoints2(a, b) {
+ return {
+ x: (a.x + b.x) * 0.5,
+ y: (a.y + b.y) * 0.5,
+ z: (a.z + b.z) * 0.5
+ };
+ }
+
+ // Rebuild the pre-computed "metadata", for the Shape |shape|. This
+ // calculates the centroids and normal vectors for each QuadFace.
+ function rebuildMeta(shape) {
+ var quads = shape.quads;
+ var num_quads = quads.length;
+ var vertices = shape.vertices;
+
+ // TODO: It's possible we could save some work here, we could mark the
+ // faces "dirty" which need their centroid or normal recomputed. Right now
+ // if we do an operation on a single face, we rebuild all of them. A
+ // simple scheme would be to track any writes to a QuadFace, and to set
+ // centroid / normal1 / normal2 to null. This would also prevent bugs
+ // where you forget to call rebuildMeta() and used stale metadata.
+
+ for (var i = 0; i < num_quads; ++i) {
+ var qf = quads[i];
+
+ var centroid;
+ var n1, n2, na;
+
+ var vert0 = vertices[qf.i0];
+ var vert1 = vertices[qf.i1];
+ var vert2 = vertices[qf.i2];
+ var vec01 = subPoints3d(vert1, vert0);
+ var vec02 = subPoints3d(vert2, vert0);
+ var n1 = crossProduct(vec01, vec02);
+
+ if (qf.isTriangle()) {
+ n2 = na = n1;
+ centroid = averagePoints([vert0, vert1, vert2]);
+ } else {
+ var vert3 = vertices[qf.i3];
+ var vec03 = subPoints3d(vert3, vert0);
+ n2 = crossProduct(vec02, vec03);
+ na = averagePoints2(n1, n2);
+ centroid = averagePoints([vert0, vert1, vert2, vert3]);
+ }
+
+ qf.centroid = centroid;
+ qf.normal1 = n1;
+ qf.normal2 = n2;
+ }
+ }
+
+ // Call |func| for each face of |shape|. The callback |func| should return
+ // false to continue iteration, or true to stop. For example:
+ // forEachFace(shape, function(quad_face, quad_index, shape) {
+ // return false;
+ // });
+ function forEachFace(shape, func) {
+ var quads = this.quads;
+ for (var i = 0, il = quads.length; i < il; ++i) {
+ if (func(quads[i], i, this) === true)
+ break;
+ }
+ };
+
+ function makePlane(p1, p2, p3, p4) {
+ var s = new Pre3d.Shape();
+ s.vertices = [p1, p2, p3, p4];
+ s.quads = [new Pre3d.QuadFace(0, 1, 2, 3)];
+ rebuildMeta(s);
+ return s;
+ }
+
+ // Make a box with width (x) |w|, height (y) |h|, and depth (z) |d|.
+ function makeBox(w, h, d) {
+ var s = new Pre3d.Shape();
+ s.vertices = [
+ {x: w, y: h, z: -d}, // 0
+ {x: w, y: h, z: d}, // 1
+ {x: w, y: -h, z: d}, // 2
+ {x: w, y: -h, z: -d}, // 3
+ {x: -w, y: h, z: -d}, // 4
+ {x: -w, y: h, z: d}, // 5
+ {x: -w, y: -h, z: d}, // 6
+ {x: -w, y: -h, z: -d} // 7
+ ];
+
+ // 4 -- 0
+ // /| /| +y
+ // 5 -- 1 | |__ +x
+ // | 7 -|-3 /
+ // |/ |/ +z
+ // 6 -- 2
+
+ s.quads = [
+ new Pre3d.QuadFace(0, 1, 2, 3), // Right side
+ new Pre3d.QuadFace(1, 5, 6, 2), // Front side
+ new Pre3d.QuadFace(5, 4, 7, 6), // Left side
+ new Pre3d.QuadFace(4, 0, 3, 7), // Back side
+ new Pre3d.QuadFace(0, 4, 5, 1), // Top side
+ new Pre3d.QuadFace(2, 6, 7, 3) // Bottom side
+ ];
+
+ rebuildMeta(s);
+
+ return s;
+ }
+
+ // Make a cube with width, height, and depth |whd|.
+ function makeCube(whd) {
+ return makeBox(whd, whd, whd);
+ }
+
+
+ function makeBoxWithHole(w, h, d, hw, hh) {
+ var s = new Pre3d.Shape();
+ s.vertices = [
+ {x: w, y: h, z: -d}, // 0
+ {x: w, y: h, z: d}, // 1
+ {x: w, y: -h, z: d}, // 2
+ {x: w, y: -h, z: -d}, // 3
+ {x: -w, y: h, z: -d}, // 4
+ {x: -w, y: h, z: d}, // 5
+ {x: -w, y: -h, z: d}, // 6
+ {x: -w, y: -h, z: -d}, // 7
+
+ // The front new points ...
+ {x: hw, y: h, z: d}, // 8
+ {x: w, y: hh, z: d}, // 9
+ {x: hw, y: hh, z: d}, // 10
+ {x: hw, y: -h, z: d}, // 11
+ {x: w, y: -hh, z: d}, // 12
+ {x: hw, y: -hh, z: d}, // 13
+
+ {x: -hw, y: h, z: d}, // 14
+ {x: -w, y: hh, z: d}, // 15
+ {x: -hw, y: hh, z: d}, // 16
+ {x: -hw, y: -h, z: d}, // 17
+ {x: -w, y: -hh, z: d}, // 18
+ {x: -hw, y: -hh, z: d}, // 19
+
+ // The back new points ...
+ {x: hw, y: h, z: -d}, // 20
+ {x: w, y: hh, z: -d}, // 21
+ {x: hw, y: hh, z: -d}, // 22
+ {x: hw, y: -h, z: -d}, // 23
+ {x: w, y: -hh, z: -d}, // 24
+ {x: hw, y: -hh, z: -d}, // 25
+
+ {x: -hw, y: h, z: -d}, // 26
+ {x: -w, y: hh, z: -d}, // 27
+ {x: -hw, y: hh, z: -d}, // 28
+ {x: -hw, y: -h, z: -d}, // 29
+ {x: -w, y: -hh, z: -d}, // 30
+ {x: -hw, y: -hh, z: -d} // 31
+ ];
+
+ // Front Back (looking from front)
+ // 4 - - 0 05 14 08 01 04 26 20 00
+ // /| /|
+ // 5 - - 1 | 15 16--10 09 27 28--22 21
+ // | 7 - |-3 |////| |////|
+ // |/ |/ 18 19--13 12 30 31--25 24
+ // 6 - - 2
+ // 06 17 11 02 07 29 23 03
+
+ s.quads = [
+ // Front side
+ new Pre3d.QuadFace( 1, 8, 10, 9),
+ new Pre3d.QuadFace( 8, 14, 16, 10),
+ new Pre3d.QuadFace(14, 5, 15, 16),
+ new Pre3d.QuadFace(16, 15, 18, 19),
+ new Pre3d.QuadFace(19, 18, 6, 17),
+ new Pre3d.QuadFace(13, 19, 17, 11),
+ new Pre3d.QuadFace(12, 13, 11, 2),
+ new Pre3d.QuadFace( 9, 10, 13, 12),
+ // Back side
+ new Pre3d.QuadFace( 4, 26, 28, 27),
+ new Pre3d.QuadFace(26, 20, 22, 28),
+ new Pre3d.QuadFace(20, 0, 21, 22),
+ new Pre3d.QuadFace(22, 21, 24, 25),
+ new Pre3d.QuadFace(25, 24, 3, 23),
+ new Pre3d.QuadFace(31, 25, 23, 29),
+ new Pre3d.QuadFace(30, 31, 29, 7),
+ new Pre3d.QuadFace(27, 28, 31, 30),
+ // The hole
+ new Pre3d.QuadFace(10, 16, 28, 22),
+ new Pre3d.QuadFace(19, 31, 28, 16),
+ new Pre3d.QuadFace(13, 25, 31, 19),
+ new Pre3d.QuadFace(10, 22, 25, 13),
+ // Bottom side
+ new Pre3d.QuadFace( 6, 7, 29, 17),
+ new Pre3d.QuadFace(17, 29, 23, 11),
+ new Pre3d.QuadFace(11, 23, 3, 2),
+ // Right side
+ new Pre3d.QuadFace( 1, 9, 21, 0),
+ new Pre3d.QuadFace( 9, 12, 24, 21),
+ new Pre3d.QuadFace(12, 2, 3, 24),
+ // Left side
+ new Pre3d.QuadFace( 5, 4, 27, 15),
+ new Pre3d.QuadFace(15, 27, 30, 18),
+ new Pre3d.QuadFace(18, 30, 7, 6),
+ // Top side
+ new Pre3d.QuadFace(14, 26, 4, 5),
+ new Pre3d.QuadFace( 8, 20, 26, 14),
+ new Pre3d.QuadFace( 1, 0, 20, 8)
+ ];
+
+ rebuildMeta(s);
+
+ return s;
+ }
+
+ // Tessellate a sphere. There will be |tess_y| + 2 vertices along the Y-axis
+ // (two extras are for zenith and azimuth). There will be |tess_x| vertices
+ // along the X-axis. It is centered on the Y-axis. It has a radius |r|.
+ // The implementation is probably still a bit convulted. We just handle the
+ // middle points like a grid, and special case zenith/aximuth, since we want
+ // them to share a vertex anyway. The math is pretty much standard spherical
+ // coordinates, except that we map {x, y, z} -> {z, x, y}. |tess_x| is phi,
+ // and |tess_y| is theta.
+ // TODO(deanm): This code could definitely be more efficent.
+ function makeSphere(r, tess_x, tess_y) {
+ // TODO(deanm): Preallocate the arrays to the final size.
+ var vertices = [ ];
+ var quads = [ ];
+
+ // We walk theta 0 .. PI and phi from 0 .. 2PI.
+ var theta_step = Math.PI / (tess_y + 1);
+ var phi_step = (k2PI) / tess_x;
+
+ // Create all of the vertices for the middle grid portion.
+ for (var i = 0, theta = theta_step;
+ i < tess_y;
+ ++i, theta += theta_step) { // theta
+ var sin_theta = Math.sin(theta);
+ var cos_theta = Math.cos(theta);
+ for (var j = 0; j < tess_x; ++j) { // phi
+ var phi = phi_step * j;
+ vertices.push({
+ x: r * sin_theta * Math.sin(phi),
+ y: r * cos_theta,
+ z: r * sin_theta * Math.cos(phi)
+ });
+ }
+ }
+
+ // Generate the quads for the middle grid portion.
+ for (var i = 0; i < tess_y-1; ++i) {
+ var stride = i * tess_x;
+ for (var j = 0; j < tess_x; ++j) {
+ var n = (j + 1) % tess_x;
+ quads.push(new Pre3d.QuadFace(
+ stride + j,
+ stride + tess_x + j,
+ stride + tess_x + n,
+ stride + n
+ ));
+ }
+ }
+
+ // Special case the zenith / azimuth (top / bottom) portion of triangles.
+ // We make triangles (degenerated quads).
+ var last_row = vertices.length - tess_x;
+ var top_p_i = vertices.length;
+ var bot_p_i = top_p_i + 1;
+ vertices.push({x: 0, y: r, z: 0});
+ vertices.push({x: 0, y: -r, z: 0});
+
+ for (var i = 0; i < tess_x; ++i) {
+ // Top triangles...
+ quads.push(new Pre3d.QuadFace(
+ top_p_i,
+ i,
+ ((i + 1) % tess_x),
+ null
+ ));
+ // Bottom triangles...
+ quads.push(new Pre3d.QuadFace(
+ bot_p_i,
+ last_row + ((i + 2) % tess_x),
+ last_row + ((i + 1) % tess_x),
+ null
+ ));
+ }
+
+ var s = new Pre3d.Shape();
+ s.vertices = vertices;
+ s.quads = quads;
+ rebuildMeta(s);
+ return s;
+ }
+
+ // Smooth a Shape by averaging the vertices / faces. This is something like
+ // Catmull-Clark, but without the proper weighting. The |m| argument is the
+ // amount to smooth, between 0 and 1, 0 being no smoothing.
+ function averageSmooth(shape, m) {
+ // TODO(deanm): Remove this old compat code for calling without arguments.
+ if (m === void(0))
+ m = 1;
+
+ var vertices = shape.vertices;
+ var psl = vertices.length;
+ var new_ps = Array(psl);
+
+ // Build a connection mapping of vertex_index -> [ quad indexes ]
+ var connections = Array(psl);
+ for (var i = 0; i < psl; ++i)
+ connections[i] = [ ];
+
+ for (var i = 0, il = shape.quads.length; i < il; ++i) {
+ var qf = shape.quads[i];
+ connections[qf.i0].push(i);
+ connections[qf.i1].push(i);
+ connections[qf.i2].push(i);
+ if (!qf.isTriangle())
+ connections[qf.i3].push(i);
+ }
+
+ // For every vertex, average the centroids of the faces it's a part of.
+ for (var i = 0, il = vertices.length; i < il; ++i) {
+ var cs = connections[i];
+ var avg = {x: 0, y: 0, z: 0};
+
+ // Sum together the centroids of each face.
+ for (var j = 0, jl = cs.length; j < jl; ++j) {
+ var quad = shape.quads[cs[j]];
+ var p1 = vertices[quad.i0];
+ var p2 = vertices[quad.i1];
+ var p3 = vertices[quad.i2];
+ var p4 = vertices[quad.i3];
+ // The centroid. TODO(deanm) can't shape just come from the QuadFace?
+ // That would handle triangles better and avoid some duplication.
+ avg.x += (p1.x + p2.x + p3.x + p4.x) / 4;
+ avg.y += (p1.y + p2.y + p3.y + p4.y) / 4;
+ avg.z += (p1.z + p2.z + p3.z + p4.z) / 4;
+ // TODO combine all the div / 4 into one divide?
+ }
+
+ // We summed up all of the centroids, take the average for our new point.
+ var f = 1 / jl;
+ avg.x *= f;
+ avg.y *= f;
+ avg.z *= f;
+
+ // Interpolate between the average and the original based on |m|.
+ new_ps[i] = linearInterpolatePoints3d(vertices[i], avg, m);
+ }
+
+ shape.vertices = new_ps;
+
+ rebuildMeta(shape);
+ }
+
+ // Divide each face of a Shape into 4 equal new faces.
+ // TODO(deanm): Better document, doesn't support triangles, etc.
+ function linearSubdivide(shape) {
+ var num_quads = shape.quads.length;
+
+ var share_points = { };
+
+ for (var i = 0; i < num_quads; ++i) {
+ var quad = shape.quads[i];
+
+ var i0 = quad.i0;
+ var i1 = quad.i1;
+ var i2 = quad.i2;
+ var i3 = quad.i3;
+
+ var p0 = shape.vertices[i0];
+ var p1 = shape.vertices[i1];
+ var p2 = shape.vertices[i2];
+ var p3 = shape.vertices[i3];
+
+ // p0 p1 p0 n0 p1
+ // -> n3 n4 n1
+ // p3 p2 p3 n2 p2
+
+ // We end up with an array of vertex indices of the centroids of each
+ // side of the quad and the middle centroid. We start with the vertex
+ // indices that should be averaged. We cache centroids to make sure that
+ // we share vertices instead of creating two on top of each other.
+ var ni = [
+ [i0, i1].sort(),
+ [i1, i2].sort(),
+ [i2, i3].sort(),
+ [i3, i0].sort(),
+ [i0, i1, i2, i3].sort(),
+ ];
+
+ for (var j = 0, jl = ni.length; j < jl; ++j) {
+ var ps = ni[j];
+ var key = ps.join('-');
+ var centroid_index = share_points[key];
+ if (centroid_index == null) { // hasn't been seen before
+ centroid_index = shape.vertices.length;
+ // TODO(deanm): Remove use of Array.prototype.map.
+ var s = shape;
+ shape.vertices.push(
+ averagePoints(ps.map(function(x) { return s.vertices[x]; })));
+ share_points[key] = centroid_index;
+ }
+
+ ni[j] = centroid_index;
+ }
+
+ // New quads ...
+ var q0 = new Pre3d.QuadFace( i0, ni[0], ni[4], ni[3]);
+ var q1 = new Pre3d.QuadFace(ni[0], i1, ni[1], ni[4]);
+ var q2 = new Pre3d.QuadFace(ni[4], ni[1], i2, ni[2]);
+ var q3 = new Pre3d.QuadFace(ni[3], ni[4], ni[2], i3);
+
+ shape.quads[i] = q0;
+ shape.quads.push(q1);
+ shape.quads.push(q2);
+ shape.quads.push(q3);
+ }
+
+ rebuildMeta(shape);
+ }
+
+ // The Extruder implements extruding faces of a Shape. The class mostly
+ // exists as a place to hold all of the extrusion parameters. The properties
+ // are meant to be private, please use the getter/setter APIs.
+ function Extruder() {
+ // The total distance to extrude, if |count| > 1, then each segment will
+ // just be a portion of the distance, and together they will be |distance|.
+ this.distance_ = 1.0;
+ // The number of segments / steps to perform. This is can be different
+ // than just running extrude multiple times, since we only operate on the
+ // originally faces, not our newly inserted faces.
+ this.count_ = 1;
+ // Selection mechanism. Access these through the selection APIs.
+ this.selector_ = null;
+ this.selectAll();
+
+ // TODO(deanm): Need a bunch more settings, controlling which normal the
+ // extrusion is performed along, etc.
+
+ // Set scale and rotation. These are public, you can access them directly.
+ // TODO(deanm): It would be great to use a Transform here, but there are
+ // a few problems. Translate doesn't make sense, so it is not really an
+ // affine. The real problem is that we need to interpolate across the
+ // values, having them in a matrix is not helpful.
+ this.scale = {x: 1, y: 1, z: 1};
+ this.rotate = {x: 0, y: 0, z: 0};
+ };
+
+ // Selection APIs, control which faces are extruded.
+ Extruder.prototype.selectAll = function() {
+ this.selector_ = function(shape, vertex_index) { return true; };
+ };
+
+ // Select faces based on the function select_func. For example:
+ // extruder.selectCustom(function(shape, quad_index) {
+ // return quad_index == 0;
+ // });
+ // The above would select only the first face for extrusion.
+ Extruder.prototype.selectCustom = function(select_func) {
+ this.selector_ = select_func;
+ };
+
+ Extruder.prototype.distance = function() {
+ return this.distance_;
+ };
+ Extruder.prototype.set_distance = function(d) {
+ this.distance_ = d;
+ };
+
+ Extruder.prototype.count = function() {
+ return this.count_;
+ };
+ Extruder.prototype.set_count = function(c) {
+ this.count_ = c;
+ };
+
+ Extruder.prototype.extrude = function extrude(shape) {
+ var distance = this.distance();
+ var count = this.count();
+
+ var rx = this.rotate.x;
+ var ry = this.rotate.y;
+ var rz = this.rotate.z;
+ var sx = this.scale.x;
+ var sy = this.scale.y;
+ var sz = this.scale.z;
+
+ var vertices = shape.vertices;
+ var quads = shape.quads;
+
+ var faces = [ ];
+ for (var i = 0, il = quads.length; i < il; ++i) {
+ if (this.selector_(shape, i))
+ faces.push(i);
+ }
+
+ for (var i = 0, il = faces.length; i < il; ++i) {
+ // This is the index of the original face. It will eventually be
+ // replaced with the last iteration's outside face.
+ var face_index = faces[i];
+ // As we proceed down a count, we always need to connect to the newest
+ // new face. We start |quad| as the original face, and it will be
+ // modified (in place) for each iteration, and then the next iteration
+ // will connect back to the previous iteration, etc.
+ var qf = quads[face_index];
+ var original_cent = qf.centroid;
+
+ // This is the surface normal, used to project out the new face. It
+ // will be rotated, but never scaled. It should be a unit vector.
+ var surface_normal = unitVector3d(addPoints3d(qf.normal1, qf.normal2));
+
+ var is_triangle = qf.isTriangle();
+
+ // These are the normals inside the face, from the centroid out to the
+ // vertices. They will be rotated and scaled to create the new faces.
+ var inner_normal1 = subPoints3d(vertices[qf.i0], original_cent);
+ var inner_normal2 = subPoints3d(vertices[qf.i1], original_cent);
+ var inner_normal2 = subPoints3d(vertices[qf.i2], original_cent);
+ if (!is_triangle) {
+ var inner_normal3 = subPoints3d(vertices[qf.i3], original_cent);
+ }
+
+ for (var z = 0; z < count; ++z) {
+ var m = (z + 1) / count;
+
+ var t = new Pre3d.Transform();
+ t.rotateX(rx * m);
+ t.rotateY(ry * m);
+ t.rotateZ(rz * m);
+
+ // For our new point, we simply want to rotate the original normal
+ // proportional to how many steps we're at. Then we want to just scale
+ // it out based on our steps, and add it to the original centorid.
+ var new_cent = addPoints3d(original_cent,
+ mulPoint3d(t.transformPoint(surface_normal), m * distance));
+
+ // We multiplied the centroid, which should not have been affected by
+ // the scale. Now we want to scale the inner face normals.
+ t.scalePre(
+ linearInterpolate(1, sx, m),
+ linearInterpolate(1, sy, m),
+ linearInterpolate(1, sz, m));
+
+ var index_before = vertices.length;
+
+ vertices.push(addPoints3d(new_cent, t.transformPoint(inner_normal1)));
+ vertices.push(addPoints3d(new_cent, t.transformPoint(inner_normal2)));
+ vertices.push(addPoints3d(new_cent, t.transformPoint(inner_normal2)));
+ if (!is_triangle) {
+ vertices.push(
+ addPoints3d(new_cent, t.transformPoint(inner_normal3)));
+ }
+
+ // Add the new faces. These faces will always be quads, even if we
+ // extruded a triangle. We will have 3 or 4 new side faces.
+ quads.push(new Pre3d.QuadFace(
+ qf.i1,
+ index_before + 1,
+ index_before,
+ qf.i0));
+ quads.push(new Pre3d.QuadFace(
+ qf.i2,
+ index_before + 2,
+ index_before + 1,
+ qf.i1));
+
+ if (is_triangle) {
+ quads.push(new Pre3d.QuadFace(
+ qf.i0,
+ index_before,
+ index_before + 2,
+ qf.i2));
+ } else {
+ quads.push(new Pre3d.QuadFace(
+ qf.i3,
+ index_before + 3,
+ index_before + 2,
+ qf.i2));
+ quads.push(new Pre3d.QuadFace(
+ qf.i0,
+ index_before,
+ index_before + 3,
+ qf.i3));
+ }
+
+ // Update (in place) the original face with the new extruded vertices.
+ qf.i0 = index_before;
+ qf.i1 = index_before + 1;
+ qf.i2 = index_before + 2;
+ if (!is_triangle)
+ qf.i3 = index_before + 3;
+ }
+ }
+
+ rebuildMeta(shape); // Compute all the new normals, etc.
+ };
+
+ return {
+ rebuildMeta: rebuildMeta,
+ forEachFace: forEachFace,
+
+ makePlane: makePlane,
+ makeCube: makeCube,
+ makeBox: makeBox,
+ makeBoxWithHole: makeBoxWithHole,
+ makeSphere: makeSphere,
+
+ averageSmooth: averageSmooth,
+ linearSubdivide: linearSubdivide,
+
+ Extruder: Extruder,
+ };
+})();

0 comments on commit 6120588

Please sign in to comment.
Something went wrong with that request. Please try again.