如何在iOS中使用OpenGL ES 3.0和GLKit创建360视频播放器

360视频是同时记录每个方向的视图的视频记录。 在播放期间,观众可以像全景图(Wiki)一样控制观看方向。 现在,它越来越受欢迎,通常您会在Facebook的新闻源,Youtube的360个频道中看到360个视频,甚至在诸如NYTimes和Wallstreet Journal之类的新闻应用程序中也是如此。 在本教程中,您将学习如何使用OpenGL ES 3.0和GLKit从零开始制作360视频播放器。 由于该实现很大程度上依赖于OpenGL,并且OpenGL是跨平台的,因此该应用程序可以移植到其他平台,例如Android,Windows甚至是Web(WebGL)。 在此过程中,您将学到:

  • 如何使用GLKit在iOS中以编程方式绘制几何
  • 如何与OpenGL几何体交互
  • 如何将视频帧用作OpenGL纹理

不禁开始? 让我们开始🙂

注意:此360视频播放器应用程序教程假定您了解OpenGL和GLKit开发的基础知识。 如果您不熟悉OpenGL或GLKit开发,请查看我们的iOS OpenGL教程:OpenGL ES 2.0,以GLKit和OpenGL视频教程开始OpenGL ES 2.0,首先是OpenGL ES和GLKit入门系列。

入门

下载入门项目, 然后在Xcode中打开Go360.xcodeproj 。 在Xcode控制台左侧的导航栏中,您将看到demo.m4v 360视频,最终将其显示在应用程序中。 另外还有4个其他文件夹: MainShaderModelExtension 。 当时无需关心它们。 生成并运行应用程序; 您将看到一个彩色的球体旋转。

目前,您只听到360度视频的音乐。 不用担心 您最终将看到该视频。

如何绘制球体

您只能在OpenGL中绘制三角形。 通过连接Vertex ,您可以绘制三角形。 使用越来越多的三角形,您可以建立一个看起来光滑的球体。

在项目中,使用OpenGL ES 3.0编程指南中的示例代码esShapes.c。 该代码用于以编程方式生成球体的“ 顶点” ,“ 纹理坐标”和“ 索引” 。 esShapes用C编写。要使用它,您需要将其头文件导入Go360-Bridging-Header.h中 。 看一下Model文件夹中的Sphere.swift

 导入GLKit 
 类Sphere { 
// 1
var顶点:UnsafeMutablePointer ?
var texCoords:UnsafeMutablePointer ?
var索引:UnsafeMutablePointer ?
var vertexCount:GLint = 0
var indexCount:GLint = 0
  // 2 
在里面() {
让sliceCount:GLint = 200
令半径:GLfloat = 1.0
vertexCount =(sliceCount / 2 + 1)*(sliceCount + 1)
indexCount = esGenSphere(sliceCount,radius,&vertices,&texCoords和&indices)
}
}

1.在此声明球体的一些变量,包括其顶点数组,纹理坐标数组,索引数组,顶点数和索引数。
2.通过使用esGenSphere函数生成一个球,该球具有200个切片和1.0的半径。 然后,您将获得在OpenGL ES中绘制所需的所有数据(顶点,纹理坐标,索引)。 太棒了,这就是您通过…代码绘制球体的方式!

使球面互动

在下一步中,您将使球体与手指互动。

注意:如果您没有UIGestureRecognizer和OpenGL ES转换的经验,请务必先查看“使用手势进行OpenGL ES转换”以及如何使用OpenGL使用触摸来旋转3D对象。

首先,将以下代码添加到VideoViewController.swift中

 私人无功旋转X:浮点数= 0.0 
私人var rotationY:浮点= 0.0

声明两个新的私有变量以存储X轴和Y轴的旋转度。 接下来,添加以下方法:

 覆盖func touchesMoved(_ touches:Set ,事件:UIEvent?){ 
// 1
let radiansPerPoint:浮点= 0.005
让touch = touches.first!
让location = touch.location(在touch.view中)
让previousLocation = touch.previousLocation(在touch.view中)
var diffX = Float(location.x — previousLocation.x)
var diffY = Float(location.y — previousLocation.y)
// 2
diffX * = -radiansPerPoint
diffY * = -radiansPerPoint
  // 3 
旋转X + = diffY
旋转Y + = diffX
}

让我们逐步介绍一下。

1.获取X轴和Y轴的触摸距离。
2.对于用户拖动的每个像素,您将球体旋转0.005弧度。 同样,为了模拟现实生活中的拖动,以朝同一方向看更多区域,请在此处乘以-1。
3.请记住,x轴在屏幕上是水平的,y轴是垂直的。 因此,当用户从左向右拖动(diffX)时,我们实际上想绕y轴旋转(rotationY),反之亦然。 下图应有助于使这一点变得清楚:

要将手指的旋转应用于球体的旋转,请将以下代码添加到glkViewControllerUpdate(_ controller 🙂

  //更新模型视图投影矩阵 
renderer?.updateModelViewProjectionMatrix(rotationX,-rotationY)

打开Renderer.swift ,将updateModelViewProjectionMatrix(_ rotationX :, _ rotationY 🙂更改为:

  // 1 
func updateModelViewProjectionMatrix(_ rotationX:Float,_ rotationY:Float){
让Aspect = fabs(Float(UIScreen.main.bounds.size.width)/ Float(UIScreen.main.bounds.size.height))
让Z附近:浮点= 0.1
let farZ:浮点数= 100.0
让fieldOfViewInRadians = GLKMathDegreesToRadians(fieldOfView)
让projectionMatrix = GLKMatrix4MakePerspective(fieldOfViewInRadians,Aspect,nearZ,farZ)
var modelViewMatrix = GLKMatrix4Identity
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix,0.0,0.0,-2.0)
// 2
modelViewMatrix = GLKMatrix4RotateX(modelViewMatrix,rotationX)
modelViewMatrix = GLKMatrix4RotateY(modelViewMatrix,rotationY)
modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix,modelViewMatrix)
}

让我们逐步解决它。

1.添加两个新参数以传递手指旋转。
2.将手指在X轴和Y轴上的旋转应用于模型视图矩阵。 生成并运行,在屏幕上拖动时,您可以看到球体正在旋转。

到目前为止很好good

将视频投影到球体

下一步是使用等角投影将视频帧投影到球体模型。

将眼睛移到球体的中心

打开Renderer.swift ,然后将updateModelViewProjectionMatrix(_ rotationX,_ rotationY)修改为:

  func updateModelViewProjectionMatrix(_ rotationX:Float,_ rotationY:Float){ 
让Aspect = fabs(Float(UIScreen.main.bounds.size.width)/ Float(UIScreen.main.bounds.size.height))
让Z附近:浮点= 0.1
let farZ:浮点数= 100.0
让fieldOfViewInRadians = GLKMathDegreesToRadians(fieldOfView)
让projectionMatrix = GLKMatrix4MakePerspective(fieldOfViewInRadians,Aspect,nearZ,farZ)
var modelViewMatrix = GLKMatrix4Identity
//注释掉这一行
// modelViewMatrix = GLKMatrix4Translate(modelViewMatrix,0.0,0.0,-2.0)
modelViewMatrix = GLKMatrix4RotateX(modelViewMatrix,rotationX)
modelViewMatrix = GLKMatrix4RotateY(modelViewMatrix,rotationY)
modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix,modelViewMatrix)
}

这很简单。 只需注释掉翻译代码。 构建并运行该应用程序,您现在位于球体的中心。

从视频播放器获取像素缓冲区

VideoPlayer.swift中 ,添加以下方法:

 私人功能configureOutput(framesPerSecond:Int){ 
// 1
让pixelBuffer = [kCVPixelBufferPixelFormatTypeKey作为字符串:
NSNumber(值:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)]
输出= AVPlayerItemVideoOutput(pixelBufferAttributes:pixelBuffer)
// 2
output.requestNotificationOfMediaDataChange(withAdvanceInterval:1.0 / TimeInterval(framesPerSecond))
avPlayerItem.add(输出)
}

1.将像素缓冲区格式设置为kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange ,它表示双平面分量Y’CbCr 8位4:2:0,视频范围(luma = [16,235] chroma = [16,240])。 您可以在此处找到有关这种转换的更多信息。
2.在这里,您可以设置与GLViewController的时间范围相对应的视频播放器的更新时间。 接下来,向该类添加另一个方法:

  func restorePixelBuffer()-> CVPixelBuffer?  { 
//返回当前视频帧的像素缓冲区。
让pixelBuffer = output.copyPixelBuffer(forItemTime:avPlayerItem.currentTime(),itemTimeForDisplay:nil)
返回pixelBuffer
}

要将视频帧传递给渲染器,请打开VideoViewController.swift ,并将以下代码添加到glkView(_ view :, drawIn rect:)`之前renderer?.render()

  //检索视频像素缓冲区 
保护让pixelBufer = videoPlayer?.retrievePixelBuffer()否则{返回}
//通过使用当前视频像素缓冲区来更新OpenGL ES纹理
渲染器?.updateTexture(pixelBufer)

使用360视频帧作为纹理

将以下变量添加到Renderer.swift中

  var lumaTexture:CVOpenGLESTexture? 
var chromaTexture:CVOpenGLESTexture?
var videoTextureCache:CVOpenGLESTextureCache?

下一步,实现updateTexture(_ pixelBuffer 🙂

  func updateTexture(_ pixelBuffer:CVPixelBuffer){ 
// 1
如果videoTextureCache == nil {
let result = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,nil,context,nil,&videoTextureCache)
如果结果!= kCVReturnSuccess {
打印(“创建CVOpenGLESTextureCacheCreate失败”)
返回
}
}
让textureWidth = GLsizei(CVPixelBufferGetWidth(pixelBuffer))
让textureHeight = GLsizei(CVPixelBufferGetHeight(pixelBuffer))
  var结果:CVReturn 
  // 2 
cleanTextures()
  // 3 
glActiveTexture(GLenum(GL_TEXTURE0))
结果= CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
videoTextureCache !,
pixelBuffer,
零,
GLenum(GL_TEXTURE_2D),
GL_LUMINANCE,
textureWidth,
textureHeight,
GLenum(GL_LUMINANCE),
GLenum(GL_UNSIGNED_BYTE),
0,
&lumaTexture)
如果结果!= kCVReturnSuccess {
打印(“创建CVOpenGLESTextureCacheCreateTextureFromImage失败%d”,结果)
返回
}
  glBindTexture(CVOpenGLESTextureGetTarget(lumaTexture!),CVOpenGLESTextureGetName(lumaTexture!)) 
glTexParameteri(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_MIN_FILTER),GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_MAG_FILTER),GL_LINEAR)
glTexParameterf(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_WRAP_S),GLfloat(GL_CLAMP_TO_EDGE))
glTexParameterf(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_WRAP_T),GLfloat(GL_CLAMP_TO_EDGE))
  // 4 
glActiveTexture(GLenum(GL_TEXTURE1))
结果= CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
videoTextureCache !,
pixelBuffer,
零,
GLenum(GL_TEXTURE_2D),
GL_LUMINANCE_ALPHA,
textureWidth / 2
textureHeight / 2,
GLenum(GL_LUMINANCE_ALPHA),
GLenum(GL_UNSIGNED_BYTE),
1,
&chromaTexture)
如果结果!= kCVReturnSuccess {
打印(“创建CVOpenGLESTextureCacheCreateTextureFromImage失败%d”,结果)
返回
}
  glBindTexture(CVOpenGLESTextureGetTarget(chromaTexture!),CVOpenGLESTextureGetName(chromaTexture!)) 
glTexParameteri(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_MIN_FILTER),GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_MAG_FILTER),GL_LINEAR)
glTexParameterf(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_WRAP_S),GLfloat(GL_CLAMP_TO_EDGE))
glTexParameterf(GLenum(GL_TEXTURE_2D),GLenum(GL_TEXTURE_WRAP_T),GLfloat(GL_CLAMP_TO_EDGE))
}

这是这样做的:

1.创建CVOpenGLESTextureCache(如果尚不存在)。
2.删除当前纹理,然后再更新它们。
3.将420v缓冲区的亮度平面映射为源纹理。
4.将420v缓冲区的色度平面映射为源纹理。 另外,您需要实现cleanTextures()

 私人函数cleanTextures(){ 
如果lumaTexture!= nil {
lumaTexture =零
}
如果chromaTexture!= nil {
chromaTexture =无
}
 如果让videoTextureCache = videoTextureCache { 
CVOpenGLESTextureCacheFlush(videoTextureCache,0)
}
}

片段着色器

注意:如果您不知道着色器的功能,请务必先阅读OpenGL ES Pixel Shaders Tutorial。

由于视频是以YCbCr颜色格式记录的,因此OpenGL ES仅以RGB颜色格式进行渲染。 获得视频帧纹理后,必须将纹理的颜色格式转换为RGB颜色格式。 为此,使用了HDTV的标准ITU-R BT.709的变换矩阵。 片段着色器是着色器阶段,它将把通过栅格化生成的片段处理为一组颜色和一个深度值(OpenGL Wiki)。 在这一部分中,您将使用它来进行颜色格式转换。 在fragmentShader.glsl中 ,将内容更改为:

  #version 300 es 
 精密中型浮子; 
 均匀采样器2D采样器Y; 
均匀采样器2D采样器UV;
 在vec2 textureCoordinate中; 
  vec4fragmentColor; 
  void main(){ 
mediump vec3 yuv;
lowp vec3 rgb;
  // 1 
yuv.x = texture(samplerY,textureCoordinate).r —(16.0 / 255.0);
yuv.yz = texture(samplerUV,textureCoordinate).ra — vec2(128.0 / 255.0,128.0 / 255.0);
rgb = mat3(1.164,1.164,1.164,
0.0,-0.213、2.112,
1.793,-0.533,0.0)* yuv;
  fragmentColor = vec4(rgb,1); 
}

1.在这里,您使用ITU-R BT.709的变换矩阵,将颜色从YCbCr颜色格式转换为RGB颜色格式以进行渲染。 在Shader.swift中再向该类添加两个变量:

  var samplerY = GLuint() 
var samplerUV = GLuint()

init()中 ,在方法底部添加以下代码:

  samplerY = GLuint(glGetUniformLocation(program,“ samplerY”)) 
samplerUV = GLuint(glGetUniformLocation(program,“ samplerUV”))

该代码将新创建的变量绑定到OpenGL ES着色器fragmentShader.glsl 。 在Renderer.swift中 ,在glUniformMatrix4fv(shader.modelViewProjectionMatrix,1,GLboolean(GL_FALSE),modelViewProjectionMatrix.array)之前添加以下代码。

  //在渲染之前设置samplerY和samplerUV的值 
glUniform1i(GLint(shader.samplerY),0)
glUniform1i(GLint(shader.samplerUV),1)

生成并运行。 您将看到一个360度视频播放,并且可以拖动以查看不同的角度。 做得好!

然后去哪儿?

您可以在此处下载完整的示例项目。 如果您想阅读Apple的文档,了解我们在此处介绍的OpenGL ES和GLKit,那么最好的起点是《 OpenGL ES编程指南》。 本文档全面介绍了如何在iOS中使用OpenGL。 在发布在iOS中运行OpenGL的应用程序之前,您应该了解一些可能与您的应用程序相关的详细信息。 恭喜,您现在拥有360视频播放器。 但是,如果您的360视频播放器具有更多功能,那就太好了,例如:

  • 设备移动模式:通过移动手机观看视频的不同区域(Sensor Fusion)
  • VR模式:支持使用VR设备(例如Cardboard)观看
  • 纵向模式:无需旋转手机
  • 金属版

在一个教程中教授所有这些功能太长时间了,因此请留意有关360视频播放器的新教程。 他们将在不久的将来推出。 希望您喜欢本教程。