WebGL 抓取

您所在的位置:网站首页 canvas改变尺寸 WebGL 抓取

WebGL 抓取

2023-09-27 05:43| 来源: 网络整理| 查看: 265

这篇文章是关于如何使用 WebGL 来让用户抓取或选择对象。

如果你读过本网站的其他文章,你可能已经意识到 WebGL 本身只是一个栅格化库。它在画布上绘制三角形、直线和点。 它在画布上绘制三角形、线和点,所以它没有"选择对象"的概念,它只是通过你提供的着色器输出像素。 这意味着任何"抓取"对象的概念都必须来自你的代码,你需要自行定义你让用户选择对象的形式。 这也意味着虽然这篇文章可以覆盖(WebGL抓取的)常用概念,但你需要自己决定如何将你在这里看到的东西转化为你自己应用中可用的程序。

点击一个物体

关于找到用户点击的物体,一个最简单的方法是:为每一个对象赋予一个数字id,我们可以在关闭光照和纹理的情况下将数字id当作颜色绘制所有对象。 随后我们将得到一帧图片,上面绘制了所有物体的剪影,而深度缓冲会自动帮我们排序。 我们可以读取鼠标坐标下的像素颜色为数字id,就能得到这个位置上渲染的对应物体。

为了实现这一技术,我们需要结合以前的几篇文章。 第一篇是关于绘制多个物体, 参考它的内容,我们可以绘制多个物体并尝试抓取。

此外,我们需要在屏幕外渲染这些id,渲染到纹理 中的代码也将添加进来。

那么,让我们参考上个案例,在多物体绘制中绘制了200个物体。

同时,让我们为它添加一个带有纹理和深度缓冲器的帧缓冲器,参考渲染到纹理.

// 创建一个纹理对象作为渲染目标 const targetTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, targetTexture); 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); // 创建一个深度缓冲 const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); function setFramebufferAttachmentSizes(width, height) { gl.bindTexture(gl.TEXTURE_2D, targetTexture); // 定义 0 级贴图的尺寸和格式 const level = 0; const internalFormat = gl.RGBA; const border = 0; const format = gl.RGBA; const type = gl.UNSIGNED_BYTE; const data = null; gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, format, type, data); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); } // 创建并绑定帧缓冲 const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); // 绑定纹理作为一个颜色附件 const attachmentPoint = gl.COLOR_ATTACHMENT0; const level = 0; gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level); // 创建一个和渲染目标储存相同的深度缓冲 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);

我们将纹理和深度渲染缓冲区配置代码放到一个函数中,通过调用它来调整它们的尺寸,使之与画布的大小一致。

在我们的代码里,如果Canvas改变尺寸,我们将调整纹理和渲染缓冲区以匹配它。

function drawScene(time) { time *= 0.0005; - webglUtils.resizeCanvasToDisplaySize(gl.canvas); + if (webglUtils.resizeCanvasToDisplaySize(gl.canvas)) { + // 当canvas改变尺寸后,同步帧缓冲的尺寸 + setFramebufferAttachmentSizes(gl.canvas.width, gl.canvas.height); + } ...

接下来我们需要第二个着色器。例子中的着色器使用顶点颜色,但这个案例中我们的着色器要可以通过id设置固定颜色。 所以以下是我们的第二个着色器。

attribute vec4 a_position; uniform mat4 u_matrix; void main() { // 顶点坐标与矩阵相乘 gl_Position = u_matrix * a_position; } precision mediump float; uniform vec4 u_id; void main() { gl_FragColor = u_id; }

然后我们需要编译, 链接和查找着色器指向,参考建议.

// 设置 GLSL 程序 const programInfo = webglUtils.createProgramInfo( gl, ["3d-vertex-shader", "3d-fragment-shader"]); +const pickingProgramInfo = webglUtils.createProgramInfo( + gl, ["pick-vertex-shader", "pick-fragment-shader"]);

我们需要实现渲染所有的对象两次。一次是用我们分配给它们的着色器,第二次用我们上面写的着色器渲染。 所以我们把目前渲染所有物体的代码提取到一个函数中。

function drawObjects(objectsToDraw, overrideProgramInfo) { objectsToDraw.forEach(function(object) { const programInfo = overrideProgramInfo || object.programInfo; const bufferInfo = object.bufferInfo; gl.useProgram(programInfo.program); // 设置所有需要的 attributes webglUtils.setBuffersAndAttributes(gl, programInfo, bufferInfo); // 设置 uniforms. webglUtils.setUniforms(programInfo, object.uniforms); // 绘制图形 gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements); }); }

drawObjects 中的可选参数 overrideProgramInfo 让我们得以传入指定的着色器而不是对象自带的着色器。

我们要调用它两次,一次通过物体id绘制到纹理上,第二次绘制场景到画布上。

// 绘制场景. function drawScene(time) { time *= 0.0005; ... // 计算所有对象的矩阵. objects.forEach(function(object) { object.uniforms.u_matrix = computeMatrix( viewProjectionMatrix, object.translation, object.xRotationSpeed * time, object.yRotationSpeed * time); }); + // ------ 将对象绘制到纹理 -------- + + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + // 清空画布和深度缓冲. + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + drawObjects(objectsToDraw, pickingProgramInfo); + + // ------ 将对象绘制到画布 + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + drawObjects(objectsToDraw); requestAnimationFrame(drawScene); }

我们指定的着色器需要使用 u_id 来设置物体 id ,我们在设置物体时将它添加到uniforms数据中.

// 配置每个对象的信息 const baseHue = rand(0, 360); const numObjects = 200; for (let ii = 0; ii < numObjects; ++ii) { + const id = ii + 1; const object = { uniforms: { u_colorMult: chroma.hsv(eMod(baseHue + rand(0, 120), 360), rand(0.5, 1), rand(0.5, 1)).gl(), u_matrix: m4.identity(), + u_id: [ + ((id >> 0) & 0xFF) / 0xFF, + ((id >> 8) & 0xFF) / 0xFF, + ((id >> 16) & 0xFF) / 0xFF, + ((id >> 24) & 0xFF) / 0xFF, + ], }, translation: [rand(-100, 100), rand(-100, 100), rand(-150, -50)], xRotationSpeed: rand(0.8, 1.2), yRotationSpeed: rand(0.8, 1.2), }; objects.push(object); objectsToDraw.push({ programInfo: programInfo, bufferInfo: shapes[ii % shapes.length], uniforms: object.uniforms, }); }

以上代码通过我们的实用工具来处理uniforms调用。

由于我们的目标纹理类型是 gl.RGBA, gl.UNSIGNED_BYTE,这里必须把id分解为 R, G, B, A 四个通道,每个通道容量为8bit。 8bit意味着只能容纳256个值,但是当我们将id分解为4通道,就拥有了32bit总容量,这对应着40亿以上个值。

我们把id + 1是因为在这里我们使用0代表“指针下没有东西”。

现在让我们高亮指针下的物体。

首先我们需要获取画布下的指针坐标。

// mouseX 和 mouseY 是CSS显示空间下画布中指针的相对位置 let mouseX = -1; let mouseY = -1; ... gl.canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = e.clientY - rect.top; });

需要注意的是,上面代码中的 mouseX 和 mouseY 将返回CSS显示空间的像素位置。 这意味着他们在画布显示空间中,而不是画布渲染的像素空间。 换句话说,如果你有这样一个画布

指针穿过画布时 mouseX 将从0变化到33,而 mouseY 将从0变化到44。查看这个以获得更多信息。

现在我们有了指针坐标,编写代码来找到指针下方的像素。

const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth; const pixelY = gl.canvas.height - mouseY * gl.canvas.height / gl.canvas.clientHeight - 1; const data = new Uint8Array(4); gl.readPixels( pixelX, // x pixelY, // y 1, // width 1, // height gl.RGBA, // format gl.UNSIGNED_BYTE, // type data); // typed array to hold result const id = data[0] + (data[1]


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3