☰
视图矩阵揭秘:游戏摄像机背后的数学魔法
2026/10/16 3:39:33 网站建设 项目流程

一、视图矩阵是什么

渲染时一个顶点要经过三次变换:

模型空间 ──Model──▶ 世界空间 ──View──▶ 观察空间 ──Projection──▶ 裁剪空间

视图矩阵(View Matrix)把世界坐标转换到"以摄像机为原点"的坐标系。它就是摄像机自身变换矩阵的逆矩阵:

摄像机的世界变换: CameraWorld = T(位置) × R(朝向) 视图矩阵: View = CameraWorld⁻¹ = Rᵀ × T(−位置)

可以理解为:游戏里并没有真的"移动摄像机",而是把整个世界反向移动、反向旋转,让摄像机始终待在原点、看向固定方向(OpenGL 约定看向 −Z,DirectX 约定看向 +Z)。


二、构建方法:LookAt

最常用的构建方式是给出"摄像机位置、看向的目标、上方向":

// 右手坐标系(OpenGL / glm::lookAt 的做法)mat4LookAt(vec3 eye,vec3 target,vec3 worldUp){vec3 f=normalize(target-eye);// 前vec3 r=normalize(cross(f,worldUp));// 右vec3 u=cross(r,f);// 真正的上(与 f、r 正交)mat4V(1.0f);V[0][0]=r.x;V[1][0]=r.y;V[2][0]=r.z;V[0][1]=u.x;V[1][1]=u.y;V[2][1]=u.z;V[0][2]=-f.x;V[1][2]=-f.y;V[2][2]=-f.z;V[3][0]=-dot(r,eye);V[3][1]=-dot(u,eye);V[3][2]=dot(f,eye);returnV;}

矩阵结构(行表示):

| R.x R.y R.z −R·eye | 前三行是摄像机的三个轴(右、上、后) | U.x U.y U.z −U·eye | 旋转部分是正交矩阵,所以求逆只需转置 |−F.x −F.y −F.z F·eye | 平移部分是"摄像机位置在自身轴上的投影"取负 | 0 0 0 1 |

⚠️ 当前方向f与worldUp平行(正上方或正下方)时,叉积为零,矩阵失效。所以 FPS 摄像机的俯仰角要限制在 ±89° 以内。


三、各类摄像机

1. 第一人称(FPS)

用偏航角(yaw)和俯仰角(pitch)表示朝向,鼠标改变角度:

voidFpsCamera::OnMouse(floatdx,floatdy){yaw+=dx*sensitivity;pitch-=dy*sensitivity;pitch=clamp(pitch,-89.0f,89.0f);// 防止翻转和 LookAt 退化}mat4FpsCamera::GetView(){vec3 forward;forward.x=cos(radians(pitch))*cos(radians(yaw));forward.y=sin(radians(pitch));forward.z=cos(radians(pitch))*sin(radians(yaw));returnlookAt(position,position+forward,vec3(0,1,0));}

移动时用摄像机的前、右方向(通常把前方向压平到水平面,这样抬头走路不会飞起来)。

2. 第三人称(环绕跟随)

摄像机绕角色转动,保持一定距离:

mat4ThirdPersonCamera::GetView(vec3 playerPos){vec3 target=playerPos+vec3(0,1.6f,0);// 看向角色头部附近vec3 offset=SphericalToCartesian(yaw,pitch,distance);vec3 desired=target+offset;// 防穿墙:从目标向摄像机发射线,碰到墙就把摄像机拉近floathit;if(Physics::Raycast(target,normalize(offset),distance,&hit))desired=target+normalize(offset)*(hit-0.2f);// 平滑跟随:位置做指数插值,避免抖动position=lerp(position,desired,1.0f-exp(-followSpeed*dt));returnlookAt(position,target,vec3(0,1,0));}

3. 俯视角 / 等距视角(RTS、ARPG)

摄像机固定角度,只做平移和缩放:

vec3 eye=focusPoint+vec3(0,height,-height*0.8f);// 固定倾斜角mat4 view=lookAt(eye,focusPoint,vec3(0,1,0));

等距(Isometric)游戏通常再配合正交投影。

4. 过场动画

摄像机沿样条曲线运动,朝向用四元数插值:

vec3 pos=CatmullRom(p0,p1,p2,p3,t);quat rot=slerp(q1,q2,t);mat4 view=inverse(translate(pos)*mat4_cast(rot));// 直接对摄像机变换求逆

💡 插值时要分别插值位置和四元数,再构建矩阵。不要直接对两个视图矩阵做线性插值,否则旋转部分会变形、缩放。


四、视图矩阵的其他用途

1. 天空盒:去掉平移

天空盒应该"无限远",摄像机怎么移动都不应靠近它,所以只保留旋转:

mat4 skyView=mat4(mat3(view));// 取左上 3×3,平移清零

2. 公告板(Billboard):取摄像机轴

粒子、草、血条要始终面向摄像机。视图矩阵的前两行就是摄像机在世界空间中的右方向和上方向,直接取出即可:

vec3 camRight=vec3(view[0][0],view[1][0],view[2][0]);vec3 camUp=vec3(view[0][1],view[1][1],view[2][1]);// 四边形的四个角vec3 corner=center+camRight*(dx*size)+camUp*(dy*size);

血条、树木这类只想绕 Y 轴转的,把camUp换成(0,1,0)即可。

3. 鼠标拾取:逆视图矩阵

点击屏幕选中物体,要把屏幕坐标反推成世界空间的射线:

RayScreenToWorldRay(vec2 mouse,vec2 screen,mat4 proj,mat4 view){vec2 ndc={2.0f*mouse.x/screen.x-1.0f,1.0f-2.0f*mouse.y/screen.y};// 屏幕 Y 向下,需翻转vec4 clip={ndc.x,ndc.y,-1.0f,1.0f};vec4 eye=inverse(proj)*clip;eye=vec4(eye.x,eye.y,-1.0f,0.0f);// 方向向量,w = 0vec3 dir=normalize(vec3(inverse(view)*eye));vec3 origin=vec3(inverse(view)[3]);// 摄像机世界位置return{origin,dir};}

逆视图矩阵的第 4 列就是摄像机的世界坐标,这在着色器里也常用(计算高光、雾效时需要摄像机位置)。

4. 阴影贴图:光源的视图矩阵

渲染阴影时,要从光源的位置看一遍场景,记录深度:

// 平行光:沿光方向放一个虚拟摄像机,配正交投影mat4 lightView=lookAt(sceneCenter-lightDir*50.0f,sceneCenter,vec3(0,1,0));mat4 lightProj=ortho(-40,40,-40,40,0.1f,100.0f);mat4 lightVP=lightProj*lightView;

点光源要用 6 个视图矩阵(立方体贴图的 6 个面);级联阴影(CSM)则为每一级算一个光源视图矩阵,并让它包住主摄像机视锥的一段。

5. 平面反射(水面、镜子)

把摄像机按反射平面镜像,生成反射视图矩阵:

// 水面 y = h:摄像机位置镜像到水下,俯仰角取反vec3 reflEye=vec3(eye.x,2*h-eye.y,eye.z);vec3 reflTarget=vec3(target.x,2*h-target.y,target.z);mat4 reflView=lookAt(reflEye,reflTarget,vec3(0,1,0));

用通用反射矩阵view × Reflect(plane)时,镜像会翻转三角形绕序,需要把背面剔除方向反过来,并用斜裁剪平面裁掉水面以下的物体。

6. 视锥剔除

从proj × view直接提取 6 个裁剪平面(Gribb-Hartmann 方法),然后用包围盒或包围球测试,视野外的物体不提交渲染:

mat4 m=transpose(proj*view);// 按行取planes[0]=m[3]+m[0];// 左planes[1]=m[3]-m[0];// 右planes[2]=m[3]+m[1];// 下planes[3]=m[3]-m[1];// 上planes[4]=m[3]+m[2];// 近planes[5]=m[3]-m[2];// 远// 每个平面再除以 length(xyz) 归一化

7. 摄像机震动

爆炸、受击时的屏幕震动,在构建视图矩阵之前叠加一个局部偏移,不要改摄像机的逻辑位置:

vec3 shakeOffset=camRight*noise(t*25)*trauma+camUp*noise(t*25+100)*trauma;mat4 view=lookAt(position+shakeOffset,target+shakeOffset,up);

这样震动只影响画面,不影响射线检测、AI 视线等游戏逻辑。

8. 观察空间中的效果

很多效果在观察空间里计算更方便,因为摄像机在原点、深度就是 −z:

  • 距离雾:直接用观察空间深度计算雾浓度;
  • SSAO、SSR:从深度缓冲重建观察空间位置,在该空间采样;
  • 前向+ / 分簇光照:把光源变换到观察空间后按屏幕格子和深度分组。

9. 大世界精度:摄像机相对渲染

世界坐标很大时(例如距原点几十公里),float 精度不足,顶点会抖动。做法是让视图矩阵的平移部分为零,在 CPU 上用 double 先减去摄像机位置:

dvec3 camPos=camera.positionDouble;mat4 view=mat4(mat3(camera.rotationInverse));// 只有旋转for(auto&obj:objects){vec3 relPos=vec3(obj.positionDouble-camPos);// double 相减后再转 floatmat4 model=translate(relPos)*obj.rotationScale;Draw(obj,proj*view*model);}

《坎巴拉太空计划》、各类飞行模拟和开放世界游戏都使用这种方法(或"浮动原点"方案)。

10. 分屏与多视口

本地多人分屏、小地图、后视镜:同一帧里用不同的视图矩阵渲染多次,每次设置不同的视口或渲染目标即可。小地图通常是正上方的视图矩阵配正交投影。


五、常见坑

问题原因 / 解决
画面上下颠倒或左右镜像左手 / 右手坐标系混用;glm 默认右手,DirectXMath 有LH/RH两套函数
矩阵乘法顺序错误列向量约定是P × V × M × v;行向量约定(HLSL 常见)是v × M × V × P
俯仰到 90° 时画面乱转LookAt 中前方向与上方向平行,需限制角度或改用四元数
长时间旋转后画面变形反复累乘旋转矩阵产生误差,应每帧从角度或四元数重新构建
插值矩阵导致缩放分别插值位置和四元数,再构建矩阵
法线变换错误观察空间法线要用transpose(inverse(mat3(view × model)))

六、总结

应用视图矩阵的用法
FPS / 第三人称 / 俯视 / 过场不同方式计算摄像机位置和朝向,再构建视图矩阵
天空盒去掉平移,只保留旋转
公告板取矩阵行得到摄像机的右、上方向
鼠标拾取求逆,把屏幕射线还原到世界空间
阴影从光源位置构建视图矩阵
反射镜像摄像机
视锥剔除从proj × view提取裁剪平面
屏幕震动构建前叠加临时偏移
大世界平移清零,摄像机相对渲染

核心只有一句话:视图矩阵是摄像机变换的逆,它决定了"从哪里、朝哪个方向看世界"。游戏里所有摄像机、以及阴影、反射这类"从另一个视角看"的效果,本质上都是在构建不同的视图矩阵。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询