zParticle is an app that converts arbitrary images into 3d particle clouds. It runs in the browser and leverages three.js along with some basic shader code. The z-coordinate of each point is based on the corresponding pixel color from the source image. With a decent GPU, it can render at least 10 million points at 60fps right in your browser. If the framerate is low, increase the “pixel skip” value to reduce the number of points rendered on screen. Check it out:

Launch zParticle

While it’s a lot more fun to play with the app directly, I’ve included some examples of an input image (left) and their corresponding particle clouds (right).

in motion

These gifs have rather huge file sizes. Hang in there.

All the images on this page use original artwork that I’ve previously made. There’s a certain satisfaction in remixing my own content. Within the app, however, I’ve also included images from a number of other digital artists to demonstrate how different visual styles manifest under this technique. Credit goes to Matt Mills, Matt Desl, Rik Oostenbroek, Saskia Freeke, Uon Visuals, Hyperglu, Hoodass, and Peder Norrby.

To those interested, I explain how to implement the effect below.

extracting pixel data

var image = document.createElement("img");
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
var imageWidth, imageHeight, imageData;

image.src = "path/to/image";
image.crossOrigin = "Anonymous";
image.onload = function() {
  imageWidth = canvas.width = image.width;
  imageHeight = canvas.height = image.height;

  context.fillStyle = context.createPattern(image, 'no-repeat');
  context.fillRect(0, 0, imageWidth, imageHeight);

  // this is your raw image data
  imageData = context.getImageData(0, 0, imageWidth, imageHeight).data;

  pixelsToGeometry();
  createPointCloud();
  window.requestAnimationFrame(update);
};

transforming pixel data

var pixels, vertices, colors;

function pixelsToGeometry() {

  pixels = imageWidth*imageHeight;
  vertices = new Float32Array( pixels * 3 );
  colors = new Float32Array( pixels * 3 );

  var color = new THREE.Color();
  var x = imageWidth * -0.5;
  var y = imageHeight * 0.5;
  var loc = 0, c = 0;

  for (var i = 0; i < imageHeight; i++) {
    for (var j = 0; j < imageWidth; j++) {

      color.setRGB(imageData[c] / 255, imageData[c + 1] / 255, imageData[c + 2] / 255);

      var weight = color.r * 0.33 + color.g * 0.33 + color.b * 0.33;

      vertices[loc]     = x;
      vertices[loc + 1] = y;
      vertices[loc + 2] = (100 * -0.5) + (100 * weight);

      colors[loc]     = color.r;
      colors[loc + 1] = color.g;
      colors[loc + 2] = color.b;

      loc+=3;
      c += 4;
      x++;
    }

    x = imageWidth * -0.5;
    y--;
  }

}

creating a point cloud with three.js

function createPointCloud() {

  var container = document.getElementById('container');
  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 1, 100000);
  camera.position.z = 4000;
  camera.lookAt(scene.position);

  var renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  shaderUniforms = {amplitude: { type: "f", value: 0.5 }};

  var shaderMaterial = new THREE.ShaderMaterial({
    transparent: true,
    opacity: 0.8,
    uniforms: shaderUniforms,
    vertexShader: document.getElementById("vertexShader").textContent,
    fragmentShader: document.getElementById("fragmentShader").textContent
  })

  geometry = new THREE.BufferGeometry();
  geometry.addAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
  geometry.addAttribute( 'vertexColor', new THREE.BufferAttribute( colors, 3 ) );

  geometry.attributes.position.needsUpdate = true;
  geometry.verticesNeedUpdate = true;

  particleSystem = new THREE.Points(geometry, shaderMaterial);
  scene.add(particleSystem);
}

animating it

function update() {
  window.requestAnimationFrame(update);

  shaderUniforms.amplitude.value = amplitude;
}

shader code

<script type="x-shader/x-vertex" id="vertexShader">
  uniform float amplitude;
  varying vec4 varColor;
  attribute vec3 vertexColor;

  void main() {
    varColor = vec4(vertexColor, 1.0);

    vec4 pos = vec4(position , 1.0);
    pos.z *= amplitude;

    vec4 mvPosition = modelViewMatrix * pos;

    gl_Position = projectionMatrix * mvPosition;
  }
</script>

<script type="x-shader/x-fragment" id="fragmentShader">
  varying vec4 varColor;

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

I certainly didn’t invent this technique, but I do think it’s under utilized. I hope this project can help get the word out.