學習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的熟練程度。

相關文章
相關標籤/搜索