本文章主要介绍关于VR全景图片浏览的实现,主要是基于OpenGL ES 2.0 / Swift3.0实现的代码,之后会放入OC版。(接下来会发布关于VR全景视频播放器文章,现在主要是在封装播放器)
实现思路:
- 创建一个球体模型
- 获取图片的纹理数据,通过着色器渲染到球体上
- 通过手势的变换,改变球体模型视图矩阵值
- VR模式,则通过拖陀螺仪获取用户的行为,调整视图矩阵。
一、文件介绍。
- Sphere.h: 引入C语言头文件 #include <stdio.h>。
- Sphere.c: 生成球体坐标的C语言方法。
- Bridging-Header.h: 桥接文件。
- MMPhotoView.swift: 继承于GLKView,用来渲染球体的。 注: 桥接文件路径。
二、VR全景图片浏览实现
- 属性一览。
/// 传过来的VR全景图片路径 public var photoURL: String? { didSet { guard let filePath = photoURL else { return } /// 将图片转为纹理信息 photoToSwitchTexture(filePath) } } /// 相机广角角度 fileprivate var overture: CGFloat = 0 /// 索引数 fileprivate var numIndices: Int = 0 /// 顶点索引缓存指针 fileprivate var vertexIndicesBufferID: GLuint = 0 /// 顶点缓存指针 fileprivate var vertexBufferID: GLuint = 0 /// 纹理缓存指针 fileprivate var vertexTexCoordID: GLuint = 0 /// 着色器 fileprivate var effect: GLKBaseEffect? /// 图片纹理信息 fileprivate var textureInfo: GLKTextureInfo? /// 模型坐标系 fileprivate var modelViewMatrix: GLKMatrix4 = GLKMatrix4Identity /// 拖拽手势 fileprivate var panX: CGFloat = 0 fileprivate var panY: CGFloat = 0 let sphereSliceNum = 200 /// 每一帧片数 let sphereRadius = 1.0 /// 球体半径复制代码
- 初始化GLKView。
fileprivate func setupGLKView() { /// 设置颜色格式和深度格式 drawableColorFormat = GLKViewDrawableColorFormat.RGBA8888 drawableDepthFormat = GLKViewDrawableDepthFormat.format24 self.delegate = self context = EAGLContext.init(api: EAGLRenderingAPI.openGLES2) //将此“EAGLContext”实例设置为OpenGL的“当前激活”的“Context” EAGLContext.setCurrent(context) /// 注意: 激活深度检测,设置深度检测一定要放在设置上一句的下面, 要不然context还没有激活 glEnable(GLenum(GL_DEPTH_TEST)) }复制代码
- 运行Sphere.c C语言文件获取球体索引坐标数据, 然后将索引坐标加载到GPU 中去。
fileprivate func setupBuffer() { var vertices: UnsafeMutablePointer? // 顶点 var texCoord: UnsafeMutablePointer ? // 纹理 var indices: UnsafeMutablePointer ? // 索引 var numVertices: Int32? = 0 /// 编译C文件 获取顶点/纹理/索引 numIndices = Int(GLuint(initSphere(Int32(sphereSliceNum), Float(sphereRadius), &vertices, &texCoord, &indices, &numVertices!))) /// 加载顶点索引数据 glGenBuffers(1, &vertexIndicesBufferID) // 申请内存 glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), vertexIndicesBufferID) // 将命名的缓冲对象绑定到指定的类型上去 glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), numIndices * MemoryLayout .size, indices, GLenum(GL_STATIC_DRAW)) /// 加载顶点坐标数据 glGenBuffers(1, &vertexBufferID) glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBufferID) glBufferData(GLenum(GL_ARRAY_BUFFER), Int(numVertices!) * 3 * MemoryLayout .size, vertices, GLenum(GL_STATIC_DRAW)) /// 激活顶点位置属性 glEnableVertexAttribArray(GLuint(GLKVertexAttrib.position.rawValue)) glVertexAttribPointer(GLuint(GLKVertexAttrib.position.rawValue), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout .size * 3), nil) // 纹理 glGenBuffers(1, &vertexTexCoordID) glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexTexCoordID) glBufferData(GLenum(GL_ARRAY_BUFFER), Int(numVertices!) * 2 * MemoryLayout .size, texCoord, GLenum(GL_DYNAMIC_DRAW)) glEnableVertexAttribArray(GLuint(GLint(GLKVertexAttrib.texCoord0.rawValue))) glVertexAttribPointer(GLuint(GLint(GLKVertexAttrib.texCoord0.rawValue)), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayout .size * 2), nil) }复制代码
- 初始化陀螺仪。
fileprivate func startDeviceMotion() { /**设置初始坐标系, 并开始监控 CMAttitudeReferenceFrameXArbitraryCorrectedZVertical: 描述的参考系默认设备平放(垂直于Z轴),在X轴上取任意值。实际上当你开始刚开始对设备进行motion更新的时候X轴就被固定了。不过这里还使用了罗盘来对陀螺仪的测量数据做了误差修正 使用pull形式获取数据 */ motionManager.startDeviceMotionUpdates(using: CMAttitudeReferenceFrame.xArbitraryCorrectedZVertical) modelViewMatrix = GLKMatrix4Identity }复制代码
- 添加定时器CADisplayLink。主要的目的是让它执行GLKView中的display()方法,让屏幕刷新率相同的频率相同。
fileprivate func addDisplayLink() { let displayLink = CADisplayLink.init(target: self, selector: #selector(displayAction)) displayLink.add(to: RunLoop.current, forMode: RunLoopMode.commonModes) } @objc fileprivate func displayAction() { display() // 执行display() 不断刷新屏幕。 }复制代码
- 在GLKViewDelegate代理方法内进行绘制。
// MARK: - GLKViewDelegate func glkView(_ view: GLKView, drawIn rect: CGRect) { // 清除缓冲区的内容 glClearColor(0, 0, 0, 1) // 清除颜色缓冲区与深度缓冲区内容 glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)) // 渲染着色器 effect?.prepareToDraw() glDrawElements(GLenum(GL_TRIANGLES), GLsizei(numIndices), GLenum(GL_UNSIGNED_SHORT), nil) update() } // MARK: - 生命周期方法 fileprivate func update() { let aspect: Float = fabs(Float(bounds.size.width) / Float(bounds.size.height)) var projectionMatrix: GLKMatrix4 = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(85.0), aspect, 0.1, 400.0) projectionMatrix = GLKMatrix4Scale(projectionMatrix, -1.0, 1.0, 1.0) if motionManager.deviceMotion != nil { let w: Float = Float(motionManager.deviceMotion!.attitude.quaternion.w) let x: Float = Float(motionManager.deviceMotion!.attitude.quaternion.x) let y: Float = Float(motionManager.deviceMotion!.attitude.quaternion.y) let z: Float = Float(motionManager.deviceMotion!.attitude.quaternion.z) projectionMatrix = GLKMatrix4RotateX(projectionMatrix, -(Float)(0.005 * panY)) let quaternion: GLKQuaternion = GLKQuaternionMake(-x, y, z, w) let rotation: GLKMatrix4 = GLKMatrix4MakeWithQuaternion(quaternion) projectionMatrix = GLKMatrix4Multiply(projectionMatrix, rotation) /// 为了保证在水平放置手机的时候, 是从下往上看, 因此首先坐标系沿着x轴旋转90度 projectionMatrix = GLKMatrix4RotateX(projectionMatrix, -Float(M_PI_2)) effect?.transform.projectionMatrix = projectionMatrix var modelViewMatrix: GLKMatrix4 = GLKMatrix4Identity modelViewMatrix = GLKMatrix4RotateY(modelViewMatrix, Float(0.005 * panX)) effect?.transform.modelviewMatrix = modelViewMatrix } }复制代码
- 最后一步获取VR全景图片,将图片纹理信息添加到着色器中。
/// 传过来的VR全景图片路径 public var photoURL: String? { didSet { guard let filePath = photoURL else { return } /// 将图片转为纹理信息 runningTexture(filePath) } }复制代码
fileprivate func runningTexture(_ filePath: String) { // 获取图片纹理信息 textureInfo = try? GLKTextureLoader.texture(withContentsOfFile: filePath, options: [GLKTextureLoaderOriginBottomLeft: NSNumber(booleanLiteral: true)]) effect = GLKBaseEffect() effect?.texture2d0.enabled = GLboolean(GL_TRUE) effect?.texture2d0.name = textureInfo!.name }复制代码
- 示例演示。(注意:要真机测试才可以)
注: 如果你喜欢OpenGL ES,想学习OpenGL ES的知识,可以去看和文章。