使用键盘方向键在WebGL中移动3D对象
嘿,我仔细看了你的代码和问题,核心问题出在两个地方:一是你误解了gl.viewport的作用,二是没有在按键更新后重新应用视口设置(不过这其实不是移动3D物体的正确方式)。先给你理清楚思路,再一步步解决问题:
先纠正一个核心概念
gl.viewport是用来控制WebGL在Canvas上的绘制区域,不是用来移动3D空间里的物体的——手动修改它的x/y参数看起来物体动了,其实是把整个绘制画面在Canvas上偏移了,这种方式会导致画面被裁剪,完全不符合3D场景的逻辑。正确的做法是用**模型矩阵(Model Matrix)**来控制物体在3D空间中的位置,这是WebGL里移动、旋转、缩放物体的标准方案。
具体修改步骤
1. 替换平移变量,修改按键事件逻辑
先把原来的xPos/yPos换成3D空间的平移变量,然后在按键事件里更新这个变量(用小数值而不是像素,因为3D空间用的是标准化坐标):
// 在init函数里替换原来的xPos/yPos定义 var translate = vec3(0, 0, 0); // 存储物体在3D空间的x/y/z平移量 // 修改按键事件处理 document.addEventListener("keydown", function(e) { e.preventDefault(); const key = e.key; switch (key) { case "ArrowLeft": translate[0] -= 0.1; // 沿X轴负方向移动 break; case "ArrowRight": translate[0] += 0.1; // 沿X轴正方向移动 break; case "ArrowUp": translate[1] += 0.1; // 沿Y轴正方向移动 break; case "ArrowDown": translate[1] -= 0.1; // 沿Y轴负方向移动 break; } });
2. 补充矩阵运算函数
因为需要生成旋转和平移矩阵,添加几个基础的矩阵生成函数(如果你的代码依赖的库没有这些的话):
// 生成X轴旋转矩阵(角度转弧度) function rotateXMat(angle) { const rad = angle * Math.PI / 180; const c = Math.cos(rad); const s = Math.sin(rad); return mat4( 1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1 ); } // 生成Y轴旋转矩阵 function rotateYMat(angle) { const rad = angle * Math.PI / 180; const c = Math.cos(rad); const s = Math.sin(rad); return mat4( c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ); } // 生成Z轴旋转矩阵 function rotateZMat(angle) { const rad = angle * Math.PI / 180; const c = Math.cos(rad); const s = Math.sin(rad); return mat4( c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); } // 生成平移矩阵 function translateMat(x, y, z) { return mat4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1 ); }
3. 修改顶点着色器,添加模型矩阵uniform
更新你的顶点着色器,用模型矩阵来变换顶点位置(替换原来的旋转逻辑):
attribute vec4 aPosition; attribute vec4 aColor; attribute vec2 aTexCoord; uniform mat4 uModelMatrix; // 新增模型矩阵uniform uniform sampler2D uTex0; varying vec4 vColor; varying vec2 vTexCoord; void main() { gl_Position = uModelMatrix * aPosition; // 用模型矩阵完成顶点变换 vColor = aColor; vTexCoord = aTexCoord; }
然后在JS的init函数里获取这个uniform的位置:
// 在thetaLoc定义之后添加 var modelMatrixLoc = gl.getUniformLocation(program, "uModelMatrix");
4. 在render函数中计算并传递模型矩阵
修改render函数,把旋转和平移结合成模型矩阵,传递给着色器:
var render = function() { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); if(flag) theta[axis] += 2.0; // 生成各个旋转矩阵 const rotateX = rotateXMat(theta[0]); const rotateY = rotateYMat(theta[1]); const rotateZ = rotateZMat(theta[2]); // 生成平移矩阵 const translateM = translateMat(translate[0], translate[1], translate[2]); // 模型矩阵:先旋转再平移(让物体绕自身中心旋转后移动位置) // 矩阵乘法顺序很重要,WebGL是列主序,所以顺序是 平移 * 旋转Z * 旋转Y * 旋转X const modelMatrix = mult(translateM, mult(rotateZ, mult(rotateY, rotateX))); // 传递模型矩阵给着色器 gl.uniformMatrix4fv(modelMatrixLoc, false, flatten(modelMatrix)); gl.drawArrays(gl.TRIANGLES, 0, numPositions); requestAnimationFrame(render); }
为什么原来的方法不行?
你之前只在init里调用了一次gl.viewport(xPos, yPos, canvas.width, canvas.height),之后按键更新xPos/yPos后没有重新调用这个方法,所以画面不会变化。就算你把gl.viewport移到render里,这种方式也只是偏移了WebGL在Canvas上的绘制区域,不是真正移动3D物体——比如当你把xPos设为25,WebGL会从Canvas的(25,0)位置开始绘制,超出Canvas的部分会被裁剪,这显然不是你想要的3D物体移动效果。
用模型矩阵的方式,是在3D空间中真正改变物体的位置,和旋转逻辑完美结合,也是WebGL开发的标准做法,以后你做更复杂的3D场景也会用到这个思路。
备注:内容来源于stack exchange,提问作者s73c

