Cocos Shader丨通过 UV 坐标实现「点击产生水波纹」效果

共 11977字,需浏览 24分钟

 ·

2023-02-18 00:50

在开发中,我们可以通过 UV 坐标实现多种 Shader 效果。本次,Cocos 布道师孙二喵将分享如何用 UV 坐标实现水波纹。Demo 见文末。


什么是 UV 坐标


UV 坐标,也称为纹理坐标,是 3D 建模和游戏开发中的一个重要概念。


在 3D 建模中,我们常通过纹理映射,将一张纹理(也就是图片)黏在模型表面,以控制模型外观。模型的每个顶点都有一个 UV 坐标,定义了该顶点在纹理中对应的 2D 坐标。通过 UV 坐标,我们就可以将 2D 图像上的每一个点精确映射到 3D 模型表面,实现高精度的纹理和贴图效果。


命名为 UV 的原因是 XYZ 已经被用于表示三维空间中对象的坐标轴,为了区分开,所以用了 UV 命名。其中 U 是水平方向坐标轴,V 是垂直方向坐标轴,U 和 V 的范围都是 0 到 1。


Cocos 中的 UV 坐标


在 Cocos 中,UV 坐标系的原点默认在左上角,纹理和图片像素的垂直轴、以及在着色器中对纹理进行采样时,都是下指向的,即从上到下。


这与大多数图像文件格式存储像素数据的方式一致,也与大多数图形 API 的工作方式一致,包括 DirectX、Vulkan、Metal、WebGPU,但 OpenGL 除外。如果你以前的开发经验是基于 OpenGL,在用同样的方式开发 Cocos 游戏的 Shader 时,会发现你的网格上的纹理是垂直翻转的。此时务必要以左上角为 UV 的原点,做一下调整。


UV 坐标系也与 Cocos 中其他地方使用的世界坐标系(Y 轴指向上方,如下图)不一致,通过世界坐标计算 UV 位置的时候也需要注意这个问题。


Cocos 中的世界坐标系


在 Shader 内使用 UV


在 Cocos 中,2D 精灵的 Shader 和 3D Mesh 的 UV 都是在顶点着色器(VS)中获得,并传入像素着色器中(FS)。



在 Cocos 中,3D 的 Shader 默认会乘以平铺 Tilling 系数并加上 Offset 偏移系数,同时支持 RenderTexture 的翻转修复。



我们先来看一个 UV 应用的小效果:


vec4 frag () {
    vec4 col = mainColor * texture(mainTexture, v_uv);

    vec2 uv = v_uv;

    if(uv.y>0.5){
      col.rgb *= 0.5;
    }

    CC_APPLY_FOG(col, v_position);
    return CCFragOutput(col);
  }



当 UV 的 V 大于 0.5 时候,把颜色的 RGB 都乘 0.5,使颜色变暗。动态改变这个数值,就可以实现简单的动画效果:



UV 在 Shader 中的更多应用


使用 UV 坐标可以实现多种 Shader 效果,如帧动画、水波纹、烟雾、火焰等。


帧动画


帧动画的实现,可以参考麒麟子的文章:

https://forum.cocos.org/t/topic/145096


水波纹效果


本次重点分享如何通过 UV 在 2D 精灵和 3D 面片上实现“点击后出现水波纹”的效果。


实现思路大同小异,通过 Sin 函数波动模拟水波,再在 Update 函数内增加波动的范围。


vec2 waveOffset (in vec2 uv0) {
      float waveWidth = 0.25;
      vec2 uv = uv0;
      #if USE_WAVE
      vec2 clickPos = vec2(waveFactor.x-uv0.x,waveFactor.y-uv0.y);
      float dis = sqrt(clickPos.x * clickPos.x + clickPos.y * clickPos.y);
      float discardFactor = clamp(waveWidth - abs(waveFactor.w - dis), 0.01.0)/waveWidth;
      float sinFactor =  sin(dis * 100.0 + waveFactor.z * 10.0) * 0.01;
      vec2 offset = normalize(clickPos) * sinFactor * discardFactor;
      uv += offset;
      #endif
      return uv;
  }


这里简单做了一些优化,UV 在外部提前计算好(为了方便动画效果),使用 1 个 Vec4 进行 Shader 参数设置,减少开销。


2D 精灵上实现水波纹效果


效果演示

@ccclass('spTouchExample')
export class spTouchExample extends Component {

    @property
    waveDis = 0.4;
    @property
    waveSpeed = 1;
    @property
    waveStr = 0.5;
    @property(Node)
    debug: Node = null;

    @property(Label)
    debugText: Label = null;

    public waveProp: Vec4 = new Vec4();

    private _trans: UITransform;
    private _pass: renderer.Pass;
    private _handle: number;

    start() {
        this._trans = this.node.getComponent(UITransform);

        this._pass = this.node.getComponent(Sprite).material.passes[0];

        this.waveProp.w = 100;

        this.waveProp.z = this.waveStr;

        this._handle = this._pass.getHandle("waveFactor");

    }

    onEnable() {

        this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
    }

    onDisable() {

        this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);

    }

    onTouchStart(event: EventTouch) {

        const touch = event.touch;

        touch.getUILocation(v2_0);

        v3_0.set(v2_0.x, v2_0.y);

        this._trans.convertToNodeSpaceAR(v3_0, v3_0);

        this.debug.setPosition(v3_0);

        const size = this._trans.contentSize;

        const x = size.x;

        const y = size.y;

        v4_0.x = (x * 0.5 + v3_0.x) / x;

        v4_0.y = 1 - (y * 0.5 + v3_0.y) / y;

        v4_0.w = 0;

        this.waveProp.set(v4_0);

        this.debugText.string = "Clicked: " + this.node.name + "  UV: " + v4_0.x.toFixed(2) + ", " + v4_0.y.toFixed(2);

    }


    update(dt) {

        if (this.waveProp.w < 100) {

            this.waveProp.w += dt * this.waveSpeed;
            if (this.waveProp.w > this.waveDis) {
                this.waveProp.w = 100;
            }
            this._pass.setUniform(this._handle, this.waveProp);

        }

    }


}


这里在图片节点上添加了点击的监听,把点击的 UI 世界坐标转换成了图片的本地坐标,再用本地坐标和图片的长宽算出 UV 的位置,在 update 里设置材质的 uniform 参数。


3D 面片上实现水波纹效果


效果演示

onTouchStart(event: EventTouch) {
        const touch = event.touch!;
        this.cameraCom.screenPointToRay(touch.getLocationX(), touch.getLocationY(), this._ray);
        this.rayHit();

    }

    /* check model hit */
    rayHit() {
        let distance = 300;
        let mesh: MeshRenderer
        for (let v of this.meshes) {
            let dis = geometry.intersect.rayModel(this._ray, v.model);
            if (dis && dis < distance) {
                distance = dis;
                mesh = v;
            }
        }

        if (mesh) {
            this._ray.computeHit(v3_0, distance);
            const node = mesh.node;
            m4_0.set(node.worldMatrix);
            const halfSize = mesh.model.modelBounds.halfExtents;

            const scale = halfSize.x * 0.1 * node.scale.x;

            this.debug.setWorldPosition(v3_0);

            this.debug.setScale(scale, scale, scale);

            m4_0.invert();
            Vec3.transformMat4(v3_0, v3_0, m4_0)


            if (halfSize.y == 0) {

                const x = halfSize.x;
                const z = halfSize.z;

                v4_0.x = (x + v3_0.x) / (x * 2);
                v4_0.y = (z + v3_0.z) / (z * 2);
            } else {
                const x = halfSize.x;
                const y = halfSize.y;
                v4_0.x = (x + v3_0.x) / (x * 2);
                v4_0.y = (y - v3_0.y) / (y * 2);
            }

            v4_0.w = 0.1;

            const meshCtrl = node.getComponent(meshTouchCtrl);

            meshCtrl.waveProp.set(v4_0);

            this.debugText.string = "Clicked: " + node.name + "  UV: " + v4_0.x.toFixed(2) + ", " + v4_0.y.toFixed(2);

        }
    }


注:这里只针对 UV Mapping 时候平铺的 3D 面片。


这里通过屏幕射线检测点击的位置。



在计算 UV 的时候会有所不同。通过点击位置的世界坐标乘以面片节点的世界矩阵的逆矩阵,把点击位置的世界坐标转换为在面片节点的下的本地坐标。这里还需要点击的物体判断是面片 Quad 还是地面 Plane(Cocos 内置的基础几何体,通过半包围确认),算出点击位置所在的 UV 坐标。


资源链接


  • Demo 源码下载:

https://forum.cocos.org/uploads/short-url/9FaEj4MDNfBPu1b2rEEcqUBWX09.zip


  • 论坛讨论帖:

https://forum.cocos.org/t/topic/145096


需要 Demo 的小伙伴,请将上方的源码链接复制到浏览器,即可自动下载。欢迎前往论坛专贴,一起交流探讨!


未来,我也将定期分享 Cocos Shader 的实际应用,以及相关 Demo 源码,带大家快速上手实现更多漂亮的 Shader 效果!


往期精彩

浏览 38
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报