本文章主要介绍关于VR全景图片浏览的实现,Github VR全景图片(喜欢的朋友点一下star吧)主要是基于OpenGL ES 2.0 / Swift3.0实现的代码,之后会放入OC版。(接下来会发布关于VR全景视频播放器文章,现在主要是在封装播放器)
实现思路:
- 创建一个球体模型
- 获取图片的纹理数据,通过着色器渲染到球体上
- 通过手势的变换,改变球体模型视图矩阵值
- VR模式,则通过拖陀螺仪获取用户的行为,调整视图矩阵。
一、文件介绍。
- Sphere.h: 引入C语言头文件 #include
。 - 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 * 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)
}
复制代码
- 初始化陀螺仪。
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的知识,可以去看落影loyinglin和酷走天涯文章。