【英文论坛获奖教程】RenderTexture + Blur

RenderTexture + Blur

原作者:
Victor_K
英文论坛原帖:
http://discuss.cocos2d-x.org/t/cocos3-0-tutorial-rendertexture-blur/13622

  这个教程将帮助你理解如何使用cocos2d-x 3.0的着色器和渲染纹理并且给你一些关于数学基础的独特见解。在我的其中一项项目中我需要制造一个根据屏幕大小关于背景和地形的模糊版本。云层固定在屏幕上方,地面固定在底部,所以并不能简单的把一个模糊版本的图像放到资源Resource文件夹中。如果你这么做的话地形图片肯定是静态的,要先经过渲染纹理处理后再进行模糊着色处理,并保存到硬盘上在一系列的应用中使用。图像可能会非常大而且模糊范围也可能会非常大,所以我们需要一些能够运行更为快捷迅速的东西。

这篇教程能够分解为以下几个步骤:

  1. 深入数学研究之中并计算高斯权重。

  2. 创建模糊材质

  3. 渲染纹理并保存到文件夹中

  4. 在样本程序中使用纹理模糊

    我们开始吧。要模糊一个像素我们需要计算附近的像素的权重,权重总和为1。另一个我们需要的属性是中心点权重必须比周边大。为什么高斯函数经常被用来制作模糊效果?因为他有三个非常好的特性。

  5. 它的整体为1

  6. 它的最大值在对称点上

  7. 他有着以下特性:

    二维高斯方程能够拆开成两个一维函数的乘积。这会给我们带来什么样的便利呢?试想,如果我们直接计算出模糊像素的颜色及坐标(I, j)的方法,我们需要把(i-R, i+R)x(j-R, j+R)这个范围内的像素相加起来,R是模糊半径。这将执行一个嵌套循环。嵌套循环意味着他有着n*n的时间复杂度(译者注:算法的时间复杂度,即该算法对n个数据进行处理所需要的时间是哪个数量级。)而这并不是我们所想要加进着色器shader里的结果。记住这个方程的高斯分布的第三个特性,我们能把嵌套循环分裂成两个相关的循环。首先我们处理水平模糊,而后垂直模糊。因此我们在这里就能获得n的时间复杂度。这个方法的神奇之处在于简化后的结果与之前运行较慢的嵌套循环相比没有差别。

    我们开始使用参数sigma = 3 和 mu = 0,以及范围从-1到1的x来计算高斯函数的积分。得到结果0.9973,接近于1。现在我们用一个常用的技术来计算数值积分:我们要画出范围-1< x < 1上的2*R – 1个点,用高斯方程计算它们,得到的结果乘以1/(R - 1)并相加。

    好消息是和接近于1,精度也会随着R值增长。唯一的问题是我们需要让和恰好为1的系数,不然模糊图像在透明度上会出些问题(完全不透明的物体会变得有那么一点透明)。通过计算中央系数1减去其他所有的和来确保这个问题的解决。所以,对于代码。方法就是在开始的时候预先计算高斯系数,把他们的数值传到着色器shader上然后只需在里面做乘法即可。

void TextureBlur::calculateGaussianWeights(const int points, float* weights)
{
    float dx = 1.0f/float(points-1);
    float sigma = 1.0f/3.0f;
    float norm = 1.0f/(sqrtf(2.0f*M_PI)*sigma*points);
    float divsigma2 = 0.5f/(sigma*sigma);
    weights = 1.0f;
    for (int i = 1; i < points; i++)
    {
        float x = float(i)*dx;
        weights* = norm*expf(-x*x*divsigma2);
        weights -= 2.0f*weights*;
    }
}
```


      好的,我们的下一步就是创建一个模糊材质。

#ifdef GL_ES                                                                      
precision mediump float;
#endif                                                                            

varying vec4 v_fragmentColor;                                                     
varying vec2 v_texCoord;                                                          
uniform sampler2D CC_Texture0;

uniform vec2 pixelSize;
uniform int radius;
uniform float weights;
uniform vec2 direction;

void main() {
    gl_FragColor = texture2D(CC_Texture0, v_texCoord)*weights;
    for (int i = 1; i < radius; i++) {
        vec2 offset = vec2(float(i)*pixelSize.x*direction.x, float(i)*pixelSize.y*direction.y);
        gl_FragColor += texture2D(CC_Texture0, v_texCoord + offset)*weights*;
        gl_FragColor += texture2D(CC_Texture0, v_texCoord - offset)*weights*;
    }
}   
```

      
      此外,对于标准的v_fragmentColor, v_texCoord and CC_Texture0我们有四个额外的参数:

1.   pixelSize – 在OpenGL着色语言中并没有像素,只有十进制数,比如,0代表着材质边框的左边或右边1代表着右边框或上边框。这意味着我们需要知道十进制数来制作接下来的像素点。

2.  半径 — 我们的模糊范围。

3.  权重 — 用于高斯预计算的权重数组。

4.  Direction — 用于指示水平和垂直模糊的2D矢量。
      前面代码里的“uniform”意思是这些值在运行的过程中不会改变。剩下的shader部分就比较直接了当了:取得一个像素,用一些系数来积累附近的像素。我不得不将数组最大尺寸硬编码成64因为我找不到将动态数组在shader中写成uniform类。

      我们的下一步就是把我们要的参数传递给shader,但在我们写下一个GLProgram的因子前我们要给GLProgram类加个补丁。问题在于传递一个一维数组到shader作为一个uniform类。在Cocos2d里有着相对应于二维、三维、四维的数组包装。没有单独给一维的,所以接下来我们要做的是:

void GLProgram::setUniformLocationWith1fv(GLint location, const GLfloat* floats, unsigned int numberOfArrays)
{
    bool updated =  updateUniformLocation(location, floats, sizeof(float)*numberOfArrays);

    if( updated )
    {
        glUniform1fv( (GLint)location, (GLsizei)numberOfArrays, floats );
    }
}
```


      现在,当所有的东西就位后,我们就能继续我们的模糊shader:

GLProgram* TextureBlur::getBlurShader(Size pixelSize, Point direction, const int radius, float* weights)
{
    GLProgram* blur = new GLProgram();
    std::string blurShaderPath = FileUtils::getInstance()->fullPathForFilename("Shaders/Blur.fsh");
    const GLchar* blur_frag = String::createWithContentsOfFile(blurShaderPath.c_str())->getCString();
    blur->initWithByteArrays(ccPositionTextureColor_vert, blur_frag);
    blur->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
    blur->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);
    blur->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);

    blur->link();
    blur->updateUniforms();

    GLuint pixelSizeLoc = glGetUniformLocation(blur->getProgram(), "pixelSize");
    blur->setUniformLocationWith2f(pixelSizeLoc, pixelSize.width, pixelSize.height);
    GLuint directionLoc = glGetUniformLocation(blur->getProgram(), "direction");
    blur->setUniformLocationWith2f(directionLoc, direction.x, direction.y);
    GLuint radiusLoc = glGetUniformLocation(blur->getProgram(), "radius");
    blur->setUniformLocationWith1i(radiusLoc, radius);
    GLuint weightsLoc = glGetUniformLocation(blur->getProgram(), "weights");
    blur->setUniformLocationWith1fv(weightsLoc, weights, radius);

    return blur;
}
```


      每一样代码都是非常明了的。在绑定了标准的属性后,我们传递额外的参数和模糊参数已经准备就绪。一个人可以用另一种方法分配shader给GLchar 的变量,就像这样“
const GLchar* shader = 
#include "Shader.fsh"
```


      还有Shader.fsh像这样:
"\n\
#ifdef GL_ES \n\
precision lowp float; \n\
#endif \n\
...
```


我更喜欢使用代码String::createWithContentsOfFile 因为它让你从每行结尾都要写\n的杯具中解脱了出来。
唯一剩下还没做的就是实质上去模糊一张材质。下面就是我们的步骤:

1.从将其作为一个参数材质中创建一个精灵。

2.把它用水平模糊shader绘制到RenderTexture。

3.从得到的材质中创建一个精灵。

4.用垂直shader绘制这个精灵。

5.把图像保存到文件。

6.等至存储完毕,清理并通知caller.

      目前为止一切都好,除了上面说的第六点。 你应该已经看过了下面这段迷人的文字:“在Cocos2d-x v3里渲染器从场景图表中分离了。“但这到底什么意思?(如果你还没看过,那么连接在这:http://www.cocos2d-x.org/docs/manual/framework/native/renderer/en***)。实际上他们的意思是说:在我们这个例子中不能用下面的:

saveToFile(fileName);
useTheFile(fileName);
```

 
      如果你已经用了RenderTexture的saveToFile方法,那么你会发现这里不能存档,相应的一个指令被创建并被添加到人物队列。什么时候它会被执行呢?在这个线程或者其他的?为了是我们的模糊工具稳定并可用,我们必须确保所有关系到渲染的物体在指令saveToFile执行时都必须可用。当它完成时,我们需要在现有线程得到一个返回函数。不幸的是,在RenderTexture里并没有返回函数。但,这是一个开源社区,对吧?我们接下去给另一个类打点补丁,修改一个已经存在的方法,并添加一个新的。
bool RenderTexture::saveToFile(const std::string& fileName, Image::Format format, std::function callback)
{
    saveToFile(fileName, format);
    onSaveToFileCallback = callback;
    return true;
}
```


void RenderTexture::onSaveToFile(const std::string& filename)
{
    Image *image = newImage(true);
    if (image)
    {
        image->saveToFile(filename.c_str(), true);
    }

    CC_SAFE_DELETE(image);

    if (onSaveToFileCallback) {
        Director::getInstance()->getScheduler()->performFunctionInCocosThread(onSaveToFileCallback);
        onSaveToFileCallback = nullptr;
    }
}
```


      onSaveToFileCallBack 是std::function 添加到RenderTexture 类声明并且在构造函数里初始化为nullptr。好的。现在去主要方法那。我们需要以下的东西能够以命令行参数的方式通过:纹理模糊、模糊半径、导出图片名称、一个当每件事都处理好后供调用的返回函数,我们还需要几秒的时间来完成这一步。

void TextureBlur::create(Texture2D* target, const int radius, const std::string& fileName, std::function callback, const int step)
{
    CCASSERT(target != nullptr, "Null pointer passed as a texture to blur");
    CCASSERT(radius <= maxRadius, "Blur radius is too big");
    CCASSERT(radius > 0, "Blur radius is too small");
    CCASSERT(!fileName.empty(), "File name can not be empty");
    CCASSERT(step <= radius/2 + 1 , "Step is too big");
    CCASSERT(step > 0 , "Step is too small");
```


     在公共界面上的声明都是挺好的。声明保证了我们的程序不会损坏谁知道在什么地方,但会稍微提示你,提供一个关于出了什么问题的线索。 
    
Size textureSize = target->getContentSizeInPixels();
    Size pixelSize = Size(float(step)/textureSize.width, float(step)/textureSize.height);
    int radiusWithStep = radius/step;
```


    为了能够再稍微提升速度,我们可以在计算模糊的时候略过一些像素。如果材质中像素的颜色没有在像素间快速转变很好的利用每一秒,因此要再次减少计算量,甚至更近一步。但是不应该出现大的改动毕竟这样会减少图像的质量。

   
 float* weights = new float;
    calculateGaussianWeights(radiusWithStep, weights);

    Sprite* stepX = CCSprite::createWithTexture(target);
    stepX->retain();
    stepX->setPosition(Point(0.5f*textureSize.width, 0.5f*textureSize.height));
    stepX->setFlippedY(true);

    GLProgram* blurX = getBlurShader(pixelSize, Point(1.0f, 0.0f), radiusWithStep, weights);
    stepX->setShaderProgram(blurX);

    RenderTexture* rtX = RenderTexture::create(textureSize.width, textureSize.height);
    rtX->retain();
    rtX->begin();
    stepX->visit();
    rtX->end();

    Sprite* stepY = CCSprite::createWithTexture(rtX->getSprite()->getTexture());
    stepY->retain();
    stepY->setPosition(Point(0.5f*textureSize.width, 0.5f*textureSize.height));
    stepY->setFlippedY(true);

    GLProgram* blurY = getBlurShader(pixelSize, Point(0.0f, 1.0f), radiusWithStep, weights);
    stepY->setShaderProgram(blurY);

    RenderTexture* rtY = RenderTexture::create(textureSize.width, textureSize.height);
    rtY->retain();
    rtY->begin();
    stepY->visit();
    rtY->end();
```


    在这里rtY包含了一个应该被保存进文件的模糊材质,所以动手吧:

    auto completionCallback = ()
    {
        stepX->release();
        stepY->release();
        rtX->release();
        rtY->release();
        delete blurX;
        delete blurY;
        callback();
    };

    rtY->saveToFile(fileName, Image::Format::PNG, completionCallback);
}
```

      在这里我用了一个lambda变量——C++11里最酷的一个特性。记下stepX,stepY这些东西,这表示我们要这些变量通过他们的值被捕获。这意味着我们以lambda变量使用他们时,我们实际上使用的是他们的本地文件的备份。另一个方法是捕获那些相关的变量。但在我们的这个例子中,在返回函数执行的时候这些变量就将会被全部消除。因此导致错误:未定义的行为。在Cocos2d-x资源里你能找到&]和=]的文件构架。他们相应的表示所有的变量都应通过他们的相关性和值以此被捕获。一些C++的安全标准要求在声明lambda捕获方法时对语法尽可能的细致,这个标准对不同的变量可能你会有所不同。

      最后,我们让TextureBlur开始工作。样本图像已画出,还有一些在HelloScene里额外的代码。这就是刚开始打开时的样子。
        

      这是模糊版本:
        

      你能使用一下链接下载资源:
      https://cloud.mail.ru/public/3a0279fe2195/TextureBlur.zip
      
        
       我已经在Mac, iOS和Android上测试了这个程序,请尽量发问以及指出bug等。
       非常感谢。(原作者语)

*

:7:,mark了,我是一楼?

nice,非常不错,学习了

收藏,日后研究!

me mark too

3.2alpha0 套用没成功, 好多函数都改了,更新版本真是个可怕的事情啊:3:

rtY->saveToFile(fileName, Image::Format::PNG, completionCallback);

这个函数会在 文档里 生成一个 back_blur.png 的图片

我想要做实时变化的模糊效果,我不想一直生成这个文件,怎样跳过save 直接调用 completionCallback(const std::string& fileName) ???

在线等

我移到我的工程里,提示这个:

cocos2d: cocos2d: fullPathForFilename: No file found at Shaders/Blur.fsh. Possible missing file.
cocos2d: cocos2d: fullPathForFilename: No file found at Shaders/Blur.fsh. Possible missing file.
cocos2d: Get data from file(Shaders/Blur.fsh) failed!

但是我明明把这个文件放进来了:

我明白了。我添加的是黄色的文件夹。。。。应该要用蓝色的文件夹才是有路径的

选择 Create Folder References for any added folders

而不是默认的 Recursively create groups for any added folders

:10:

为什么我做出来的效果这么烂啊:3:

不应该是这样子的么:6:。。为什么我做出来那么丑:3:

哈哈哈。 :11: :11:

请把代码粘贴在这里
void RenderTexture::onSaveToFile(const std::string& filename, bool isRGBA)
{
    Image *image = newImage(true);
    if (image)
    {
        image->saveToFile(filename.c_str(), !isRGBA);
    }

    CC_SAFE_DELETE(image);

    if (_onSaveToFileCallback) 
    {
        Director::getInstance()->getScheduler()->performFunctionInCocosThread(_onSaveToFileCallback);
        _onSaveToFileCallback = nullptr;
    }
}


```


要报错,Director::getInstance()->getScheduler()->performFunctionInCocosThread(_onSaveToFileCallback); 
提示信息是  : Error: pointer to incomplete class type is not allowed.
我的引擎是3.2.0

Good. :2::2::14::14:

先mark再看

mark 感谢分享