WebGL学习(九)选中物体

56 阅读8分钟

1. 选中物体

这里的思路很简单:

  1. 用户点击canvas
  2. 将物体设置为单一颜色,比如红色
  3. 读取点击位置的像素颜色
  4. 如果是红色就是点击到了物体,否则就不是

1.1. 点击canvas物体变红

// 顶点着色器
// 代码是根据前面改的,主要看判断
uniform bool clicked;
void main(){
  // 绘制立方体顶点
  gl_Position = mvpMat * pos;
  gl_PointSize = 10.0;
  // 计算顶点世界坐标
  _vertexPosition = modelMat * pos;
  // 如果点击了物体就变红
  if(bool(clicked)) {
    _color = vec4(1.0, 0.0, 0.0, 1.0);
  }else {
    _color = color;
  }
  _originalNormal = originalNormal;
}

const clicked = gl.getUniformLocation(program, 'clicked')
canvas.addEventListener('click', function(e){
  const {
    height,
    left,
    right,
    top,
    bottom,
    width
  } = this.getBoundingClientRect()
  const {
    clientX,
    clientY
  } = e
  // 点在canvas内部
  if(clientX >= left && clientX <= right && clientY <= bottom && clientY >= top) {
     // 不能传递true或者false
     // 只能间接传递
    gl.uniform1i(clicked, 1)
  }else {
    gl.uniform1i(clicked, 0)
  }
  draw()
})

msedge_SmhLIsld6j.gif

1.2. 判断点击物体

这里要注意坐标,html中原点在左上角。

document.addEventListener('click', function(e){
  const {
    height,
    left,
    right,
    top,
    bottom,
    width
  } = canvas.getBoundingClientRect()
  const {
    clientX,
    clientY
  } = e
  // 点在canvas内部
  if(clientX >= left && clientX <= right && clientY <= bottom && clientY >= top) {
    // clicked标志置为1
    gl.uniform1i(clicked, 1)
    draw()
    // 将浏览器坐标转化为canvas上的坐标
    const canvasX = clientX - left
    const canvasY = clientY - top
    // 使用一个长度为4的8位无符号整形数组接收像素值
    const res = new Uint8Array(4)
    // 获取像素值
    gl.readPixels(canvasX, canvasY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, res)
    // 如果r分量不是102,102是背景的颜色
    if(res[0] !== 102) {
      alert('点击了物体')
    }
  }
  // 复原颜色
  gl.uniform1i(clicked, 0)
  draw()
})

msedge_Vd32tE8z0R.gif 可以发现没有在正确的位置触发弹窗,看起来好像是坐标相对于正确的位置偏移了,这是为什么呢?

这是因为canvas的设置问题,我的设置是这样

<canvas
    id="webgl"
    width="1200"
    height="1200"
    style="width: 600px; height: 600px"
></canvas>

画布实际宽高是1200canvas元素的宽高是600,这样做相当于dpr = 2,会让图像更清晰。 同时也会导致基于元素宽高的计算会出现问题,解决方法很简单,就是把比例考虑进去。

    // 计算缩放比
    const sacle = canvas.width / width
    gl.readPixels(canvasX * sacle, canvasY * sacle, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, res)

msedge_NOBlRP1mfA.gif

还有其他一些方法出几种方法:

  • 使用颜色编码:给每个绘制的图像分配一个唯一的颜色,并在另一个不可见的canvas上绘制它们。当用户点击时,获取点击位置的像素颜色,并与颜色编码表进行匹配,找到对应的图像。
  • 使用射线拾取:根据用户点击的屏幕坐标,计算出一个从摄像机位置出发的射线,并与绘制的图像的顶点坐标进行相交测试,找到最近的相交点所属的图像。
  • 使用深度缓冲区:在绘制图像时,记录每个像素的深度值,并存储在一个不可见的缓冲区中。当用户点击时,获取点击位置的深度值,并与绘制的图像的深度值进行比较,找到最近的图像。

上面代码使用了readPixels函数,具体作用就是获取指定宽高矩形区域的像素值。 image.png

2. 选中物体表面

思路:

  1. 使用一个缓冲区face存储每个顶点是在哪一个面
  2. 点击画面时将face的值存入对应顶点的像素颜色值的α分量中
  3. 读取点击位置的像素值,取出α分量的值就是表面位置
  4. 将点击的表面值设置到顶点变量中,使对应的面的顶点颜色变为激活的颜色

2.1. 第一步,创建face缓冲区

// 顶点着色器
// 先不用管,后面解释
// ....
attribute float faceIndex;
void main(){
    // ....
}

  // Create a cube
  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3

var vertices = new Float32Array([   // Vertex coordinates
     1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,    // v0-v1-v2-v3 front
     1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,    // v0-v3-v4-v5 right
     1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,    // v0-v5-v6-v1 up
    -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,    // v1-v6-v7-v2 left
    -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,    // v7-v4-v3-v2 down
     1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0     // v4-v7-v6-v5 back
  ]);
// 一个顶点坐标对应一个face
var faces = new Uint8Array([   // Faces
    1, 1, 1, 1,     // v0-v1-v2-v3 front
    2, 2, 2, 2,     // v0-v3-v4-v5 right
    3, 3, 3, 3,     // v0-v5-v6-v1 up
    4, 4, 4, 4,     // v1-v6-v7-v2 left
    5, 5, 5, 5,     // v7-v4-v3-v2 down
    6, 6, 6, 6,     // v4-v7-v6-v5 back
  ]);
  
// 绑定缓冲区
const faceIndex = gl.getAttribLocation(program, 'faceIndex')
var faceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, faceBuffer)
gl.bufferData(gl.ARRAY_BUFFER, faces, gl.STATIC_DRAW)
gl.vertexAttribPointer(faceIndex, 1, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(faceIndex)

2.2. 第二步,存储face

再次回顾一下步骤:

  1. 点击canvas后将face值写入α通道
  2. 读取点击位置的像素值
  3. 像素值的第四位就是点击的哪一个面
  4. 将读取出来的face值设置给顶点变量
  5. face对应的顶点设置成高亮色

所以我们需要设置一个顶点变量clickedFace,来存储点击的面。

// 顶点着色器
// ....
attribute float faceIndex;
uniform int clickedFace;
void main(){
// ...
  // clickedFace == 0代表初始化
  // 此时将faceIndex中的值写入对应顶点的颜色α分量
  if(clickedFace == 0) {
    _color = vec4(color.rgb, faceIndex / 255.0);
  }else {
  // 这里暂不进行下一步
  // 保持原样
    _color = color;
  }
}
// 主程序
// ....
const clickedFace = gl.getUniformLocation(program, 'clickedFace')
// 初始化clickedFace为-1,绘制的图像保持原样
gl.uniform1i(clickedFace, -1)
draw();
document.addEventListener('click', function(e){
  const {
    height,
    left,
    right,
    top,
    bottom,
    width
  } = canvas.getBoundingClientRect()
  const {
    clientX,
    clientY
  } = e
  // 点在canvas内部
  if(clientX >= left && clientX <= right && clientY <= bottom && clientY >= top) {
    // 设置clickedFace=0,将face值存入α分量
    gl.uniform1i(clickedFace, 0)
    draw()
    const canvasX = clientX - left
    const canvasY = clientY - top
    const sacle = canvas.width / width
    const pixel = new Uint8Array(4)
    // 读取点击点的像素值
    gl.readPixels(canvasX * sacle, canvasY * sacle, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel)
  }
})

这里注意几点:

  1. 为什么一定要 clickedFace = 0时初始化,而不是顶点着色器main函数一执行就初始化?

如果一开始就初始化,会导致原本颜色的α分量值不对。

  1. 顶点着色器中_color = vec4(color.rgb, faceIndex / 255.0),为什么faceIndex / 255
  1. 颜色值范围被归一化为(0, 1),对应无符号字节类型就是(0, 255)
  2. 所以readPixels的输出相当于乘以了255
  3. 为了能正确显示

2.3. 对比点击处的α值和faceIndex

// 顶点着色器
// ....
if(clickedFace == 0) {
    _color = vec4(color.rgb, faceIndex / 255.0);
}else {
    if(clickedFace == int(faceIndex)) {
      // 如果点击处的α值等于这个顶点(面)对应的faceIndex
      // 变成紫红色
      _color = vec4(1, 0.5, 1, color.a);
    }else {
      _color = color;
    }
}

2.4.完整代码

这里的代码是接着前面光照 的代码写的,可以忽略光照的代码

// 顶点着色器
// mvp矩阵
uniform mat4 mvpMat;
// model矩阵
uniform mat4 modelMat;
// 顶点位置
attribute vec4 pos;
// 顶点颜色
attribute vec4 color;
// 初始位置法线向量
attribute vec4 originalNormal;

varying vec4 _color;
varying vec4 _originalNormal;
varying vec4 _vertexPosition;

attribute float faceIndex;
uniform int clickedFace;

void main(){
  // 绘制立方体顶点
  gl_Position = mvpMat * pos;
  gl_PointSize = 10.0;
  // 计算顶点世界坐标
  _vertexPosition = modelMat * pos;
  if(clickedFace == 0) {
    _color = vec4(color.rgb, faceIndex / 255.0);
  }else {
    if(clickedFace == int(faceIndex)) {
      _color = vec4(1, 0.5, 1, color.a);
    }else {
      _color = color;
    }
  }

  // 法向量是存储在顶点缓冲区的,所以只能间接传递给片元着色器
  _originalNormal = originalNormal;
}

// 片元着色器 没有改动
precision mediump float;
// 光源颜色
uniform vec3 lightColor;
// 光源方向
uniform vec3 lightDirection;
// 环境光颜色
uniform vec3 ambientColor;
// 法向量变换矩阵 也就是逆转置矩阵
uniform mat4 normalMat;
varying vec4 _color;
varying vec4 _originalNormal;
varying vec4 _vertexPosition;

void main(){
  // 计算该顶点的光线方向
  vec3 vertexLightDirection = normalize(lightDirection - vec3(_vertexPosition));
  // 计算变换后的法向量
  vec3 normal = normalize(vec3(normalMat * _originalNormal));
  // 计算反射颜色
  float cos = max(dot(vertexLightDirection, normal), 0.0);
  vec3 diffuse = lightColor * _color.rgb * cos;
  // 计算环境反射光
  vec3 ambient = ambientColor * _color.rgb;

  gl_FragColor = vec4(diffuse + ambient,  _color.a);
}

import vertexCode from './vertex.vert'
import fragmentCode from './fragment.frag'
import { mat4 } from 'gl-matrix'
import { getModelMat } from './utils'
const canvas = document.getElementById('webgl') as HTMLCanvasElement
const output = document.getElementById('output') as HTMLDivElement
// 获取webgl上下文
const gl = canvas.getContext('webgl')
// 1.创建顶点、片元着色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
// 2.装载着色器代码
gl.shaderSource(vertexShader, vertexCode)
gl.shaderSource(fragmentShader, fragmentCode)
// 3.编译着色器
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
// 4.创建WebGLProgram对象
const program = gl.createProgram();
// 5.添加着色器
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
// 6.链接程序
gl.linkProgram(program);
// 7.WebGLProgram对象添加到渲染状态中
gl.useProgram(program);

gl.clearColor(0.4, 0.4, 0.4, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.TRIANGLES, 0, 9)
const pos = gl.getAttribLocation(program, 'pos')
const mvpMatrix = gl.getUniformLocation(program, 'mvpMat')
const color = gl.getAttribLocation(program, 'color')
const originalNormal = gl.getAttribLocation(program, 'originalNormal')
var vertexBuffer = gl.createBuffer();
var colorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
var normalBuffer = gl.createBuffer();
// Create a cube
//    v6----- v5
//   /|      /|
//  v1------v0|
//  | |     | |
//  | |v7---|-|v4
//  |/      |/
//  v2------v3
// 每个面都单独定义了一组顶点
const vertex = new Float32Array([
  1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,    // v0-v1-v2-v3 front
  1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,    // v0-v3-v4-v5 right
  1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,    // v0-v5-v6-v1 up
 -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,    // v1-v6-v7-v2 left
 -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,    // v7-v4-v3-v2 down
  1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0     // v4-v7-v6-v5 back
])
// 分离颜色参数
var colors = new Float32Array([
  0.4, 0.4, 1.0,   0.4, 0.4, 1.0,   0.4, 0.4, 1.0,   0.4, 0.4, 1.0,  // v0-v1-v2-v3 前(blue)
  0.4, 1.0, 0.4,   0.4, 1.0, 0.4,   0.4, 1.0, 0.4,   0.4, 1.0, 0.4,  // v0-v3-v4-v5 右(green)
  1.0, 0.4, 0.4,   1.0, 0.4, 0.4,   1.0, 0.4, 0.4,   1.0, 0.4, 0.4,  // v0-v5-v6-v1 上(red)
  1.0, 1.0, 0.4,   1.0, 1.0, 0.4,   1.0, 1.0, 0.4,   1.0, 1.0, 0.4,  // v1-v6-v7-v2 左
  1.0, 1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, 1.0,  // v7-v4-v3-v2 下
  0.4, 1.0, 1.0,   0.4, 1.0, 1.0,   0.4, 1.0, 1.0,   0.4, 1.0, 1.0   // v4-v7-v6-v5 后
]);

// 法线向量
var normals = new Float32Array([
  0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,  // v0-v1-v2-v3 front
  1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,  // v0-v3-v4-v5 right
  0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,  // v0-v5-v6-v1 up
  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  // v1-v6-v7-v2 left
  0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,  // v7-v4-v3-v2 down
  0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0   // v4-v7-v6-v5 back
]);
var faces = new Float32Array([   // Faces
1, 1, 1, 1,     // v0-v1-v2-v3 front
2, 2, 2, 2,     // v0-v3-v4-v5 right
3, 3, 3, 3,     // v0-v5-v6-v1 up
4, 4, 4, 4,     // v1-v6-v7-v2 left
5, 5, 5, 5,     // v7-v4-v3-v2 down
6, 6, 6, 6,     // v4-v7-v6-v5 back
]);
var indices = new Uint8Array([
  0, 1, 2,      0, 2, 3,   // 前
  4, 5, 6,      4, 6, 7,   // 右
  8, 9, 10,     8, 10, 11,   // 上
  12, 13, 14,   12, 14, 15,   // 左
  16, 17, 18,   16, 18, 19,   // 下
  20, 21, 22,   20, 22, 23    // 后
]);

// 绑定顶点缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
gl.bufferData(gl.ARRAY_BUFFER, vertex, gl.STATIC_DRAW)
gl.vertexAttribPointer(pos, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(pos)

// 绑定颜色缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)
gl.vertexAttribPointer(color, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(color)

// 绑定法线向量缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer)
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW)
gl.vertexAttribPointer(originalNormal, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(originalNormal)

// 绑定面
const faceIndex = gl.getAttribLocation(program, 'faceIndex')
var faceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, faceBuffer)
gl.bufferData(gl.ARRAY_BUFFER, faces, gl.STATIC_DRAW)
gl.vertexAttribPointer(faceIndex, 1, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(faceIndex)
// 绑定索引
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);


// 设置光线
const lightColor = gl.getUniformLocation(program, 'lightColor')
const lightDirection = gl.getUniformLocation(program, 'lightDirection')
gl.uniform3fv(lightColor, [1, 1, 1])
// 光线方向
let lightPos = [ -4, 4, 1]
gl.uniform3fv(lightDirection, lightPos)

// 设置环境光颜色
const ambientColor = gl.getUniformLocation(program, 'ambientColor')
gl.uniform3fv(ambientColor, [0.2, 0.2, 0.2])

// 绘制立方体
let fov = 10 * Math.PI / 180
let aspect = canvas.width / canvas.height
let near = 1
let far = 100

gl.enable(gl.DEPTH_TEST)

const normalMatrix = gl.getUniformLocation(program, 'normalMat')
const modelMatrix = gl.getUniformLocation(program, 'modelMat')
let modelMat: mat4 =  getModelMat([0, 0, 0], [30, 30, 0])

const drawBox = (modelMat: mat4) => {
  const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
  const _viewMat = mat4.lookAt(mat4.create(), [0, 0, 30], [0, 0, 0], [0, 1, 0])
  let mvpMat = mat4.multiply(mat4.create(), perspectiveMat, _viewMat)
  mvpMat = mat4.multiply(mat4.create(), mvpMat, modelMat)
  // modelMat的逆转置
  gl.uniformMatrix4fv(normalMatrix, false, mat4.transpose(mat4.create(), mat4.invert(mat4.create(), modelMat)))
  gl.uniformMatrix4fv(modelMatrix, false, modelMat)
  gl.uniformMatrix4fv(mvpMatrix, false, mvpMat)
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
}
const draw = () => {
  gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
  gl.clearColor(0.4, 0.4, 0.4, 1)
  drawBox(modelMat)
}

const clickedFace = gl.getUniformLocation(program, 'clickedFace')
gl.uniform1i(clickedFace, -1)
draw();

document.addEventListener('click', function(e){
  const {
    height,
    left,
    right,
    top,
    bottom,
    width
  } = canvas.getBoundingClientRect()
  const {
    clientX,
    clientY
  } = e
  // 点在canvas内部
  if(clientX >= left && clientX <= right && clientY <= bottom && clientY >= top) {
    // 初始化,用α通道存储faceIndex
    gl.uniform1i(clickedFace, 0)
    draw()
    const canvasX = clientX - left
    const canvasY = clientY - top
    const sacle = canvas.width / width
    const pixel = new Uint8Array(4)
    gl.readPixels(canvasX * sacle, canvasY * sacle, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel)
    // 设置被点击的面
    gl.uniform1i(clickedFace, pixel[3])
  }
  draw()
})

效果:

LenovoPcManager_E9zxlz5CQy.gif 好像正确了,但是点击1面下半部分的时候,显示的却是点击的3.

原因是因为我们将鼠标点击位置转化为readPixel的前两个参数时有问题。

根据opengl对于readPixel规定readPixel是从当前颜色缓冲区的左下角开始获取的(因为纹理坐标通常是反过来的),可以复习一下

所以,我们转化坐标的时候一定得注意,只需要改动这一句

-const canvasY = clientY - top
+const canvasY = bottom - clientY

现在就可以看看效果了