jax 2.0.4 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ * 2.0.5 *
2
+
3
+ * Setting `shadowcaster` to `false` for a light source now has the expected result: it
4
+ disables all shadow mapping for that light, resulting in a significant performance boost.
5
+
6
+ * Updated gl-matrix to the latest version (v1.1), and updated Jax to take advantage of
7
+ it, resulting in a moderate performance boost.
8
+
9
+ * Light sources no longer generate shadow maps for, nor cast shadows upon, objects that
10
+ are beyond their range. This is based on the light's intensity (its ambient,
11
+ specular and diffuse color components), and its attenuation coefficients.
12
+
13
+ * Fixed `Jax.Camera#unproject` -- it had some stupid errors that kept it from working
14
+ at all. This method had also somehow crept into Jax without any tests; it is now
15
+ appropriately tested, of course.
16
+
17
+
18
+
1
19
  * 2.0.4 *
2
20
 
3
21
  * Jax was not differentiating between development mode and production mode, resulting
data/Rakefile CHANGED
@@ -164,9 +164,9 @@ namespace :doc do
164
164
  @hide_links_to_api_docs = true
165
165
  header = ERB.new(File.read(File.expand_path("guides/partials/_top_nav.html.erb", File.dirname(__FILE__)))).result(binding)
166
166
  PDoc.run({
167
- :source_files => (['lib/assets/javascripts/jax.js', 'vendor/assets/javascripts/glMatrix.js'] +
167
+ :source_files => (['lib/assets/javascripts/jax.js', 'vendor/assets/javascripts/gl-matrix-pdoc.js'] +
168
168
  # Dir['vendor/ejs/src/**/*.js'] +
169
- Dir['lib/assets/javascripts/jax/**/*.js']),
169
+ Dir['lib/assets/javascripts/jax/**/*.{js,js.erb}']),
170
170
  :destination => "doc",
171
171
  # :index_page => 'src/README.markdown',
172
172
  :syntax_highlighter => 'coderay',
@@ -8,10 +8,6 @@
8
8
  //= require_everything_matching "shaders"
9
9
  //= require_everything_matching "resources"
10
10
 
11
- <% if Rails.env == "production" %>
12
- Jax.environment = Jax.PRODUCTION;
13
- <% end %>
14
-
15
11
  for (var shaderName in Jax._shader_data) {
16
12
  if (!(Jax.shaders[shaderName] instanceof Jax.Shader)) {
17
13
  var descriptor = Jax._shader_data[shaderName];
@@ -1,5 +1,5 @@
1
1
  //= require "jax/vendor/ejs"
2
- //= require "jax/vendor/glMatrix"
2
+ //= require "jax/vendor/gl-matrix"
3
3
  //= require "jax/prototype/class"
4
4
  //= require "jax/core/helper"
5
5
  //= require "jax/core/math"
@@ -59,6 +59,10 @@ var Jax = {
59
59
  click_speed: 0.2,
60
60
  };
61
61
 
62
+ <% if Rails.env == "production" %>
63
+ Jax.environment = Jax.PRODUCTION;
64
+ <% end %>
65
+
62
66
  // note: the default_shader is used immediately after Jax.Material has been defined. So, most
63
67
  // likely the end user won't be able to customize it with the expected result. Materials created
64
68
  // *after* the default shader has been changed are fine, but materials already existing
@@ -35,6 +35,28 @@ vec3.toQuatRotation = function(first, second, dest) {
35
35
  }
36
36
  };
37
37
 
38
+
39
+ /**
40
+ * vec3.distance(first, second) -> Number
41
+ * - first (vec3): a vector
42
+ * - second (vec3): a vector
43
+ *
44
+ * Returns the scalar distance between the two vectors. This
45
+ * is equivalent to:
46
+ *
47
+ * vec3.length(vec3.subtract(first, second, tmp));
48
+ *
49
+ **/
50
+ vec3.distance = function(a, b) {
51
+ var x = a[0] - b[0];
52
+ var y = a[1] - b[1];
53
+ var z = a[2] - b[2];
54
+
55
+ var sum = x*x + y*y + z*z;
56
+ if (sum > 0) return Math.sqrt(sum);
57
+ return 0;
58
+ };
59
+
38
60
  /**
39
61
  * vec3.multiply(a, b[, dest]) -> vec3
40
62
  * - a (vec3): left operand
@@ -1,4 +1,4 @@
1
- //= require "../vendor/glMatrix"
1
+ //= require "../vendor/gl-matrix"
2
2
  //= require "../prototype/class"
3
3
 
4
4
  /**
@@ -109,6 +109,24 @@
109
109
  **/
110
110
  isShadowCaster: function() { return this.shadow_caster; },
111
111
 
112
+ /** alias of: Jax.Model#isShadowCaster
113
+ * Jax.Model#isShadowcaster() -> Boolean
114
+ *
115
+ * Returns true if this model casts shadows upon other models in the scene. Note that
116
+ * even if true, shadows will only be cast upon models which utilize +Jax.Material+s that support
117
+ * both the +Lighting+ and +ShadowMap+ effects.
118
+ *
119
+ **/
120
+ isShadowcaster: function() { return this.shadow_caster; },
121
+
122
+ /**
123
+ * Jax.Model#disableShadows() -> Boolean
124
+ *
125
+ * Disables shadows cast by this model. Returns whether or not shadows were previously enabled
126
+ * prior to this method call.
127
+ **/
128
+ disableShadows: function() { var was = this.shadow_caster; this.shadow_caster = false; return was; },
129
+
112
130
  /**
113
131
  * Jax.Model#render(context) -> undefined
114
132
  *
@@ -1,4 +1,4 @@
1
- //= require "glMatrix"
1
+ //= require "gl-matrix"
2
2
  //= require "jax/core/glMatrix_ext/mat3.js"
3
3
  //= require "jax/core/glMatrix_ext/mat4.js"
4
4
  //= require "jax/core/glMatrix_ext/quat4.js"
@@ -4,7 +4,7 @@ a new global called GL_METHODS. This will later be used for method delegation
4
4
  within Jax.Context.
5
5
  */
6
6
 
7
- //= require "jax/vendor/glMatrix"
7
+ //= require "jax/vendor/gl-matrix"
8
8
  //= require_self
9
9
  /*
10
10
  all files in the webgl/ subdirectory will have access to a temporary GL context,
@@ -47,7 +47,7 @@ Jax.Camera = (function() {
47
47
  }
48
48
 
49
49
  function tmpRotQuat(self) {
50
- return self._rotquat = self._rotquat || quat4.create();
50
+ return self._rotquat || (self._rotquat = quat4.create());
51
51
  }
52
52
 
53
53
  function storeVecBuf(self, buftype) {
@@ -94,10 +94,7 @@ Jax.Camera = (function() {
94
94
  self.stale = false;
95
95
 
96
96
  var pos = storeVecBuf(self, POSITION);
97
- quat4.toMat4(self.rotation, self.matrices.mv);
98
- mat4.translate(self.matrices.mv, vec3.negate(pos), self.matrices.mv);
99
- mat4.inverse(self.matrices.mv);
100
-
97
+ mat4.fromRotationTranslation(self.rotation, pos, self.matrices.mv);
101
98
  mat4.toInverseMat3(self.matrices.mv, self.matrices.n);
102
99
  mat3.transpose(self.matrices.n);
103
100
 
@@ -316,16 +313,6 @@ Jax.Camera = (function() {
316
313
  }
317
314
 
318
315
  return this.rotateWorld(amount, quat4.multiplyVec3(this.rotation, vec, this._tmp[0]));
319
-
320
- // var axis = storeVecBuf(this, RIGHT);
321
- // this.rotateWorld(amount, axis);
322
- //
323
- // var rotquat = quat4.fromAngleAxis(amount, vec, tmpRotQuat(this));
324
- // quat4.normalize(rotquat);
325
- // quat4.multiply(rotquat, this.rotation, this.rotation);
326
- //
327
- // this.fireEvent('updated');
328
- // return this;
329
316
  },
330
317
 
331
318
  /**
@@ -513,9 +500,9 @@ Jax.Camera = (function() {
513
500
  options = options || {};
514
501
  if (!options.width) throw new Error("Expected a screen width in Jax.Camera#perspective");
515
502
  if (!options.height)throw new Error("Expected a screen height in Jax.Camera#perspective");
516
- options.fov = options.fov || 45;
517
- options.near = options.near || 0.01;
518
- options.far = options.far || 200;
503
+ options.fov || (options.fov = 45);
504
+ options.near || (options.near = 0.01);
505
+ options.far || (options.far = 200);
519
506
 
520
507
  var aspect_ratio = options.width / options.height;
521
508
  mat4.perspective(options.fov, aspect_ratio, options.near, options.far, this.matrices.p);
@@ -587,35 +574,33 @@ Jax.Camera = (function() {
587
574
  // winz is either 0 (near plane), 1 (far plane) or somewhere in between.
588
575
  // if it's not given a value we'll produce coords for both.
589
576
  if (typeof(winz) == "number") {
590
- winx = parseFloat(winx);
591
- winy = parseFloat(winy);
592
- winz = parseFloat(winz);
593
-
594
- var inf = [];
577
+ var inf = vec4.create();
595
578
  var mm = this.getTransformationMatrix(), pm = this.matrices.p;
596
- var viewport = [0, 0, pm.width, pm.height];
579
+ var viewport = [0, 0, this.projection.width, this.projection.height];
597
580
 
598
581
  //Calculation for inverting a matrix, compute projection x modelview; then compute the inverse
599
- var m = mat4.set(mm, mat4.create());
600
-
601
- mat4.inverse(m, m); // WHY do I have to do this? --see Jax.Context#reloadMatrices
582
+ var m;
583
+ mat4.inverse(mm, m = mat4.create()); // WHY do I have to invert first?
602
584
  mat4.multiply(pm, m, m);
603
- mat4.inverse(m, m);
604
-
585
+ if (!mat4.inverse(m, m)) return null;
586
+
605
587
  // Transformation of normalized coordinates between -1 and 1
606
- inf[0]=(winx-viewport[0])/viewport[2]*2.0-1.0;
607
- inf[1]=(winy-viewport[1])/viewport[3]*2.0-1.0;
608
- inf[2]=2.0*winz-1.0;
609
- inf[3]=1.0;
610
-
588
+ inf[0] = (winx-viewport[0])/viewport[2]*2.0-1.0;
589
+ inf[1] = (winy-viewport[1])/viewport[3]*2.0-1.0;
590
+ inf[2] = 2.0*winz-1.0;
591
+ inf[3] = 1.0;
592
+
611
593
  //Objects coordinates
612
- var out = vec3.create();
594
+ var out = vec4.create();
613
595
  mat4.multiplyVec4(m, inf, out);
614
- if(out[3]==0.0)
615
- return null;
596
+ if (out[3] == 0.0) return null;
616
597
 
617
- out[3]=1.0/out[3];
618
- return [out[0]*out[3], out[1]*out[3], out[2]*out[3]];
598
+ var result = vec3.create();
599
+ out[3] = 1.0/out[3];
600
+ result[0] = out[0] * out[3];
601
+ result[1] = out[1] * out[3];
602
+ result[2] = out[2] * out[3];
603
+ return result;
619
604
  }
620
605
  else
621
606
  return [this.unproject(winx, winy, 0), this.unproject(winx, winy, 1)];
@@ -43,6 +43,10 @@ Jax.Scene.LightSource = (function() {
43
43
  spot_exponent: 0,
44
44
  shadowcaster: true
45
45
  });
46
+ // Jax.Model uses `shadow_caster`, but previous versions of LightSource used `shadowcaster`.
47
+ // TODO deprecate both `shadow_caster` and `shadowcaster` in favor of something like boolean `shadows`.
48
+ data.shadow_caster = data.shadowcaster;
49
+ delete data.shadowcaster;
46
50
 
47
51
  if (typeof(data.type) == "string") data.type = Jax[data.type] || data.type;
48
52
  data.color.ambient = Jax.Util.colorize(data.color.ambient);
@@ -56,12 +60,12 @@ Jax.Scene.LightSource = (function() {
56
60
  this.spotExponent = this.spot_exponent;
57
61
  delete this.spot_exponent;
58
62
 
59
- this.shadowMatrix = mat4.create();
60
-
61
- this.framebuffers = [new Jax.Framebuffer({width:1024,height:1024,depth:true,color:GL_RGBA}),
62
- new Jax.Framebuffer({width:1024,height:1024,depth:true,color:GL_RGBA})];
63
-
64
- setupProjection(this);
63
+ if (this.isShadowCaster()) {
64
+ this.shadowMatrix = mat4.create();
65
+ this.framebuffers = [new Jax.Framebuffer({width:1024,height:1024,depth:true,color:GL_RGBA}),
66
+ new Jax.Framebuffer({width:1024,height:1024,depth:true,color:GL_RGBA})];
67
+ setupProjection(this);
68
+ }
65
69
  },
66
70
 
67
71
  format: function() {
@@ -136,8 +140,6 @@ Jax.Scene.LightSource = (function() {
136
140
  /* FIXME remove the shadowmap event listener from object's camera matrix */
137
141
  },
138
142
 
139
- isShadowcaster: function() { return this.shadowcaster; },
140
-
141
143
  getDPShadowNear: function() { setupProjection(this); return this.camera.projection.near; },
142
144
 
143
145
  getDPShadowFar: function() { setupProjection(this); return this.camera.projection.far; },
@@ -146,35 +148,68 @@ Jax.Scene.LightSource = (function() {
146
148
 
147
149
  render: function(context, objects, options) {
148
150
  if (!this.valid) {
151
+ // calculate maximum distance, holding attenuation at 0.001, so we can
152
+ // skip shadowmapping of objects too far away
153
+ // Dmax = R(sqrt(Li/Ic) - 1)
154
+ // where R is light radius (always 1 in Jax)
155
+ // and Li is light intensity
156
+ // and Ic is lowest acceptable attenuation
157
+ this.max_distance = Math.sqrt(this.getIntensity() / 0.001) - 1;
158
+
149
159
  var real_pass = context.current_pass;
150
160
  /* shadowgen pass */
151
161
  context.current_pass = Jax.Scene.SHADOWMAP_PASS;
152
162
  // Look for errors while rendering the shadow map; if any occur, log them, but then
153
163
  // disable shadowmap generation so Jax can continue without shadow support.
154
- try {
155
- if (this.shadowcaster)
164
+ if (this.isShadowCaster())
165
+ try {
156
166
  this.updateShadowMap(context, this.boundingRadius, objects, options);
157
- this.valid = true;
158
- } catch(e) {
159
- if (Jax.getGlobal().console && Jax.getGlobal().console.log)
160
- Jax.getGlobal().console.log(e.toString());
161
- this.shadowcaster = false;
162
- }
167
+ } catch(e) {
168
+ if (Jax.getGlobal().console && Jax.getGlobal().console.log)
169
+ Jax.getGlobal().console.log(e.toString());
170
+ this.disableShadows();
171
+ }
172
+ this.valid = true;
163
173
  context.current_pass = real_pass;
164
174
  }
165
175
 
176
+ this.renderWithinRange(context, objects, options);
177
+ },
178
+
179
+ // Renders all objects within range of this light source, based on its color intensity
180
+ // and attenuation coefficients.
181
+ //
182
+ // Pass force=true in options to render all objects even if #isLit returns false. Even
183
+ // if force=true, the object will be skipped if it is outside of the light's range.
184
+ //
185
+ renderWithinRange: function(context, objects, options) {
166
186
  for (var j = 0; j < objects.length; j++) {
167
187
  options.model_index = j;
168
188
 
169
- /* TODO optimization: see if objects[j] is even affected by this light (based on attenuation) */
170
- if (objects[j].isLit()) // it could be unlit but still in array if it casts a shadow
171
- objects[j].render(context, options);
189
+ if (options.force || objects[j].isLit()) {
190
+ var dist = vec3.distance(this.camera.getPosition(), objects[j].camera.getPosition())
191
+ - objects[j].getBoundingSphereRadius();
192
+
193
+ if (dist < this.max_distance)
194
+ // it could be unlit but still in array if it casts a shadow
195
+ objects[j].render(context, options);
196
+ }
172
197
  }
173
198
  },
174
199
 
200
+ // returns a single float value representing the sum of the average intensities of each color channel
201
+ // of ambient, diffuse and specular components.
202
+ getIntensity: function() {
203
+ var c = this.color;
204
+ return (c.ambient[0] * c.ambient[3] + c.ambient[1] * c.ambient[3] + c.ambient[2] * c.ambient[3] +
205
+ c.diffuse[0] * c.diffuse[3] + c.diffuse[1] * c.diffuse[3] + c.diffuse[2] * c.diffuse[3] +
206
+ c.specular[0]* c.specular[3]+ c.specular[1]* c.specular[3]+ c.specular[2]* c.specular[3]) / 3.0;
207
+ },
208
+
175
209
  updateShadowMap: function(context, sceneBoundingRadius, objects, render_options) {
176
210
  // we can't afford to taint the original options
177
- render_options = Jax.Util.normalizeOptions(render_options, {});
211
+ render_options = Jax.Util.normalizeOptions(render_options, {force: true});
212
+ if (this.max_distance < sceneBoundingRadius) this.max_distance = sceneBoundingRadius;
178
213
 
179
214
  setupProjection(this);
180
215
 
@@ -217,9 +252,7 @@ Jax.Scene.LightSource = (function() {
217
252
  context.loadViewMatrix(self.camera.getTransformationMatrix());
218
253
  mat4.set(context.getInverseViewMatrix(), sm);
219
254
 
220
- for (var i = 0; i < objects.length; i++) {
221
- objects[i].render(context, render_options);
222
- }
255
+ self.renderWithinRange(context, objects, render_options);
223
256
  });
224
257
 
225
258
  render_options.direction = -1;
@@ -229,9 +262,7 @@ Jax.Scene.LightSource = (function() {
229
262
  self.framebuffers[1].viewport(context);
230
263
  context.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
231
264
  context.loadViewMatrix(self.camera.getTransformationMatrix());
232
- for (var i = 0; i < objects.length; i++) {
233
- objects[i].render(context, render_options);
234
- }
265
+ self.renderWithinRange(context, objects, render_options);
235
266
  });
236
267
 
237
268
  context.glDisable(GL_POLYGON_OFFSET_FILL);
@@ -264,9 +295,7 @@ Jax.Scene.LightSource = (function() {
264
295
  context.glDisable(GL_BLEND);
265
296
  context.glEnable(GL_POLYGON_OFFSET_FILL);
266
297
  context.glPolygonOffset(2.0, 2.0);
267
- for (var i = 0; i < objects.length; i++) {
268
- objects[i].render(context, render_options);
269
- }
298
+ self.renderWithinRange(context, objects, render_options);
270
299
  context.glDisable(GL_POLYGON_OFFSET_FILL);
271
300
  context.glEnable(GL_BLEND);
272
301
  context.glCullFace(GL_BACK);
@@ -2,7 +2,7 @@ module Jax
2
2
  module Version
3
3
  MAJOR = 2
4
4
  MINOR = 0
5
- PATCH = 4
5
+ PATCH = 5
6
6
  BUILD = nil
7
7
  STRING = BUILD ? [MAJOR, MINOR, PATCH, BUILD].join(".") : [MAJOR, MINOR, PATCH].join(".")
8
8
  end
@@ -177,6 +177,15 @@ describe("Jax.Util", function() {
177
177
 
178
178
  describe("normalizeOptions", function() {
179
179
  var normalized;
180
+
181
+ describe("with a false value that defaults to true", function() {
182
+ beforeEach(function() { normalized = Jax.Util.normalizeOptions({enabled:false}, {enabled:true}); });
183
+
184
+ it("should not have a truthy value", function() {
185
+ expect(normalized.enabled).toBeFalsy();
186
+ });
187
+ });
188
+
180
189
  describe("with a missing default array", function() {
181
190
  var arr = [1,2,3];
182
191
  beforeEach(function() { normalized = Jax.Util.normalizeOptions(null, {def:arr}); });
@@ -3,6 +3,31 @@ describe("Camera", function() {
3
3
 
4
4
  beforeEach(function() { camera = new Jax.Camera(); });
5
5
 
6
+ it("should unproject properly", function() {
7
+ camera.perspective({width:800,height:500,near:0.1,far:200});
8
+
9
+ camera.setPosition([38.375, 75, 44.25]);
10
+ // camera.setUpVector([0, 0.196116, -0.980580]);
11
+ // camera.setRightVector([1,0,0]);
12
+ camera.setViewVector([0, -0.980580, -0.196116]);
13
+
14
+ // sanity checks
15
+ expect(camera.getPosition()).toEqualVector([38.375, 75, 44.25]);
16
+ expect(camera.getUpVector()).toEqualVector([0, 0.196116, -0.980580]);
17
+ expect(camera.getRightVector()).toEqualVector([1,0,0]);
18
+ expect(camera.getViewVector()).toEqualVector([0, -0.980580, -0.196116]);
19
+
20
+
21
+ var nearest = [ 38.308727, 74.893821, 44.271007 ];
22
+ var farthest = [-94.170333, -137.355712, 86.261283];
23
+
24
+ // all that for this:
25
+ expect(camera.unproject(0, 0, 0)).toEqualVector(nearest);
26
+ expect(camera.unproject(0, 0, 1)).toEqualVector(farthest);
27
+ expect(camera.unproject(0, 0)[0]).toEqualVector(nearest);
28
+ expect(camera.unproject(0, 0)[1]).toEqualVector(farthest);
29
+ });
30
+
6
31
  it("movement with rotations", function() {
7
32
  camera.move(10);
8
33
  camera.yaw(Math.PI/2);