欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

程序员文章站 2022-07-12 23:31:09
...

以前龙之谷喜欢选战士,帅气。
战士的武器在甩动过程中会有扭曲拖尾。
自己测试项目中想给武器也添加这效果,所以顺便学习以下。

先来看看效果

Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

还可以给拖尾添加着色、亮度、和一些扭曲强度的参数设置。
Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

实现思路

  • 生成拖尾网格
  • 编写热扭曲shader处理

生成拖尾网格

在此前,我用过unity的TrailRenderer
Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

但是对我来说不好用,因为我需要将拖尾的头部可以与我的武器的位置吻合,如果用TrailRenderer挂到武器上,就算怎么调好起始对好的位置,运行后,你就会发现各种对不准,因为TrailRenderer是只一个坐标为对准拖尾头部的。

看看unity自带的制作效果
Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

效果不理想啊,所以啊,没办法下,就自己动手写个实时生成拖尾网格的脚本。

只要对齐三个坐标位置即可,原来只要两个坐标的,但是不好做拖尾边缘平滑过渡处理,所以调整为三个坐标。

生成网格,就是根据三个点,移动超过一定距离,就记录三个点的位置到一个段列表中。

段列表:每一段数据都记录点,与过去时间的信息。

    // 拖尾段数据
    public class TrailSegment
    {
        public Vector3 pos1;        // 三坐标记录
        public Vector3 pos2;
        public Vector3 pos3;
        public float elapsedTime;   // 保持不变时长,记录:已用时(秒)
        public float fadeTime;      // 当elapsedTime >= duration 时,将开始记录fade time,就是淡出的时间已用时(秒)
    }

然后update中遍历位置段列表的每一项。

根据每一段的前后数据,生成网格。

再根据时间更新网格位置,颜色,等参数即可。

线框模式查看过程

Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

放慢一些时间,方便观察
Unity Shader - 使用GrabPass{"name"}实现武器热扭曲拖尾效果

CSharp

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// authro      :   jave.lin
/// date        :   2020.03.06
/// 拖尾脚本
/// </summary>
public class TrailScript : MonoBehaviour
{
    // 拖尾段数据
    public class TrailSegment
    {
        public Vector3 pos1;        // 三坐标记录
        public Vector3 pos2;
        public Vector3 pos3;
        public float elapsedTime;   // 保持不变时长,记录:已用时(秒)
        public float fadeTime;      // 当elapsedTime >= duration 时,将开始记录fade time,就是淡出的时间已用时(秒)
    }

    public Transform trans1;        // 拖尾头的三个挂点
    public Transform trans2;
    public Transform trans3;

    public float duration = 1;      // 拖尾段保持不变的时长(秒)
    public float fadeOut = 1;       // 拖尾段保持不变时长到时后,开始淡出的时长(秒)
    public Color startColor;        // 
    public Color endColor;

    public Texture2D tex;
    public Material mat;

    public Camera cam;

    public bool emit;
    public float emitDistance = 0.1f;

    private List<TrailSegment> segmentList;

    private Vector3[] vertics;      // world position
    private int[] indices;
    //private Vector2[] uvs;
    private Color[] colors;

    private MeshRenderer meshRender;
    private MeshFilter meshFilter;

    private Mesh mesh;

    private Vector3 lastPos1;
    private Vector3 lastPos2;
    private Vector3 lastPos3;

    private void Start()
    {
        segmentList = new List<TrailSegment>();
        mesh = new Mesh();
        meshRender = gameObject.AddComponent<MeshRenderer>();
        meshFilter = gameObject.AddComponent<MeshFilter>();
        meshFilter.mesh = mesh;
        meshRender.material = mat;
        mesh.MarkDynamic(); // 文档说是底层会加速处理那些频繁更新网格信息时使用 https://docs.unity3d.com/ScriptReference/Mesh.MarkDynamic.html

        lastPos1 = trans1.position;
        lastPos2 = trans2.position;
        lastPos3 = trans3.position;
    }

    private void Update()
    {
        if (emit)
        {
            var deltaPos1 = trans1.position - lastPos1;
            var deltaPos2 = trans2.position - lastPos2;
            var deltaPos3 = trans3.position - lastPos3;

            if ((!IsZero(deltaPos1) && deltaPos1.magnitude > emitDistance) ||
                (!IsZero(deltaPos2) && deltaPos2.magnitude > emitDistance) ||
                (!IsZero(deltaPos3) && deltaPos3.magnitude > emitDistance))
            {
                segmentList.Add(new TrailSegment { pos1 = trans1.position, pos2 = trans2.position, pos3 = trans3.position });
                lastPos1 = trans1.position;
                lastPos2 = trans2.position;
                lastPos3 = trans3.position;
            }
        }

        var count = segmentList.Count;
        var offset = 0;
        TrailSegment curSeg = null;
        TrailSegment nextSeg = null;

        if (segmentList.Count > 1)
        {
            // 更新追后一个段的位置为:当前最新的拖尾头的位置
            var lastOne = segmentList[segmentList.Count - 1];
            lastOne.pos1 = trans1.position;
            lastOne.pos2 = trans2.position;
            lastOne.pos3 = trans3.position;
        }

        if (segmentList.Count > 0)
        {
            vertics = new Vector3[count * 3];               // 顶点
            indices = new int[(count) * (4 * 3)];           // 索引
            //uvs = new Vector2[count * 3];                 // uv,暂时不用
            colors = new Color[count * 3];                  // 颜色

            var w2lMatrix = transform.worldToLocalMatrix;   // 世界坐标转本地坐标的矩阵

            do
            {
                curSeg = segmentList[offset];

                var etT = Mathf.Clamp01(curSeg.elapsedTime / duration);
                var fadeT = Mathf.Clamp01(curSeg.fadeTime / fadeOut);

                // 要靠近的左边
                var closeToPos = (offset == (segmentList.Count - 1) ? trans2.position : segmentList[offset + 1].pos2);
                vertics[offset * 3 + 0] = w2lMatrix.MultiplyPoint(Vector3.Lerp(curSeg.pos1, closeToPos, fadeT));
                vertics[offset * 3 + 1] = w2lMatrix.MultiplyPoint(Vector3.Lerp(curSeg.pos2, closeToPos, fadeT));
                vertics[offset * 3 + 2] = w2lMatrix.MultiplyPoint(Vector3.Lerp(curSeg.pos3, closeToPos, fadeT));

                nextSeg = (offset + 1) < segmentList.Count ? segmentList[offset] : null;
                if (nextSeg != null)
                {
                    indices[offset * 4 * 3 + 0] = offset * 3 + 0;
                    indices[offset * 4 * 3 + 1] = offset * 3 + 1;
                    indices[offset * 4 * 3 + 2] = (offset + 1) * 3 + 1;     // next seg

                    indices[offset * 4 * 3 + 3] = offset * 3 + 0;
                    indices[offset * 4 * 3 + 4] = (offset + 1) * 3 + 1;     // next seg
                    indices[offset * 4 * 3 + 5] = (offset + 1) * 3 + 0;     // next seg

                    indices[offset * 4 * 3 + 6] = offset * 3 + 1;
                    indices[offset * 4 * 3 + 7] = offset * 3 + 2;
                    indices[offset * 4 * 3 + 8] = (offset + 1) * 3 + 2;     // next seg

                    indices[offset * 4 * 3 + 9] = offset * 3 + 1;
                    indices[offset * 4 * 3 + 10] = (offset + 1) * 3 + 2;    // next seg
                    indices[offset * 4 * 3 + 11] = (offset + 1) * 3 + 1;    // next seg
                }
                //uvs[offset * 3 + 0] = new Vector2(etT, 0.0f);
                //uvs[offset * 3 + 1] = new Vector2(etT, 0.5f);
                //uvs[offset * 3 + 2] = new Vector2(etT, 1.0f);

                // rgb作为过渡颜色,a作为控制扭曲强度
                Color c1 = startColor;
                Color c2 = endColor;
                Color c3 = Color.Lerp(c1, c2, etT);
                c3.a = 0.8f;                        // 让第一个挂点的alpha值大一些,所以我们第一个点通常挂在武器最远端
                colors[offset * 3 + 0] = c3;
                c3.a = 1 - fadeT;                   // 中间点,根据淡出时间做alpha
                colors[offset * 3 + 1] = c3;
                c3.a = 0;
                colors[offset * 3 + 2] = c3;
            } while (++offset < count);
            mesh.Clear();
            mesh.vertices = vertics;
            mesh.triangles = indices;
            //mesh.uv = uvs;
            mesh.colors = colors;
            //mesh.RecalculateBounds();
            //Graphics.DrawMesh(mesh, Matrix4x4.identity, mat, gameObject.layer, cam);
            int fadeOutCount = 0;
            offset = 0;
            while (offset < count)
            {
                curSeg = segmentList[offset];
                var dt = Time.deltaTime;
                curSeg.elapsedTime += dt;
                if (curSeg.elapsedTime >= duration)
                {
                    curSeg.fadeTime = curSeg.elapsedTime - duration;
                    if (curSeg.fadeTime >= fadeOut) fadeOutCount++;
                }
                offset++;
            }
            segmentList.RemoveRange(0, fadeOutCount);
        }
    }
    private bool IsZero(Vector3 v) => v.x == 0 && v.y == 0 && v.z == 0;
}

Shader

// jave.lin 2020.03.06
Shader "Custom/DistortionTrail" {
    Properties {
        _NoiseTex ("NoiseTex", 2D) = "white" {}                                 // 噪点图
        _Brightness ("Brightness", Range(0, 1)) = 0                             // 拖尾亮度
        _DistortionIntensity ("DistortionIntensity", Range(0, 10)) = 1          // 扭曲强度
        _DistortionFrequency ("DistortionFrequency", Range(1, 20)) = 1          // 扭曲变化频率
    }
    SubShader {
        /*
        Opaque: 用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。
        Transparent:用于半透明着色器(透明着色器、粒子着色器、字体着色器、地形额外通道的着色器)。
        TransparentCutout: 蒙皮透明着色器(Transparent Cutout,两个通道的植被着色器)。
        Background: Skybox shaders. 天空盒着色器。
        Overlay: GUITexture, Halo, Flare shaders. 光晕着色器、闪光着色器。
        TreeOpaque: terrain engine tree bark. 地形引擎中的树皮。
        TreeTransparentCutout: terrain engine tree leaves. 地形引擎中的树叶。
        TreeBillboard: terrain engine billboarded trees. 地形引擎中的广告牌树。
        Grass: terrain engine grass. 地形引擎中的草。
        GrassBillboard: terrain engine billboarded grass. 地形引擎何中的广告牌草。
        */
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }
        LOD 100
        GrabPass { "_wpTrailTexture" }
        Pass {
            ZWrite Off
            Cull Off
            Lighting Off
            Fog { Mode Off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                fixed4 color : COLOR;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 grabPos : TEXCOORD1;
                fixed4 color : COLOR0;
            };
            sampler2D _wpTrailTexture;
            sampler2D _NoiseTex;
            float4 _NoiseTex_ST;
            fixed _Brightness;
            fixed _DistortionIntensity;
            fixed _DistortionFrequency;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _NoiseTex);
                o.grabPos = ComputeGrabScreenPos(o.vertex);
                o.color = v.color;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.grabPos.xy /= i.grabPos.w;
                float intensity = ((tex2D(_NoiseTex, i.uv).r * 2) - 1) * 0.1;
                fixed4 col = tex2D(_wpTrailTexture, i.grabPos.xy + intensity * i.color.a * sin(_Time.w * _DistortionFrequency) * _DistortionIntensity);
                col.rgb = lerp(col.rgb, col.rgb * i.color.rgb + _Brightness * col.rgb, i.color.a);
                return col;
            }
            ENDCG
        }
    }
}

注意性能

之前写过一篇 GrabPass 注意的问题
手机上的话,还是得用另一种方式来实现。

Project

备份记录
UnityShader_WeaponDirtortionTrailEffectTesting_2018.3.0f2