Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial project setup, nothing special.

Most content was simply copied from my WebGL demos.
  • Loading branch information...
commit 957ce91bd2a0d1dc97d1f68b4c731e71c936680e 1 parent 563e523
@sinisterchipmunk authored
Showing with 8,278 additions and 11 deletions.
  1. +3 −0  .gitignore
  2. +4 −3 Gemfile
  3. +108 −0 Gemfile.lock
  4. +8 −4 README.rdoc
  5. +6 −3 Rakefile
  6. +2 −0  lib/rails-webgl.rb
  7. +38 −0 lib/webgl.rb
  8. +28 −0 lib/webgl/railtie.rb
  9. 0  log/development.log
  10. +12 −0 public/javascripts/control/keycodes.js
  11. +150 −0 public/javascripts/control/mouse_weight.js
  12. +366 −0 public/javascripts/culling/octree.js
  13. +103 −0 public/javascripts/engine/animation.js
  14. +48 −0 public/javascripts/engine/assertions.js
  15. +102 −0 public/javascripts/engine/buffer.js
  16. +392 −0 public/javascripts/engine/camera.js
  17. +69 −0 public/javascripts/engine/canvas_texture.js
  18. +361 −0 public/javascripts/engine/context.js
  19. +272 −0 public/javascripts/engine/core.js
  20. +269 −0 public/javascripts/engine/frustum.js
  21. +295 −0 public/javascripts/engine/heightmap.js
  22. 0  public/javascripts/engine/lighting.js
  23. +301 −0 public/javascripts/engine/mesh.js
  24. +43 −0 public/javascripts/engine/plane.js
  25. +240 −0 public/javascripts/engine/shader.js
  26. +243 −0 public/javascripts/engine/text.js
  27. +99 −0 public/javascripts/engine/texture.js
  28. +270 −0 public/javascripts/engine/vector.js
  29. +36 −0 public/javascripts/engine/video_texture.js
  30. +188 −0 public/javascripts/engine/world.js
  31. +181 −0 public/javascripts/gl-utils.js
  32. +38 −0 public/javascripts/models/actor.js
  33. +78 −0 public/javascripts/models/ai.js
  34. +76 −0 public/javascripts/models/creature.js
  35. +52 −0 public/javascripts/objects/axis.js
  36. +72 −0 public/javascripts/objects/cube.js
  37. +37 −0 public/javascripts/objects/json3d.js
  38. +32 −0 public/javascripts/objects/line.js
  39. +571 −0 public/javascripts/objects/md2.js
  40. +17 −0 public/javascripts/objects/point.js
  41. +36 −0 public/javascripts/objects/quad.js
  42. +266 −0 public/javascripts/objects/renderable.js
  43. +380 −0 public/javascripts/objects/skeleton.js
  44. +85 −0 public/javascripts/objects/sphere.js
  45. +81 −0 public/javascripts/sylvester-ext.js
  46. +1,254 −0 public/javascripts/sylvester.js
  47. +65 −0 public/javascripts/systems/particle_manager.js
  48. +320 −0 public/javascripts/systems/particle_system.js
  49. +188 −0 public/javascripts/tests/engine/camera.js
  50. +23 −0 public/javascripts/tests/engine/core.js
  51. +19 −0 public/javascripts/tests/engine/heightmap.js
  52. 0  public/javascripts/tests/engine/lighting.js
  53. +14 −0 public/javascripts/tests/engine/renderable.js
  54. +45 −0 public/javascripts/tests/engine/shader.js
  55. 0  public/javascripts/tests/engine/world.js
  56. +1 −0  public/javascripts/tests/objects/md2.js
  57. +26 −0 public/javascripts/tests/objects/skeleton.js
  58. 0  public/javascripts/tests/test_helper.js
  59. +82 −0 public/javascripts/webgl.js
  60. +58 −0 spec/javascripts/PlayerSpec.js
  61. +9 −0 spec/javascripts/helpers/SpecHelper.js
  62. +73 −0 spec/javascripts/support/jasmine.yml
  63. +32 −0 spec/javascripts/support/jasmine_runner.rb
  64. +11 −1 spec/spec_helper.rb
View
3  .gitignore
@@ -1,3 +1,6 @@
+.idea
+
+
# rcov generated
coverage
View
7 Gemfile
@@ -1,11 +1,12 @@
source "http://rubygems.org"
-# Add dependencies required to use your gem here.
-# Example:
-# gem "activesupport", ">= 2.3.5"
+
+gem 'rails', '~> 3.0'
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
+ gem 'jasmine'
+ gem 'genspec', '>= 0.2.0.prerails3.2'
gem "rspec", "~> 2.1.0"
gem "bundler", "~> 1.0.0"
gem "jeweler", "~> 1.5.1"
View
108 Gemfile.lock
@@ -0,0 +1,108 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ abstract (1.0.0)
+ actionmailer (3.0.3)
+ actionpack (= 3.0.3)
+ mail (~> 2.2.9)
+ actionpack (3.0.3)
+ activemodel (= 3.0.3)
+ activesupport (= 3.0.3)
+ builder (~> 2.1.2)
+ erubis (~> 2.6.6)
+ i18n (~> 0.4)
+ rack (~> 1.2.1)
+ rack-mount (~> 0.6.13)
+ rack-test (~> 0.5.6)
+ tzinfo (~> 0.3.23)
+ activemodel (3.0.3)
+ activesupport (= 3.0.3)
+ builder (~> 2.1.2)
+ i18n (~> 0.4)
+ activerecord (3.0.3)
+ activemodel (= 3.0.3)
+ activesupport (= 3.0.3)
+ arel (~> 2.0.2)
+ tzinfo (~> 0.3.23)
+ activeresource (3.0.3)
+ activemodel (= 3.0.3)
+ activesupport (= 3.0.3)
+ activesupport (3.0.3)
+ arel (2.0.6)
+ builder (2.1.2)
+ diff-lcs (1.1.2)
+ erubis (2.6.6)
+ abstract (>= 1.0.0)
+ genspec (0.2.0.prerails3.2)
+ rspec (>= 2.0.0.beta.14)
+ sc-core-ext (>= 1.2.1)
+ git (1.2.5)
+ i18n (0.5.0)
+ jasmine (1.0.1.1)
+ json_pure (>= 1.4.3)
+ rack (>= 1.0.0)
+ rake (>= 0.8.7)
+ rspec (>= 1.1.5)
+ selenium-client (>= 1.2.17)
+ selenium-rc (>= 2.1.0)
+ jeweler (1.5.1)
+ bundler (~> 1.0.0)
+ git (>= 1.2.5)
+ rake
+ json_pure (1.4.6)
+ mail (2.2.12)
+ activesupport (>= 2.3.6)
+ i18n (>= 0.4.0)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.16)
+ polyglot (0.3.1)
+ rack (1.2.1)
+ rack-mount (0.6.13)
+ rack (>= 1.0.0)
+ rack-test (0.5.6)
+ rack (>= 1.0)
+ rails (3.0.3)
+ actionmailer (= 3.0.3)
+ actionpack (= 3.0.3)
+ activerecord (= 3.0.3)
+ activeresource (= 3.0.3)
+ activesupport (= 3.0.3)
+ bundler (~> 1.0)
+ railties (= 3.0.3)
+ railties (3.0.3)
+ actionpack (= 3.0.3)
+ activesupport (= 3.0.3)
+ rake (>= 0.8.7)
+ thor (~> 0.14.4)
+ rake (0.8.7)
+ rcov (0.9.9)
+ rspec (2.1.0)
+ rspec-core (~> 2.1.0)
+ rspec-expectations (~> 2.1.0)
+ rspec-mocks (~> 2.1.0)
+ rspec-core (2.1.0)
+ rspec-expectations (2.1.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.1.0)
+ sc-core-ext (1.2.1)
+ activesupport (>= 2.3.5)
+ selenium-client (1.2.18)
+ selenium-rc (2.2.4)
+ selenium-client (>= 1.2.18)
+ thor (0.14.6)
+ treetop (1.4.9)
+ polyglot (>= 0.3.1)
+ tzinfo (0.3.23)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (~> 1.0.0)
+ genspec (>= 0.2.0.prerails3.2)
+ jasmine
+ jeweler (~> 1.5.1)
+ rails (~> 3.0)
+ rcov
+ rspec (~> 2.1.0)
View
12 README.rdoc
@@ -1,6 +1,10 @@
= webgl
-Description goes here.
+A WebGL framework for Rails. This is the _official_ project spawned from my Rails-based demos, which you can read more
+about at http://github.com/sinisterchipmunk/webgl.
+
+And so far, that's about all there is to say about this project. Check back soon when I have something you can actually
+use.
== Contributing to webgl
@@ -10,10 +14,10 @@ Description goes here.
* Start a feature/bugfix branch
* Commit and push until you are happy with your contribution
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
-* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
+* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise
+ necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
== Copyright
-Copyright (c) 2010 Colin MacKenzie IV. See LICENSE.txt for
-further details.
+Copyright (c) 2010 Colin MacKenzie IV. See LICENSE.txt for further details.
View
9 Rakefile
@@ -13,10 +13,10 @@ require 'jeweler'
Jeweler::Tasks.new do |gem|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
gem.name = "webgl"
- gem.homepage = "http://github.com/sinisterchipmunk/webgl"
+ gem.homepage = "http://thoughtsincomputation.com"
gem.license = "MIT"
- gem.summary = %Q{TODO: one-line summary of your gem}
- gem.description = %Q{TODO: longer description of your gem}
+ gem.summary = %Q{A WebGL framework for Rails.}
+ gem.description = %Q{A WebGL framework for Rails.}
gem.email = "sinisterchipmunk@gmail.com"
gem.authors = ["Colin MacKenzie IV"]
# Include your dependencies below. Runtime dependencies are required when using your gem,
@@ -48,3 +48,6 @@ Rake::RDocTask.new do |rdoc|
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
+
+require 'jasmine'
+load 'jasmine/tasks/jasmine.rake'
View
2  lib/rails-webgl.rb
@@ -0,0 +1,2 @@
+# For easier require from Rails gemfiles
+require File.join(File.dirname(__FILE__), 'webgl')
View
38 lib/webgl.rb
@@ -0,0 +1,38 @@
+$:.unshift(File.dirname(__FILE__))
+
+require 'rails'
+require 'webgl/railtie'
+
+module WebGL
+ class << self
+ def path
+ File.expand_path(File.join(File.dirname(__FILE__), '..'))
+ end
+
+ def public_path
+ File.join(path, 'public')
+ end
+
+ # Copies static resources (JavaScripts, CSS, etc.) to the Rails public directory.
+ #
+ # IMPORTANT:
+ #
+ # I've decided to replace this method with a manually-fired Rails generator when I get time.
+ # This way I don't step on developers' toes if they make customizations to the static files.
+ #
+ def install
+ Dir[File.join(public_path, '*')].each do |source|
+ dest = File.join(Rails.root, 'public', source.sub(public_path, ''))
+
+ # Don't copy if the dest file exists, and is newer than the source.
+ next if File.file?(dest) && File.stat(dest).mtime >= File.stat(source).mtime
+
+ if File.directory?(source)
+ FileUtils.cp_r source.concat('/.'), dest
+ else
+ FileUtils.cp source, dest
+ end
+ end
+ end
+ end
+end
View
28 lib/webgl/railtie.rb
@@ -0,0 +1,28 @@
+module WebGL
+ class Railtie < Rails::Railtie
+# rake_tasks do
+# load 'webgl/rake_tasks.rb'
+# end
+
+ initializer 'webgl' do |config|
+ if Rails.env.production?
+ WebGL.install
+ else
+ config.middleware.use '::ActionDispatch::Static', WebGL.public_path
+ end
+ end
+
+ config.before_configuration do
+ config.action_view.javascript_expansions[:webgl] = Dir[File.join(WebGL.public_path, "javascripts/**/*.js")]
+ end
+
+# generators do
+# require File.join(WebGL.path, "generators/webgl/webgl_generator")
+# end
+
+# config.to_prepare do
+# ApplicationController.helper WebGL::Helper
+# ApplicationController.layout 'name'
+# end
+ end
+end
View
0  log/development.log
No changes.
View
12 public/javascripts/control/keycodes.js
@@ -0,0 +1,12 @@
+var KC_BACKSPACE = 8,
+ KC_TAB = 9,
+ KC_ENTER = 13,
+ KC_SHIFT = 16,
+ KC_CTRL = 17,
+ KC_ALT = 18,
+ KC_PAUSE = 19,
+ KC_BREAK = 19,
+ KC_PAUSE_BREAK=19,
+ KC_CAPS_LOCK = 20
+
+;
View
150 public/javascripts/control/mouse_weight.js
@@ -0,0 +1,150 @@
+var MouseWeight = Class.create(Renderable, {
+ initialize: function($super, world, options) {
+ options = options || (world && (world.segments || world.radius) ? world : {});
+ if (options.world) world = options.world;
+
+ this.world = world;
+ this.segments = options.segments || 32;
+ this.radius = options.radius || 100;
+ this.sensitivity = options.sensitivity ||10;
+ this.xsize = options.xsize || 10;
+ this.magnitude = {x:0, y:0};
+ this.x = world.context.gl.viewportWidth / 2.0;
+ this.y = world.context.gl.viewportHeight / 2.0;
+ this.speed = Math.PI/4;
+ this.cursor = {};
+ this.cursor.out_of_bounds = options.cursor && options.cursor.out_of_bounds || "none";
+ this.cursor.suspended = options.cursor && options.cursor.suspended || "default";
+ this.outer_color = options.outer_color || [1,0.5,0.5,0.15];
+ this.inner_color = options.inner_color || [0.5,1,0.5,0.50];
+ this.x_color = options.x_color || [1.0,1.0,1,0.50];
+ this.invert = typeof(options.invert) == "undefined" ? false : options.invert;
+ this.suspended = false;
+
+ $super();
+ },
+
+ /*
+ initialization function used by Renderable to build the mouseweight's mesh.
+ This method is only called if the mouseweight actually gets rendered.
+ */
+ init: function(verts, colors) {
+ this.draw_mode = GL_LINES;//_STRIP;
+ var theta, increment = Math.PI*2 / this.segments;
+ var cos, sin, cos2, sin2;
+ var i;
+
+ this.updateX(verts);
+ for (i = 0; i < 4; i++)
+ colors.push(this.x_color[0], this.x_color[1], this.x_color[2], this.x_color[3]);
+
+ for (i = 0; i < this.segments; i++) {
+ theta = increment * i;
+ cos = Math.cos(theta);
+ sin = Math.sin(theta);
+ cos2 = Math.cos(theta+increment);
+ sin2 = Math.sin(theta+increment);
+
+
+ verts.push(cos * this.radius, sin * this.radius, 0);
+ verts.push(cos2* this.radius, sin2* this.radius, 0);
+ colors.push(this.outer_color[0], this.outer_color[1], this.outer_color[2], this.outer_color[3]);
+ colors.push(this.outer_color[0], this.outer_color[1], this.outer_color[2], this.outer_color[3]);
+
+ // sensitivity ring
+ verts.push(cos * this.sensitivity, sin * this.sensitivity, 0);
+ verts.push(cos2* this.sensitivity, sin2* this.sensitivity, 0);
+ colors.push(this.inner_color[0], this.inner_color[1], this.inner_color[2], this.inner_color[3]);
+ colors.push(this.inner_color[0], this.inner_color[1], this.inner_color[2], this.inner_color[3]);
+ }
+ },
+
+ /* renders the mouseweight at the center of the canvas. */
+ render: function($super, options) {
+ if (this.suspended) return;
+ $super(options);
+ },
+
+ /* overrides Renderable#applyMatrices so that the mouseweight will be rendered in ortho mode */
+ applyMatrices: function(options) {
+ loadIdentity();
+ mvTranslate(0,0,-1);
+ var w = options.context.gl.viewportWidth / 2.0;
+ var h = options.context.gl.viewportHeight / 2.0;
+ pMatrix = makeOrtho(-w, w, -h, h, 0.01, 250);
+ },
+
+ /* updates the position of the marker which represents the mouse-driven weight */
+ updateX: function(buf) {
+ var xsize = this.xsize / 2.0;
+ var x = this.x, y = this.y;
+ var gl = this.world.context.gl;
+
+ x -= gl.viewportWidth / 2.0;
+ y -= gl.viewportHeight / 2.0;
+
+ // cap values at 1.0 magnitude
+ // first find normal from origin
+ var normal = [x,y].normalize();
+
+ // multiply by radius to get extreme values
+ var nx1 = normal[0] * this.radius, nx2 = normal[0] * this.sensitivity;
+ var ny1 = normal[1] * this.radius, ny2 = normal[1] * this.sensitivity;
+
+ // clamp values to extremes
+ if (nx1 > 0 && x > nx1) x = nx1;
+ if (nx1 < 0 && x < nx1) x = nx1;
+ if (ny1 > 0 && y > ny1) y = ny1;
+ if (ny1 < 0 && y < ny1) y = ny1;
+ if (nx2 > 0 && x < nx2) x = 0;
+ if (nx2 < 0 && x > nx2) x = 0;
+ if (ny2 > 0 && y < ny2) y = 0;
+ if (ny2 < 0 && y > ny2) y = 0;
+
+ if ((x == 0 || mouse.x - gl.viewportWidth / 2.0 == x) &&
+ (y == 0 || mouse.y - gl.viewportHeight / 2.0 == y))
+ this.world.context.canvas.style.cursor = 'none';
+ else
+ this.world.context.canvas.style.cursor = this.cursor.out_of_bounds;
+
+ this.magnitude.x = parseFloat(x) / parseFloat(this.radius);
+ this.magnitude.y = parseFloat(y) / parseFloat(this.radius);
+
+ buf[0] = x - xsize; buf[ 1] = y - xsize; buf[ 2] = 0;
+ buf[3] = x + xsize; buf[ 4] = y + xsize; buf[ 5] = 0;
+ buf[6] = x + xsize; buf[ 7] = y - xsize; buf[ 8] = 0;
+ buf[9] = x - xsize; buf[10] = y + xsize; buf[11] = 0;
+ },
+
+ /*
+ applies the specified X, Y and Z rotations to the world's camera. This is only here
+ so you can override it as needed, for instance to control some object other than
+ the camera.
+ */
+ apply: function(x, y, z) {
+ this.world.camera.rotateView(x, y, z);
+ },
+
+ /*
+ Update function, called by Renderable. Updates the weight position and calls #apply.
+ Does nothing if this.suspended evaluates to true.
+ */
+ update: function(tc) {
+ if (this.suspended) {
+ this.world.context.canvas.style.cursor = this.cursor.suspended;
+ return;
+ }
+
+ var buf = this.mesh.getVertexBuffer();
+ if (buf && mouse && mouse.context && mouse.context == this.world.context
+ && (mouse.x != this.x || mouse.y != this.y)) {
+ this.x = mouse.x;
+ this.y = mouse.y;
+
+ this.updateX(buf.js);
+ buf.refresh();
+ }
+
+ this.apply((this.invert ? -1 : 1) * this.magnitude.y * this.speed * tc, this.magnitude.x * this.speed * tc, 0);
+ }
+});
View
366 public/javascripts/culling/octree.js
@@ -0,0 +1,366 @@
+var Octree = (function() {
+ var FRONT_TOP_LEFT = 1, FRONT_TOP_RIGHT = 2, FRONT_BOTTOM_LEFT = 3, FRONT_BOTTOM_RIGHT = 4,
+ BACK_TOP_LEFT = 5, BACK_TOP_RIGHT = 6, BACK_BOTTOM_LEFT = 7, BACK_BOTTOM_RIGHT = 8;
+
+ function objPosition(obj) { return obj.orientation ? obj.orientation.getPosition() : obj.position; }
+
+ function delegateObjectToNode(self, obj)
+ {
+ // first figure out which quadrant the object is in. We'll use just its position for this.
+
+ // FIXME since we're going solely on position, this means some octree nodes can overlap when
+ // an object close to one edge has triangles that cross that edge, and the opposite octree
+ // has its own object with triangles crossing the boundary in the reverse direction.
+ // o =>|
+ // |<= o
+
+ var quadrant = self.quadrantForPoint(objPosition(obj));
+
+
+ // then add the object to the node in that quadrant.
+ self.nodes[quadrant].addObject(obj);
+ }
+
+ function objectSize(obj)
+ {
+ var vertices = obj.mesh ? obj.mesh.getVertexBuffer().js : obj.vertices;
+ if (vertices.length == 0) { logger.info("no size!"); return [0,0,0]; }
+ var right = null, left = null, top = null, bottom = null, front = null, back = null;
+ for (var i = 0; i < vertices.length; i += 3)
+ {
+ var x = vertices[i], y = vertices[i+1], z = vertices[i+2];
+ if (right == null || right < x) right = x;
+ if (left == null || left > x) left = x;
+ if (top == null || top < y) top = y;
+ if (bottom== null || bottom> y) bottom= y;
+ if (front == null || front < z) front = z;
+ if (back == null || back > z) back = z;
+ }
+ return [right-left, top-bottom, front-back];
+ }
+
+ function recalculateNodeSize(node)
+ {
+ var right = null, left = null, top = null, bottom = null, front = null, back = null;
+ var r, l, t, b, f, z;
+ var pos, i;
+ var p = node.position;
+ if (node.objects)
+ {
+ for (i = 0; i < node.objects.length; i++)
+ {
+ pos = objPosition(node.objects[i]);
+ var size = objectSize(node.objects[i]);
+ r = pos[0] + size[0];
+ l = pos[0] - size[0];
+ t = pos[1] + size[1];
+ b = pos[1] - size[1];
+ f = pos[2] + size[2];
+ z = pos[2] - size[2];
+
+ if (right == null) { right = r; left = l; top = t; bottom = b; front = f; back = z; }
+ else
+ {
+ right = Math.max(right, r); left = Math.min(left, l); top = Math.max(top, t);
+ bottom= Math.min(bottom,b); front= Math.max(front,f); back= Math.min(back,z);
+ }
+ }
+ }
+// else if (node.nodes)
+// {
+// for (i in node.nodes)
+// {
+// var n = node.nodes[i];
+// pos = n.position;
+// r = pos[0]+n.width /2.0;
+// l = pos[0]-n.width /2.0;
+// t = pos[1]+n.height/2.0;
+// b = pos[1]-n.height/2.0;
+// f = pos[2]+n.depth /2.0;
+// z = pos[2]-n.depth /2.0;
+// if (r > right) right = r;
+// if (l < left) left = l;
+// if (t > top) top = t;
+// if (b < bottom)bottom= b;
+// if (f > front) front = f;
+// if (z < back) back = z;
+// }
+// }
+
+ var width = right - left, height = top - bottom, depth = front - back;
+ if (width != node.width || height != node.height || depth != node.depth) {
+ node.width = right - left;
+ node.height = top - bottom;
+ node.depth = front - back;
+
+ // we also need to reset the node position so it's at the center of the new dimensions.
+ node.position[0] = left + node.width / 2.0;
+ node.position[1] = bottom + node.height/ 2.0;
+ node.position[2] = back + node.depth / 2.0;
+
+ node.callbacks.fire("size_changed");
+ }
+ }
+
+ function subdivide(self)
+ {
+ var objs = self.max_objects;
+ var lvls = self.max_levels - 1;
+ self.nodes = {};
+ self.nodes[FRONT_TOP_LEFT] = new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[1,0,0,1]});
+ self.nodes[FRONT_TOP_RIGHT] = new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[0,1,0,1]});
+ self.nodes[FRONT_BOTTOM_LEFT]= new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[0,0,1,1]});
+ self.nodes[FRONT_BOTTOM_RIGHT]=new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[1,1,0,1]});
+ self.nodes[BACK_TOP_LEFT] = new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[0,1,1,1]});
+ self.nodes[BACK_TOP_RIGHT] = new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[1,0,1,1]});
+ self.nodes[BACK_BOTTOM_LEFT] = new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[1,0.5,0.5,1]});
+ self.nodes[BACK_BOTTOM_RIGHT]= new Octree({max_objects:objs,max_levels:lvls,level:self.level+1,color:[0.5,1,0.5,1]});
+ }
+
+ var klass = Class.create({
+ /* Options may include any of the following:
+ max_objects: a number representing how many objects can be added to this octree. If this threshold is exceeded,
+ the octree will be subdivided and its objects will be sorted into its nodes. Defaults to 8.
+ max_levels : a number representing how many sublevels this octree may contain. max_levels trumps max_objects
+ such that if max_levels has been reached, max_objects may be exceeded. Defaults to 8.
+ */
+ initialize: function(options) {
+ var self = this;
+ options = options || {};
+ this.nodes = null;
+ this.position = [0,0,0];
+ this.level = options.level || 1;
+ this.width = this.height = this.depth = 0;
+ this.max_levels = typeof(options.max_levels) == "undefined" ? 8 : options.max_levels;
+ this.max_objects = typeof(options.max_objects)== "undefined" ? 8 : options.max_objects;
+ this.objects = [];
+ this.color = options.color;
+ this.callbacks = {
+ fire: function(name) {
+ if (self.callbacks[name])
+ for (var i = 0; i < self.callbacks[name].length; i++)
+ self.callbacks[name][i]();
+ },
+ size_changed: []
+ };
+
+ if (options.callbacks && options.callbacks.size_changed) this.callbacks.size_changed.push(options.callbacks.size_changed);
+
+ /* an octree's "position" is the calculated center of its width, height and depth -- so this is useless because
+ it will be replaced as soon as the first object is added. */
+// if (options.position) {
+// this.position[0] = options.position[0];
+// this.position[1] = options.position[1];
+// this.position[2] = options.position[2];
+// }
+ },
+
+ isSubdivided: function() { return !!this.nodes; },
+
+ /*
+ Returns a number representing the quadrant this point appears in, with this Octree's position as the origin.
+ The return value is one of the predefined quadrant identifiers: FRONT_TOP_LEFT, BACK_BOTTOM_RIGHT, etc.
+ */
+ quadrantForPoint: function(point) {
+ if (point[0] > this.position[0]) {
+ if (point[1] > this.position[1]) {
+ if (point[2] > this.position[2]) return FRONT_TOP_RIGHT;
+ else return BACK_TOP_RIGHT;
+ } else {
+ if (point[2] > this.position[2]) return FRONT_BOTTOM_RIGHT;
+ else return BACK_BOTTOM_RIGHT;
+ }
+ } else {
+ if (point[1] > this.position[1]) {
+ if (point[2] > this.position[2]) return FRONT_TOP_LEFT;
+ else return BACK_TOP_LEFT;
+ } else {
+ if (point[2] > this.position[2]) return FRONT_BOTTOM_LEFT;
+ else return BACK_BOTTOM_LEFT;
+ }
+ }
+ },
+
+ /*
+ At a minimum, options must contain 'context'. It may also contain any of the following:
+ shader: a Shader object, default null. A shader to be used for all objects in place of their normal shaders.
+ render_octree: boolean, default false. If true, the edges of the octree will be rendered.
+ render_objects: boolean, default true. If false, the objects within this node will not be rendered.
+ frustum: the instance of Frustum to check nodes against. If omitted, defaults to context.world.camera.frustum.
+ */
+ render: function(options) {
+ if (!this.normalized) this.normalize();
+ if (!options.context) throw new Error("A context is required!");
+
+ var frustum = options.frustum || options.context.world.camera.frustum;
+ if (!frustum) throw new Error("A frustum is required!");
+
+ if (typeof(options.render_objects) == "undefined") options.render_objects = true;
+
+ var visibility;
+ if (this.isSubdivided())
+ for (var id in this.nodes)
+ {
+ if (this.nodes[id].isSubdivided() || this.nodes[id].objects.length > 0)
+ {
+ visibility = frustum.cube(this.nodes[id].position, this.nodes[id].width, this.nodes[id].height, this.nodes[id].depth);
+ if (visibility == Frustum.INTERSECT)
+ this.nodes[id].render(options);
+ else if (visibility == Frustum.INSIDE)
+ this.nodes[id].renderObjects(options);
+ }
+ }
+ else this.renderObjects(options);
+
+ /* render the octree, if requested */
+ if (options.render_octree) this.getRenderable().render(options);
+ },
+
+ /* Returns all objects associated with this octree (or its nodes) that are currently visible in the given frustum.
+ The individual objects are not tested; only subnodes. Therefore, if there are no subnodes, all objects will be
+ returned regardless of whether they are visible.
+ */
+ getVisible: function(frustum) {
+ if (!this.normalized) this.normalize();
+
+ var visibility;
+ var all = [], i;
+ if (this.isSubdivided()) {
+ for (var id in this.nodes)
+ {
+ if (this.nodes[id].isSubdivided() || this.nodes[id].objects.length > 0)
+ {
+ visibility = frustum.cube(this.nodes[id].position, this.nodes[id].width, this.nodes[id].height, this.nodes[id].depth);
+
+ if (visibility == Frustum.INTERSECT)
+ all = all.concat(this.nodes[id].getVisible(frustum));
+ else if (visibility == Frustum.INSIDE)
+ all = all.concat(this.nodes[id].objects);
+ }
+ }
+ } else {
+ all = all.concat(this.objects);
+ }
+ return all;
+ },
+
+ /*
+ Renders all objects within this octree. Child nodes will not be rendered regardless of options, but the objects
+ within them will be.
+ */
+ renderObjects: function(options) {
+ /* TODO make a decision whether we should frustum cull objects individually. Right now I'd say no. */
+ var i;
+
+ if (options.render_objects)
+ {
+ for (i = 0; i < this.objects.length; i++)
+ this.objects[i].render(options);
+ if (this.isSubdivided())
+ {
+ for (i in this.nodes)
+ this.nodes[i].renderObjects(options);
+ }
+ }
+ },
+
+ numLevels: function() {
+ if (!this.normalized) this.normalize();
+ if (this.isSubdivided())
+ {
+ var count = 0;
+ for (var i in this.nodes)
+ count = Math.max(count, this.nodes[i].numLevels());
+ return count+1;
+ }
+
+ return 1;
+ },
+
+ addObject: function(obj) {
+ /* invalidate the octree, because its bounds are no longer accurate and it may need to be subdivided. */
+ this.normalized = false;
+
+ /* TODO add a position listener to the object so that non-static objects can be used */
+ this.objects.push(obj);
+ },
+
+ normalize: function() {
+ recalculateNodeSize(this);
+
+ if (this.objects.length >= this.max_objects && this.max_levels > 0)
+ subdivide(this);
+
+ /* handle any objects added after subdivision */
+ if (this.isSubdivided())
+ {
+ var i;
+ for (i = this.objects.length-1; i >= 0; i--)
+ delegateObjectToNode(this, this.objects.pop());
+ for (i in this.nodes)
+ this.nodes[i].normalize();
+ }
+
+ this.normalized = true;
+ },
+
+ getRenderable: function() {
+ if (this.renderable) return this.renderable;
+
+ var self = this;
+
+ var renderable = new Renderable({
+ init: function(vertices, colors, texc, norms, indices) {
+ var w = self.width / 2.0, h = self.height / 2.0, d = self.depth / 2.0;
+ renderable.draw_mode = GL_LINES;
+
+ /* edges */
+ vertices.push(-w, -h, -d); vertices.push(-w, h, -d);
+ vertices.push( w, -h, -d); vertices.push( w, h, -d);
+ vertices.push(-w, -h, d); vertices.push(-w, h, d);
+ vertices.push( w, -h, d); vertices.push( w, h, d);
+ vertices.push(-w, -h, -d); vertices.push(-w, -h, d);
+ vertices.push( w, -h, -d); vertices.push( w, -h, d);
+ vertices.push(-w, h, -d); vertices.push(-w, h, d);
+ vertices.push( w, h, -d); vertices.push( w, h, d);
+ vertices.push(-w, -h, -d); vertices.push( w, -h, -d);
+ vertices.push(-w, h, -d); vertices.push( w, h, -d);
+ vertices.push(-w, -h, d); vertices.push( w, -h, d);
+ vertices.push(-w, h, d); vertices.push( w, h, d);
+
+ /* and a small star at the node center */
+ vertices.push(-0.05, -0.05, 0);
+ vertices.push( 0.05, 0.05, 0);
+ vertices.push(-0.05, 0.05, 0);
+ vertices.push( 0.05, -0.05, 0);
+ vertices.push( 0, 0, -0.05);
+ vertices.push( 0, 0, 0.05);
+ },
+
+ update: null
+ });
+
+ self.renderable = renderable;
+ self.renderable.orientation.setPosition(self.position);
+ self.callbacks.size_changed.push(function() {
+ self.renderable.invalidate();
+ self.renderable.orientation.setPosition(self.position);
+ });
+ if (self.color) { self.renderable.mesh.setColor(self.color); }
+
+ return renderable;
+ }
+ });
+
+ klass.FRONT_TOP_LEFT = FRONT_TOP_LEFT;
+ klass.FRONT_TOP_RIGHT = FRONT_TOP_RIGHT;
+ klass.FRONT_BOTTOM_LEFT = FRONT_BOTTOM_LEFT;
+ klass.FRONT_BOTTOM_RIGHT = FRONT_BOTTOM_RIGHT;
+ klass.BACK_TOP_LEFT = BACK_TOP_LEFT;
+ klass.BACK_TOP_RIGHT = BACK_TOP_RIGHT;
+ klass.BACK_BOTTOM_LEFT = BACK_BOTTOM_LEFT;
+ klass.BACK_BOTTOM_RIGHT = BACK_BOTTOM_RIGHT;
+
+ return klass;
+}());
+
View
103 public/javascripts/engine/animation.js
@@ -0,0 +1,103 @@
+// each object should have its own instance of Animation so that it can
+// be stopped, started, etc. independently of other objects. However, the
+// data used by those Animations is shared, so this is OK.
+var Animation = (function() {
+ // calculates interpolation from current frame to next frame. This amount will
+ // be multiplied by the interpolation percentage, which is increased based on
+ // timechange via calls to #update.
+ function interpolate(anim)
+ {
+ var next = anim.nextFrame();
+ var current = anim.currentFrame();
+ var i, j;
+
+ // figure out vertex information
+ for (i = 0; i < current.vertices.length; i++)
+ {
+ anim.interpolation.vertices[i] = next.vertices[i] - current.vertices[i];
+ anim.interpolation.normals[i] = next.normals[i] - current.normals[i];
+ }
+
+ // reset the timer and mark the interpolation as valid
+ anim.interpolation.timer = 0;
+ anim.interpolation.timeout = 1.0 / anim.meta.fps;
+ anim.interpolation.valid = true;
+ }
+
+ return Class.create({
+ initialize: function(meta, options)
+ {
+ if (!options.frames) throw new Error("No frame data given!");
+ if (!meta) throw new Error("No meta data given!");
+
+ // meta contains: start, stop, fps, name, loop
+ this.name = meta.name;
+ this.meta = meta;
+ this.frames = options.frames;
+ this.loop = (typeof(options.loop) == "undefined") ? meta.loop : options.loop;
+ this.playing = true;
+ this.interpolation = {vertices:{},normals:{}};
+ this.current = meta.start;
+ },
+
+ currentFrameIndex: function() { return this.current; },
+
+ /* Returns the next frame index. If the animation has ended, the current index is returned.
+ If the animation has ended and is set to loop, the first index is returned. */
+ nextFrameIndex: function() {
+ var current = this.currentFrameIndex(), fi = current + 1;
+ if (fi > this.meta.stop)
+ if (this.loop) return this.meta.start;
+ else return current;
+ else return fi;
+ },
+
+ currentFrame: function() { return this.frames[this.currentFrameIndex()]; },
+ nextFrame: function() { return this.frames[this.nextFrameIndex()]; },
+
+ /* "steps" ahead a number of frames. The default is 1. */
+ step: function(distance) {
+ this.interpolation.valid = false;
+ if (typeof(distance) != "number") distance = 1;
+ for (var i = 0; i < distance; i++)
+ this.current = this.nextFrameIndex();
+ },
+
+ /* update should be called by the object which is being animated. It expects two arguments:
+ self - the object being animated (expected to be an instance of Renderable).
+ timechange - the number of seconds since the last call to #update.
+ */
+ update: function(self, timechange) {
+ if (!this.interpolation.valid)
+ interpolate(this);
+ else
+ {
+ var i, current = this.currentFrame();
+ this.interpolation.timer += timechange;
+ var pcnt = (this.interpolation.timer / this.interpolation.timeout);
+
+ if (pcnt > 0)
+ {
+ // have we run out the timer?
+ if (1 - pcnt <= Math.EPSILON)
+ { //...yes
+ this.step();
+ pcnt = 1;
+ }
+
+ var v = self.mesh.getVertexBuffer(), n = self.mesh.getNormalBuffer();
+ if (v && n)
+ {
+ for (i = 0; i < v.js.length; i++)
+ {
+ v.js[i] = (current.vertices[i] + (this.interpolation.vertices[i] * pcnt)) * self.scale;
+ n.js[i] = (current.normals[i] + (this.interpolation.normals[i] * pcnt)); // don't scale normals :)
+ }
+ v.refresh();
+ n.refresh();
+ }
+ }
+ }
+ }
+ });
+})();
View
48 public/javascripts/engine/assertions.js
@@ -0,0 +1,48 @@
+function assert(cond, msg)
+{
+// try {
+ if (!cond) { throw new Error(msg || "Assertion failed!"); }
+// }
+// catch (e) {
+// logger.error(e+"\n\n"+e.stack);
+// throw e;
+// }
+}
+
+function assert_not_equal(not_expected, actual, message)
+{
+ try { if ((!not_expected.equals || !not_expected.equals(actual)) && not_expected != actual) return; }
+ catch (e) { if (not_expected != actual) return; }
+
+ message = message || "Expected <"+actual.toString()+"> to not equal <"+not_expected.toString()+">";
+ assert(false, message);
+}
+
+function assert_equal(expected, actual, message)
+{
+ try { if (expected.equals && expected.equals(actual) || expected == actual) return; }
+ catch (e) { if (expected == actual) return; }
+
+ message = message || "Expected <"+actual.toString()+"> to equal <"+expected.toString()+">";
+ assert(false, message);
+}
+
+function assert_false(cond, msg)
+{
+ assert(!cond, msg);
+}
+
+function assert_defined(obj, msg)
+{
+ assert(obj, msg || "Expected value to be defined");
+}
+
+function assert_null(obj, msg)
+{
+ assert_equal(null, obj, msg || "Expected <"+obj.toString()+"> to be null");
+}
+
+function assert_not_null(obj, msg)
+{
+ assert_not_equal(null, obj, msg || "Expected to not be null");
+}
View
102 public/javascripts/engine/buffer.js
@@ -0,0 +1,102 @@
+/*
+ Wrapper to manage JS and GL buffer (array) types. Automates context juggling by requiring the context to generate the
+ buffer for as an argument to #bind. If the context doesn't have a corresponding GL buffer for this data, it will be
+ created. Calling #refresh will regenerate the buffer data for all contexts.
+*/
+var Buffer = (function() {
+ function each_gl_buffer(self, func)
+ {
+ for (var id in self.gl)
+ func(self.gl[id].context, self.gl[id].buffer);
+ }
+
+ return Class.create({
+ initialize: function(bufferType, classType, drawType, jsarr, itemSize) {
+ if (jsarr.length == 0) throw new Error("No elements in array to be buffered!");
+ if (!itemSize) throw new Error("Expected an itemSize - how many JS array elements represent a single buffered element?");
+ this.classType = classType;
+ this.itemSize = itemSize;
+ this.js = jsarr;
+ this.gl = {};
+ this.numItems = jsarr.length / itemSize;
+ this.bufferType = bufferType;
+ this.drawType = drawType;
+ },
+
+ refresh: function() {
+ var self = this;
+ if (self.classTypeInstance)
+ for (var i = 0; i < self.js.length; i++)
+ self.classTypeInstance[i] = self.js[i];
+ else
+ self.classTypeInstance = new self.classType(self.js);
+
+ if (!self.gl) return;
+
+ logger.attempt("buffer#refresh", function() {
+ each_gl_buffer(self, function(context, buffer) {
+ context.bindBuffer(self.bufferType, buffer);
+ context.bufferData(self.bufferType, self.classTypeInstance, self.drawType);
+ });
+ });
+ },
+
+ dispose: function() {
+ var self = this;
+ each_gl_buffer(this, function(context, buffer) {
+ context.deleteBuffer(buffer);
+ self.gl[context.id] = null;
+ });
+ self.gl = {};
+ },
+
+ isDisposed: function() { return !this.gl; },
+
+ bind: function(context) { context.bindBuffer(this.bufferType, this.getGLBuffer(context)); },
+
+ getGLBuffer: function(context)
+ {
+ if (!context || typeof(context.id) == "undefined")
+ throw new Error("Cannot build a buffer without a context!");
+
+ if (!this.gl[context.id])
+ {
+ var buffer = context.createBuffer();
+ buffer.itemSize = this.itemSize;
+ buffer.numItems = this.js.length;
+ this.gl[context.id] = {context:context,buffer:buffer};
+ this.refresh();
+ }
+ return this.gl[context.id].buffer;
+ }
+ });
+})();
+
+// More user-friendly versions of the above
+var ElementArrayBuffer = Class.create(Buffer, {
+ initialize: function($super, jsarr) {
+ $super(GL_ELEMENT_ARRAY_BUFFER, Uint16Array, GL_STREAM_DRAW, jsarr, 1);
+ }
+});
+
+var FloatArrayBuffer = Class.create(Buffer, {
+ initialize: function($super, jsarr, itemSize) {
+ $super(GL_ARRAY_BUFFER, Float32Array, GL_STATIC_DRAW, jsarr, itemSize);
+ }
+});
+
+var VertexBuffer = Class.create(FloatArrayBuffer, {
+ initialize: function($super, jsarr) { $super(jsarr, 3); }
+});
+
+var ColorBuffer = Class.create(FloatArrayBuffer, {
+ initialize: function($super, jsarr) { $super(jsarr, 4); }
+});
+
+var TextureCoordsBuffer = Class.create(FloatArrayBuffer, {
+ initialize: function($super, jsarr) { $super(jsarr, 2); }
+});
+
+var NormalBuffer = Class.create(FloatArrayBuffer, {
+ initialize: function($super, jsarr) { $super(jsarr, 3); }
+});
View
392 public/javascripts/engine/camera.js
@@ -0,0 +1,392 @@
+function Camera(options)
+{
+ var default_options = {
+ lock_up_vector: false,
+ lock_y_axis: false
+ };
+ options = options || default_options;
+
+ var self = this;
+ var view = [0,0,-1];
+ var position = [0,0,0];
+ var up = [0,1,0];
+ var right = view.cross(up).normalize();
+ var matrix = Matrix.I(4);
+ var pmatrix = null;
+ matrix.setLookAt(position, view, up, right);
+
+ self.callbacks = self.callbacks || {
+ matrices_changed: function() { self.fireCallbacks('matrices'); },
+ orientation_changed: function() { self.fireCallbacks('orientation'); },
+ position_changed: function(pos, vec) { self.fireCallbacks('position', [pos, vec]); },
+ position: [],
+ matrices: [],
+ orientation: []
+ };
+
+ self.lock_up_vector = options.lock_up_vector || default_options.lock_up_vector;
+ self.lock_y_axis = options.lock_y_axis || default_options.lock_y_axis;
+
+ self.getProjectionMatrix = function() { return pmatrix; };
+ self.getMatrix = function() { return matrix; };
+ self.getView = function() { return view; };
+ self.getUp = function() { return up; };
+ self.getPosition = function() { return position; };
+ self.getRight = function() { return right; };
+ self.getFrustum = function() { return self.frustum = self.frustum || new Frustum(self.getMatrix(), self.getProjectionMatrix()); };
+
+ self.setPosition = function(vec, y, z) {
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ if (self.callbacks && self.callbacks.position_changed) self.callbacks.position_changed(position, vec);
+ position = vec;
+ self.look();
+ return self;
+ };
+
+ self.setView = function(vec, y, z) {
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ view = vec.normalize();
+ right = view.cross(up).normalize();
+ up = right.cross(view).normalize();
+ self.look();
+ self.callbacks.orientation_changed();
+ return self;
+ };
+
+ self.setUp = function(vec, y, z) {
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ up = vec.normalize();
+ right = view.cross(up).normalize();
+ view = up.cross(right).normalize();
+ self.look();
+ self.callbacks.orientation_changed();
+ return self;
+ };
+
+ self.setRight = function(vec, y, z) {
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ right = vec.normalize();
+ view = up.cross(right).normalize();
+ up = right.cross(view).normalize();
+ self.look();
+ self.callbacks.orientation_changed();
+ return self;
+ };
+
+ /* Updates the Camera's internal matrix to ensure that it is currently "looking" at the correct position
+ and that it has the correct orientation. Returns the Camera itself.
+
+ If gl is specified, this Camera will apply its matrix transformations to the given WebGL context.
+ */
+ self.look = function(gl) {
+ matrix.setLookAt(position, view, up, right, self.callbacks.matrices_changed);
+ if (gl) self.lookGL(gl);
+ return self;
+ };
+
+ /* this Camera will apply its matrix transformations to the given WebGL context *without* updating its
+ internal matrix.
+ */
+ self.lookGL = function(gl) {
+ setMatrix(matrix);
+ if (!pmatrix) self.perspective(gl);
+ setPMatrix(pmatrix);
+ if (!self.frustum.upToDate) self.frustum.fireListeners('update');
+ };
+
+ /*
+ options can include the following:
+ unit: true or false. If true, scale will be set to {left:-1,bottom:-1,right:1,top:1,near:0.1,far:200}
+ scale: values for viewport size:
+ left : leftmost coord
+ top : topmost coord
+ bottom: bottom-most coord
+ right : rightmost coord
+ near : nearest (positive) coord
+ far : most-distant coord
+ */
+ self.ortho = function(gl, options) {
+ if (!gl) throw new Error("No WebGL context given!");
+ if (gl.gl) gl = gl.gl;
+ options = options || {};
+ if (options.unit) {
+ options.left = typeof(options.left) != "undefined" ? options.left : -1;
+ options.top = typeof(options.top) != "undefined" ? options.top : 1;
+ options.bottom = typeof(options.bottom) != "undefined" ? options.bottom : -1;
+ options.right = typeof(options.right) != "undefined" ? options.right : 1;
+ }
+ else {
+ options.left = typeof(options.left) != "undefined" ? options.left : -(gl.viewportWidth/2.0);
+ options.top = typeof(options.top) != "undefined" ? options.top : gl.viewportHeight/2.0;
+ options.bottom = typeof(options.bottom) != "undefined" ? options.bottom : -(gl.viewportHeight/2.0);
+ options.right = typeof(options.right) != "undefined" ? options.right : gl.viewportWidth/2.0;
+ }
+ options.near = options.near || 0.1;
+ options.far = options.far || 200;
+
+ pmatrix = makeOrtho(options.left, options.right, options.bottom, options.top, options.near, options.far);
+ self.fireCallbacks('matrices');
+ };
+
+ self.perspective = function(gl, options)
+ {
+ if (gl.gl) gl = gl.gl;
+ if (!options) options = {};
+
+ options.fov = options.fov || 45;
+ options.near = options.near || 0.1;
+ options.far = options.far || 200.0;
+ options.ratio = options.ratio || (parseFloat(gl.viewportWidth) / parseFloat(gl.viewportHeight));
+
+ if (!gl) throw new Error("No WebGL context given!");
+ if (gl.gl) gl = gl.gl;
+ pmatrix = makePerspective(options.fov, options.ratio, options.near, options.far);
+ self.fireCallbacks('matrices');
+ };
+
+ /* Explicitly sets this Camera's orientation. This is a dangerous function, because it does NOT do any
+ calculations. So you must first manually verify that viewVec, upVec and rightVec are all at right angles to
+ one another, that they are relative to positionVec, and that they are normalized.
+
+ rightVec is optional and will default to the cross product of view and up.
+ positionVec is optional and will default to the Camera's current position.
+ */
+ self.orient = function(viewVec, upVec, rightVec, positionVec) {
+ view = viewVec.normalize();
+ up = upVec.normalize();
+ right = (rightVec || view.cross(up)).normalize();
+ if (self.callbacks && self.callbacks.orientation_changed) self.callbacks.orientation_changed();
+ if (positionVec)
+ if (self.callbacks && self.callbacks.position_changed) self.callbacks.position_changed(position, positionVec);
+ position = positionVec || position;
+ self.look();
+ return self;
+ };
+
+ self.addListener = self.addCallback = function(name, func) {
+ self.callbacks[name] = self.callbacks[name] || [];
+ self.callbacks[name].push(func);
+ };
+
+ self.fireCallbacks = function(name, args) {
+ if (self.callbacks[name])
+ {
+ for (var i = 0; i < self.callbacks[name].length; i++)
+ {
+ self.callbacks[name][i].apply(self, args);
+ }
+ }
+ };
+
+
+ function updateFrustum() {
+ if (self.frustum)
+ {
+ self.frustum.setMatrices(self.getMatrix(), self.getProjectionMatrix());
+ return self.frustum;
+ }
+ self.frustum = new Frustum(self.getMatrix(), self.getProjectionMatrix());
+ return self.frustum;
+ }
+ self.addListener("matrices", updateFrustum);
+}
+
+/* Sets the view to point at the specified position in world space. Up and right vectors are automatically
+ recalculated; you should set these explicitly using #orient if you need a specific orientation.
+ */
+Camera.prototype.lookAt = function(vec, y, z) {
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ var new_view = vec.minus(this.getPosition());
+ this.setView(new_view);
+ this.look();
+ this.getFrustum().update();
+ return this;
+};
+
+// Converts screen coordinates into a ray segment with one point at the NEAR plane and the other
+// at the FAR plane relative to the camera's current matrices.
+// Code adapted from gluUnproject(), found at http://www.opengl.org/wiki/GluProject_and_gluUnProject_code
+Camera.prototype.unproject = function(context, winx, winy, winz) {
+ if (typeof(winx) != "number" || typeof(winy) != "number") { throw new Error("one or both of Context / X / Y is missing"); }
+
+ // winz is either 0 (near plane), 1 (far plane) or somewhere in between.
+ // if it's not given a value we'll produce coords for both.
+ if (typeof(winz) == "number") {
+ winx = parseFloat(winx);
+ winy = parseFloat(winy);
+ winz = parseFloat(winz);
+
+ var inf = [];
+ var mm = this.getMatrix(), pm = this.getProjectionMatrix();
+ var viewport = [0, 0, context.gl.viewportWidth, context.gl.viewportHeight];
+
+ //Calculation for inverting a matrix, compute projection x modelview; then compute the inverse
+ var m = pm.multiply(mm).inverse();
+
+ // Transformation of normalized coordinates between -1 and 1
+ inf[0]=(winx-viewport[0])/viewport[2]*2.0-1.0;
+ inf[1]=(winy-viewport[1])/viewport[3]*2.0-1.0;
+ inf[2]=2.0*winz-1.0;
+ inf[3]=1.0;
+
+ //Objects coordinates
+ var out = m.multiply($V(inf)).elements;
+ if(out[3]==0.0)
+ return null;
+
+ out[3]=1.0/out[3];
+ return [out[0]*out[3], out[1]*out[3], out[2]*out[3]];
+ }
+ else
+ {
+ return [this.unproject(context, winx, winy, 0), this.unproject(context, winx, winy, 1)];
+ }
+};
+
+/* Rotates the view vector of this Camera, effectively "turning" to look in a new direction. This is very useful
+ when linked with mouse coordinates, or joystick axes, for instance.
+
+ amount_x effectively rotates up and down.
+ amount_y effecitvely rotates right and left.
+ amount_z effectively "twists" clockwise or counterclockwise. This value is optional, and defaults to 0.
+ */
+Camera.prototype.rotateView = function(amount_x, amount_y, amount_z) {
+ if (typeof(amount_x) != "number") { amount_y = amount_x[1]; amount_z = amount_x[2]; amount_x = amount_x[0]; }
+
+ amount_y = -amount_y; //because user is expecting positive y to rotate right, not left.
+ amount_z = amount_z || 0;
+
+ var up = this.getUp(), view = this.getView(), right = this.getRight();
+
+ if (amount_x != 0) // up & down
+ {
+ view = view.rotate(amount_x, right);
+ if (!this.lock_up_vector) up = right.cross(view).normalize();
+ }
+
+ if (amount_y != 0) // left & right
+ {
+ view = view.rotate(amount_y, up);
+ right = view.cross(up).normalize();
+ }
+
+ if (amount_z != 0) // clockwise / counterclockwise
+ {
+ up = up.rotate(amount_z, view);
+ right = view.cross(up).normalize();
+ }
+
+ view = view.normalize();
+
+ if (this.lock_up_vector)
+ {
+ // prevent view from rotating too far
+ // FIXME: I really hate these hard numbers, but I can't figure out a better way to detect this. Seems to work, in
+ // any case, but it may fail if amount_x is too high.
+ var angle = Math.acos(view.dot(up)) - 0.05;
+ if (angle != 0)
+ {
+ if (angle >= 3 - 0.05) angle = -angle;
+ angle /= Math.abs(angle); // we want 1 or -1
+ this.last_angle = this.last_angle || angle;
+ if (angle != this.last_angle && amount_x != 0) return this.rotateView(-amount_x, 0, 0);
+ else this.last_angle = angle;
+ }
+ }
+
+ this.orient(view, up, right);
+ this.look();
+ this.getFrustum().update();
+// if (self.callbacks && self.callbacks.orientation_changed) self.callbacks.orientation_changed();
+ return this;
+};
+
+/* Moves left or right, in relation to the supplied vector or in relation to the camera's current orientation */
+Camera.prototype.strafe = function(distance, vec, y, z)
+{
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ else if (!vec) vec = this.getView();
+
+ var direction = vec.cross(this.getUp()).normalize();
+ if (this.lock_y_axis) direction[1] = 0;
+ direction = direction.times(distance);
+
+ this.setPosition(this.getPosition().plus(direction));
+ this.look();
+ this.getFrustum().update();
+ return this;
+};
+
+/* Moves forward or back, in relation to the supplied vector or in relation to the camera's current orientation. */
+Camera.prototype.move = function(distance, vec, y, z)
+{
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ else if (!vec) vec = this.getView();
+
+ var direction = vec.normalize();
+ if (this.lock_y_axis) direction[1] = 0;
+ direction = direction.multiply(distance);
+
+ this.setPosition(this.getPosition().plus(direction));
+ this.look();
+ this.getFrustum().update();
+ return this;
+};
+
+/* Resets the position and orientation of this camera. This is the same as reloading the identity matrix
+ glLoadIdentity(), and it also resets this camera's position, up, view and right vectors.
+ */
+Camera.prototype.reset = function()
+{
+ var view = [0,0,-1];
+ var position = [0,0,0];
+ var up = [0,1,0];
+ var right = view.cross(up).normalize();
+
+ this.orient(view, up, right);
+ this.setPosition(position);
+ this.look(); // update the matrix with these values
+ this.getFrustum().update();
+ return this;
+};
+
+/* Translates the camera's current coordinates to the specified world position, ignoring its local axes.
+ The right, up and view vectors remain the same, since they are relative to the camera's position.
+
+ This method ignores the state of #lock_y_axis.
+*/
+Camera.prototype.moveTo = function(vec, y, z)
+{
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ this.setPosition(vec);
+ this.getMatrix().setTranslateTo(vec, this.callbacks.matrices_changed);
+ return this;
+};
+
+/* Translates the camera's current coordinates by the supplied amount, relative to its current orientation.
+ For instance, if it is translated 0, 1, 0 (one unit on the positive Y axis), that will be converted into
+ "one unit towards the up vector". Use #moveTo(camera.getPosition().plus(translation)) if you want to translate
+ relative to worldspace (ignoring the right, up and view vectors).
+
+ The right, view and up vectors are not modified by this method, because they are relative to the camera's
+ position.
+
+ This method ignores the state of #lock_y_axis
+*/
+Camera.prototype.translate = function(vec, y, z)
+{
+ if (typeof(vec) == "number") vec = [vec, y, z];
+ var right = this.getRight();
+ var up = this.getUp();
+ var view = this.getView();
+ var position = this.getPosition();
+ var translation = position.plus(right.times(vec[0]).plus(up.times(vec[1]).plus(view.times(vec[2]))));
+
+ this.setPosition(translation);
+ this.getMatrix().setTranslateTo(translation, this.callbacks.matrices_changed);
+ return this;
+};
+
+/* Aliases. */
+Camera.prototype.translateTo = Camera.prototype.moveTo;
+Camera.prototype.loadIdentity = Camera.prototype.reset;
View
69 public/javascripts/engine/canvas_texture.js
@@ -0,0 +1,69 @@
+var CanvasTexture = Class.create({
+ initialize: function(canvas) {
+ var self = this;
+ this.glTexture = null;
+ this.min_filter = GL_LINEAR_MIPMAP_NEAREST;
+ this.mag_filter = GL_LINEAR;
+ this.target = GL_TEXTURE_2D;
+
+ this.source = canvas;
+ this.source.addEventListener("onchange", function() { self.update(); }, true);
+ this.out_of_date = true;
+ this.buffer = document.createElement("canvas");
+ this.buffer.context = this.buffer.getContext("2d");
+
+ var body = document.getElementsByTagName("body")[0];
+ body.appendChild(this.buffer);
+ this.ready = false;
+
+ if (this.source.height <= 0) {
+ var original = this.source.style.display;
+ this.source.style.display = "";
+ this.source.height = this.source.offsetHeight;
+ this.source.width = this.source.offsetWidth;
+ this.source.style.display = original;
+ }
+ this.buffer.style.display = "none";
+
+ this.buffer.height = Math.pow2(this.source.height);
+ this.buffer.width = Math.pow2(this.source.width);
+ },
+
+ generateTexture: function(context) {
+ context.bindTexture(this.target, this.glTexture);
+ this.buffer.context.save();
+ if (this.source.nodeName.toLowerCase() == "video")
+ /* TODO FIXME will this hold across implementations? is this a driver issue? what the hell is with the 1.25 magic number?? */
+ this.buffer.context.scale(this.source.width/this.buffer.width*1.25, this.source.height/this.buffer.height*1.25);
+ else
+ this.buffer.context.scale(this.buffer.width/this.source.width, this.buffer.height/this.source.height);
+ this.buffer.context.clearRect(0, 0, this.buffer.width, this.buffer.height);
+ this.buffer.context.drawImage(this.source, 0, 0);
+ this.buffer.context.restore();
+ context.texImage2D(this.target, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, this.buffer);
+ context.texParameteri(this.target, GL_TEXTURE_MAG_FILTER, this.mag_filter);
+ context.texParameteri(this.target, GL_TEXTURE_MIN_FILTER, this.min_filter);
+ context.generateMipmap(this.target);
+ this.ready = true;
+ },
+
+ update: function() {
+ this.out_of_date = true;
+ },
+
+ bind: function(context, func) {
+ if (!this.glTexture)
+ this.glTexture = context.createTexture();
+
+ if (this.out_of_date)
+ this.generateTexture(context);
+
+ if (!this.ready) { context.bindTexture(this.target, null); }
+ if (func) {
+ func();
+ context.bindTexture(this.target, null);
+ }
+ }
+});
+
+CanvasTexture.instance = function(attributes) { return Texture.instance(attributes); };
View
361 public/javascripts/engine/context.js
@@ -0,0 +1,361 @@
+var WebGLContext = function() {
+
+ function disableShaderAttributes(self) {
+ for (var i = 0; i < GL_MAX_VERTEX_ATTRIBS; i++)
+ self.gl.disableVertexAttribArray(i);
+ self.checkError();
+ }
+
+ return Class.create({
+ /*
+ The first argument is the canvas or ID of the canvas from which the WebGL context will be extracted.
+
+ The second argument is an object whose property names are the names of the shaders which will be used,
+ and whose property values are instances of Shader(). This is not an explicitly required argument, but
+ other objects will expect at least a minimal set of shaders before they can be rendered.
+
+ render_func is optional and will be passed to #startRendering.
+ */
+ initialize: function(element_or_name, shaders, render_func)
+ {
+ this.id = ++WebGLContext.identifier;
+ this.canvas = $(element_or_name);
+ this.callbacks = { mouse: { moved: [], dragged:[], pressed:[], released:[], over:[], out:[], clicked:[] }};
+ if (!this.canvas) throw new Error("Could not find canvas '"+element_or_name+"'");
+
+ this.registerMouseListeners();
+
+ try {
+ this.gl = this.canvas.getContext("experimental-webgl");
+ this.gl.context = this;
+ this.gl.canvas = this.canvas;
+ this.gl.viewportWidth = this.canvas.width;
+ this.gl.viewportHeight = this.canvas.height;
+ }
+ catch(e) { }
+ if (!this.gl) throw new Error("WebGL could not be initialized!");
+
+ WebGLContext.mostRecent = this;
+ //this.frame_count = 0;
+ this.renderInterval = null;
+ this.shaders = shaders || [];
+ this.world = new World(this);
+
+ if (shaders)
+ for (var i in shaders) {
+ shaders[i].context = this;
+ }
+
+ logger.info("WebGL context created for "+this.canvas.id);
+ this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ this.gl.clearDepth(1.0);
+ this.gl.enable(this.gl.DEPTH_TEST);
+ this.gl.depthFunc(this.gl.LEQUAL);
+ this.gl.enable(this.gl.BLEND);
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
+ this.checkError();
+
+ this.startRendering(render_func);
+ },
+
+ /* Adds the function to the list of callbacks to fire whenever the mouse is moved. */
+ onMouseMove: function(func) { this.callbacks.mouse.moved.push(func); },
+ onMouseDrag: function(func) { this.callbacks.mouse.dragged.push(func); },
+ onMousePress: function(func) { this.callbacks.mouse.pressed.push(func); },
+ onMouseRelease:function(func) { this.callbacks.mouse.released.push(func); },
+ onMouseIn: function(func) { this.callbacks.mouse.over.push(func); },
+ onMouseOut: function(func) { this.callbacks.mouse.out.push(func); },
+ onMouseClick: function(func) { this.callbacks.mouse.clicked.push(func); },
+
+ buildShader: function(name) {
+ var shader = new Shader();
+ shader.context = this;
+ if (name) shaders[name] = shader;
+ return shader;
+ },
+
+ checkError: function() {
+ if (typeof(RELEASE) != "undefined" && RELEASE) return;
+ var error = this.gl.getError();
+ if (error != this.gl.NO_ERROR)
+ {
+ var str = "GL error in "+this.canvas.id+": "+error;
+ var err = new Error(str);
+ var message = err;
+ if (err.stack)
+ {
+ var stack = err.stack.split("\n");
+ stack.shift();
+ message += "\n\n"+stack.join("\n");
+ }
+ if (logger) logger.error(message);
+ else alert(message);
+ throw err;
+ }
+ },
+
+ isRendering: function() { return this.renderInterval != null; },
+
+ registerMouseListeners: function() {
+ var self = this;
+ Event.observe(this.canvas, "mousemove", function(event) {
+ event = event || {};
+ event.source = this;
+ mouse.overCanvas = mouse.canvas = mouse.target = self.canvas;
+ mouse.context = self;
+ mouse.offsetx = mouse.x;
+ mouse.offsety = mouse.y;
+ mouse.x = event.clientX - self.canvas.cumulativeOffset()[0];
+ mouse.y = event.clientY - self.canvas.cumulativeOffset()[1];
+ mouse.y = self.gl.viewportHeight - mouse.y;
+ // add scroll offsets
+ if (window.pageXOffset)
+ {
+ mouse.x += window.pageXOffset;
+ mouse.y += window.pageYOffset;
+ }
+ else
+ {
+ mouse.x += document.body.scrollLeft;
+ mouse.y += document.body.scrollTop;
+ }
+ if (mouse.offsetx)
+ mouse.diffx = mouse.x - mouse.offsetx;
+ if (mouse.offsety)
+ mouse.diffy = mouse.y - mouse.offsety;
+
+ if (mouse.down == null)
+ for (var i = 0; i < self.callbacks.mouse.moved.length; i++)
+ self.callbacks.mouse.moved[i](event);
+ else
+ for (var i = 0; i < self.callbacks.mouse.dragged.length; i++)
+ self.callbacks.mouse.dragged[i](event);
+ });
+
+ Event.observe(this.canvas, "mouseover", function(event) {
+ event = event || {};
+ event.source = this;
+ for (var i = 0; i < self.callbacks.mouse.over.length; i++)
+ self.callbacks.mouse.over[i](event);
+ });
+
+ Event.observe(this.canvas, "mouseout", function(event) {
+ event = event || {};
+ event.source = this;
+ for (var i = 0; i < self.callbacks.mouse.out.length; i++)
+ self.callbacks.mouse.out[i](event);
+ });
+
+ Event.observe(this.canvas, "click", function(event) {
+ event = event || {};
+ event.source = this;
+ for (var i = 0; i < self.callbacks.mouse.clicked.length; i++)
+ self.callbacks.mouse.clicked[i](event);
+ });
+
+ Event.observe(this.canvas, "mousedown", function(event) {
+ event = event || {};
+ event.source = this;
+ var button = event.which;
+
+ mouse.down = mouse.down || {count:0,down:{}};
+ button = mouse.down["button"+button] = {at:[mouse.x,mouse.y]};
+
+ for (var i = 0; i < self.callbacks.mouse.pressed.length; i++)
+ self.callbacks.mouse.pressed[i](event);
+ });
+
+ Event.observe(this.canvas, "mouseup", function(event) {
+ event = event || {};
+ event.source = this;
+ mouse.down.count--;
+ if (mouse.down.count <= 0)
+ mouse.down = null;
+
+ for (var i = 0; i < self.callbacks.mouse.released.length; i++)
+ self.callbacks.mouse.released[i](event);
+ });
+ },
+
+ /* Sets the render interval. If already rendering, the current one will be cleared and a new one will be set.
+ Takes an optional render_func parameter, which will be called as part of the render process. If omitted and
+ a render function already exists, the current one will be used. Note that the render_func argument is
+ assigned to this.render, so you can also just replace that function. It's usually unnecessary to call
+ #startRendering directly.
+
+ If no render function exists and none is supplied, it is simply delegated to this.world.render().
+
+ This WebGLContext is passed as an argument to the render_func.
+ */
+ startRendering: function(render_func) {
+ var self = this;
+ if (self.isRendering()) self.stopRendering();
+ self.render = render_func || self.render || function() { self.world.render(); };
+
+ function render() {
+ logger.attempt(self.canvas.id+":render", function() {
+ if (self.renderBlocking) return;
+ try {
+ self.useShader('color_without_texture');
+
+ //self.frame_count += 1;
+
+ self.gl.viewport(0, 0, self.gl.viewportWidth, self.gl.viewportHeight);
+ self.gl.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ perspective(45, self.gl.viewportWidth / self.gl.viewportHeight, 0.1, 200.0);
+ loadIdentity();
+
+ if (self.render) self.render(self);
+
+ self.checkError();
+ } catch(e) {
+ self.renderBlocking = true;
+ throw e;
+ }
+ });
+ self.renderInterval = setTimeout(render, WebGLContext.render_interval)
+ }
+
+ self.renderInterval = setTimeout(render, WebGLContext.render_interval);
+ },
+ stopRendering: function() {
+ if (this.isRendering()) clearInterval(this.renderInterval);
+ this.renderInterval = null;
+ },
+
+ /* Forces use of a particular Shader until further notice. Note that #pushShader is preferred over this. */
+ useShader: function(name) {
+ var context = this;
+ var gl = context.gl;
+ var shaders = context.shaders;
+
+ if (!name) throw new Error("No shader or shader name given!");
+ if (name.context && name.context != this) throw new Error("Tried to use a shader from a different context!");
+ if (name.getCompiledProgram) name = name.getCompiledProgram();
+ if ((name != this.activeShaderName && name != this.activeShader) || !this.activeShader)
+ {
+ this.checkError();
+ disableShaderAttributes(this);
+ this.checkError();
+ if (typeof(name) == "string")
+ {
+ if (shaders[name] == null) throw new Error("Shader named '"+name+"' was not found");
+ //if (shaders[name].isDisposed()) throw new Error("Shader named '"+name+"' has been disposed!");
+ this.activeShaderName = name;
+ this.activeShader = shaders[name];
+ if (this.activeShader.context)
+ gl.useProgram(this.activeShader.getCompiledProgram());
+ else
+ gl.useProgram(this.activeShader);
+ this.checkError();
+ }
+ else
+ {
+ this.activeShaderName = null;
+ this.activeShader = name;
+ gl.useProgram(name);
+ this.checkError();
+ }
+ }
+ return this.activeShader;
+ },
+
+
+ /* Temporarily sets the shader, and resets it when finished.
+
+ Ex:
+ pushShader(function() { useShader(aNewShader); do some stuff });
+ // activeShader == previous shader
+
+ pushShader(aNewShader, function() { do some stuff });
+ // activeShader == previous shader
+ */
+ pushShader: function(newShader, func)
+ {
+ // "push"
+ var oldShader = this.activeShader;
+ if (typeof(newShader) == "function") { func = newShader; }
+ else this.useShader(newShader);
+
+ // callback
+ func();
+
+ // "pop"
+ this.useShader(oldShader);
+ }
+ });
+}();
+
+/* Target number of milliseconds to wait before rendering the next frame. 1000 divided by this number equals frames per
+ second. */
+WebGLContext.render_interval = 15;
+
+/* internal use - used to set a unique value to context.id */
+WebGLContext.identifier = 0;
+
+(function() {
+ var i;
+ var canvas = document.createElement('canvas');
+ canvas.setAttribute('id', "temporary internal use");
+ canvas.style.display = "block";
+ var body = document.getElementsByTagName("body")[0], temporaryBody = false;
+ if (!body)
+ {
+ temporaryBody = true;
+ body = document.createElement('body');
+ document.getElementsByTagName("html")[0].appendChild(body);
+ }
+ document.getElementsByTagName("body")[0].appendChild(canvas);
+
+ var context = new WebGLContext(canvas);
+ context.stopRendering(); // we don't need or want to draw stuff
+
+ /* add methods to Context prototype to auto check for errors. */
+ for (var method_name in context.gl)
+ {
+ if (typeof(context.gl[method_name]) == "function")
+ {
+ var args = "";
+ for (var arg = 0; arg < context.gl[method_name].length; arg++)
+ {
+ if (arg > 0) args += ",";
+ args += "arg"+arg;
+ }
+
+ var func = "function() {"
+ + " var result;"
+ + " try { "
+ + " result = this.gl."+method_name+".apply(this.gl, arguments);"
+ + " this.checkError();"
+ + " } catch(e) { "
+ + " var args = [], i;"
+ + " for (i = 0; i < arguments.length; i++) args.push(arguments[i]);"
+ + " args = JSON.stringify(args);"
+ + " if (e.stack || (e = new Error(e.toString())).stack) {"
+ + " var stack_array = e.stack.split('\\n').reverse();" // reverse because logger shows newest messages first.
+ + " for (i = 0; i < stack_array.length; i++) logger.error(' '+stack_array[i]);"
+ + " }"
+ + " logger.error('WebGL FAILURE: in call to "+method_name+"<"+context.gl[method_name].length+"> with arguments '+args);"
+ + " throw e;"
+// + " logger.attempt('"+method_name+"', function() { throw e; });"
+ + " }"
+ + " return result;"
+ + "}";
+
+ WebGLContext.prototype[method_name] = eval("("+func+")");
+ }
+ else
+ /* define the GL enums globally so we don't need a context to reference them */
+ if (!/[a-z]/.test(method_name)) // no lowercase letters
+ window[('GL_'+method_name)] = context.gl[method_name];
+ }
+
+ // define some values that the iteration above probably didn't catch
+ window.GL_MAX_VERTEX_ATTRIBS = context.gl.getParameter(context.gl.MAX_VERTEX_ATTRIBS);
+ window.GL_DEPTH_COMPONENT = context.gl.DEPTH_COMPONENT || context.gl.DEPTH_COMPONENT16;
+ window.GL_TEXTURES = [];
+ for (i = 0; i < 32; i++) window.GL_TEXTURES[i] = context.gl["TEXTURE"+i];
+
+ if (temporaryBody)
+ $(body).remove();
+})();
View
272 public/javascripts/engine/core.js
@@ -0,0 +1,272 @@
+// note to self -- don't use 0, we might accidentally evaluate it as false / null
+var FILL = 1;
+var WIREFRAME = 2;
+var RENDER_PICK = 3;
+
+var after_initialize_callbacks = new Array();
+
+var mouse = {};
+
+function after_initialize(func) { after_initialize_callbacks.push(func); }
+
+// Date#strftime
+Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(x,pad,r){if(typeof (r)=="undefined"){r=10}for(;parseInt(x,10)<r&&r>1;r/=10){x=pad.toString()+x}return x.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(d){return Date.ext.locales[d.locale].a[d.getDay()]},A:function(d){return Date.ext.locales[d.locale].A[d.getDay()]},b:function(d){return Date.ext.locales[d.locale].b[d.getMonth()]},B:function(d){return Date.ext.locales[d.locale].B[d.getMonth()]},c:"toLocaleString",C:function(d){return Date.ext.util.xPad(parseInt(d.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(d){return Date.ext.util.xPad(parseInt(Date.ext.util.G(d)/100,10),0)},G:function(d){var y=d.getFullYear();var V=parseInt(Date.ext.formats.V(d),10);var W=parseInt(Date.ext.formats.W(d),10);if(W>V){y++}else{if(W===0&&V>=52){y--}}return y},H:["getHours","0"],I:function(d){var I=d.getHours()%12;return Date.ext.util.xPad(I===0?12:I,0)},j:function(d){var ms=d-new Date(""+d.getFullYear()+"/1/1 GMT");ms+=d.getTimezoneOffset()*60000;var doy=parseInt(ms/60000/60/24,10)+1;return Date.ext.util.xPad(doy,0,100)},m:function(d){return Date.ext.util.xPad(d.getMonth()+1,0)},M:["getMinutes","0"],p:function(d){return Date.ext.locales[d.locale].p[d.getHours()>=12?1:0]},P:function(d){return Date.ext.locales[d.locale].P[d.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(d){var dow=d.getDay();return dow===0?7:dow},U:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=6-d.getDay();var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0)},V:function(d){var woy=parseInt(Date.ext.formats.W(d),10);var dow1_1=(new Date(""+d.getFullYear()+"/1/1")).getDay();var idow=woy+(dow1_1>4||dow1_1<=1?0:1);if(idow==53&&(new Date(""+d.getFullYear()+"/12/31")).getDay()<4){idow=1}else{if(idow===0){idow=Date.ext.formats.V(new Date(""+(d.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(idow,0)},w:"getDay",W:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=7-Date.ext.formats.u(d);var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0,10)},y:function(d){return Date.ext.util.xPad(d.getFullYear()%100,0)},Y:"getFullYear",z:function(d){var o=d.getTimezoneOffset();var H=Date.ext.util.xPad(parseInt(Math.abs(o/60),10),0);var M=Date.ext.util.xPad(o%60,0);return(o>0?"-":"+")+H+M},Z:function(d){return d.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(d){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(fmt){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var d=this;while(fmt.match(/%[cDhnrRtTxXzZ]/)){fmt=fmt.replace(/%([cDhnrRtTxXzZ])/g,function(m0,m1){var f=Date.ext.aggregates[m1];return(f=="locale"?Date.ext.locales[d.locale][m1]:f)})}var str=fmt.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(m0,m1){var f=Date.ext.formats[m1];if(typeof (f)=="string"){return d[f]()}else{if(typeof (f)=="function"){return f.call(d,d)}else{if(typeof (f)=="object"&&typeof (f[0])=="string"){return Date.ext.util.xPad(d[f[0]](),f[1])}else{return m1}}}});d=null;return str};
+// String#capitalize
+String.prototype.capitalize = function() { return this.substring(0,1).toUpperCase()+this.substring(1,this.length); };
+// Math#pow2 - find the nearest power of 2 for given number
+Math.pow2 = function(k) {
+ if (k == 0)
+ return 1;
+ k--;
+ for (var i=1; i < 64; i<<=1)
+ k = k | k >> i;
+ return k+1;
+};
+
+
+function instanceFor(klass, attributes)
+{
+ klass.instances = klass.instances || [];
+ if (attributes)
+ klass.instances[attributes.id] = klass.instances[attributes.id] || new klass(attributes);
+ else
+ return null;
+
+ return klass.instances[attributes.id];
+}
+
+function Logger(name)
+{
+ var self = this;
+ var buffer = "";
+
+ self.name = name || "logger";
+ self.autoupdate = true;
+ self.level = Logger.INFO;
+
+ function update() {
+ if ($(self.name) && self.autoupdate)
+ {
+ var string = self.toString();
+ if (string.length > 65536) string = string.substring(0, 65536);
+ $(self.name).update(string);
+ }
+ }
+
+ function getLine() {
+ var e = new Error();
+ var line;
+ if (e.stack) line = e.stack.split("\n")[4];
+ else line = "";
+ line = line.substring(line.lastIndexOf("/")+1, line.length);
+ line = line.split(":");
+ while (line[0].length < 15) line[0] = " " + line[0];
+ while (line[1] && line[1].length < 4) line[1] = line[1] + " ";
+
+ return line.join(":");
+ }
+
+ function format(level, message) { return self.name+" " + new Date().strftime("%T") + " "+level+" "+getLine()+" "+message; }
+
+ function insert(level, message)
+ {
+ buffer = format(level, message).strip() + "\n" + buffer;
+ update();
+ }
+
+ self.toString = function() { return buffer; };
+ self.attempt = function(caption, func) {
+ self.attempt.captions.push("'"+caption+"'");
+ try {
+ func();
+ } catch(e) {
+ // avoid double logging the error in the event that one #attempt was nested within another
+ if (!e.attempted)
+ {
+ caption = self.attempt.captions.join(" =&gt; "); // 'outer' => 'inner'
+ var msg = e.toString();
+ if (e.stack) msg += "\n\n"+e.stack;
+ self.error("During "+caption+": "+msg);
+ }
+ e.attempted = true;
+ throw e;
+ } finally {
+ self.attempt.captions.pop(caption);
+ }
+ };
+
+ self.attempt.captions = [];
+
+
+ self.error= function(message) { if (self.level >= Logger.ERROR) insert("ERROR", message); };
+ self.warn = function(message) { if (self.level >= Logger.WARN ) insert("WARN ", message); };
+ self.info = function(message) { if (self.level >= Logger.INFO ) insert("INFO ", message); };
+ self.debug= function(message) { if (self.level >= Logger.DEBUG) insert("DEBUG", message); };
+}
+
+Logger.WARN = 1;
+Logger.ERROR = 2;
+Logger.INFO = 3;
+Logger.DEBUG = 4;
+
+var logger = new Logger();
+
+/*
+ takes a series of paths (1 or more). Multiple paths can be required and loaded asynchronously,
+ and the contents will be executed in order of appearance. Note that multiple calls to require()
+ do not do this, so a single, consolidated call is more time-efficient than multiple calls.
+
+ This method blocks until all files have been loaded, and throws an error if any file cannot
+ be loaded.
+ */
+function load(paths)
+{
+ if (typeof(paths) == "string") paths = [paths];
+
+ var retrieved = [];
+
+ // We need to force this function to block until all files have been loaded, but we want to
+ // load the files themselves asynchronously. To do that, we'll go down both routes: we'll load
+ // all files asynchronously, and while that's getting started we'll load them synchronously.
+ // The catch is that we won't load files that have already been retrieved: we'll skip the first
+ // one during async since we know we have to load it synchronously; after that, we'll check
+ // the results of async after each sync request completes.
+
+ var loadFile = function(xhr, index) {
+ logger.debug("assign : "+index+" => "+paths[index]+"\n");
+ if (xhr.status && xhr.status == 200) retrieved[index] = xhr.responseText;
+ else
+ retrieved[index] = 'throw new Error("Error: unexpected status "+'+
+ xhr.status+
+ '+" while loading file "+'+paths[index]+'); }';
+ };
+
+ var callback = function(response) {
+ logger.debug("async complete : "+paths[response.request.index]+"\n");
+ loadFile(response, response.request.index);
+ };
+
+ var options = {
+ onSuccess: callback,
+ onFailure: callback,
+ evalJS: false,
+ method: 'get'
+ };
+
+ var req;
+ for (var i = 1; i < paths.length; i++)
+ { // async requests
+ logger.debug("begin async "+paths[i]+" ("+i+")\n");
+ req = new Ajax.Request(paths[i], options);
+ req.index = i;
+ }
+
+ var xhr;
+ if (window.XMLHttpRequest) xhr = new XMLHttpRequest();
+ else xhr = new ActiveXObject("Microsoft.XMLHTTP");
+ for (var j = 0; j < paths.length; j++)
+ { // sync requests
+ logger.debug("begin sync "+paths[j]+" ("+j+") <ret: "+(typeof(retrieved[j]) == "undefined")+">\n");
+ if (typeof(retrieved[j]) == "undefined") // already retrieved
+ {
+ req = new Ajax.Request(paths[i], options);
+ req.index = i;
+ xhr.open("GET", paths[j], false);
+ xhr.send();
+ logger.debug("sync complete : "+paths[j]+"\n");
+ loadFile(xhr, j);
+ }
+ }
+
+ // finally: load the files!
+ for (var k = 0; k < paths.length; k++)
+ {
+ logger.info("Loading dependency: "+paths[k]);
+ // this condition shouldn't be possible, plus, it tests true if the returned file is empty.
+ // since an empty file has no eval result, it's fine to leave the condition in place -- but remove
+ // the else.
+ if (retrieved[k])
+ window.eval(retrieved[k]);
+ //else { throw new Error("Error: no data for file "+paths[k]); }
+ }
+}
+
+/*
+ takes a series of paths (1 or more). Multiple paths can be required and loaded asynchronously,
+ and the contents will be executed in order of appearance. Note that multiple calls to require()
+ do not do this, so a single, consolidated call is more time-efficient than multiple calls.
+
+ does not require the same file twice; if a repeat is detected, it will be omitted.
+
+ Prepends require.prefix to each file; this defaults to "/javascripts".
+*/
+function require(paths)
+{
+ if (typeof(paths) == "string") paths = [paths];
+ require.paths = require.paths || "|";
+ var fullpaths = [];
+
+ for (var i = 0; i < paths.length; i++)
+ {
+ var path = paths[i];
+
+ var fullpath = (require.prefix = require.prefix || "/javascripts");
+ if (fullpath.lastIndexOf("/") != fullpath.length - 1) fullpath = fullpath + "/";
+
+ if (path.indexOf("/") == 0) fullpath = fullpath + path.substring(1, path.length);
+ else fullpath = fullpath + path;
+
+ if (fullpath.indexOf(".js") != fullpath.length - 4) fullpath = fullpath + ".js";
+
+ if (require.paths.indexOf("|"+fullpath+"|") != -1) continue; // already loaded
+ require.paths = require.paths + fullpath+"|";
+ fullpaths.push(fullpath);
+ }
+
+ load(fullpaths);
+}
+
+function initializationComplete() {
+ logger.attempt("init callbacks", function() {
+ for (var i = 0; i < after_initialize_callbacks.length; i++)
+ after_initialize_callbacks[i](WebGLContext.mostRecent);
+ });
+}
+
+function encodeToColor(number) {
+ var g = (parseInt(number / 256));
+ var r = (number % 256);
+ // b and a are reserved
+
+ // r and g together allow for 65536 indices
+ // b is used as a key
+ // if this isn't enough (!), perhaps (b / 16) could be used to allow 16*65536 indices.
+ // a could cause problems with picking because it's done with readPixels() and so it can't be used here
+
+ return [r, g, 255, 255];
+}
+
+function decodeFromColor(r, g, b, a) {
+ if (typeof(g) == "undefined") { g = r[1]; b = r[2]; a = r[3]; r = r[0]; }
+ // b and a are reserved, see #encodeToColor
+
+ var number = (g * 256) + r;
+ return number;
+}
+
+
+try{
+ Float32Array;
+}catch(ex){
+ Float32Array = WebGLFloatArray;
+ Uint8Array = WebGLUnsignedByteArray;
+}
+
+// Dependencies
+//require(["engine/vector",
+// "engine/assertions",
+// "engine/camera",
+// "engine/shader",
+// 'engine/world',
+// "engine/lighting"
+// ]);
View
269 public/javascripts/engine/frustum.js
@@ -0,0 +1,269 @@
+var Frustum = (function() {
+// var INSIDE = 1, OUTSIDE = 2, INTERSECT = 3;
+
+ var RIGHT = 0, LEFT = 1, BOTTOM = 2, TOP = 3, FAR = 4, NEAR = 5;
+ var OUTSIDE = Plane.BACK, INTERSECT = Plane.INTERSECT, INSIDE = Plane.FRONT;
+
+ function extents(self)
+ {
+ /* TODO see how this can be combined with #unproject */
+ function extent(x, y, z)
+ {
+ var inf = [];
+ var mm = self.mv, pm = self.p;
+
+ //Calculation for inverting a matrix, compute projection x modelview; then compute the inverse
+ var m = pm.multiply(mm).inverse();
+
+ // Transformation of normalized coordinates between -1 and 1
+ inf[0]=x;//*2.0-1.0; /* x*2-1 translates x from 0..1 to -1..1 */
+ inf[1]=y;//*2.0-1.0;
+ inf[2]=z;//*2.0-1.0;
+ inf[3]=1.0;
+
+ //Objects coordinates
+ var out = m.multiply($V(inf)).elements;
+ if(out[3]==0.0)
+ return [0,0,0];//null;
+
+ out[3]=1.0/out[3];
+ return [out[0]*out[3], out[1]*out[3], out[2]*out[3]];
+ }
+
+ var ntl = extent(-1,1,-1), ntr = extent(1,1,-1), nbl =<