DEV Community

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

Posted on

1 1

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

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay