學(xué)習(xí)OpenGL ES之繪制地形

本系列所有文章目錄

獲取示例代碼


本文將介紹如何使用一張灰度地形圖片生成下面的地形模型。


本文用到的灰度地形圖片如下

什么是地形模型

地形模型一般是由NxN的網(wǎng)格構(gòu)成刃榨,網(wǎng)格的點在y軸上的坐標(biāo)由灰度地形圖上相應(yīng)的顏色決定弹砚。顏色越亮,高度越高枢希。顏色每個通道的取值范圍可以是0~ 255桌吃,通過公式轉(zhuǎn)換,可以很容易的控制生成模型的高度晴玖。

生成網(wǎng)格頂點數(shù)據(jù)

上篇文章中读存,我們使用三角帶生成圓柱體的中間部分∥鳎現(xiàn)在我們要用多個三角帶來生成地形呕屎。

如何生成單個三角帶我就不贅述了,上篇文章已經(jīng)介紹了敬察。下面主要介紹如何計算每個頂點的位置秀睛,法線和UV。

計算頂點位置

計算頂點位置之前莲祸,我們先要獲取到灰度地形圖的像素數(shù)據(jù)蹂安。因為我們需要知道指定點的像素顏色。

- (GLubyte *)dataFromImage:(UIImage *)img {
    CGImageRef imageRef = [img CGImage];
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    GLubyte *textureData = (GLubyte *)malloc(width * height * 4);
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    
    CGContextRef context = CGBitmapContextCreate(textureData, width, height,
                                                 bitsPerComponent, bytesPerRow, colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);
    return textureData;
}

上面的代碼將像素格式不確定的圖片轉(zhuǎn)換成4通道RGBA格式的圖片數(shù)據(jù)锐帜。textureData的內(nèi)存布局是R田盈,G,B缴阎,A允瞧,R,G,B述暂,A痹升,R,G畦韭,B疼蛾,A,...不停重復(fù)艺配。位置(x,y)的像素數(shù)據(jù)在偏移量y * 圖片寬度 * 4 + x * 4處察郁。獲取頂點位置的代碼如下。

- (GLKVector3)vertexPosition:(int)col row:(int)row buffer:(unsigned char *)buffer bytesPerRow:(size_t)bytesPerRow bytesPerPixel:(size_t)bytesPerPixel {
    long long offset = (int)(row / self.terrainSize.height * self.heightMap.size.height) * bytesPerRow + (int)(col / self.terrainSize.width * self.heightMap.size.width) * bytesPerPixel;
    unsigned char r = buffer[offset];
    GLfloat x = col;
    GLfloat y = r / 255.0 * self.terrainHeight;
    GLfloat z = row;
    return GLKVector3Make(x, y, z);
}

bytesPerRow是指圖片一行的字節(jié)數(shù)妒挎,也就是圖片寬度 * 4绳锅,bytesPerPixel是每個像素的字節(jié)數(shù),也就是4酝掩。使用red通道的值鳞芙,計算出y軸上的坐標(biāo)GLfloat y = r / 255.0 * self.terrainHeight;self.terrainHeight是可以配置的地形高度期虾。self.terrainSize是地形的大小原朝。self.heightMap.size是灰度圖片的大小。通過計算(int)(row / self.terrainSize.height * self.heightMap.size.height)在圖片上進(jìn)行采樣镶苞。

計算法線

因為我想給每個頂點指定唯一的法線喳坠,所以必須計算出頂點在每個面上的法線之和。在網(wǎng)格上每個頂點最多被4個面共享茂蚓,也就是頂點的前后左右各有一個頂點壕鹉。假設(shè)這四個頂點是Va,Vb,Vc,Vd,中間的點為Vce聋涨,那么第一個面的法線就是(Vb - Vce) 叉乘 (Va - Vce)晾浴,以此類推,算出四個法線牍白,相加后歸一化脊凰,就可以得到最終的法線了。因為邊緣的頂點可能只被2或3個面共享茂腥,所以需要處理一下這種特殊情況狸涌。下面是法線計算代碼。

- (GLKVector3)vertexNormal:(GLKVector3)position col:(int)col row:(int)row buffer:(unsigned char *)buffer bytesPerRow:(size_t)bytesPerRow bytesPerPixel:(size_t)bytesPerPixel {
    GLKVector3 sides[4]; // 最多四條共享邊
    int sideCount = 0;
    // 統(tǒng)計頂點有幾條共享邊最岗,從而計算法線
    if (col >= 1) {
        //左邊有共享邊
        GLKVector3 leftPosition = [self vertexPosition:col - 1 row:row buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
        GLKVector3 vectorLeft = GLKVector3Subtract(leftPosition, position);
        sides[sideCount] = vectorLeft;
        sideCount++;
    }
    if (row >= 1) {
        //前面有共享邊
        GLKVector3 frontPosition = [self vertexPosition:col row:row - 1 buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
        GLKVector3 vectorFront = GLKVector3Subtract(frontPosition, position);
        sides[sideCount] = vectorFront;
        sideCount++;
    }
    if (col <= self.terrainSize.width - 1) {
        //右邊有共享邊
        GLKVector3 rightPosition = [self vertexPosition:col + 1 row:row buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
        GLKVector3 vectorRight = GLKVector3Subtract(rightPosition, position);
        sides[sideCount] = vectorRight;
        sideCount++;
    }
    if (row <= self.terrainSize.width - 1) {
        //后面有共享邊
        GLKVector3 backPosition = [self vertexPosition:col row:row + 1 buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
        GLKVector3 vectorBack = GLKVector3Subtract(backPosition, position);
        sides[sideCount] = vectorBack;
        sideCount++;
    }
    
    GLKVector3 normal = GLKVector3Make(0, 0, 0);
    for (int i = 0; i < sideCount; ++i) {
        GLKVector3 vec = sides[i];
        if (i == sideCount - 1 && i != 3) {
            continue;
        }
        GLKVector3 vec2 = i == sideCount - 1 ? sides[0] : sides[i + 1];
        normal = GLKVector3Add(normal, GLKVector3CrossProduct(vec2, vec));
    }
    return GLKVector3Normalize(normal);
}

GLKVector3CrossProduct就是用作叉乘的方法帕胆,兩個向量叉乘得出的向量將垂直于它們兩個,也就是法向量般渡。

構(gòu)建地形幾何體

有了獲取位置和法線的方法懒豹,就可以很方便的構(gòu)建幾何體了右蹦。

- (void)buildGeometry {
    CGImageRef image = self.heightMap.CGImage;
    size_t bytesPerRow = CGImageGetBytesPerRow(image);
    size_t bitsPerComponent = CGImageGetBitsPerComponent(image);
    size_t bitesPerPixel = CGImageGetBitsPerPixel(image);
    size_t bytesPerPixel = bitesPerPixel / bitsPerComponent;
    UInt8 * buffer = [self dataFromImage:self.heightMap];
    for (int row = 0;row < self.terrainSize.height; ++row) {
        GLGeometry * terrainMeshStrip = [[GLGeometry alloc] initWithGeometryType:GLGeometryTypeTriangleStrip];
        for (int col = 0;col <= self.terrainSize.width; ++col) {
            GLKVector3 position1 = [self vertexPosition:col row:row buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
            GLKVector3 normal1 = [self vertexNormal:position1 col:col row:row buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
            GLVertex vertex1 = GLVertexMake(position1.x, position1.y, position1.z, normal1.x, normal1.y, normal1.z, col / (GLfloat)self.terrainSize.width * 2, row / (GLfloat)self.terrainSize.height * 2);
            [terrainMeshStrip appendVertex:vertex1];
            
            GLKVector3 position2 = [self vertexPosition:col row:row + 1 buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
            GLKVector3 normal2 = [self vertexNormal:position2 col:col row:row + 1 buffer:buffer bytesPerRow:bytesPerRow bytesPerPixel:bytesPerPixel];
            GLVertex vertex2 = GLVertexMake(position2.x, position2.y, position2.z, normal2.x, normal2.y, normal2.z, col / (GLfloat)self.terrainSize.width * 2, (row + 1) / (GLfloat)self.terrainSize.height * 2) ;
            [terrainMeshStrip appendVertex:vertex2];
        }
        [self.terrainMeshStrips addObject:terrainMeshStrip];
    }
    free(buffer);
}

每一行構(gòu)建一個三角帶幾何體,計算UV的時候可以乘以一個縮放系數(shù)歼捐,控制地形貼圖的重復(fù)次數(shù)何陆。通過乘以2col / (GLfloat)self.terrainSize.width * 2,UV的范圍就變成了0~2豹储。為了使貼圖可以重復(fù)贷盲,還需要添加下面的代碼。

    GLKTextureInfo *grass = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"grass_01.jpg"].CGImage options:nil error:nil];
    NSError *error;
    GLKTextureInfo *dirt = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"dirt_01.jpg"].CGImage options:nil error:&error];
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, grass.name);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glBindTexture(GL_TEXTURE_2D, dirt.name);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

GLKTextureInfo創(chuàng)建后剥扣,使用glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);配置它們支持重復(fù)貼圖巩剖。

多重貼圖

為了使地形看起來更加自然,我添加了草和泥土兩種貼圖钠怯,并為地形編寫了新的fragment shader佳魔。

precision highp float;

varying vec3 fragPosition;
varying vec3 fragNormal;
varying vec2 fragUV;

uniform float elapsedTime;
uniform vec3 lightDirection;
uniform mat4 normalMatrix;
uniform sampler2D grassMap;
uniform sampler2D dirtMap;

void main(void) {
    vec3 normalizedLightDirection = normalize(-lightDirection);
    vec3 transformedNormal = normalize((normalMatrix * vec4(fragNormal, 1.0)).xyz);
    
    float diffuseStrength = dot(normalizedLightDirection, transformedNormal);
    diffuseStrength = clamp(diffuseStrength, 0.0, 1.0);
    vec3 diffuse = vec3(diffuseStrength);
    
    vec3 ambient = vec3(0.3);
    
    vec4 finalLightStrength = vec4(ambient + diffuse, 1.0);
    
    
    vec4 grassColor = texture2D(grassMap, fragUV);
    vec4 dirtColor = texture2D(dirtMap, fragUV);
    vec4 materialColor = vec4(0.0);
    if (fragPosition.y <= 30.0) {
        materialColor = dirtColor;
    } else if (fragPosition.y > 30.0 && fragPosition.y < 60.0) {
        float dirtFactor = (60.0 - fragPosition.y) / 30.0;
        materialColor = dirtColor * dirtFactor + grassColor * (1.0 - dirtFactor);
    } else {
        materialColor = grassColor;
    }
    gl_FragColor = vec4(materialColor.rgb * finalLightStrength.rgb, 1.0);
}

增加了兩個紋理uniform sampler2D grassMap; uniform sampler2D dirtMap;,y坐標(biāo)小于30使用泥土的貼圖晦炊,30到60使用泥土和草混合鞠鲜,高于60使用草貼圖。地形在繪制時同時綁定這兩種貼圖断国。

- (void)draw:(GLContext *)glContext {
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glFrontFace(GL_CCW);
    [glContext setUniformMatrix4fv:@"modelMatrix" value:self.modelMatrix];
    bool canInvert;
    GLKMatrix4 normalMatrix = GLKMatrix4InvertAndTranspose(self.modelMatrix, &canInvert);
    [glContext setUniformMatrix4fv:@"normalMatrix" value:canInvert ? normalMatrix : GLKMatrix4Identity];
    [glContext bindTexture:self.grassTexture to:GL_TEXTURE0 uniformName:@"grassMap"];
    [glContext bindTexture:self.dirtTexture to:GL_TEXTURE1 uniformName:@"dirtMap"];
    for (GLGeometry * geometry in self.terrainMeshStrips) {
        [glContext drawGeometry:geometry];
    }
}

因為地形需要使用不一樣的Fragment Shader贤姆,所以在ViewController中為Terrain生成新的GLContext

- (void)createTerrain {
    NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"vertex" ofType:@".glsl"];
    NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"frag_terrain" ofType:@".glsl"];
    GLContext *terrainContext = [GLContext contextWithVertexShaderPath:vertexShaderPath fragmentShaderPath:fragmentShaderPath];
    GLKTextureInfo *grass = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"grass_01.jpg"].CGImage options:nil error:nil];
    NSError *error;
    GLKTextureInfo *dirt = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"dirt_01.jpg"].CGImage options:nil error:&error];
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, grass.name);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glBindTexture(GL_TEXTURE_2D, dirt.name);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    
    UIImage *heightMap = [UIImage imageNamed:@"terrain_01.jpg"];
    Terrain *terrain = [[Terrain alloc] initWithGLContext:terrainContext heightMap:heightMap size:CGSizeMake(500, 500) height:100 grass:grass dirt:dirt];
    terrain.modelMatrix = GLKMatrix4MakeTranslation(-250, 0, -250);
    [self.objects addObject:terrain];
}

到此稳衬,繪制地形就介紹完了霞捡。如果讀者有興趣,可以去網(wǎng)上下載更多的灰度地形圖來嘗試薄疚,使用簡單的材料生成復(fù)雜的地形模型算是繪制地形最有魅力的一面了吧碧信。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市街夭,隨后出現(xiàn)的幾起案子砰碴,更是在濱河造成了極大的恐慌,老刑警劉巖莱坎,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衣式,死亡現(xiàn)場離奇詭異寸士,居然都是意外死亡檐什,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門弱卡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乃正,“玉大人,你說我怎么就攤上這事婶博∥途撸” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長名党。 經(jīng)常有香客問我叹阔,道長,這世上最難降的妖魔是什么传睹? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任耳幢,我火速辦了婚禮,結(jié)果婚禮上欧啤,老公的妹妹穿的比我還像新娘睛藻。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惹挟,像睡著了一般硕噩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锯岖,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼院峡。 笑死,一個胖子當(dāng)著我的面吹牛系宜,可吹牛的內(nèi)容都是我干的照激。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盹牧,長吁一口氣:“原來是場噩夢啊……” “哼俩垃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起汰寓,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤口柳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后有滑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跃闹,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年毛好,在試婚紗的時候發(fā)現(xiàn)自己被綠了望艺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡肌访,死狀恐怖找默,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吼驶,我是刑警寧澤惩激,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布店煞,位于F島的核電站,受9級特大地震影響风钻,放射性物質(zhì)發(fā)生泄漏顷蟀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一骡技、第九天 我趴在偏房一處隱蔽的房頂上張望衩椒。 院中可真熱鬧,春花似錦哮兰、人聲如沸毛萌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阁将。三九已至,卻和暖如春右遭,著一層夾襖步出監(jiān)牢的瞬間做盅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工窘哈, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留吹榴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓滚婉,卻偏偏與公主長得像图筹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子让腹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容