绘制地形
地形模型一般是由NxN的网格构成,网格的点在y轴上的坐标由灰度地形图上相应的颜色决定。颜色越亮,高度越高。颜色每个通道的取值范围可以是0~ 255,通过公式转换,可以很容易的控制生成模型的高度。
生成网格顶点数据
用多个三角带来生成地形。根据单个三角带的的顶点数据的生成规则,计算每个顶点的位置,法线和UV。
计算顶点位置
计算顶点位置之前,我们先要获取到灰度地形图的像素数据。因为我们需要知道指定点的像素颜色。
- (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); //textureData的内存布局是R,G,B,A,R,G,B,A,R,G,B,A,...不停重复。位置(x,y)的像素数据在偏移量y * 图片宽度 * 4 + x * 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; }
获取顶点位置
- (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); }
计算法线
因为我想给每个顶点指定唯一的法线,所以必须计算出顶点在每个面上的法线之和。在网格上每个顶点最多被4个面共享,也就是顶点的前后左右各有一个顶点。假设这四个顶点是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;
// 统计顶点有几条共享边,从而计算法线,GLKVector3CrossProduct
就是用作叉乘的方法,两个向量叉乘得出的向量将垂直于它们两个,也就是法向量
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);
}
构建地形几何体
获取了位置和法线,就可以很方便的构建几何体了。
- -(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); }
GLKTextureInfo *grass = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"dirt_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
创建后,使用 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
配置它们支持重复贴图。
dirt_01.jpg
最终效果