OpenGL编程指南(第八版) ((美)DaveShreiner著) 跟随认识OpenGL ES for Android
目标:
- 如何使用OpenGL ES进行绘制
- 通过使用猜测和验证工作原理
- 什么是OpenGL ES
从写一个应用了解OpenGL ES
学习记录:
- 任意图形都是由点/线/三角型组成
- 使用GL绘制一个图型:
- 设置顶点
- 设置顶点着色器 (vertex shader)
- 设置片段着色器(fragment shader)(光栅化)
其中着色器是直接提供给GPU 运行的程序,可以看到里面是main 函数,对应了GPU硬件参数中的管线即GPU特性多个core 同时工作。 - 可以理解每一个着色器都是GPU 并行运行的一个core,着色器使用的语言是GLSL
- 光栅化把需要画的非点状内容分割为不同的小块,然后片段着色器对每一个小块进行绘制,每个片段着色器单次绘制的颜色是同一纯色。这里在光栅化的时候就会引入锯齿,抗锯齿问题就来了,光栅化最理想的是每一个像素都对应一个着色器。
- 定义顶点着色器和片段着色器
shader:
//fragment shader
precision mediump float;
uniform vec4 u_Color;
void main()
{
gl_FragColor = u_Color;
}
//vertex shader
attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
gl_PointSize = 10.0; //点的大小
}
- 顶点着色器(vertex shader)
- 片段着色器(fragment shader)(光栅化)
- 编译着色器及再屏幕上绘制
加载着色器:
读取glsl语言的内容
读文件,然后转成字符串,或者直接把glsl 通过宏定义直接定义在code里面。
编译着色器:
GLuint shader = glCreateShader(type);
if(shader == 0) { err;}
glShaderSource(shader, 1, &shaderSrc, nullptr);
glCompileShader(shader);
GLint compiled = 0
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
printf("Error compiling %s shader for %s\n", (type==GL_VERTEX_SHADER) ? "vtx":"pxl", name);
GLint size = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &size);
if (size > 0)
{
// Get and report the error message
std::unique_ptr infoLog(new char[size]);
glGetShaderInfoLog(shader, size, NULL, infoLog.get());
printf(" msg:\n%s\n", infoLog.get());
} glDeleteShader(shader);
return 0;
}
把着色器连接进OpenGL的程序:
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(programObject);
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked)
{
printf("Error linking program.\n");
GLint size = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &size);
if (size > 0)
{
// Get and report the error message
std::unique_ptr infoLog(new char[size]);
glGetProgramInfoLog(program, size, NULL, infoLog.get());
printf(" msg: %s\n", infoLog.get());
}
glDeleteProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(pixelShader);
return 0;
}
最后的拼接:
C++ 获取shader中的内容
private static final String U_COLOR = "u_Color";
private int uColorLocation;
//获取fragment u_Color变量位置
uColorLocation = glGetUniformLocation(program, U_COLOR);
private static final String A_POSITION = "a_Position";
private int aPositionLocation;
//获取vertex a_Position变量位置
aPositionLocation = glGetAttribLocation(program, A_POSITION);
//关联属性与顶点数据
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT, false, 0, vertexData);
//glVertexAttribPointer函数原型:对顶点shader的位置进行关联指定。
glVertexAttriPointer(int index, int size, int type, boolean normalized, int stride, Buffer ptr);
// index: vertex 对应的位置属性
// size: vertex 对应的分量数量,默认一个vertex存在4个分量:xyzw,默认不初始化时其中xyz 默认为0,w为1
// type:指的是vertex 分量定义的类型:比如GL_INT, GL_FLOAT
// normalized :归一化,只有type 为int 时才生效。
// stride:prt 中含有多个属性是,他才有意义,目前只使用了1个,传递0.
//prt:这个参数告诉OpenGL去哪里读取数据
//使能顶点数组
glEnableVertexAttribArray(aPositionLocation);
在屏幕上绘制:
// set the viewport
glViewport(0, 0, mWidth, mHeight);
//桌子
//clear the buffer color
glClearColor(0.1f, 0.5f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//更新着色器代码中的u_Color的值,vec4:4个分量(rgba)都需要指定。
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
//绘制出两个三角形:桌子
glDrawArrays(GL_TRIANGLES, 0, 6);
//分割线
//设置颜色红色,rgba
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
//GL画线,坐标素组中第6个点开始读,读2个点
glDrawArrays(GL_LINES, 6, 2);
//木槌
//设置颜色红色,rgba
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
//GL画点,坐标素组中第8个点开始读,读1个点
glDrawArrays(GL_POINTS, 8, 1);
//设置颜色红色,rgba
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
//GL画点,坐标素组中第8个点开始读,读1个点
glDrawArrays(GL_POINTS, 9, 1);
OpenGL如何把坐标映射到屏幕
OpenGL的平面像素坐标系:
图片
猜测3D的话Z方向应该也是(-1,1),但是还有一个w分量,没搞懂是啥?
绘制结果:
暂时无法在飞书文档外展示此内容
实现code 绘制以下内容:
图片
遇到的问题:
在PTG4.3上yocto x9e native Linux上绘制时,发现需要先画点,然后在画其他图形,这样后面的点才能画出来。待查明。
-
增加颜色和着色
平滑着色
我们期望在桌子的正上方有一盏灯,模拟光照体现桌子的细节。实现平滑着色
图片
引入三角形扇 & 给每个点都添加颜色属性
//Triangle fan coordinate
{ //six coordinate
0, 0,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
//line 1 red
-0.5f, 0f, 1f, 0f, 0f,
-0.5f, 0f, 1f, 0f, 0f,
//mallets
0f, -0.25f, 0f, 0f, 1f,
0f, 0.25f, 1f, 0f, 0f,
}
// 使用如下替换。
glDrawArrays(GL_TRAINGLE_FAN, 0, 6);
一个三角形扇以一个中心顶点作为起始,使用相邻的两个顶点创建三角形,实现了一个以中心点的形扇。为了使扇形闭合,第一个点需要使用两遍。
给着色器增加颜色属性
//vertex shader
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = a_Position;
gl_PointSize = 10.0; //点的大小
}
//fragment shader
precision mediump float;
varying vec4 v_Color; // uniform -> varying 从统一变为了变化
void main()
{
gl_FragColor = v_Color;
}
- Varying(变化) 定义的是glsl 中的变量,需要变化时引入的变量。它把给它的那些值进行混合,并把这些混合后的值发给片段着色器。比如直线,varying会给每个片段着色器配置一个颜色,使直线两端进行颜色渐变
- Attribute 定义的是可以和平台code 进行访问和交换的属性
一个varying 如何生成每个片段上混合后的颜色
使用的算法就是:线性插值算法
图片
- Line 插值计算公式:
blended_value = (vertex_0_value * (1 - distance_ratio)) + (vertex_1_value * distance_ratio) - 三角形插值计算公式:(通过面积权重进行混合计算)
blend_value = (vertex_0_value * vertex_0_weight) + (vertex_1_value * vertex_1_weight)- (vertex_2_value * (1 - vertex_0_weight - vertex_1_weight))
使用新的颜色属性渲染
//因为vertex增加了颜色,所以需要每个vertex的stride
private static final String A_COLOR = "a_Color";
private static final int COLOR_COMPOENT_COUNT = 3;
private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
//a_Color set
private int aColorLocation = glGetAttribLocation(program, A_COLOR);
vertexData.position(POSITION_COMPONENT_COUNT);
glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GL_FLOAT, false, STRIDE, vertexData);
glEnableVertexAttribArray(aColorLocation);
//a_Postion set
glVertexAttribPointer(aPositionLocation, COLOR_COMPONENT_COUNT, GL_FLOAT, false, STRIDE, vertexData);
//同时删掉glUniform4f() 设置颜色的配置和u_Color
绘制结果:
图片
- 调整屏幕的宽高比
怎么自适应布局?如何决定在屏幕上显示什么以及如何针对屏幕尺寸做出调整?
不同的设备比例尺寸不同,如何夸平台自适应?
通用解决办法:在OpenGL我们可以使用投影吧真是世界的一部分映射到屏幕上,以这种方式映射会是它在不同的屏幕尺寸或方向上看起来总是正确的。考虑到有如此大的设备量,能够适应这些设备是很重要的。
投影修复 ?
虚拟坐标空间
使用正交投影,把绘制在虚拟空间坐标转换回归一化设备坐标的方法,让OpenGL可以正确的渲染。
虚拟空间坐标定义了三维世界内部的一个区域,在这个区域内的所有东西都会显示在屏幕上,而区域外的所有东西都会被裁剪掉。图片
线性代数基础
OpenGL 大量的使用了向量和矩阵,矩阵的最重要的用途之一就是建立正交和透视投影。矩阵大量的加法和乘法在现代GPOU上执行的非常快。
向量
一个向量(vector)是一个有多个元素的一维数组。在OpenGL里,一个位置通常是一个四元素向量,颜色也是一样。我们使用的大多数向量一般都有四个元素。举例一个位置向量:
(xyzw)\begin{pmatrix} x \\ y \\ z \\ w \end{pmatrix} xyzw
矩阵
一个矩阵(Matrix)是一个由多个元素的二维数组。在OpenGL里,我们一般使用矩阵作向量投影,如正交或者透视投影,并且也用他们使物体旋转(rotation)、平移(translation)、以及缩放(scaling)。我们把矩阵与每个要变换的向量相乘即可实现这些变换。
4x4 齐次坐标变换矩阵示例 (OpenGL ES 常用)
4x4 矩阵在 3D 图形中用于 齐次坐标 (Homogeneous Coordinates) 变换,它结合了旋转、缩放和平移操作。
xxxyxzxwyxyyyzyyzxzyzzzzwxwywzww\]\\begin{bmatrix} x_x \& x_y \& x_z \& x_w \\\\ y_x \& y_y \& y_z \& y_y \\\\ z_x \& z_y \& z_z \& z_z \\\\ w_x \& w_y \& w_z \& w_w \\end{bmatrix} xxyxzxwxxyyyzywyxzyzzzwzxwyyzzww 矩阵与向量乘法 \[xxxyxzxwyxyyyzywzxzyzzzwwxwywzww\]\\begin{bmatrix} x_x \& x_y \& x_z \& x_w \\\\ y_x \& y_y \& y_z \& y_w \\\\ z_x \& z_y \& z_z \& z_w \\\\ w_x \& w_y \& w_z \& w_w \\end{bmatrix} xxyxzxwxxyyyzywyxzyzzzwzxwywzwww \[xyzw\]\\begin{bmatrix} x \\\\ y \\\\ z \\\\ w \\\\ \\end{bmatrix} xyzw = \[xxx+xyy+xzz+xwwyxx+yyy+yzz+ywwzxx+zyy+zzz+zwwwxx+wyy+wzz+www\]\\begin{bmatrix} x_xx + x_yy + x_zz + x_ww \\\\ y_xx + y_yy + y_zz + y_ww \\\\ z_xx + z_yy + z_zz + z_ww \\\\ w_xx + w_yy + w_zz + w_ww \\\\ \\end{bmatrix} xxx+xyy+xzz+xwwyxx+yyy+yzz+ywwzxx+zyy+zzz+zwwwxx+wyy+wzz+www 变换矩阵(左侧) x 坐标向量(右侧) = 坐标向量的变换。 即使用变换矩阵对坐标进行图形学的变换,使图形学代数运算化。 单位矩阵 单位矩阵诚意任何向量都等于任何向量的值。即 1 x X = X。 I=\[1000010000100001\]\\mathbf{I} = \\begin{bmatrix} 1 \& 0 \& 0 \& 0 \\\\ 0 \& 1 \& 0 \& 0 \\\\ 0 \& 0 \& 1 \& 0 \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix}I= 1000010000100001 使用平移矩阵 I=\[100xtranslation010ytranslation001ztranslation0001\]\\mathbf{I} = \\begin{bmatrix} 1 \& 0 \& 0 \& x_{translation} \\\\ 0 \& 1 \& 0 \& y_{translation} \\\\ 0 \& 0 \& 1 \& z_{translation} \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix}I= 100001000010xtranslationytranslationztranslation1 设置x,y,z 即可对向量进行三分量上平移。 定义正交投影 Android中有Martix类,用于定义矩阵。 orthoM(float\[\] m, int mOffset, float left, float right, float bottom, float top, float near, float far) //float\[\] m : 目标数组,用于存储正交投影矩阵,大小:4x4 = 16 //int mOffset :结果矩阵其值的偏移值 //float left:x 轴的最小范围 //float right:x 轴的最大范围 //float bottom:y轴的最小范围 //float top:y的最大范围 //float near:z的最小范围 //float near:z的最大范围 生成的矩阵: \[2right−left00right+leftright−left02top−bottom0top+bottomtop−bottom00−2far−nearfar+nearfar−near0001\]\\begin{bmatrix} \\frac{2}{\\text{right} - \\text{left}} \& 0 \& 0 \& \\frac{\\text{right} + \\text{left}}{\\text{right} - \\text{left}} \\\\ 0 \& \\frac{2}{\\text{top} - \\text{bottom}} \& 0 \& \\frac{\\text{top} + \\text{bottom}}{\\text{top} - \\text{bottom}} \\\\ 0 \& 0 \& \\frac{-2}{\\text{far} - \\text{near}} \& \\frac{\\text{far} + \\text{near}}{\\text{far} - \\text{near}} \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix} right−left20000top−bottom20000far−near−20right−leftright+lefttop−bottomtop+bottomfar−nearfar+near1 正交投影背后的数学: * 单位正交投影 把x,y,z取\[-1,1\]的范围填入矩阵,可以得到单位正交矩阵(z轴取右手坐标系)。 I=\[1000010000−100001\]\\mathbf{I} = \\begin{bmatrix} 1 \& 0 \& 0 \& 0 \\\\ 0 \& 1 \& 0 \& 0 \\\\ 0 \& 0 \& -1 \& 0 \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix}I= 1000010000−100001 * 常规正交投影 把x,y,z取\[-1,1\]的范围填入矩阵,可以得到常规正交矩阵(z轴取右手坐标系)。 \[200−1020−100−2−10001\]\\begin{bmatrix} 2 \& 0 \& 0 \& -1 \\\\ 0 \& 2 \& 0 \& -1 \\\\ 0 \& 0 \& -2 \& -1 \\\\ 0 \& 0 \& 0 \& 1 \\end{bmatrix} 2000020000−20−1−1−11 取三个点进行验证:(0, 0, 0, 1) (0.5, 0.5, -0.5, 1)(1, 1, -1, 1) 进行矩阵乘法得到: (-1,-1,-1,1)(0,0,0,1)(1,1,1,1) 0点,中间点,最大点,通过正交投影后可以得到在OpenGL坐标系中对应的点。 左/右手坐标系 左:Z 越大离得越远。右:Z越大越近(OpenGL默认); 拇指x+,食指y+,中指对应Z。 代码加入正交投影 * glsl //glsl //定义mat4 u_Matrix : 4x4的矩阵 uniform mat4 u_Matrix; attribute vec4 a_Position; //位置进行转换 gl_Position = u_Matrix \* a_Position; 经过以上定义后a_Position 不再属于归一化坐标空间,而是属于矩阵定义的虚拟坐标空间。 * Code private final float\[\] projectionMatrix = new float\[16\]; private int uMatrixLocation = glGetUniformLocation(program, "u_Matrix"); final float aspectRatio = width \> heigh ? (float) width / (float) height : (float) height / (float) width; if (width \> height) { // landscape orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); } else { //Protrait or square orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); } glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0); 这里还是以归一化坐标空间为基准,取短的一个方向为归一化,长的方向基于归一化进行按比例扩大。举例:w \> h, x取\[-1,1\], y取\[-1 \* w/h, 1 \* w/h\]; 即对长的进行按比例定位。 5. 进入第三维 学习目标: 6. 学习关于OpenGL的透视除法(perspective division)内容,以及如何在二维屏幕上使用w分量创造三维的幻象。 7. 了解w分量之后,学习如何设置透视投影,让我们可以看到三维形式的桌子。 三维的艺术: * 线性投影:在一个想象中的消失点处把并行线段聚合在一起,从而创建出立体化的幻象。 举例:每个铁路枕木被测量出来的尺寸将按照我们的眼睛与其之间的距例成比例递减。 \[图片
从着色器到屏幕的坐标变换
把一个在顶点着色器上的原始gl_Position 坐标变换为最终的屏幕坐标。
图片
这里涉及两个变换步骤和三个不同的坐标系。
裁剪空间
裁剪空间(clip space):对于任何给定的未知,它的x、y以及z分量都需要在那个未知的-w 和 w 之间。任何在这个范围之外的十五在屏幕上都是不可见的。
透视除法
在一个顶点未知成为一个归一化设备坐标之前,OpenGL实际上执行了一个额外的步骤,他被称为透视除法(perspective division)。透视除法之后,那个位置就在归一化设备坐标中了,不管渲染区域的大小和形状,对于其中的每个可视坐标,其中x、y、z分量的取值都位于[-1, 1]的范围内。
位置向量中的w 用于透视除法。在创建三维的幻象时,OpenGL会把每个gl_Position的x、y、z都除以w分量。举例:
假设一个物体,它有两个顶点,每个顶点在三维空间中的x、y、z 分量相同,w分量不同。坐标分别是(1,1,1,1) 和(1,1,1,2),经过透视除法之后,归一化设备坐标:(1,1,1) 和(0.5,0.5,0.5)。有较大的w值得坐标被移动到距离(0,0,0)更近得位置,(0,0,0)就是归一化设备坐标里渲染区域得中心。
图片
-
同质化坐标
因为透视除法,裁剪空间中得坐标经常被称为同质化坐标,被称为同质化得原因是因为裁剪空间的几个坐标可以映射到同一个点。举例:
(1, 1, 1, 1), (2, 2, 2, 2), (3, 3, 3, 3), (4, 4, 4, 4), (5, 5, 5, 5) = 透视除法 =》 (1, 1, 1).
-
除以w的优势
为什么不用z,而添加w分量?z分量表示离屏幕的距离。尽管行得通,但是导致了透视投影和z坐标的耦合,使用w解耦后可以在正交投影和透视投影之间切换。同时保留z分量作为深度缓冲器还有其他好处。
视口变换
OpenGL 把归一化坐标的x 和 y 分量映射到屏幕上的一个区域内,这个区域是操作系统预留出来用于显示的,被称为视口(viewport),这些被映射的坐标被称为窗口坐标(window coordinate)。
glViewport() 接口用于告诉OpenGL的视口。
添加w分量创建三维图
//坐标分量 从 2 个 增加到 4 个
#define POSITION_COMPONENT_COUNT 4
GLfloat vVertices[] = {
// triangle fan (x,y,z,w,r,g,b,a)
0.0f, 0.0f, 0.0f, 1.5f, 1.0f, 1.0f, 1.0f,
-0.5f, -0.8f, 0.0f, 1.0f, 0.0f, 0.7f, 0.7f,
0.5f, -0.8f, 0.0f, 1.0f, 0.7f, 0.0f, 0.7f,
0.5f, 0.8f, 0.0f, 2.0f, 0.7f, 0.7f, 0.0f,
-0.5f, 0.8f, 0.0f, 2.0f, 0.7f, 0.0f, 0.7f,
-0.5f, -0.8f, 0.0f, 1.0f, 0.0f, 0.7f, 0.7f,
// line 1
-0.5f, 0.0f, 0.0f, 1.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f, 1.5f, 1.0f, 0.0f, 0.0f,
// mallets
0.0f, -0.25f, 0.0f, 1.25f, 0.0f, 0.0f, 1.0f,
0.0f, 0.25f, 0.0f, 1.75f, 1.0f, 0.0f, 0.0f,
//testPoint
0.0f, 0.0f,
};
使用透视投影
走进透视投影背后的数学之前,先在视觉层次上讨论一下。
- 视椎体
这个形状叫做视锥体,这个观看空间是由一个透视投影矩阵和投影除法创建的。简单说,视锥体只是一个立方体,其远端比近端大,从而使其变成一个被截断的金字塔。两端的大小差别越大,观察的范围越宽,我们能看到的也越多。图片
一个视锥体有一个焦点(focal point)。顺着从视锥体较大端向较小端扩展出来的那些直线,一直向前通过较小端直到他们汇聚到一起。像是人的视角。焦点和视锥体小端的距离被称为焦距(focal length),它影响视锥体小端和大端的比例,以及其对应的视野。
从焦点观察视锥体,任意垂直切面在屏幕上占据的大小都是一样的。
定义透视投影
创造三维,透视投影矩阵需要和透视除法一起发挥作用。 如果一个物体向屏幕中心移动,当它离我们越来越远时,他的大小也越来越小,因此,投用矩阵最重要的任务就是为w产生正确的值。这样,当OpenGL做透视除法的时候,远处的物体看起来比近处的物体小。能够实现方法之一是使用z分量,把它作为物体与焦点的距离并且把这个举例映射到w。这个距离越大,w的值越大,所得到的物体越小。
图片
-
代码实现:(原型取自Android Matrix.java)
#include <math.h>
static void perspectiveM(float* m, int offset,
float fovy, float aspect, float zNear, float zFar)
{
float f = 1.0f / (float) tan(fovy * (M_PI / 360.0));
float rangeReciprocal = 1.0f / (zNear - zFar);
m[offset + 0] = f / aspect;
m[offset + 1] = 0.0f;
m[offset + 2] = 0.0f;
m[offset + 3] = 0.0f;
m[offset + 4] = 0.0f;
m[offset + 5] = f;
m[offset + 6] = 0.0f;
m[offset + 7] = 0.0f;
m[offset + 8] = 0.0f;
m[offset + 9] = 0.0f;
m[offset + 10] = (zFar + zNear) * rangeReciprocal;
m[offset + 11] = -1.0f;
m[offset + 12] = 0.0f;
m[offset + 13] = 0.0f;
m[offset + 14] = 2.0f * zFar * zNear * rangeReciprocal;
m[offset + 15] = 0.0f;
}
使用透视投影
perspectiveM(projectMatrix, 0, 45, (float)(uWindowWidth)/(float)(uWindowHeight), 1.0f, 10.0f);
使用45°投影矩阵代替正交矩阵后,桌子消失不见。因为桌子定义的z坐标是0,但是投影矩阵定义的z坐标范围是-1,-10. 不在视野范围内,所以消失。
-
使用模型矩阵移动物体到视野范围内。
- 定义一个单位矩阵,并通过translate x = 0、y = 0、z = -2 ,把模型矩阵平移到z坐标范围内。
static void translateM(float* m, int mOffset,
float x, float y, float z)
{
for (int i = 0; i < 4; i++) {
int mi = mOffset + i;
m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
}
}
- 定义一个单位矩阵,并通过translate x = 0、y = 0、z = -2 ,把模型矩阵平移到z坐标范围内。
//model matrix, 单位矩阵?
static void setIdentityM(float* sm, int smOffset) {
for (int i = 0; i < 16; i++) {
sm[smOffset + i] = 0;
}
for(int i = 0; i < 16; i += 5) {
sm[smOffset + i] = 1.0f;
}
}
GLfloat modelMatrix[16];
setIdentityM(modelMatrix, 0);
translateM(modelMatrix, 0.0f, 0.0f, 0.0f, -2.0f);
- 矩阵乘法
桌子坐标最终使用的矩阵为:透视矩阵、模型矩阵。
可以在glsl中使 透视矩阵 * 模型矩阵 * vertex(顶点),也可以在编码时直接计算 透视矩阵 * 模型矩阵 传入glsl顶点着色器中。
透视矩阵 * 模型矩阵:
void multiplyMM(float* r, const float* lhs, const float* rhs)
{
for (int i=0 ; i<4 ; i++) {
const float rhs_i0 = rhs[ I(i,0) ];
float ri0 = lhs[ I(0,0) ] * rhs_i0;
float ri1 = lhs[ I(0,1) ] * rhs_i0;
float ri2 = lhs[ I(0,2) ] * rhs_i0;
float ri3 = lhs[ I(0,3) ] * rhs_i0;
for (int j=1 ; j<4 ; j++) {
const float rhs_ij = rhs[ I(i,j) ];
ri0 += lhs[ I(j,0) ] * rhs_ij;
ri1 += lhs[ I(j,1) ] * rhs_ij;
ri2 += lhs[ I(j,2) ] * rhs_ij;
ri3 += lhs[ I(j,3) ] * rhs_ij;
}
r[ I(i,0) ] = ri0;
r[ I(i,1) ] = ri1;
r[ I(i,2) ] = ri2;
r[ I(i,3) ] = ri3;
}
}
GLfloat tmpMatrix[16];
multiplyMM(tmpMatrix, 0, projectMatrix, 0, modelMatrix, 0);
memcpy(projectMatrix, tmpMatrix, sizeof(tmpMatrix));
//glsl 顶点坐标最终使用的matrix
glUniformMatrix4fv(uMatrixLocation, 1, GL_FALSE, projectMatrix);
矩阵CPU只计算一次保存,然后传入给GPU,这种方法应该可以节省GPU的开销。同时也少了和glsl之间的传递。
增加旋转
旋转方向
使用右手坐标系,分别可以沿着x,y,z轴进行旋转。可以想象坐标系中的物体根据旋转发生的视角变换。
旋转矩阵
图片
- 代码实现:
// --- 辅助函数实现 (Length)
float length(float x, float y, float z)
{
float sum_of_squares = x * x + y * y + z * z;
return sqrtf(sum_of_squares);
}
/**
-
@brief 4x4 矩阵的轴角旋转实现。
-
- 计算旋转矩阵 R,然后执行 m = m * R。
-
- @param m source matrix (in-place modification)
-
@param mOffset index into m where the matrix starts
-
@param a angle to rotate in degrees
-
@param x X axis component
-
@param y Y axis component
-
@param z Z axis component
*/
void rotateM(float *m, int mOffset,
float a, float x, float y, float z)
{
float norm_x, norm_y, norm_z;
float len = length(x, y, z);
// 1. 归一化旋转轴 (如果长度接近零,则不旋转)
if (len < 1e-6) {
return;
}
float inv_len = 1.0f / len;
norm_x = x * inv_len;
norm_y = y * inv_len;
norm_z = z * inv_len;
// 2. 将角度转换为弧度
float rad = a * PI / 180.0f;
float c = cosf(rad);
float s = sinf(rad);
float omc = 1.0f - c; // One Minus Cosine
// 3. 构建 4x4 旋转矩阵 R (列主序)
float R[16];
// R 的元素 (i, j) 索引是 i + 4*j
// R[0][0] = c + x^2 * (1 - c)
R[0 + 40] = c + norm_x * norm_x * omc;
// R[1][0] = x y*(1 - c) + zs
R[1 + 4 0] = norm_x * norm_y * omc + norm_z * s;// R[2][0] = xz (1 - c) - ys
R[2 + 4 0] = norm_x * norm_z * omc - norm_y * s;// R[3][0] = 0
R[3 + 4*0] = 0.0f;
// R[0][1] = yx (1 - c) - zs
R[0 + 4 1] = norm_y * norm_x * omc - norm_z * s;// R[1][1] = c + y^2 * (1 - c)
R[1 + 41] = c + norm_y * norm_y * omc;
// R[2][1] = y z*(1 - c) + xs
R[2 + 4 1] = norm_y * norm_z * omc + norm_x * s;// R[3][1] = 0
R[3 + 4*1] = 0.0f;
// R[0][2] = zx (1 - c) + ys
R[0 + 4 2] = norm_z * norm_x * omc + norm_y * s;// R[1][2] = zy (1 - c) - xs
R[1 + 4 2] = norm_z * norm_y * omc - norm_x * s;// R[2][2] = c + z^2 * (1 - c)
R[2 + 42] = c + norm_z * norm_z * omc;
// R[3][2] = 0
R[3 + 42] = 0.0f;// R[0][3] = 0
R[0 + 43] = 0.0f;
// R[1][3] = 0
R[1 + 4 3] = 0.0f;// R[2][3] = 0
R[2 + 43] = 0.0f;
// R[3][3] = 1 (齐次坐标)
R[3 + 43] = 1.0f;// 4. 执行矩阵乘法 M_new = M_old * R
// 注意:在 GL/GLES 约定中,变换通常是右乘 (M * R)
float temp_m[16];
// 复制 M 的当前状态到 temp_m,作为乘法的 LHS
memcpy(temp_m, m + mOffset, 16 * sizeof(float));
// 执行乘法:结果写回 m (从 mOffset 开始)
// m[new] = temp_m[old] * R
multiplyMM(m, mOffset,
temp_m, 0, // LHS: 原矩阵
R, 0); // RHS: 旋转矩阵
}
- 使用旋转矩阵
rotateM(projectMatrix, 0, -(GLfloat)uRotationRate, 1.0f, 1.0f, 1.0f);
- 使用纹理增加细节(texture)
简单说:纹理就是一个图像或照片,他们可以被加载进OpenGL中。使用纹理增加细节使物体变得更加真实。
- 介绍纹理,并加载到OpenGL中
- 显示纹理,并支持多个着色器程序
- 纹理过滤模式及应用场景