IOS文章收集OpenGLES程序员

OpenGL ES 2.0 (iOS)[06-1]:基础纹理

2017-05-06  本文已影响641人  半纸渊

前言:如果你没有 OpenGL ES 2 的基础知识,请先移步 《OpenGL ES 2.0 (iOS) 笔记大纲》 学习一下基础的知识。


目录

一、软件运行效果演示
(一)、最终效果
(二)、信息提取

二、纹理处理的流程【核心】
(一)、Texture 是什么?
(二)、Texture
(三)、引入了 Texture 的 Shader 文件
(四)、Texture 正确的 “书写” 顺序

三、知识扩充:图片加载
使用 Quartz Core 的知识加载图片数据


一、软件运行效果演示

(一)、最终效果

工程地址:Github

Texture-Base.gif

(二)、信息提取

  1. 不同的模型【2D & 3D】,不同维度下,Texture 的处理区别;

  2. 单一像素信息【pixelBuffer】 与 复杂像素信息【图片】的显示区别;

  3. 正方图【单张或多张图片】 与 长方图,像素的显示控制区别;


二、纹理处理的流程【核心】

(一)、Texture 是什么?

Texture 纹理,就是一堆被精心排列过的像素;

  1. 因为 OpenGL 就是图像处理库,所以 Texture 在 OpenGL 里面有多重要,可想而知;

  2. 其中间接地鉴明了一点,图片本身可以有多大变化,OpenGL 就可以有多少种变化;

学好 Texture 非常重要

(二)、Texture

Texture 在 OpenGL 里面有很多种类,但在 ES 版本中就两种——Texture_2D + Texture_CubeMap;

Texture_2D: 就是 {x, y} 二维空间下的像素呈现,也就是说,由效果图上演示可知,很难做到使正方体的六个面出现不同的像素组合;图片处理一般都使用这个模式;[x 、y 属于 [0, 1] 这个范围]


Texture_CubeMap: 就是 { x, y, z } 三维空间下的像素呈现,也就如效果图中演示的正方体的六个面可以出现不同的像素组合;它一般是用于做环境贴图——就是制作一个环境,让 3D 模型如同置身于真实环境中【卡通环境中也行】。[x、y、z 属于 [-1, 1] 这个范围,就是与 Vertex Position 的值范围一致]


注:上面提到的所有坐标范围是指有效渲染范围,也就是说你如果提供的纹理坐标超出了这个范围也没有问题,只不过超出的部分就不渲染了;

感受一下怎么具体表达:

// VYVertex
typedef struct {
    GLfloat position[3];
    GLfloat texCoord[2];
    GLfloat normalCoord[3];
}VYVertex;

Texture_2D:

// Square
static const VYVertex tex2DSquareDatas[] = {
    {{-1.0, -1.0, 0.0}, {0.0, 0.0}},
    {{ 1.0, -1.0, 0.0}, {1.0, 0.0}},
    {{ 1.0,  1.0, 0.0}, {1.0, 1.0}},
    {{-1.0,  1.0, 0.0}, {0.0, 1.0}},
};
// Cube
static const VYVertex tex2DCubeDatas[] = {

    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, // 0
    {{ 1.0, -1.0,  1.0}, {1.0, 0.0}}, // 1
    {{ 1.0,  1.0,  1.0}, {1.0, 1.0}}, // 2
    {{-1.0,  1.0,  1.0}, {0.0, 1.0}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {0.0, 0.0}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {1.0, 0.0}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {1.0, 1.0}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {0.0, 1.0}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, //8[0]
    {{-1.0,  1.0,  1.0}, {1.0, 0.0}}, //9[3]
    {{-1.0,  1.0, -1.0}, {1.0, 1.0}}, //10[4]
    {{-1.0, -1.0, -1.0}, {0.0, 1.0}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {0.0, 0.0}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {1.0, 0.0}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {1.0, 1.0}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {0.0, 1.0}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {0.0, 0.0}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {1.0, 0.0}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {1.0, 1.0}}, //18[5]
    {{-1.0,  1.0, -1.0}, {0.0, 1.0}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {0.0, 0.0}}, //20[0]
    {{-1.0, -1.0, -1.0}, {1.0, 0.0}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {1.0, 1.0}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {0.0, 1.0}}, //23[1]
    
};

Texture_CubeMap:

// Cube Map
static const VYVertex texCubemapCubeDatas[] = {
    
    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, // 0
    {{ 1.0, -1.0,  1.0}, {}, { 1.0, -1.0,  1.0}}, // 1
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, // 2
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {}, {-1.0, -1.0, -1.0}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, //8[0]
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, //9[3]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //10[4]
    {{-1.0, -1.0, -1.0}, {}, {-1.0, -1.0, -1.0}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {}, { 1.0, -1.0,  1.0}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {}, {-1.0,  1.0,  1.0}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {}, { 1.0,  1.0,  1.0}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {}, { 1.0,  1.0, -1.0}}, //18[5]
    {{-1.0,  1.0, -1.0}, {}, {-1.0,  1.0, -1.0}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {}, {-1.0, -1.0,  1.0}}, //20[0]
    {{-1.0, -1.0, -1.0}, {}, { 1.0, -1.0,  1.0}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {}, { 1.0, -1.0, -1.0}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {}, {-1.0, -1.0, -1.0}}, //23[1]
    
};

这种坐标,是刚好贴合【完全覆盖】的状态;

数据特点:一个顶点数据绑定一个纹理数据;

【有没有注意到,CubeMap 里面就是直接拷贝顶点数据到纹理坐标上,就行了。(CubeMap 中间那个空的 {} 是结构体中的 2D 纹理数据(就是空的))】

其它的数据形态【对于不是正方的图片】,
【希望大一点,或小一点,即只显示某一部分】:


都是类似图中的分割一样,划分成多个小图片【小的 TexCoord】,最终的数据形态是:

static const VYVertex tex2DElongatedDDCubeDatas[] = {
    
    // Front [Front 的 z 是正的]
    {{-1.0, -1.0,  1.0}, {0.000, 0.000}}, // 0
    {{ 1.0, -1.0,  1.0}, {0.250, 0.000}}, // 1
    {{ 1.0,  1.0,  1.0}, {0.250, 0.500}}, // 2
    {{-1.0,  1.0,  1.0}, {0.000, 0.500}}, // 3
    // Back [Back 的 z 是负的]
    {{-1.0,  1.0, -1.0}, {0.000, 0.500}}, //4[3: -Z]
    {{ 1.0,  1.0, -1.0}, {0.250, 0.500}}, //5[2: -Z]
    {{ 1.0, -1.0, -1.0}, {0.250, 1.000}}, //6[1: -Z]
    {{-1.0, -1.0, -1.0}, {0.000, 1.000}}, //7[0: -Z]
    // Left [Left 的 x 是负的]
    {{-1.0, -1.0,  1.0}, {0.250, 0.000}}, //8[0]
    {{-1.0,  1.0,  1.0}, {0.500, 0.000}}, //9[3]
    {{-1.0,  1.0, -1.0}, {0.500, 0.500}}, //10[4]
    {{-1.0, -1.0, -1.0}, {0.250, 0.500}}, //11[7]
    // Right [Right 的 x 是正的]
    {{ 1.0, -1.0,  1.0}, {0.250, 0.500}}, //12[1]
    {{ 1.0, -1.0, -1.0}, {0.500, 0.500}}, //13[6]
    {{ 1.0,  1.0, -1.0}, {0.500, 1.000}}, //14[5]
    {{ 1.0,  1.0,  1.0}, {0.250, 1.000}}, //15[2]
    // Top [Top 的 y 是正的]
    {{-1.0,  1.0,  1.0}, {0.500, 0.000}}, //16[3]
    {{ 1.0,  1.0,  1.0}, {0.750, 0.000}}, //17[2]
    {{ 1.0,  1.0, -1.0}, {0.750, 0.500}}, //18[5]
    {{-1.0,  1.0, -1.0}, {0.500, 0.500}}, //19[4]
    // Bottom [Bottom 的 y 是负的]
    {{-1.0, -1.0,  1.0}, {0.750, 0.000}}, //20[0]
    {{-1.0, -1.0, -1.0}, {1.000, 0.000}}, //21[7]
    {{ 1.0, -1.0, -1.0}, {1.000, 0.500}}, //22[6]
    {{ 1.0, -1.0,  1.0}, {0.750, 0.500}}, //23[1]
    
};

也可以是没有填充完整的图片,只取其中的一部分,数据形态也是上面的:


扩展:
CubeMap 用于做环境贴图,还需要 Light + Shadow 【光 + 阴影】的知识,为什么?环境,有物体 + 自然光 + 人造光 + 光与物体产生的阴影 + 光与物体作用后的颜色;【颜色和阴影是因为有光才产生的,OpenGL 本身默认有一个全局光,不然你没有写光的代码,为什么可以看到你渲染的模型体】
即只有在具备了 光 + 影 的知识,去学习 环境贴图才好理解;【贴图:HDR 图片 (效果中的那张蓝色森林就是 HDR 图,没有做 CubeMap) + CubeMap 格式】

CubeMap 图片格式,就是把下图中的 HDR 图片直接转换成,六个黄色框框的图像,框框之间的边缘是连接的哦:



连接

MipMapping: 根据不同的情形加载不同大小的图片进行渲染;【不同情形,指不同远近,不同光影环境下对图片“看清”“看不清”的程度,OpenGL 自动选择合适的图片大小】【不同大小的图片,程序员要事先加载一张图片的不同大小 ( 2^n , 2^m ) 的像素数据(0 ~ n level),又因为 ES 是基于移动端的,所以内存容易告急,即能不用则不用】

Fliter + 特效 : 我们天天看到的最多的东西,就是给图片像素加入各种“想法”变成你想要的效果【加雾、马赛克、调色、镜像、模糊、素描、液化、叠加、艺术化 ......】,它的核心知识在 Fragment Shader【重点】 + OpenGL ES 提供的基础混合模式【滤波 + Blend】,放在下一篇文章专门讲;

粒子系统:Texture + Point Sprites,制作雨水、下雪、飞舞的花瓣...... 只要渲染效果要求有多个相似点在那动来动去的,都可以用它们来实现;【数学中的分形理论好像也可以用上】【粒子,会用专门的一篇文章讲】

所有的 “花样” 特效,不管被称之为什么,都与 数学知识【算法】 和 颜色构成知识【光构成、色彩构成】 密不可分;

所以我就要怕了吗?
错,你应该兴奋;因为~~ **反正我也没有什么可以失去的了,上来不就是干了吗?
** ^ _ ^ + ~_~ + $-$

(三)、引入了 Texture 的 Shader 文件

Texture_2D:

2D Vertex:

#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

attribute vec4 a_position;
attribute vec2 a_texCoord;

varying highp vec2 v_texCoord;

void main(void) {
    gl_Position = u_projectionMat4 * u_modelViewMat4 * a_position;
    v_texCoord  = a_texCoord;
}

纹理输入输出:

...
attribute vec2 a_texCoord;
varying highp vec2 v_texCoord;

void main(void) {
    ...
    v_texCoord  = a_texCoord;
}

输入:
vec2 a_texCoord,上面提到过它是 {x, y} 的坐标,所以使用的也是 vec2 ;

输出:
同样是 vec2 ,但是一定要记住加 highp 精度限定符,不然编译会报错哦;

不知道,你是否还记得渲染管线中的 Texture Memory ,看下图:


渲染管线

红色框框住的虚线,就是指代 Vertex Shader 中的纹理坐标信息;

直接给的,为什么是虚线?

看清楚 Shader 代码,这里是直接就赋值【输入 = 输出,经过其它变换也行】了,也就是 Vertex Shader 内部不需要使用到它,它只是为了传到 Fragment 里面使用的【varying 的作用】,所以就使用虚线来表示;

2D Fragment:

#version 100

uniform sampler2D us2d_texture;

varying highp vec2 v_texCoord;

void main(void) {
//    gl_FragColor = vec4(1, 1, 0.5, 1);
    gl_FragColor = texture2D(us2d_texture, v_texCoord);
}

上面的渲染管线图中,黄色框框住的实线,就是指代 Fragment Shader 中的像素数据【sampler2D】来源;

这里是核心,输入输出:

uniform sampler2D us2d_texture;
...

void main(void) {
    gl_FragColor = texture2D(us2d_texture, ...);
}

输入:
sampler2D 就是一堆静态数据的意思,像素信息就是一堆固定【不管是写死,还是程序自动生成,都一样】的颜色信息,所以要使用这种常量块的类型限定符;

输出:
这里要使用 texture2D 内置函数来处理像素信息生成 vec4 的颜色信息,原型 vec4 texture2D(sampler2D s, vec2 texCoord);

所以剩下的问题就是如何得到 sampler2D 数据,并如何将像素数据写入到 Shader 中

Texture_CubeMap:


#version 100

uniform mat4 u_modelViewMat4;
uniform mat4 u_projectionMat4;

attribute vec4 a_position;
attribute vec3 a_normalCoord;
varying highp vec3 v_normalCoord;

void main(void) {
    gl_Position = u_projectionMat4 * u_modelViewMat4 * a_position;
    v_normalCoord  = a_normalCoord;
}
#version 100

uniform samplerCube us2d_texture;
varying highp vec3 v_normalCoord;

void main(void) {
    gl_FragColor = textureCube(us2d_texture, v_normalCoord);
}

CubeMap 与 2D 的 Fragment 区别并不大,原理一样的;

CubeMap Vertex ,只要把 vec2 --> vec3 即可;

CubeMap Fragment , 只要把 sampler2D --> samplerCube , texture2D 函数改成 textureCube 即可;

(四)、Texture 正确的 “书写” 顺序

前提,假设基本的渲染管线已经配置完成了,这里只重点讲纹理相关的;

1、 绑定 Texture Coord 纹理坐标:

GLuint texCoordAttributeComCount = 2;
    
glEnableVertexAttribArray(texCoordAttributeIndex);
if ( texture2D ) {
    glVertexAttribPointer(texCoordAttributeIndex,
                          texCoordAttributeComCount,
                          GL_FLOAT, GL_FALSE,
                          sizeof(VYVertex),
                          (const GLvoid *) offsetof(VYVertex, texCoord));
 } else {
    texCoordAttributeComCount = 3;
    glVertexAttribPointer(texCoordAttributeIndex,
                          texCoordAttributeComCount,
                          GL_FLOAT, GL_FALSE,
                          sizeof(VYVertex),
                          (const GLvoid *) offsetof(VYVertex, normalCoord));
 }

【如果看不懂,请回去看看第一篇文章,里面有详细讲】

2、 请求 Texture 内存:

    GLuint texture = 0;
    glGenTextures(1, &texture);
    
    GLenum texMode = texture2D ? GL_TEXTURE_2D : GL_TEXTURE_CUBE_MAP;
    glBindTexture(texMode, texture);

glGenTextures(GLsizei n, GLuint* textures); 和 glGenBuffers 等的使用是一样的;它的意思就是,向 GPU 请求一块 Texture 内存;

glBindTexture (GLenum target, GLuint texture); 和其它的 glBind... 方法一样;它的意思是,告诉 GPU 请求一块 target 【只有 2D 和 CubeMap 两种】 类型的内存,只有当这个方法完成请求后,这块 Texture 内存才会生成【如果当前内存标识符指向的内存已经存在,则不会再创建,只会指向此处】;

3、 加载像素数据:

    glUseProgram(programObject);
    
    [self setTextureWithProgram:programObject 
                        texture:texture
                        texMode:texMode];

(1)一定要在 glUseProgram 函数后进行这个步骤,为什么?

因为 Fragment 使用的是 uniform samplerXXX 的数据,uniform 常量数据要在 glUseProgram 后再加载才有效,而且它的内存标识符【内存】要在 link Program 之后 OpenGL 才会分配;

(2)进入 setTextureWithProgram: texture: texMode:方法

先准备像素数据【pixelsDatas 或 ImageDatas】:
这里的是,Pixels 的数据,就是写死的数据

// 2 * 2 For Texture_2D
static const GLfloat tex2DPixelDatas[3*4] = {
    1.000, 1.000, 0.108,//[UIColor colorWithRed:1.000 green:1.000 blue:0.108 alpha:1.000]
    0.458, 1.000, 0.404,//[UIColor colorWithRed:0.458 green:1.000 blue:0.404 alpha:1.000]
    0.458, 1.000, 0.770,//[UIColor colorWithRed:0.458 green:1.000 blue:0.770 alpha:1.000]
    0.729, 0.350, 0.770,//[UIColor colorWithRed:0.729 green:0.350 blue:0.770 alpha:1.000]
};

// (2 * 2 * 6) For Texture_CubeMap
static const GLfloat texCubemapPixelDatas[6][3*4] = {
    1.000, 1.000, 0.108,//[UIColor colorWithRed:1.000 green:1.000 blue:0.108 alpha:1.000]
    0.458, 1.000, 0.404,//[UIColor colorWithRed:0.458 green:1.000 blue:0.404 alpha:1.000]
    0.458, 1.000, 0.770,//[UIColor colorWithRed:0.458 green:1.000 blue:0.770 alpha:1.000]
    0.729, 0.350, 0.770,//[UIColor colorWithRed:0.729 green:0.350 blue:0.770 alpha:1.000]
    
    0.145, 0.319, 0.308,//[UIColor colorWithRed:0.145 green:0.319 blue:0.308 alpha:1.000]
    0.732, 0.319, 0.308,//[UIColor colorWithRed:0.732 green:0.319 blue:0.308 alpha:1.000]
    0.732, 0.727, 0.308,//[UIColor colorWithRed:0.732 green:0.727 blue:0.308 alpha:1.000]
    0.732, 0.727, 0.889,//[UIColor colorWithRed:0.732 green:0.727 blue:0.889 alpha:1.000]
    
    0.633, 0.820, 0.058,//[UIColor colorWithRed:0.633 green:0.820 blue:0.058 alpha:1.000]
    0.936, 0.820, 0.994,//[UIColor colorWithRed:0.936 green:0.820 blue:0.994 alpha:1.000]
    0.017, 0.029, 0.994,//[UIColor colorWithRed:0.017 green:0.029 blue:0.994 alpha:1.000]
    0.000, 0.000, 0.000,//[UIColor colorWithWhite:0.000 alpha:1.000]
    
    0.593, 0.854, 0.000,//[UIColor colorWithRed:0.593 green:0.854 blue:0.000 alpha:1.000]
    0.593, 0.337, 0.000,//[UIColor colorWithRed:0.593 green:0.337 blue:0.000 alpha:1.000]
    1.000, 0.407, 0.709,//[UIColor colorWithRed:1.000 green:0.407 blue:0.709 alpha:1.000]
    0.337, 0.407, 0.709,//[UIColor colorWithRed:0.337 green:0.407 blue:0.709 alpha:1.000]
    
    0.337, 0.738, 0.709,//[UIColor colorWithRed:0.337 green:0.738 blue:0.709 alpha:1.000]
    0.337, 0.994, 0.709,//[UIColor colorWithRed:0.337 green:0.994 blue:0.709 alpha:1.000]
    0.186, 0.105, 0.290,//[UIColor colorWithRed:0.186 green:0.105 blue:0.290 alpha:1.000]
    0.633, 0.872, 0.500,//[UIColor colorWithRed:0.633 green:0.872 blue:0.500 alpha:1.000]
    
    0.290, 0.924, 0.680,//[UIColor colorWithRed:0.290 green:0.924 blue:0.680 alpha:1.000]
    0.290, 0.924, 0.174,//[UIColor colorWithRed:0.290 green:0.924 blue:0.174 alpha:1.000]
    0.982, 0.163, 0.174,//[UIColor colorWithRed:0.982 green:0.163 blue:0.174 alpha:1.000]
    0.628, 0.970, 0.878,//[UIColor colorWithRed:0.628 green:0.970 blue:0.878 alpha:1.000]
};

因为 Texture_2D 状态下,只有 {x, y} 平面的数据需要填充,所以这里就只有一个面的颜色数据;

而在 Texture_CubeMap 状态下,是 { x, y, z } 三维坐标,即六个面需要填充,所以就是6 * 1(1 = 2 * 2) = 6个面的颜色数据;

注:图片类型的数据要自己写转换方法,生成像素数据;当然也可以使用 GLKit 提供的 TextureLoder 类来加载图片像素数据;

(3)【核心】glTexImage2D得到纹理像素的方法,就是加载纹理像素到 GPU 的方法:

glTexImage2D
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels)
target *指 如果是 2D ,就是 GL_Texture_2D,如果是 CubeMap 就是 GL_TEXTURE_CUBE_MAP_XXX [+-x, +-y, +-z, 六个面] *
level *指 mipmapping level 没有做 mipmapping 则为 0 ;如果做了,则为 0 ~ levelMax [这个 max 是由你自己图片数据决定的] *
internalformat 指 像素数据的格式是什么 GL_RGB 等等
width 指 一块像素的宽 [2D 下只有一块,cubemap 会有多块(六个面)]
height 指 一块像素的高
border *指 ES 下是 GL_FALSE *
format 指 与 internalformat 格式一致
type 指 像素数据存储的类型,如:GL_FLOAT, GL_UNSIGNED_BYTE
pixels 指 一块像素的内存首地址

a. 像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {
    glTexImage2D(texMode, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT, tex2DPixelDatas);
} else {
    
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[0]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[1]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[2]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[3]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[4]);
//                glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGB, 2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[5]);

    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    for (NSUInteger i = 0; i < 6; i++) {
        glTexImage2D(target, 0, GL_RGB,
                     2, 2, GL_FALSE, GL_RGB, GL_FLOAT,  texCubemapPixelDatas[i]);
        target++;
    }
    
}

上面在 GL_TEXTURE_2D 状态下的加载,只要理解了glTexImage2D函数参数的意思,也就会使用且明白了,这里就不再赘述了;

特别要注意的是在 GL_Texture_Cube_Map 状态下的使用,一定要六个面都进行像素数据加载;

#define GL_TEXTURE_CUBE_MAP_POSITIVE_X                   0x8515
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X                   0x8516
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y                   0x8517
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y                   0x8518
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z                   0x8519
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z                   0x851A

看看GL_TEXTURE_CUBE_MAP_POSITIVE_X它们的定义,因为定义是连续的,所以我们才可以用 for 循环来 “偷懒”;

b. 图片像素模式下的使用:

if (texMode == GL_TEXTURE_2D) {
    
    UIImage *img = // img;
    
    [self.loadTexture textureDataWithResizedCGImageBytes:img.CGImage completion:^(NSData *imageData, size_t newWidth, size_t newHeight) {
        glTexImage2D(texMode, 0, GL_RGBA,
                     (GLsizei)newWidth, (GLsizei)newHeight,
                     GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                     imageData.bytes);
    }];
    
} else {
    
    NSArray<UIImage *> *imgs = // imgs;
    
    GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
    [self.loadTexture textureDatasWithResizedUIImages:imgs completion:^(NSArray<NSData *> *imageDatas, size_t newWidth, size_t newHeight) {
        [imageDatas enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            glTexImage2D((GLenum)(target + idx), 0, GL_RGBA,
                         (GLsizei)newWidth, (GLsizei)newHeight,
                         GL_FALSE, GL_RGBA, GL_UNSIGNED_BYTE,
                         obj.bytes);
        }];
    }];
    
}

这里的核心就是,self.loadTexture 的图片加载方法,这是自己写的加载方法,使用的技术是 Quartz Core ;具体的在下一节【三、知识扩充:图片加载】会讲到;

两者的使用并不会有什么区别,这只是两种像素数据提供的方式不同罢了

(4)指定滤波设置【下一篇会重点讲】 + 像素绑定 + 激活纹理

glTexParameteri(texMode, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(texMode, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    
GLuint textureSourceLoc = glGetUniformLocation(programObject, "us2d_texture");
glUniform1i(textureSourceLoc, 0);
    
glEnable(texMode);
glActiveTexture(GL_TEXTURE0);
glBindTexture(texMode, texObj);

a. 设置滤波模式,函数就是glTexparameteri方法
原型:glTexParameteri (GLenum target, GLenum pname, GLint param);

ES 2 有四个这种滤波函数,下图的参数已经说得很明白了,我就不一一解释了:



MIN / MAG ?:



magnification【MAG】:放大的意思,指显示在屏幕上的一个像素是纹理像素放大后的结果;

【只有 x、y 方向都进行放大,才需要这个参数,也就是说它是可选的】

minification【MIN】: 缩小的意思,指显示在屏幕上的一个像素是一个纹理像素集缩小后的结果;

【一定要做的设置,如上述代码中的glTexParameteri(xxx, GL_TEXTURE_MIN_FILTER, xxx);

【MipMapping 发挥作用的地方就是在缩小的时候,OpenGL 会自动选择合适大小的像素数据】

如果纹理像素在 x、y 方向上是做同一个动作【拉伸或压缩】,则需要放大或缩小像素;如果纹理像素在 x、y 方向上是做不同的动作,则需要放大或者缩小,不确定【由 OpenGL 自己选择】;

WRAP_S / WRAP_T ? : 就是 x 或 y 方向填充覆盖的意思;

LINEAR / NEAREST ? :


前者是指启用线性滤波【就是平滑过渡】,后者是禁用线性滤波;

平滑过滤使用的技术——信号采样,先看看一维的信号采样:


意思就是,采样提供的纹理像素,在放大、缩小的时候,使相邻的像素进行“一定程度的融合”产生新的像素信息,使最终显示在屏幕在的图片更加平滑;上图【猴子】中的效果就是利用这项技术来的,对于二维、三维,就相应地做多次采样【二维,两次;三维,三次......】;

b. 像素绑定【就是告诉 GPU Shader 的像素数据在那】+ 激活纹理

GLuint textureSourceLoc = glGetUniformLocation(programObject, 
                                               "us2d_texture");
glUniform1i(textureSourceLoc, 0);

glEnable(texMode);
glActiveTexture(GL_TEXTURE0);

glBindTexture(texMode, texObj);

glUniform1i类函数,可以理解成绑定一块内存【像素块内存】,也可以理解成绑定一个内存空间【一般常量】;

函数原型:void glUniform1i(GLint location, GLint x)

glEnable函数,就是打开一些什么东西,这里是打开 GL_TEXTURE_XXX ,不写也行,这里和其它地方的默认一样, 0 这个位置的纹理就是打开的;【为了良好习惯,还是写吧】

glActiveTexture函数,名字已经告诉是激活纹理的意思,不用多说了;

重点:glUniform1i 的第二个参数是和 glActiveTexture 的第二个参数是对应的,前者使用的是 0,那么后者就是对应 GL_TEXTURE0 【0~31,共32个】,依此类推

为什么还要做glBindTexture(texMode, texObj);重新绑定像素内存,其实就是防止中途有什么地方把它给改了【如,bind 了其它的纹理】,所以是为了保险起见,就最好写上;但是因为这里很明显地,只有 layoutSubviews 函数【此渲染代码都是写在这个函数内运行的】会绑定它,而且都是同一个的,所以也可以不写;


三、知识扩充:图片加载

使用 Quartz Core 技术 加载图片数据,Bitmap Context :

本来它不属于 OpenGL 的内容,但是它本身也是图像处理的技术,包括 Core Image、 Accelerate等图像处理的框架,如果可以,请尽量去了解或去掌握或去熟练。

核心代码:

#define kBitsPerComponent   8

#define kBytesPerPixels     4
#define kBytesPerRow(width)         ((width) * kBytesPerPixels)

- (NSData *)textureDataWithResizedCGImageBytes:(CGImageRef)cgImage
                                      widthPtr:(size_t *)widthPtr
                                     heightPtr:(size_t *)heightPtr {
    
    if (cgImage == nil) {
        NSLog(@"Error: CGImage 不能是 nil ! ");
        return [NSData data];
    }
    
    if (widthPtr == NULL || heightPtr == NULL) {
        NSLog(@"Error: 宽度或高度不能为空。");
        return [NSData data];
    }
    
    size_t originalWidth  = CGImageGetWidth(cgImage);
    size_t originalHeight = CGImageGetHeight(cgImage);
    
    // Calculate the width and height of the new texture buffer
    // The new texture buffer will have power of 2 dimensions.
    size_t width  = [self aspectSizeWithDataDimension:originalWidth];
    size_t height = [self aspectSizeWithDataDimension:originalHeight];
    
    // Allocate sufficient storage for RGBA pixel color data with
    // the power of 2 sizes specified
    NSMutableData *imageData =
    [NSMutableData dataWithLength:height * width * kBytesPerPixels]; // 4 bytes per RGBA pixel
    
    // Create a Core Graphics context that draws into the
    // allocated bytes
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef cgContext = CGBitmapContextCreate([imageData mutableBytes],
                                                   width, height,
                                                   kBitsPerComponent,
                                                   kBytesPerRow(width),
                                                   colorSpace,
                                                   kCGImageAlphaPremultipliedLast); // RGBA
    CGColorSpaceRelease(colorSpace);
    // Flip the Core Graphics Y-axis for future drawing
    CGContextTranslateCTM (cgContext, 0, height);
    CGContextScaleCTM (cgContext, 1.0, -1.0);
    // Draw the loaded image into the Core Graphics context
    // resizing as necessary
    CGContextDrawImage(cgContext, CGRectMake(0, 0, width, height), cgImage);
    CGContextRelease(cgContext);
    
    *widthPtr  = width;
    *heightPtr = height;
    
    return imageData;
}

主流程:

1、规格化图片尺寸,让其符合 (2^n, 2^m)[n,m 均为自然数 ]
为什么?

(1)因为 CGBitmapContextCreate支持的是 size_t ((long) unsigned int) 的【来个 0.25 个像素也是醉了】;

(2)而且 OpenGL ES 支持的最大像素尺寸也是有限制的,当前环境支持的最大值是 (4096, 4096),这个值由以下两个 xx_MAX_xx 得到【就在 aspectSizeWithDataDimension: 方法内】:

    GLint _2dTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_2dTextureSize);
    
    GLint cubeMapTextureSize;
    glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &cubeMapTextureSize);

glGetIntegerv函数是可以获取当前环境下所有的默认常量的方法;

2、确定图片像素最终输出的颜色空间

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();,这个最容易出错,它的颜色格式要和你使用glTexImage2D函数指名的颜色格式要一致,不然不可能显示正常【如,你这里定义成 CYMK, 指名了 GL_RGB 那么肯定不对的】

3、确定最终像素的位深与位数

这里是明确用多少位来表示一个像素位【如:R 用 8 位表示】,一个像素由多少个成员组成【如:RGBA 就是 4 个】

4、创建上下文环境

Bitmap 图就是像素图,包含所有的像素信息,没有什么 jpg / png 容器什么的;
CGBitmapContextCreate函数的各个参数都很明显了,所以就不废话了;

5、变换像素的坐标空间

为什么?
Texture 纹理坐标空间的坐标原点在,左下角,而苹果设备显示的图形的坐标系的坐标原点在左上角,刚好是反的;

6、绘制生成最终的像素数据


谢谢看完,如果有描述不清或讲述错误的地方,请评论指出!!!

上一篇下一篇

猜你喜欢

热点阅读