學習OpenGL ES之教你實現SkyBox

獲取示例代碼


佔位圖之神保佑我吧!

前言

不知道你們是否還記得基於CubeMap的反射效果裏介紹的CubeMap,本文將使用CubeMap來製做3D遊戲中的一個經典技術,天空盒(SkyBox)。老規矩,先來看下效果圖。效果中的地形模型在前面的文章中已經介紹過了。沒看過的能夠點此查看html

原理

SkyBox正如其名,它是基於正方體模型的渲染技術。將6個方向拍下來的天空圖片分別貼在正方體的對應面上,從內部觀察正方體,就能夠獲得一個密閉的天空環境了。下面是一張典型的SkyBox貼圖。bash

能夠把它分紅上下左右先後六張圖,而後建立CubeMap。標準化ModelMatrix矩陣處理後的頂點位置,就能夠用來在CubeMap上進行採樣了。渲染SkyBox時不須要使用光照模型,直接使用採樣CubeMap獲得的顏色便可。因此須要爲天空盒單獨寫一個Fragment Shader。

天空盒幾何體

首先咱們爲天空盒準備基本的Cube幾何體,建立SkyBox類,讓它繼承Cube類,這樣就具有了渲染一個Cube的能力。而後咱們對draw方法進行一些特殊處理。由於咱們須要在內部觀察這個Cube,因此咱們須要讓這個Cube的正面被裁剪,反面顯示出來。glCullFace(GL_FRONT);正是用來作這件事情。渲染完後咱們還須要將裁減狀態迴歸到裁減背面。至於glDepthMask(GL_FALSE);的做用,我會在後面提到。ui

- (void)draw:(GLContext *)glContext {
    glCullFace(GL_FRONT);
    glDepthMask(GL_FALSE);
    [super draw:glContext];
    glDepthMask(GL_TRUE);
    glCullFace(GL_BACK);
}
複製代碼

準備天空CubeMap

接下來咱們準備用於渲染天空盒的CubeMap,用於生成CubeMap的6張圖已經在項目中包含。spa

使用GLKit提供的API能夠很方便的生成CubeMap。

- (void)createCubeTexture {
    NSMutableArray *files = [NSMutableArray new];
    for (int i = 0; i < 6; ++i) {
        NSString *filename = [NSString stringWithFormat:@"cube-%d", i + 1];
        NSString *filePath = [[NSBundle mainBundle] pathForResource:filename ofType:@"tga"];
        [files addObject:filePath];
    }
    NSError *error;
    self.cubeTexture = [GLKTextureLoader cubeMapWithContentsOfFiles:files options:nil error:&error];
}
複製代碼

cubeMapWithContentsOfFiles接受的圖片文件順序爲Right(右), Left(左), Top(上), Bottom(下), Front(前), Back(後),這個順序能夠在API的註釋中找到。生成好cubeTexture後,後面就能夠傳遞給天空盒的Fragment Shader直接使用了。3d

Fragment Shader

我爲SkyBox編寫了新的Fragment Shader , frag_skybox.glsl。內容很是簡單,生成CubeMap的採樣向量,而後返回採樣結果。code

precision highp float;

varying vec2 fragUV;
varying vec3 fragPosition;

uniform samplerCube envMap;
uniform mat4 modelMatrix;
void main(void) {
    vec3 sampleVector = normalize(modelMatrix * vec4(fragPosition, 1.0)).xyz;
    gl_FragColor = textureCube(envMap, sampleVector);
}
複製代碼

正如前言所說,SkyBox的渲染不須要光照模型。在生成SkyBox模型時,將這個Fragment Shader用做SkyBox的渲染。爲了使天空看起來更真實,咱們須要把天空盒縮放到足夠大,離咱們足夠遠。這裏我用了1000的縮放量。同時你還要注意天空盒沒有超出你的視距。你若是單獨給天空盒指定一個足夠視距的投影矩陣那是最好。orm

- (void)createSkyBox {
    NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"vertex" ofType:@".glsl"];
    NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"frag_skybox" ofType:@".glsl"];
    GLContext *skyGlContext = [GLContext contextWithVertexShaderPath:vertexShaderPath fragmentShaderPath:fragmentShaderPath];
    
    self.skyBox = [[SkyBox alloc] initWithGLContext:skyGlContext diffuseMap:nil normalMap:nil];
    self.skyBox.modelMatrix = GLKMatrix4MakeScale(1000, 1000, 1000);
}
複製代碼

和地形一塊兒渲染

咱們但願天空應該老是在全部場景物體以後,因此渲染時,先渲染天空盒,在渲染其餘物體。而且在渲染天空盒時禁用深度寫入 ,就是前面用到的glDepthMask(GL_FALSE);所作的事。這樣渲染完天空盒後,深度緩衝區中全部的深度值依舊時1.0,後續渲染的物體就必然在天空盒的前面了。下面是渲染代碼。cdn

- (void)drawObjects {
    
    [self.skyBox.context active];
    [self.skyBox.context setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
    [self.skyBox.context setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];
    [self.skyBox.context bindCubeTexture:self.cubeTexture to:GL_TEXTURE4 uniformName:@"envMap"];
    [self.skyBox draw: self.skyBox.context];
    
    [self.objects enumerateObjectsUsingBlock:^(GLObject *obj, NSUInteger idx, BOOL *stop) {
        [obj.context active];
        [obj.context setUniform1f:@"elapsedTime" value:(GLfloat)self.elapsedTime];
        [obj.context setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
        [obj.context setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];
        [obj.context setUniform3fv:@"eyePosition" value:self.eyePosition];
        [obj.context setUniform3fv:@"light.direction" value:self.light.direction];
        [obj.context setUniform3fv:@"light.color" value:self.light.color];
        [obj.context setUniform1f:@"light.indensity" value:self.light.indensity];
        [obj.context setUniform1f:@"light.ambientIndensity" value:self.light.ambientIndensity];
        [obj.context setUniform3fv:@"material.diffuseColor" value:self.material.diffuseColor];
        [obj.context setUniform3fv:@"material.ambientColor" value:self.material.ambientColor];
        [obj.context setUniform3fv:@"material.specularColor" value:self.material.specularColor];
        [obj.context setUniform1f:@"material.smoothness" value:self.material.smoothness];
        
        [obj.context setUniform1i:@"useNormalMap" value:self.useNormalMap];
        
        [obj.context bindCubeTexture:self.cubeTexture to:GL_TEXTURE4 uniformName:@"envMap"];
        
        [obj draw:obj.context];
    }];
}
複製代碼

擴展

除了天空盒,還有天空穹,形象來講就是用一個半球蓋在場景上,貼在半球上的貼圖也和天空盒不一樣,大體以下圖所示。和咱們用手機拍的全景圖很像。 htm

總結

本文使用CubeMap實現了經常使用的遊戲技術SkyBox,固然你也可使用其餘方式爲Cube進行貼圖。好比把6張圖放在一塊兒,而後爲Cube設置適當的UV映射。讀者能夠去尋找或者本身製做其餘環境的天空貼圖,譬如夜晚,日落等等,放在項目中觀察效果。blog

相關文章
相關標籤/搜索