3.glPixelStore

像glPixelStorei(GL_PACK_ALIGNMENT, 1)这样的调用,通常会用于像素传输(PACK/UNPACK)的场合。尤其是导入纹理(glTexImage2D)的时候:

C++代码
  1. glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  2. glTexImage2D(,,,, &pixelData);
  3. glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

很明显地,它是在改变某个状态量,然后再Restore回来。——为什么是状态?你难道8知道OpenGL就是以状态机不?——什么状态?其实名字已经很直白了,glPixelStore这组函数要改变的是像素的存储格式。

涉及到像素在CPU和GPU上的传输,那就有个存储格式的概念。在本地内存中端像素集合是什么格式?传输到GPU时又是什么格式?格式会是一样么?在glTexImage2D这个函数中,包含两个关于颜色格式的参数,一个是纹理(GPU端,也可以说server端)的,一个是像素数据(程序内存上,也就是client端)的,两者是不一定一样的,哪怕一样也无法代表GPU会像内存那样去存储。或者想象一下,从一张硬盘上的图片提取到内存的像素数据,上传给GPU成为一张纹理,这个“纹理”还会是原来的那种RGBARGBA的一个序列完事么?显然不是的。作为一张纹理,有其纹理ID、WRAP模式、插值模式,指定maipmap时还会有一串各个Level下的map,等等。就纹理的数据来说,本质纹理是边长要满足2的n次方(power of two)的数据集合,这样首先大小上就有可能不一样,另外排列方式也未必就是RGBA的形式。在OpenGL的“解释”中,纹理就是一个“可以被采样的复杂的数据集合”,无论外面世界千变万化,GPU只认纹理作为自己“图像数据结构”,这体现着“规范化”这条世界纽带的伟大之处。

姑且把GPU里面的像素存储格式看做一个未知数,把该存储空间内那批像素看做一堆X。不要深究一堆X究竟是什么样子的,嘛,反正就想象成一堆软绵绵的,或者模糊不清的,打满马赛克的,(哔——)的一样的东西就可以了。与此相比,内存中的像素数据实在太规则规范了!可能源文件各种图片格式,什么bmp、jpg、png甚至dds,但只要你按该格式的算法结构来提取(类似[Bmp文件的结构与基本操作(逐像素印屏版)] ),总可以提取出一列整齐的RGBARGBA(或者RGBRGB什么的,反正很整齐就行了管他呢)的数据堆出来,是可以在程序中实测的东西。

涉及到像素在CPU和GPU上的传输,那就有个传输方向的概念。那就是大家耳濡目染的PACK和UNPACK。嘛,装载和卸载也可以,打包和解压也可以,随你怎么译了。结合上述存储格式的概念:

  • PACK —— 把像素从一堆X的状态转变到规则的状态(把一堆泥土装载进一个花盆,把散散的货物装上货柜,或者把一堆各样的文件打包成一个rar压缩包,等等);
  • UNPACK —— 把像素从规则的状态转变到一堆X的状态(把花盆里的泥倒出来,把货柜中的货物卸载到盐田港,或者解压压缩包,等等)。

我认为这两个概念还是很容易混淆的,所以形象化一点总好点嘛。从本地内存向GPU的传输(UNPACK),包括各种glTexImage、glDrawPixel;从GPU到本地内存的传输(PACK),包括glGetTexImage、glReadPixel等。也正因如此,PBO也有PACK和UNPACK模式的区别。

好像说了好多不相关的事情。嘛,适当也当做延伸。回头来真正看一下glPixelStore吧。它的第一个参数,譬如ALIGNMENT、ROW_LENGTH、IMAGE_HEIGHT等等,都有PACK和UNPACK的两种版本,所以对应的也是上述关于PACH和UNPACK的两类函数。所以对于glTexImage2D,才使用GL_UNPACK_ALIGNMENT的版本。但要说明的是,无论是哪种传输方式,它都是针对本地内存端(client端)上的像素数据的。在上述例子中,它起着补充glTexImage2D中关于传输起点——本地像素集合的格式,的作用。

一般来说,这些本地的数据集合,只要知道其起始位置、大小(width*height)和颜色格式(譬如GL_RGBA等等)、值格式(GL_UNSIGNED_CHAR、GL_FLOAT等等),就能准确地传输。而这些都是需要向glTexImage2D函数(或者上述的其他传输型函数)提供的。但是,这里头也一些细节,其实是需要glPixelStore这个函数来进行设置的。

3.1 GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT

通常,提取一张图像的时候,我们怎么知道一行的数据量呢?这个一行的数据量应该是:width * sizeof(Pixel) ,应对最一般RGBA、各通道各占一个字节的像素结构,width * sizeof(Pixel) = width * 4 * sizeof(byte),是4的整数倍。但是也有时候,我们的像素数据中一行的数据量不一定是4的整数倍(譬如一张RGB的图像、宽度150、各通道各占一个字节的像素结构,一行的数据量就是450个字节)。

另一方面,跟编译器一样,GPU传输时也喜欢4字节对齐,也即是说喜欢对像素数据按4字节存取。所以它更偏向于喜欢每一行的数据量是4的整数倍(按上述,这恰好是比较常见的)。所以为了更高的存取效率,OpenGL默认让像素数据按4字节4字节的方式传输向GPU——但是问题在于,对于行非4字节对齐的像素数据,第一行的最后一次存取的4字节将部分包括第一行的数据部分包括第二行的数据,当然致命的不是在这里,而是在最后一行:存取将很可能会越界。为了防止这样的情况,一是硬性把像素数据延展成4字节对齐的(就像BMP文件的存储方式一样,[Bmp文件的结构与基本操作(逐像素印屏版)] );二是选择绝对会造成4字节对齐的颜色格式或值格式(GL_RGBA啦,或者GL_INT、GL_FLOAT之类);三是以牺牲一些存取效率为代价,去更改OpenGL的字节对齐方式——这就是glPixelStore结合GL_UNPACK_ALIGNMENT / GL_PACK_ALIGNMENT。

C++代码
  1. glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  2. glTexImage2D(,,,, &pixelData);
  3. glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

再次看回这段代码,这时候就明白了:让字节对齐从默认的4字节对齐改成1字节对齐(选择1的话,无论图片本身是怎样都是绝对不会出问题的,嘛,以效率的牺牲为代价),UNPACK像素数据,再把字节对齐方式设置回默认的4字节对齐。至于哪种方式更适合,就看你依据硬件环境限制、麻烦程度等,去选择了。

3.2 GL_UNPACK_ROW_LENGTH/ GL_PACK_ROW_LENGTH 和 
GL_UNPACK_SKIP_ROWS / GL_PACK_SKIP_ROWS 、 GL_UNPACK_SKIP_PIXELS/GL_PACK_SKIP_PIXELS

有的时候,我们把一些小图片拼凑进一张大图片内,这样使用大图片生成的纹理,一来可以使多个原本使用不同的图片作为纹理的同质物件如今能够在同一个Batch内,节省了一些状态切换的开销,二来也容易综合地降低了显存中纹理的总大小。但是,也有些时候,我们需要从原本一张大的图片中,截取图片当中的某一部分作为纹理。要能够做到这样,可以通过预先对图片进行裁剪或者在获得像素数据后,把其中需要的那一部分另外存储到一个Buffer内再交给glTexImage2D之类的函数。而上述这些参数下glPixelStore的使用将帮助我们更好地完成这个目的:

C++代码
  1. //原图中需要单独提取出来制成纹理的区域
  2. RECT subRect = {{100, 80}, {500, 400}}; //origin.x, origin.y, size.width, size.height
  3. //假设原图的宽度为BaseWidth, 高度为BaseHeight
  4. glPixelStorei(GL_UNPACK_ROW_LENGTH,  BaseWidth);    //指定像素数据中原图的宽度
  5. glPixelStorei(GL_UNPACK_SKIP_ROWS,      subRect. origin.y.); //指定纹理起点偏离原点的高度值
  6. glPixelStorei(GL_UNPACK_SKIP_PIXELS,     subRect. origin.x); //指定纹理起点偏离原点的宽度值
  7. glTexImage2D(..., subRect.size.width, ubRect.size.height,.. &pixelData);  //使用区域的宽高
  8. glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
  9. glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
  10. glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);

这段代码本身,即使没有注释也很清楚了。注意的是GL_UNPACK_ROW_LENGTH的必要性,因为为了确认区域起点的Offset,就需要把线性数据pixelData上标记起点的“游标”从0移动到OffsetToData = subRect. origin.y * BaseWidth + subRect. origin.x的位置。有了区域纹理原点的在原图数据的位置,以及区域的尺寸,glTexImage2D就可以确定区域纹理生成所需要的信息了。通过glPixelStore的使用,避免了新建Buffer和自己处理图像数据的开销和麻烦了。

说到这里,到底为什么要这样做来提取区域纹理呢?尤其是原图若其他部分都是程序所需要的,那是不是就可以直接通过纹理坐标去切割更好呢?我想到的是一种情况(也可以说我是因为这种情况才注意到glPixelStore的这种用法):如果这块区域纹理需要作重复铺设(wrap mode选择GL_REPEAT)呢?这时候纹理坐标的方法就没用了,因为REPEAT所依据的也是纹理坐标(使用纹理坐标的小数部分进行采样)。这时候就需要上述做法了。(事实上3DSMAX等软件纹理导入的类似区域纹理平铺的功能就能如此实现。)

4.glScissor

我想这个函数也应该很常见才对。裁剪测试啊,当年跟Alpha测试、Depth测试、Stencil测试可以并列哦,而今更是不掉时髦值啊。因为我实在很难想象在Shader里能容易地实现它的功能:裁剪。当然这只是矩形裁剪,但是对于discard掉渲染中不需要的像素真是颇简单粗暴。我使用它最多的是一些二维图片缩略图栏——有时候我们只需要把这些缩略图的显示限制在一个区域里,但又要支持滑动。

C++代码 (OpenGL)
  1. glEnable(GL_SCISSOR_TEST);
  2. glScissor(GLint(m_rtThumbRegion.x), GLint(m_rtThumbRegion.y), GLint(m_rtThumbRegion.width), GLint(m_rtThumbRegion.height));
  3. //.....  Render
  4. glDisable(GL_SCISSOR_TEST);

其中,除了启用GL_SCISSOR_TEST外,只要给glScissor指出需要保留显示的区域就可以了。在此区域外的像素依然会被渲染(不会怎么省流水线操作,所以也别指望它附带什么提高效率之类的功能),在下图中,其实左右两侧还是继续渲染其他的图片(或者说,其实这个缩略图栏横跨整个屏幕),但是就在fragment shader之后,它们会被检测到不在该区域内而被discard掉罢

分享:

glPixelStorei 详解 包括像素传输的更多相关文章

  1. Nginx安装及配置详解包括windows环境

    nginx概述 nginx是一款自由的.开源的.高性能的HTTP服务器和反向代理服务器:同时也是一个IMAP.POP3.SMTP代理服务器:nginx可以作为一个HTTP服务器进行网站的发布处理,另外 ...

  2. C++对象内存分布详解(包括字节对齐和虚函数表)

    转自:https://www.jb51.net/article/101122.htm 1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数 ...

  3. Android中_TextView属性的XML详解 包括单行显示等等。

    <pre name="code" class="html">属性名称 描述 android:autoLink 设置是否当文本为URL链接/email ...

  4. Cloudera Impala源码分析: SimpleScheduler调度策略详解包括作用、接口及实现等

    问题导读:1.Scheduler任务中Distributed Plan.Scan Range是什么?2.Scheduler基本接口有哪些?3.QuerySchedule这个类如何理解?4.Simple ...

  5. win10 安装vue 详解-包括node.js、npm、webpack

    1.下载 去官网下载 node.js https://nodejs.org/en/download/ 一般不会选择最新的,我安装的是 12.18.4 进入历史记录页面网址 https://nodejs ...

  6. MongoDB各种查询操作详解

    这篇文章主要介绍了MongoDB各种查询操作详解,包括比较查询.关联查询.数组查询等,需要的朋友可以参考下   一.find操作 MongoDB中使用find来进行查询,通过指定find的第一个参数可 ...

  7. Nginx+Tomcat的服务器端环境配置详解

    这篇文章主要介绍了Nginx+Tomcat的服务器端环境配置详解,包括Nginx与Tomcat的监控开启方法,需要的朋友可以参考下 Nginx+tomcat是目前主流的Javaweb架构,如何让ngi ...

  8. TCP、UDP详解与抓包工具使用

    参考:https://www.cnblogs.com/HPAHPA/p/7737641.html TCP.UDP详解 1.传输层存在的必要性 由于网络层的分组传输是不可靠的,无法了解数据到达终点的时间 ...

  9. (转载)详解网络传输中的三张表,MAC地址表、ARP缓存表以及路由表

    郑重声明:原文转载于http://dengqi.blog.51cto.com/5685776/1223132 向好文章致敬!!! 一:MAC地址表详解 说到MAC地址表,就不得不说一下交换机的工作原理 ...

随机推荐

  1. jqGrid刷新不取消选中

    也就是说,刷新前选中的行,刷新后还是选中 var jqGrid = $("#jqGrid"); // 获取刷新前选中的行id var jqGridRowid=jqGrid.jqGr ...

  2. Target Sum

    You are given a list of non-negative integers, a1, a2, ..., an, and a target, S. Now you have 2 symb ...

  3. 《Mysql - SQL优化》

    一:在查询语句时,应该注意的优化问题 - SELECT语句务必指明字段名称 - SELECT * 会增加很多不必要的消耗(CPU.IO.内存.网络带宽) - 同时会让 Mysql 优化器无法优化 -  ...

  4. 第一坑:class编译版本

    这个坑是刚去公司的时候,公司项目运行环境的jdk版本是1.5,当时我编译版本是1.7,然后放上去一直报找不到class的错误,我硬是找了半天,后来才听说要用1.5版本编译.

  5. TypeScript 枚举

    我们常常会有这样的场景,比如与后端开发约定订单的状态开始是0,未结账是1,运输中是2,运输完成是3,已收货是4.这样的纯数字会使得代码缺乏可读性.枚举就用于这样的场景.枚举可以让我们定义一些名字有意义 ...

  6. day0~day13

    day0 day1 day2 day4 day5 day7 day9 day10 day12 day13

  7. unicode 格式 转汉字

    function decodeUnicode($str){ return preg_replace_callback('/\\\\u([0-9a-f]{4})/i', create_function( ...

  8. Django导出数据为Excel,调用浏览器下载

    1. 环境 Django (2.1.10) + Python3.6 + xlwt (1.3.0) 操作系统使用的为:Windows 7 2. 接口代码 def now_export(request): ...

  9. EXTI中断开关点亮LED源码

    在KEY点亮LED源码的基础上 USER下新建EXIT文件夹,新建bsp_exit.c和bsp_exit.h,添加到工程中(魔术棒添加头文件所在文件夹) bsp_exit.h内容 #ifndef BS ...

  10. Ubuntu截图工具Flameshot

    今天来介绍一款Ubuntu下的截图工具,名叫Flameshot. 安装 Flameshot的安装很简单. 命令行安装 sudo apt-get install flameshot 一条命令搞定! 软件 ...