1 /**
  2  * Copyright (C) 2009-2012 Klaus Reimer <k@ailis.de>
  3  * See LICENSE.txt for licensing information
  4  * 
  5  * @require threedee.js
  6  * @require threedee/Color.js
  7  * @require threedee/Vector.js
  8  * @use threedee/rendering/FpsCounter.js
  9  * @use threedee/rendering/RenderOptions.js
 10  * @use threedee/rendering/TransformedLight.js
 11  * @use threedee/rendering/Frustum.js
 12  */
 13 
 14 /**
 15  * @constructor
 16  * Constructs a new polygon buffer.
 17  *            
 18  * @class
 19  * A polygon buffer
 20  */
 21 
 22 threedee.PolygonBuffer = function()
 23 {
 24     this.vertices = [];
 25     this.polygons = [];
 26     this.lights = [];
 27     this.fpsCounter = new threedee.FpsCounter();
 28     this.renderOptions = new threedee.RenderOptions();
 29 };
 30 
 31 /** 
 32  * Speed-optimization vector cache. 
 33  * @private 
 34  * @type {!threedee.Vector} 
 35  */
 36 threedee.PolygonBuffer.V = new threedee.Vector();
 37 
 38 /**
 39  * The render options.
 40  * @private
 41  * @type {!threedee.RenderOptions}
 42  */
 43 threedee.PolygonBuffer.prototype.renderOptions;
 44 
 45 /** 
 46  * The last gathered debug info. 
 47  * @private 
 48  * @type {string} 
 49  */
 50 threedee.PolygonBuffer.prototype.debugInfo = "";
 51 
 52 /** 
 53  * The vertices in the buffer. 
 54  * @private 
 55  * @type {!Array.<!threedee.Vector>} 
 56  */
 57 threedee.PolygonBuffer.prototype.vertices;
 58 
 59 /** 
 60  * The polygons in the buffer. 
 61  * @private 
 62  * @type {!Array.<!threedee.RenderPolygon>} 
 63  */
 64 threedee.PolygonBuffer.prototype.polygons;
 65 
 66 /** 
 67  * The registered light sources. 
 68  * @private 
 69  * @type {!Array.<!threedee.TransformedLight>} 
 70  */
 71 threedee.PolygonBuffer.prototype.lights;
 72 
 73 /** 
 74  * The screen width. 
 75  * @private 
 76  * @type {number} 
 77  */
 78 threedee.PolygonBuffer.prototype.width = 0;
 79 
 80 /** 
 81  * The screen height. 
 82  * @private 
 83  * @type {number} 
 84  */
 85 threedee.PolygonBuffer.prototype.height = 0;
 86 
 87 /** 
 88  * The factor used for scaling the perspective view transformation. 
 89  * @private 
 90  * @type {number} 
 91  */
 92 threedee.PolygonBuffer.prototype.factor = 1;
 93 
 94 /** 
 95  * The view frustum. 
 96  * @private 
 97  * @type {!threedee.Frustum} */
 98 threedee.PolygonBuffer.prototype.frustum;
 99 
100 /** 
101  * The FPS counter. 
102  * @private 
103  * @type {!threedee.FpsCounter} 
104  */
105 threedee.PolygonBuffer.prototype.fpsCounter;
106 
107 /** 
108  * The global ambient color. 
109  * @private 
110  * @type {!threedee.Color} 
111  */
112 threedee.PolygonBuffer.prototype.globalAmbient = threedee.Color.DARK_GRAY;
113 
114 /**
115  * Sets the global ambient color.
116  * 
117  * @param {!threedee.Color} globalAmbient
118  *            The global ambient color to set
119  */
120 threedee.PolygonBuffer.prototype.setGlobalAmbient = function(globalAmbient)
121 {
122     this.globalAmbient = globalAmbient;
123 };
124 
125 /**
126  * Sets the render options.
127  * 
128  * @param {!threedee.RenderOptions} renderOptions
129  *            The render options to set.
130  */
131 threedee.PolygonBuffer.prototype.setRenderOptions = function(renderOptions)
132 {
133     this.renderOptions = renderOptions;
134 };
135 
136 /**
137  * Prepares the polygon buffer for the next use.
138  * 
139  * @param {number} width
140  *            The output width in pixels.
141  * @param {number} height
142  *            The output height in pixels.
143  */
144 threedee.PolygonBuffer.prototype.prepare = function(width, height)
145 {
146     var eyeDistance, screenInMeters, screenInPixels, dpm;
147     
148     // Reset the buffer
149     this.vertices = [];
150     this.polygons = [];
151     this.lights = [];
152 
153     // Setup the view frustum (Repeated when output size changes)
154     if (width != this.width || height != this.height)
155     {
156         eyeDistance = 0.5; // Eye is 0.5 meters from screen
157         screenInMeters = 0.38;
158         screenInPixels = Math.min(width, height);
159         dpm = screenInPixels / screenInMeters;
160         this.factor = eyeDistance * dpm;
161     
162         // Generate the view frustum for clipping the polygons
163         this.frustum = new threedee.Frustum(width, height, this.factor);
164     
165         // Remember output size
166         this.width = width;
167         this.height = height;
168     }
169 };
170 
171 /**
172  * Adds the specified model to the polygon buffer.
173  * 
174  * @param {!threedee.RenderModel} model
175  *            The model to add
176  * @param {!threedee.Matrix} transform
177  *            The transformation matrix to use
178  */
179 threedee.PolygonBuffer.prototype.addModel = function(model, transform)
180 {
181     var backfaceCulling, i, max, polygon;
182     
183     // Pull render options into local variables
184     backfaceCulling = this.renderOptions.backfaceCulling;
185     
186     // Inform the model about the transformation if it is a LoD model
187     //if (model instanceof threedee.LoDModel)
188         //model.prepareLoD(transform, this.factor);
189 
190     // Transform the vertices of the model
191     model.transform(transform);
192     
193     // Create the transformed polygons and add them to the buffer
194     for (i = 0, max = model.countPolygons(); i < max; i++)
195     {
196         polygon = model.getPolygon(i);
197         
198         polygon.init();
199 
200         // Perform back-face culling
201         if (backfaceCulling && polygon.isBackface()) continue;
202 
203         // Clip the polygon with the frustum. If it was completely clipped
204         // away then ignore it
205         if (this.frustum.clipPolygon(polygon)) continue;
206 
207         // Calculate the average Z position of the polygon so it can be
208         // z-sorted later
209         polygon.updateAverageZ();
210         
211         // Add the polygon to the buffer
212         this.polygons.push(polygon);        
213     }
214 };
215 
216 /**
217  * Adds the specified light to the polygon buffer.
218  * 
219  * @param {!threedee.Light} light
220  *            The light to add
221  * @param {!threedee.Matrix} transform
222  *            The transformation matrix to use
223  */
224 threedee.PolygonBuffer.prototype.addLight = function(light, transform)
225 {
226     this.lights.push(new threedee.TransformedLight(light, transform));
227 };
228 
229 /**
230  * Renders the polygon buffer.
231  * 
232  * @param {!CanvasRenderingContext2D} g
233  *            The graphics context
234  */
235 threedee.PolygonBuffer.prototype.render = function(g)
236 {
237     var x, y, factor, i, max, vertexCount, polygon, v, vertex, dx, dy, dz,
238         displayNormals, lighting, solid, normal, center, normalEnd, cx, cy,
239         cx2, cy2, sortZ, debugInfo, fpsInfo, polygonCounter, vertexCounter;
240 
241     // Pull render options in local variables
242     displayNormals = this.renderOptions.displayNormals;
243     sortZ = this.renderOptions.sortZ;
244     lighting = this.renderOptions.lighting;
245     solid = this.renderOptions.solid;
246     debugInfo = this.renderOptions.debugInfo;
247     fpsInfo = this.renderOptions.fpsInfo;
248     
249     // Initialize counters if needed
250     polygonCounter = 0;
251     vertexCounter = 0;
252     
253     // Do Z-sorting
254     if (sortZ) this.polygons.sort(threedee.RenderPolygon.compare);
255 
256     // Remember the old transformation and then apply the screen center
257     // transformation
258     g.save();
259     g.translate(this.width / 2, this.height / 2);
260 
261     // Get scale factor
262     factor = this.factor;
263 
264     // Set the default stroke color
265     g.strokeStyle = "#fff";
266 
267     // Draw the polygons
268     for (i = 0, max = this.polygons.length; i < max; i++)
269     {
270         polygon = this.polygons[i];
271         
272         // Project the 3D vertices into 2D coordinates and draw the
273         // polygon
274         vertexCount = polygon.countVertices();
275         g.beginPath();
276         for (v = 0; v < vertexCount; v++)
277         {
278             vertex = polygon.getVertex(v);
279 
280             dx = vertex.x;
281             dy = vertex.y;
282             dz = vertex.z;
283     
284             x = dx * factor / dz;
285             y = -dy * factor / dz;
286 
287             if (v)
288                 g.lineTo(x, y);
289             else
290                 g.moveTo(x, y);
291         }
292         g.closePath();
293 
294         // Set fill or stroke color (Depends on if solid polygons are rendered)
295         if (solid)
296         {
297             if (lighting)
298                 this.applyPolygonColor(polygon, g);
299             else
300                 g.fillStyle = polygon.getMaterial().getDiffuse().toCSS();
301             g.fill();
302             if (this.renderOptions.outline)
303             {
304                 g.strokeStyle = this.renderOptions.outlineColor || g.fillStyle;
305                 g.stroke();
306             }
307         }
308         else
309         {
310             g.stroke();
311         }
312         
313         // Display normals if needed
314         if (displayNormals)
315         {
316             normal = polygon.getNormal();
317             g.save();
318             g.strokeStyle = threedee.Color.YELLOW.toCSS();
319             center = polygon.getCenter();
320             normalEnd = normal.add(center);
321             cx = center.x * factor / center.z;
322             cy = -center.y * factor  / center.z;
323             cx2 = normalEnd.x * factor  / normalEnd.z;
324             cy2 = -normalEnd.y * factor  / normalEnd.z;
325             g.beginPath();
326             g.moveTo(cx, cy);
327             g.lineTo(cx2, cy2);
328             g.stroke();
329             g.restore();
330         }       
331         
332         // Update counters if needed
333         if (debugInfo)
334         {
335             polygonCounter++;
336             vertexCounter += vertexCount; 
337         }
338     }
339     
340     // Gather debugging info if requested
341     if (fpsInfo || debugInfo)
342     {
343         if (fpsInfo)
344         {
345             this.fpsCounter.frame();
346             this.debugInfo = "Frames/s: " + this.fpsCounter.getFps();
347             if (debugInfo) this.debugInfo += "\n\n";
348         }
349         else this.debugInfo = "";
350         
351         if (debugInfo)
352         {
353             this.debugInfo += "Vertices: " + vertexCounter +
354                 "\nPolygons: " + polygonCounter +
355                 "\n\nNew objects for this frame:" +
356                 "\n  Vector: " + threedee.Vector.count() +
357                 "\n  Matrix: " + threedee.Matrix.count() +
358                 "\n  Color: " + threedee.Color.count() +
359                 "\n  Material: " + threedee.Material.count() +
360                 "\n  Polygon: " + threedee.Polygon.count();
361         }
362     }
363 
364     // Restore the original transformation
365     g.restore();
366 };
367 
368 /**
369  * Calculates the polygon color according to the material and the light
370  * sources and applies it to the specified graphics context.
371  * 
372  * @param {!threedee.RenderPolygon} polygon
373  *            The polygon
374  * @param {!CanvasRenderingContext2D} g       
375  *            The graphics context
376  */
377 threedee.PolygonBuffer.prototype.applyPolygonColor = function(polygon, g)
378 {
379     var material, position, normal, globalAmbient, ambient, diffuse, emissive,
380         result, addedDiffuse, i, j, max, transLight, light, lightColor,
381         lightPosition, L, diffuseLight;
382     
383     // Get the material from the polygon
384     material = polygon.getMaterial();
385 
386     // Get the position (The center) and the normal from the polygon
387     position = polygon.getCenter();
388     normal = polygon.getNormal();
389 
390     // Get the color components of the global ambient color
391     globalAmbient = this.globalAmbient.getComponents();
392 
393     // Get the ambient, diffuse and emissive color components of the
394     // material
395     ambient = material.getAmbient().getComponents();
396     diffuse = material.getDiffuse().getComponents();
397     emissive = material.getEmissive().getComponents();
398 
399     // We store the resulting color components here:
400     result = new Array(3);
401 
402     // We store the added diffuse color components here:
403     addedDiffuse = (/** @type {!Array.<number>} */ []);
404     addedDiffuse[0] = 0;
405     addedDiffuse[1] = 0;
406     addedDiffuse[2] = 0;
407 
408     // Now we cycle through all color components and calculate the values
409     for (i = 0; i < 3; i++)
410     {
411         // Calculate the ambient color value
412         ambient[i] = ambient[i] * globalAmbient[i];
413 
414         // Iterate over all registered light sources and process them
415         for (j = 0, max = this.lights.length; j < max; j++)
416         {
417             transLight = this.lights[j];
418             light = transLight.getLight();
419             if (light instanceof threedee.PointLight)
420             {
421                 lightColor = light.getColor().getComponent(i);
422 
423                 lightPosition = transLight.getPosition().copy(threedee.PolygonBuffer.V);
424                 L = lightPosition.sub(position).toUnit();
425                 diffuseLight = Math.max(normal.dot(L), 0);
426 
427                 addedDiffuse[i] += diffuse[i] * lightColor * diffuseLight;
428             }
429         }
430 
431         result[i] = Math.min(ambient[i] + emissive[i] + addedDiffuse[i], 1);
432     }
433 
434     // Apply color to graphics context
435     g.fillStyle = "rgb(" +
436         parseInt(result[0] * 255, 10) + "," +
437         parseInt(result[1] * 255, 10) + "," +
438         parseInt(result[2] * 255, 10) + ")";
439 };
440 
441 
442 /**
443  * Returns debugging info. The debugInfo flag in the rendering options must
444  * be set to true to get up-to-date debug info.
445  * 
446  * @return {string} The debug info
447  */
448 
449 threedee.PolygonBuffer.prototype.getDebugInfo = function()
450 {
451     return this.debugInfo;
452 };
453