gpu_particles.js
  1 /*{# Copyright (c) 2013 Turbulenz Limited #}*/
  2 /*
  3 * @title: GPU ParticleSystem
  4 * @description:
  5 * This sample demonstrates the capabilities and usage of the GPU ParticleSystem.
  6 */
  7 /*{{ javascript("jslib/observer.js") }}*/
  8 /*{{ javascript("jslib/requesthandler.js") }}*/
  9 /*{{ javascript("jslib/utilities.js") }}*/
 10 /*{{ javascript("jslib/floor.js") }}*/
 11 /*{{ javascript("jslib/services/turbulenzservices.js") }}*/
 12 /*{{ javascript("jslib/services/turbulenzbridge.js") }}*/
 13 /*{{ javascript("jslib/services/gamesession.js") }}*/
 14 /*{{ javascript("jslib/services/mappingtable.js") }}*/
 15 /*{{ javascript("jslib/camera.js") }}*/
 16 /*{{ javascript("jslib/aabbtree.js") }}*/
 17 /*{{ javascript("jslib/texturemanager.js") }}*/
 18 /*{{ javascript("jslib/shadermanager.js") }}*/
 19 /*{{ javascript("jslib/effectmanager.js") }}*/
 20 /*{{ javascript("jslib/material.js") }}*/
 21 /*{{ javascript("jslib/scenenode.js") }}*/
 22 /*{{ javascript("jslib/scene.js") }}*/
 23 /*{{ javascript("jslib/scenedebugging.js") }}*/
 24 /*{{ javascript("jslib/renderingcommon.js") }}*/
 25 /*{{ javascript("jslib/forwardrendering.js") }}*/
 26 /*{{ javascript("jslib/particlesystem.js") }}*/
 27 /*{{ javascript("jslib/fontmanager.js") }}*/
 28 /*{{ javascript("jslib/canvas.js") }}*/
 29 /*{{ javascript("scripts/htmlcontrols.js") }}*/
 30 /*global CameraController: false */
 31 /*global Camera: false */
 32 /*global Canvas: false */
 33 /*global EffectManager: false */
 34 /*global Floor: false */
 35 /*global FontManager: false */
 36 /*global ForwardRendering: false */
 37 /*global HTMLControls: false */
 38 /*global ParticleBuilder: false */
 39 /*global ParticleSystem: false */
 40 /*global ParticleView: false */
 41 /*global RequestHandler: false*/
 42 /*global Scene: false */
 43 /*global SceneNode: false */
 44 /*global ShaderManager: false */
 45 /*global TextureManager: false */
 46 /*global TurbulenzEngine: true */
 47 /*global TurbulenzServices: false */
 48 /*global window: false */
 49 TurbulenzEngine.onload = function onloadFn() {
 50     var errorCallback = function errorCallback(msg) {
 51         window.alert(msg);
 52     };
 53 
 54     //==========================================================================
 55     // Turbulenz Initialization
 56     //==========================================================================
 57     var graphicsDevice = TurbulenzEngine.createGraphicsDevice({});
 58     if (graphicsDevice.maxSupported("VERTEX_TEXTURE_UNITS") === 0) {
 59         errorCallback("Device does not support sampling of textures from vertex shaders " + "required by GPU particle system");
 60         return;
 61     }
 62 
 63     var mathDevice = TurbulenzEngine.createMathDevice({});
 64     var inputDevice = TurbulenzEngine.createInputDevice({});
 65 
 66     var requestHandler = RequestHandler.create({});
 67     var textureManager = TextureManager.create(graphicsDevice, requestHandler, null, errorCallback);
 68     var shaderManager = ShaderManager.create(graphicsDevice, requestHandler, null, errorCallback);
 69     var effectManager = EffectManager.create();
 70     var fontManager = FontManager.create(graphicsDevice, requestHandler, null, errorCallback);
 71 
 72     // region of world where systems will be spawned.
 73     var sceneWidth = 1000;
 74     var sceneHeight = 1000;
 75 
 76     // speed of generation.
 77     var generationSpeed = 50;
 78     var lastGen = 0;
 79 
 80     // speed of simulation (log 1.3)
 81     var simulationSpeed = 0;
 82 
 83     var camera = Camera.create(mathDevice);
 84     var halfFOV = Math.tan(30 * Math.PI / 180);
 85     camera.recipViewWindowX = 1 / halfFOV;
 86     camera.recipViewWindowY = 1 / halfFOV;
 87     camera.lookAt(mathDevice.v3Build(0, 0, 0), mathDevice.v3BuildYAxis(), mathDevice.v3Build(sceneWidth / 2, 30, sceneHeight / 2));
 88     camera.updateProjectionMatrix();
 89     camera.updateViewMatrix();
 90     var cameraController = CameraController.create(graphicsDevice, inputDevice, camera);
 91     var maxCameraSpeed = 200;
 92 
 93     var renderer;
 94     var clearColor = mathDevice.v4Build(0, 0, 0, 1);
 95     var scene = Scene.create(mathDevice);
 96 
 97     var floor = Floor.create(graphicsDevice, mathDevice);
 98     floor.color = mathDevice.v4Build(0, 0, 0.6, 1);
 99     floor.fadeToColor = clearColor;
100 
101     var drawRenderableExtents = false;
102     function extraDrawCallback() {
103         floor.render(graphicsDevice, camera);
104         if (drawRenderableExtents) {
105             (scene).drawVisibleRenderablesExtents(graphicsDevice, shaderManager, camera, false, true);
106         }
107     }
108 
109     // Create canvas object for minimap.
110     var canvas = Canvas.create(graphicsDevice);
111     var ctx = canvas.getContext('2d');
112 
113     // Scaling to use when drawing to minimap, targetting a size of (150,150) for minimap
114     var scaleX = 150 / sceneWidth;
115     var scaleY = 150 / sceneHeight;
116     ctx.lineWidth = 0.1;
117 
118     var fontTechnique;
119     var fontTechniqueParameters;
120 
121     var fpsElement = document.getElementById("fps");
122     var fpsText = "";
123     function displayFPS() {
124         if (!fpsElement) {
125             return;
126         }
127 
128         var text = graphicsDevice.fps.toFixed(2) + " fps";
129         if (text !== fpsText) {
130             fpsText = text;
131             fpsElement.innerHTML = fpsText;
132         }
133     }
134 
135     //==========================================================================
136     // Particle Systems
137     //==========================================================================
138     var particleManager = ParticleManager.create(graphicsDevice, textureManager, shaderManager);
139 
140     particleManager.registerParticleAnimation({
141         name: "fire",
142         // Define a texture-size to normalize uv-coordinates with.
143         // This avoids needing to use fractional values, especially if texture
144         // may be changed in future.
145         //
146         // In this case the actual texture is 512x512, but we map the particle animation
147         // to the top-half, so can pretend it is really 512x256.
148         //
149         // To simplify the uv-coordinates further, we can 'pretend' it is really 4x2 as
150         // after normalization the resulting uv-coordinates would be equivalent.
151         "texture0-size": [4, 2],
152         texture0: [
153             [0, 0, 1, 1],
154             [1, 0, 1, 1],
155             [2, 0, 1, 1],
156             [3, 0, 1, 1],
157             [0, 1, 1, 1],
158             [1, 1, 1, 1],
159             [2, 1, 1, 1],
160             [3, 1, 1, 1]
161         ],
162         animation: [
163             {
164                 frame: 0
165             },
166             {
167                 // after 0.6 seconds, ensure colour is still [1,1,1,1]
168                 time: 0.6,
169                 color: [1, 1, 1, 1]
170             },
171             {
172                 // after another 0.1 seconds
173                 time: 0.1,
174                 // want to be 'just past' the last frame.
175                 // so all frames of animation have equal screen presence.
176                 frame: 8,
177                 color: [1, 1, 1, 0]
178             }
179         ]
180     });
181 
182     particleManager.registerParticleAnimation({
183         name: "smoke",
184         // smoke is similarly mapped as "fire" particle above, but to bottom of packed texture.
185         "texture0-size": [4, 2],
186         texture0: [
187             [0, 0, 1, 1],
188             [1, 0, 1, 1],
189             [2, 0, 1, 1],
190             [3, 0, 1, 1],
191             [0, 1, 1, 1],
192             [1, 1, 1, 1],
193             [2, 1, 1, 1],
194             [3, 1, 1, 1]
195         ],
196         animation: [
197             {
198                 // these are values applied by default to the first snapshot in animation
199                 // we could omit them here if we wished.
200                 frame: 0,
201                 "frame-interpolation": "linear",
202                 color: [1, 1, 1, 1],
203                 "color-interpolation": "linear"
204             },
205             {
206                 // after 0.8 seconds
207                 time: 0.8,
208                 color: [1, 0.5, 0.5, 1]
209             },
210             {
211                 // after another 0.5 seconds, we fade out.
212                 time: 0.5,
213                 // want to be 'just past' the last frame.
214                 // so all frames of animation have equal screen presence.
215                 frame: 8,
216                 color: [0, 0, 0, 0]
217             }
218         ]
219     });
220 
221     particleManager.registerParticleAnimation({
222         name: "portal",
223         animation: [
224             {
225                 "scale-interpolation": "catmull",
226                 color: [0, 1, 0, 1]
227             },
228             {
229                 // after 0.3 seconds
230                 time: 0.3,
231                 scale: [2, 2],
232                 color: [1, 1, 1, 1]
233             },
234             {
235                 // after another 0.7 seconds
236                 time: 0.7,
237                 scale: [0.5, 0.5],
238                 color: [1, 0, 0, 0]
239             }
240         ]
241     });
242 
243     var description1 = {
244         system: {
245             // define local system extents, particles will be clamped against these extents when reached.
246             //
247             // We make extents a little larger than necessary so that in movement of system
248             // particles will not push up against the edges of extents so easily.
249             center: [0, 6, 0],
250             halfExtents: [7, 6, 7]
251         },
252         updater: {
253             // set noise texture to use for randomization, and allow acceleration (when enabled)
254             // to be randomized to up to the given amounts.
255             noiseTexture: "textures/noise.dds",
256             randomizedAcceleration: [10, 10, 10]
257         },
258         renderer: {
259             // use default renderer with additive blend mode
260             name: "additive",
261             // set noise texture to use for randomizations.
262             noiseTexture: "textures/noise.dds",
263             // for particles that enable these options, we're going to allow particle alphas
264             // if enabled on particles, allow particle orientation to be randomized up to these
265             // spherical amounts (+/-), in this case, to rotate around y-axis by +/- 0.3*Math.PI
266             // specify this variation should change over time
267             randomizedOrientation: [0, 0.3 * Math.PI],
268             animatedOrientation: true,
269             // if enabled on particles, allow particle scale to be randomized up to these
270             // amounts (+/-), and define that this variation should not change over time.
271             randomizedScale: [3, 3],
272             animatedScale: false
273         },
274         // All particles make use of this single texture.
275         packedTexture: "textures/flamesmokesequence.png",
276         particles: {
277             fire: {
278                 animation: "fire",
279                 // select sub-set of packed texture this particles animation should be mapped to.
280                 "texture-uv": [0, 0, 1, 0.5],
281                 // apply animation tweaks to increase size of animation (x5)
282                 tweaks: {
283                     "scale-scale": [5, 5]
284                 }
285             },
286             ember: {
287                 animation: "fire",
288                 "texture-uv": [0, 0.0, 1, 0.5],
289                 // apply animation tweaks so that only the second half of flip-book is used.
290                 // and double the size.
291                 tweaks: {
292                     "scale-scale": [2, 2],
293                     // The animation we're using has 8 frames, we want to use the second
294                     // half of the flip-book animation, so we scale by 0.5 and offset by 4.
295                     "frame-scale": 0.5,
296                     "frame-offset": 4
297                 }
298             },
299             smoke: {
300                 animation: "smoke",
301                 // select sub-set of packed texture this particles animation should be mapped to.
302                 "texture-uv": [0, 0.5, 1, 0.5],
303                 // apply animation tweaks to increase size of animation (x3)
304                 tweaks: {
305                     "scale-scale": [3, 3]
306                 }
307             }
308         },
309         emitters: [
310             {
311                 particle: {
312                     name: "fire",
313                     // let life time of particle vary between 0.6 and 1.2 of animation life time.
314                     lifeTimeScaleMin: 0.6,
315                     lifeTimeScaleMax: 1.2,
316                     // set userData so that its orientation will be randomized, and will have a
317                     // also define scale should be randomized.
318                     renderUserData: {
319                         facing: "billboard",
320                         randomizeOrientation: true,
321                         randomizeScale: true
322                     }
323                 },
324                 emittance: {
325                     // emit particles 10 times per second. With 0 - 2 particles emitted each time.
326                     rate: 10,
327                     burstMin: 0,
328                     burstMax: 2
329                 },
330                 position: {
331                     // position 2 units above system position
332                     position: [0, 2, 0],
333                     // and with a randomized radius in disc of up to 1 unit
334                     // with a normal (gaussian) distribution to focus on centre.
335                     radiusMax: 1,
336                     radiusDistribution: "normal"
337                 },
338                 velocity: {
339                     // spherical angles defining direction to emit particles in.
340                     // the default 0, 0 means to emit particles straight up the y-axis.
341                     theta: 0,
342                     phi: 0
343                 }
344             },
345             {
346                 particle: {
347                     name: "ember",
348                     // override animation life times.
349                     lifeTimeMin: 0.2,
350                     lifeTimeMax: 0.6,
351                     // set userData so that acceleration will be randomized and also orientation.
352                     updateUserData: {
353                         randomizeAcceleration: true
354                     },
355                     renderUserData: {
356                         randomizeOrientation: true
357                     }
358                 },
359                 emittance: {
360                     // emit particles 3 times per second. With 0 - 15 particles emitted each time.
361                     rate: 3,
362                     burstMin: 0,
363                     burstMax: 15,
364                     // only start emitting after 0.25 seconds
365                     delay: 0.25
366                 },
367                 velocity: {
368                     // set velocity to a random direction in conical spread
369                     conicalSpread: Math.PI * 0.25,
370                     // and with speeds between these values.
371                     speedMin: 1,
372                     speedMax: 3
373                 },
374                 position: {
375                     // position 3 units above system position
376                     position: [0, 3, 0],
377                     // and in a random radius of this position in a sphere.
378                     spherical: true,
379                     radiusMin: 1,
380                     radiusMax: 2.5
381                 }
382             },
383             {
384                 particle: {
385                     name: "smoke",
386                     // set userData so that acceleration will be randomized.
387                     updateUserData: {
388                         randomizeAcceleration: true
389                     }
390                 },
391                 emittance: {
392                     // emit particles 20 times per second, with 0 - 3 every time.
393                     rate: 20,
394                     burstMin: 0,
395                     burstMax: 3
396                 },
397                 velocity: {
398                     // set velocity to a random direction in conical spread
399                     conicalSpread: Math.PI * 0.25,
400                     // and with speeds between these values.
401                     speedMin: 2,
402                     speedMax: 6
403                 },
404                 position: {
405                     // position 2.5 units above system position
406                     position: [0, 2.5, 0],
407                     // and in a random radius of this position in a sphere.
408                     spherical: true,
409                     radiusMin: 0.5,
410                     radiusMax: 2.0
411                 }
412             }
413         ]
414     };
415 
416     var description2 = {
417         system: {
418             // define local system extents
419             // as with first system these are defined to be a bit larger to account for
420             // movements of the system.
421             center: [0, 6, 0],
422             halfExtents: [12, 6, 12]
423         },
424         renderer: {
425             // we're going to use the default renderer with the "additive" blend mode.
426             name: "additive",
427             // set noise texture to use for randomizations
428             noiseTexture: "textures/noise.dds",
429             // for particles that enable these options, we're going to allow particle alphas
430             //    to vary +/- 0.5, and this alpha variation will change over time.
431             randomizedAlpha: 1.0,
432             animatedAlpha: true,
433             // for particles that enable these options, we're going to allow particle orientations
434             //    to vary by the given spherical angles (+/-), and this variation will change over time.
435             randomizedOrientation: [Math.PI * 0.25, Math.PI * 0.25],
436             animatedOrientation: true,
437             // for particles that enable these options, we're going to allow particle rotations
438             //    to vary by the given angle (+/-), and this variation will change over time.
439             randomizedRotation: Math.PI * 2,
440             animatedRotation: true
441         },
442         updater: {
443             // In the absense of acceleration, set drag so that particles will come to a stop after
444             //    1 second of simulation.
445             drag: 1,
446             // for particles that enable these options, we're going to allow acceleration applied to
447             //    particles to vary according to the noise texture, up to a defined maximum in each
448             //    coordinate (+/-)
449             noiseTexture: "textures/noise.dds",
450             randomizedAcceleration: [10, 0, 10]
451         },
452         particles: {
453             // Define two particles to be used in this system.
454             // As these define their own textures, textures will be packed at runtime by the particleManager.
455             spark: {
456                 animation: "portal",
457                 // define animation tweaks to be applied for this particle.
458                 tweaks: {
459                     // this defines that we're going to half the animated scale of the particle.
460                     //   In effect, we're making this particle half the size the animation said it should be.
461                     "scale-scale": [0.5, 0.5]
462                 },
463                 texture: "textures/particle_spark.png"
464             },
465             smoke: {
466                 animation: "portal",
467                 tweaks: {
468                     // The effect of these parameters will be to invert the RGB colours of the particle as
469                     //   defined by the animation, and particle texture.
470                     "color-scale": [-1, -1, -1, 1],
471                     "color-offset": [1, 1, 1, 0]
472                 },
473                 texture: "textures/smoke.dds"
474             }
475         },
476         emitters: [
477             {
478                 emittance: {
479                     // After 1 second from the start of the effect, we're going to emit particles 80 times per second.
480                     delay: 1,
481                     rate: 80,
482                     // Whenever we emit particles, we will emit exactly 4 particles.
483                     burstMin: 4,
484                     burstMax: 4
485                 },
486                 particle: {
487                     name: "spark",
488                     // Here we access functions of the updater and renderer that will be used, to set the userData
489                     //    that will be applied to each particle emitted.
490                     // We define that we want particles emitted by this emitter to have their acceleration randomize
491                     //    and also their alpha, orientation and rotation. We specify particle quad should be aligned
492                     //    with the particles velocity vector.
493                     updateUserData: {
494                         randomizeAcceleration: true
495                     },
496                     renderUserData: {
497                         facing: "velocity",
498                         randomizeAlpha: true,
499                         randomizeOrientation: true,
500                         randomizeRotation: true
501                     }
502                 },
503                 velocity: {
504                     // Particles will be emitted with local speeds between these values.
505                     speedMin: 3,
506                     speedMax: 20,
507                     // And with a conical spread of the given angle about the default direction (y-axis).
508                     conicalSpread: Math.PI / 10
509                 },
510                 position: {
511                     // Particles will be generated at radii between these values.
512                     radiusMin: 4,
513                     radiusMax: 5,
514                     // And the distribution of the radius selected will be according to a normal (Gaussian) distribution
515                     //   with the given sigma parameter.
516                     radiusDistribution: "normal",
517                     radiusSigma: 0.125
518                 }
519             },
520             {
521                 emittance: {
522                     // We will emit particles 20 times per second.
523                     rate: 20,
524                     // And whenever we emit particles, we'll emit between 0 and 6 particles.
525                     burstMin: 0,
526                     burstMax: 6
527                 },
528                 particle: {
529                     name: "smoke",
530                     // Particles of this emitter will have their quads billboarded to face camera.
531                     renderUserData: {
532                         facing: "billboard"
533                     },
534                     // Particles will live for between these amounts of time in seconds.
535                     useAnimationLifeTime: false,
536                     lifeTimeMin: 0.5,
537                     lifeTimeMax: 1.5
538                 },
539                 velocity: {
540                     speedMin: 5,
541                     speedMax: 15
542                 },
543                 position: {
544                     spherical: false,
545                     radiusMin: 0,
546                     radiusMax: 2
547                 }
548             }
549         ]
550     };
551 
552     // Produce ParticleArchetype objects based on these descriptions.
553     //   These calls will verify the input descriptions for correctness, and fill in all missing parameters
554     //   with the default values defined by the individual components of a particle system.
555     var archetype1 = particleManager.parseArchetype(description1);
556     var archetype2 = particleManager.parseArchetype(description2);
557 
558     //==========================================================================
559     // Main loop
560     //=========================================================================
561     var previousFrameTime;
562     function init() {
563         fontTechnique = shaderManager.get("shaders/font.cgfx").getTechnique('font');
564         fontTechniqueParameters = graphicsDevice.createTechniqueParameters({
565             clipSpace: mathDevice.v4BuildZero(),
566             alphaRef: 0.01,
567             color: mathDevice.v4BuildOne()
568         });
569 
570         renderer = ForwardRendering.create(graphicsDevice, mathDevice, shaderManager, effectManager, {});
571 
572         // particleManager is initialized with the Scene to be worked with.
573         // and the transparent pass index of the renderer, so that particle systems
574         // created will be sorted with other transparent renderable elements of the Scene.
575         particleManager.initialize(scene, renderer.passIndex.transparent);
576 
577         previousFrameTime = TurbulenzEngine.time;
578     }
579 
580     // All systems are added as children of this node so we can shuffle them around
581     // in space, demonstrating trails.
582     var particleNode = SceneNode.create({
583         name: "particleNode",
584         dynamic: true
585     });
586     scene.addRootNode(particleNode);
587 
588     var moveSystems = false;
589     var movementTime = 0;
590 
591     // movement radius of particleNode.
592     var radius = 50;
593 
594     function mainLoop() {
595         var currentTime = TurbulenzEngine.time;
596         var deltaTime = (currentTime - previousFrameTime);
597         previousFrameTime = currentTime;
598         displayFPS();
599 
600         inputDevice.update();
601 
602         cameraController.maxSpeed = (deltaTime * maxCameraSpeed);
603         cameraController.update();
604 
605         // Update the aspect ratio of the camera in case of window resizes
606         var aspectRatio = (graphicsDevice.width / graphicsDevice.height);
607         if (aspectRatio !== camera.aspectRatio) {
608             camera.aspectRatio = aspectRatio;
609             camera.updateProjectionMatrix();
610         }
611         camera.updateViewProjectionMatrix();
612 
613         // alter deltaTime for simulation speed after camera maxSpeed was set to avoid
614         // slowing down the camera movement.
615         deltaTime *= Math.pow(1.3, simulationSpeed);
616 
617         // Update ParticleManager object with elapsed time.
618         // This will add the deltaTime to the managers internal clock used by systems when synchronizing
619         // and will also remove any expired ParticleInstance objects created in the particleManager.
620         particleManager.update(deltaTime);
621 
622         // Create new ParticleInstances in particleManager.
623         lastGen += deltaTime;
624         var limit = 0;
625         while (lastGen > 1 / generationSpeed && limit < 100) {
626             limit += 1;
627             lastGen -= 1 / generationSpeed;
628 
629             var instance, x, z, s, timeout;
630             timeout = 2 + 2 * Math.random();
631             instance = particleManager.createInstance(archetype1, timeout);
632             x = Math.random() * (sceneWidth - radius * 2) + radius;
633             z = Math.random() * (sceneHeight - radius * 2) + radius;
634             s = 1 + Math.random() * 2;
635 
636             // this local transform will be applied to the entire system
637             // allowing us to re-use the same particle archetype around the
638             // scene at different positions and scales.
639             instance.renderable.setLocalTransform(mathDevice.m43Build(s, 0, 0, 0, s, 0, 0, 0, s, x, 0, z));
640             particleManager.addInstanceToScene(instance, particleNode);
641 
642             timeout = 2 + 2 * Math.random();
643             instance = particleManager.createInstance(archetype2, timeout);
644             x = Math.random() * (sceneWidth - radius * 2) + radius;
645             z = Math.random() * (sceneHeight - radius * 2) + radius;
646             s = 1 + Math.random() * 2;
647             instance.renderable.setLocalTransform(mathDevice.m43Build(s, 0, 0, 0, s, 0, 0, 0, s, x, 0, z));
648             particleManager.addInstanceToScene(instance, particleNode);
649         }
650         lastGen %= (1 / generationSpeed);
651 
652         if (moveSystems) {
653             movementTime += deltaTime;
654             var time = movementTime / 5;
655             var rad = radius * Math.sin(time);
656             var transform = mathDevice.m43BuildTranslation(Math.sin(time) * rad, 0, Math.cos(time) * rad);
657             particleNode.setLocalTransform(transform);
658         }
659 
660         // Update scene
661         scene.update();
662 
663         if (!graphicsDevice.beginFrame()) {
664             return;
665         }
666 
667         // Update renderer, this will as a side-effect of particle instances becoming visible to the camera
668         //   cause particle systems if required to be lazily created along with any views onto a particle system
669         //   the low-level particle system will deal with this itself the way it is used by the particleManager.
670         renderer.update(graphicsDevice, camera, scene, currentTime);
671 
672         // Render scene including all particle systems.
673         renderer.draw(graphicsDevice, clearColor, extraDrawCallback);
674 
675         // Gather metrics about object usages in the particleManager, and display on the screen.
676         graphicsDevice.setTechnique(fontTechnique);
677         mathDevice.v4Build(2 / graphicsDevice.width, -2 / graphicsDevice.height, -1, 1, fontTechniqueParameters.clipSpace);
678         graphicsDevice.setTechniqueParameters(fontTechniqueParameters);
679 
680         var metrics = particleManager.gatherMetrics();
681         var text = "ParticleManager Metrics:\n";
682         for (var f in metrics) {
683             if (metrics.hasOwnProperty(f)) {
684                 text += f + ": " + metrics[f] + "\n";
685             }
686         }
687 
688         var font = fontManager.get("fonts/hero.fnt");
689         var fontScale = 0.5;
690         var dimensions = font.calculateTextDimensions(text, fontScale, 0);
691         font.drawTextRect(text, {
692             rect: mathDevice.v4Build(0, 0, dimensions.width, dimensions.height),
693             scale: fontScale,
694             alignment: 0
695         });
696 
697         if (canvas.width !== graphicsDevice.width) {
698             canvas.width = graphicsDevice.width;
699         }
700         if (canvas.height !== graphicsDevice.height) {
701             canvas.height = graphicsDevice.height;
702         }
703 
704         var width = sceneWidth * scaleX;
705         var height = sceneHeight * scaleY;
706         var viewport = mathDevice.v4Build(canvas.width - width - 2, canvas.height - 2, width, height);
707         viewport = null;
708         ctx.beginFrame(null, viewport);
709         ctx.setTransform(1, 0, 0, 1, 0, 0);
710         ctx.translate(canvas.width - sceneWidth * scaleX - 2, 2);
711 
712         ctx.strokeStyle = "#ffffff";
713         ctx.strokeRect(0, 0, sceneWidth * scaleX, sceneHeight * scaleY);
714 
715         var instanceMetrics = particleManager.gatherInstanceMetrics();
716         var count = instanceMetrics.length;
717         var i;
718         for (i = 0; i < count; i += 1) {
719             var metric = instanceMetrics[i];
720             var extents = metric.instance.renderable.getWorldExtents();
721             x = (extents[0] + extents[3]) / 2 * scaleX;
722             z = (extents[2] + extents[5]) / 2 * scaleY;
723             ctx.strokeStyle = metric.active ? "#00ff00" : metric.allocated ? "#ffff00" : "#ff0000";
724             ctx.strokeRect(x - 0.5, z - 0.5, 1, 1);
725         }
726 
727         // Display camera (xz) position on minimap also.
728         var pos = mathDevice.m43Pos(mathDevice.m43Inverse(camera.viewMatrix));
729         if (pos[0] >= 0 && pos[2] >= 0 && pos[0] <= sceneWidth && pos[2] <= sceneHeight) {
730             ctx.strokeStyle = "#ffffff";
731             ctx.strokeRect(pos[0] * scaleX - 1.5, pos[2] * scaleY - 1.5, 3, 3);
732         }
733 
734         ctx.endFrame();
735         graphicsDevice.endFrame();
736     }
737 
738     //==========================================================================
739     // Asset and Mapping table loading
740     //=========================================================================
741     var intervalID;
742     function loadingLoop() {
743         if (graphicsDevice.beginFrame()) {
744             graphicsDevice.clear(clearColor);
745             graphicsDevice.endFrame();
746         }
747 
748         if (textureManager.getNumPendingTextures() === 0 && shaderManager.getNumPendingShaders() === 0) {
749             TurbulenzEngine.clearInterval(intervalID);
750             init();
751             intervalID = TurbulenzEngine.setInterval(mainLoop, 1000 / 60);
752         }
753     }
754     function loadAssets() {
755         // Load assets required to render renderable extents.
756         shaderManager.load("shaders/debug.cgfx");
757 
758         // Load assets required to render the fonts on screen.
759         shaderManager.load("shaders/font.cgfx");
760         fontManager.load('fonts/hero.fnt');
761 
762         // Load all assets required to create and work with the particle system archetypes we're using.
763         particleManager.loadArchetype(archetype1);
764         particleManager.loadArchetype(archetype2);
765 
766         intervalID = TurbulenzEngine.setInterval(loadingLoop, 10);
767     }
768     function mappingTableReceived(table) {
769         textureManager.setPathRemapping(table.urlMapping, table.assetPrefix);
770         shaderManager.setPathRemapping(table.urlMapping, table.assetPrefix);
771         fontManager.setPathRemapping(table.urlMapping, table.assetPrefix);
772 
773         loadAssets();
774     }
775     function sessionCreated(gameSession) {
776         TurbulenzServices.createMappingTable(requestHandler, gameSession, mappingTableReceived);
777     }
778     var gameSession = TurbulenzServices.createGameSession(requestHandler, sessionCreated);
779 
780     //==========================================================================
781     // Sample tear-down
782     //=========================================================================
783     TurbulenzEngine.onunload = function unloadFn() {
784         TurbulenzEngine.clearInterval(intervalID);
785 
786         if (gameSession) {
787             gameSession.destroy();
788             gameSession = null;
789         }
790         if (shaderManager) {
791             shaderManager.destroy();
792             shaderManager = null;
793         }
794         if (textureManager) {
795             textureManager.destroy();
796             textureManager = null;
797         }
798         if (fontManager) {
799             fontManager.destroy();
800             fontManager = null;
801         }
802         if (renderer) {
803             renderer.destroy();
804             renderer = null;
805         }
806         if (particleManager) {
807             particleManager.destroy();
808             particleManager = null;
809         }
810 
811         effectManager = null;
812         requestHandler = null;
813         cameraController = null;
814         camera = null;
815         floor = null;
816 
817         TurbulenzEngine.flush();
818 
819         inputDevice = null;
820         graphicsDevice = null;
821         mathDevice = null;
822     };
823 
824     //=========================================================================
825     // HTML Controls
826     //=========================================================================
827     var htmlControls = HTMLControls.create();
828 
829     htmlControls.addSliderControl({
830         id: "speedSlider",
831         value: (simulationSpeed),
832         max: 6,
833         min: -10,
834         step: 1,
835         fn: function () {
836             simulationSpeed = this.value;
837             htmlControls.updateSlider("speedSlider", simulationSpeed);
838         }
839     });
840 
841     htmlControls.addSliderControl({
842         id: "instanceSlider",
843         value: (generationSpeed),
844         max: 200,
845         min: 20,
846         step: 20,
847         fn: function () {
848             generationSpeed = this.value;
849             htmlControls.updateSlider("instanceSlider", generationSpeed);
850         }
851     });
852 
853     var scaleOffset = 0;
854     function refreshArchetype(description, archetype, scale) {
855         var emitters = description.emitters;
856         var count = emitters.length;
857         var i;
858         for (i = 0; i < count; i += 1) {
859             var emitter = emitters[i];
860             emitter.emittance.burstMin *= scale;
861             emitter.emittance.burstMax *= scale;
862         }
863 
864         // build new archetype from modified description.
865         // replacing all instances of old with new.
866         // and destroying the old.
867         var newArchetype = particleManager.parseArchetype(description);
868         particleManager.replaceArchetype(archetype, newArchetype);
869         particleManager.destroyArchetype(archetype);
870         return newArchetype;
871     }
872     htmlControls.addButtonControl({
873         id: "button-decrease-particles",
874         value: "-",
875         fn: function () {
876             if (scaleOffset > -5) {
877                 scaleOffset -= 1;
878                 archetype1 = refreshArchetype(description1, archetype1, 1 / 1.5);
879                 archetype2 = refreshArchetype(description2, archetype2, 1 / 1.5);
880             }
881         }
882     });
883     htmlControls.addButtonControl({
884         id: "button-increase-particles",
885         value: "+",
886         fn: function () {
887             if (scaleOffset < 4) {
888                 scaleOffset += 1;
889                 archetype1 = refreshArchetype(description1, archetype1, 1.5);
890                 archetype2 = refreshArchetype(description2, archetype2, 1.5);
891             }
892         }
893     });
894 
895     htmlControls.addCheckboxControl({
896         id: "move-systems",
897         value: "moveSystems",
898         isSelected: moveSystems,
899         fn: function () {
900             moveSystems = !moveSystems;
901             return moveSystems;
902         }
903     });
904 
905     htmlControls.addCheckboxControl({
906         id: "draw-extents",
907         value: "drawRenderableExtents",
908         isSelected: drawRenderableExtents,
909         fn: function () {
910             drawRenderableExtents = !drawRenderableExtents;
911             return drawRenderableExtents;
912         }
913     });
914 
915     htmlControls.addButtonControl({
916         id: "button-clear",
917         value: "Clear",
918         fn: function () {
919             // remove all instances of both archetypes, retaining other state like object
920             // pools and allocated memory on gpu.
921             particleManager.clear(archetype1);
922             particleManager.clear(archetype2);
923         }
924     });
925 
926     htmlControls.addButtonControl({
927         id: "button-destroy-1",
928         value: "Destroy 1",
929         fn: function () {
930             // destroy all state and instances associated with archetype1 (complete reset)
931             particleManager.destroyArchetype(archetype1);
932         }
933     });
934     htmlControls.addButtonControl({
935         id: "button-destroy-2",
936         value: "Destroy 2",
937         fn: function () {
938             // destroy all state and instances associated with archetype2 (complete reset)
939             particleManager.destroyArchetype(archetype2);
940         }
941     });
942 
943     htmlControls.addButtonControl({
944         id: "button-replace-1-2",
945         value: "Replace 1 to 2",
946         fn: function () {
947             // replace all instances of archetype1 with ones of archetype2 in-place.
948             particleManager.replaceArchetype(archetype1, archetype2);
949         }
950     });
951     htmlControls.addButtonControl({
952         id: "button-replace-2-1",
953         value: "Replace 2 to 1",
954         fn: function () {
955             // replace all instances of archetype2 with ones of archetype1 in-place.
956             particleManager.replaceArchetype(archetype2, archetype1);
957         }
958     });
959 
960     htmlControls.register();
961 };