獲取示例代碼
本文將介紹如何使用一張灰度地形圖片生成下面的地形模型。
本文用到的灰度地形圖片如下
什么是地形模型
地形模型一般是由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ù)雜的地形模型算是繪制地形最有魅力的一面了吧碧信。