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

第 5 篇:WebGL 从 2D 到 3D - 坐标系、透视与相机

至今为止,我们所有的工作都发生在一个 (x, y) 坐标的平面上。现在,我们要引入一个全新的维度:深度,也就是 Z 轴

X 轴通常代表左右,Y 轴代表上下,而 Z 轴则代表前后。在 WebGL 的标准坐标系(右手坐标系)中,Z 轴的正方向通常指向屏幕,负方向指向屏幕

有了 (x, y, z) 三个坐标,我们就可以在虚拟空间中定义任何一个点的位置了。但是,一个严峻的问题摆在我们面前:我们的显示器终究是一块 2D 的平面,它要如何展示一个 3D 的世界呢?

答案来自于生活:透视 (Perspective)。我们的大脑之所以能感知到深度,一个关键因素就是“近大远小”。WebGL 要做的,就是用数学来模拟这个过程。

终极武器:MVP 矩阵

在第 3 篇中,我们用一个变换矩阵来控制图形。在 3D 世界中,这个过程被扩展为一条更精密的流水线,由三个核心矩阵相乘,得到一个最终的超级矩阵。这就是传说中的 MVP 矩阵,理解了它,就理解了现代 3D 渲染的核心。

  1. M - Model (模型) 矩阵:

    • 作用: 把模型“摆好姿势”。
    • 职责: 这个矩阵和我们之前用的变换矩阵几乎一样。它负责对一个模型进行平移、旋转和缩放,定义它在整个世界空间 (World Space) 中的位置、朝向和大小。
    • 回答的问题: “这个物体在世界的哪个角落?它朝向哪?有多大?”
  2. V - View (视图) 矩阵:

    • 作用: 架设“相机”。
    • 职责: 这个矩阵非常巧妙。它不是真的去移动一个虚拟相机,而是反过来移动整个世界,使得我们想观察的场景正好落在相机的视野里。想象一下,为了拍清楚一个杯子,你是后退一步(移动相机),还是把整个桌子连同杯子一起推远一点(移动世界)?在 WebGL 里,我们选择后者。这个矩阵定义了相机的位置和它所看向的目标点。
    • 回答的问题: “我(相机)正站在哪里,朝哪个方向看?”
  3. P - Projection (投影) 矩阵:

    • 作用: 定义“镜头”并施展“近大远小”的魔法。
    • 职责: 这是实现 3D 观感的最后一步。它会创建一个被称为视锥体 (Frustum) 的虚拟观察空间(一个被切掉顶部的金字塔)。所有在这个“金字塔”内的物体都会被保留,并在最终被“压扁”到 2D 屏幕上。这个“压扁”的过程,就会自动产生透视效果——离“金字塔”小头(相机)近的物体,压扁后会更大;远的物体,压扁后会更小。
    • 回答的问题: “我的镜头有多广角(FOV)?物体在多近或多远时会被我忽略(近/远裁剪面)?”

最终流程: 最终变换 = 投影矩阵 * 视图矩阵 * 模型矩阵

这三个矩阵在 JavaScript 中计算好,相乘得到一个最终的 mat4 (4x4 矩阵),然后作为一个 uniform 变量,一次性发送给顶点着色器。顶点着色器的工作反而变得异常简单,它只需要用这个最终的 MVP 矩阵去乘以顶点的原始坐标就行了。

新的挑战:深度遮挡

在 3D 空间里,物体会互相遮挡。如果我们不告诉 WebGL 如何处理,它可能会把后面的三角形画到前面三角形的上面,造成“穿模”的混乱效果。

解决方法很简单:开启深度测试 (Depth Test)

你可以想象屏幕的每个像素点除了有颜色值,还有一个深度值(Z 值)。当 WebGL 准备绘制一个像素时,它会检查这个新像素的深度值,和已经画在那里的像素的深度值。如果新像素更“靠前”(深度值更小),就覆盖掉旧的;否则,就直接丢弃。

我们只需要在初始化时告诉 WebGL 启用这个功能即可。

构建我们的第一个 3D 场景:旋转的立方体

是时候把理论付诸实践了!我们将创建一个由 6 个不同颜色的面组成的立方体,并让它在 3D 空间中自由旋转。

1. 升级顶点着色器

attribute vec4 a_position; // 从 vec2/vec3 升级到 vec4
attribute vec4 a_color;uniform mat4 u_mvpMatrix; // 接收最终的 MVP 矩阵varying vec4 v_color;void main() {// 工作变得无比简单!gl_Position = u_mvpMatrix * a_position;v_color = a_color;
}

2. JavaScript:矩阵的交响乐

JavaScript 的部分是这次的重头戏。我们将:

  • 定义一个立方体的所有顶点和颜色。
  • 引入一套 4x4 矩阵的数学函数(在实际项目中请务必使用 gl-matrix 这样的库)。
  • 在初始化时开启深度测试。
  • 在动画循环中,分别计算 M、V、P 三个矩阵,将它们相乘,然后上传给着色器。

这部分代码会显得很长,但逻辑非常清晰。仔细阅读注释,你会发现它正是我们上面所讨论的 MVP 流程的完美再现。

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>WebGL 教程 5:3D 立方体</title><style>body { background-color: #333; color: #eee; text-align: center; }canvas { background-color: #000; border: 1px solid #555; }</style>
</head>
<body onload="main()"><h1>从 2D 到 3D - 坐标系、透视与相机</h1><canvas id="webgl-canvas" width="600" height="600"></canvas><!-- 顶点着色器 (已更新) --><script id="vertex-shader" type="x-shader/x-vertex">attribute vec4 a_position; // 顶点位置 (x, y, z, 1.0)attribute vec4 a_color;    // 顶点颜色uniform mat4 u_mvpMatrix;  // 接收合并后的 MVP 矩阵varying vec4 v_color;void main() {// 将顶点位置与 MVP 矩阵相乘,得到最终裁剪空间中的坐标gl_Position = u_mvpMatrix * a_position;v_color = a_color;}</script><!-- 片元着色器 (无变化) --><script id="fragment-shader" type="x-shader/x-fragment">precision mediump float;varying vec4 v_color;void main() {gl_FragColor = v_color;}</script><!-- 引入 gl-matrix 库来简化矩阵运算 --><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 vsSource = document.getElementById('vertex-shader').text;const fsSource = document.getElementById('fragment-shader').text;const program = createProgram(gl, vsSource, fsSource);const locations = {position: gl.getAttribLocation(program, "a_position"),color: gl.getAttribLocation(program, "a_color"),mvpMatrix: gl.getUniformLocation(program, "u_mvpMatrix"),};const buffer = initBuffers(gl);gl.useProgram(program);// 关键:开启深度测试gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL); // 近处的物体遮挡远处的let cubeRotation = 0.0;let lastTime = 0;function animate(now) {now *= 0.001; // convert to secondsconst deltaTime = now - lastTime;lastTime = now;drawScene(gl, locations, buffer, cubeRotation);cubeRotation += deltaTime;requestAnimationFrame(animate);}requestAnimationFrame(animate);}// 绘制场景的函数function drawScene(gl, locations, buffer, cubeRotation) {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);// --- MVP 矩阵计算 ---// P - Projection (投影) 矩阵const fieldOfView = 45 * Math.PI / 180; // 45度视角const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;const zNear = 0.1;const zFar = 100.0;const projectionMatrix = mat4.create();mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);// V - View (视图) 矩阵 - "相机"const viewMatrix = mat4.create();// 相机位置在(0, 0, 6),看向原点(0,0,0),头部朝上(0,1,0)mat4.lookAt(viewMatrix,,,);// M - Model (模型) 矩阵const modelMatrix = mat4.create();mat4.translate(modelMatrix, modelMatrix, [0.0, 0.0, 0.0]); // 平移mat4.rotate(modelMatrix, modelMatrix, cubeRotation * .7,); // 绕 Y 轴旋转mat4.rotate(modelMatrix, modelMatrix, cubeRotation,); // 绕 (1,0,1) 轴旋转// 合并 MVPconst mvpMatrix = mat4.create();mat4.multiply(mvpMatrix, projectionMatrix, viewMatrix);mat4.multiply(mvpMatrix, mvpMatrix, modelMatrix);// --- 数据绑定与绘制 ---const FSIZE = Float32Array.BYTES_PER_ELEMENT;gl.bindBuffer(gl.ARRAY_BUFFER, buffer);gl.vertexAttribPointer(locations.position, 3, gl.FLOAT, false, 6 * FSIZE, 0);gl.enableVertexAttribArray(locations.position);gl.vertexAttribPointer(locations.color, 3, gl.FLOAT, false, 6 * FSIZE, 3 * FSIZE);gl.enableVertexAttribArray(locations.color);gl.uniformMatrix4fv(locations.mvpMatrix, false, mvpMatrix);// 绘制 36 个顶点gl.drawArrays(gl.TRIANGLES, 0, 36);}// 初始化立方体顶点数据的函数function initBuffers(gl) {const buffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, buffer);const verticesColors = new Float32Array([// Front face-1.0, -1.0,  1.0,  1.0,  0.0,  0.0,1.0, -1.0,  1.0,  1.0,  0.0,  0.0,1.0,  1.0,  1.0,  1.0,  0.0,  0.0,-1.0,  1.0,  1.0,  1.0,  0.0,  0.0,// Back face-1.0, -1.0, -1.0,  0.0,  1.0,  0.0,-1.0,  1.0, -1.0,  0.0,  1.0,  0.0,1.0,  1.0, -1.0,  0.0,  1.0,  0.0,1.0, -1.0, -1.0,  0.0,  1.0,  0.0,// Top face-1.0,  1.0, -1.0,  0.0,  0.0,  1.0,-1.0,  1.0,  1.0,  0.0,  0.0,  1.0,1.0,  1.0,  1.0,  0.0,  0.0,  1.0,1.0,  1.0, -1.0,  0.0,  0.0,  1.0,// Bottom face-1.0, -1.0, -1.0,  1.0,  1.0,  0.0,1.0, -1.0, -1.0,  1.0,  1.0,  0.0,1.0, -1.0,  1.0,  1.0,  1.0,  0.0,-1.0, -1.0,  1.0,  1.0,  1.0,  0.0,// Right face1.0, -1.0, -1.0,  1.0,  0.0,  1.0,1.0,  1.0, -1.0,  1.0,  0.0,  1.0,1.0,  1.0,  1.0,  1.0,  0.0,  1.0,1.0, -1.0,  1.0,  1.0,  0.0,  1.0,// Left face-1.0, -1.0, -1.0,  0.0,  1.0,  1.0,-1.0, -1.0,  1.0,  0.0,  1.0,  1.0,-1.0,  1.0,  1.0,  0.0,  1.0,  1.0,-1.0,  1.0, -1.0,  0.0,  1.0,  1.0,]);// 为了让立方体看起来是一个整体,我们需要明确指定每个面的顶点索引const indices = new Uint16Array([0,  1,  2,      0,  2,  3,    // front4,  5,  6,      4,  6,  7,    // back8,  9,  10,     8,  10, 11,   // top12, 13, 14,     12, 14, 15,   // bottom16, 17, 18,     16, 18, 19,   // right20, 21, 22,     20, 22, 23,   // left]);const positionColorBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, positionColorBuffer);gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);const indexBuffer = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);return {position: positionColorBuffer,indices: indexBuffer};}// 辅助函数function createProgram(gl, vsSource, fsSource) { /* ... same as before ... */ function createShader(type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error("Shader error:", gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } const vs = createShader(gl.VERTEX_SHADER, vsSource); const fs = createShader(gl.FRAGMENT_SHADER, fsSource); const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error("Program link error:", gl.getProgramInfoLog(prog)); gl.deleteProgram(prog); return null; } return prog;}</script>
</body>
</html>
总结与展望

我们成功了! 浏览器中现在应该有一个真正在 3D 空间中旋转的彩色立方体。你可以清晰地看到它的不同面,以及它们是如何根据透视正确地显示和遮挡的。

今天,我们跨越了从 2D 到 3D 最大的鸿沟:

  • 掌握了 MVP 矩阵:这是现代实时 3D 渲染的基石,分离了模型、相机和投影的控制。
  • 学会了设置“相机”和“镜头”:通过视图矩阵和投影矩阵来定义我们的观察视角。
  • 启用了深度测试:解决了 3D 场景中物体间的遮挡问题。

然而,你可能会注意到,我们的立方体看起来有点“平”,缺乏立体感。它有形状,但没有光影带来的体积感。

在下一篇文章中,我们将解决这个问题。我们将引入 3D 世界的灵魂——光照。我们将学习最基础的光照模型,计算每个面的法向量,并模拟光线与物体表面的交互,让我们的立方体第一次拥有明暗变化和真实的体积感。

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

相关文章:

  • 文字转语音——sherpa-onnx语音识别离线部署C++实现
  • 深度学习------专题《图像处理项目》下
  • wordpress 伪链接昭通seo
  • 【rabbitmq 高级特性】全面详解RabbitMQ TTL (Time To Live)
  • STM32启动流程全面解析:从上电复位到进入应用程序main函数
  • QQ可以在网站做临时会话么ysl免费网站建设
  • 做网站的女生多么镇江市住房城乡建设局网站
  • 后端两个接口需分开写,前端需不串行并同时刷新调用但数据不同步NOTE
  • 华北建设集团有限公司oa网站上海旅游网站建设情况
  • Appinventor笔记4-数字块与文本块
  • 龙溪营销型网站制作上海官网
  • 支付方式大升级!AI代理将进入购物环节
  • 前端实验(序)——前端开发基础
  • reset saved-configuration 概念及题目
  • 口腔病变识别分割数据集labelme格式1317张1类别
  • 做网站的qq兼职网易企业邮箱登录登录入口
  • 【开题答辩全过程】以 springboot毕业设计管理系统为例,包含答辩的问题和答案
  • 越南网站建设青岛做网络直播的网站
  • 【愚公系列】《人工智能70年》045-生成式Al的辉煌与难题(ChatGPT一鸣惊人)
  • 学习嵌入式的第四十二天——ARM——UART
  • MCP协议深度解析(理论篇):AI工具生态的统一语言
  • 沧州网站建设哪家专业微娱网络小程序代理
  • git-filter-repo - 强大的Git历史重写工具
  • 阿里云wordpress在哪里设置密码网站建设相关优化
  • 常州专业网站建设公司咨询做家具有那个网站好
  • Vim复制粘贴剪切命令详解
  • STM32H743-ARM例程8-EXTI外部中断
  • ARM(IMX6ULL)——通信(UART)
  • 网站 开发逻辑开发app开发公司
  • Kong Gateway 实操实例:代理上游服务并配置限流插件