在基於CubeMap的反射效果一文中,介紹到如何使用CubeMap讓物體反射環境的光,從而製造逼真的3D效果。本文將介紹另外一種反射效果的製做,模擬真實平面鏡的反射。反射效果是實時的,並且能夠反射任何3D模型。下面是一張比較醜的效果圖,例子裏面設置的燈光比較暗,導出gif後效果很差,最好仍是下載例子本身運行看的比較清楚。 html
我將使用高中關於鏡面反射的物理知識來做爲實現鏡面效果的理論基石。下面是2D下的關於鏡面反射的一張圖。bash
本文代碼依然延續學習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繼承於Plane,繪製一個四邊形,目前並無實現任何獨特的代碼,主要用於後期將鏡面相關的邏輯移入其中。如今將它看作一個普通的四邊形便可,在渲染它時,使用了特別編寫的Shader frag_mirror.glsl
。orm
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
,以及頂點着色器須要的projectionMatrix
和cameraMatrix
,用來參與常規頂點着色流程。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的熟練程度。