OpenGL ES _ 入门_01
OpenGL ES _ 入门_02
OpenGL ES _ 入门_03
OpenGL ES _ 入门_04
OpenGL ES _ 入门_05
OpenGL ES _ 入门练习_01
OpenGL ES _ 入门练习_02
OpenGL ES _ 入门练习_03
OpenGL ES _ 入门练习_04
OpenGL ES _ 入门练习_05
OpenGL ES _ 入门练习_06
OpenGL ES _ 着色器 _ 介绍
OpenGL ES _ 着色器 _ 程序
OpenGL ES _ 着色器 _ 语法
OpenGL ES_着色器_纹理图像
OpenGL ES_着色器_预处理
OpenGL ES_着色器_顶点着色器详解
OpenGL ES_着色器_片断着色器详解
OpenGL ES_着色器_实战01
OpenGL ES_着色器_实战02
OpenGL ES_着色器_实战03
实战2中,详细介绍了多屏显示的原理和实现过程,今天我们继续我们的OpenGL 旅程!技术再牛逼也要学习!
学习目标
打造全景视频,以及VR 眼镜专用的双屏显示框架!
你应该知道的
- 全景显示的原理
通俗的将,好比红色区域就是你的手机屏幕,当你旋转手机的时候,我们球体向相反的方向旋转,这样,你就可以看到球体上的画面了.
准备工作
找一个全景视频,添加到项目中去。
- 实现步骤
1.创建一个球体模型
2.获取视频数据的每一帧数据 转换成RGB 格式,渲染到球体上
3.通过手势的变换,改变球体模型视图矩阵值
4.如果是VR模式,则通过角度传感器获取用户的行为,调整视图矩阵。
实现了那些功能
- 支持普通视频播放
- 支持全景视频播放
- 支持VR 双屏显示模式
- 支持快进,快退
- 支持播放,暂停
- 支持暂停广告功能
核心代码讲解
如果你想要和我一样,能够从零开始把代码敲出来,请确保自己有OpenGL ES 2.0 的基础知识 和 GLSL 的简单基本知识,如果你不具备这方面的知识,没关系,我已经写好了OpenGL学习教程和GLSL教程,请移步开始学习。下面开始我们的内容讲解.
- 视频采集
工程中的两个文件 XJVRPlayerViewController.h和XJVRPlayerController.m主要负责视频数据采集,界面布局在XJVRPlayerViewController中可以更改,主要使用AVFoundation框架这部分内容今天咱不讲解,后面我会写关于视频采集的教程
模型创建
a.全景播放器生成球体的顶点坐标和纹理坐标
b.普通播放器生成长方形的顶点坐标和纹理坐标
两个生成函数在OSShere.h中-
将数据加载到GPU中去
// 加载顶点索引数据 glGenBuffers(1, &_indexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, _numIndices*sizeof(GLushort), _indices, GL_STATIC_DRAW); // 加载顶点坐标 glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, numVertices*strideNum*sizeof(GLfloat), _vertices, GL_STATIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, strideNum, GL_FLOAT, GL_FALSE, strideNum*sizeof(GLfloat), NULL); //加载纹理坐标 glGenBuffers(1, &_textureCoordBuffer); glBindBuffer(GL_ARRAY_BUFFER, _textureCoordBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*2*numVertices, _texCoords, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribTexCoord0); glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(GLfloat), NULL);
以上函数的具体用法,在之前的教程中都讲过,这里就不赘述了。
-
着色器程序
我把着色器分为两种类型,一种是渲染全景视频的,一种是渲染普通视频的,两个没有多大区别,只是在全景着色器中添加了一个视图转换矩阵 (全景着色器:ShadePanorama,普通着色器:ShaderNormal)
下面给出的是全景的着色器的代码:
a.顶点着色器attribute vec4 position; // 顶点坐标属性 attribute vec2 texCoord0;// 纹理坐标 varying vec2 texCoordVarying;// 片段着色器输入变量,负责获取纹理坐标的值 uniform mat4 modelViewProjectionMatrix;//视图变换矩阵 void main (){ texCoordVarying = texCoord0; gl_Position = modelViewProjectionMatrix*position; }
b.片段着色器
precision mediump float;//设置float精度
varying vec2 texCoordVarying;
uniform sampler2D sam2DY; // 纹理采样器Y
uniform sampler2D sam2DUV;// 纹理采样器UV
void main(){
mediump vec3 yuv;
lowp vec3 rob;
// YUV 转RGB 的转换矩阵
mediump mat3 convert = mat3(1.164, 1.164, 1.164,
0.0, -0.213, 2.112,
1.793, -0.533, 0.0);
yuv.x = texture2D(sam2DY,texCoordVarying).r - (16.0/255.0);
yuv.yz = texture2D(sam2DUV,texCoordVarying).rg - vec2(0.5, 0.5);
rgb = convert*yuv;
gl_FragColor = vec4(rgb,1);
}
如果想要了解更多关于着色器语言的知识,请猛戳我
-
创建着色器程序
创建着色器程序的目的是编译刚才我们编写好的着色器源代码,以及将着色器的变量和我们的应用程序代码相关联/** * 创建编译shader程序 * * @param vshName 顶点着色器文件名称 * @param fshName 片段着色器文件名称 */ -(void)createShaderProgramVertexShaderName: (NSString*)vshName FragmentShaderName: (NSString*)fshName{ self.shaderManager = [[OSShaderManager alloc]init]; // 编译连个shader 文件 GLuint vertexShader,fragmentShader; NSURL *vertexShaderPath = [[NSBundle mainBundle]URLForResource:vshName withExtension:@"vsh"]; NSURL *fragmentShaderPath = [[NSBundle mainBundle]URLForResource:fshName withExtension:@"fsh"]; if (![self.shaderManager compileShader:&vertexShader type:GL_VERTEX_SHADER URL:vertexShaderPath]||! [self.shaderManager compileShader:&fragmentShader type:GL_FRAGMENT_SHADER URL:fragmentShaderPath]){ return ; } // 注意获取绑定属性要在连接程序之前 location 随便你写,如果你随便写请记住他,后面要用到 [self.shaderManager bindAttribLocation:GLKVertexAttribPosition andAttribName:"position"]; [self.shaderManager bindAttribLocation:GLKVertexAttribTexCoord0 andAttribName:"texCoord0"]; // 将编译好的两个对象和着色器程序进行连接 if(![self.shaderManager linkProgram]){ [self.shaderManager deleteShader:&vertexShader]; [self.shaderManager deleteShader:&fragmentShader]; } _textureBufferY = [self.shaderManager getUniformLocation:"sam2DY"]; _textureBufferUV = [self.shaderManager getUniformLocation:"sam2DUV"]; _modelViewProjectionMatrixIndex = [self.shaderManager getUniformLocation:"modelViewProjectionMatrix"]; [self.shaderManager detachAndDeleteShader:&vertexShader]; [self.shaderManager detachAndDeleteShader:&fragmentShader]; // 启用着色器 [self.shaderManager useProgram]; }
// 上面的OSShaderManager 这个类,我把着色器程序编译链接的一些方法简单的封装了一下,具体的方向看下面
/**
* 编译shader程序
* @param shader shader名称
* @param type shader 类型
* @param URL shader 本地路径
* @return 是否编译成功
*/
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type URL:(NSURL *)URL;
/**
* 连接程序
* @return 连接程序是否成功
*/
- (BOOL)linkProgram;
/**
* 验证程序是否成功
* @param prog 程序标示
* @return 返回是否成功标志
*/
- (BOOL)validateProgram;
/**
* 绑定着色器的属性
* @param index 属性在shader 程序的索引位置
* @param name 属性名称
*/
- (void)bindAttribLocation:(GLuint)index andAttribName: (GLchar*)name;
/**
* 删除shader
*/
- (void)deleteShader:(GLuint*)shader;
/**
* 获取属性值索引位置
* @param name 属性名称
* @return 返回索引位置
*/
- (GLint)getUniformLocation:(const GLchar*) name;
/**
* 释放, 删除shader
* @param shader 着色器名称
*/
-(void)detachAndDeleteShader:(GLuint*)shader;
/**
* 使用程序
*/
-(void)useProgram;
方法的具体实现请阅读工程文件
-
纹理采样器指向
glUniform1i(_textureBufferY, 0); // 0 代表GL_TEXTURE0 glUniform1i(_textureBufferUV, 1); // 1 代表GL_TEXTURE1
在这里我有必要提醒你,这两个方法,一定要放在着色器程序链接成功之后,不然你调用这个两个方法,没有效果。
-
如何将YUV 数据分离,并且加载到两个着色器中去, 这里我们又要用到之前我们使用过的框架了CoreVideo. 干涉么的呢,专门处理我们的像素数据的。我们从视频采集到的视频是CVPixelBufferRef 类型的
下面我们先看一下我们像素数据的格式{type = immutable dict, count = 4, entries => 1 : {contents = "PixelFormatType"} = {type = mutable-small, count = 1, values = ( 0 : {value = +875704438, type = kCFNumberSInt64Type} )} 2 : {contents = "Height"} = {value = +1024, type = kCFNumberSInt32Type} 5 : {contents = "PropagatedAttachments"} = {type = mutable dict, count = 4, entries => 0 : {contents = "CVImageBufferYCbCrMatrix"} = {contents = "ITU_R_601_4"} 1 : {contents = "CVImageBufferTransferFunction"} = {contents = "ITU_R_709_2"} 2 : {contents = "ColorInfoGuessedBy"} = {contents = "VideoToolbox"} 5 : {contents = "CVImageBufferColorPrimaries"} = {contents = "SMPTE_C"} } 6 : {contents = "Width"} = {value = +2048, type = kCFNumberSInt32Type} } propagatedAttachments= {type = mutable dict, count = 10, entries => 0 : {contents = "ColorInfoGuessedBy"} = {contents = "VideoToolbox"} 1 : {contents = "CVImageBufferYCbCrMatrix"} = {contents = "ITU_R_601_4"} 2 : {contents = "CVFieldCount"} = {value = +1, type = kCFNumberSInt32Type} 3 : {contents = "CVPixelAspectRatio"} = {type = immutable dict, count = 2, entries => 1 : {contents = "HorizontalSpacing"} = {value = +1, type = kCFNumberSInt32Type} 2 : {contents = "VerticalSpacing"} = {value = +1, type = kCFNumberSInt32Type} } 4 : {contents = "QTMovieTime"} = {type = immutable dict, count = 2, entries => 0 : {contents = "TimeValue"} = {value = +0, type = kCFNumberSInt64Type} 1 : {contents = "TimeScale"} = {value = +30000, type = kCFNumberSInt32Type} } 5 : {contents = "CVImageBufferColorPrimaries"} = {contents = "SMPTE_C"} 8 : {contents = "CVImageBufferTransferFunction"} = {contents = "ITU_R_709_2"} 9 : {contents = "CVImageBufferChromaSubsampling"} = {contents = "TopLeft"} 10 : {contents = "CVImageBufferChromaLocationBottomField"} = {contents = "4:2:0"} 12 : {contents = "CVImageBufferChromaLocationTopField"} = {contents = "4:2:0"} } nonPropagatedAttachments= {type = mutable dict, count = 0, entries => } >
我们从上面的日志输出找到了下面的东西
我们能得到的信息是:
像素格式: 420v
数据通道: 2 个
通道1: width=2048 height=1024
通道2: width=1024 height=512
从上面信息可以得出我们数据的排列方式为YY....YY....UV.....UV,
2048\1024 个Y 数据,1024\512 从 bytesPerRow 可以看出每个Y、U、V 各占一个字节.
接下来就是如何将数据加载到我们的纹理缓冲区去了
CVReturn CVOpenGLESTextureCacheCreateTextureFromImage(
CFAllocatorRef CV_NULLABLE allocator,
CVOpenGLESTextureCacheRef CV_NONNULL textureCache,
CVImageBufferRef CV_NONNULL sourceImage,
CFDictionaryRef CV_NULLABLE textureAttributes,
GLenum target,
GLint internalFormat,
GLsizei width,
GLsizei height,
GLenum format,
GLenum type,
size_t planeIndex,
CV_RETURNS_RETAINED_PARAMETER CVOpenGLESTextureRef CV_NULLABLE * CV_NONNULL textureOut )
这个函数作用是: 通过CVImageBufferRef 创建一个纹理对象
allocator : 写默认值就可以了 kCFAllocatorDefault
textureCache:我们需要手动创建一个纹理缓冲对象,
sourceImage:传我们的CVImageBufferRef 数据
textureAttributes:纹理属性,可以为NULL
target:纹理的类型(GL_TEXTURE_2D 和GL_RENDERBUFFER)
internalFormat:数据格式,就是这个数据步伐的意思
width:纹理的高度
height : 纹理的长度
format: 像素数据的格式
type: 数据类型
planeIndex: 通道索引
接下来看我们的代码:
// 启用纹理缓冲区0
glActiveTexture(GL_TEXTURE0);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RED_EXT,
width,
height,
GL_RED_EXT,
GL_UNSIGNED_BYTE,
0,
&_lumaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// UV-plane.
// 启用纹理缓冲区1
glActiveTexture(GL_TEXTURE1);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RG_EXT,
width /2,
height /2,
GL_RG_EXT,
GL_UNSIGNED_BYTE,
1,
&_chromaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
GL_RED_EXT 代表 1位数据 GL_RG_EXT 代表2位数据 。UV 就是两位数据 所以我们选择GL_RG_EXT。
刚才说了,参数中需要一个纹理缓冲TextureCacha,接下来我们就自己创建一个.
CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, self.eagContext, NULL, &_videoTextureCache);
以上基本的工作都做完了,接下来,我们就只剩下显示了
-
渲染绘制
// 清除颜色缓冲区 glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); if (_isVR){ // 渲染双屏 glViewport(0, 0, self.view.bounds.size.width, self.view.bounds.size.height*2); glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, 0); glViewport(self.view.bounds.size.width, 0, self.view.bounds.size.width, self.view.bounds.size.height*2); glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, 0); }else{ // 渲染单屏 glViewport(0, 0, self.view.bounds.size.width*2, self.view.bounds.size.height*2); glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, 0); }
到这里,视频已经可以显示了。
-
视图矩阵初始化
-(void)initModelViewProjectMatrix{ // 创建投影矩阵 float aspect = fabs(self.view.bounds.size.width / self.view.bounds.size.height); _projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(OSVIEW_CORNER), aspect, 0.1f, 400.0f); _projectionMatrix = GLKMatrix4Rotate(_projectionMatrix, ES_PI, 1.0f, 0.0f, 0.0f); // 创建模型矩阵 _modelViewMatrix = GLKMatrix4Identity; float scale = OSSphereScale; _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix, scale, scale, scale); // 最终传入到GLSL中去的矩阵 _modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, _modelViewMatrix); glUniformMatrix4fv(_modelViewProjectionMatrixIndex, 1, GL_FALSE, _modelViewProjectionMatrix.m); }
-
全景单屏模式
手势操纵矩阵- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if(self.isVR || self.vedioType == OSNormal ) return; UITouch *touch = [touches anyObject]; float distX = [touch locationInView:touch.view].x - [touch previousLocationInView:touch.view].x; float distY = [touch locationInView:touch.view].y - [touch previousLocationInView:touch.view].y; distX *= -0.005; distY *= -0.005; self.fingerRotationX += distY * OSVIEW_CORNER / 100; self.fingerRotationY -= distX * OSVIEW_CORNER / 100; _modelViewMatrix = GLKMatrix4Identity; float scale = OSSphereScale; _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix, scale, scale, scale); _modelViewMatrix = GLKMatrix4RotateX(_modelViewMatrix, self.fingerRotationX); _modelViewMatrix = GLKMatrix4RotateY(_modelViewMatrix, self.fingerRotationY); _modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, _modelViewMatrix); glUniformMatrix4fv(_modelViewProjectionMatrixIndex, 1, GL_FALSE, _modelViewProjectionMatrix.m); } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (self.isVR || self.vedioType == OSNormal) return; for (UITouch *touch in touches) { [self.currentTouches removeObject:touch]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { [self.currentTouches removeObject:touch]; } }
-
全景 VR模式
使用角度传感器-(void)startMotionManager{ self.motionManager = [[CMMotionManager alloc]init]; self.motionManager.deviceMotionUpdateInterval = 1.0 / 60.0; self.motionManager.gyroUpdateInterval = 1.0f / 60; self.motionManager.showsDeviceMovementDisplay = YES; [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryCorrectedZVertical]; self.referenceAttitude = nil; [self.motionManager startGyroUpdatesToQueue: [[NSOperationQueue alloc]init] withHandler:^(CMGyroData * _Nullable gyroData, NSError * _Nullable error) { if(self.isVR) { [self calculateModelViewProjectMatrixWithDeviceMotion:self.motionManager.deviceMotion]; } }]; self.referenceAttitude = self.motionManager.deviceMotion.attitude; } -(void)calculateModelViewProjectMatrixWithDeviceMotion:(CMDeviceMotion*)deviceMotion{ _modelViewMatrix = GLKMatrix4Identity; float scale = OSSphereScale; _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix, scale, scale, scale); if (deviceMotion != nil) { CMAttitude *attitude = deviceMotion.attitude; if (self.referenceAttitude != nil) { [attitude multiplyByInverseOfAttitude:self.referenceAttitude]; } else { self.referenceAttitude = deviceMotion.attitude; } float cRoll = attitude.roll; float cPitch = attitude.pitch; _modelViewMatrix = GLKMatrix4RotateX(_modelViewMatrix, -cRoll); _modelViewMatrix = GLKMatrix4RotateY(_modelViewMatrix, -cPitch*3); _modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, _modelViewMatrix); // 下边这个方法必须在主线程中完成. dispatch_async(dispatch_get_main_queue(), ^{ glUniformMatrix4fv(_modelViewProjectionMatrixIndex, 1, GL_FALSE, _modelViewProjectionMatrix.m); }); }}
操作矩阵这里,暂时不想讲,后面我会专门来讲矩阵变换和角度传感器的使用,因为这两个东西在游戏和VR,还是AR的世界,都太重要了。今天先说的这里,给几张展示图欣赏一下。
需要代码在这里和这里
全景播放器-实现方案2
使用SceneKit 也可以实现全景播放器,需要了解的朋友请查看这里