OpenGL系列(六)变换

news2024/11/24 6:50:02

    在三角形和纹理贴图示例中,顶点使用的是归一化设备坐标,在该坐标系下,顶点的每个轴的取值为-1到1,超出范围的顶点不可见。

    基于归一化设备坐标的物体的形状随着设备的大小变换而变化,这里产生的第一个问题是,本来要画一个正方形,顶点为(-0.5,0.5)、(0.5,0.5)(0.5,-0.5)(-0.5,-0.5),由于显示设备的宽与高不一致,结果显示出来的是一个长方形。即使针对某个设备定义好了一组顶点,显示出来刚好是正方形,可以在不一样尺寸显示的却是长方形。

    基于归一化坐标系定义物体顶点遇到的另一个问题是,无法让物体动起来,如果确实要动起来,需要为每一帧画面定义新的顶点数据,这个工作量非常大,一般不会这么操作。

     为了更好的绘制出想要的物体,一般在物体本身的坐标系定义顶点数据以确定物体的形状,再经过一系列的变换映射到归一化设备坐标系,在设备上才会显示期望的效果。通过改变变换的参数,顶点不用改动,物体显示效果也不一样,因此通过不断改变变换参数可以让物体动起来。

    变换是从一种坐标系映射到另一种坐标系的过程,接下来介绍变换过程中涉及到的坐标系。

坐标系

  1. Local Space(局部空间),局部空间基于局部坐标系。局部坐标系是指相对于某个物体或者某个参考点而言的坐标系。它是通过将物体的中心或者某个特定点作为原点,以物体自身的方向作为坐标轴来定义的。局部坐标系通常用于描述物体的内部结构或者变换。
  2. World Space(世界空间),世界空间基于世界坐标系。世界坐标系是指全局的坐标系,它是以整个场景或者世界空间作为参考的坐标系。世界坐标系通常是固定的,用于描述场景中各个物体之间的相对位置关系。
  3. View Space(观察空间),观察空间针对摄像机而已,把摄像机比作眼睛,在世界坐标系中不同的位置朝着不同的方向观察到的景象是不一样的,需要使用一个独立的坐标系来表示物体在观察者视图中的位置,这就是观察坐标系。
  4. Clip Space (裁剪空间),   裁剪空间的主要作用是减少处理的计算量,提高图形处理的效率。通过裁剪空间,可以只对感兴趣的图像区域进行处理,而不用对整个图像进行操作。这样可以节省计算资源。裁剪空间使用归一化设备坐标系(Normalized Device Coordinates ),每个轴的范围为-1到1,超出范围的图形不显示。
  5. Screen Space(屏幕空间),屏幕空间主要针对具体的显示设备,解决的是图形中的每个像素在屏幕中的位置。不同设备屏幕的宽、高、原点、及轴方向会有差异,经过渲染后的图形需要映射到屏幕坐标才能正确显示出来。

变换过程

    了解坐标系后,接下来介绍物体在各个坐标之间的变换过程,以三角形为例。

    首先在局部坐标系定义三角形的顶点数据,在局部坐标系中根据期望的形状来定义三角形的顶点数据,数据的取值范围不再局限在-1~1之间。

    模型矩阵

    接下来把三角形从局部坐标系映射到世界坐标系,这一步映射的目的是确定三角形在世界坐标系中的位置、占据的空间及朝向,从局部坐标系映射到世界坐标系通过模型矩阵(Model Matrix)进行变换,模型矩阵可由位移矩阵、缩放矩阵、旋转矩阵进行结合:比如只是简单把三角形移动到世界坐标的(1,0,0),模块矩阵使用位移矩阵即可;如何三角形移动到(1,0,0)后再缩小一半,模型矩阵为位移矩阵乘缩放矩阵;如果三角形移动到(1,0,0)后再缩小一半最后再绕z轴旋转90度,模型矩阵为位移矩阵乘缩放矩阵再乘旋转矩阵。

    值得注意的是矩阵乘法是由先后顺序的,顺序不一样,一般情况下结果也不一样。比如先位移再缩放和先缩放再位移结果是不一样的。

    观察矩阵

   三角形的顶点坐标映射到世界坐标系后,观察者在不同的位置超着不同的方向看到的结果是不一样的,因此需要从世界坐标系映射到观察坐标系。这一步映射使用视图矩阵(View Matrix)进行变换。观察矩阵需要通过3个向量才能确定:观察者的位置,观察中心点,穹顶向量,其中穹顶向量的作用是观察者头部的朝向。

    投影矩阵

    有了观察矩阵,观察者的位置、观察的中心点及朝向都已确定,此时观察者可以看到正前方的物体,但是这里还有视野的问题,观察者的眼睛睁大和眯成一条缝,看到的结果也是不一样的,睁大了可以看到整个三角形,眯成一条缝可能只看到三角形的一部分。视野在这里使用投影矩阵表示。

    投影分为正交投影(orthographic projection)和透视投影(perspective projection)。

    正交投影:在该投影方式下,物体的大小与距离无关。

    透视投影:在该投影方式下,物体靠观察者越近,大小越大。

    

    经过投影矩阵的映射后,得到的是NDC坐标,此时可以开始绘制了。接下来通过示例来加深对变换过程的理解。

示例

    在本示例中绘制立方体,首先定义立方体的顶点数据


struct Vertex {
    glm::vec3 Position;
    glm::vec3 Color;
};

struct Transform {
    glm::vec3 Pose;
    glm::vec3 Scale;
};

constexpr glm::vec3 Red{1, 0, 0};
constexpr glm::vec3 DarkRed{0.25f, 0, 0};
constexpr glm::vec3 Green{0, 1, 0};
constexpr glm::vec3 DarkGreen{0, 0.25f, 0};
constexpr glm::vec3 Blue{0, 0, 1};
constexpr glm::vec3 DarkBlue{0, 0, 0.25f};

// Vertices for a 1x1x1 meter cube. (Left/Right, Top/Bottom, Front/Back)
constexpr glm::vec3 LBB{-0.5f, -0.5f, -0.5f};
constexpr glm::vec3 LBF{-0.5f, -0.5f, 0.5f};
constexpr glm::vec3 LTB{-0.5f, 0.5f, -0.5f};
constexpr glm::vec3 LTF{-0.5f, 0.5f, 0.5f};
constexpr glm::vec3 RBB{0.5f, -0.5f, -0.5f};
constexpr glm::vec3 RBF{0.5f, -0.5f, 0.5f};
constexpr glm::vec3 RTB{0.5f, 0.5f, -0.5f};
constexpr glm::vec3 RTF{0.5f, 0.5f, 0.5f};

#define CUBE_SIDE(V1, V2, V3, V4, V5, V6, COLOR) {V1, COLOR}, {V2, COLOR}, {V3, COLOR}, {V4, COLOR}, {V5, COLOR}, {V6, COLOR},

constexpr Vertex c_cubeVertices[] = {
    CUBE_SIDE(LTB, LBF, LBB, LTB, LTF, LBF, DarkRed)    // -X
    CUBE_SIDE(RTB, RBB, RBF, RTB, RBF, RTF, Red)        // +X
    CUBE_SIDE(LBB, LBF, RBF, LBB, RBF, RBB, DarkGreen)  // -Y
    CUBE_SIDE(LTB, RTB, RTF, LTB, RTF, LTF, Green)      // +Y
    CUBE_SIDE(LBB, RBB, RTB, LBB, RTB, LTB, DarkBlue)   // -Z
    CUBE_SIDE(LBF, LTF, RTF, LBF, RTF, RBF, Blue)       // +Z
};

    c_cubeVertices表示立法体的顶点数据,由6个面组成。

    CUBE_SIZE表示立法体的一个面,由3个2角形构成,需要6个顶点,外加该面的颜色

    接下来定义绘制立法体的顶点索引,如下所示。

constexpr unsigned short c_cubeIndices[] = {
    0,  1,  2,  3,  4,  5,   // -X
    6,  7,  8,  9,  10, 11,  // +X
    12, 13, 14, 15, 16, 17,  // -Y
    18, 19, 20, 21, 22, 23,  // +Y
    24, 25, 26, 27, 28, 29,  // -Z
    30, 31, 32, 33, 34, 35,  // +Z
};

   

    在这里所有的变换由着色器进行处理,着色器的定义如下。

    static const char* strVs = R"_(#version 300 es

    in vec3 VertexPos;
    in vec3 VertexColor;
    out vec3 PSVertexColor;
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;

    void main() {
       gl_Position = projection * view * model * vec4(VertexPos, 1.0);
       PSVertexColor = VertexColor;
    }
    )_";


    static const char* strFs = R"_(#version 300 es
    in lowp vec3 PSVertexColor;
    out lowp vec4 FragColor;

    void main() {
       FragColor = vec4(PSVertexColor, 1);
    }
    )_";

    由于变换只针对顶点,因此 只需在顶点着色器中对进行做变换,在着色器有3个变换矩阵。

  1. model为模型矩阵。
  2. view为视图矩阵。
  3. projection为投影矩阵。

    在顶点着色器中,模型矩阵先与顶点向量先乘,得到新的向量再与视图矩阵相乘,最后于投影矩阵相乘,因此顶点变换的另一种写法如下。

    gl_Position = projection * ( view * (model * vec4(VertexPos, 1.0)));

    这个写法更容易体现model、view、projection与向量相乘的顺序。

    接下来把顶点数据绑定到VBO、VAO和EBO中,

    auto coordsLocation = mShader->getAttribLocation("VertexPos");
    auto colorLocation = mShader->getAttribLocation("VertexColor");

    //init VBO VAO EBO
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(c_cubeVertices), c_cubeVertices, GL_STATIC_DRAW);
    glGenBuffers(1,&EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(c_cubeIndices), c_cubeIndices, GL_STATIC_DRAW);
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    LOGI("TransformSample::init coordsLocation %d colorLocation %d",coordsLocation,colorLocation);
    glEnableVertexAttribArray(coordsLocation);
    glEnableVertexAttribArray(colorLocation);
    glVertexAttribPointer(coordsLocation, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)0);
    glVertexAttribPointer(colorLocation, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
                          reinterpret_cast<const void*>(sizeof(glm::vec3)));

    VBO、VAO已经对这部分内容做过介绍,不一样的地方是顶点属性的序号并没有在着色器通过location指定,这里通过getAttribLocation获取属性的序号。

    顶点数据和着色器准备好以后,在绘制时确定变换矩阵,接下来确定要绘制的立方体,如下所示。

    cubes.push_back(Transform{{-1.5,-0.5,0}, {0.5f, 0.5f, 0.5f}});//25cm
    cubes.push_back(Transform{{0,-0.3,0}, {0.5f, 0.5f, 0.5f}});
    cubes.push_back(Transform{{1.5,0.5,0}, {0.55f, 0.55f, 0.55f}});

    这里要绘制3个立法体:第1个平移到位置(-1.5, -0.5, 0),各个轴大小都缩小到原来的0.5倍;第2个平移到(0,-0.3,0),各个轴大小同样缩小为原来的0.5倍;第3个平移到(1.5, 0.5, 0),大小缩小为原来的0.55倍。

    glm::mat4 view = glm::lookAt(glm::vec3(0,0,3),
                                 glm::vec3(0,0,0),
                                 glm::vec3(0,1,0));
    glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)mWidth / (float)mHeight, 0.1f, 1000.0f);

    mShader->setMatrix4f("view", view);
    mShader->setMatrix4f("projection", projection);
    static float angle = 0;
    int i = 0;
    for(auto cube: cubes){
       //pass  identity matrix
       glm::mat4 model(1.0f);
       model = glm::translate(model,cube.Pose);
       model = glm::scale(model, cube.Scale);
       if(i%3==0){
           model = glm::rotate(model,glm::radians(angle),glm::vec3(1,0,0));
       }else if(i%3 == 1){
           model = glm::rotate(model,glm::radians(angle),glm::vec3(0,1,0));
       }else{
           model = glm::rotate(model,glm::radians(angle),glm::vec3(0,0,1));
       }
       i++;
       mShader->setMatrix4f("model", model);
       glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, nullptr);
    }

  通过lookAt可以获得视图矩阵。

glm::mat4 view = glm::lookAt(glm::vec3(0,0,3), glm::vec3(0,0,0), glm::vec3(0,1,0));

第1个参数表示观察者位置,第2个参数表示观察中心点,第3个参数表示穹顶向量。

    通过perspective可获得透视投影矩阵。

 glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)mWidth / (float)mHeight, 0.1f, 1000.0f);

    第1个参数表示视野角度,第2个参数表示宽高比例,第3个参数表示最近距离,第4个参数表示最远距离。
 

    对于模型矩阵,首先定义一个初始的单位矩阵,glm::mat4 model(1.0f),先进行平移变换 model = glm::translate(model,cube.Pose); 接着进行缩放变换model = glm::scale(model, cube.Scale); 最后通过glm::rotate进行旋转变换,这里分3种情况,第1个绕x轴旋转,第2个绕y轴旋转,第3个绕z轴旋转。

    这些模型矩阵通过Shader类的setMatrix4f传递到着色器。着色器拿到这些矩阵后,执行变换后可得到期望的效果,示例运行效果图如下所示。

      从效果图可以看到,显示的是一个立方体,通过不断改变旋转变换矩阵以达到不断旋转的动画效果。  

  

本示例工程代码已上传到github,地址如下。

示例工程代码icon-default.png?t=N7T8https://github.com/leesino/samples/tree/main/OpenGL/Transformations

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1830498.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Semantic Kernel 直接调用本地大模型与阿里云灵积 DashScope

本文主要介绍如何在无需网关&#xff0c;无需配置 HttpClient 的情况下&#xff0c;使用 Semantic Kernel 直接调用本地大模型与阿里云灵积 DashScope 等 OpenAI 接口兼容的大模型服务。 1. 背景 一直以来&#xff0c;我们都在探索如何更好地利用大型语言模型&#xff08;LLM&…

如何免费试用阿里云的视频画质增强服务50元额度

上文有说到阿里云有画质增强的服务&#xff0c;我也试了&#xff0c;确实画质提升不少。 本文讲解如何免费试用视频画质增强服务。 首先我们得有一个阿里云的账号&#xff0c;大家自行注册&#xff1a; 阿里云-计算&#xff0c;为了无法计算的价值 注册好后我们打开阿里云的视频…

12.3 Go 测试覆盖率

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

【BES2500x系列 -- RTX5操作系统】系列文章索引

&#x1f48c; 所属专栏&#xff1a;【BES2500x系列】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f49…

可视化大屏搞这样,是对前端开发尊严的巨大挑战。

现在可视化大屏不搞点炫酷的效果和3D交互&#xff0c;出门都不好意思给别人打招呼&#xff0c;作为前端领域的老司机&#xff0c;我感觉尊严受到了巨大挑战&#xff0c;必须迎难而上&#xff0c;hold住他们&#xff0c;老铁们你们觉得呢&#xff1f;

Nuxt快速学习开发 -- Nuxt3配置

Nuxt配置 nuxt.config.ts文件位于 Nuxt 项目的根目录下&#xff0c;可以覆盖或扩展应用程序的行为 使用可组合项&#xff0c;这些变量会暴露给应用程序 //nuxt.config.ts import { fileURLToPath } from "url"; ​ export default defineNuxtConfig({alias: {//配置…

LeetCode | 344.反转字符串

设置头尾两个指针&#xff0c;依靠中间变量temp交换头尾指针所指元素&#xff0c;头指针后移&#xff0c;尾指针前移&#xff0c;直到头尾指针重合或者头指针在尾指针后面一个元素 class Solution(object):def reverseString(self, s):""":type s: List[str]:r…

大数据量列表渲染优化:前端实战经验让性能飙升50%,页面速度提升95%

引言&#xff1a;在处理大规模数据集渲染时&#xff0c;前端性能常常面临巨大的挑战。本文将探讨 react-virtualized-list 库如何通过虚拟化技术和 Intersection Observer&#xff0c;实现前端渲染性能飙升 50% 的突破&#xff0c;页面渲染速度提升 95% &#xff01;&#x1f5…

智能化软件开发微访谈·第三十一期 代码大模型训练、微调与增强

CodeWisdom “智能化软件开发沙龙是由CodeWisdom团队组织的围绕智能化软件开发、数据驱动的软件开发质量与效能分析、云原生与智能化运维等相关话题开展的线上沙龙&#xff0c;通过微信群访谈交流等线上交流方式将学术界与工业界专家学者汇聚起来&#xff0c;共同分享前沿研究进…

【Linux】使用 iptables 验证访问HDFS 所使用到的端口

目录 ​编辑 一、实操背景 二、iptables 简介 三、模拟操作 一、实操背景 背景&#xff1a; 在客户有外网的服务器需要访问内网大数据集群HDFS&#xff0c;使用iptable模拟测试需要开放的端口。 二、iptables 简介 具体介绍看文章&#xff1a; 【Linux】Iptables 详解与实战…

性能工具之 JMeter 常用组件介绍(七)

文章目录 一、后置处理器1、Regular Expression Extractor(正则表达式提取器)2、JSON Extractor(JSON表达式提取器)3、Regular Expression Extractor(正则表达式提取器) 二、小结 一、后置处理器 从上面可以看出后置处理可以插件挺多&#xff0c;在我工作生涯中常用的就是几个组…

【探索Linux命令行】从基础指令到高级管道操作的介绍与实践

目录 man 指令&#xff08;说明&#xff09; 介绍 cp 指令&#xff08;复制&#xff09; ​编辑 mv 指令&#xff08;移动&#xff09; ​编辑 cat 指令&#xff08;类似cout&#xff09; less&#xff08;查找&#xff09; head & tail&#xff08;打印&#xff…

基于jeecgboot-vue3的Flowable流程-流程表单显示控制

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 这个部分主要讲流程起始表单的显示控制&#xff0c;因为开始的时候可以进行输入处理&#xff0c;在流程过程中只能只读状态&#xff0c;当然返回到发起人节点也可以进行编辑提交 1、开始发…

Spring配置那些事

一、引言 配置是一个项目中不那么起眼&#xff0c;但却有非常重要的东西。在工程项目中&#xff0c;我们一般会将可修改、易变、不确定的值作为配置项&#xff0c;在配置文件/配置中心中设置。 比方说&#xff0c;不同环境有不同的数据库地址、不同的线程池大小等&#xff0c…

【NOI题解】1656. 是两位的偶数吗1658. 游乐设施1659. 是否含有数字5 1660. 今天要上课吗1661. 宇航员选拔

文章目录 一、前言二、问题问题&#xff1a;1656. 是两位的偶数吗问题&#xff1a;1658. 游乐设施问题&#xff1a;1659. 是否含有数字5问题&#xff1a;1660. 今天要上课吗问题&#xff1a;1661. 宇航员选拔 三、感谢 一、前言 本章节主要对关于分支结构的中需要进行逻辑运算…

OpenCV目标识别

一 图像轮廓 具有相同颜色或强度的连续点的曲线。 图像轮廓的作用 可以用于图像分析 物体的识别与检测 注意 为了检测的准确性&#xff0c;需要先对图像进行二值化或Canny操作。 画轮廓时会修改输入的图像。 轮廓查找的API findContours(img,mode,ApproximationMode,...)…

GUI Guider(V1.7.2) 设计UI在嵌入式系统上的应用(N32G45XVL-STB)

目录 概述 1 使用GUI Guider 设计UI 1.1 创建页面 1.2 页面切换事件实现 1.3 生成代码和仿真 1.3.1 生成和编译代码 1.3.2 仿真UI 2 GUI Guider生成的代码结构 2.1 代码结构介绍 2.2 Project目录下的文件 3 板卡上移植UI 3.1 加载代码至工程目录 3.2 主函数中调…

新旧torch中傅里叶变换实现(fft)

由泰勒级数我们知道&#xff0c;一个函数可以被分解成无穷个幂函数叠加的形式&#xff0c;于是同样地&#xff0c;一个周期函数也可以被分解成多个周期函数叠加&#xff0c;于是自然而然地&#xff0c;三角函数符合这个需求&#xff0c;由傅里叶级数我们可以将周期函数分解成无…

【车载音视频AI电脑】铁路视频监控系统解决方案

方案简介 铁路视频监控系统解决方案针对铁路行业安全运营保障需求&#xff0c;根据中国铁路总公司的技术规范要求&#xff0c;基于铁路系统的IP网络&#xff0c;采用先进的视频监控技术&#xff0c;构建一套完备的数字化、智能化、分布式铁路综合视频监控系统&#xff0c;实现视…

第二证券A股重要变化!今起实施

A股系列重要指数迎来样本股调整&#xff01; 此前&#xff0c;深交所及其全资子公司深证信息发布公告&#xff0c;将对深证成指、创业板指、深证100&#xff08;以下统称“深市中心指数”&#xff09;施行样本股定时调整。此次调整于6月17日&#xff08;今日&#xff09;正式施…