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

本系列所有文章目录

获取示例代码


我是闷骚的占位图

前言

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

原理

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


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

写代码之前

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

封装摄像机

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

@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表示在法线上移动的位移。

渲染镜像摄像机内容

想要把镜像摄像机的内容渲染到镜面的平面上,我们需要建立一个新的Framebuffer,并且绑定一个纹理到它的颜色附件中。这样就可以把镜像摄像机的内容渲染到纹理了。如果你看过渲染到纹理这一篇文章,下面的代码你就会感觉很熟悉。

- (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裁剪平面相关的代码,我们后面再介绍。

- (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.glsl

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,用来参与常规顶点着色流程。

- (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相乘。

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的熟练程度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容