0
votes

Link to thread threejs discourse: https://discourse.threejs.org/t/fbo-particles-with-cumulative-movement/7221

This is difficult for me to explain because of my limited knowledge on the subject, but I'm gonna do my best..

At this point, I have a basic FBO particle system in place that works. The following is how it's set up:

var FBO = function( exports ){

    var scene, orthoCamera, rtt;
    exports.init = function( width, height, renderer, simulationMaterial, renderMaterial ){

        var gl = renderer.getContext();

        //1 we need FLOAT Textures to store positions
        //https://github.com/KhronosGroup/WebGL/blob/master/sdk/tests/conformance/extensions/oes-texture-float.html
        if (!gl.getExtension("OES_texture_float")){
            throw new Error( "float textures not supported" );
        }

        //2 we need to access textures from within the vertex shader
        //https://github.com/KhronosGroup/WebGL/blob/90ceaac0c4546b1aad634a6a5c4d2dfae9f4d124/conformance-suites/1.0.0/extra/webgl-info.html
        if( gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) == 0 ) {
            throw new Error( "vertex shader cannot read textures" );
        }

        //3 rtt setup
        scene = new THREE.Scene();
        orthoCamera = new THREE.OrthographicCamera(-1,1,1,-1,1/Math.pow( 2, 53 ),1 );

        //4 create a target texture
        var options = {
            minFilter: THREE.NearestFilter,//important as we want to sample square pixels
            magFilter: THREE.NearestFilter,//
            format: THREE.RGBAFormat,//180407 changed to RGBAFormat
            type:THREE.FloatType//important as we need precise coordinates (not ints)
        };
        rtt = new THREE.WebGLRenderTarget( width,height, options);


        //5 the simulation:
        //create a bi-unit quadrilateral and uses the simulation material to update the Float Texture
        var geom = new THREE.BufferGeometry();
        geom.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array([   -1,-1,0, 1,-1,0, 1,1,0, -1,-1, 0, 1, 1, 0, -1,1,0 ]), 3 ) );
        geom.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array([   0,1, 1,1, 1,0,     0,1, 1,0, 0,0 ]), 2 ) );
        scene.add( new THREE.Mesh( geom, simulationMaterial ) );


        //6 the particles:
        //create a vertex buffer of size width * height with normalized coordinates
        var l = (width * height );
        var vertices = new Float32Array( l * 3 );
        for ( var i = 0; i < l; i++ ) {

            var i3 = i * 3;
            vertices[ i3 ] = ( i % width ) / width ;
            vertices[ i3 + 1 ] = ( i / width ) / height;
        }

        //create the particles geometry
        var geometry = new THREE.BufferGeometry();
        geometry.addAttribute( 'position',  new THREE.BufferAttribute( vertices, 3 ) );

        //the rendermaterial is used to render the particles
        exports.particles = new THREE.Points( geometry, renderMaterial );
        exports.particles.frustumCulled = false;
        exports.renderer = renderer;

    };

    //7 update loop
    exports.update = function(){

        //1 update the simulation and render the result in a target texture
        // exports.renderer.render( scene, orthoCamera, rtt, true );
        exports.renderer.setRenderTarget( rtt );
        exports.renderer.render( scene, orthoCamera );
        exports.renderer.setRenderTarget( null );

        //2 use the result of the swap as the new position for the particles' renderer
        // had to add .texture on the end of rtt for r103
        exports.particles.material.uniforms.positions.value = rtt.texture;

    };
    return exports;
}({});

The following are the shaders it uses:

    <script type="x-shader/x-vertex" id="simulation_vs">
    //vertex shader
    varying vec2 vUv;
    void main() {
        vUv = vec2(uv.x, uv.y);

        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
    </script>

    <script type="x-shader/x-fragment" id="simulation_fs">
    //fragment Shader
    uniform sampler2D positions;//DATA Texture containing original positions
    varying vec2 vUv;
    void main() {

        //basic simulation: displays the particles in place.
        vec3 pos = texture2D( positions, vUv ).rgb;

        // we can move the particle here 


        gl_FragColor = vec4( pos,1.0 );
    }
    </script>

    <script type="x-shader/x-vertex" id="render_vs">
    //vertex shader
    uniform sampler2D positions;//RenderTarget containing the transformed positions
    uniform float pointSize;//size
    void main() {

        //the mesh is a nomrliazed square so the uvs = the xy positions of the vertices
        vec3 pos = texture2D( positions, position.xy ).xyz;
        //pos now contains a 3D position in space, we can use it as a regular vertex

        //regular projection of our position
        gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );

        //sets the point size
        gl_PointSize = pointSize;
    }
    </script>

    <script type="x-shader/x-fragment" id="render_fs">
    //fragment shader
    void main()
    {
        gl_FragColor = vec4( vec3( 1. ), .25 );
    }
    </script>

I understand that I would move the particles in the "simulation_fs", but if I move a particle in that shader, if I try to do something like this,

pos.x += 1.0;

it will still only shift it one unit from the original texture position. I want the movement to be cumulative.

Would using a 2nd set of simulation shaders allow me to move the particles in a cumulative way? Is that a practical solution?

2

2 Answers

1
votes

For cumulative movement, you need to use uniforms:

Look into passing a uniform named time to your vertex shader. Then you can update the time once per frame, and you can use that to animate your vertex positions. For example:

position.x = 2.0 * time; // Increment linearly

position.x = sin(time); // Sin wave back-forth animation

Without a changing variable, your vertex animations will be static from one frame to the next.

0
votes

I needed to accomplish something like this, and set up an absolutely minimal example that I can tweak in the future. You'll see the positional changes are cumulative.

The following was simplified from a wonderful discussion of FBO's by Nicolas Barradeau (a webgl wizard):

// specify the container where we'll render the scene
var elem = document.querySelector('body'),
    elemW = elem.clientWidth,
    elemH = elem.clientHeight

// generate a scene object
var scene = new THREE.Scene();

// generate a camera
var camera = new THREE.PerspectiveCamera(75, elemW/elemH, 0.001, 100);

// generate a renderer
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(elemW, elemH);
elem.appendChild(renderer.domElement);

// generate controls
var controls = new THREE.TrackballControls(camera, renderer.domElement);

// position camera and controls
camera.position.set(0.5, 0.5, -5);
controls.target = new THREE.Vector3(0.5, 0.5, 0);

/**
* FBO
**/

// verify browser agent supports "frame buffer object" features
gl = renderer.getContext();
if (!gl.getExtension('OES_texture_float') ||
     gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) == 0) {
  alert(' * Cannot create FBO :(');
}

// set initial positions of `w*h` particles
var w = h = 256,
    i = 0,
    data = new Float32Array(w*h*3);
for (var x=0; x<w; x++) {
  for (var y=0; y<h; y++) {
    data[i++] = x/w;
    data[i++] = y/h;
    data[i++] = 0;
  }
}

// feed those positions into a data texture
var dataTex = new THREE.DataTexture(data, w, h, THREE.RGBFormat, THREE.FloatType);
dataTex.minFilter = THREE.NearestFilter;
dataTex.magFilter = THREE.NearestFilter;
dataTex.needsUpdate = true;

// add the data texture with positions to a material for the simulation
var simMaterial = new THREE.RawShaderMaterial({
  uniforms: { posTex: { type: 't', value: dataTex }, },
  vertexShader: document.querySelector('#sim-vs').textContent,
  fragmentShader: document.querySelector('#sim-fs').textContent,
});

// delete dataTex; it isn't used after initializing point positions
delete dataTex;

THREE.FBO = function(w, simMat) {
  this.scene = new THREE.Scene();
  this.camera = new THREE.OrthographicCamera(-w/2, w/2, w/2, -w/2, -1, 1);
  this.scene.add(new THREE.Mesh(new THREE.PlaneGeometry(w, w), simMat));
};

// create a scene where we'll render the positional attributes
var fbo = new THREE.FBO(w, simMaterial);

// create render targets a + b to which the simulation will be rendered
var renderTargetA = new THREE.WebGLRenderTarget(w, h, {
  wrapS: THREE.RepeatWrapping,
  wrapT: THREE.RepeatWrapping,
  minFilter: THREE.NearestFilter,
  magFilter: THREE.NearestFilter,
  format: THREE.RGBFormat,
  type: THREE.FloatType,
  stencilBuffer: false,
});

// a second render target lets us store input + output positional states
renderTargetB = renderTargetA.clone();

// render the positions to the render targets
renderer.render(fbo.scene, fbo.camera, renderTargetA, false);
renderer.render(fbo.scene, fbo.camera, renderTargetB, false);

// store the uv attrs; each is x,y and identifies a given point's
// position data within the positional texture; must be scaled 0:1!
var geo = new THREE.BufferGeometry(),
    arr = new Float32Array(w*h*3);
for (var i=0; i<arr.length; i++) {
  arr[i++] = (i%w)/w;
  arr[i++] = Math.floor(i/w)/h;
  arr[i++] = 0;
}
geo.addAttribute('position', new THREE.BufferAttribute(arr, 3, true))

// create material the user sees
var material = new THREE.RawShaderMaterial({
  uniforms: {
    posMap: { type: 't', value: null }, // `posMap` is set each render
  },
  vertexShader: document.querySelector('#ui-vert').textContent,
  fragmentShader: document.querySelector('#ui-frag').textContent,
  transparent: true,
});

// add the points the user sees to the scene
var mesh = new THREE.Points(geo, material);
scene.add(mesh);

function render() {
  // at the start of the render block, A is one frame behind B
  var oldA = renderTargetA; // store A, the penultimate state
  renderTargetA = renderTargetB; // advance A to the updated state
  renderTargetB = oldA; // set B to the penultimate state

  // pass the updated positional values to the simulation
  simMaterial.uniforms.posTex.value = renderTargetA.texture;

  // run a frame and store the new positional values in renderTargetB
  renderer.render(fbo.scene, fbo.camera, renderTargetB, false);

  // pass the new positional values to the scene users see
  material.uniforms.posMap.value = renderTargetB.texture;

  // render the scene users see as normal
  renderer.render(scene, camera);
  controls.update();
  requestAnimationFrame(render);
};

render();
  html, body { width: 100%; height: 100%; background: #000; }
  body { margin: 0; overflow: hidden; }
  canvas { width: 100%; height: 100%; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/101/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>

<!-- The simulation shaders update positional attributes -->
<script id='sim-vs' type='x-shader/x-vert'>
  precision mediump float;

  uniform mat4 projectionMatrix;
  uniform mat4 modelViewMatrix;

  attribute vec2 uv; // x,y offsets of each point in texture
  attribute vec3 position;

  varying vec2 vUv;

  void main() {
    vUv = vec2(uv.x, 1.0 - uv.y);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
</script>

<script id='sim-fs' type='x-shader/x-frag'>
  precision mediump float;

  uniform sampler2D posTex;

  varying vec2 vUv;

  void main() {

    // read the supplied x,y,z vert positions
    vec3 pos = texture2D(posTex, vUv).xyz;

    // update the positional attributes here!
    pos.x += cos(pos.y) / 100.0;
    pos.y += tan(pos.x) / 100.0;

    // render the new positional attributes
    gl_FragColor = vec4(pos, 1.0);
  }
</script>

<!-- The ui shaders render what the user sees -->
<script id='ui-vert' type='x-shader/x-vert'>
  precision mediump float;

  uniform sampler2D posMap; // contains positional data read from sim-fs
  uniform mat4 projectionMatrix;
  uniform mat4 modelViewMatrix;

  attribute vec2 position;

  void main() {

    // read this particle's position, which is stored as a pixel color
    vec3 pos = texture2D(posMap, position.xy).xyz;

    // project this particle
    vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
    gl_Position = projectionMatrix * mvPosition;

    // set the size of each particle
    gl_PointSize = 0.3 / -mvPosition.z;
  }

</script>

<script id='ui-frag' type='x-shader/x-frag'>
  precision mediump float;

  void main() {
    gl_FragColor = vec4(0.0, 0.5, 1.5, 1.0);
  }
</script>