VR全景图片浏览实现

本文章主要介绍关于VR全景图片浏览的实现,Github VR全景图片(喜欢的朋友点一下star吧)主要是基于OpenGL ES 2.0 / Swift3.0实现的代码,之后会放入OC版。(接下来会发布关于VR全景视频播放器文章,现在主要是在封装播放器)

实现思路:

  1. 创建一个球体模型
  2. 获取图片的纹理数据,通过着色器渲染到球体上
  3. 通过手势的变换,改变球体模型视图矩阵值
  4. VR模式,则通过拖陀螺仪获取用户的行为,调整视图矩阵。

一、文件介绍。

  1. Sphere.h: 引入C语言头文件 #include
  2. Sphere.c: 生成球体坐标的C语言方法。
  3. Bridging-Header.h: 桥接文件。
  4. MMPhotoView.swift: 继承于GLKView,用来渲染球体的。 注: 桥接文件路径。

二、VR全景图片浏览实现

  1. 属性一览。
/// 传过来的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   /// 球体半径
复制代码
  1. 初始化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))
    }
复制代码
  1. 运行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 * MemoryLayoutfloat>.size, vertices, GLenum(GL_STATIC_DRAW))
        
        /// 激活顶点位置属性
        glEnableVertexAttribArray(GLuint(GLKVertexAttrib.position.rawValue))
        glVertexAttribPointer(GLuint(GLKVertexAttrib.position.rawValue), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), GLsizei(MemoryLayoutfloat>.size * 3), nil)
        
        // 纹理
        glGenBuffers(1, &vertexTexCoordID)
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexTexCoordID)
        glBufferData(GLenum(GL_ARRAY_BUFFER), Int(numVertices!) * 2 * MemoryLayoutfloat>.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(MemoryLayoutfloat>.size * 2), nil)
    }
复制代码
  1. 初始化陀螺仪。
fileprivate func startDeviceMotion() {
        
        /**设置初始坐标系, 并开始监控
         CMAttitudeReferenceFrameXArbitraryCorrectedZVertical: 描述的参考系默认设备平放(垂直于Z轴),在X轴上取任意值。实际上当你开始刚开始对设备进行motion更新的时候X轴就被固定了。不过这里还使用了罗盘来对陀螺仪的测量数据做了误差修正
         使用pull形式获取数据
         */
        motionManager.startDeviceMotionUpdates(using: CMAttitudeReferenceFrame.xArbitraryCorrectedZVertical)
        modelViewMatrix = GLKMatrix4Identity
    }
复制代码
  1. 添加定时器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() 不断刷新屏幕。
    }
复制代码
  1. 在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
        }
    }
复制代码
  1. 最后一步获取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
    }
复制代码
  1. 示例演示。(注意:要真机测试才可以)

注: 如果你喜欢OpenGL ES,想学习OpenGL ES的知识,可以去看落影loyinglin和酷走天涯文章。

你可能感兴趣的:(VR全景图片浏览实现)