DEV Community

Cover image for WebGL Month. Day 28. Click detection. Part II
Andrei Lesnitsky
Andrei Lesnitsky

Posted on

WebGL Month. Day 28. Click detection. Part II

This is a series of blog posts related to WebGL. New post will be available every day

GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo


Hey 👋

Welcome to WebGL month

Yesterday we've rendered our minecraft terrain to a offscreen texture, where each object is encoded into a specific color and learned how to read pixel colors from the texture back to JS. Now let's decode this color to an object index and highlight selected cube

gl.readPixels fills the Uint8Array with pixel colors startig from the bottom left corner. We need to convert client coordinates to the pixels coordinate in the array. Don't forget the pixel ration, since our offscreen framebuffer takes it into account, and event coordinates don't.

📄 src/minecraft.js

      requestAnimationFrame(render);
  }

- document.body.addEventListener('click', () => {
+ document.body.addEventListener('click', (e) => {
      coloredCubesRenderBuffer.bind(gl);

      renderTerrain(gl, viewMatrix, projectionMatrix, true);

      const pixels = new Uint8Array(canvas.width * canvas.height * 4);
      gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+ 
+     const x = e.clientX * devicePixelRatio;
+     const y = (canvas.offsetHeight - e.clientY) * devicePixelRatio;
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

We need to skip y rows (y * canvas.width) multiplied by 4 (4 integers per pixel)

📄 src/minecraft.js


      const x = e.clientX * devicePixelRatio;
      const y = (canvas.offsetHeight - e.clientY) * devicePixelRatio;
+ 
+     const rowsToSkip = y * canvas.width * 4;
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

Horizontal coordinate is x * 4 (coordinate multiplied by number of integers per pixel)

📄 src/minecraft.js

      const y = (canvas.offsetHeight - e.clientY) * devicePixelRatio;

      const rowsToSkip = y * canvas.width * 4;
+     const col = x * 4;
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

So the final index of pixel is rowsToSkip + col

📄 src/minecraft.js


      const rowsToSkip = y * canvas.width * 4;
      const col = x * 4;
+ 
+     const pixelIndex = rowsToSkip + col;
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

Now we need to read each pixel color component

📄 src/minecraft.js

      const col = x * 4;

      const pixelIndex = rowsToSkip + col;
+ 
+     const r = pixels[pixelIndex];
+     const g = pixels[pixelIndex + 1];
+     const b = pixels[pixelIndex + 2];
+     const a = pixels[pixelIndex + 3];
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

Now we need to convert back to integer from r g b

📄 src/minecraft.js

      requestAnimationFrame(render);
  }

+ function rgbToInt(r, g, b) {
+     return b + g * 255 + r * 255 ** 2;
+ }
+ 
  document.body.addEventListener('click', (e) => {
      coloredCubesRenderBuffer.bind(gl);


Enter fullscreen mode Exit fullscreen mode

Let's drop camera rotation code to make scene static

📄 src/minecraft.js

  function render() {
      offscreenRenderBuffer.clear(gl);

-     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [0, 0, -30]);
-     mat4.rotateY(cameraFocusPointMatrix, cameraFocusPointMatrix, Math.PI / 360);
-     mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [0, 0, 30]);
- 
-     mat4.getTranslation(cameraFocusPoint, cameraFocusPointMatrix);
- 
      mat4.lookAt(viewMatrix, cameraPosition, cameraFocusPoint, [0, 1, 0]);

      renderSkybox(gl, viewMatrix, projectionMatrix);
      const g = pixels[pixelIndex + 1];
      const b = pixels[pixelIndex + 2];
      const a = pixels[pixelIndex + 3];
+ 
+     const index = rgbToInt(r, g, b);
+ 
+     console.log(index);
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

and update initial camera position to see the scene better

📄 src/minecraft.js


  gl.viewport(0, 0, canvas.width, canvas.height);

- const cameraPosition = [0, 5, 0];
- const cameraFocusPoint = vec3.fromValues(0, 0, 30);
+ const cameraPosition = [0, 10, 0];
+ const cameraFocusPoint = vec3.fromValues(30, 0, 30);
  const cameraFocusPointMatrix = mat4.create();

  mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);

Enter fullscreen mode Exit fullscreen mode

Next let's pass selected color index into vertex shader as varying

📄 src/shaders/3d-textured.v.glsl


  uniform mat4 viewMatrix;
  uniform mat4 projectionMatrix;
+ uniform float selectedObjectIndex;

  varying vec2 vTexCoord;
  varying vec3 vColor;

Enter fullscreen mode Exit fullscreen mode

And multiply object color if its index matches selected object index

📄 src/shaders/3d-textured.f.glsl

  varying vec3 vColor;

  uniform float renderIndices;
+ varying vec4 vColorMultiplier;

  void main() {
-     gl_FragColor = texture2D(texture, vTexCoord * vec2(1, -1) + vec2(0, 1));
+     gl_FragColor = texture2D(texture, vTexCoord * vec2(1, -1) + vec2(0, 1)) * vColorMultiplier;

      if (renderIndices == 1.0) {
          gl_FragColor.rgb = vColor;

Enter fullscreen mode Exit fullscreen mode

📄 src/shaders/3d-textured.v.glsl


  varying vec2 vTexCoord;
  varying vec3 vColor;
+ varying vec4 vColorMultiplier;

  vec3 encodeObject(float id) {
      int b = int(mod(id, 255.0));

      vTexCoord = texCoord;
      vColor = encodeObject(index);
+     
+     if (selectedObjectIndex == index) {
+         vColorMultiplier = vec4(1.5, 1.5, 1.5, 1.0);
+     } else {
+         vColorMultiplier = vec4(1.0, 1.0, 1.0, 1.0);
+     }
  }

Enter fullscreen mode Exit fullscreen mode

and reflect shader changes in js

📄 src/minecraft-terrain.js

      State.ext.vertexAttribDivisorANGLE(State.programInfo.attributeLocations.index, 0);
  }

- export function render(gl, viewMatrix, projectionMatrix, renderIndices) {
+ export function render(gl, viewMatrix, projectionMatrix, renderIndices, selectedObjectIndex) {
      gl.useProgram(State.program);

      setupAttributes(gl);
      gl.uniformMatrix4fv(State.programInfo.uniformLocations.viewMatrix, false, viewMatrix);
      gl.uniformMatrix4fv(State.programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);

+     gl.uniform1f(State.programInfo.uniformLocations.selectedObjectIndex, selectedObjectIndex);
+ 
      if (renderIndices) {
          gl.uniform1f(State.programInfo.uniformLocations.renderIndices, 1);
      } else {

Enter fullscreen mode Exit fullscreen mode

📄 src/minecraft.js


  gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);

+ let selectedObjectIndex = -1;
+ 
  function render() {
      offscreenRenderBuffer.clear(gl);

      mat4.lookAt(viewMatrix, cameraPosition, cameraFocusPoint, [0, 1, 0]);

      renderSkybox(gl, viewMatrix, projectionMatrix);
-     renderTerrain(gl, viewMatrix, projectionMatrix);
+     renderTerrain(gl, viewMatrix, projectionMatrix, false, selectedObjectIndex);

      gl.useProgram(program);


      const index = rgbToInt(r, g, b);

-     console.log(index);
+     selectedObjectIndex = index;
  });

  (async () => {

Enter fullscreen mode Exit fullscreen mode

That's it! We now know selected object index, so that we can perform JS operations as well as visual feedback!

Thanks for reading!


GitHub stars
Twitter Follow

Join mailing list to get new posts right to your inbox

Source code available here

Built with

Git Tutor Logo

Top comments (0)