需求背景

本文描述,如何利用OpenGL shader 实现一些有趣的事情,其中包括:

  1. 对一张纹理累加循环处理
  2. 实现一个细胞自动机的例子

纹理绘制、纹理循环处理

在上文 OpenGL 图像处理中已经描述如何使用OpenGL 来绘制一张纹理图,这里和上次一样使用 GLKViewController 的 GLKView 来显示纹理图,核心绘制方法有2个其中一个是正常的绘图到屏幕:

// 输入 inTex, 并绘制
void GLPlayground::drawTexture(const TextureInfo &inTex, bool isVFlip) {
    GLPlayground &d = *this;
    glDisable(GL_BLEND);
    QuadRender *quadRender = d.sharedQuadRender();
    Program *copyPass = d.sharedCopyPass();
    copyPass->use();
    copyPass->setUniformTexture("uTexture0", 0, inTex.textureID);
    quadRender->draw(copyPass, isVFlip);
}

不同的是由于需要反复使用OpenGL,需要复用 program、shader程序,以及纹理的顶点数据。故而都使用享元模式,复用一个,上面代码中, sharedQuadRender 为纹理的二维定点和纹理坐标,

QuadRender *GLPlayground::sharedQuadRender() {
    if (_sharedQuardRender == nullptr) {
        _sharedQuardRender = new QuadRender();
    }
    return _sharedQuardRender;
}

sharedCopyPass 为指定shader 的program

// 每个 vsh fsh 对应一个program
Program *GLPlayground::sharedCopyPass() {
    return getSharedProgram(65536, passthrough_vsh, passthrough_fsh);
}

// 多个program 保存在_sharedPrograms 这个map 里
Program *GLPlayground::getSharedProgram(int key, const char *vsh, const char *fsh) {
    auto iter = _sharedPrograms.find(key);
    if (iter != _sharedPrograms.end()) {
        return iter->second;
    }
    Program *program = new Program();
    if (program->init(vsh, fsh)) {
        _sharedPrograms[key] = program;
        return program;
    } else {
        assert(false);
        delete program;
        return nullptr;
    }
}

最终的绘制方法 quadRender->draw(pass, isVFlip) 则是

void QuadRender::draw(Program *program, bool bVFlip /*= false*/) {
    glBindBuffer(GL_ARRAY_BUFFER, bVFlip ? _vertexBufferVFlip : _vertexBuffer);
    program->setVertexAttribPointer("aPosition", 2, GL_FLOAT, GL_FALSE, sizeof(QuadRender::Vertex), 0);
    program->setVertexAttribPointer(
        "aTextureCoord", 2, GL_FLOAT, GL_FALSE, sizeof(QuadRender::Vertex), BUFFER_OFFSET(sizeof(float) * 2));
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
    glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, 0);
    program->disableVertexAttrib("aPosition");
    program->disableVertexAttrib("aTextureCoord");

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

以上则完成将一张纹理绘制在屏幕上,接下来要实现另一个功能,即使用指定的shader 处理纹理之后,保存输出在另一个纹理b上,其实只要在use program,指定输出到纹理即可:

 // 输出到纹理 outTex
void GLPlayground::drawToTexture(const TextureInfo &outTex, bool isVFlip, const std::function<Program *()> &fun) {
    GLPlayground &d = *this;
    glDisable(GL_BLEND);
    QuadRender *quadRender = d.sharedQuadRender();
    
    d.bindFBO(outTex);
    
    Program *pass = fun();
    glViewport(0, 0, outTex.width, outTex.height);
    quadRender->draw(pass, isVFlip);
}

// 绑定纹理
void GLPlayground::bindFBO(const TextureInfo &texture) {
    glBindTexture(texture.target, texture.textureID);
    FrameBuffer *frameBuffer = sharedFrameBuffer();
    frameBuffer->attachTexture(texture.target, texture.textureID, GL_COLOR_ATTACHMENT0);
    frameBuffer->bind();
}

void FrameBuffer::attachTexture(GLenum texTarget,
                                    GLuint texId,
                                    GLenum attachment /*= GL_COLOR_ATTACHMENT0*/,
                                    int mipLevel /*= 0*/,
                                    int zSlice /*= 0*/) {
    glBindFramebuffer(GL_FRAMEBUFFER, _fboId);
    // 把这个纹理与FBO绑定在一起,以便把数据渲染到纹理空间中去
    glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, texTarget, texId, mipLevel);
}

到此为止则实现将帧缓存的数据,渲染写入到纹理outTex 中。 我们可以通过如下调用实现将纹理inText,通过指定shader 处理后输出到 outTex:

drawToTexture(outTex, [&] {
    // 指定vsh fsh
    static const char *vsh =
    #include "../glutils/passthrough_vsh.glsl"
    static const char *fsh =
    #include "../fluid/update_fsh.glsl"
    Program *pass = getSharedProgram(1, vsh, fsh);
    pass->use();
    // 指定输入的纹理和输入参数
    pass->setUniformTexture("uTexture0", 0, inText.textureID);
    pass->setUniform1i("deltaT", delta);
    return pass;
});

如果把 inText 替换成 outTex,则实现了对纹理 outTex 的循环处理,最终通过如下方式在屏幕上实现最后效果:

// 第一步:初始化inputTexture
[_inputTexture createWithImage: image];

// 通过 CGContext 从图片生成纹理
- (void)createWithImage:(UIImage *)image {
    _width = (int)CGImageGetWidth(image.CGImage);
    _height = (int)CGImageGetHeight(image.CGImage);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    void *imageData = malloc(_height * _width * 4);

    CGContextRef context = CGBitmapContextCreate(imageData,
                                                 _width,
                                                 _height,
                                                 8,
                                                 4 * _width,
                                                 colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(context, CGRectMake(0, 0, _width, _height));
    CGContextDrawImage(context, CGRectMake(0, 0, _width, _height), image.CGImage);

    glBindTexture(_target, _textureID);
    glTexImage2D(_target, 0, GL_RGBA, _width, _height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);

    CGContextRelease(context);

    free(imageData);
}
// 第二步,将_inputTexture 拷贝给 _inputTexture
preProcessing(_inputTexture, _outputTexture);
void Simulation::preProcessing(const TextureInfo &inTex, const TextureInfo &outTex) {

    drawToTexture(outTex, [&] {
        static const char *vsh =
        #include "../glutils/passthrough_vsh.glsl"
        static const char *fsh =
        #include "../fluid/prep_fsh.glsl"
        Program *pass = getSharedProgram(0, vsh, fsh);
        pass->use();
        pass->setUniformTexture("uTexture0", 0, inTex.textureID);
        pass->setUniform1i("deltaT", delta);
        return pass;
    });    
}
// 第三步,对 _outputTexture 循环处理

for (,,) {
    _simulation->stepFrameTest(_stepTime, _outputTexture);
}

void Simulation::stepFrameTest(int dt, const TextureInfo &outTex) {

    drawToTexture(outTex, [&] {
        static const char *vsh =
        #include "../glutils/passthrough_vsh.glsl"
        static const char *fsh =
        #include "../fluid/update_fsh.glsl"
        Program *pass = getSharedProgram(1, vsh, fsh);
        pass->use();
        pass->setUniformTexture("uTexture0", 0, outTex.textureID);
        pass->setUniform1i("deltaT", delta);
        return pass;
    });
            
}

// 最后,每次拿出新的 _outputTexture 绘制到屏幕

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    ......
    glutils::TextureInfo inTex = toFlowImageTexture(_outputTexture);
    _simulation->drawTexture(inTex, true);
}

细胞自动机

在上面的循环处理下,可以做出细胞自动机的demo: 首先,根据任意一张纹理,初始化细胞环境和第一代存活的细胞,其 fsh 如下:

float getY(vec4 color) {
    float r = color.x;
    float g = color.y;
    float b = color.z;
    float y = 0.299*r+0.587*g+0.114*b;
    return y;
}

void main()
{
    vec4 color = texture2D(uTexture0, vTexCoord);
    if (deltaT<0) {
        return;
    }
    // 世界是 deltaT*deltaT 大小
    float d = 1.0/float(deltaT);
    float x = vTexCoord.x;
    float y = vTexCoord.y;
    // 坐标网格化
    x = x - mod(x, d);
    y = y - mod(y, d);
    vec2 newPosion = vec2(x + d/2.0, y + d/2.0);
    vec4 newColor = texture2D(uTexture0, newPosion);
    
    vec4 dieColor = vec4(0.09, 0.09, 0.09, 1.0);
    vec4 liveColor = vec4(40.0/255.0, 128.0/255.0, 20.0/255.0, 1.0);
    // 指定初始状态的生死态
    if (sin(getY(newColor)*6.283) > 0.5){
        gl_FragColor = liveColor;
    } else {
        gl_FragColor = dieColor;
    }
}

每次迭代的 fsh 如下:

// 根据颜色亮度判断生死
int isLive(vec4 color) {
    float r = color.x;
    float g = color.y;
    float b = color.z;
    float y = 0.299*r+0.587*g+0.114*b;
    if (y>0.1) {
        return 1;
    } else {
        return 0;
    }
}

void main()
{
    vec4 color = texture2D(uTexture0, vTexCoord);
    gl_FragColor = color;
    
    if (deltaT == 1) {
        return;
    }
    
    float d = 1.0/float(deltaT);
    vec2 ct = vTexCoord;
    if (ct.x <= d || ct.y <= d || ct.x >= 1.0-d|| ct.y >= 1.0-d) {
        gl_FragColor = vec4(0.09, 0.09, 0.09, 1.0);
        return;
    }
    float x = vTexCoord.x;
    float y = vTexCoord.y;
    x = x - mod(x, d);
    y = y - mod(y, d);
    vec2 newPosion = vec2(x + d/2.0, y + d/2.0);
    vec4 newColor = texture2D(uTexture0, newPosion);
    
    vec2 up    = vec2(ct.x,   ct.y-d);
    vec2 left  = vec2(ct.x-d, ct.y);
    vec2 down  = vec2(ct.x,   ct.y+d);
    vec2 right = vec2(ct.x+d, ct.y);

    // 周围生存例子数
    int liveCount = // getY(texture2D(uTexture0, ct)) +
    isLive(texture2D(uTexture0, vec2(ct.x,   ct.y-d))) + // up
    isLive(texture2D(uTexture0, vec2(ct.x-d, ct.y))) +   //left
    isLive(texture2D(uTexture0, vec2(ct.x,   ct.y+d))) + // down
    isLive(texture2D(uTexture0, vec2(ct.x+d, ct.y))) +   // right
    isLive(texture2D(uTexture0, vec2(ct.x-d, ct.y-d))) +
    isLive(texture2D(uTexture0, vec2(ct.x+d, ct.y-d))) +
    isLive(texture2D(uTexture0, vec2(ct.x+d, ct.y+d))) +
    isLive(texture2D(uTexture0, vec2(ct.x-d, ct.y+d)));
        
    vec4 dieColor = vec4(0.09, 0.09, 0.09, 1.0);
    vec4 liveColor = vec4(40.0/255.0, 128.0/255.0, 20.0/255.0, 1.0);

    // 生命游戏迭代规则:
    // “繁殖”:任何死细胞如果活邻居正好是3个,则活过来
    // “人口过少”:任何活细胞如果活邻居少于2个,则死掉。
    // “正常”:任何活细胞如果活邻居为2个或3个,则继续活
    // “人口过多”:任何活细胞如果活邻居大于3个,则死掉。
    if (liveCount == 3) { 
        gl_FragColor = liveColor;
    } else if (isLive(newColor)==1 && liveCount < 2) { 
        gl_FragColor = dieColor;
    } else if (isLive(newColor)==1 && (liveCount == 3 || liveCount == 2)) { 
       gl_FragColor = liveColor;
    } else if (isLive(newColor)==1 && liveCount>3) { 
        gl_FragColor = dieColor;
     }
}

最终效果如下(任意取 1 帧):

image02:

左上角为输入的原图。

本文的代码可以在 首页->代码仓库->demo 里获取

参考文献:

关于glsl:

https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/05%20Shaders/