当前位置: 首页 > news >正文

第 7 篇:交互的乐趣 - 响应用户输入

我们已经点亮了世界,创造了一个有体积感的旋转立方体。它很漂亮,但它是一个“独裁者”——它只按照我们预设的 cubeRotation += deltaTime 指令来行动。用户,作为这个世界名义上的“主人”,却没有任何发言权。

是时候改变这一切了。今天,我们将学习如何监听用户的操作(比如鼠标点击和拖动),并将这些操作转化为 3D 场景中的变化。我们将赋予用户“上帝之手”,让他们能够随意拨弄我们的立方体。

沟通的桥梁:JavaScript 事件监听器

我们的 WebGL-Canvas 元素,本质上就是一个普通的 HTML 元素。这意味着,我们可以使用所有标准的 Web API 来监听发生在它身上的事件。

实现“点击并拖动来旋转”这个经典交互,我们需要三个核心事件:

  1. mousedown:当用户在 canvas 上按下鼠标按键时触发。我们用它来标记“开始拖动”。
  2. mouseup:当用户松开鼠标按键时触发。我们用它来标记“结束拖动”。
  3. mousemove:当用户在 canvas 上移动鼠标时触发。这是最关键的事件,我们将在这里计算鼠标的移动,并将其转化为旋转。
记住状态:交互中的变量

计算机不会像人一样“记得”鼠标是不是正按着。我们必须用变量来明确地告诉它当前的状态。对于拖动旋转,我们需要几个“记忆”变量:

  • isDragging = false;:一个布尔值(“开关”),用来记录鼠标当前是否处于按下并拖动的状态。
  • previousMousePosition = { x: 0, y: 0 };:一个对象,用来存储上一次 mousemove 事件触发时鼠标的位置。没有“上一次”的位置,我们就无法计算出鼠标移动的距离和方向
  • currentRotation = { x: 0, y: 0 };:一个对象,用来累积旋转的角度。用户的每次拖动,都会在之前旋转的基础上继续增加或减少角度。
将 2D 鼠标移动映射到 3D 旋转

这是最核心的逻辑。当用户在 2D 屏幕上移动鼠标时,我们如何让 3D 物体做出符合直觉的反应?

一个简单而有效的方法是:

  • 鼠标的水平移动 (deltaX) 映射为 围绕 Y 轴(垂直轴)的旋转。想象一下用手左右拨动一个地球仪。
  • 鼠标的垂直移动 (deltaY) 映射为 围绕 X 轴(水平轴)的旋转。想象一下用手上下拨动一个地球仪。

mousemove 事件的处理函数中,我们的伪代码是这样的:

function onMouseMove(event) {if (isDragging is false) {return; // 如果没按着鼠标,就什么都不做}// 1. 计算鼠标移动了多少const deltaX = event.clientX - previousMousePosition.x;const deltaY = event.clientY - previousMousePosition.y;// 2. 将移动距离累加到总旋转角度上//    (乘以一个小的系数,让旋转速度更合适)currentRotation.y += deltaX * 0.01;currentRotation.x += deltaY * 0.01;// 3. 更新“上一次”的位置,为下次移动做准备previousMousePosition.x = event.clientX;previousMousePosition.y = event.clientY;
}
着色器:完全不用动!

这是最美妙的部分。我们的顶点着色器和片元着色器一行代码都不需要修改

为什么?因为光照、投影、相机这些逻辑都没有变。我们改变的,仅仅是每一帧发送给 u_mvpMatrix 这个 uniform数据。交互完全是 CPU (JavaScript) 侧的逻辑,GPU (GLSL) 只负责接收最终指令并忠实地执行渲染。

JavaScript:主导一切

所有的改动都将发生在我们的 JavaScript 代码中。

  1. 移除自动旋转:我们将删除 animate 函数中基于时间的 cubeRotation += deltaTime。旋转将完全由用户掌控。
  2. 设置状态变量:在 main 函数中初始化我们上面讨论的 isDragging 等变量。
  3. 绑定事件监听器:为 canvas 添加 mousedown, mouseup, mousemove 的监听。
  4. 修改矩阵计算:在 drawScene 函数中,模型矩阵的旋转部分,不再使用时间变量,而是使用我们通过鼠标交互更新的 currentRotation 变量。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>WebGL 教程 9:交互的乐趣</title><style>body { background-color: #222; color: #eee; text-align: center; }canvas { background-color: #000; border: 1px solid #555; cursor: grab; }canvas:active { cursor: grabbing; }</style>
</head>
<body onload="main()"><h1>交互的乐趣 - 响应用户输入</h1><p>请在下方的画布中点击并拖动鼠标</p><canvas id="webgl-canvas" width="600" height="600"></canvas><!-- Shader 代码与上一篇 (基础光照) 完全相同,无需修改 --><script id="vertex-shader" type="x-shader/x-vertex">attribute vec4 a_position;attribute vec3 a_normal;uniform mat4 u_mvpMatrix;uniform mat4 u_normalMatrix;varying vec3 v_normal;void main() {gl_Position = u_mvpMatrix * a_position;v_normal = (u_normalMatrix * vec4(a_normal, 0.0)).xyz;}</script><script id="fragment-shader" type="x-shader/x-fragment">precision mediump float;varying vec3 v_normal;uniform vec3 u_lightDirection;uniform vec3 u_lightColor;uniform vec3 u_ambientLight;void main() {vec3 normal = normalize(v_normal);float light_factor = max(dot(normal, -normalize(u_lightDirection)), 0.0);vec3 diffuse = u_lightColor * light_factor;vec3 finalColor = u_ambientLight + diffuse;gl_FragColor = vec4(finalColor, 1.0);}</script><script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script><script>function main() {const canvas = document.getElementById('webgl-canvas');const gl = canvas.getContext('webgl');if (!gl) { alert('WebGL not supported!'); return; }const program = createProgram(gl, document.getElementById('vertex-shader').text,document.getElementById('fragment-shader').text);const locations = { /* ... locations ... */ };// (Code is the same as before, omitted for brevity)locations.position=gl.getAttribLocation(program,"a_position"); locations.normal=gl.getAttribLocation(program,"a_normal"); locations.mvpMatrix=gl.getUniformLocation(program,"u_mvpMatrix"); locations.normalMatrix=gl.getUniformLocation(program,"u_normalMatrix"); locations.lightDirection=gl.getUniformLocation(program,"u_lightDirection"); locations.lightColor=gl.getUniformLocation(program,"u_lightColor"); locations.ambientLight=gl.getUniformLocation(program,"u_ambientLight");const buffer = initBuffers(gl);// --- 新增: 交互状态变量 ---let isDragging = false;let previousMousePosition = { x: 0, y: 0 };let rotation = { x: 0.5, y: -0.5 }; // 初始旋转// --- 新增: 事件监听 ---canvas.addEventListener('mousedown', (e) => {isDragging = true;previousMousePosition.x = e.clientX;previousMousePosition.y = e.clientY;});canvas.addEventListener('mouseup', () => {isDragging = false;});canvas.addEventListener('mousemove', (e) => {if (!isDragging) return;const deltaX = e.clientX - previousMousePosition.x;const deltaY = e.clientY - previousMousePosition.y;rotation.y += deltaX * 0.01;rotation.x += deltaY * 0.01;previousMousePosition.x = e.clientX;previousMousePosition.y = e.clientY;});gl.useProgram(program);gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);function animate() {// 不再需要基于时间的旋转drawScene(gl, locations, buffer, rotation);requestAnimationFrame(animate);}requestAnimationFrame(animate);}// drawScene 函数现在接收 rotation 对象,而不是单个旋转值function drawScene(gl, locations, buffers, rotation) {gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);gl.clearColor(0.1, 0.1, 0.1, 1.0);gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);const projectionMatrix = mat4.create();mat4.perspective(projectionMatrix, 45 * Math.PI / 180, gl.canvas.clientWidth / gl.canvas.clientHeight, 0.1, 100.0);const viewMatrix = mat4.create();mat4.lookAt(viewMatrix, [0, 0, 5], [0, 0, 0], [0, 1, 0]);const modelMatrix = mat4.create();// 使用来自用户输入的旋转mat4.rotate(modelMatrix, modelMatrix, rotation.x, [1, 0, 0]); // 绕 X 轴mat4.rotate(modelMatrix, modelMatrix, rotation.y, [0, 1, 0]); // 绕 Y 轴const mvpMatrix = mat4.create();mat4.multiply(mvpMatrix, projectionMatrix, viewMatrix);mat4.multiply(mvpMatrix, mvpMatrix, modelMatrix);const normalMatrix = mat4.create();mat4.invert(normalMatrix, modelMatrix);mat4.transpose(normalMatrix, normalMatrix);gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);gl.vertexAttribPointer(locations.position, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(locations.position);gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);gl.vertexAttribPointer(locations.normal, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(locations.normal);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);gl.uniformMatrix4fv(locations.mvpMatrix, false, mvpMatrix);gl.uniformMatrix4fv(locations.normalMatrix, false, normalMatrix);gl.uniform3fv(locations.lightDirection, [0.5, 0.7, 1.0]);gl.uniform3fv(locations.lightColor, [1.0, 1.0, 1.0]);gl.uniform3fv(locations.ambientLight, [0.2, 0.2, 0.2]);gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);}function initBuffers(gl) {// 为每个面定义独立的顶点,这样可以有正确的法向量const positions = [// 前面-1, -1,  1,1, -1,  1,1,  1,  1,-1,  1,  1,// 后面-1, -1, -1,-1,  1, -1,1,  1, -1,1, -1, -1,// 顶面-1,  1, -1,-1,  1,  1,1,  1,  1,1,  1, -1,// 底面-1, -1, -1,1, -1, -1,1, -1,  1,-1, -1,  1,// 右面1, -1, -1,1,  1, -1,1,  1,  1,1, -1,  1,// 左面-1, -1, -1,-1, -1,  1,-1,  1,  1,-1,  1, -1,];const positionBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);const normalData = [[0, 0, 1],   // 前面[0, 0, -1],  // 后面[0, 1, 0],   // 顶面[0, -1, 0],  // 底面[1, 0, 0],   // 右面[-1, 0, 0],  // 左面];let normals = [];for(let i = 0; i < 6; i++) {for(let j = 0; j < 4; j++) {normals.push(...normalData[i]);}}const normalBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);const indices = new Uint16Array([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    // 左面]);const indexBuffer = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);return {position: positionBuffer,normal: normalBuffer,indices: indexBuffer};}function createProgram(gl, vs, fs) { /* ... same as previous ... */ function c(t,s){const h=gl.createShader(t);gl.shaderSource(h,s);gl.compileShader(h);if(!gl.getShaderParameter(h,gl.COMPILE_STATUS)){console.error(gl.getShaderInfoLog(h));gl.deleteShader(h);return null}return h}const p=gl.createProgram();gl.attachShader(p,c(gl.VERTEX_SHADER,vs));gl.attachShader(p,c(gl.FRAGMENT_SHADER,fs));gl.linkProgram(p);if(!gl.getProgramParameter(p,gl.LINK_STATUS)){console.error(gl.getProgramInfoLog(p));gl.deleteProgram(p);return null}return p; }</script>
</body>
</html>
总结与展望

现在,运行代码,你的立方体应该会静静地等待着。但当你用鼠标在它身上按下并拖动时,它会立刻响应你的操作,跟随你的鼠标轨迹进行旋转。我们成功地建立了一条从用户到 3D 世界的沟通渠道!

今天,我们完成了非常重要的一步:

  • 学会了使用 JavaScript 事件监听器 来捕捉用户输入。
  • 理解了通过状态变量来管理和累积交互状态的重要性。
  • 掌握了将 2D 屏幕输入映射为 3D 空间变换的基本思路。
  • 再次印证了 CPU 和 GPU 的职责分离:CPU (JS) 负责逻辑和交互,GPU (GLSL) 负责大规模并行计算和渲染。

我们的世界不仅看得见、有光影,现在更能“摸得着”了。这为我们未来的探索铺平了道路,比如用键盘控制相机在场景中漫游,或者实现点击拾取场景中的物体。

在下一篇中,我们将回到光照的话题,完成我们之前留下的一个悬念。我们将为材质添加“高光”,让物体表面能够反射出耀眼的光斑,从而区分出金属和塑料质感。

http://www.dtcms.com/a/477335.html

相关文章:

  • 解决Chrome 140以上版本“此扩展程序不再受支持,因此已停用”问题 axure插件安装问题
  • 如何在火语言中指定启动 Chrome 特定用户配置文件
  • 轻松测试二维码生成与识别:使用Python的qrcode、opencv和pyzbar库
  • 清河做网站报价大背景 网站
  • 迅捷视频转换器 v18.4.23 图文安装教程|支持MP4、AVI、MKV等多格式视频转换
  • 【AI论文】通过渐进式一致性蒸馏实现高效的多模态大语言模型
  • 怎么查看网站开发人网站建设流程及相应技术
  • kubecm切换k8s集群工具
  • Azure多项目管理全攻略:从资源部署到成本分析与优化
  • 怎么做游戏试玩网站城乡建设部网站 挂证
  • 历劫波,明真我——Debug Commune
  • Vue.js 模板语法
  • Spark RDD 宽窄依赖:从 DAG 到 Shuffle 的性能之道
  • scRNA-seq还是snRNA-seq,如何选择
  • 中国人做的比较好的shopify网站慈溪市住房和城乡建设局网站
  • 德州网站建设费用宁国市有做网站
  • 顺德品牌网站建设咨询建设宠物网站的可行性
  • 网站的惩罚期要怎么做苏州网站建设优化
  • 给公司做网站需要什么信息淘宝网站怎么做的好
  • 网站硬件需求淘客网站代理
  • 网站转发代码简单大气网站源码
  • 手机网站制作的公司滁州市建设局网站
  • 怎么建设手机网站首页邢台网站建设服务商
  • 春考网站建设wordpress svg
  • wordpress 网站禁用全屏代码医疗网站建设策划书
  • 高级室内设计网站织梦网站地图怎么做xml
  • 网站建设的步骤过程视频vR网站建设程序
  • 西安制作网站的公司有wordpress检测替换
  • 铭讯网站建设宁波网站建设团队
  • 一 网站建设的总体目标移动网站在线开发工具