用shader实现屏幕波纹效果

本文转载自http://blog.csdn.net/puppet_master/article/details/52975666

一.简介


最近在打黑魂,被虐得死去活来,每次都是想砸电脑的节奏,然而还是忍不住想玩,唉。下面步入正题,黑魂的传送技能之前会播放一个帅帅的屏幕特效-屏幕水波,从屏幕中间向四周扩散开来,很有赶脚。下面附上一张截图:

正好最近在学习shader,决定研究一下这种效果怎样实现。

二.原理介绍


水波纹的原理其实比较简单,与其他后处理效果一样,vertex shader千篇一律,所有操作都有fragment shader完成。我们仔细观察一下上图中的效果,其实这种波纹并不是叠加上去的,而是波纹位置的图像被向外拉伸了,而这种拉伸的强度是按照类似sin函数的波形进行拉伸的。
下面我们通过一个场景进行试验,一步一步实现屏幕水波纹的效果,下图是场景原始效果:


1.基本拉伸效果


我们先进行一版简单的拉伸效果实验。
fixed4 frag(v2f_img i) : SV_Target
    {
        //计算uv到中间点的向量(向外扩,反过来就是向里缩)
        float2 dv = float2(0.5, 0.5) - i.uv;
        //归一化
        float2 dv1 = normalize(dv);
        //计算每个像素uv的偏移值
        float2 offset = dv1  * _distanceFactor;
        //像素采样时偏移offset
        float2 uv = offset + i.uv;
        return tex2D(_MainTex, uv); 
    }
效果如下图所示,有一种恐怖的赶脚:

如果我们修改一个地方,将shader中的dv1反向,就会得到一种收缩的效果,如果以后做这种类似的效果,可以考虑用这个实现哈:



2.波纹式拉伸


第一步实现了整体拉伸,但是这并不是我们想要的波纹效果,我们如果给一个线性的输入,通过什么样的函数能够得到类似波纹效果的输出呢?
对了,就是传说中的三角函数,我们通过一个sin值,可以把线性的输入变化成波形的输出,这样就可以模拟了水波纹的效果。知道了用什么函数,函数的输入和输出分别是什么,就是偶们下一步要考虑的问题了。我们上一步中是通过像素采样时uv坐标增加一个偏移值来达到拉伸的效果,我们就可以让这个偏移值作为这个三角函数的输出,这样,有的地方拉伸的少,有的地方拉伸的多,这样就形成了不同的拉伸效果,也就形成了一个波纹的感觉。那么,输入就很明显了,输入就是距离中心位置的绝对距离。上代码:

fixed4 frag(v2f_img i) : SV_Target
    {
        //计算uv到中间点的向量(向外扩,反过来就是向里缩)
        float2 dv = float2(0.5, 0.5) - i.uv;
        //计算像素点距中点的距离
        float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
        //用sin函数计算出波形的偏移值factor
        //dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
        //sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
        float sinFactor = sin(dis * _distanceFactor) * _totalFactor * 0.01;
        //归一化
        float2 dv1 = normalize(dv);
        //计算每个像素uv的偏移值
        float2 offset = dv1  * sinFactor;
        //像素采样时偏移offset
        float2 uv = offset + i.uv;
        return tex2D(_MainTex, uv); 
    }
结果如下:

3.让波纹动起来

有了波纹效果,我们下一步的操作是让波纹动起来,说道动起来,最简单的办法就是随着时间调整某个参数。这个值我们可以从外界传递进来,也可以使用shader中内置的_Time变量。附上一张Unity官方的变量说明,一些常用的shader中特殊内置变量:


我们直接在sin函数的输入中增加一个Time变量,这样,波纹就不是固定不变的效果,而是逐渐向外扩展的效果了。

fixed4 frag(v2f_img i) : SV_Target
    {
        //计算uv到中间点的向量(向外扩,反过来就是向里缩)
        float2 dv = float2(0.5, 0.5) - i.uv;
        //计算像素点距中点的距离
        float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
        //用sin函数计算出波形的偏移值factor
        //dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
        //sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
        float sinFactor = sin(dis * _distanceFactor + _Time.y * _timeFactor) * _totalFactor * 0.01;
        //归一化
        float2 dv1 = normalize(dv);
        //计算每个像素uv的偏移值
        float2 offset = dv1  * sinFactor;
        //像素采样时偏移offset
        float2 uv = offset + i.uv;
        return tex2D(_MainTex, uv); 
    }

好了,这次来一张gif,(折腾了半天,终于用Fraps+迅雷看看播放器做了个GIF图片):


4.怎么把波形变成圆形


首先,我们注意到这里波纹是全屏幕的,会按照屏幕的分辨率进行变化,波纹不是真正的圆形,而是一个椭圆,作为一个强迫症,真是很蛋疼,所以,要处理一下这个问题,其实也很简单,我们在计算distance的时候,按照屏幕的长宽比将dis进行一下缩放就可以了,也就是在我们计算dis之前增加下面一步操作:
 //按照屏幕长宽比进行缩放
        dv = dv * float2(_ScreenParams.x / _ScreenParams.y, 1);
这下,我们的波纹就成了一个真正的圆形,强迫症终于不难受了….



5.让波形从中间从小到大扩散出去


我们已经可以模拟波形效果了,剩下的问题是,我们想像黑魂那种,让波形从画面的中间从小到大,扩散出去,只播放一次,而不是像我们现在这种无限循环的鬼畜。不过这个操作我们直接在shader里面就不好实现了,需要用外面的脚本来进行配合,一种思路是,从外界传递进来一个值,这个值随着时间逐渐增大,只有在距离这个值小于波纹宽度的部分才进行波纹操作,这样就可以模拟波纹从中间产生,然后一点一点向外界扩散的效果了。该部分代码直接在最终的代码中给出,此处只贴上一张原理图:


三.代码实现


上面已经详述了原理以及各种问题,这一部分不多说,直接上代码。

shader部分:
Shader "Custom/WaterWave Effect" 
{
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }

    CGINCLUDE
    #include "UnityCG.cginc"
    uniform sampler2D _MainTex;
    uniform float _distanceFactor;
    uniform float _timeFactor;
    uniform float _totalFactor;
    uniform float _waveWidth;
    uniform float _curWaveDis;

    fixed4 frag(v2f_img i) : SV_Target
    {
        //计算uv到中间点的向量(向外扩,反过来就是向里缩)
        float2 dv = float2(0.5, 0.5) - i.uv;
        //按照屏幕长宽比进行缩放
        dv = dv * float2(_ScreenParams.x / _ScreenParams.y, 1);
        //计算像素点距中点的距离
        float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
        //用sin函数计算出波形的偏移值factor
        //dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
        //sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
        float sinFactor = sin(dis * _distanceFactor + _Time.y * _timeFactor) * _totalFactor * 0.01;
        //距离当前波纹运动点的距离,如果小于waveWidth才予以保留,否则已经出了波纹范围,factor通过clamp设置为0
        float discardFactor = clamp(_waveWidth - abs(_curWaveDis - dis), 0, 1);
        //归一化
        float2 dv1 = normalize(dv);
        //计算每个像素uv的偏移值
        float2 offset = dv1  * sinFactor * discardFactor;
        //像素采样时偏移offset
        float2 uv = offset + i.uv;
        return tex2D(_MainTex, uv); 
    }

    ENDCG

    SubShader 
    {
        Pass
        {
            ZTest Always
            Cull Off
            ZWrite Off
            Fog { Mode off }

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest 
            ENDCG
        }
    }
    Fallback off
}

c#部分:
using UnityEngine;

public class WaterWaveEffect : PostEffectBase {


    //距离系数
    public float distanceFactor = 60.0f;
    //时间系数
    public float timeFactor = -30.0f;
    //sin函数结果系数
    public float totalFactor = 1.0f;

    //波纹宽度
    public float waveWidth = 0.3f;
    //波纹扩散的速度
    public float waveSpeed = 0.3f;

    private float waveStartTime;

    void OnRenderImage (RenderTexture source, RenderTexture destination)
    {
        //计算波纹移动的距离,根据enable到目前的时间*速度求解
        float curWaveDistance = (Time.time - waveStartTime) * waveSpeed;
        //设置一系列参数
        _Material.SetFloat("_distanceFactor", distanceFactor);
        _Material.SetFloat("_timeFactor", timeFactor);
        _Material.SetFloat("_totalFactor", totalFactor);
        _Material.SetFloat("_waveWidth", waveWidth);
        _Material.SetFloat("_curWaveDis", curWaveDistance);

        Graphics.Blit (source, destination, _Material);
    }

    void OnEnable()
    {
        //设置startTime
        waveStartTime = Time.time;
    }
}

注:PsotEffectBase为后处理基类,在 之前的文章中有完整实现,此处不予贴出。


最终结果如下面的gif所示:



四.点击屏幕触发水波纹效果


今天突然想到个好玩的,反正晚上闲着,就加了个点击屏幕触发水波纹的效果。我们可以很容易的知道点击屏幕后触点的坐标,然后把这个坐标转化到(0,1)空间,就可以很容易地对应到屏幕这张RenderTexture上的纹理坐标了,然后我们就可以把上面基于屏幕纹理中心点改成我们点击屏幕后的坐标,这样就实现了点击屏幕触发水波纹的效果。

shader部分:
Shader "Custom/WaterWave Effect" 
{
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }

    CGINCLUDE
    #include "UnityCG.cginc"
    uniform sampler2D _MainTex;
    float4 _MainTex_TexelSize;
    uniform float _distanceFactor;
    uniform float _timeFactor;
    uniform float _totalFactor;
    uniform float _waveWidth;
    uniform float _curWaveDis;
    uniform float4 _startPos;

    fixed4 frag(v2f_img i) : SV_Target
    {
        //DX下纹理坐标反向问题
        #if UNITY_UV_STARTS_AT_TOP
        if (_MainTex_TexelSize.y < 0)
            _startPos.y = 1 - _startPos.y;
        #endif
        //计算uv到中间点的向量(向外扩,反过来就是向里缩)
        float2 dv = _startPos.xy - i.uv;
        //按照屏幕长宽比进行缩放
        dv = dv * float2(_ScreenParams.x / _ScreenParams.y, 1);
        //计算像素点距中点的距离
        float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
        //用sin函数计算出波形的偏移值factor
        //dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
        //sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
        float sinFactor = sin(dis * _distanceFactor + _Time.y * _timeFactor) * _totalFactor * 0.01;
        //距离当前波纹运动点的距离,如果小于waveWidth才予以保留,否则已经出了波纹范围,factor通过clamp设置为0
        float discardFactor = clamp(_waveWidth - abs(_curWaveDis - dis), 0, 1) / _waveWidth;
        //归一化
        float2 dv1 = normalize(dv);
        //计算每个像素uv的偏移值
        float2 offset = dv1  * sinFactor * discardFactor;
        //像素采样时偏移offset
        float2 uv = offset + i.uv;
        return tex2D(_MainTex, uv); 
    }

    ENDCG

    SubShader 
    {
        Pass
        {
            ZTest Always
            Cull Off
            ZWrite Off
            Fog { Mode off }

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest 
            ENDCG
        }
    }
    Fallback off
}
C#部分:
using UnityEngine;

public class WaterWaveEffect : PostEffectBase {


    //距离系数
    public float distanceFactor = 60.0f;
    //时间系数
    public float timeFactor = -30.0f;
    //sin函数结果系数
    public float totalFactor = 1.0f;

    //波纹宽度
    public float waveWidth = 0.3f;
    //波纹扩散的速度
    public float waveSpeed = 0.3f;

    private float waveStartTime;
    private Vector4 startPos = new Vector4(0.5f, 0.5f, 0, 0);


    void OnRenderImage (RenderTexture source, RenderTexture destination)
    {
        //计算波纹移动的距离,根据enable到目前的时间*速度求解
        float curWaveDistance = (Time.time - waveStartTime) * waveSpeed;
        //设置一系列参数
        _Material.SetFloat("_distanceFactor", distanceFactor);
        _Material.SetFloat("_timeFactor", timeFactor);
        _Material.SetFloat("_totalFactor", totalFactor);
        _Material.SetFloat("_waveWidth", waveWidth);
        _Material.SetFloat("_curWaveDis", curWaveDistance);
        _Material.SetVector("_startPos", startPos);
        Graphics.Blit (source, destination, _Material);
    }

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Vector2 mousePos = Input.mousePosition;
            //将mousePos转化为(0,1)区间
            startPos = new Vector4(mousePos.x / Screen.width, mousePos.y / Screen.height, 0, 0);
            waveStartTime = Time.time;
        }

    }
}

结果:



PS:关于点击屏幕会出现水波位置Y轴反向的问题已经修正。在DX下由于uv纹理坐标与GL下Y轴反向,Unity会为我们处理大部分情况,但是当开启后处理,并且使用了抗锯齿的时候,直接从外界传来的屏幕空间坐标转化到输入的RT中时,Unity自然不会为我们处理得这么详尽,就可能出现想要的效果Y方向上反向的问题。所以Unity提供了一个#if UNITY_UV_STARTS_AT_TOP宏,我们在shader中增加这个编译宏,就可以了。关于#if UNITY_UV_STARTS_AT_TOP宏,可以参考 这篇文章


实现环形波纹效果,可以使用OpenGL的着色器语言GLSL来编着色器程序。具体的实现步骤如下: 1. 定义顶点着色器程序,将顶点坐标传递给片段着色器。 ``` #version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos, 1.0); } ``` 2. 定义片段着色器程序,根据顶点坐标计算出每个像素点的颜色值。 ``` #version 330 core out vec4 FragColor; uniform float time; uniform vec2 center; // 波纹中心 uniform float radius; // 波纹半径 uniform float strength; // 波纹强度 void main() { vec2 uv = gl_FragCoord.xy / vec2(800, 600); // 屏幕坐标系转换到纹理坐标系 float dist = length(uv - center); if (dist < radius) { float alpha = (radius - dist) * strength; float offset = time * 2.0; float angle = atan(uv.y - center.y, uv.x - center.x) + offset; uv.x += alpha * cos(angle); uv.y += alpha * sin(angle); } FragColor = texture(myTexture, uv); } ``` 在片段着色器中,我们定义了一些常量和变量来控制波纹的中心、半径和强度。根据像素点到波纹中心的距离,可以计算出波纹的强度,然后通过一个偏移量来控制波纹的运动方向和速度。最后,将波纹的偏移量应用到纹理坐标上,从而实现波纹效果。 3. 在主程序中,创建一个帧缓冲对象,并将片段着色器渲染的结果绘制到屏幕上。 ``` // 创建帧缓冲对象 unsigned int framebuffer; glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); // 创建纹理附件 unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); // 检查帧缓冲是否完整 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl; // 渲染循环 while (!glfwWindowShouldClose(window)) { float time = glfwGetTime(); // 渲染到帧缓冲 glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glViewport(0, 0, 800, 600); glClear(GL_COLOR_BUFFER_BIT); shader.use(); shader.setFloat("time", time); shader.setVec2("center", vec2(0.5, 0.5)); shader.setFloat("radius", 0.3); shader.setFloat("strength", 0.1); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 6); // 渲染到屏幕 glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, 800, 600); glClear(GL_COLOR_BUFFER_BIT); screenShader.use(); glBindVertexArray(screenVAO); glBindTexture(GL_TEXTURE_2D, texture); glDrawArrays(GL_TRIANGLES, 0, 6); glfwSwapBuffers(window); glfwPollEvents(); } ``` 在主程序中,我们创建了一个帧缓冲对象,并将片段着色器渲染的结果绘制到帧缓冲对象中。然后再将帧缓冲对象中的纹理绘制到屏幕上。这样就可以实现环形波纹效果了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值