<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8'>
    <style>* {margin: 0; height: 100%; width: 100%;}</style>
  </head>
  <body>
    <!-- rendered vert shader -->
    <script type='x-shader/x-vertex' id='vertex-shader'>
    precision highp float;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;
    uniform sampler2D fboData;

    attribute vec3 position;
    attribute vec3 translation;
    attribute vec2 fboOffset;

    varying vec4 vPixel;

    void main() {
      // scale the point size by the device DPI and window height
      gl_PointSize = 7.0;

      // position and project the point
      vec3 pos = position + translation;

      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);

      vPixel = texture2D(fboData, fboOffset);
    }
    </script>

    <!-- rendered frag shader -->
    <script type='x-shader/x-fragment' id='fragment-shader'>
    precision highp float;

    varying vec4 vPixel;

    void main() {
      gl_FragColor = vPixel;
    }
    </script>

    <!-- FBO vert -->
    <script type='x-shader/x-vertex' id='fbo-vertex'>
    precision lowp float;

    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;

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

    varying vec2 vUv;

    void main() {
      // vUv is the position of the current observation in the texture
      vUv = vec2(uv.x, 1.0 - uv.y);

      // the position of the cell in the texture
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
    </script>

    <!-- FBO frag -->
    <script type='x-shader/x-fragment' id='fbo-fragment'>
    precision lowp float;

    uniform sampler2D fboTexture;

    varying vec2 vUv;

    void main() {

      // read the x, y, and size components of this point
      vec4 pixel = texture2D(fboTexture, vUv);

      // write the updated value to the screen so it can be read
      gl_FragColor = vec4(pixel);
    }
    </script>

    <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'></script>
  </body>
</html>
THREE.FBO = function(w, m) {
  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), m));
};

function resize() {
  var w = window.innerWidth,
      h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h, false);
}

function getTexture(src) {
  var image = document.createElement('img');
  var tex = new THREE.Texture(image);
  image.addEventListener('load', function(event) {
    tex.needsUpdate = true;
  });
  image.src = src;
  return tex;
}

function getDataTexture(fboData, config) {
  var dataTexture = new THREE.DataTexture(fboData, config.w, config.w, config.format, config.type);
  //dataTexture.encoding = THREE.RGBEEncoding;
  dataTexture.minFilter = config.minFilter;
  dataTexture.magFilter = config.magFilter;
  dataTexture.needsUpdate = true;
  return dataTexture;
}

/**
* Scene
**/

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.001, 1000);
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
camera.position.set(0, 0, 2);
document.body.appendChild(renderer.domElement);

/**
* Geometry
**/

// fbo config
var config = {
  w: 512, // number of pixels in each dimension
  type: THREE.FloatType,
  format: THREE.RGBAFormat,
  stride: 4, // number of units per cell in format
  minFilter: THREE.NearestFilter,
  magFilter: THREE.NearestFilter,
}

var n = 2**10,
    rootN = Math.pow(n, 0.5),
    translations = new Float32Array(n * 3),
    fboData = new Float32Array(config.w**2 * config.stride),
    fboOffsets = new Float32Array(n * 2),
    translationIterator = 0,
    fboOffsetIterator = 0;

for (var i=0; i<n; i++) {
  // set rendered vertex offsets
  var x = (((i % rootN) / rootN) * 2) - 1;
  var y = ((Math.floor(i / rootN) / rootN) * 2) - 1;

  translations[translationIterator++] = x;
  translations[translationIterator++] = y;
  translations[translationIterator++] = 0;

  // set uvs for looking up FBO data that corresponds to this cell
  fboOffsets[fboOffsetIterator++] = (i % config.w) / config.w;
  fboOffsets[fboOffsetIterator++] = (Math.floor(i / config.w)) / config.w;

  // write the x, y components of the fboData array
  fboData[(i*config.stride)    ] = Math.random(); // make data non-negative
  fboData[(i*config.stride) + 1] = Math.random(); // make data non-negative
  fboData[(i*config.stride) + 2] = 1;
  if (config.stride === 4) fboData[(i*config.stride) + 3] = 1;
}

// create the FBO
var fboTexture = getDataTexture(fboData, config);
fboMaterial = new THREE.RawShaderMaterial({
  uniforms: {
    'fboTexture': {
      type: 't',
      value: fboTexture,
    },
  },
  vertexShader: document.querySelector('#fbo-vertex').textContent,
  fragmentShader: document.querySelector('#fbo-fragment').textContent,
});
fboMaterial.uniforms.fboTexture.needsUpdate = true;
fbo = new THREE.FBO(config.w, fboMaterial);

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

// render the initial data to target a
renderer.setRenderTarget(renderTargetA);
renderer.render(fbo.scene, fbo.camera);
renderer.setRenderTarget(null);

// render the initial data to target b
renderer.setRenderTarget(renderTargetB);
renderer.render(fbo.scene, fbo.camera);
renderer.setRenderTarget(null);

// create the geometry
var geometry = new THREE.InstancedBufferGeometry();
var position = new THREE.BufferAttribute(new Float32Array([0, 0, 0]), 3);
var translation = new THREE.InstancedBufferAttribute(translations, 3, false, 1);
var fboOffset = new THREE.InstancedBufferAttribute(fboOffsets, 2, false, 1);
geometry.setAttribute('position', position);
geometry.setAttribute('translation', translation);
geometry.setAttribute('fboOffset', fboOffset);

// build the rendered mesh
var material = new THREE.RawShaderMaterial({
  vertexShader: document.getElementById('vertex-shader').textContent,
  fragmentShader: document.getElementById('fragment-shader').textContent,
  uniforms: {
    fboData: {
      type: 't',
      value: fboTexture,
    }
  }
})
mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false;
scene.add(mesh);

// resize everything
resize();

// resize
window.addEventListener('resize', resize);

/**
* Main
**/

function updateFBO() {

  // 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 values to the fbo
  fboMaterial.uniforms.fboTexture.value = renderTargetA.texture;

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

  // pass the new positional values to the scene users see
  if (mesh) mesh.material.uniforms.fboData.value = renderTargetA.texture;
}

function animate() {
  requestAnimationFrame(animate);
  updateFBO();
  renderer.render(scene, camera);
}

animate();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.