问题与目标
图片通过 sketch 制作
1、它们的顶点数据均为:
顶点数组
VFVertex
2、借助 Matlab 把顶点数据绘制出来:
分布图
从图可以看出,这三个数据形成的其实是一个等边直角三角形,而在 iOS 模拟器中通过 OpenGL ES 绘制出来的是直角三角形,所以是有问题的,三角形被拉伸了。
3、on-Screen (屏幕) 的像素分布情况:
OpenGL ES 的屏幕坐标系
物理屏幕的坐标系
分析:前者是正方体,后者长方体,不拉伸才怪。
动画演示修复:
FixTriangle.gif
所以要做的事情是,把顶点坐标的 Y 坐标变小,而且是要根据当前显示屏幕的像素比来进行缩小。
Gif 图片,由 C4D 制作,PS 最终导出;
-- 建议 --:如果向量、矩阵知识不熟悉的可以看看《线性代数》一书;如果已经有相应的基础了,可以直接看《3D数学基础:图形与游戏开发》,了解 3D 的世界是如何用向量和矩阵知识描述的;若对 3D 知识有一定的认识,可以直接看《OpenGL Programming Guide》8th 的变换知识, 或 《OpenGL Superblble》7th 的矩阵与变换知识,明确 OpenGL 是如何应用这些知识进行图形渲染的。
注:以下核心知识均来源于,《3D数学基础:图形与游戏开发》,建议看一下第8章;
4x4 整体
图片通过 sketch 制作,请放大看
4X4方阵
线性变换
normalied
a、 v不能是零向量,即零向量为{0,0,0}; b、||v||是向量的模,即向量的长度; c、例子是2D向量的,3D/4D向量都是一样的 【 sqrt(pow(x,2)+pow(y,2)+pow(w,2)...) 】
图片来源于《3D数学基础:图形与游戏开发》5.7
1) 线性缩放
线性缩放
X方向,就是{1,0,0};Y方向,就是{0,1,0};Z方向,就是{0,0,1};分别代入上面的公式即可得到。
图片来源于《3D数学基础:图形与游戏开发》8.3.1
2) 线性旋转
线性旋转
图片来源于《3D数学基础:图形与游戏开发》8.2.2
平移
直接把平移向量,按分量{x, y, z}依次代入齐次矩阵即可;
图片来源于《3D数学基础:图形与游戏开发》9.4.2
四元数
a.向量,即4D向量,也称齐次坐标{x, y, z, w}; 4D->3D,{x/w, y/w, z/w};
b.四元数,[ w, v ]或[ w, (x,y,z) ]两种记法,其中 w 就是一个标量,即一个实数;
c.点乘
矩阵乘法,点乘 c.1 上面两种是合法的,而下面两种是不合法的,就是没有意义的; c.2 第一个为 A(1x3) 行向量(矩阵)与 B(3x3)方阵的点乘,第二个是 A(3x3) 的方阵与 A(3x1) 的列向量(矩阵)的点乘;
图片来源于《3D数学基础:图形与游戏开发》7.1.7
这块内容现在先不深究,不影响对本文内容的理解。
w
w,与平移向量{x, y, z}组成齐次坐标;一般情况下,都是1;
投影
这里主要是控制投影,如透视投影;如:
图片来源于《3D数学基础:图形与游戏开发》9.4.6
这里主要讨论第一阶段 Vertex 的 3D 变换,对于视图变换、投影变换,不作过多讨论;如果要完全掌握后面两个变换,还需要掌握 OpenGL 下的多坐标系系统,以及摄像机系统的相关知识。
图片来源于,《3D数学基础:图形与游戏开发》8.1;左右手坐标系是用来定义方向的。
右手坐标
图片来源于,Diney Bomfim 的《Cameras on OpenGL ES 2.x - The ModelViewProjection Matrix》;这个就是 OpenGL 使用的坐标系,右手坐标系;其中白色小手演示了在各轴上旋转的正方向(黑色箭头所绕方向);
行向量
列向量
图片来源于,《线性代数》矩阵及其运算一节
从图中的结果就可以看出,左乘和右乘运算后是完全不一样的结果;虽然图片中的矩阵是 2 x 2 方阵,但是扩展到 n x n 也是一样的结果;
图1,列向量
图2
红框处的向量就是 v_Position 顶点数据;即 OpenGL 用的是列向量;(木有找到更有力的证据,只能这样了)
图3
很明显,例子使用的就是左乘,即 OpenGL 用的是左乘;
图 1、3 来源于,《OpenGL Programming Guide 8th》第5章第二节 图 2 来源于,《3D数学基础:图形与游戏开发》7.1.8
多次变换
4x4 整体 OpenGL
因为列向量的影响,在做点乘的时候,平移放在下方与右侧是完全不一样的结果,所以进行了适应性修改
4X4方阵 OpenGL
平移 OpenGL
等式左侧:A(4x4)方阵点乘{v.x, v.y, v.z, 1.0}是顶点数据列向量;右侧就是一个 xyz 均增加一定偏移的列向量
图片来源于,《OpenGL Superblble》7th, Part 1, Chapter 4. Math for 3D Graphics
投影 OpenGL
物体的坐标是否与屏幕坐标原点重叠
Linaer Transforms
Identity
无变换,即此矩阵与任一向量相乘,不改变向量的所有分量值,能做到这种效果的就是单位矩阵,而我们使用的向量是齐次坐标{x, y, z, w},所以使用 4 x 4 方阵;{w === 1}.
Scale
单一的线性变换——缩放,缩放变换是作用在蓝色区域的 R(3x3) 方阵的正对角线(从m11(x)->m22(y)->m33(z))中;例子是 X、Y、Z 均放大 3 倍。
Rotate
单一的线性变换——旋转,旋转变换是作用在蓝色区域的 R(3x3) 方阵中;例子是绕 Z 轴旋转 50 度。
Translation
单一的线性变换——平移,平移变换是作用在绿色区域的 R(3x1) 矩阵中({m11, m21, m31}对应{x, y, z});例子是沿 X 正方向平移 2.5 个单位。
Translation&Scale
Translation&Rotate
以上图片内容来源于《OpenGL Programming Guide》8th, Linear Transformations and Matrices 一小节,使用 skecth 重新排版并导出
连续变换
这里的问题就是先旋转还是后旋转。旋转前后,变化的是物体的坐标系(虚线(变换后),实线(变换前)),主要是看你要什么效果,而不是去评论它的对错。
图片来源于,《OpenGL Superblble》7th, Matrix Construction and Operators 一节;
3D变换
ES 主要看红框处的顶点着色阶段即可,所以我们的变换代码是写在 Vertex Shader 的文件中。
变换转换
这里描述了三个变换阶段,第一个阶段是模型变换,第二个是视图变换阶段,第三个是投影变换阶段,最后出来的才是变换后的图形。本文讨论的是第一个阶段。
详细过程
作为了解即可
以上图片均来源于,《OpenGL Programming Guide》8th, 5. Viewing Transformations, Clipping, and Feedback 的 User Transformations 一节;
增加了一个 uniform 变量,而且是 mat4 的矩阵类型,同时左乘于顶点数据;
补充:n x m · 4 x 1 -> 4 x 1,如果要出现最终 4 x 1 那么,n 必须要是 4;如果矩阵点乘成立,那么 m 必须要是 4; 所以最终结果是 n x m = 4 x 4 ;
这里主要解决,如何给 uniform 变量赋值,而且在什么时候进行赋值的问题
核心步骤
1、在 glLinkProgram 函数之后,利用 glGetUniformLocation 函数得到 uniform 变量的 location (内存标识符);
2、从 Render Buffer 得到屏幕的像素比(宽:高)值,即为缩小的值;
3、使用 Shader Program , 调用 glUseProgram 函数;
4、使用 3D 变换知识,得到一个缩放矩阵变量 scaleMat4;
5、使用 glUniform* 函数把 scaleMat4 赋值给 uniform 变量;
1、得到 uniform 的内存标识符
要在 glLinkProgram 后,再获取 location 值,因为只有链接后 Program 才会 location 的值
- (BOOL)linkShaderWithProgramID:(GLuint)programID {
// 绑定 attribute 变量的下标
// 如果使用了两个或以上个 attribute 一定要绑定属性的下标,不然会找不到数据源的
// 因为使用了一个的时候,默认访问的就是 0 位置的变量,必然存在的,所以才不会出错
[self bindShaderAttributeValuesWithShaderProgramID:programID];
// 链接 Shader 到 Program
glLinkProgram(programID);
// 获取 Link 信息
GLint linkSuccess;
glGetProgramiv(programID, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLint infoLength;
glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLength);
if (infoLength > EmptyMessage) {
GLchar *messages = malloc(sizeof(GLchar *) * infoLength);
glGetProgramInfoLog(programID, infoLength, NULL, messages);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"Error: Link Fail %@ !", messageString);
free(messages);
}
return Failure;
}
// 在这里
[self.shaderCodeAnalyzer updateActiveUniformsLocationsWithShaderFileName:@"VFVertexShader"
programID:programID];
return Successfully;
}
- (void)updateActiveUniformsLocationsWithShaderFileName:(NSString *)fileName programID:(GLuint)programID {
NSDictionary *vertexShaderValueInfos = self.shaderFileValueInfos[fileName];
ValueInfo_Dict *uniforms = vertexShaderValueInfos[UNIFORM_VALUE_DICT_KEY];
NSArray *keys = [uniforms allKeys];
for (NSString *uniformName in keys) {
const GLchar * uniformCharName = [uniformName UTF8String];
// 在这里
GLint location = glGetUniformLocation(programID, uniformCharName);
VFShaderValueInfo *info = uniforms[uniformName];
info.location = location;
}
}
补充:
glGetActiveUniform | |
---|---|
void glGetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size, GLenum* type, char* name) | |
program 指 Shader Program 的内存标识符 | |
index 指下标,第几个 uniform 变量,[0, activeUniformCount] | |
bufSize 所有变量名的字符个数,如:v_Projection , 就有 12 个,如果还定义了 v_Translation 那么就是12 + 13 = 25个 | |
length * NULL 即可* | |
size 数量,uniform 的数量,如果不是 uniform 数组,就写 1,如果是数组就写数组的长度 | |
type uniform 变量的类型,GL_FLOAT, GL_FLOAT_VEC2,GL_FLOAT_VEC3, GL_FLOAT_VEC4,GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_BOOL,GL_BOOL_VEC2, GL_BOOL_VEC3, GL_BOOL_VEC4,GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4,GL_SAMPLER_2D, GL_SAMPLER_CUBE | |
name uniform 变量的变量名 |
// 这个函数可以得到,正在使用的 uniform 个数,即可以知道 index 是从 0 到几;
// 还有可以得到,bufSize 的长度
glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, &numUniforms);
glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH,
&maxUniformLen);
注:VFShaderValueRexAnalyzer 类就是一个方便进行调用的一种封装而已,你可以使用你喜欢的方式进行封装;
图片来源于,《OpenGL ES 2.0 Programming Guide》4. Shaders and Programs,Uniforms and Attributes 一节
- (void)drawTriangle {
[self.shaderManager useShader];
[self.vertexManager makeScaleToFitCurrentWindowWithScale:[self.rboManager windowScaleFactor]];
[self.vertexManager draw];
[self.renderContext render];
}
2、得到屏幕的像素比
- (CGFloat)windowScaleFactor {
CGSize renderSize = [self renderBufferSize];
float scaleFactor = (renderSize.width / renderSize.height);
return scaleFactor;
}
补充:renderBufferSize
- (CGSize)renderBufferSize {
GLint renderbufferWidth, renderbufferHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &renderbufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &renderbufferHeight);
return CGSizeMake(renderbufferWidth, renderbufferHeight);
}
3、使用 Shader Program
- (void)useShader {
glUseProgram(self.shaderProgramID);
}
4、使用 3D 变换知识,得到一个缩放矩阵变量 scaleMat4
VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);
扩展1:
VFMatrix4 VFMatrix4MakeXYZScale(float sx, float sy, float sz) {
VFMatrix4 r4 = VFMatrix4Identity;
VFMatrix4 _mat4 = {
sx , r4.m12, r4.m13, r4.m14,
r4.m21, sy , r4.m23, r4.m24,
r4.m31, r4.m32, sz , r4.m34,
r4.m41, r4.m42, r4.m43, r4.m44,
};
return _mat4;
};
VFMatrix4 VFMatrix4MakeScaleX(float sx) {
return VFMatrix4MakeXYZScale(sx, 1.f, 1.f);
};
VFMatrix4 VFMatrix4MakeScaleY(float sy) {
return VFMatrix4MakeXYZScale(1.f, sy, 1.f);
};
VFMatrix4 VFMatrix4MakeScaleZ(float sz) {
return VFMatrix4MakeXYZScale(1.f, 1.f, sz);
};
它们都定义在:
VFMath
注:如果不想自己去写这些函数,那么可以直接使用 GLKit 提供的
数学函数
个人建议,自己去尝试写一下会更好
5、使用 glUniform 函数把 scaleMat4 赋值给 uniform 变量*
- (void)makeScaleToFitCurrentWindowWithScale:(float)scale {
NSDictionary *vertexShaderValueInfos = self.shaderCodeAnalyzer.shaderFileValueInfos[@"VFVertexShader"];
ValueInfo_Dict *uniforms = vertexShaderValueInfos[UNIFORM_VALUE_DICT_KEY];
// NSLog(@"uniforms %@", [uniforms allKeys]);
// v_Projection 投影
// VFMatrix4 scaleMat4 = VFMatrix4Identity;
VFMatrix4 scaleMat4 = VFMatrix4MakeScaleY(scale);
VFMatrix4 transMat4 = VFMatrix4Identity; //VFMatrix4MakeTranslationX(0.3)
glUniformMatrix4fv((GLint)uniforms[@"v_Projection"].location, // 定义的 uniform 变量的内存标识符
1, // 不是 uniform 数组,只是一个 uniform -> 1
GL_FALSE, // ES 下 只能是 False
(const GLfloat *)scaleMat4.m1D); // 数据的首指针
glUniformMatrix4fv((GLint)uniforms[@"v_Translation"].location, // 定义的 uniform 变量的内存标识符
1, // 不是 uniform 数组,只是一个 uniform -> 1
GL_FALSE, // ES 下 只能是 False
(const GLfloat *)transMat4.m1D); // 数据的首指针
}
扩展2:
3、完整工程:Github: DrawTriangle_Fix
glsl 代码分析类
核心的知识是正则表达式,主要是把代码中的变量解析出来,可以对它们做大规模的处理。有兴趣可以看一下,没有兴趣的可以忽略它完全不影响学习和练习本文的内容。