读书OpenGL ES之绘制地形

目前天气温度很低,思思很不乐意:这么冷居然还不下雪?!可是好在还有冰那回事,聊以安慰。

本系列具有文章目录

今日她跟年轻人伴去小区游泳池破冰,拿着竹竿,晃着脑袋,好似丐帮一般。多少个钟头不见归来,连淡定的二伯都说要去找找,不会掉下去了吗。

获取示例代码


本文将介绍如何接纳一张灰度地形图片生成下边的地形模型。

正文用到的灰度地形图片如下

所以,今日就讲讲 ice 吧。

怎么着是地形模型

地形模型一般是由NxN的网格构成,网格的点在y轴上的坐标由灰度地形图上相应的颜色决定。颜色越亮,中度越高。颜色各种通道的取值范围可以是0~
255,通过公式转换,可以很简单的主宰转变模型的莫大。

破冰

生成网格顶点数据

上篇作品中,大家选择三角带生成圆柱体的中等部分。未来我们要用多个三角带来变化地形。

怎么样变化单个三角带小编就不赘述了,上篇作品已经介绍了。上面主要介绍如何总结逐个终端的职分,法线和UV。

思思后日 literally 破了多少个钟头的冰,回来相当取暖。而引申义的 break the
ice,相当于“破冰、暖场、打破狼狈局面”。

算算顶点地方

计量顶点地点在此以前,我们先要获取到灰度地形图的像素数量。因为我们须求领悟钦赐点的像素颜色。

- (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;
}

上边的代码将像素格式不鲜明的图纸转换来4大路路虎极光GBA格式的图形数据。textureData葡京网上娱乐场,的内存布局是R,G,B,A,R,G,B,A,R,G,B,A,...不停重复。地方(x,y)的像素数量在偏移量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是指图片一行的字节数,相当于图片宽度 * 4bytesPerPixel是各种像素的字节数,相当于4。使用red通道的值,总结出y轴上的坐标GLfloat y = r / 255.0 * self.terrainHeight;self.terrainHeight是可以配备的时势中度。self.terrainSize是形势的轻重缓急。self.heightMap.size是灰度图片的分寸。通过测算(int)(row / self.terrainSize.height * self.heightMap.size.height)在图片上展开采样。

新近到位了一部分网上的培养和讲座,觉得助教群体破冰功力的距离巨大,有个别教师开个噱头讲个段子就拉近了跟受众的离开;有个别教师或许以为本身的情节都以干货,破冰太浪费时间而忽视了,可职能的确会比较干燥的。

算算法线

因为自身想给逐个终端钦赐唯一的法线,所以必须总计出顶点在每种面上的法线之和。在网格上各类终端最多被四个面共享,约等于终端的前后左右各有2个极限。假设那八个顶峰是Va,Vb,Vc,Vd,中间的点为Vce,那么首先个面的法线就是(Vb - Vce) 叉乘 (Va - Vce),以此类推,算出八个法线,相加后归一化,就足以拿到最后的法线了。因为边缘的极端或然只被2或2个面共享,所以必要处理一下那种尤其情状。上面是法线总括代码。

- (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;
    // 统计顶点有几条共享边,从而计算法线
    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不怕作为叉乘的主意,两个向量叉乘得出的向量将垂直于它们八个,约等于法向量。

平日生活中,助教群体的破冰普遍做得比较好,不跟学平生常暖个场,课都上不下来啊。

构建地形几何体

有了拿到地点和法线的主意,就可以很有益的打造几何体了。

- (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);
}

每一行打造一个三角带几何体,统计UV的时候可以乘以二个缩放周密,控制时势贴图的双重次数。通过乘以2col / (GLfloat)self.terrainSize.width * 2,UV的限量就变成了0~2。为了使贴图可以另行,还亟需丰裕下边的代码。

    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创建后,使用glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);部署它们协助再一次贴图。

而大师级的破冰展以后上边这一个解说中。闻明的史学家 Sir Ken 罗宾森为了拉近跟观者的相距,所花时间占比为全场演说的50%竟是以上(当然不止于起首)。他做过多少场TED演讲,旁观人次应该是史上最多的。

多重贴图

为了使形势看起来更为自然,作者添加了草和泥土三种贴图,并为地形编写了新的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坐标小于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];
}

到此,绘制地形就介绍完了。要是读者有趣味,可以去网上下载越来越多的灰度地形图来品尝,使用简便的素材生成复杂的地形模型算是绘制地形最有魔力的三头了吗。

有趣味的,请点击 Schools kill
creativity

(解说者是还是不是很熟识?在记录片 Most Likely to Succeed 里,Sir Ken
罗宾森 也频有露面。)

冰棒

在讲“冰棒”怎么说此前,先明了一下,小朋友都爱的棒棒糖叫做 lollipop 或然lolly。

故而呢,“冰棒”,即冰冻的棒棒糖,就径直地称为 ice pop。

实际,在区其他国家,“冰棒”的叫法也略有差距, 列举一下:popsicle
(加拿大、美利坚合作国、新西兰)、freezer pop (美利坚协作国)、ice lolly
(U.K.、印度、爱尔兰、南非共和国(The Republic of South Africa))、ice block(新西兰、澳大布尔萨一些地方)等等。

唯恐你对 popsicle 有回想, 还记得《疯狂动物城》里尼克和小伙伴做 pawpsicle
赚钱的一些吧?Pawpsicle 就是 popsicle 的谐音,更接近动物世界(因为 paw
指“爪子”)。

那就是说为啥 popsicle 就是“棒冰”呢?
因为它是贰个棒冰的商标。在英文世界里日常有用品牌名指代一类物品,比如可以用
Xerox (施乐)指代复印机。

而 popsicle=(lolly) pop+(ic)icle,也毕竟万变不离其宗呢。

加冰

“冰棒” 有点孩子气?那就再来说点成人的饮品吧。

夏日的时候,大家在叫饮料时,平日会说“加点冰”,只怕去饭馆的时候,也会在点酒时“加冰”。

此地首先要弄领会“冰”的性状,有的是 crushed ice (碎冰),有的是 ice cube
(冰块)。

如要是平素不酒精成分的饮料,比即便汁,那么很简短,可以直接说 “I want my
orange juice with ice.”

但只如若酒类,由于经常加的都以 ice cubes, 所以可以把那一个冰块形象地说成
rocks。因而我们能够如此点酒 “I want my tequila on the rocks.” (tequila:
白兰地(BRANDY)酒)

本来若是你不想在酒里加冰块,也得以说 “I want my whisky dry.” (whisky:
白兰地)

作为不喝酒的人,笔者懂这么些干吗~