学习OpenGL ES之教你造一面镜子

获取示例代码


我是闷骚的占位图

前言

基于CubeMap的反射效果一文中,介绍到如何使用CubeMap让物体反射环境的光,从而制造逼真的3D效果。本文将介绍另外一种反射效果的制做,模拟真实平面镜的反射。反射效果是实时的,并且能够反射任何3D模型。下面是一张比较丑的效果图,例子里面设置的灯光比较暗,导出gif后效果很差,最好仍是下载例子本身运行看的比较清楚。 html

原理

我将使用高中关于镜面反射的物理知识来做为实现镜面效果的理论基石。下面是2D下的关于镜面反射的一张图。bash

镜子上显示的图像,能够看作镜像过去的另外一我的所看到的的情景。使用OpenGL的术语来讲就是把摄像机以镜子所在的平面作镜像,获得的镜像摄像机所观察到的世界,就是镜面上应该显示的内容。基本原理虽然很简单,但实现过程当中也会遇到诸多问题。好比如何把镜像摄像机的渲染结果贴到镜面上,镜像摄像机被其余物体遮挡该如何处理。

写代码以前

本文代码依然延续学习OpenGL ES的项目代码,任何以前已经介绍的代码将再也不介绍。因此你真的想看懂本文的话,至少对OpenGL和本系列Demo项目有基本的了解。学习

封装摄像机

以前的代码中一直使用GLK的方法生成观察矩阵,此次我对摄像机进行了封装,主要是为了更方便的进行镜像。摄像机的类是Camera。主要功能是生成摄像机和镜像摄像机。摄像机使用向前的向量forward,向上的向量up和位置position管理自身信息。镜像时将这三个变量分别求解出镜像值便可。求解向量的镜像主要使用了向量的反射公式,具体你们能够看代码。这里就不详细解释了。ui

@interface Camera : NSObject
@property (assign, nonatomic) GLKVector3 forward;
@property (assign, nonatomic) GLKVector3 up;
@property (assign, nonatomic) GLKVector3 position;

- (void)setupCameraWithEye:(GLKVector3)eye lookAt:(GLKVector3)lookAt up:(GLKVector3)up;
- (void)mirrorTo:(Camera *)targetCamera plane:(GLKVector4)plane;
- (GLKMatrix4)cameraMatrix;
@end
复制代码

在镜像方法- (void)mirrorTo:(Camera *)targetCamera plane:(GLKVector4)plane;中,使用GLKVector4表示平面,x,y,z表示法线,w表示在法线上移动的位移。atom

渲染镜像摄像机内容

想要把镜像摄像机的内容渲染到镜面的平面上,咱们须要创建一个新的Framebuffer,而且绑定一个纹理到它的颜色附件中。这样就能够把镜像摄像机的内容渲染到纹理了。若是你看过渲染到纹理这一篇文章,下面的代码你就会感受很熟悉。spa

- (void)createTextureFramebuffer:(CGSize)framebufferSize {
    
    glGenFramebuffers(1, &mirrorFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, mirrorFramebuffer);
    
    // 生成颜色缓冲区的纹理对象并绑定到framebuffer上
    glGenTextures(1, &mirrorTexture);
    glBindTexture(GL_TEXTURE_2D, mirrorTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, framebufferSize.width, framebufferSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mirrorTexture, 0);
    
    // 下面这段代码不使用纹理做为深度缓冲区。
    GLuint depthBufferID;
    glGenRenderbuffers(1, &depthBufferID);
    glBindRenderbuffer(GL_RENDERBUFFER, depthBufferID);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, framebufferSize.width, framebufferSize.height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBufferID);

    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        // framebuffer生成失败
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
复制代码

接着咱们在渲染主场景以前,把场景渲染到镜像专用的Framebuffer中。为了渲染镜像中观察者看到的景象,我将当前的观察矩阵设置为镜像摄像机mirrorCamera的观察矩阵,而且设置了新的Viewport匹配当前的Framebuffer大小,同时也设置了新的投影矩阵mirrorProjectionMatrix匹配新的Framebuffer的比例。至于GL_CLIP_DISTANCE0_APPLE裁剪平面相关的代码,咱们后面再介绍。code

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    self.projectionMatrix = self.mirrorProjectionMatrix;
    self.cameraMatrix = [self.mirrorCamera cameraMatrix];
    glBindFramebuffer(GL_FRAMEBUFFER, mirrorFramebuffer);
    glViewport(0, 0, 1024, 1024);
    glClearColor(0.7, 0.7, 0.9, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    self.clipplaneEnable = YES;
    self.clipplane = GLKVector4Make(0, 0, 1, 0);
    glEnable(GL_CLIP_DISTANCE0_APPLE);
    [self drawObjects];
    
    glDisable(GL_CLIP_DISTANCE0_APPLE);
    self.clipplaneEnable = NO;
    self.projectionMatrix = self.viewProjectionMatrix;
    self.cameraMatrix = [self.mainCamera cameraMatrix];
    [view bindDrawable];
    glClearColor(0.7, 0.7, 0.7, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    [self drawObjects];
    [self drawMirror];
}
复制代码

Mirror模型的渲染

Mirror继承于Plane,绘制一个四边形,目前并无实现任何独特的代码,主要用于后期将镜面相关的逻辑移入其中。如今将它看作一个普通的四边形便可,在渲染它时,使用了特别编写的Shader frag_mirror.glslorm

precision highp float;

varying vec2 fragUV;
varying vec3 fragPosition;

uniform mat4 mirrorPVMatrix;
uniform mat4 modelMatrix;
uniform sampler2D diffuseMap;

void main(void) {
    vec4 positionInWordCoord = mirrorPVMatrix * modelMatrix * vec4(fragPosition, 1.0);
    positionInWordCoord = positionInWordCoord / positionInWordCoord.w;
    positionInWordCoord = (positionInWordCoord + 1.0) * 0.5;
    gl_FragColor = texture2D(diffuseMap, positionInWordCoord.st);
}
复制代码

使用顶点位置最终投影到屏幕的坐标,计算UV,从镜像摄像机渲染出的纹理上采样。这个手法咱们在投影纹理中有介绍到,至关于把镜像摄像机看到的内容按照镜像摄像机的VP矩阵投影到镜面的平面上。 咱们在主场景渲染时才渲染镜面模型。而且开启了GL_CULL_FACE,由于让反面在渲染时使用另外一个法线进行镜像计算比较繁琐并且没有必要。在渲染过程当中传入镜像摄像机和镜像投影的矩阵相乘结果mirrorPVMatrix,以及顶点着色器须要的projectionMatrixcameraMatrix,用来参与常规顶点着色流程。cdn

- (void)drawMirror {
    glEnable(GL_CULL_FACE);
    [self.mirror.context active];
    [self.mirror.context setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
    [self.mirror.context setUniformMatrix4fv:@"mirrorPVMatrix" value: GLKMatrix4Multiply(self.mirrorProjectionMatrix, [self.mirrorCamera cameraMatrix])];
    [self.mirror.context setUniformMatrix4fv:@"cameraMatrix" value: self.cameraMatrix];
    [self.mirror draw:self.mirror.context];
    glDisable(GL_CULL_FACE);
}
复制代码

裁剪平面

在前面咱们提到过一个问题,若是镜像摄像机被遮挡应该怎么办。glEnable(GL_CLIP_DISTANCE0_APPLE);就是解决方案。裁剪平面在OpenGL中是直接支持的,但在OpenGL ES中须要使用苹果的扩展,因此GL_CLIP_DISTANCE0_APPLE后面有个APPLE。咱们将平面以Vector4的表达方式传入Vertex Shader中,最终系统会将观察点到平面之间的点都忽略掉。这里我写死了0,0,1,0这个平面,固然你也能够动态获取mirror模型的平面法线,使用normalMatrix和0,0,1,0相乘。htm

self.clipplaneEnable = YES;
self.clipplane = GLKVector4Make(0, 0, 1, 0);
glEnable(GL_CLIP_DISTANCE0_APPLE);
复制代码

在Vertex Shader中须要添加以下代码。

if (clipplaneEnabled) {
    gl_ClipDistance[0] = dot((modelMatrix * position).xyz, clipplane.xyz) + clipplane.w;
}
复制代码

总结

本文使用了渲染到纹理,纹理投影,裁剪平面等技术实现了镜面效果。同时也涉及到了很多向量的计算,算是比较考验对OpenGL ES的熟练度,读者能够看完例子以后本身尝试去实现这个效果,了解一下本身对OpenGL ES的熟练程度。

相关文章
相关标签/搜索