为什么 shader 效果在编辑器中显示正常,运行后却显示异常?合图后 uv 坐标计算

微信公众号:
二维码

感谢QQ群(521643513)内 honmono 大佬的指导

01

uv 坐标

之后的文章中,我们再详细介绍顶点着色器,片段着色器,纹理等知识

这里简单了解下基本概念

渲染流程:

我们传递给顶点着色器每个顶点坐标及其对应的纹理坐标和纹理颜色后,顶点着色器经过顶点变换,图元装配(将顶点根据原始的连接关系还原成网格结构),光栅化(通过对顶点数据进行插值,获得三角形所覆盖的像素区域)等操作后,得到片元序列(二维图像上每个点都包含了颜色、深度和纹理数据,将该点和相关信息叫做一个片元),然后将片元传递给片元着色器,它会为每个片元进行纹理采样,颜色填充等操作
图片

其中纹理坐标即 uv 坐标

uv 坐标是纹理尺寸的的百分比坐标

在 cocos 中,uv 坐标的原点在左上角,u 轴向右,v 轴向下,范围是 0-1

[image]

02

效果对比

demo 中用了两个 shader,分别是水波和圆角

在编辑器中的表现正常:
图片

而运行后,在浏览器中的表现却不是我们预期的效果

小羊的水波效果边缘出现了其他纹理

小天使的圆角效果只有左上角实现了,但被放大了好多倍

03

原因分析

为了降低 DrawCall,cocos 提供了两种合图机制

· 自动合图 (Auto Atlas)

在项目构建时的静态合图方法

· 动态合图 (Dynamic Atlas)

在项目运行时动态的将贴图合并到一张大贴图中

当渲染一张贴图的时候,动态合图系统会自动检测这张贴图是否已经被合并到了图集(图片集合)中,如果没有,并且此贴图又符合动态合图的条件,就会将此贴图合并到图集中

动态合图是按照 渲染顺序 来选取要将哪些贴图合并到一张大图中的,这样就能确保相邻的 DrawCall 能合并为一个 DrawCall(又称“合批”)

也就是说,合图后,该纹理会被放到一个较大的纹理中:

运行时,cocos 会将合图后的纹理传递给 shader,即 shader 中 texture:

uniform sampler2D texture;

同时 cocos 也会帮我们重新计算该纹理在合图纹理中的 uv 坐标,然后将此 uv 坐标传递给 shader,即 shader 中的 v_uv0:

in vec2 v_uv0;

shader 中使用 v_uv0 参与计算的公式,因为 v_uv0 的改变,计算结果也会随之改变,所以,在运行后,我们看到的效果就和在编辑器中不一致了

举个例子

对椰子头来说,假如合图前传入的 uv 坐标为(0.0, 0.0),即纹理的左上角,而经过合图后,椰子头左上角的 uv 坐标就会变成(0.3, 0.0),即为合图中的纹理的 uv 坐标

计算 uv 坐标的相关源码在 CCSpriteAtlas.js 中:

源码路径:

resources\engine\cocos2d\core\assets\CCSpriteFrame.js

_calculateUV:

_calculateUV () {
    // SpriteFrame 的纹理矩形区域 
    let rect = this._rect,
        // 获取使用的纹理实例,如果是合图的话,即合图的纹理
        texture = this._texture,
        // SpriteFrame 的 uv 数组
        uv = this.uv,
        // 使用纹理的宽度
        texw = texture.width,
        // 使用纹理的高度
        texh = texture.height;

    // SpriteFrame 是否旋转 
    if (this._rotated) {
        let l = texw === 0 ? 0 : rect.x / texw;
        let r = texw === 0 ? 0 : (rect.x + rect.height) / texw;
        let b = texh === 0 ? 0 : (rect.y + rect.width) / texh;
        let t = texh === 0 ? 0 : rect.y / texh;
        uv[0] = l;
        uv[1] = t;
        uv[2] = l;
        uv[3] = b;
        uv[4] = r;
        uv[5] = t;
        uv[6] = r;
        uv[7] = b;
    }
    else {
        // SpriteFrame 的纹理矩形区域的左上角像素坐标(即在使用纹理中的像素坐标)为(rect.x, rect.y)
        // uv 坐标的原点在左上角
        // 通过与使用纹理的宽高计算得到四个边界坐标
        // 四个边界坐标均为相对于合图纹理中的百分比坐标
        
        // xMin
        let l = texw === 0 ? 0 : rect.x / texw;
        // xMax
        let r = texw === 0 ? 0 : (rect.x + rect.width) / texw;
        // yMax
        let b = texh === 0 ? 0 : (rect.y + rect.height) / texh;
        // yMin
        let t = texh === 0 ? 0 : rect.y / texh;
        
        // 左下角
        uv[0] = l;
        uv[1] = b;
        
        // 右下角
        uv[2] = r;
        uv[3] = b;

        // 左上角
        uv[4] = l;
        uv[5] = t;

        // 右上角
        uv[6] = r;
        uv[7] = t;
    }

    // SpriteFrame 是否翻转 x 轴
    if (this._flipX) {
        let tempVal = uv[0];
        uv[0] = uv[2];
        uv[2] = tempVal;

        tempVal = uv[1];
        uv[1] = uv[3];
        uv[3] = tempVal;

        tempVal = uv[4];
        uv[4] = uv[6];
        uv[6] = tempVal;

        tempVal = uv[5];
        uv[5] = uv[7];
        uv[7] = tempVal;
    }

    // SpriteFrame 是否翻转 y 轴
    if (this._flipY) {
        let tempVal = uv[0];
        uv[0] = uv[4];
        uv[4] = tempVal;

        tempVal = uv[1];
        uv[1] = uv[5];
        uv[5] = tempVal;

        tempVal = uv[2];
        uv[2] = uv[6];
        uv[6] = tempVal;

        tempVal = uv[3];
        uv[3] = uv[7];
        uv[7] = tempVal;
    }

    let vertices = this.vertices;
    if (vertices) {
        vertices.nu.length = 0;
        vertices.nv.length = 0;
        for (let i = 0; i < vertices.u.length; i++) {
            vertices.nu[i] = vertices.u[i]/texw;
            vertices.nv[i] = vertices.v[i]/texh;
        }
    }

    this._calculateSlicedUV();
}

经过 _calculateUV() 计算后,SpriteFrame 的 uv 存放的便是该纹理在合图纹理中的 uv 坐标

04

解决方案

1关闭动态合图

通过代码关闭动态合图功能,相应的,可能会增加 DrawCall

onLoad() {
    cc.dynamicAtlasManager.enabled = false;
}

2 禁止贴图参与合图

在纹理的属性便面中关闭 Packable(是否允许贴图参与合图),依然可能会增加 DrawCall

图片

3动态计算

通过之前的源码,我们可以得知纹理在合图中的四个边界 uv 坐标,通过四个边界坐标,可以计算出归一化的 uv 坐标

在脚本组件中获取纹理的边界 uv 坐标及旋转状态,传递给 shader

let frame = this.sprite.spriteFrame;
// xMin
let l = frame.uv[0];
// xMax
let r = frame.uv[6];
// yMax
let b = frame.uv[3];
// yMin
let t = frame.uv[5];
// 纹理在合图中的四个边界 uv 坐标
let u_uvOffset = new cc.Vec4(l, t, r, b);
// 纹理是否旋转
let u_uvRotated = frame.isRotated() ? 1.0 : 0.0;
// 设置材质的属性
this.material.setProperty("u_uvOffset", u_uvOffset);
this.material.setProperty("u_uvRotated", u_uvRotated);

在 shader 的片元着色器中,通过当前的 uv 坐标(v_uv0)与纹理在合图中的偏移坐标,计算出归一化后的 uv 坐标


vec2 uvNormalize;
uvNormalize.x = (v_uv0.x - u_uvOffset.x) / (u_uvOffset.z - u_uvOffset.x);
uvNormalize.y = (v_uv0.y - u_uvOffset.y) / (u_uvOffset.w - u_uvOffset.y);
if(u_uvRotated > 0.5)
{
    float temp = uvNormalize.x;
    uvNormalize.x = uvNormalize.y;
    uvNormalize.y = 1.0 - temp;
}

uvNormalize 即为归一化的 uv 坐标,也就是纹理在合图前的 uv 坐标

使用该坐标完成 shader 中对纹理坐标的 计算

但当获取纹理的色值时,依然需要使用 v_uv0,因为 shader 中的纹理(texture)为合图纹理,v_uv0 才是该纹理在合图纹理中正确的 uv 坐标

源码获取,请在公众号回复:

合图 uv 坐标

17赞

鸦哥 yyds

鸦哥 生日快乐

1赞

鸭哥 加油

@valiancer 鸭哥,你这个圆角头像上下左右的边缘稍微有一点点被切了哦,这个怎么搞成圆滑一点呢?

旋转信息呢,是uv信息的第几个参数来着… :joy:

我刚发现·····谢谢 :revolving_hearts:

救命了!!!大神牛逼!!!!

是不是 自动合图了

写了个旋转的shader,结果网页版和预览的效果一直对不上,找了半天原因,也是因为合图