Měsíc WebGL. Den 27. Detekce kliknutí. Část I

Toto je série blogových příspěvků souvisejících s WebGL. Nový příspěvek bude k dispozici každý den


Připojte se do seznamu adresátů a získejte nové příspěvky přímo do vaší doručené pošty

Zdrojový kód je k dispozici zde

Postaveno s

Ahoj 👋

Včera jsme se naučili renderovat do textury. To je pěkná schopnost udělat nějaké pěkné efekty poté, co byla scéna kompletně vykreslena, ale můžeme využít vykreslování mimo obrazovku pro něco jiného.

Jednou z důležitých věcí v interaktivním 3D je detekce kliknutí. I když to může být provedeno pomocí javascriptu, vyžaduje to složitou matematiku. Místo toho můžeme:

  • každému objektu přiřadit jedinečnou plnou barvu
  • vykreslit scénu do textury
  • číst barvu pixelu pod kurzorem
  • sladit barvu s objektem

Protože budeme potřebovat další framebuffer, vytvoříme pomocnou třídu

📄 src/RenderBuffer.js

export class RenderBuffer {
    constructor(gl) {
        this.framebuffer = gl.createFramebuffer();
        this.texture = gl.createTexture();
    }
}

Nastavte framebuffer a texturu barev

📄 src/RenderBuffer.js

      constructor(gl) {
          this.framebuffer = gl.createFramebuffer();
          this.texture = gl.createTexture();
+ 
+         gl.bindTexture(gl.TEXTURE_2D, this.texture);
+         gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+ 
+         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ 
+         gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
+         gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
      }
  }

Nastavení vyrovnávací paměti hloubky

📄 src/RenderBuffer.js


          gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
          gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
+ 
+         this.depthBuffer = gl.createRenderbuffer();
+         gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer);
+ 
+         gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, gl.canvas.width, gl.canvas.height);
+         gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer);
      }
  }

Implementujte metodu vazby

📄 src/RenderBuffer.js

          gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, gl.canvas.width, gl.canvas.height);
          gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer);
      }
+ 
+     bind(gl) {
+         gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
+     }
  }

a jasné

📄 src/RenderBuffer.js

      bind(gl) {
          gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
      }
+ 
+     clear(gl) {
+         this.bind(gl);
+         gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+     }
  }

Použijte novou pomocnou třídu

📄 src/minecraft.js

  import { setupShaderInput, compileShader } from './gl-helpers';
  import { GLBuffer } from './GLBuffer';
  import { createRect } from './shape-helpers';
+ import { RenderBuffer } from './RenderBuffer';

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');

  mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);

- const framebuffer = gl.createFramebuffer();
- 
- const texture = gl.createTexture();
- 
- gl.bindTexture(gl.TEXTURE_2D, texture);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
- 
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- 
- gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
- 
- const depthBuffer = gl.createRenderbuffer();
- gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
- 
- gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, canvas.width, canvas.height);
- gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
+ const offscreenRenderBuffer = new RenderBuffer(gl);

  const vShader = gl.createShader(gl.VERTEX_SHADER);
  const fShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);

  function render() {
-     gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
- 
-     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+     offscreenRenderBuffer.clear(gl);

      mat4.translate(cameraFocusPointMatrix, cameraFocusPointMatrix, [0, 0, -30]);
      mat4.rotateY(cameraFocusPointMatrix, cameraFocusPointMatrix, Math.PI / 360);
      gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);

      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
-     gl.bindTexture(gl.TEXTURE_2D, texture);
+     gl.bindTexture(gl.TEXTURE_2D, offscreenRenderBuffer.texture);

      gl.drawElements(gl.TRIANGLES, indexBuffer.data.length, gl.UNSIGNED_BYTE, 0);


Místo předávání celé jedinečné barvy objektu, což je vec3, můžeme předat pouze index objektu

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

  attribute vec3 position;
  attribute vec2 texCoord;
  attribute mat4 modelMatrix;
+ attribute float index;

  uniform mat4 viewMatrix;
  uniform mat4 projectionMatrix;

a převést tuto plovoucí barvu na barvu přímo v shaderu

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


  varying vec2 vTexCoord;

+ vec3 encodeObject(float id) {
+     int b = int(mod(id, 255.0));
+     int r = int(id) / 255 / 255;
+     int g = (int(id) - b - r * 255 * 255) / 255;
+     return vec3(r, g, b) / 255.0;
+ }
+ 
  void main() {
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);


Nyní potřebujeme předat barvu do shaderu fragmentů pomocí variování

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

  uniform sampler2D texture;

  varying vec2 vTexCoord;
+ varying vec3 vColor;

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

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

  uniform mat4 projectionMatrix;

  varying vec2 vTexCoord;
+ varying vec3 vColor;

  vec3 encodeObject(float id) {
      int b = int(mod(id, 255.0));
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

      vTexCoord = texCoord;
+     vColor = encodeObject(index);
  }

Musíme také určit, co chceme vykreslit:texturovaný objekt nebo barevný, takže pro to použijeme uniformu

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

  varying vec2 vTexCoord;
  varying vec3 vColor;

+ uniform float renderIndices;
+ 
  void main() {
      gl_FragColor = texture2D(texture, vTexCoord * vec2(1, -1) + vec2(0, 1));
+ 
+     if (renderIndices == 1.0) {
+         gl_FragColor.rgb = vColor;
+     }
  }

Nyní vytvoříme pole indexů

📄 src/minecraft-terrain.js

      State.modelMatrix = mat4.create();
      State.rotationMatrix = mat4.create();

+     const indices = new Float32Array(100 * 100);
+ 
      let cubeIndex = 0;

      for (let i = -50; i < 50; i++) {

Vyplňte jej daty a nastavte GLBuffer

📄 src/minecraft-terrain.js

                  matrices[cubeIndex * 4 * 4 + index] = value;
              });

+             indices[cubeIndex] = cubeIndex;
+ 
              cubeIndex++;
          }
      }

      State.matricesBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, matrices, gl.STATIC_DRAW);
+     State.indexBuffer = new GLBuffer(gl, gl.ARRAY_BUFFER, indices, gl.STATIC_DRAW);

      State.offset = 4 * 4; // 4 floats 4 bytes each
      State.stride = State.offset * 4; // 4 rows of 4 floats

Protože máme nový atribut, musíme aktualizovat funkce setupAttribute a resetDivisorAngles

📄 src/minecraft-terrain.js


          State.ext.vertexAttribDivisorANGLE(State.programInfo.attributeLocations.modelMatrix + i, 1);
      }
+ 
+     State.indexBuffer.bind(gl);
+     gl.vertexAttribPointer(State.programInfo.attributeLocations.index, 1, gl.FLOAT, false, 0, 0);
+     State.ext.vertexAttribDivisorANGLE(State.programInfo.attributeLocations.index, 1);
  }

  function resetDivisorAngles() {
      for (let i = 0; i < 4; i++) {
          State.ext.vertexAttribDivisorANGLE(State.programInfo.attributeLocations.modelMatrix + i, 0);
      }
+ 
+     State.ext.vertexAttribDivisorANGLE(State.programInfo.attributeLocations.index, 0);
  }

  export function render(gl, viewMatrix, projectionMatrix) {

A nakonec potřebujeme další argument vykreslovací funkce, abychom rozlišili mezi "režimy vykreslení" (buď texturované kostky nebo barevné)

📄 src/minecraft-terrain.js

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

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

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

+     if (renderIndices) {
+         gl.uniform1f(State.programInfo.uniformLocations.renderIndices, 1);
+     } else {
+         gl.uniform1f(State.programInfo.uniformLocations.renderIndices, 0);
+     }
+ 
      State.ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, State.vertexBuffer.data.length / 3, 100 * 100);

      resetDivisorAngles();

Nyní potřebujeme další vyrovnávací paměť pro vykreslení barevných kostek do

📄 src/minecraft.js

  mat4.fromTranslation(cameraFocusPointMatrix, cameraFocusPoint);

  const offscreenRenderBuffer = new RenderBuffer(gl);
+ const coloredCubesRenderBuffer = new RenderBuffer(gl);

  const vShader = gl.createShader(gl.VERTEX_SHADER);
  const fShader = gl.createShader(gl.FRAGMENT_SHADER);

Nyní přidáme posluchače kliknutí

📄 src/minecraft.js

      requestAnimationFrame(render);
  }

+ document.body.addEventListener('click', () => {
+     coloredCubesRenderBuffer.bind(gl);
+ });
+ 
  (async () => {
      await prepareSkybox(gl);
      await prepareTerrain(gl);

a vykreslit barevné kostky do textury pokaždé, když uživatel klikne na plátno

📄 src/minecraft.js


  document.body.addEventListener('click', () => {
      coloredCubesRenderBuffer.bind(gl);
+ 
+     renderTerrain(gl, viewMatrix, projectionMatrix, true);
  });

  (async () => {

Nyní potřebujeme úložiště pro čtení barev pixelů do

📄 src/minecraft.js

      coloredCubesRenderBuffer.bind(gl);

      renderTerrain(gl, viewMatrix, projectionMatrix, true);
+ 
+     const pixels = new Uint8Array(canvas.width * canvas.height * 4);
  });

  (async () => {

a skutečně číst barvy pixelů

📄 src/minecraft.js

      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);
  });

  (async () => {

To je vše, nyní máme celou scénu vykreslenou do textury mimo obrazovku, kde má každý objekt jedinečnou barvu. V detekci kliknutí budeme pokračovat zítra

Děkuji za přečtení! 👋


Připojte se do seznamu adresátů a získejte nové příspěvky přímo do vaší doručené pošty

Zdrojový kód je k dispozici zde

Postaveno s