我们知道正方体是有六个面,八个顶点的.今天我们就准备使用八个顶点,绘制一个立方体到屏幕上.
效果图
显示立方体源码
#import "CubeViewController.h"
#import "os_cube.h"
@interface CubeViewController ()
@property (nonatomic ,strong) Vertex * vertexColor ;
@property (nonatomic ,strong) VertexElement * vertexElement ;
@property (nonatomic ,assign) GLuint modelMat4 ;
@property (nonatomic ,assign) GLuint eyeMat4 ;
@property (nonatomic ,assign) GLuint projectMat4;
@end
@implementation CubeViewController
#define CubeVertexNum 8
-(void)createShader{
// glEnable(GL_DEPTH_TEST);
self.shader = [Shader new];
[self.shader compileLinkSuccessShaderName:@"Shader" completeBlock:^(GLuint program) {
glBindAttribLocation(program, GLKVertexAttribPosition, "position"); // 0代表枚举位置
glBindAttribLocation(program, GLKVertexAttribColor, "vertexColor");
}];
self.modelMat4 = glGetUniformLocation(self.shader.program, "modelMat4");
self.eyeMat4 = glGetUniformLocation(self.shader.program, "eyeMat4");
self.projectMat4 = glGetUniformLocation(self.shader.program, "projectMat4");
}
-(void)loadVertex{
glEnable(GL_DEPTH_TEST);
self.vertex = [Vertex new];
int vertexNum =sizeof(cubeVertices)/(3 * sizeof(GLfloat));
[self.vertex allocVertexNum:vertexNum andEachVertexNum:3];
GLfloat tempVertex[3];
for (int i =0; i<vertexNum; i++) {
for (int j=0; j<3; j++) {
tempVertex[j]=cubeVertices[i*3+j];
}
[self.vertex setVertex:tempVertex index:i];
}
[self.vertex bindBufferWithUsage:GL_STATIC_DRAW];
[self.vertex enableVertexInVertexAttrib:GLKVertexAttribPosition numberOfCoordinates:3 attribOffset:0];
self.vertexColor = [Vertex new];
vertexNum =sizeof(cubeColors)/(3 * sizeof(GLfloat));
[self.vertexColor allocVertexNum:vertexNum andEachVertexNum:3];
for (int i =0; i<vertexNum; i++) {
for (int j=0; j<3; j++) {
tempVertex[j]=cubeColors[i*3+j];
}
[self.vertexColor setVertex:tempVertex index:i];
}
[self.vertexColor bindBufferWithUsage:GL_STATIC_DRAW];
[self.vertexColor enableVertexInVertexAttrib:GLKVertexAttribColor numberOfCoordinates:3 attribOffset:0];
self.vertexElement = [VertexElement new];
[self.vertexElement allocWithIndexNum:sizeof(tfan1)/sizeof(tfan1[0]) indexArr:tfan1];
}
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
// 清除颜色缓冲区
glClearColor(1,1, 1, 1);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
// 使用着色器程序
glUseProgram(self.shader.program);
GLKMatrix4 mat= GLKMatrix4Identity;
mat = GLKMatrix4MakeScale(0.5,0.5,0.5);
mat = GLKMatrix4Rotate(mat, M_PI/180.0*45, 1.0, 1.0, 0.0);
glUniformMatrix4fv(self.modelMat4, 1, GL_FALSE,&mat);
mat= GLKMatrix4Identity;
mat = GLKMatrix4MakeLookAt(0,0,-1,0,0,1,0,1,0);
glUniformMatrix4fv(self.eyeMat4, 1, GL_FALSE,&mat);
float aspect = fabs(self.view.bounds.size.width / self.view.bounds.size.height);
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90), aspect, 0.1f, 2.0);
glUniformMatrix4fv(self.projectMat4, 1, GL_FALSE,&projectionMatrix);
[self.vertex drawVertexWithMode:GL_TRIANGLES startVertexIndex:0 numberOfVertices:sizeof(cubeVertices)/(3 * sizeof(GLfloat))];
[self.vertexColor drawVertexWithMode:GL_TRIANGLES startVertexIndex:0 numberOfVertices:sizeof(cubeColors)/(3 * sizeof(GLfloat))];
[self.vertexElement drawElementIndexWithMode:GL_TRIANGLES];
}
@end
#import <UIKit/UIKit.h>
#import "GLBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface CubeViewController : GLBaseViewController
@end
NS_ASSUME_NONNULL_END
#import "GLBaseViewController.h"
@interface GLBaseViewController ()
@end
@implementation GLBaseViewController
-(void)createEagContext{
self.eagcontext = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.eagcontext];
}
-(void)configure{
GLKView *view = (GLKView*)self.view;
view.context = self.eagcontext;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
}
-(void)createShader{
}
-(void)loadVertex{
}
-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
}
-(void)viewDidLoad{
[super viewDidLoad];
[self createEagContext];
[self configure];
[self createShader];
[self loadVertex];
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>
#import "OpenGLUtilsHeader.h"
NS_ASSUME_NONNULL_BEGIN
@interface GLBaseViewController : GLKViewController
@property (nonatomic ,strong) EAGLContext * eagcontext;
@property (nonatomic ,assign) GLuint program;
@property (nonatomic ,strong) Shader * shader ;
@property (nonatomic ,strong) Vertex * vertex ;
-(void)loadVertex;
-(void)createShade;
@end
NS_ASSUME_NONNULL_END
立方体的顶点代码
#ifndef os_cube_h
#define os_cube_h
static const GLfloat cubeVertices[] =
{
0.5, 0.5, 0.5,
-0.5, 0.5, 0.5,
-0.5,-0.5, 0.5,
0.5,-0.5, 0.5,
0.5, 0.5, 0,
-0.5, 0.5, 0,
-0.5,-0.5,0,
0.5,-0.5,0,
//1
};
static const GLfloat cubeColors[] = {
1.0, 0, 0.0,
1.0, 0, 0.0,
1.0, 0, 0.0,
1.0, 0, 0.0,
0.0, 1.0, 0,
0.0, 1.0, 0,
0.0, 1.0, 0,
0.0, 1.0, 0,
};
//2
static const GLuint tfan1[12 * 3] =
{
0,2,1,
0,2,3,
0,7,4,
0,7,3,
0,5,4,
0,5,1,
6,1,5,
6,1,2,
6,3,2,
6,3,7,
6,4,5,
6,4,7
};
#endif /* os_cube_h */
shader 脚本代码
precision mediump float;//mediump
varying vec3 vertexColorVarying;
void main(){
lowp vec4 rgba = vec4(vertexColorVarying.r,vertexColorVarying.g,vertexColorVarying.b,1);
gl_FragColor = rgba;
}
attribute vec4 position;
attribute vec3 vertexColor;
varying vec3 vertexColorVarying;
uniform mat4 modelMat4;
uniform mat4 eyeMat4;
uniform mat4 projectMat4;
void main (){
vertexColorVarying = vertexColor;
gl_Position =projectMat4 *eyeMat4 * modelMat4 * position;
}
公用组件代码可以去github上下载.不贴代码了.
理论知识
OpenGL中的坐标处理过程包括模型变换、视变换、投影变换、视口变换等过程,如下图所示:
视图变换全图
在上面的图中,注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系,它们的关系如下图所示(来自Chapter 7. World in Motion):
图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成;而图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。
我们也经常成为左边的变化(用户定义的变换)也经常成为mvp
模型变换——从模型坐标系到世界坐标系
模型坐标系
局部坐标系(模型坐标系)是为了方便构造模型而设立的坐标系,建立模型时我们无需关心最终对象显示在屏幕哪个位置。模型的原点定位也可以有所不同,例如下面在模型坐标系定义的模型:
局部坐标系可以这样理解,就是自身所有顶点之间的关系。
我们可以对局部坐标系的物体进行缩放,移动,旋转等操作.
模型变换的主要目的是通过变换使得用顶点属性定义或者3d建模软件构造的模型,能够按照需要,通过缩小、平移等操作放置到场景中合适的位置。通过模型变换后,物体放置在一个全局的世界坐标系中,世界坐标系是所有物体交互的一个公共坐标系。
例如下面的图中在模型坐标系定义的茶壶模型(来自World, View and Projection Transformation Matrices):
世界坐标系
世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上所有点的坐标都是以该坐标系的原点来确定各自的位置的。
局部坐标系可以理解为
单个物体
内部间的关系
世界坐标系可以理解为物体间
的关系.
要找到物体在世界坐标系的具体位置,我们需要先从世界坐标系中找到局部坐标系再找到物体.
模型变换
模型变换包括:旋转、平移、缩放、错切等内容。
例如将物体从一个位置p=(x,y,z),移动到另一个位置p′=(x′,y′,z′)的过程,用矩阵表示为:
应用多个模型变换时,注意变换执行的顺序影响变换的结果,一般按照缩放–》旋转—》平移的顺序执行;
其实这里的这个矩阵T代表这局部坐标系放在世界坐标系原点进行T变换,比如变换,平移,缩放
视变换——从世界坐标系到相机坐标系
视变换是为了方便观察场景中物体而设立的坐标系,在这个坐标系中相机是个假想的概念,是为了便于计算而引入的。相机坐标系中的坐标,就是从相机的角度来解释世界坐标系中位置。相机和场景的示意图如下所示(来自World, View and Projection Transformation Matrices):
OpenGL中相机始终位于原点,指向-Z轴,而以相反的方式来调整场景中物体,从而达到相同的观察效果。
打个比方,把相机放入世界坐标系原点,朝向始终x轴,放一个立方体 在z轴的正上方。怎么才能看到这个立方体呢?两种方式
- 立方体不动,让相机绕y轴逆时针旋转90度。看到物体
- 2.相机不动,让立方体绕着y轴顺时针旋转90度,立方体现在在x轴上了。看到物体
OpenGL中采用方式2的观点来解释视变换。
这里需要知道,世界坐标系是绝对坐标系,不管物体还是相机都是通过世界坐标系建立联系的
但是相机必须放在世界坐标系原点,朝向-z轴.因此,我们每次指定相机的位置的时候其实并不在世界坐标系原点.因此,我们需要将相机移动到世界坐标系原点,因此世界坐标系的物体也要和相机做相同的操作才能保证相机和物体之间在世界坐标系中的相对位置不发生变化
通过在世界坐标系中指定相机的位置,指向的目标位置,以及viewUp向量来构造一个相机坐标系,通过视变换矩阵将物体坐标由世界坐标系转换到相机坐标系。
这里我们可以把相机当做一个物体,想固定一个在世界坐标系中,
我们需要固定相机的位置,
只有位置是不够的,因为相机可以在固定位置
上下左右前后旋转,为了让相机前后不旋转,那么我们需要将其固定只能朝向-z轴
,这样相机就不能前后旋转了,但是可以上下左右旋转,因此我们再次规定 相机正方向朝向y轴.
单纯知道一个位置是没办法确定相机不动的。(相机可以旋转),所以,我们需要规定一个相机的位置和看的方向。
知道相机的位置和方向还不够,因为相机可以旋转,看到的物体还是不一样的。因此,我们还需要知道相机正视图的法量方向。只有这样才能确定相机不会动弹
其实可以这样理解,如何悬挂一个物体让物体不移动呢? 需要吊着和拉着么
GLK_INLINE GLKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ,
float upX, float upY, float upZ)
{
GLKVector3 ev = { eyeX, eyeY, eyeZ };
GLKVector3 cv = { centerX, centerY, centerZ };
GLKVector3 uv = { upX, upY, upZ };
GLKVector3 n = GLKVector3Normalize(GLKVector3Add(ev, GLKVector3Negate(cv)));
GLKVector3 u = GLKVector3Normalize(GLKVector3CrossProduct(uv, n));
GLKVector3 v = GLKVector3CrossProduct(n, u);
GLKMatrix4 m = { u.v[0], v.v[0], n.v[0], 0.0f,
u.v[1], v.v[1], n.v[1], 0.0f,
u.v[2], v.v[2], n.v[2], 0.0f,
GLKVector3DotProduct(GLKVector3Negate(u), ev),
GLKVector3DotProduct(GLKVector3Negate(v), ev),
GLKVector3DotProduct(GLKVector3Negate(n), ev),
1.0f };
return m;
}
这个api 其实就是我们规定相机的方向,法向量方向以及在世界坐标系中的位置,我们根据该api 可以换算出相机移动到原点的矩阵.
这里不讲解公式的具体推导了.这里牵涉两个坐标系之间的转换问题.纯数学问题.
大体思路是先将相机移动到坐标原点,然后再经过两次旋转
投影变换——从世界坐标系到裁剪坐标系
投影方式决定以何种方式成像,投影方式有很多种,OpenGL中主要使用两种方式,即透视投影(perspective projection)和正交投影( orthographic projection)。
- 1.正交投影是平行投影的一种特殊情形,正交投影的投影线垂直于观察平面。平行投影的投影线相互平行,投影的结果与原物体的大小相等,因此广泛地应用于工程制图等方面。
- 2.透视投影的投影线相交于一点,因此投影的结果与原物体的实际大小并不一致,而是会近大远小。因此透视投影更接近于真实世界的投影方式。
示意图
上面的图中,红色和黄色球在视见体内,因而呈现在投影平面上,而绿色球在视见体外,没有在投影平面上成像。指定视见体通过(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble nearVal, GLdouble farVal)6个参数来指定。注意在相机在世界坐标系下,相机指向-z轴,并且在世界坐标系原点,因此,nearVal和farVal表示的剪裁平面分别为:近裁剪平面z=−nearVal,以及远裁剪平面z=−farVal。
怎么计算模型坐标系在视图坐标系的坐标呢?他们之间的唯一关系都是和世界坐标系想关联。世界坐标系其实就是个中间坐标系,方便这两个坐标系之间的相互转换。
其实很简单的,就是将相机和模型看做一个物体,把相机在世界坐标系的位置移动到世界坐标系原点,并且xyz和世界坐标系重叠,物体的世界坐标系就是相对于相机的坐标系。
假设M1 是世界坐标系原点, 相机相对世界坐标系的位置为M0, 那么将相机移动到坐标原点 的矩阵是T1,
M1=T1M0;
物体在世界坐标系S1; 那么物体和相机作为一个整体移动T1, 那么物体的位置就是T1S1。
假设物体的模型坐标系是S0, 物体的模型坐标系到世界坐标系的转换矩阵是T2, 那么S1 = T2S0;
所以物体的模型坐标系相对于相机的坐标系的关系就是M1 = T1T2S0;
相机坐标系到投影坐标系的关系假设是 K1=T3M1;
因此投影坐标系的结果就是K1=T1T2T3S0;
物体只有在投影坐标系设置的远近平面内才是可见的.
gl_Position 接收的坐标是我们用户经过mvp转换后的坐标
透视投影的两种形式
GLK_INLINE GLKMatrix4 GLKMatrix4MakeFrustum(float left, float right,
float bottom, float top,
float nearZ, float farZ)
GLK_INLINE GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, float aspect, float nearZ, float farZ)
正交投影
GLK_INLINE GLKMatrix4 GLKMatrix4MakeOrtho(float left, float right,
float bottom, float top,
float nearZ, float farZ)
视图变换坐标体系的具体讲解
要是看懂上面理论了,我们就是要看如何获取上面的mvp矩阵了以及如何调整相机的位置和投影平面让物体呈现在屏幕上.
上面的demo 投影步骤是,
1.我们把长方体放在世界坐标系的 (0,0,0.25)的位置,然后将其缩放0.5倍, 接着x,y轴旋转45度. 物体出生的时候是自带坐标系的,相当于放在世界坐标系的原点
GLKMatrix4 mat= GLKMatrix4Identity;
mat = GLKMatrix4MakeScale(0.5,0.5,0.5);
mat = GLKMatrix4Rotate(mat, M_PI/180.0*45, 1.0, 1.0, 0.0);
glUniformMatrix4fv(self.modelMat4, 1, GL_FALSE,&mat);
2.我们把相机放在z轴上 方向朝向z轴正方向 ,y轴是法向量
mat= GLKMatrix4Identity;
mat = GLKMatrix4MakeLookAt(0,0,-1,0,0,1,0,1,0);
glUniformMatrix4fv(self.eyeMat4, 1, GL_FALSE,&mat);
相机朝向的是物体的方向.
GLKMatrix4MakeLookAt 九个参数介绍
前三个参数是设置坐标位置
中间三个参数是相机的朝向
后面的三个参数是法向量方向.(可以决定物体的正反)
通过该函数GLKMatrix4MakeLookAt
我们就可以把相机移动到原点位置,并且朝向-z 轴,y轴是法向量方向. 这时候物体的在世界坐标系的位置是 (0,0,-1.25);
3.我们设置投影矩阵的远近裁切面
float aspect = fabs(self.view.bounds.size.width / self.view.bounds.size.height);
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90), aspect, 0.1f, 2.0);
glUniformMatrix4fv(self.projectMat4, 1, GL_FALSE,&projectionMatrix);
我们的裁切面设置的是 0.1 到2.0 系统转换后就是 (-0.1 , -2.0) ,物体的中心在(0,0,-1.25 ) 在 投影区间,因此可以看见正方体了.
当我设置裁切平面是
float aspect = fabs(self.view.bounds.size.width / self.view.bounds.size.height);
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90), aspect, 0.1f, 1.15);
glUniformMatrix4fv(self.projectMat4, 1, GL_FALSE,&projectionMatrix);
结果如下
有一个角被裁切掉了
学习过程遇到的问题
绘制的正方形不完整 出现下列情况
这是因为没有开启深度测试导致的
开启深度测试.该函数在绘制之前开启就可以了.调用一次就可以了
glEnable(GL_DEPTH_TEST);
开启深度测试需要每次绘制的时候清除buffer
调用
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
网友评论