精华内容
下载资源
问答
  • 今天来搞个赛艇的特技 ---- 体积云。通过 ray marching 算法可以进行体积云渲染。ray marching 算法从摄像机开始,向世界空间...此外,通过偏移采样的方式来判断云朵的光照情况。最后针对渲染的场景问题进行了优化。

    写在前面

    今天来搞了赛艇的特效 ---- 体积云。第一次看见体积云还是在 Minecraft 的光影包里面,好像也是 SE 大大写的。。。当时因为硬件条件(买不起显卡)而没能享受到,今天重新在 OpenGL 中再自己做一次!先上效果图:

    注:本篇博客的代码几乎都在 GLSL 中完成,与前面的博客的 c++ 代码无关,可以放心食用!

    上一篇博客回顾:OpenGL学习(十一):延迟渲染管线 本来想写 OpenGL学习(十二)的,可是一想体积云都是在 shader 里面写的,和 OpenGL 这套 API 没啥关系了,就改了标题。


    ray marching 算法

    与一般的实体绘制不同,体积云是一种无中生有的特效。因为体积云不是 cpu 传递三角面片信息给 GPU 而绘制的,相反,体积云是在 shader 中由算法生成的。

    注意:体积云不是实体,也没有顶点信息,我们在片元着色器中进行渲染。此外,我们渲染体积云,其实是对云后面的像素颜色做计算

    渲染体积云的思路十分简单:在片元着色器中,我们负责对场景的每一个像素进行上色。如果一个像素被云遮挡,那么我们应该把它涂上云的颜色。如图描述了体积云的渲染流程:

    在这里插入图片描述

    于是问题变为求解视线方向和云朵有无相交。如果有,那么我们绘制上对应的颜色:

    在这里插入图片描述

    如果云朵是一个三角形,或者是其他规则的几何图形,比如球形,那么我们通过数学几何的方法,就能很好进行求交,可是偏偏云朵是不规则的,无法确定形状的 “体”,我们无法通过几何方法进行求交。ray marching 算法帮助我们解决了不规则体的求交问题。


    ray marching 算法又名光线行进,在 之前的博客 中我简单讲过这种算法,并且用它来生成了一个体积光的特效作为大作业。今天来详细讲解。

    ray marching 算法从摄像机开始,向世界空间投射光线,并且逐步行进,记录沿途的信息。比如我们沿途不断判断当前点是否在云层中,如果沿途至少有一点在云层中,我们认为视线和云层相交。下图描述了 ray marching 算法的步进过程:

    在这里插入图片描述

    以上的思路是针对具有明确边界的【固体】进行的,但是云朵通常是用一种没有具体边界的【密度函数】来描述的。密度函数的输入是三维的坐标,输出是当前坐标的云朵的密度。于是每次采样我们累积云朵的密度,就可以知道当前光线穿越了多厚的云。二维下的算法图示如下:

    在这里插入图片描述
    两条光线穿越厚度不同的云层,于是累积了不同的云密度。我们根据积累的密度,将云层的颜色和背景的颜色进行混合(时刻记得在任何 “体积” 特效中,我们都是针对背景的像素进行着色!),这里需要用到透明混合的技巧。


    在 RGBA 色彩空间中,RGB 通道存储了颜色,而 A 通道则是透明度。已知背景的颜色为 bgColor,透明覆盖物的颜色为 cvColor,最终的颜色为 c,那么可以用如下的公式进行透明物体的颜色混合:

    c = b g C o l o r ∗ ( 1.0 − c v C o l o r . a ) + c v C o l o r c =bgColor * (1.0 - cvColor.a) + cvColor c=bgColor(1.0cvColor.a)+cvColor

    此处 1.0 - color.a 为透明物体的 “不透明度”,比如透明度是 0.4,不透明度就是 0.6 。我们将不透明度乘以背景色,然后叠加透明物体的颜色即可!


    至此,我们知晓了 ray marching 的整个流程,下面我们来实现一个简单的 ray marching 以绘制带体积的物体,我们在指定范围内生成一个立方体。因为最基础的 ray marching 需要两个变量:

    1. 当前片元的世界坐标:worldPos
    2. 摄像机在世界空间下的位置:cameraPos

    这里 cameraPos 不是眼坐标,不要搞混了。然后我们编写一个函数,执行 ray marching 算法:

    #define bottom 13   // 云层底部
    #define top 20      // 云层顶部
    #define width 5     // 云层 xz 坐标范围 [-width, width]
    
    // 获取体积云颜色
    vec4 getCloud(vec3 worldPos, vec3 cameraPos) {
        vec3 direction = normalize(worldPos - cameraPos);   // 视线射线方向
        vec3 step = direction * 0.25;   // 步长
        vec4 colorSum = vec4(0);        // 积累的颜色
        vec3 point = cameraPos;         // 从相机出发开始测试
    
        // ray marching
        for(int i=0; i<100; i++) {
            point += step;
            if(bottom>point.y || point.y>top || -width>point.x || point.x>width || -width>point.z || point.z>width) {
                continue;
            }
            
            float density = 0.1;
            vec4 color = vec4(0.9, 0.8, 0.7, 1.0) * density;    // 当前点的颜色
            colorSum = colorSum + color * (1.0 - colorSum.a);   // 与累积的颜色混合
        }
    
        return colorSum;
    }
    

    首先朝视线方向 direction 投射光线,然后沿途记录光线是否在指定的盒子中,如果在,那么我们积累颜色,并且进行颜色混合。注意这里我们的 ray marching 是从摄像机出发,在代公式的时候我们要注意:

    • 当前点的颜色 color,是背景色 bgColor
    • 累积的颜色 colorSum,是覆盖物的颜色 cvColor

    于是有:

    colorSum = colorSum + color * (1.0 - colorSum.a);   // 与累积的颜色混合
    

    这样的混合公式。不要搞错了。。。

    最后我们在片元着色器的 main 中添加如下的调用,其中 fColor 是片元着色器输出的颜色。同样,云的颜色是公式中的 cvColor,而背景色是公式中的 bgColor,于是有:

    vec4 cloud = getCloud(worldPos, cameraPos); // 云颜色
    fColor.rgb = fColor.rgb*(1.0 - cloud.a) + cloud.rgb;    // 混色
    

    我们可以看到,一个立方体被绘制了出来:

    在这里插入图片描述

    注意到每次采样,我们都认为当前点的密度为 0.1,然后均匀地叠加颜色,所以我们渲染出来的 “体” 是一个规则的立方体。如果我们随即地改变每次采样的密度,就可以得到形状不规则的云了!


    这里还要引入两个小优化。因为我们的云层是在有效范围 [bottom, top] 内才会生成,而测试却从相机原点开始投射光线。假设摄像机在云层下方,那么从相机开始到云层底部这一段路绝对不会有云,我们可以直接 pass。我们将采样原点移动至云层底部即可:

    在这里插入图片描述


    根据相似三角形法则,我们可以这么挪:

    在这里插入图片描述
    于是在计算完起始点 point 之后,我们马上执行如下代码:

    // 如果相机在云层下,将测试起始点移动到云层底部 bottom
    if(point.y<bottom) {
        point += direction * (abs(bottom - cameraPos.y) / abs(direction.y));
    }
    // 如果相机在云层上,将测试起始点移动到云层顶部 top
    if(top<point.y) {
        point += direction * (abs(cameraPos.y - top) / abs(direction.y));
    }
    

    此外,还有一个问题,就是目前的云层会不正确地遮挡本应该遮挡云层的物体,比如树明明在云层之下,却被云层遮挡了。尤其是我们增大云层的范围 width 的时候:

    在这里插入图片描述

    出现这个问题的原因是我们没有判断当前片元和云层之间的关系。解决方案也很简单,通过距离来判断:

    在这里插入图片描述

    知道原理就可以进行操作了。在平移采样点之后,我们加入:

    // 如果目标像素遮挡了云层则放弃测试
    float len1 = length(point - cameraPos);     // 云层到眼距离
    float len2 = length(worldPos - cameraPos);  // 目标像素到眼距离
    if(len2<len1) {
        return vec4(0);
    }
    

    可以看到现在云层与物体的遮蔽关系能够被正确地描绘:

    在这里插入图片描述
    最终的 ray marching 代码如下:

    #define bottom 13   // 云层底部
    #define top 20      // 云层顶部
    #define width 40    // 云层 xz 坐标范围 [-width, width]
    
    // 获取体积云颜色
    vec4 getCloud(vec3 worldPos, vec3 cameraPos) {
        vec3 direction = normalize(worldPos - cameraPos);   // 视线射线方向
        vec3 step = direction * 0.25;   // 步长
        vec4 colorSum = vec4(0);        // 积累的颜色
        vec3 point = cameraPos;         // 从相机出发开始测试
    
        // 如果相机在云层下,将测试起始点移动到云层底部 bottom
        if(point.y<bottom) {
            point += direction * (abs(bottom - cameraPos.y) / abs(direction.y));
        }
        // 如果相机在云层上,将测试起始点移动到云层顶部 top
        if(top<point.y) {
            point += direction * (abs(cameraPos.y - top) / abs(direction.y));
        }
    
        // 如果目标像素遮挡了云层则放弃测试
        float len1 = length(point - cameraPos);     // 云层到眼距离
        float len2 = length(worldPos - cameraPos);  // 目标像素到眼距离
        if(len2<len1) {
            return vec4(0);
        }
    
        // ray marching
        for(int i=0; i<100; i++) {
            point += step;
            if(bottom>point.y || point.y>top || -width>point.x || point.x>width || -width>point.z || point.z>width) {
                break;
            }
            
            float density = 0.1;
            vec4 color = vec4(0.9, 0.8, 0.7, 1.0) * density;    // 当前点的颜色
            colorSum = colorSum + color * (1.0 - colorSum.a);   // 与累积的颜色混合
        }
    
        return colorSum;
    }
    

    这里还额外添加了一个福利:因为我们手动将采样点移动到贴近云层底层或者顶层的地方,于是我们一旦发现采样超出云层范围,就直接 break 掉,能够节省更多的计算量:

    在这里插入图片描述

    通过噪声图生成云朵

    在上文,我们讨论到只要在每次采样的时候,将每一点的密度随机化(即我们从云朵的密度函数中取值)就可以生成随机的云朵形状。那么现在的问题就是云朵密度函数的获取了!

    随机数在编程语言里面非常常见,不管是 python 的 random 还是 c++ 的 uniform_distribution,可是 GLSL 中我们没这好待遇,于是我们需要从 cpu 中生成,我们以噪声图(纹理)的形式将一张图上面摆满随机数,并且传递给 GPU,这样我们在 shaders 中就可以使用随机数了。。。

    云朵是在一定范围内比较连续的图形,一般的离散噪声还不足以生成云朵。理论上我们要生成一张 连续 的噪声图,比如柏林噪声或者别的,但是这里我 没有学过信号与系统 ,我也不会写生成噪声图的代码(再度摆烂),于是我直接使用了现成的图片:

    在这里插入图片描述

    注:
    这图是从 minecraft 中生成的,在 final 着色器中直接输出光影 mod 生成噪声图然后截图保存的
    又是 mc

    这张图片在边缘也是连续的,这意味着在 c++ 读取纹理时,设置了纹理环绕方式为 REPEAT 之后,超出纹理的采样可以被连续地映射回纹理。这是好的!

    我们通过简单的 c++ 代码就可以加载噪声图,前提是安装了 SOIL2 库:

    // 加载噪声图
    glGenTextures(1, &noisetex);
    glBindTexture(GL_TEXTURE_2D, noisetex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    int textureWidth, textureHeight;
    unsigned char* image = SOIL_load_image("textures/noisetex2.png", &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);   // 生成纹理
    delete[] image;
    

    然后将噪声图传递到着色器:

    // 传阴噪声纹理
    glActiveTexture(GL_TEXTURE6);
    glBindTexture(GL_TEXTURE_2D, noisetex);
    glUniform1i(glGetUniformLocation(composite0, "noisetex"), 6);
    

    注:
    这里传递到任意的纹理单元都可,只是我这里按照顺序排下来是 6 号

    接下来在着色器中添加:

    uniform sampler2D noisetex;
    

    即可访问噪声纹理!


    在使用了噪声图之后,我们就可以根据采样点的世界坐标,手动采样噪声图,同时返回该点的云密度。我们编写一个函数 getDensity 来获取某一点的云密度:

    // 计算 pos 点的云密度
    float getDensity(sampler2D noisetex, vec3 pos) {
        vec2 coord = pos.xz * 0.0025;
        float noise = texture2D(noisetex, coord).x;
        return noise;
    }
    

    随后我们将 getCloud 中的密度计算,由:

    float density = 0.1;
    

    改为:

    float density = getDensity(noisetex, point) * 0.1;
    

    其中 0.1 是为了防止积累的颜色过亮导致看不出差别。最后别忘了在 getCloud 的形参中,加入一个 sampler2D 以传递噪声图给函数。再次运行程序,我们看到:

    在这里插入图片描述
    唔。。。与其说是云,不如说是雾! 云朵的明显特征是云朵之间有间隔,而不是像上图那样相当连续且饱满。 下面我们改变密度的生成方式,来逼近云朵。


    注意到我们使用的柏林噪声是连续的,但是如果我们以一定的阈值去截取,即认为一旦噪声图数据小于某个值,就认为它是0.这意味着有如下的云密度函数,我们通过按照阈值截取,可以生成不连续的密度函数:

    在这里插入图片描述

    我们在计算云密度之前,对云的密度进行截断:

    在这里插入图片描述
    再次加载程序,可以看到云层像一块奶酪一样,被挖了各种洞洞,其中空缺的地方对应噪声图中数值小于 0.4 的部分:

    在这里插入图片描述
    云朵水平方向的形状是有了,可是竖直方向上,像被直直地切了一刀,并不是云朵的椭圆型。我们希望将云朵调整成椭圆型:

    在这里插入图片描述
    我们根据高度,对密度进行调整即可。位于云层中部的密度最大,而位于 bottom 和 top 附近的采样应当获取更小的密度。我们将密度计算函数 getDensity 改为:

    #define bottom 13   // 云层底部
    #define top 20      // 云层顶部
    #define width 40    // 云层 xz 坐标范围 [-width, width]
    
    // 计算 pos 点的云密度
    float getDensity(sampler2D noisetex, vec3 pos) {
        // 高度衰减
        float mid = (bottom + top) / 2.0;
        float h = top - bottom;
        float weight = 1.0 - 2.0 * abs(mid - pos.y) / h;
        weight = pow(weight, 0.5);
    
        // 采样噪声图
        vec2 coord = pos.xz * 0.0025;
        float noise = texture2D(noisetex, coord).x;
        noise *= weight;
    
        // 截断
        if(noise<0.4) {
            noise = 0;
        }
    
        return noise;
    }
    

    这里先把高度决定的权重映射到 [0, 1] 范围,然后开根号,最终进行权重的计算。不要在意那个开根号(二分之一幂)因为这是调参数调的,我认为比较好看。

    重新加载程序,我们得到了一个人模狗样的结果!现在云朵终于圆润了:

    在这里插入图片描述
    显然云朵的细节不够丰富,这是因为我们直接采样噪声图,频率太低,对应云朵非常平缓的边缘。我们尝试利用多个采样的噪声来进行多次采样并且合并出最终的结果。


    关于多次采样噪声。。。我没学过信号与系统,无法解释这个现象,但是就是有效(逃

    开始抄代码(摆烂)我们对噪声进行多次采样,就可以获得一些有丰富细节的高频噪声。我们将 getDensity 中对噪声图的采样增加几次:

    在这里插入图片描述

    注:
    上述采样参数来自 Minecraft 的光影包 Chocapic13_V5_Extreme
    c13 大大早期作品
    OTL

    你可能会说怎么又偷 mc 的?答案是塞拉斯大招转好了 然后重新加载程序,现在的云拥有更多的细节了:

    在这里插入图片描述
    距离炒鸡逼真(并不)的体积云只差一步,下面我们将进行光照的绘制!

    云朵光照效果的绘制

    体积云的光照分为两个部分:

    1. 基础颜色
    2. 光照明暗颜色

    其中基础颜色由累积的密度决定,而光照敏感颜色则取决于云与光源的相对位置。基础颜色所决定的是云的散射光,即云层越厚的地方,要越昏暗,而云层薄的地方则稍亮。在开始之前,我们预设一组颜色变量如下:

    #define baseBright  vec3(1.26,1.25,1.29)    // 基础颜色 -- 亮部
    #define baseDark    vec3(0.31,0.31,0.32)    // 基础颜色 -- 暗部
    #define lightBright vec3(1.29, 1.17, 1.05)  // 光照颜色 -- 亮部
    #define lightDark   vec3(0.7,0.75,0.8)      // 光照颜色 -- 暗部
    

    他们分别是基础颜色和光照颜色的亮暗部分,待会我们会根据密度对亮暗颜色进行混合。马上开始编程!


    先从基础颜色开始绘制。基础颜色根据云的密度决定当前云的颜色,那么我们根据当前采样点的密度,对基础颜色进行线性混合即可。此外每一次采样积累的颜色的亮度,还要取决于该点的密度,于是将 ray marching 中的累积颜色的代码改为:

    float density = getDensity(noisetex, point);                // 当前点云密度
    vec3 base = mix(baseBright, baseDark, density) * density;   // 基础颜色
    vec4 color = vec4(base, density);                           // 当前点的最终颜色
    colorSum = color * (1.0 - colorSum.a) + colorSum;           // 与累积的颜色混合
    

    即:
    在这里插入图片描述

    重新加载程序,可以看到基础颜色的绘制。在云层比较厚的地方呈现暗色,而云层薄的地方则亮一些:

    在这里插入图片描述
    好!接下来我们进行光照云颜色的绘制。


    想要知道一点的亮度,我们就得知道这一点到光源之间隔了多厚的云层。我们从该点出发,朝着光源方向投射射线,再积累一次密度,就能最准确的知道当前点距离光源还隔了多少云。如图:

    在这里插入图片描述
    但是这个方法的复杂度是 n 方,因为每一次采样我们都要重新执行一次 ray marching。总所周知在计算机中 n 方的算法是爬着走的,于是我们引入优化。

    我们没有必要精确地计算每一点到光源到底间隔了多少云,因为现实中在强阳光的照射下,云的渲染总是一半黑一半白,换句话说,云的光照具有二值性,经典非黑即白:

    在这里插入图片描述

    那么我们通过一次采样就够了。我们朝着光源的方向,仅做一次采样,然后计算两点的密度差,以此粗略地估计云的颜色。原理如下:

    在这里插入图片描述
    于是我们将刚才的颜色积累的代码改为:

    // 采样
    float density = getDensity(noisetex, point);                // 当前点云密度
    vec3 L = normalize(lightPos - point);                       // 光源方向
    float lightDensity = getDensity(noisetex, point + L);       // 向光源方向采样一次 获取密度
    float delta = clamp(density - lightDensity, 0.0, 1.0);      // 两次采样密度差
    
    // 控制透明度
    density *= 0.5;
    
    // 颜色计算
    vec3 base = mix(baseBright, baseDark, density) * density;   // 基础颜色
    vec3 light = mix(lightDark, lightBright, delta);            // 光照对颜色影响
    
    // 混合
    vec4 color = vec4(base*light, density);                     // 当前点的最终颜色
    colorSum = color * (1.0 - colorSum.a) + colorSum;           // 与累积的颜色混合
    

    即:
    在这里插入图片描述
    别忘了在 getCloud 函数的形参列表中加入一个 lightPos 表示光源在世界空间下的坐标。此外,这里偷偷加了一个小 trick,就是在计算光照颜色之前,控制一下透明度以达到更好的效果(否则透过云层难以看到背景)

    重新加载程序,我们看到云对于光照有了响应,那只巨大的鸭子模型表示了光源的位置:

    在这里插入图片描述

    注:这种判断方法其实是有误差的,但是实际效果还不错,而且可以模拟云层背光时候,边缘透光的真实效果,故采用之。此外,向着光源进行采样,我们偏移的步长越大,云朵能够透光的 “边缘” 也就越大,这个参数要根据你生成云朵的空间范围大小自己调整。

    优化与改进

    在这一步,我们的体积云已经勉强能看了(虽然写的真的非常 shit

    在这里插入图片描述
    虽然还有很多优化与改进的空间,比如性能的优化,品质的优化等等,毕竟 ray march 非常依赖 GPU 的计算能力。但是由于篇幅有限,以下内容不一定会完全实现这些改进,而是探讨问题所在与大致的解决方案。因为不同的方案可以适用于不同的运用场合。。。

    注:
    翻译过来就是:摆烂了,后面的我不会了,不写了,就嗯编
    编辑下,又肝了一天。下文的优化基本都实现了


    云层移动

    我们希望云层飘动。于是我们在 c++ 中传递 uniform 给着色器,每一帧累加一个变量,以此间接地作为 “时间”,然后我们根据当前时间,在采样噪声图的时候添加一个偏移,因为噪声图是连续的,我们可以模拟云层飘动的效果。

    在 c++ 端我们这么做:

    int frameCounter = 0;
    
    ...
    
    // 传递帧计数器
    frameCounter++;
    if (frameCounter == INT_MAX)
    {
        frameCounter = 0;
    }
    glUniform1i(glGetUniformLocation(composite0, "frameCounter"), frameCounter);
    

    然后在采样噪声的时候我们以 frameCounter 作为偏移量进行采样即可,此处我们只对 x 做偏移,所以云会朝着 x 方向移动:

    在这里插入图片描述
    最终效果如下:

    在这里插入图片描述
    通过改变常数(就是那个 0.0001)可以调整云的移动速度。此外这里我偷懒了,这里使用的是每一帧都++ 的计数器,那么在不同的设备上会有不同的速率,最优的方法应该是计算每一帧的时间,然后提供一个固定 trick 的计数器。。。


    云层内正确的遮挡关系

    还记得上文我们用距离来计算云层与物体的遮挡关系吗?在大多数情况下都是行得通的,但是如果你把云层调整到和物体一个水平,那么就会有 bug,我们可以穿透物体看到其后面的云,这种遮挡一半的关系我们没有正确地处理。如图:

    在这里插入图片描述
    于是会有如下的鬼畜的效果:

    在这里插入图片描述

    解决方案也很简单,我们在 ray march 的时候,每一步都判断光线是否和物体相交。判断的方式是通过深度纹理。将世界坐标转换到屏幕坐标,然后根据当前屏幕坐标采样深度纹理,如果采样深度小于测试深度,说明命中实体,此时应该停止 ray march。

    所以需要如下的 uniform 变量:

    // 透视投影近截面 / 远截面
    uniform float near;
    uniform float far;
    
    // 视图,投影矩阵
    uniform mat4 view;
    uniform mat4 projection;
    
    // 屏幕深度纹理
    uniform sampler2D gdepth;
    

    此外,需要额外的一个负责函数帮助我们判断,因为透视投影的深度是非线性的:

    // 屏幕深度转线性深度
    float linearizeDepth(float depth) {
        return (2.0 * near) / (far + near - depth * (far - near));
    }
    

    最后在 ray march 的 for 循环中加入如下的判断条件:

    // 转屏幕坐标
    vec4 screenPos = projection * view * vec4(point, 1.0);
    screenPos /= screenPos.w;
    screenPos.xyz = screenPos.xyz * 0.5 + 0.5;
    
    // 深度采样
    float sampleDepth = texture2D(gdepth, screenPos.xy).r;    // 采样深度
    float testDepth = screenPos.z;  // 测试深度
    
    // 深度线性化
    sampleDepth = linearizeDepth(sampleDepth);
    testDepth = linearizeDepth(testDepth);
    
    // hit 则停止
    if(sampleDepth<testDepth) {
        break;
    }
    

    现在正常了:

    在这里插入图片描述
    可以看到物体和云层的部分遮挡关系。当物体插入云层的时候,只会显示前方的云。


    可变步长的采样

    在上文中每次采样我们选择固定 0.25 作为步长,迭代 100 次,那么最大范围就是 25 的距离。想要增大云层的范围我们就必须增大【步长与迭代次数的乘积】,我们也可以增大步长,从而减小迭代次数,但代价是云的品质会降低。下图展示了 4 中步长与迭代次数的组合带来的效果变化:

    在这里插入图片描述

    因为远处的云朵对细节的要求没有那么高,我们可以在近处使用低步长,在远处的采样使用高步长,以弥补采样次数过少的我、不足。下面提供一种简单的可变步长的写法:

    // ray marching
    for(int i=0; i<50; i++) {
        point += step * (1.0 + i*0.05);
    
    	....
    }
    

    通过线性增大每一次采样的步长,我们可以使用更少的采样次数,就可以达到凑合的效果。下面是使用了 50 次迭代的结果,与 100 次迭代相比,品质差距不是很大,但是性能却提升了:

    在这里插入图片描述

    注:这里可变步长仅是一个经验公式,需要根据场景的大小尺寸自行定义。此外,仅通过步长变化减少迭代次数并不是最优的优化方式。如果你的显卡足够强劲那么可以无视这个优化。


    分层步进的采样

    注意到一个问题,当视线非常平行于云层的时候,远处的云会有缺失:

    在这里插入图片描述
    原理也很简单,因为我们的 ray marching 是固定步长,这意味着会有一个固定的生成距离,以摄像机为圆心,半径为 R 的球内的云层才会被生成。而当我们平视云层的时候,ray marching 不足以走完整个云层,于是必定会发生缺失:

    在这里插入图片描述
    解决方案也很简单,我们在 y 轴方向上,对采样的步长做一个投影。换句话说,就是使得不管视线方向如何变化,每一次采样我们都在 y 方向上行走固定的距离,如图:

    在这里插入图片描述
    我们将每次步进的代码改为:

    point += step / abs(direction.y);
    

    即可,这样每次步进都会在 y 方向上前进 1 单位的距离,即使我们把云的生成范围增大十倍,最远处也不会发生云的缺失,因为所有光线都能走完云层,但是代价是因为巨大的步长造成的采样品质的下降。

    在这里插入图片描述

    这种方法适合于摄像机不会超出云层的场景。如果一定要渲染在云中穿行的效果,那么还是老老实实一步一步执行 ray marching 吧。。。


    3D噪声

    我没有学过信号与系统,我只会抄代码,我爬 ,关于 3D 噪声的生成,我直接照抄了 szszss大佬的博客,而据他(她?)所言该方法又来自于 iq 大佬在 shadertoy 发表的某篇博客,总之我是看不懂了,抄就完事了(经典摆烂

    将噪声生成的代码 copy 上:

    float noise(vec3 x)
    {
        vec3 p = floor(x);
        vec3 f = fract(x);
        f = smoothstep(0.0, 1.0, f);
         
        vec2 uv = (p.xy+vec2(37.0, 17.0)*p.z) + f.xy;
        float v1 = texture2D( noisetex, (uv)/256.0, -100.0 ).x;
        float v2 = texture2D( noisetex, (uv + vec2(37.0, 17.0))/256.0, -100.0 ).x;
        return mix(v1, v2, f.z);
    }
     
    float getCloudNoise(vec3 worldPos) {
        vec3 coord = worldPos;
        coord *= 0.2;
        float n  = noise(coord) * 0.5;   coord *= 3.0;
              n += noise(coord) * 0.25;  coord *= 3.01;
              n += noise(coord) * 0.125; coord *= 3.02;
              n += noise(coord) * 0.0625;
        return max(n - 0.5, 0.0) * (1.0 / (1.0 - 0.5));
    }
    

    然后重新启动程序,可以看到 3D 噪声的效果还是非常惊艳的,能够更进一步逼近现实中的云:

    在这里插入图片描述

    能用 3D 噪声还是用上。。。毕竟比较真实。我尝试着 copy SE 大大体积云光影里面的 3D 噪声,可惜无奈他代码写的太乱,再加上参数不对口,我嗯是没调出来好的效果。。。这里就不放了

    此外,我在 GitHub 上面找到了另一种 3D 噪声的实现,因为没有效果图,我就没 copy,这里放上 链接 来,有兴趣可以康康


    摩尔纹的解决

    摩尔纹是因为浮点精度不足造成的。如果你是使用累积颜色的方式输出最后的颜色值,而不是混合颜色的话,就会出现摩尔纹。摩尔纹将最终的图形以圆圈的形式分隔开来,因为浮点精度不足,相似方向的 vec3 会被认为是同一个方向,于是就会出现圆形的分隔带。

    虽然在渲染体积云的时候没有出现,但是我上学期做大作业的时候确实遇到过这种现象,如图:

    解决方案也很简单,使用随机步长进行采样即可。随机数从噪声图中获取即可。值得注意的是,随机步长会带来噪点,可以通过后处理阶段对体积云进行一次高斯模糊进行解决。


    低分辨率采样与LOD

    因为云通常是非常模糊的东西,我们没有必要对每个像素都进行 ray march,相反,我们可以只对屏幕上的某些像素进行 ray march,比如左下角 1/4 的像素,得到一张低分辨率的体积云图像,然后在最终合成的时候,再将低分辨率的图像采样到屏幕上。

    相比于使用 1920×1080 的完全分辨率进行采样,下 1/4 的采样能够节省很多的计算量。1920×1080 总共需要调用 2073600次 ray marching,而下 1/4 采样的分辨率是 480×270,需要 129600 次 ray march,整整少了一个数量级!我们可以把宝贵的计算能力节省下来。

    Minecraft 早期的体积云光影就是这么做的,如果你贴近观察,就会发现其实体积云是非常模糊的:

    在这里插入图片描述
    此外,也可以叠加多个不同分辨率下计算的体积云图像,作为最终的输出,也能够达到很好的效果。这个思想叫做 LOD,Level Of Detail,在 shadertoy 上有一篇 iq 大佬的代码 就是通过这个思想,取了 4 个不同的分辨率,最后进行叠加,你敢信这是他 2013 年写的代码?你说是照片我都信:

    在这里插入图片描述
    注意到云仍然模糊并且品质非常高,而且使用 LOD 带来的性能提升是明显的。我的电脑能跑到 60 fps 了


    云阴影

    在原神中就有实现,因为我 是狗罕见 电脑带不动,我没下游戏,于是偷了一张截图:

    在这里插入图片描述

    有三种实现方法,首先是根据世界坐标的 xz 轴,直接去云的噪声图中采样,并且判断当前位置是否有云,有则涂黑。我估计原神就使用了这种方法,优点是简单且快速。

    其次可以从光源方向直接做一次光线求交,注意是求交不是 ray march,这样可以实时地让云阴影响应光源位置的变化。

    第三种方法就是最逼真的,我们从光源方向进行 ray march 以获取实际云层的厚度,同时可以根据厚度涂黑地面。优点是逼真,缺点是计算开销。当然也可以通过降分辨率进行优化,这里就不详细讨论了。。。

    什么?你想看代码?我懒得写了,再度摆烂


    着色器代码

    注:
    这里我只给出最基本的 ray march + 光照的代码,在、方便移植
    不包含 上文【改进与优化】部分的代码

    着色器输出:

    out vec4 fColor;
    

    uniform 变量声明:

    uniform sampler2D noisetex;     // 噪声纹理
    
    uniform vec3 lightPos;  // 光源位置
    uniform vec3 cameraPos; // 相机位置
    

    函数定义:

    #define bottom 13   // 云层底部
    #define top 20      // 云层顶部
    #define width 40    // 云层 xz 坐标范围 [-width, width]
    
    #define baseBright  vec3(1.26,1.25,1.29)    // 基础颜色 -- 亮部
    #define baseDark    vec3(0.31,0.31,0.32)    // 基础颜色 -- 暗部
    #define lightBright vec3(1.29, 1.17, 1.05)  // 光照颜色 -- 亮部
    #define lightDark   vec3(0.7,0.75,0.8)      // 光照颜色 -- 暗部
    
    // 计算 pos 点的云密度
    float getDensity(sampler2D noisetex, vec3 pos) {
        // 高度衰减
        float mid = (bottom + top) / 2.0;
        float h = top - bottom;
        float weight = 1.0 - 2.0 * abs(mid - pos.y) / h;
        weight = pow(weight, 0.5);
    
        // 采样噪声图
        vec2 coord = pos.xz * 0.0025;
        float noise = texture2D(noisetex, coord).x;
    	noise += texture2D(noisetex, coord*3.5).x / 3.5;
    	noise += texture2D(noisetex, coord*12.25).x / 12.25;
    	noise += texture2D(noisetex, coord*42.87).x / 42.87;	
    	noise /= 1.4472;
        noise *= weight;
    
        // 截断
        if(noise<0.4) {
            noise = 0;
        }
    
        return noise;
    }
    
    // 获取体积云颜色
    vec4 getCloud(sampler2D noisetex, vec3 worldPos, vec3 cameraPos, vec3 lightPos) {
        vec3 direction = normalize(worldPos - cameraPos);   // 视线射线方向
        vec3 step = direction * 0.25;   // 步长
        vec4 colorSum = vec4(0);        // 积累的颜色
        vec3 point = cameraPos;         // 从相机出发开始测试
    
        // 如果相机在云层下,将测试起始点移动到云层底部 bottom
        if(point.y<bottom) {
            point += direction * (abs(bottom - cameraPos.y) / abs(direction.y));
        }
        // 如果相机在云层上,将测试起始点移动到云层顶部 top
        if(top<point.y) {
            point += direction * (abs(cameraPos.y - top) / abs(direction.y));
        }
    
        // 如果目标像素遮挡了云层则放弃测试
        float len1 = length(point - cameraPos);     // 云层到眼距离
        float len2 = length(worldPos - cameraPos);  // 目标像素到眼距离
        if(len2<len1) {
            return vec4(0);
        }
    
        // ray marching
        for(int i=0; i<100; i++) {
            point += step;
            if(bottom>point.y || point.y>top || -width>point.x || point.x>width || -width>point.z || point.z>width) {
                break;
            }
    
            // 采样
            float density = getDensity(noisetex, point);                // 当前点云密度
            vec3 L = normalize(lightPos - point);                       // 光源方向
            float lightDensity = getDensity(noisetex, point + L);       // 向光源方向采样一次 获取密度
            float delta = clamp(density - lightDensity, 0.0, 1.0);      // 两次采样密度差
    
            // 控制透明度
            density *= 0.5;
    
            // 颜色计算
            vec3 base = mix(baseBright, baseDark, density) * density;   // 基础颜色
            vec3 light = mix(lightDark, lightBright, delta);            // 光照对颜色影响
    
            // 混合
            vec4 color = vec4(base*light, density);                     // 当前点的最终颜色
            colorSum = color * (1.0 - colorSum.a) + colorSum;           // 与累积的颜色混合
        }
    
        return colorSum;
    }
    

    main 函数:

    vec3 worldPos = ...;	// 想办法弄到当前片元的世界坐标,可以是深度重建或者读坐标纹理
    
    ...
    
    vec4 cloud = getCloud(noisetex, worldPos, cameraPos, lightPos); // 云颜色
    fColor.rgb = fColor.rgb*(1.0 - cloud.a) + cloud.rgb;    // 混色
    

    总结

    经过不懈的努力,我们终于得到了一个还凑合的体积云渲染,也了解了体积渲染的一些工作流程和常识。下次有时间我把它移植到 mc 里面,下次再说吧,咕!

    展开全文
  • Android 性能优化 ~ 包体积优化实战

    千次阅读 2020-03-29 23:54:27
    概述 用户通常都不愿意去下载一个比较大的程序,特别是不在 WIFI 的情况下。...在移动网络情况下,包体积越小,用户安装的的可能性越大。所以安装包大小对用户的转换率有很大的影响。接下来就和大家分享下我在实...

    概述

    用户通常都不愿意去下载一个比较大的程序,特别是不在 WIFI 的情况下。如果你的安装包很小,用户还是愿意下载安装体验下的。现在市面上满足某种需求的 App 通常都会有很多款,如何让用户愿意下载你的 App 来体验?安装包越小,在 WIFI 情况下,极速下载安装,开始体验。在移动网络情况下,包体积越小,用户安装的的可能性越大。所以安装包大小对用户的转换率有很大的影响。接下来就和大家分享下我在实际中工作中对包体积优化的一些经验。

    APK 文件结构

    既然是要优化 Android APK 安装文件的大小,首要需要了解下 APK 文件的结构。将 APK 文件拖进 AndroidStudio 可以清楚的看到 APK 文件组成部分。APK 主要由以下几部分组成:

    • META-INF/: 该文件夹下主要包含 CERT.SF 和 CERT.RSA 签名文件, 以及 MANIFEST.MF 清单文件
    • assets/: 该文件夹主要包含 app 中的资产文件,在程序中通过 AssetManager 对象来获取
    • res/: 该文件夹主要包含没有被编译进 resources.arsc 的文件
    • lib/: 该文件夹包含一些平台的 so 库, 如 armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, and mips.
    • resources.arsc: 该文件主要存放着编译后的资源。主要存放着 res/values 目录下的文件内容,打包工具会将该目录下的 XML 内容(string、style)提取出来编译成二进制格式。
    • classes.dex: 该文件主要包含能够被 Dalvik/ART 虚拟机理解的 DEX 格式的 class 文件
    • AndroidManifest.xml: 该文件主要核心的 Android 清单文件,该文件使用 Android 的二进制 XML 格式。

    优化手段

    其实 APK 最核心的就两个内容,图片资源和代码。所以包体积优化主要是从这两方面入手。例如检查 assets 目录下是否有没有用到的资源。一般来说很少会在 assets 目录放一些没用的资源,主要是集成第三方 SDK (如高德、Baidu地图等)的时候需要放一些资源进去,比如图片、音频文件等。随着项目的迭代,界面 UI 的风格和以前相比发生了很大的变化,那么以前很多图片资源也就不可用了,所以在 res 目录下的可能会存在很多不用的图片,这是我们清理未使用资源最重要的一个文件夹。除了图片,然后就是 classes.dex 文件 了,一般我们自己的程序的业务代码不会对包体积产生很大的影响,主要是使用了大量的第三方库,以及集成公司内部其他团队的一些 module ,可能这些 module 包含了大量我们用不到的代码或者资源。

    在优化之前,来看下我所做项目的安装包大小为 73437KB(71.7MB),为后面做的优化好有一个对比,看看具体的优化幅度。

    通过 AndroidStudio 移除未使用的资源

    手动移除资源有两个好处:一个是减少安装包的体积,另一个是减少源代码的体积。

    在 AndroidStudio 中有两种方式帮我们找到未使用的资源:

    • Analyze -> Inspect Code,实际上就是通过 lint 工具帮我们找不用的资源,除了图片资源,还会帮我找到代码中存在的潜在问题,运行效果如下图所示:

      Inspect Code

    • 双击 shift,输入 Remove Unused Resources,然后回车。由于上面的方式不仅找出未使用到的资源,还会检测代码,所以运行的比较耗时。如果你仅仅只想找出未使用的资源,可以使用双击 shift 的方式,它们检测的结果都是一样的。

    上面的工具在使用的过程中有两个坑:

    • 用到的资源,依然报没有引用。如一些 drawable 文件的 xml 资源

    • 它还会移除很多布局中的id,如果项目中使用了 ButterKnife,是通过 R2 来应用 id 的,该工具无法检测这种情况

    所以,在针对 drawable 目录下的资源我们可以通过 git 将其 revert,因为我们的 icon 很少会放进 drawable 目录的。对于布局中声明的 id 被移除,我们可以将 layout 文件夹 revert。

    通过上面的操作,成功将包体积减少了 2.3M:

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)

    在手动移除未使用的资源的过程中,发现了另一个问题。现在都是模块化工程了,我们项目有几十个 module,很多 module 中尽然包含了系统默认的 ic_launcher 图标,新建 module 默认生成的,而我们项目的图标名字改为了 app_icon,也就是里面的 ic_launcher 是没有用的。每个 module 下关于 ic_launcher 就 8 个文件夹:

    drawable
        -> ic_launcher_background.xml
    
    drawable-v24
        -> ic_launcher_foreground.xml
    
    mipmap-anydpi-v26
        -> ic_launcher.xml
        -> ic_launcher_round.xml
    
    mipmap-hdpi
        -> ic_launcher.png
        -> ic_launcher_round.png
    
    mipmap-mdpi
        -> ic_launcher.png
        -> ic_launcher_round.png
    
    mipmap-xhdpi
        -> ic_launcher.png
        -> ic_launcher_round.png
    
    mipmap-xxhdpi
        -> ic_launcher.png
        -> ic_launcher_round.png
    
    mipmap-xxxhdpi
        -> ic_launcher.png
        -> ic_launcher_round.png
    

    有的时候,这些 module 可能需要这些 launcher,虽然在发布的时候不需要,但是我们可能需要单独是运行这个组件,一般会有一个 debug manifest 和 release manifest,然后通过一个标记来判断是 library 还是 application。其实也可以用过其他方式来实现这种 debug 和 release 的情况(可以在 module 工程外 套一层工程,该工程包含这个 module,作为可以运行的 application)。通过这种方式,module 就不需要存在 application 的情况,也就不需要 launcher 图标了。

    其实这也是开发者非常容易忽略的问题,例如,我们依赖的很多其他部门的内部库,通过 ctrl+shift+r 查找 ic_launcher,会发现很多 aar 会有 launcher 资源。甚至有些不规范的第三方开源库也同样存在这些问题。

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB

    为什么移除了这么多的 launcher 图片,为什么 apk 的大小只是减少了 19KB?(具体哪些地方减少了,可以通过 Compare with previous APK 功能进行对比)。

    由于最终生成 APK 的时候,同名文件只会使用一个资源,也即是只会存在一份,所以优化的幅度不大(关于多个 module 相同路径存在相同文件名,打包时会有优先级,大家可以查看官方文档)。但是清理我们项目中一些垃圾资源。

    开启 shrink resource

    其实,在我们工程的 app/build.gradle 中配置了开启 shrink resource 了:

    minifyEnabled true
    shrinkResources true
    

    我们使用的程序的图标名字使用的不是 ic_launcher,而是 app_icon,我们通过 APK Analyze 分析我们的 APK 发现 ic_launcher 资源还在,ic_launcher 名字的图标上在程序中应该没有被用到,为什么没有被 shrink 呢?有两种可能:

    • 有某个地方隐形用到了 ic_launcher 文件。
    • shrink 没有生效

    我们先来项目中的 shrink 有没有生效。 我放一个新的资源(abc.webp)到工程中去,然后重新打包,如果该文件被shrink了说明 shrink 是生效的(也就间接说明了程序中某个地方用到了 ic_launcher),如果没有被 shrink 说明上面的配置没有使得 shrink 生效,想办法让其生效即可。

    通过 APK Analyze 打开新生成 APK 文件,发现新加入的 abc.webp 文件依然存在:

    abc.webp

    说明 shrink 没有生效。明明已经配置了 minifyEnabled、shrinkResources,为什么没有生效呢。

    经过一番查找,原来是在 proguard 文件中设置了不要 shrink :

    -dontshrink
    

    把这行注释,然后重新打包,发现减少了 3.37MB

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)

    shrinkMode 主要有两种:safe、strict,默认模式为 safe。

    可以在 res/raw/keep.xml 文件中配置 shrinkMode:

    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:shrinkMode="safe"/>
    

    如果开启了 shrink resource,当 shrinkMode = safe 时,打包的时候会主动寻找那些可能被引用的资源,如通过 resources.getIdentifier() 方式获取资源,该资源不会被缩减,当 shrinkMode = strict 严格模式时该资源不会被缩减。

    我在做实验的时候发现,如果一个资源被 shrink 了,它可能还在 APK 中,只不过该资源的体积变得非常小。

    如果你将 shrinkMode 设置为 safe,那么可能没有被用到的也被保留了,因为检测可能没有那么精准。

    你可以将 shrinkMode 设置为 strict,这个时候需要将通过 resources.getIdentifier(A)方式获取的资源 keep 起来。可以在 keep.xml 中配置要保留的文件:

    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:shrinkMode="strict"
    	tools:keep="@drawable/ic_get_by_identifier"/>
    

    更多关于混淆相关的知识,可以查看 AndroidAll

    png 转成 webp

    Android4.0 开始支持 webp,但是只有在 Android4.3 才支持透明度、无损 webp。所以如果你的 app 最低支持 4.3 的话,可以使用 webp 代替 png。

    在 AndroidStudio 中支持一键转化,可以选择转码的质量比,还可以选择如果转成的 webp 反而比原来的 png 还要大,可以跳过。

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)
    Png2webp64505KB(62.9M)3071KB(3M)

    Enable R8

    由于之前 R8 还不是很稳定,所以我们将其关闭了。现在都 AndroidStudio 3.6 了,我们将其打开:

    android.enableR8=true
    

    虽然官网上说 R8 支持现有 ProGuard 规则文件,但是在实际使用的时候还是会有些问题,解决一些混淆配置上的问题,重新打一个 release 包,发现减少了 0.9M:

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)
    Png2webp64505KB(62.9M)3071KB(3M)
    R863506KB(62M)999KB(0.97M)

    上面是 R8 的普通模式,R8 还有完全模式,还会做一些额外的优化操作,R8 开启完全模式,但是目前还是实验性质的:

    android.enableR8.fullMode=true
    

    重新打一个 release 包,发现减少了 0.16M:

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)
    Png2webp64505KB(62.9M)3071KB(3M)
    R863506KB(62M)999KB(0.97M)
    R8 FullMode63333KB(61.8M)173KB(0.16M)

    通过自定义 View 来代替图标

    我们还可以通过自定义 View 来代替一些状态图标,比如订单状态、退款状态等。如下所示:

    在这里插入图片描述

    类似这些图标都是可以使用自定义 View 来完成,可以减少大量的图片资源。如果状态很多,就会需要很多的状态图标,如果支持国际化的话,还需要为每个国家生成对应的状态图标。

    经过自定义 View 替换状态图标后,包体积减少了 0.366M:

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)
    Png2webp64505KB(62.9M)3071KB(3M)
    R863506KB(62M)999KB(0.97M)
    R8 FullMode63333KB(61.8M)173KB(0.16M)
    CustomView62958KB(61.4M)173KB(0.36M)

    使用 AndResGuard

    微信使用的 AndResGuard 可以对资源资源路径以及资源名字进行混淆,资源名字全部改成类似 abc 的样子。可以大大减少名字字符占用的空间大小。

    特别是模块化后,为了防止资源重名,我们都会在资源的加上模块前缀,这样导致资源的名称就更长了。使用 AndResGuard 的时,程序中通过 getIdentifier 方式获取资源,一定要加入白名单,这个可以在程序中全局查找。

    通过 AndResGuard 混淆后,包体积减少了 3.54M:

    操作体积减少
    优化前73437KB(71.7MB)-
    Inspect Code71054KB(69.3MB)2383KB(2.3M)
    Remove Launcher71035KB(69.3MB)19KB
    ShrinkResources67576KB(65.9MB)3459KB(3.37M)
    Png2webp64505KB(62.9M)3071KB(3M)
    R863506KB(62M)999KB(0.97M)
    R8 FullMode63333KB(61.8M)173KB(0.16M)
    CustomView62958KB(61.4M)173KB(0.36M)
    AndResGuard59323KB(57.9M)3635KB(3.54M)

    so 文件

    在主流的手机CPU架构都是 ARM,基本上只要支持这一种架构就可以了。更多关于这方面的知识可以查看 Android NDK ~ 基础入门指南

    我们来看下市面上主流的 app 支付宝和微信的 CPU 架构:

    alipay-arm

    weixin-arm

    armeabi-v7a 是向下兼容 armeabi,arm64-v8a 能兼容 armeabi-v7a 和 armeabi

    我们项目中也是只支持一种 armeabi-v7a 架构,减少 so 文件体积大小

    release {
        ndk {
            abiFilters 'armeabi-v7a'
        }
        //...
    }
    

    小结

    到此,就介绍完了我这次包体积优化相关内容了,差不多了减少了 20% 的包体积大小。当然优化是无止尽的,除了上面的一些优化手段还有 app Bundles 的方式(需要结合 Google Play 一起);还可以考虑通过 BackgroundLibrary 替换程序中大量的 shape、selector 文件,减少包体积,但是该库对性能有一定的影响,所以我还没有使用,后面可以考虑是否还有更好的方案;还可以找出程序中重复的图片(图片内容一致,名字不同);当然还有插件化,插件也需要瘦身,减少下发消耗的流量。

    另外本文涉及到的代码都在我的 AndroidAll GitHub 仓库中。该仓库除了 性能优化,还有 Android 程序员需要掌握的技术栈,如:程序架构、设计模式、性能优化、数据结构算法、Kotlin、Flutter、NDK,以及常用开源框架 Router、RxJava、Glide、LeakCanary、Dagger2、Retrofit、OkHttp、ButterKnife、Router 的原理分析 等,持续更新,欢迎 star。

    展开全文
  • 发现,如果阴极表面同样粗糙,我们可以获得均匀的在没有任何预电离的情况下在SF6混合物中进行大体积放电,并省去了均匀的电场电极轮廓。 自持续体积放电(SSVD)的形式是自启动体积放电(SIVD)。 我们在这里展示在...
  • 克拉默法则、逆矩阵、体积

    千次阅读 2014-07-11 16:08:47
    这一篇主要介绍行列式的3个应用:求逆矩阵、方程求解、计算面积和体积。 应用1:求逆矩阵 首先直接给出求逆公式 , 是A的代数余子式矩阵(matrix of cofactors), ,其中C11为元素a11的代数余子式,以此类推。...

    这一篇主要介绍行列式的3个应用:求逆矩阵、方程求解、计算面积和体积。

    应用1:求逆矩阵

    首先直接给出求逆公式 是A的代数余子式矩阵(matrix of cofactors), ,其中C11为元素a11的代数余子式,以此类推。那么要证明上面的逆公式成立,即要证明 成立,将此公式展开成下式后我们知道 确实是成立的。

    因为对于对角线元素,它们分别等于


    这些加起来都等于detA,至于非对角线元素,由于它们都是某数乘以代数余子式然后相加的形式,所以我们可将其看成是某个矩阵As的行列式,比如说对于 ,那么A的第2行看成是As的某行,因为C11…C1n是A第1行元素的代数余子式,所以同时C11…C1n中又包含了A的第2行,因此As中有两行相同,因此As的行列式为0。

    应用2:求解方程

    因为前面有了求逆公式,因此对于Ax=b有 ,克莱姆法则(Cramer’s rule)其实就是在此公式基础上做了进一步讨论,克莱姆从这个公式中得出了 , …,并且他发现了这些矩阵B1、B2…的规律,B1就是将矩阵A的第一列用b来代替得到的矩阵,Bj就是将矩阵A的第j列用b来代替得到的矩阵,因此 ,但是克莱姆法则实在是不实用,计算很不方便,因此不建议用它们来计算x。

    应用3:求面积和体积


    行列式可用来求二维形状的面积或三维形状的体积,2阶行列式对应的是平行四边形的面积公式,比如上面的图中,已知两个向量的坐标分别为(a,b),(c,d),现在要求这两个向量构成的平行四边形的面积,按照正常思路我们应该求得平行四边形的底和高,然后利用面积公式,但是很明显这样做非常麻烦,现在利用行列式直接求,不需要任何边长和角度,有:面积= ,这个内容非常重要,而且求得了平行四边形的面积,这两个向量围成的三角形面积也很好求,即为平行四边形面积的一半。但是如果三角形顶点不在原点呢?比如说把三角形移到某处,三个顶点的坐标分别为(x1,y1),(x2,y2),(x3,y3),则同样可以用行列式来求,即面积= ,其实消元过程就是在将三角形移回原点,以上是2维的情况,在三维里,3*3单位阵对应的是顶点位于原点的正方体;正交矩阵Q对应的也是一个立方体,因为正交矩阵的特点是各列都正交,只不过跟单位矩阵对应的立方体不同之处在于这个立方体被旋转了。

    展开全文
  • [OpenGL] 体积光效果实现

    千次阅读 2019-02-23 14:12:25
    reference: Volumetric Light Effects in Killzone: Shadow Fall [1] ...游戏开发相关实时渲染技术之体积光 [3] 本文包含的内容: 1.体积光介绍 2.使用Ray-match实现最基本的体积...

    reference:

    Volumetric Light Effects in Killzone: Shadow Fall [1]

    Interactive Rendering Method for Displaying Shafts of Light [2]

    游戏开发相关实时渲染技术之体积光 [3]

    本文包含的内容:

    1.体积光介绍

    2.使用Ray-match实现最基本的体积光

    3. 加入大气散射效果

    4.体积光遮挡关系计算,实现缝隙穿过体积光效果

    5.体积光性能优化,半分辨率 + 随机扰动采样位置

            场景中往往有很多比较"虚"的东西,比如烟、雾、光、云等,关于制作这类效果,我之前所知道的方法包括比较trick的,如用billborad和uv动画来做障眼法;又或是比较复杂的,用粒子来模拟。最近了解到有一种比较常见的方法,也就是用体素(体绘制)的方法来绘制物体。这是一个比较有意思的思路,因为在三维造型中,网格表达是非常常见的,但是在这种表达之下,物体往往是“空心"的。而更加符合人直觉的表达空间中三维物体的方法,应该是记录物体在空间哪些位置存在,存在则为1,不存在则记录为0,这就引出了体绘制的概念。

           本次我们更侧重于用体素的思想来渲染不存在实体的”体积光“。也就是说,我们可以不为体积光绘制mesh,而是直接在着色器中,在物体所在的位置,直接进行绘制。体素只是一种绘制的思想,它的实现有很多方法。

    体积光

           首先,我们要确认一下什么是体积光。体积光通俗来说是我们能看见的”光路“,并不是所有灯光都会形成体积光效果,它是光照到大气中粒子散射后得到的效果(丁达尔效应)。我们有时候还会看到一束束光散开的效果,这是光在传播过程中遇到了障碍物(比如穿过云层、树木的光束)导致的。

           根据物理原理,我们知道体积光是粒子散射的结果,如果我们用体素的思想来考虑体积光,我们所看到的某一点处的体积光颜色是眼睛到当前点的射线上,光路中所有粒子散射光的叠加。基于这一思路,我们引入Ray-match的方式来模拟体积光。

            备注:以下代码实现均基于延迟渲染。

    最基本的Ray-Match

           

             Ray-Match也就是从眼睛向场景发出许多条射线。对于每条射线而言,我们截取落在体积光中的线段,并每次推进一定步长在线段中进行位置采样,并做散射光强的计算,最终把所有结果加在一起,得到当前位置的光强。

              在延迟渲染的框架中,我们可以在g-buffer的后处理过程中完成这一过程。在不考虑任何优化的情况下,我们可以逐像素做这件事(一般情况下,我们会做降采样)。也就是对屏幕上的每个像素,从眼睛处经过该像素的位置发出射线,然后按一定步长采样,把所有结果加起来,计算最终的像素颜色。

             在具体实现中,对于每个像素而言,我们要考虑的问题是,我们应该从射线的何处开始采样,何处停止采样,以及采样的步长应该为多少。

              确定采样的起点和终点,我们可以给出三种做法:

             (1) 无需求交,直接从近裁剪面裁剪到当前深度采样。适用于全屏阳光,也可用于区域光,但对后者而言,采样精度较低,因为采样的大部分位置并没有落在体积光内,会出现重影现象,要达到同样的效果需要更多的采样点。

             (2) 通过求交,求出光所在的范围,在该范围内采样。适用于区域光。问题在于对复杂几何体求交对性能损耗很大,像球体这样的光源则会简化不少。

             (3) 渲染光所在范围的几何体正面和背面的深度,然后利用这个深度来确定采样的范围。缺点在于会消耗2个通道,如果在延迟渲染框架下,我们不一定有位置留给这两个通道,但是计算很简单,适用于复杂几何体。

             还有一种思路是,直接绘制几何体,然后在几何体的着色器中算体积光的像素颜色。不过这也需要求得几何体每个位置的深度。

             对于全屏的体积光,我们不一定要最远采样到远裁剪面,也可以自己设定一个采样终止点,当体积光距离视点达到一定距离时,可以将它视为不重要信息,所以可以无需绘制,来提升体积光的渲染性能。

            

             以下是利用无需求交的方式计算的体积光:

    点光源,100步采样

     

    光柱,200步采样

          点光源的示例代码(G-Buffer后处理部分),以下代码中ray-match推进求世界坐标的代码可简化,不需要每次求逆,利用起始坐标和视线方向即可。 (pos = viewpos + viewdir * step)

           vertex shader

    #version 450 core
    uniform float fov;
    uniform float zFar;
    uniform float aspect;
    attribute vec4 a_position;
    attribute vec2 a_texcoord;
    
    varying vec2 v_texcoord;
    varying vec2 farPlanePos;
    
    void main()
    {
        gl_Position = a_position;
        v_texcoord = a_texcoord;
    
        float t = tan(fov/2);
        farPlanePos.x = (v_texcoord.x * 2 - 1) * zFar * t*aspect;
        farPlanePos.y = (v_texcoord.y * 2 - 1) * zFar * t;
    }
    

            fragment shader

    #version 450 core
    
    in vec2 v_texcoord;
    
    uniform vec3 cameraPos;
    uniform vec3 lightPosition;
    uniform sampler2D NormalAndDepth;
    uniform sampler2D ColorTex;
    uniform mat4 Inverse_ViewMatrix;
    
    uniform float zFar;
    uniform float zNear;
    
    varying vec2 farPlanePos;
    
    vec3 yellow_light = vec3(1,198.0/255.0,107.0/255.0);
    
    // use linear z depth
    vec3 ComputeWorldPos(float depth)
    {
        vec4 pos = vec4(vec3(farPlanePos.x, farPlanePos.y,-zFar) * depth , 1);
        vec4 ret = Inverse_ViewMatrix * pos;
        return ret.xyz / ret.w;
    }
    
    void main(void)
    {
        vec4 result = texture2D(NormalAndDepth, v_texcoord);
    
        vec3 normal = result.xyz * 2 - 1;
        float depth = result.w;
        vec3 worldPos = ComputeWorldPos(depth);
    
        vec3 result;
    
        // 体积光
        {
            float I = 0.0;
            float d = depth * zFar;
            int virtual_plane_num = 100;
            int begin = int(virtual_plane_num * zNear / (d - zNear));
            int end = int(virtual_plane_num * (zFar - d) / (d - zNear));
            for(int j = begin;j <= virtual_plane_num + begin;j++)
            {
                float z = 1.0 * j / (begin + virtual_plane_num + end);
                // the judge below can be ignored
                if(z < depth)
                {
                    vec3 pos = ComputeWorldPos(z);
    
                    vec3 lightDis = pos - lightPosition;
                    float lightDis2 = lightDis.x * lightDis.x + lightDis.y * lightDis.y + lightDis.z * lightDis.z;
    
                    float attenuation = 1.0 / (0.05 * lightDis2);
                    I += clamp(attenuation  / virtual_plane_num ,0,1);
                }
    
    
            }
            I = clamp(I,0,1);
            result += I * yellow_light;
    
        }
    
        // 加一层平行光源
        {
            vec3 ViewDir = normalize( cameraPos - worldPos );
            vec3 lightDir = normalize(vec3(0.5,1,0.2) );
            vec3 halfDir = normalize(lightDir + ViewDir);
            float diffuse = 0.3 * clamp(dot(normal, lightDir), 0, 1) ;
            vec3 reflectDir = normalize(reflect(-lightDir,normal));
            float specular =  0.3 * pow(clamp(dot(reflectDir,halfDir),0,1),50.0);
            vec3 color = (diffuse + specular) *vec3(1,1,1);
            result += color;
        }
        float ambient = 0.3;
    
        vec3 color = vec3(texture2D(ColorTex,v_texcoord));
        gl_FragColor = vec4(result + ambient * vec3(1,1,1),1);
    
    }
    

    Mie散射

           为了模拟更真实的物理效果,我们可以考虑对像太阳光这样的光源加入大气散射的效果,可以模拟大气环境表现。

           

           (网上有两种计算方式,包括(1-g)^2和1-g^2,我也不清楚哪个是对的,上图是用第二个来计算的)

           我们选用hg公式作为Mie散射效应的一个近似,然后将计算得到的光照结果乘以hg系数,作为大气散射的衰减,最终的效果如上图。具体计算如下:

    vec3 lightDis = pos - lightPosition;
    vec3 viewDis = pos - cameraPos;
    
    vec3 lightDir = normalize(lightDis);
    vec3 viewDir = normalize(viewDis);
    float cosTheta = dot(lightDir,-viewDir);
    
    float hg = 1.0/(4*3.14)* (1 - g*g)/ pow(1 + g*g -2*g * cosTheta , 1.5);

    遮挡关系

            我们常常看到这样的场景,阳光穿过云层或者树林,一缕缕撒在地面上;或是一束光透过窗户,照亮黑暗的小屋之类的效果。这些都是因为光遇到了障碍物,因而不再前行,只有不被遮挡的光才最终达到更远的地方,才形成了特定的光效。

            参考文章[3]给出的一个比较直观的方法是:连接采样点与光源,判断线段是否与场景物体相交。我们知道线段与复杂物体的求交是很复杂的,它意味着我们要遍历所有的三角形。游戏中一般是通过AABB包围盒来简化判断点选,但点选对判断精度要求较低,此处对遮挡关系计算要求较高,否则投射的光线位置会不对。

            

             所以此处采用的是参考文章[2]给出的基于shadow map的方法,它的基本思路是,对于每一个采样点,通过阴影图采样,来判断它是否落在阴影中,这个算法本身比较简单,主要麻烦的在于shadow map的实现。对于聚光灯而言,实现的demo效果如下:

            以上demo的shadow map是基于透视投影的,也就是它只适用于像这样的聚光灯表达。

            在参考文章[1]中,Killzone给出了这么一个效果图:

            要实现以上效果,我们需要使用cube shadowmap,也就是要对点光源周围一圈的遮挡关系在光源位置做采样,并记录到cube texture中,我们才能获得所有的遮挡信息。

            此外,如果想要实现像太阳光这样的平行光(非区域光),我们需要使用基于正交投影,加上cascade shadowmap,即多层级阴影映射,以保证尽可能采样到场景中所有物体的遮挡关系(如下图)。因为一个shadow map能够采样到的范围是有一定限度的。

            以下给出聚光灯的遮挡计算代码:

            写入阴影部分。这里就直接把深度放在一个通道里了,使用透视投影,线性深度。

            vertex shader:

    #version 450 core
    
    uniform mat4 ProjectMatrix;
    uniform mat4 LightMatrix;
    uniform mat4 ModelMatrix;
    
    attribute vec4 a_position;
    
    varying vec2 v_depth;
    
    void main()
    {
        gl_Position = ModelMatrix * a_position;
        gl_Position = LightMatrix * gl_Position;
        v_depth = gl_Position.zw;
        gl_Position = ProjectMatrix * gl_Position;
    }
    

            fragment shader:

    #version 450 core
    
    varying vec2 v_depth;
    uniform float zFar;
    
    void main()
    {
        float fColor =  -(v_depth.x/v_depth.y )/zFar;
        gl_FragColor = vec4(fColor,fColor,fColor,1);
    }
    

            后处理部分,仅做了简单的阴影采样,未做抗锯齿(因为多层采样下不做影响并不大)

            vertex shader:

    #version 450 core
    uniform float fov;
    uniform float zFar;
    uniform float aspect;
    attribute vec4 a_position;
    attribute vec2 a_texcoord;
    
    varying vec2 v_texcoord;
    varying vec2 farPlanePos;
    
    void main()
    {
        gl_Position = a_position;
        v_texcoord = a_texcoord;
    
        float t = tan(fov/2);
        farPlanePos.x = (v_texcoord.x * 2 - 1) * zFar * t*aspect;
        farPlanePos.y = (v_texcoord.y * 2 - 1) * zFar * t;
    }
    

            fragment shader:

    #version 450 core
    
    in vec2 v_texcoord;
    
    uniform vec3 cameraPos;
    uniform sampler2D NormalAndDepth;
    uniform sampler2D ColorTex;
    uniform sampler2D ShadowMap;
    uniform mat4 Inverse_ViewMatrix;
    uniform mat4 LightMatrix;
    uniform mat4 ProjectMatrix;
    uniform float g;
    uniform float zFar;
    uniform float zNear;
    uniform vec3 lightPosition;
    
    varying vec2 farPlanePos;
    
    vec3 yellow_light = vec3(1,198.0/255.0,107.0/255.0);
    
    vec3 ComputeWorldPos(float depth)
    {
        vec4 pos = vec4(vec3(farPlanePos.x, farPlanePos.y,-zFar) * depth , 1);
        vec4 ret = Inverse_ViewMatrix * pos;
        return ret.xyz / ret.w;
    }
    
    bool IsInShadow(vec4 worldPos)
    {
        float fShadow = 0.0;
        vec4 lightPos = (LightMatrix * (worldPos));
        float fDistance = -lightPos.z / zFar;
        lightPos = ProjectMatrix * lightPos;
    
        vec2 uv = lightPos.xy / lightPos.w * 0.5 + vec2(0.5, 0.5);
    
        uv.x = clamp(uv.x, 0, 1);
        uv.y = clamp(uv.y, 0, 1);
    
        float offset = 0.5/zFar;
    
        float distanceMap = texture2D(ShadowMap, uv).r;
        return fDistance - offset > distanceMap;
    }
    
    void main(void)
    {
        vec4 result = texture2D(NormalAndDepth, v_texcoord);
    
        vec3 normal = result.xyz * 2 - 1;
        float depth = result.w;
        vec3 worldPos = ComputeWorldPos(depth);
    
        vec3 total_light;
    
        // 体积光
        {
            float I = 0.0;
            float d = depth * zFar;
            int virtual_plane_num = 100;
            int begin = int(virtual_plane_num * zNear / (d - zNear));
            int end = int(virtual_plane_num * (zFar - d) / (d - zNear));
            for(int j = begin;j <= virtual_plane_num + begin;j++)
            {
                float z = 1.0 * j / (begin + virtual_plane_num + end);
    
                vec3 pos = ComputeWorldPos(z);
                if(z < depth && !IsInShadow(vec4(pos,1)))
                {
                    vec3 lightDis = pos - lightPosition;
                    vec3 viewDis = pos - cameraPos;
    
                    float lightDis2 = lightDis.x * lightDis.x + lightDis.y * lightDis.y  + lightDis.z * lightDis.z;
    
                    vec3 lightDir = normalize(lightDis);
                    vec3 viewDir = normalize(viewDis);
    
                    float cosTheta = dot(lightDir,normalize(-lightPosition));
                    float hg = 1.0/(4*3.14)* (1 - g*g)/ pow(1 + g*g -2*g * dot(lightDir,-viewDir), 1.5);
                    if(cosTheta > 0.9)
                    {
                        I += clamp(10 * hg / virtual_plane_num, 0, 1);
                    }
                }
    
            }
            I = clamp(I,0,1);
            total_light += I * yellow_light;
    
        }
    
        // 平行光源
        {
            vec3 ViewDir = normalize( cameraPos - worldPos );
            vec3 lightDir = normalize(vec3(0.5,1,0.2) );
            vec3 halfDir = normalize(lightDir + ViewDir);
            float diffuse = 0.3 * clamp(dot(normal, lightDir), 0, 1) ;
            vec3 reflectDir = normalize(reflect(-lightDir,normal));
            float specular =  0.3 * pow(clamp(dot(reflectDir,halfDir),0,1),50.0);
            vec3 color = (diffuse + specular) *vec3(1,1,1);
            total_light += color;
        }
    
        float ambient = 0.3;
        vec3 color = vec3(texture2D(ColorTex,v_texcoord));
        gl_FragColor = vec4(total_light + ambient * vec3(1,1,1),1);
    }
    

    优化

            体积光的计算消耗比较大,按照原先的计算,在pc等平台上会有卡顿,而几乎无法用于移动平台。所以,我们需要在此基础上考虑一些优化的方式。

            我们的Ray-match主要包括了两部分内容,一部分是发出多条视线,另一部分是在视线上进行采样。所以我们的优化也可以从两方面着手,一方面是减少发出的视线,一方面是减少某一视线上的采样数,同时保证效果不打太大的折扣。

            网格点计算+硬件线性插值

            参考文章[2]中给出的思路是,仅对屏幕一部分点发出射线计算采样结果,剩余结果通过硬件自带的Gouraud线性插值得到。

            根据论文描述的方法,我猜测它的做法是,在延迟渲染绘制屏幕四边形时,我们一般绘制只有四个顶点、两个三角形组成的全屏四边形即可。而在该方法中,我们根据采样网格精度绘制成有更多三角形的屏幕四边形,如40x40的网格点。

            然后,我们在顶点着色器中,计算网格点处的像素颜色,然后通过硬件插值进而得到屏幕上每个点的像素颜色。它有一个缺陷是,在明暗交界处可能因为采样不够而导致效果错误。原论文中已经给出了不同网格点大小的效果对比。

            半分辨率采样

            为了减少采样数,我们可以将体积光单独渲染到一张纹理,该纹理的宽高均为屏幕大小的一半。为此,我们要做的是:

           (1) 生成屏幕一半大小的纹理,作为体积光写入纹理:

    // ...
     GenerateFrameBuffer(lightBufferTex, lightBuffer,screenX/2,screenY/2);
    //...
    
    
    void RenderCommon::GenerateFrameBuffer(GLuint& texId, GLuint& bufferId,int x, int y)
    {
        glBindFramebuffer(GL_FRAMEBUFFER, bufferId);
    
        glGenTextures(1, &texId);
        glBindTexture(GL_TEXTURE_2D, texId);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F_ARB, x, y, 0, GL_RGB, GL_FLOAT, nullptr);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
        glBindTexture(GL_TEXTURE_2D, 0);
    
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0);
    
    }

            (2) 渲染时,绘制1/4屏幕的四边形,而不是绘制全屏四边形。

            作为对比,这是全屏四边形的顶点数据:

        const int vertexNum = 4;
        VertexData Vertices[vertexNum];
        Vertices[0].position = QVector3D(-1.0f, -1.0f, 0.0f); Vertices[0].texcoord = QVector2D(0,0);
        Vertices[1].position = QVector3D(1.0f,  -1.0f, 0.0f); Vertices[1].texcoord = QVector2D(1,0);
        Vertices[2].position = QVector3D(1.0f,  1.0f,  0.0f); Vertices[2].texcoord = QVector2D(1,1);
        Vertices[3].position = QVector3D(-1.0f, 1.0f,  0.0f); Vertices[3].texcoord = QVector2D(0,1);

           这是半屏四边形的顶点数据:

        const int vertexNum = 4;
        VertexData Vertices[vertexNum];
        Vertices[0].position = QVector3D(-1.0f, -1.0f, 0.0f); Vertices[0].texcoord = QVector2D(0,0);
        Vertices[1].position = QVector3D(0.0f,  -1.0f, 0.0f); Vertices[1].texcoord = QVector2D(1,0);
        Vertices[2].position = QVector3D(0.0f,  0.0f,  0.0f); Vertices[2].texcoord = QVector2D(1,1);
        Vertices[3].position = QVector3D(-1.0f, 0.0f,  0.0f); Vertices[3].texcoord = QVector2D(0,1);

            其余计算和之前保持一致。

            可以看到,使用半分辨率后,(球)边缘有那么模糊:

    1/2分辨率,100步采样

     

    1/4分辨率,100步采样

         

            为了保持边缘锐利,我们可以做一次双边插值。

          使用dither纹理

            此处我们使用参考文章[1]给出的思路,Dithered Ray Marching,使用伪随机在屏幕空间做采样位置的偏移。最后在对图像做一次模糊,如下图所示:

            使用的伪随机偏移纹理为:

           

            经过该操作,最终图像的锯齿感会明显下降,因此我们可以在此基础上降低分辨率或者减少某条光线上的采样点,而不降低渲染图像的质量。

           然后,再做一次模糊。需要用两个pass来完成双边高斯模糊效果。为什么使用两次一维而不是一次二维卷积?我猜测应该是一维的运算速度更快,可以让人忽略两次pass的消耗。

           此外,模糊仅对体积光生效(单独写入一张buffer),不影响场景物体。

           以下是我的测试结果:

    全分辨率,40步采样,无偏移,失真严重
    全分辨率,40步采样,使用偏移,有颗粒感

          

    全分辨率,40次采样,使用偏移,加入模糊

           以下是实现部分:

           因为准备一个纹理比较麻烦,所以此处我直接使用了矩阵,构造方式如下:

    mat4 dither = mat4(
       0,       0.5,    0.125,  0.625,
       0.75,    0.25,   0.875,  0.375,
       0.1875,  0.6875, 0.0625, 0.5625,
       0.9375,  0.4375, 0.8125, 0.3125
    );

            以下是体积光计算的部分:

    float I = 0.0;
    float d = depth * zFar;
    int virtual_plane_num = 40;
    int sampleCoordX = int(mod((ScreenX * v_texcoord.x),4));
    int sampleCoordY = int(mod((ScreenY * v_texcoord.y),4));
    
    int begin = int(virtual_plane_num * zNear / (d - zNear));
    int end = int(virtual_plane_num * (zFar - d) / (d - zNear));
    int total = begin + virtual_plane_num + end;
    float offset = 1.0 / total * dither[sampleCoordX][sampleCoordY];
    for(int j = begin;j <= virtual_plane_num + begin;j++)
    {
        float z = 1.0 * j / total + offset;
    
        vec3 pos = ComputeWorldPos(z);
        if(z < depth && !IsInShadow(vec4(pos,1)))
        {
            vec3 lightDis = pos - lightPosition;
            vec3 viewDis = pos - cameraPos;
    
            vec3 lightDir = normalize(lightDis);
            vec3 viewDir = normalize(viewDis);
    
            float cosTheta = dot(lightDir,normalize(-lightPosition));
            float hg = 1.0/(4*3.14)* (1 - g*g)/ pow(1 + g*g -2*g * dot(lightDir,-viewDir), 1.5);
            if(cosTheta > 0.9)
            {
                I += clamp(10 * hg / virtual_plane_num, 0, 1);
            }
        }
        I = clamp(I,0,1);
        total_light += I * yellow_light;
    }

           模糊部分,为了简化此处只用了一次pass。

    #version 450 core
    
    uniform sampler2D Light;
    uniform int ScreenX;
    uniform int ScreenY;
    varying vec2 v_texcoord;
    
    void main(void)
    {
        const int size = 7;
    
    
        float gauss[] = float[]
        (
            0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067,
            0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292,
            0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117,
            0.00038771, 0.01330373, 0.11098164, 0.22508352, 0.11098164, 0.01330373, 0.00038771,
            0.00019117, 0.00655965, 0.05472157, 0.11098164, 0.05472157, 0.00655965, 0.00019117,
            0.00002292, 0.00078633, 0.00655965, 0.01330373, 0.00655965, 0.00078633, 0.00002292,
            0.00000067, 0.00002292, 0.00019117, 0.00038771, 0.00019117, 0.00002292, 0.00000067
        );
    
    
       vec3 finalColor = vec3(0,0,0);
    
       int idx = 0;
       for(int i = -3;i <= 3;i++)
       {
           for(int j = -3; j <= 3;j++)
           {
               vec2 uv = v_texcoord + vec2(1.0 * i /ScreenX, 1.0 * j /ScreenY);
               vec3 color = texture2D(Light, uv).xyz;
               float weight = gauss[idx];
               finalColor = finalColor + weight * color;
               idx++;
           }
       }
    
       gl_FragColor = vec4(finalColor, 1);
    }
    

     

    展开全文
  • 作为回归的第一篇,来玩一个比较酷炫的效果,一般称之为GodRay,也有人叫它云隙光,当然,最高大上的名字就是体积光散射了。这几个名字对应几种类似的效果,但是实现方式相差甚远。先来几张照片以及其他游戏的截图看...
  • 减小VC编译文件体积

    千次阅读 2008-03-10 15:35:00
    其实情况并不是这样,我们可以通过一些办法有效的减少执行程序的体积。下面通过一个具体的范例给你讲述:1,首先我们打开vc++建立一个Win32 Application,工程名随便,接下来选择“a Typical "hel
  • 在x=a,x=bx=a,x=b之间的曲线y=f(x)y=f(x)绕xx轴旋转产生一个区域,这是一个三维图像。这种对称形状的面积相对比较容易计算。这种情况如图1所示。...此圆盘的体积是我们体积dVdV的元素。因为圆盘是一个
  • 导读:如何科学而优雅地测量一直喵的体积,并不使它被伤害?各位铲屎官,你有想过吗?...要实现这一测猫法,需要一种瞬时确定某点是猫还是非猫的方法,否则在猫运动的情况下,会测得猫扫过的体积而不
  • 长大后做了图形程序,一直想做一个真正的云海出来,但由于移动端的计算瓶颈,一直没能做出一个兼顾性能和效果的体积云(体积云是基于物理的云渲染系统,在游戏中模拟出具有半透明、无规则的表现效果的云)。...
  • 利用ApkPlug插件化框架将ShareSDK组件化,以实现按需动态加载和减小应用体积的目的。 ShareSDK是非常棒的移动社会化分享和评论工具,相信我们在开发应用过程中都离不开它。但是当我们集成ShareSDK时候同样会遇到一些...
  • 安卓减小apk的体积,整体优化代码

    千次阅读 2018-07-20 10:40:03
    在这种情况下,只需使用工具来抑制lint警告:忽略="ContentDescription"属性。注意,对于文本字段,不应该同时设置提示和contentDescription属性,因为提示永远不会显示。只是设置提示。 b、3、Keyboard ...
  • 把猫装进已知体积为V_box的盒子,在盒子内均匀取N个随机点,其中M个在猫体内,猫体积近似为V_box*M/N。
  • 两个圆柱相交的体积 UVALive 5096 Volume

    万次阅读 2016-04-05 14:11:59
    本博客主要讨论从积分和非积分方式计算两个圆柱体交集的体积。由于博主的高数很烂,有什么不得当的地方还望大家指出来。 两个圆柱相交: 不难发现相交有两种情况,如图所示是2R的情况。 我们先暂且不考虑第二种...
  • 如何缩减接近 50% 的 Flutter 包体积

    千次阅读 2019-12-19 13:57:42
    以下是字节跳动移动平台部 Flutter 资深工程师李梦云的分享主题沉淀,《如何缩减接近 50% 的 Flutter 包体积》。 演讲内容大纲: 包体积问题现状 Dart 编译产物优化 Flutter 引擎编译产物优化 机器码指令优化 ...
  • 关于so库 减少体积的事情

    千次阅读 2018-06-11 18:08:47
    作者:Caspar ...来源:知乎 Android 应用开发中不可避免的会引入第三方的代码。如果是开源项目风险相对可控,如果引入商用的 SDK 那就要谨慎了,难免会有这样或那样的问题...正常情况下我们只需要将不同版本的 .so ...
  • 行列式求面积和体积

    千次阅读 2018-02-26 22:43:38
    我的目标,是给出求任意矩阵的公式,但,在此之前,让我们先看三阶矩阵的情况\begin{bmatrix} a & b & c\\ d& e& f\\g & h & i \\ \end{bmatrix}经过前面的一番铺垫后, 你们也不知道接下来要怎么做。。。 让我们...
  • 关于VC6.0的文件体积优化

    千次阅读 2011-07-29 02:37:58
    平时写程序,比较注重的就是文件的大小,当然体积越小越好,不管是存储节省,还是提高效率,都是有好处的。今天突发奇想,去研究研究这方面,假设各种体积优化都是有好处的,那MS为什么不这么做呢?下面就是我的个人...
  • 数学笔记17——定积分的应用2(体积

    千次阅读 多人点赞 2017-11-07 19:44:56
     一条曲线y = f(x),如果曲线绕x轴旋转,则曲线经过的区域将形成一个橄榄球形状的体积,如图所示: 曲线绕x轴旋转一周  现在要计算体积。我们依然按照黎曼和切片的思路去计算,只不过这回需要一点想象力。...
  • 基于LinkMap分析iOSAPP各模块体积

    千次阅读 2018-04-08 18:13:03
    同时,有些动态库封装了所有架构(比如x86_64,arm)的代码,但编译的时候实际打到安装包里的只有当前架构的那部分,那么这部分体积是多少呢?有时候一个模块写了很多方法,但是这些方法都没有被调用到,编译的时候...
  • 一般情况下,产品的反求设计策略有哪些? 【填空题】补体经典激活途径可分为____、____、____三个阶段。 【单选题】鸟 【判断题】补体旁路途径的激活跳过了C1、C4和C2,直接从C3开始。 【多选题】对于初学者,计算机上...
  • 基于2D SDF的体积字实现

    千次阅读 2018-08-31 18:38:10
    对于侧面先走步长再Clip,尽量提前剔除掉,对于视角比较接近正面,厚度不大的情况下只用执行少量几次。_outlineEpsilon是用于微调到达轮廓的条件,_outline表示边界的SDF值,默认0.5。最后用执行for循环的次数给文字...
  • 前言 对于大多数希望使用kiftd来搭建属于自己...因此,本文将通过图文并茂的方式,详细介绍如何在kiftd开源网盘系统中【为账户设定允许上传文件的最大体积】以及【设置默认最大上传文件体积限制】的方法,并用具有...
  • 1 从几件小事说起 ...后来我检查了这几个App的体积,发现每个App体积都是40-50M的样子,这让我很吃惊,因为我记得两年前这些App也就在10-20M的样子。  另一件记忆犹新的事情,是去公园景点游玩,当时公园
  • 对于Android开发来说,关于减少APK体积的重要性,就不再多说了,直接进入正题吧。 首先对于APK瘦身,分为两种:压缩资源,压缩代码 1:移除无用资源和无用代码 移除无用资源分为两种:手动移除和自动移除 手动移除:...
  • 很多新手对这方面的概念都比较模糊,这是我在网上总结来的有关画质、码率、帧数、分辨率、体积的基础编码知识,只要认真看完,基本就对这方面有个全面基础的了解了。   什么是视频编码率?  可以简单的理解为,...
  • 都知道「热胀冷缩」,的确,大部分当物体受冷冻结时(从液体变成固体),它们都会收缩或者变小。这是因为,通常情况下,如果你让物体变热,它所含的分子会更频繁地振动。当振动得更多...
  • APP弱网络条件下,体验优化之道

    万次阅读 2016-07-16 16:47:21
    然而,他们在移动2/3G的网络环境,APP经常会出现Loading很久的情况,这里我把我们所分析与使用到的网络优化方案与大家分享一下。 所谓的弱网络,也就是指在网络不好的条件下进行使用APP,如2G、3G网络,这类网络...
  • 在上周末广州举办的feday中,webpack的核心开发者Sean在介绍webpack插件系统原理时,隆重介绍了一个中国学生于Google夏令营,在导师Tobias带领写的一个webpack插件,webpack-deep-scope-analysis-plugin,这个插件...
  • 与 js 代码分离开来,不要合并,不要合并,因为文件体积同样比较大。 4、 注意 electron 版本号 和 electron-builder 版本号 使用新的 electron 版本打包出来的安装包会比旧版本大几兆,其实很容易理解。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 57,049
精华内容 22,819
关键字:

同样体积的条件下