neutrinoparticles.js: Programming Guide


Introduction

Neutrinoparticles.js allows you to simulate and render particle effects exported from NeutrinoParticles editor. You can render effects on 2D canvas, as well as on WebGL one.

The library doesn’t load images used in the effect but gives you their file names, so you can do it by yourself. After you loaded all imaged you have to pass them into the effect before start to render it.

Every animation frame you update the effect by passing time spent from previous frame and then render it.

You can use emulation of 3D camera for 2d canvas to achieve perspective effect similar to perspective projection in WebGL.

Effect has position, so you can move it around the screen programmatically or attach to some object (like mouse pointer) by passing new position on every effect update.

2D Canvas

Quick Start

Below is a simplest neutrinoparticles.js example description when using with 2d canvas. You can find this code at GitHub repository (samples/simplest/simplest.html).

First of all you will need rendering context of your canvas:

var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');

You will need a single NeutrinoParticles object to load and simulate all effects:

var neutrino = new NeutrinoParticles();

Then you can load effect model:

var effectModel = new NeutrinoPart_3boom_stars(neutrino);

Here NeutrinoPart_3boom_stars is an automatically generated class name of the effect model, which exported from “3boom_stars” effect (NeutrinoPart_ + <name>). Every non-alphabetical and non-digit characters will be converted to underlines.

Then you create instance of the effect:

var effect = effectModel.createCanvas2DInstance(
    [0, 0, 0] // position of the effect
    );

You can create as many effect as you want with the same model.

Further images loading and effect updating/drawing is very dependent on your framework. Below is an example of straight loading of all images required for the effect. But if you already have a map of loaded images in your framework or use atlases of images, you would probably want to use them instead of duplicating. So, consider next code as a reference of necessary steps for images loading:

var onImagesLoaded = null;

var loadImages = function() {
    var imagesToLoad = effect.model.textures.length;

    // result map of (image path -&gt; image description)
    var imageDescs = [];
    
    for (var imageIndex = 0; imageIndex &lt; effect.model.textures.length; ++imageIndex) {
        var image = new Image();
        
        image.onload = (function () {
            var savedImageIndex = imageIndex;
            var savedImage = image;

            return function () {
                // assign image description to image path
                imageDescs[savedImageIndex] = new neutrino.ImageDesc(
                    savedImage, // image
                    0, // X displace inside image
                    0, // Y displace inside image
                    savedImage.width, // width of sub-image to use
                    savedImage.height // height of sub-image to use
                    );
                    
                if (--imagesToLoad == 0) {
                    effect.textureDescs = imageDescs; // send image descriptions to the effect
                    onImagesLoaded();
                };
            };
        })();
        
        image.src = 'textures/' + effect.model.textures[imageIndex];
    };
}

And next is updating/drawing cycle where most interesting parts are effect.update(..) and effect.draw(..) while the rest is time calculations and background preparation.

onImagesLoaded = function() {
    var lastCalledTime = null;
    
    var updateFrame = function () {
        requestAnimationFrame(updateFrame);

        if (lastCalledTime == null) {
          lastCalledTime = Date.now();
        }
        
        var currentTime = Date.now();
        var elapsedTime = (currentTime - lastCalledTime) / 1000;
        lastCalledTime = currentTime;

        // update the effect
        effect.update(
            elapsedTime &gt; 1.0 ? 1.0 : elapsedTime, // time from previous frame in seconds
            [0, 0, 0] // new position of the effect
            );
        
        context.fillStyle = "grey";
        context.fillRect(0, 0, canvas.width, canvas.height);
        
        // draw the effect
        effect.draw(
            context // rendering context
            // , camera // camera to use when drawing
            );
    };
    
    updateFrame();
}

loadImages();

3D Camera

You can use 3D camera emulation with neutrinoparticles.js.

First, you need to create the 3dcamera:

var camera = new neutrino.Camera3D(
    [800, 600],  // size of screen area
    60 // horizontal angle in degrees
    );

and modify a little the draw call:

// draw the effect
effect.draw(context, camera);

In this case camera object will modify particles’ position and size dependently from their Z coordinate, and you will get emulation of 3D view.

Camera3D object assumes that rendering area’s left top corner has [0; 0] coordinates and right bottom one has coordinates passed in the constructor.

You can write your own camera and pass it to the draw call. All you need is to create class of camera with transform function, which should modify passed in arguments in desired way. Here is it’s signature:

MyCamera.prototype.transform = function (pos, size) { ... }

where

  • pos – array with position [x, y, z];
  • size – array with size [width, height].

After modifying position you should write it’s X and Y components back to pos, they will be used to draw the particle on the canvas.

WebGL

Quick Start

Below is a simplest WebGL example description. You can find this code at GitHub repository (samples/simplest/simplest.webgl.html).

To easily integrate and start to use NeutrinoParticles with WebGL, neutrinoparticles.webgl.js module is introduced. You will need to include it to your project.

First of all you will need to get canvas and initialize OpenGL:

var canvas = document.getElementById('myCanvas');

var gl;
{
    try {
        gl = canvas.getContext("webgl", { premultipliedAlpha: false, alpha: false });
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
    } catch (e) {
    }

    if (!gl) {
        alert("Could not initialise WebGL, sorry :-(");
    }
}

Then create basic NeutrinoParticles context and NeutrinoParticlesWGL context:

var neutrino = new NeutrinoParticles();
var neutrinoWGL = new NeutrinoParticlesWGL(gl);

Create model of the effect and it’s WGL instance:

var effectModel = new NeutrinoPart_3boom_stars(neutrino);

var effect = effectModel.createWGLInstance(
    [0, 0, 0] // position of the effect
    );

You can create as many effect as you want with the same model.

Create WebGL renderer of the effect:

var effectWGL = new neutrinoWGL.Renderer(effect);

Further images loading, textures creation and effect updating/drawing is very dependent on your framework. Below is an example of straight loading of all images required for the effect and converting them to the GL with provided WebGL renderer. But if you already have a map of loaded images/textures in your framework or use atlases of images/textures, you would probably want to use them instead of duplicating. So, consider next code as a reference of necessary steps for images loading:

var onImagesLoaded = null;

// load all images/textures
var loadImages = function () {
    // list of textures used by the effects is in effect.model.textures
    var imagesToLoad = effect.model.textures.length;

    // result map of (image path -&gt; image description)
    var images = [];

    for (var imageIndex = 0; imageIndex &lt; effect.model.textures.length; ++imageIndex) {
        var image = new Image();

        //test texture remapping (for textures atlases)
        //effect.texturesRemap[imageIndex] = new neutrino.SubRect(
        //    0, // x of subrect
        //    0, // y of subrect
        //    0.5, // width of subrect
        //    0.5 // height of subrect
        //);
        image.onload = (function () {
            var savedImageIndex = imageIndex;
            var savedImage = image;

            return function () {
                // assign image description to image path
                images[savedImageIndex] = savedImage;

                if (--imagesToLoad == 0) {
                    effectWGL.createTexturesFromImages(images);
                    onImagesLoaded();
                };
            };
        })();

        image.src = 'textures/' + effect.model.textures[imageIndex];
    };
}

And the last part is rendering cycle:

onImagesLoaded = function() {
    
    // setup clear viewport color to grey
    gl.clearColor(0.5, 0.5, 0.5, 1.0);
    
    // create matrices
    var mvMatrix = mat4.create();
    var pMatrix = mat4.create();
    
      var lastCalledTime = null;
    
    var updateFrame = function () {
        requestAnimationFrame(updateFrame);

        // time from previous frame calculations
        if (lastCalledTime == null) {
          lastCalledTime = Date.now();
        }
        
        var currentTime = Date.now();
        var elapsedTime = (currentTime - lastCalledTime) / 1000;
        lastCalledTime = currentTime;

        // update the effect
        effectWGL.update(
            elapsedTime &gt; 1.0 ? 1.0 : elapsedTime, // time from previous frame in seconds
            [0, 0, 0] // new position of the effect
            );

        // this will create geometry inside effect object regarding to current camera setup
        effectWGL.prepareGeometry(
            [1, 0, 0], //cameraRight
            [0, -1, 0], //cameraUp
            [0, 0, -1] //cameraDir
            );
        
        // clear viewport with background color (grey)
        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        // prepare projection matrix with horizontal fov 60 degrees and Y axis looking down (as Canvas2D has)
        var angleX = 60.0 * Math.PI / 180.0; 
        var angleY = angleX * gl.viewportHeight / gl.viewportWidth;
        var near = 1.0;
        var far = 10000.0;
        var projX = Math.tan(angleX * 0.5) * near;
        var projY = Math.tan(angleY * 0.5) * near;
        var cameraZ = near * gl.viewportWidth * 0.5 / projX;
        mat4.frustum(pMatrix, -projX, projX, projY, -projY, near, far);
        
        // modelview matrix will shift camera to the center of the screen
        mat4.identity(mvMatrix);
        mat4.translate(mvMatrix, mvMatrix, [-gl.viewportWidth / 2, -gl.viewportHeight / 2, -cameraZ]);

        // fill geometry buffers with geometry of the effect and render it
        effectWGL.render(pMatrix, mvMatrix);
    };
    
    updateFrame();
}

loadImages();

In the code below, effectWGL.update(…) updates all particles parameters according to spent time from previous frame. It doesn’t make any WebGL related things.

effectWGL.prepareGeometry(…) creates triangles geometry inside ‘effect’ object. It doesn’t make any OpenGL calls, though it prepares ArrayBuffers with all necessary data for such calls.

effectWGL.render(…) fills OpenGL buffers with data prepared on previous step and make all necessary render calls to draw the effect.

You might have several cameras inside your project, so prepareGeometry() and render() steps you will need to make for every camera you want to render from.

Using textures loaded by your framework

Please, refer to the source code of NeutrinoParticlesWGL.Renderer.createTexturesFromImages() to fill textures array with OpenGL textures loaded by your own.

Texture coordinates remapping (for texture atlases)

To use texture atlases with neutrinoparticles.webgl.js you need to make remapping of texture coordinates for each texture. This remapping will influence on texture coordinates generated in the geometry on stage effectWGL.prepareGeometry() described above. To make remapping you need to fill effect.textureRemap array with neutrino.SubRect() objects. Indices inside textureRemap array satisfies to textures’ indices inside effect.model.textures array. You

effect.texturesRemap[imageIndex] = new neutrino.SubRect(
        0, // x of subrect
        0, // y of subrect
        0.5, // width of subrect
        0.5 // height of subrect
        );

Coordinates and sizes of subrect is normalized and 1 – is full width or full height.

Low level integration

Though neutrinoparticles.webgl.js provides a convenient renderer for WebGL, you might want to make deep integration of the library inside your graphical framework. To make that, please, use provided renderer’s code as a reference to make your own renderer integrated into your framework. Detailed description of the low level integration is to be done.

Instant effect position change

To instantly change effect’s position you can use resetPosition():

effect.resetPosition(
    [x, y, z] // new effect's position
    );

This is useful for effects that generate particles depending on spent distance, so with this method you indicate, that effect is not moved to the new position, but jumped there. And no particles will trail to the new position.

Noise (Turbulence)

If you plan to use Noise3 block in your effects, you also need to make next call before effects simulation:

neutrino.initializeNoise(
    "/neutrino/" // path to directory where "neutrinoparticles.noise.bin" is
    , callback // function to call when noise binary is loaded and ready to use in effects
    );

When you made this call the library will try to load neutrinoparticles.noise.bin. When it is successfully done it will call the callback.

If you update/render effects  that use noise before the library loaded noise binary, you will get effects without noise.

Note, that neutrinoparticles.noise.bin is pretty big file (768K), so if your project has strict download size requirements consider twice to use it.

Changing emitter’s properties

Using the library API you can setup emitter’s properties to programmatically change effects. In case if you want to change a property in one emitter, you can access this property directly:

effect._EmitterName._PropertyName = 10; //[10, 20] or [10, 20, 30] for vectors

Please note, that you need to add underline before emitter and property name.

In case when you need to change properties with the same name in all emitters of the effect, you can use next function:

effect.setPropertyInAllEmitters("PropertyName", 10); //[10, 20] or [10, 20, 30] for vectors