当前位置: 首页 > 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.hskmm.com/?act=detail&tid=30687

相关文章:

  • CUDA+torch+flash-attn安装
  • 2025 年离合器厂家最新推荐排行榜:聚焦国内优质厂商,从技术到服务多维度解析,助力企业精准选购矿山/气胎/通风式/推盘离合器厂家推荐
  • 2025 年离心机厂家最新推荐排行榜:聚焦平板 / 吊袋 / 刮刀 / 拉袋等多类型设备,精选优质企业助力用户精准选型
  • 共模电压测量:原理、方法与应用探析
  • ​​差分探头技术解析:高精度电子测量的核心工具​​
  • LGP9869 [NOIP 2023] 三值逻辑 学习笔记
  • Windows 程序开机自启的方法
  • 详细介绍:全球资本开支激增,就业增长停滞:AI时代的双刃剑
  • 2025 年昆明商务车总代理推荐艾维诺:16 年深耕高端定制与销售,奔驰丰田等品牌优选服务商
  • java面向对象
  • Chrome 安装失败且提示“无可用的更新” 或 “与服务器的连接意外终止”,Chrome 离线版下载安装教程
  • keepalived日志报错Error exec-ing command /usr/local/keepalived/chk.sh, error 8: Exec format
  • 2025 年国内树脂瓦厂家最新推荐排行榜:聚焦品质与服务,助力建筑屋面选材更可靠PVC/asa 加厚合成 / FRP/PET 树脂瓦厂家推荐!
  • 2025 年隔音板厂家最新推荐排行榜:阻尼 / 聚酯纤维 / 室外等多品类适配,聚焦口碑厂商与一站式服务
  • 2025-10-13
  • 剑指offer-34、第⼀次出现的字符
  • 斜率优化DP
  • 2025 年海运服务最新推荐排行榜:聚焦优质运输网络的澳洲悉尼墨尔本家具及大型物品海运公司盘点
  • LGP7963 [NOIP 2021] 棋局 学习笔记
  • 310、清平调三首其三
  • 2025 年国内地坪源头厂商最新推荐排行榜:聚焦优质企业服务与性能,助力客户精准选型固化剂/水性聚氨酯砂浆/环氧/聚氨酯超耐磨地坪工程厂商推荐
  • 2025 最新国际搬家公司推荐榜:海运 / 移民 / 家具运输实测解析,靠谱服务商甄选指南
  • 2025 年数据恢复系统公司推荐转转大师数据恢复,深度剖析各款系统平台核心优势与适用场景数据恢复系统推荐指南
  • 2025 年最新推荐防静电地板源头厂家权威排行榜,涵盖机房陶瓷全钢等多类型产品优质品牌汇总车间/生产防静电地板/防静电活动地板/抗静电地板公司推荐
  • 秋假集训记
  • golang优化
  • AI智能体开发!和Kiro、Kimi、PPIO、TEN、memU、MiniMax一起Vibe丨Convo AIRTE2025
  • 实用指南:Transformer模型:深度解析自然语言处理的革命性架构
  • 第一章
  • 生成式AI基础设施面临网络攻击威胁:企业安全新挑战