WebGL基础教程(十四):投影矩阵深度解析——正交 vs 透视,彻底搞懂3D视觉魔法

还在为 WebGL 中物体"近大远小"的效果感到神奇?或者苦恼于正交投影和透视投影到底该怎么选?为什么 3D 物体总被裁剪看不见?

今天这篇教程,带你彻底搞懂 WebGL 中的投影矩阵。我们从数学原理到实战代码,手把手教你区分正交与透视投影,并理解 MVP 矩阵中投影矩阵的核心作用。

左图为正交,六个面为平行四边形,右图为透视,六个面近大远小

左图为正交,六个面为平行四边形,右图为透视,六个面近大远小

1.先搞懂:为什么需要投影矩阵?

在 WebGL 中,顶点着色器输出的 gl_Position 坐标范围是 裁剪空间 (四个分量 x,y,z,w 都在 -w 到 w 之间)。但我们的世界是三维的,屏幕是二维的,投影矩阵 的作用就是:

将 3D 空间中的物体,变换到 2D 屏幕上,并决定"近大远小"的视觉感。

没有投影矩阵,你只能看到物体在 [-1,1] 范围内的正交投影,毫无立体感。投影矩阵是连接 3D 世界与 2D 屏幕的魔法桥梁。

核心优势(投影矩阵版)
  • 透视投影:产生景深感,符合人眼视觉,是 3D 游戏/场景的首选。

  • 正交投影:保持物体实际大小,无透视变形,适合 2D 游戏、UI 界面、工程制图。

  • GPU 统一处理:无论哪种投影,都通过 4x4 矩阵乘法完成,性能高效。

2.WebGL + 投影矩阵工作原理

投影矩阵是 MVP 矩阵(模型-视图-投影)中的最后一环:

  1. 模型矩阵:将物体从局部坐标变换到世界坐标(平移/旋转/缩放)。

  2. 视图矩阵:将世界坐标变换到观察者(相机)坐标(定义相机位置和朝向)。

  3. 投影矩阵 :将观察坐标变换到裁剪坐标,并定义可视范围(视景体)。

关键公式

glsl

复制代码
gl_Position = 投影矩阵 * 视图矩阵 * 模型矩阵 * 顶点坐标;

3.投影矩阵核心原理

3.1 透视投影(Perspective Projection)

透视投影模拟人眼成像,遵循"近大远小"规则。它定义了一个 平截头体 视景体:近平面大、远平面小,物体越靠近近平面,在屏幕上投影越大。

透视矩阵原理(重点!)

标准透视矩阵(由视野、宽高比、近平面、远平面构建):

text

复制代码
[ 1/(aspect*tan(fov/2))    0               0                     0          ]
[ 0                        1/tan(fov/2)    0                     0          ]
[ 0                        0        -(far+near)/(far-near)  -2*far*near/(far-near) ]
[ 0                        0               -1                    0          ]
  • fov:视野角度(Field of View),角度越大,可见范围越广。

  • aspect:canvas 宽高比,防止画面拉伸。

  • near/far:近平面和远平面距离,在此范围内的物体才会被渲染。

矩阵乘法效果 :顶点的 z 坐标被用于控制缩放比例,实现"近大远小"。

3.2 正交投影(Orthographic Projection)

正交投影无视物体远近,在屏幕上保持相同大小。它定义了一个长方体视景体,适合精确的 2D 布局、UI 或技术绘图。

正交矩阵原理

text

复制代码
[ 2/(right-left)    0              0          -(right+left)/(right-left) ]
[ 0                 2/(top-bottom) 0          -(top+bottom)/(top-bottom) ]
[ 0                 0              -2/(far-near)  -(far+near)/(far-near) ]
[ 0                 0              0          1                          ]

参数直接定义左右上下远近的范围,物体在此范围内则显示,且大小不随距离变化。

4.实战:透视投影 vs 正交投影(同屏对比)

以下代码在一个页面中左右分屏展示两种投影效果,并支持鼠标拖拽同步旋转视角,让你直观感受差异。

html

复制代码
        // 渲染正交投影(左侧)
     
            const canvas = orthoGL.gl.canvas;
            canvas.width = width;
            canvas.height = height;
            orthoGL.gl.viewport(0, 0, width, height);
            
            const view = mat4.create();
            const camDist = 5.0;
            const cx = camDist * Math.sin(rotY) * Math.cos(rotX);
            const cy = camDist * Math.sin(rotX);
            const cz = camDist * Math.cos(rotY) * Math.cos(rotX);
            mat4.lookAt(view, [cx, cy, cz], [0,0,0], [0,1,0]);
            
            const proj = mat4.create();
            const aspect = width / height;
            let left, right, bottom, top;
            if (aspect >= 1) {
                left = -orthoRange * aspect;
                right = orthoRange * aspect;
                bottom = -orthoRange;
                top = orthoRange;
            } else {
                left = -orthoRange;
                right = orthoRange;
                bottom = -orthoRange / aspect;
                top = orthoRange / aspect;
            }
            mat4.ortho(proj, left, right, bottom, top, 0.5, 20);
            
            const mvp = mat4.create();
            mat4.multiply(mvp, proj, view);
            orthoGL.gl.uniformMatrix4fv(orthoGL.mvpLoc, false, mvp);
            orthoGL.gl.clearColor(0.08, 0.08, 0.12, 1);
            orthoGL.gl.clear(orthoGL.gl.COLOR_BUFFER_BIT | orthoGL.gl.DEPTH_BUFFER_BIT);
            orthoGL.gl.drawArrays(orthoGL.gl.TRIANGLES, 0, 36);
    
        
        // 渲染透视投影(右侧)
  
            const canvas = perspGL.gl.canvas;
            canvas.width = width;
            canvas.height = height;
            perspGL.gl.viewport(0, 0, width, height);
            
            const view = mat4.create();
            const cx = perspDist * Math.sin(rotY) * Math.cos(rotX);
            const cy = perspDist * Math.sin(rotX);
            const cz = perspDist * Math.cos(rotY) * Math.cos(rotX);
            mat4.lookAt(view, [cx, cy, cz], [0,0,0], [0,1,0]);
            
            const proj = mat4.create();
            const aspect = width / height;
            mat4.perspective(proj, Math.PI / 4, aspect, 0.3, 30);
            
            const mvp = mat4.create();
            mat4.multiply(mvp, proj, view);
            perspGL.gl.uniformMatrix4fv(perspGL.mvpLoc, false, mvp);
            perspGL.gl.clearColor(0.08, 0.08, 0.12, 1);
            perspGL.gl.clear(perspGL.gl.COLOR_BUFFER_BIT | perspGL.gl.DEPTH_BUFFER_BIT);
            perspGL.gl.drawArrays(perspGL.gl.TRIANGLES, 0, 36);

5.正交 vs 透视:核心区别一览表

对比维度 正交投影 透视投影
视觉效果 物体大小不随距离变化,无"近大远小" 近大远小,符合人眼视觉
视景体 长方体(平行投影) 平截头体(锥体被切去顶部)
平行线 始终保持平行 在远处交汇于灭点
典型用途 2D游戏、UI界面、工程制图、CAD 3D游戏、虚拟场景、模型展示
矩阵函数 mat4.ortho(left, right, bottom, top, near, far) mat4.perspective(fov, aspect, near, far)
参数含义 直接定义可视范围的左右上下边界 定义视野角度、宽高比、近远平面

6.投影矩阵核心知识点(必记!)

  • MVP 矩阵顺序固定gl_Position = 投影矩阵 * 视图矩阵 * 模型矩阵 * 顶点坐标,顺序不可颠倒。

  • 透视投影调节

    • fov:越大,看到的范围越广,物体越小(类似广角镜头)。

    • aspect:必须与 canvas 的宽高比一致,否则图形会拉伸。

    • near/far:物体必须在两者之间才能显示,过近或过远会被裁剪。

  • 正交投影调节

    • left/right/bottom/top:决定了可视范围的大小,范围越大,物体在屏幕上显得越小。
  • 深度测试必须开启gl.enable(gl.DEPTH_TEST),否则远近遮挡关系会错误。

🎁 新手投影避坑指南

  • 物体看不见:检查物体是否在 near/far 范围内。可以临时将 near 设小、far 设大来测试。

  • 画面拉伸:透视投影中,aspect 必须与 canvas 的宽高比一致,并在窗口改变时更新。

  • 深度冲突(Z-fighting):near/far 范围太大或物体靠太近时,表面会闪烁。尽量缩小范围,或提高深度缓冲区精度。

  • 纹理扭曲 :WebGL 会自动进行透视校正插值,但必须通过 varying 传递纹理坐标,不要手动计算。

7.总结

  • 投影矩阵 是 WebGL 3D 渲染的最后一步,决定了视觉效果是"近大远小"还是"保持真实大小"。

  • 透视投影 通过平截头体实现景深,正交投影 通过长方体实现精确尺寸。

  • MVP 矩阵 的构建顺序至关重要,工业级开发推荐使用 gl-matrix 库。

💡 下期预告

投影矩阵学完,MVP 矩阵就完整了!下期我们将深入 光照模型,教你如何让物体拥有明暗变化、阴影和高光,彻底告别扁平感,打造真正逼真的 3D 场景。

关注我,下期手把手教你用矩阵+光照,新手也能轻松拿捏🎊!

相关推荐
Jack Yan3 小时前
WebGL平台动态修改窗口大小
webgl
Tisfy3 小时前
LeetCode 3567.子矩阵的最小绝对差:暴力模拟
leetcode·矩阵·题解·模拟·暴力
成都渲染101云渲染66663 小时前
C4D/Blender云渲染计费详解|5090显卡实测:速度对比+成本核算
3d·ue5·blender·maya·houdini
Frostnova丶3 小时前
LeetCode 3567.子矩阵的最小绝对差
算法·leetcode·矩阵
sin°θ_陈3 小时前
行业调研——XGRIDS (其域创新):空间数据生产、资产化与工业工作流的真正价值
经验分享·笔记·深度学习·3d·金融·3dgs·空间智能
小彭努力中15 小时前
192.Vue3 + OpenLayers 实战:点击地图 Feature,列表自动滚动定位
vue·webgl·openlayers·geojson·webgis
sheeta199821 小时前
LeetCode 每日一题笔记 日期:2025.03.19 题目:3212.统计X和Y频数相等的子矩阵数量
笔记·leetcode·矩阵
css在哪里21 小时前
小程序版 Three.js 入门 Demo(完整可运行)
3d·小程序·threejs
平行云1 天前
数字孪生信创云渲染系列(一):混合信创与全国产化架构
unity·ue5·3dsmax·webgl·gpu算力·实时云渲染·像素流送