尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
OpenGL [ 坐标系 ]
前面的文章已经介绍了 VAO、VBO、EBO、Shader、纹理和矩阵。现在先把矩阵放到一边回答一个更基础、也更容易混乱的问题OpenGL 里的坐标到底是什么一个顶点从模型文件进入顶点着色器以后经过哪些坐标空间最后怎样变成窗口中的像素很多“物体不见了”“上下颠倒”“摄像机方向反了”“窗口比例变形”表面上是代码问题根源其实是把不同坐标系混成了一个坐标系。本文按“概念讲解 → 官方语义 → 实际代码 → 单点追踪 → 常见误区”的方式把现代 OpenGL 3.3 Core Profile 的坐标链路完整走一遍。本文的核心判断OpenGL 不替应用程序规定世界坐标应用程序自己定义局部空间和世界空间。OpenGL 真正规定的是从裁剪空间开始的固定流程gl_Position中的齐次坐标经过裁剪、透视除法得到 NDC再经过glViewport和深度范围映射到窗口坐标。本文使用 C17、GLFW、GLAD 2、GLM 和 OpenGL 3.3 Core Profile。重点是坐标约定与空间转换不重复展开 VAO、纹理和 Shader 编译的完整工程配置。一、先看完整坐标链路一个顶点通常会经过下面五【六详细来说多一个 ndc 转化】个概念空间为了将坐标从一个坐标系变换到另一个坐标系我们需要用到几个变换矩阵最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。我们的顶点坐标起始于局部空间(Local Space)在这里它称为局部坐标(Local Coordinate)它在之后会变为世界坐标(World Coordinate)观察坐标(View Coordinate)裁剪坐标(Clip Coordinate)并最后以屏幕坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么局部坐标是对象相对于局部原点的坐标也是物体起始的坐标。下一步是将局部坐标变换为世界空间坐标世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点它们会和其它物体一起相对于世界的原点进行摆放。接下来我们将世界坐标变换为观察空间坐标使得每个坐标都是从摄像机或者说观察者的角度进行观察的。坐标到达观察空间之后我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内并判断哪些顶点将会出现在屏幕上。最后我们将裁剪坐标变换为屏幕坐标我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器将其转化为片段。你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如当需要对物体进行修改的时候在局部空间中来操作会更说得通如果要对一个物体做出一个相对于其它物体位置的操作时在世界坐标系中来做这个才更说得通等等。如果我们愿意我们也可以定义一个直接从局部空间变换到裁剪空间的变换矩阵但那样会失去很多灵活性。对应的数学过程是p_local vec4(position, 1) p_world Model * p_local p_view View * p_world p_clip Projection * p_view p_ndc p_clip.xyz / p_clip.w p_window Viewport(p_ndc)在现代 Core Profile 中前面的 Model、View、Projection 通常由应用程序使用 GLM 计算再作为uniform mat4上传给顶点着色器。OpenGL 固定功能阶段从gl_Position开始接管后续裁剪、透视除法和视口映射。二、OpenGL 的基础轴向约定1. 三维坐标轴OpenGL 本身允许应用程序建立自己的世界坐标约定但常见的 OpenGL 教程和 GLM 用法通常使用右手坐标系在经典 OpenGL 观察约定中摄像机位于观察空间原点朝向-Z。因此一个默认位于摄像机前方的物体通常放在世界坐标的负 Z 方向glm::vec3 objectPosition(0.0f, 0.0f, -2.0f);这不是“所有 OpenGL 程序只能这样做”。你可以定义不同的世界坐标但 Model、View、Projection、摄像机控制和资源导入必须统一。2. 左手和右手不是 API 自动切换左手、右手主要是应用程序和数学库的坐标约定。OpenGL 3.3 Core Profile 不会读取一个“我是左手坐标系”的全局开关然后替你修正所有矩阵。真正重要的是顶点数据使用哪套轴向glm::lookAt的 eye、center、up 是否按同一套轴向传入投影矩阵的深度范围是否和 API 约定一致正面方向、背面剔除和法线计算是否采用一致的绕序。3. 纹理坐标不是世界坐标纹理 UV 通常用二维(u, v)表示范围常见为[0, 1]。它描述“从纹理的哪个位置取颜色”不表示模型在世界中的位置世界坐标位置、方向、距离 纹理坐标图片采样位置图片上下颠倒时通常是图像文件原点、纹理坐标约定和stb_image翻转选项没有统一不是 OpenGL 的三维坐标轴自动倒置。三、局部空间模型自己的坐标1. 什么是局部坐标模型文件中的顶点通常以模型自身的原点、轴和单位描述const glm::vec3 localVertex(0.0f, 1.0f, 0.0f);这个顶点只表示“模型局部坐标中原点上方一个单位的位置”。它还不知道模型在场景中的位置也不知道摄像机在哪里。局部空间有两个好处模型可以围绕自己的原点旋转和缩放同一个网格可以使用不同 Model 矩阵放到多个世界位置。2. Model 矩阵建立世界位置#include glm/glm.hpp #include glm/gtc/matrix_transform.hpp glm::mat4 model(1.0f); model glm::translate(model, glm::vec3(2.0f, 0.0f, -3.0f)); model glm::rotate(model, glm::radians(30.0f), glm::vec3(0.0f, 1.0f, 0.0f)); model glm::scale(model, glm::vec3(0.5f));在 GLM/OpenGL 的列向量写法中最终可理解为p_world Model * p_localModel 矩阵不是“摄像机矩阵”也不是“把顶点投影到屏幕”的矩阵。它只负责把模型从自己的局部空间放进应用程序建立的世界空间。3. 世界空间是应用程序的概念OpenGL 规范明确描述了从顶点处理到裁剪和视口映射的过程但并没有一个叫“世界空间”的 OpenGL 状态槽位。世界空间是应用程序为了组织场景而建立的中间概念模型 A 的世界位置 模型 B 的世界位置 灯光的世界位置 摄像机的世界位置因此可以有多个 Model 矩阵却通常共享同一套 View 和 Projection。四、观察空间摄像机看到的坐标1. OpenGL 没有一个“摄像机对象”从 OpenGL 的角度看顶点着色器只接收顶点并输出gl_Position。摄像机对象、位置、朝向和移动速度都是应用程序的数据结构。我们通常使用 View 矩阵把世界变换到摄像机空间p_view View * p_world从效果上看摄像机向右移动世界中的物体会向左移动摄像机向后移动物体会显得更近。View 矩阵通常是摄像机变换的逆变换。2. 使用glm::lookAt#include glm/gtc/matrix_transform.hpp glm::vec3 cameraPosition(0.0f, 0.0f, 3.0f); glm::vec3 cameraTarget(0.0f, 0.0f, 0.0f); glm::vec3 worldUp(0.0f, 1.0f, 0.0f); glm::mat4 view glm::lookAt( cameraPosition, cameraTarget, worldUp);glm::lookAt使用 eye、center 和 up 构造观察矩阵。GLM 官方手册把它列为gluLookAt的现代替代函数。如果摄像机保存的是前方向glm::vec3 cameraFront(0.0f, 0.0f, -1.0f); glm::mat4 view glm::lookAt( cameraPosition, cameraPosition cameraFront, worldUp);3. 判断物体是否在摄像机前方默认情况下摄像机朝-Z看摄像机空间 Z 0 通常是摄像机前方 Z 0 通常是摄像机后方真正决定是否可见的是投影矩阵和裁剪测试所以不要只凭世界坐标的z值判断。正确的排查方式是把点转换到 View Space 和 Clip Space检查它是否落在视锥体内。五、裁剪空间顶点着色器输出的最后结果1.gl_Position是什么顶点着色器必须写入#version 330 core layout (location 0) in vec3 aPosition; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position projection * view * model * vec4(aPosition, 1.0); }GLSL 3.30 规范将gl_Position定义为用于写入齐次顶点位置的内建输出。它的值会被后续的图元装配、裁剪和固定功能阶段使用如果顶点着色器没有写它后续结果是未定义的。2. 为什么不能直接把世界坐标写给gl_Position简单的 Hello Triangle 可以直接使用已经处在 NDC 范围附近的坐标gl_Position vec4(aPosition, 1.0);但真正的 3D 场景需要考虑摄像机、深度和透视gl_Position projection * view * model * vec4(aPosition, 1.0);gl_Position中的是 Clip Space 坐标不是最终的屏幕像素坐标。它还要经过裁剪保留视锥体范围内的图元透视除法x/w、y/w、z/w视口映射转换到窗口像素。3. Clip Space 的范围为什么带wOpenGL 进行裁剪时使用齐次坐标关系常见理解是-w ≤ x ≤ w -w ≤ y ≤ w -w ≤ z ≤ w这里的w不是固定的 1。透视投影正是通过让w随深度变化再在后续除以w产生“远处变小”的效果。六、NDC透视除法之后的标准立方体1. 从 Clip Space 到 NDC透视除法由固定功能阶段完成x_ndc x_clip / w_clip y_ndc y_clip / w_clip z_ndc z_clip / w_clip得到的 NDCNormalized Device Coordinates通常在三个轴上都使用[-1, 1]左x -1 右x 1 下y -1 上y 1 近z -1 远z 1OpenGL 3.3 Core Profile 规范把透视除法和视口映射列为顶点处理之后的固定步骤。NDC 不是像素坐标原点在视口中心范围也与窗口分辨率无关。2. 为什么改变窗口大小不应该改变 NDCNDC 是归一化空间。同一个 NDC 点(0, 0, 0)在 800×600 和 1600×1200 的窗口中都位于视口中心只是对应的像素位置不同。这让投影矩阵和视口设置可以分工Projection 负责把相机可见体积变成 NDCglViewport负责把 NDC 映射到当前 framebuffer 的像素区域。七、窗口空间glViewport和深度范围1. 视口映射设置视口glViewport(0, 0, framebufferWidth, framebufferHeight);OpenGL 将 NDC 映射到窗口坐标。对于 x、y核心公式可以理解为x_window (x_ndc 1) * viewportWidth / 2 viewportX y_window (y_ndc 1) * viewportHeight / 2 viewportY因此 NDC 的(-1, -1)对应视口左下角(1, 1)对应视口右上角。窗口坐标的单位是 framebuffer 像素。2. 视口不等于窗口逻辑尺寸高 DPI 屏幕上GLFW 的窗口逻辑大小和 framebuffer 像素大小可能不同。应在 framebuffer size callback 中更新static void framebufferSizeCallback( GLFWwindow* window, int width, int height) { (void)window; glViewport(0, 0, width, height); }投影矩阵的 aspect 也应该使用 framebuffer 宽高const float aspect static_castfloat(width) / static_castfloat(height);3. z_window 和深度范围深度范围由glDepthRange控制默认是[0, 1]glDepthRange(0.0, 1.0);可以把 NDC 的 z 映射理解为z_window (z_ndc 1) / 2窗口 z 会被深度缓冲以相应精度存储。颜色坐标的 y 方向、纹理坐标的 v 方向和窗口坐标的原点不要混为一谈它们来自不同阶段的约定。八、用一个点追踪完整坐标变化为了不让坐标空间停留在名词层面假设局部空间有一个顶点glm::vec4 local(1.0f, 0.0f, 0.0f, 1.0f);1. Model放到世界空间glm::mat4 model(1.0f); model glm::translate(model, glm::vec3(0.0f, 0.0f, -2.0f)); glm::vec4 world model * local;这个点从(1, 0, 0)变成世界坐标中的(1, 0, -2)。2. View从摄像机角度观察glm::mat4 view glm::lookAt( glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)); glm::vec4 eye view * world;现在eye表示这个点在摄像机坐标中的位置。摄像机的位置并没有作为某个 OpenGL “摄像机状态”被传入它已经被 View 矩阵编码进去了。3. Projection进入裁剪空间glm::mat4 projection glm::perspective( glm::radians(45.0f), 16.0f / 9.0f, 0.1f, 100.0f); glm::vec4 clip projection * eye; glm::vec3 ndc glm::vec3(clip) / clip.w;调试时可以打印每一步std::cout world: world.x , world.y , world.z \n; std::cout eye: eye.x , eye.y , eye.z \n; std::cout clip: clip.x , clip.y , clip.z , w clip.w \n; std::cout ndc: ndc.x , ndc.y , ndc.z \n;如果 NDC 已经超出[-1, 1]物体的一部分或全部可能被裁剪。这个排查方法比盲目移动模型更可靠先确定它在哪一个空间开始出错。4. 计算窗口像素位置GLM 还提供与传统gluProject对应的函数#include glm/gtc/matrix_transform.hpp const glm::ivec4 viewport( 0, 0, framebufferWidth, framebufferHeight); const glm::vec3 windowPoint glm::project( glm::vec3(local), model, projection * view, viewport);这里第二个矩阵参数是 Model第三个是 View 与 Projection 的组合。glm::project适合调试“模型中的某个点对应屏幕哪里”但实际绘制仍然由顶点着色器和 OpenGL 管线完成。九、坐标和 Shader 的最小连接1. 顶点 Shader#version 330 core layout (location 0) in vec3 aPosition; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position projection * view * model * vec4(aPosition, 1.0); }2. CPU 上传矩阵#include glm/gtc/type_ptr.hpp const GLint modelLocation glGetUniformLocation(program, model); const GLint viewLocation glGetUniformLocation(program, view); const GLint projectionLocation glGetUniformLocation(program, projection); glUseProgram(program); glUniformMatrix4fv(modelLocation, 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(viewLocation, 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, glm::value_ptr(projection));3. C / GLSL 之间的约定CPU GLM mat4 ↓ glm::value_ptr OpenGL uniform mat4 ↓ glsl projection * view * model ↓ gl_Position常规 GLM 与 OpenGL 用法中glUniformMatrix4fv的transpose传GL_FALSE。如果把GL_TRUE当成“让 OpenGL 帮我适配矩阵”随便打开常常会把本来正确的矩阵转置成错误结果。十、常见坐标系误区1. 把世界空间当成 OpenGL 内置空间OpenGL 不会自动保存一个“世界矩阵”。世界空间是应用程序定义的组织方式必须由 Model 和 View 明确构造。2. 把gl_Position当成屏幕像素错误理解gl_Position vec4(400.0, 300.0, 0.0, 1.0);这不会把点放到 400×300 像素。gl_Position是 Clip Space 齐次坐标屏幕像素要等到透视除法和glViewport之后才出现。3. 把[-1, 1]同时套在所有坐标上Local、World、View 空间的范围由应用程序场景决定可能是几厘米也可能是几千公里。只有 NDC 是标准化范围不能把 NDC 范围错误地当成世界坐标限制。4. 摄像机向前移动却把世界坐标加到Z在常见 OpenGL 约定下摄像机朝-Z物体放在负 Z 方向更容易进入视锥体。若采用自己的轴向也必须同步修改 View、Projection 和控制逻辑。5. 忘记w和透视除法vec4(position, 1.0)是点的齐次表示。Projection 生成的w参与裁剪和透视缩小。自己在 CPU 中调试时如果直接看clip.xyz而不除以clip.w就还没有得到 NDC。6. framebuffer 大小和窗口大小混用高 DPI 环境下窗口回调的逻辑尺寸可能不是 framebuffer 像素尺寸。glViewport和投影 aspect 使用错误尺寸会造成画面拉伸或只占窗口一部分。7. 物体上下颠倒就去改 Model 矩阵先判断问题发生在哪个阶段顶点几何本身反了检查 Model/坐标轴图片内容反了检查 UV 和图像加载翻转屏幕输出方向反了检查 framebuffer/窗口系统约定。不要用一个阶段的修正去掩盖另一个阶段的坐标错误。十一、一个可执行的排查顺序画面不对时建议固定按以下顺序检查Local顶点数组中的点是否符合预期WorldModel * local是否把物体放到了正确位置View摄像机 eye、center、up 是否合理Clipprojection * view * world的w是否有效NDC除以w后是否落在视锥体范围附近ViewportglViewport是否使用当前 framebuffer 尺寸Depth是否启用了深度测试并清理了深度缓冲Winding是否因为顶点绕序和面剔除被丢弃可以在 CPU 中打印world、eye、clip和ndc也可以在 Shader 调试阶段把位置映射为颜色vec3 debugColor gl_Position.xyz / gl_Position.w; fragColor vec4(debugColor * 0.5 0.5, 1.0);这段只适合临时调试不能替代真正的片元颜色输出。十二、坐标空间速查表空间常见表示谁负责产生主要用途Local / Objectvec3/vec4模型数据描述模型本身WorldModel * local应用程序组织场景View / EyeView * worldView 矩阵以摄像机为原点观察ClipProjection * view顶点 Shader裁剪和透视除法之前NDCclip.xyz / clip.wOpenGL 固定功能标准化设备范围WindowglViewport映射OpenGL 固定功能窗口像素和深度缓冲Texture UVvec2网格与 Shader图片采样位置坐标系真正要掌握的不是背下“左手还是右手”而是知道一个值当前属于哪个空间、下一步由谁解释以及它应该使用什么范围。沿着 Local → World → View → Clip → NDC → Window 逐步追踪绝大多数 OpenGL 位置问题都能定位。官方资料与延伸阅读OpenGL 3.3 Core Profile SpecificationThe OpenGL Shading Language 3.30 SpecificationKhronos OpenGL Reference PagesGLM 官方手册OpenGL FAQ: TransformationsOpenGL FAQ: Viewing and Camera Transforms
RELATED

相关推荐

普通人也能低成本入局AI大模型,抢占千亿红利赛道!

普通人也能低成本入局AI大模型,抢占千亿红利赛道!

文章指出,国产AI大模型已从研发阶段走向商业化落地,市场规模预计将在2026年突破680亿元,2030年攀升至3250亿元。国内大模型落地数量全球领先,用户调用量持续暴涨,商业化落地不再是空谈。AI大模型应用开发工程师成为普通…

📅 2026/10/9 17:01:35
双活数据中心原理图制作指南:存储双活、仲裁机制与PPT改造技巧

双活数据中心原理图制作指南:存储双活、仲裁机制与PPT改造技巧

简介:这是一份聚焦双活数据中心整体设计的原理图PPT,原图内容可修改,适合数据中心运维、架构规划及网络/存储工程师用于方案讲解与二次整理。资源共1个ppt文件,约3.77MB,以架构拓扑图为主,清晰拆解主故障域…

📅 2026/10/9 16:56:34
MySQL GROUP_CONCAT 长度截断与调优避坑实战指南

MySQL GROUP_CONCAT 长度截断与调优避坑实战指南

简介:PDF教程以MySQL中GROUP_CONCAT聚合函数为主线,面向需要把多行查询结果合并为单个字符串的开发者和数据库使用者,帮助简化一对多查询的数据展示与汇总。内容借助文章分类、文章、附件三张表的真实案例,先呈现一对多关联查询返…

📅 2026/10/9 16:56:34
MORE NEWS

更多资讯

📰

校园线上超市小程序源码拆解:从业务模型到二次开发实践

记得第一次拿到这类“可白嫖源码”的校园线上超市平台项目,我的操作和大多数人一样:解压、打开微信开发者工具、导入、然后坐等报错。等我把页面加载出来、点了几下能正常跳转之后,才意识到自己对这套系统的理解还停留在“能跑”的阶段。项目…

📰

GB/T 31455.7-2025 BRT信号优先通信接口标准化技术解读与工程实践

GB/T 31455.7-2025这个编号放在智能交通圈里,懂行的人一眼就能看出分量。BRT公交与交通信号通信接口的标准化升级,说到底解决的是快速公交在路口"能不能优先、怎么优先、优先之后怎么闭环"这一整条链路的问题。国内这些年做BRT信号优先的项目很…

📰

PMP备考教材全攻略:PMBOK指南、辅助材料与刷题资源的高效使用策略

1. 先搞清楚PMP备考的教材体系到底分几层很多人一上来就问“该买哪本书”,这个问题本身就问偏了。PMP备考的教材不是一个单层结构,而是分成三个层次:官方指定教材、辅助理解材料、刷题与模拟资源。这三层各有各的用途,缺一层都会在…

📰

短剧APP开发方案:广告解锁与变现架构全解析

过去一年做APP开发社区里聊得最多的方向之一,就是短剧。从去年开始,“看广告解锁短剧”这类产品密集上线,本质上把短视频的碎片化消费和广告变现做成了闭环:用户不用掏钱,看完一条30秒广告就能解锁下一集;平…

📰

技术人做自媒体:把技术能力转化为内容资产

做技术的人,对“反馈”这件事特别敏感。你写完一个函数,跑一遍,要么对要么报错,反馈是即时的;你优化完一个接口,压测数据上来,吞吐量涨了就是涨了。这就是为什么很多技术兄弟一开始碰自媒体会非…

📰

再见Fable 5,OpenAI出手了!GPT-5.6真香~用TaoToken统一Key跑Codex agent

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬