JSFiddle - React, Tailwind, and code Playground

by Douglas Duhaime

HTML

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

// these come built in with three.js; they're basically always used in the same way (see below)
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec3 cameraPosition;

// these are uniforms we specified
uniform float atlasPx;
uniform float cellPx;
uniform float pointScale;

// these are the buffer attributes we specified when creating the geometry
attribute vec2 position;
attribute vec2 uv;

// these are attributes we will pass from the vertex to the fragment shader
varying vec2 vUv;

void main() {
  // get the position of the particle
  vec4 mvPosition = modelViewMatrix * vec4(position, 0.0, 1.0);
  gl_Position = projectionMatrix * mvPosition;

  // set the point size
  gl_PointSize = pointScale;

  // pass the varying data to the fragment shader
  vUv = uv;
}
</script>

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

uniform sampler2D texture;
uniform float atlasPx;
uniform float cellPx;

varying vec2 vUv;

void main() {
  vec2 uv = (vUv * cellPx + gl_PointCoord.xy * cellPx) / atlasPx;

  gl_FragColor = texture2D(texture, uv);

  // this line mixes the actual texture color with some red
  //gl_FragColor = mix(gl_FragColor, vec4(1.0, 0.0, 0.0, 1.0), 0.5);
}
</script>

<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r111/three.min.js'></script>

CSS

body { margin: 0 }
canvas { display: block }

JavaScript

var camera, scene, renderer, geometry, material, mesh, n, rootN;

function init() {
  camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 10 );
  camera.position.z = 1.8;

  scene = new THREE.Scene();
  n = 60; // number of cells to render
  rootN = Math.ceil(Math.pow(n, 0.5)); // positions in each axis of grid

  // create buffer attributes
  const uvs = new Float32Array(n * 2)
  const positions = new Float32Array(n * 2)

  for (var i=0; i<n; i++) {
    var x = i % rootN; // x position 0:rootN-1
    var y = Math.floor(i / rootN); // y position 0:rootN-1
    var clipX = ((x / (rootN-1))-0.5)*2; // scale x dimension -1:1
    var clipY = -((y / Math.floor(n/rootN))-0.5)*2; // scale y dimension -1:1
    positions[i*2] = clipX;
    positions[i*2+1] = clipY;
    uvs[i*2] = x; // store uv as x,y grid unit offset of the cell
    uvs[i*2+1] = y;
  }

  // create geometry
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 2, true));
  geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2, true));

  const url = 'https://raw.githubusercontent.com/slnsw/dxlab-fellowship-2019/master/public/assets/medals.jpg';
  const texture = new THREE.TextureLoader().load(url);
  texture.flipY = false;

  const material = new THREE.RawShaderMaterial({
    vertexShader: document.getElementById('vertex-shader').textContent,
    fragmentShader: document.getElementById('fragment-shader').textContent,
    uniforms: {
      texture: {
        type: 't',
        value: texture,
      },
      atlasPx: {
        type: 'f',
        value: 256,
      },
      cellPx: {
        type: 'f',
        value: 32,
      },
      pointScale: {
        type: 'f',
        value: getPointSize(),
      },
    },
  });

  mesh = new THREE.Points(geometry, material);
  mesh.frustumCulled = false;
  scene.add( mesh );

  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize(...