科幻能量盾,技术美术教程

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!

一、演示效果

 

视频链接:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/1.mp4

 

二、搭建工程

 

新建工程,安装Universal RP,我的Unity版本是2022.2。

 

记得勾选DepthTexture和OpaqueTexture,之后会用到。

 

三、新建Shader

 

 

 

添加UV、法线等信息,用于后续计算:

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
};

struct v2f
{
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD1;
    float3 normal : TEXCOORD2;
    float3 worldPos : TEXCOORD3;
    float4 screenPos : TEXCOORD4;
    float4 localPos : TEXCOORD5;
};

v2f vert (appdata v)
{
    v2f o;
    o.vertex = TransformObjectToHClip(v.vertex.xyz);
    o.uv = v.uv;
    o.normal = TransformObjectToWorldNormal(v.normal);
    o.worldPos = TransformObjectToWorld(v.vertex.xyz);
    o.localPos = v.vertex;
    o.screenPos = o.vertex;
    #if UNITY_UV_STARTS_AT_TOP
    o.screenPos.y *= -1;
    #endif
    return o;
}

  

四、边缘光

 

先添加边缘光,用模型法线和观察方向做点乘:

//Properties
_RimPower ("RimPower", Float) = 1
[HDR] _RimColor ("RimColor", Color) = (1, 1, 1, 1)

float _RimPower;
float4 _RimColor;

//frag
float3 normal = normalize(i.normal);
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
float ndv = dot(normal, viewDir);
if(ndv < 0) {
    ndv = abs(ndv);
}
ndv = 1 - ndv;

float rimIntensity = pow(ndv, _RimPower);
finalColor += _RimColor * rimIntensity;
finalColor.a = saturate(finalColor.a);

  

 

发觉忘记开Bloom了。

 

五、接触高亮

 

能量盾和其他物体接触时,需要有亮边,通过深度来实现。用盾像素点的屏幕坐标采样深度图,得到场景深度,再与像素点深度做比较,当两者足够接近时,显示亮边:

//Properties
_IntersectionWidth ("IntersectionWidth", Float) = 1
[HDR] _IntersectionColor ("IntersectionColor", Color) = (1, 1, 1, 1)

float _IntersectionWidth;
float4 _IntersectionColor;

sampler2D _CameraDepthTexture;

//frag
i.screenPos.xyz /= i.screenPos.w;
float2 screenUV = i.screenPos.xy;
screenUV = (screenUV + 1) / 2;

float selfZ = i.screenPos.z;
float sceneZ = tex2D(_CameraDepthTexture, screenUV).r;
float linearSelfZ = LinearEyeDepth(selfZ, _ZBufferParams);
float linearSceneZ = LinearEyeDepth(sceneZ, _ZBufferParams);
float zDifference = linearSceneZ - linearSelfZ;
if(zDifference < _IntersectionWidth) {
    float intersectionIntensity = (1 - zDifference / _IntersectionWidth);
    intersectionIntensity = saturate(intersectionIntensity);
    intersectionIntensity = pow(intersectionIntensity, 4);
    finalColor += _IntersectionColor * intersectionIntensity;
    finalColor.a = saturate(finalColor.a);
}

  

 

六、贴图

 

接下来给能量盾添加贴图,球的UV是不均匀的,如果直接用UV采样贴图,贴图在顶部会被压缩,在中间区域会被拉伸。

这里我把2D贴图合成了Cubemap,用法线采样,并且判断了像素点是否是背面,如果是,则不显示贴图:

 

 

//Properties
_PatternTex ("PatternTex", Cube) = "white" {}
_PatternPower ("PatternPower", Float) = 1
[HDR] _PatternColor ("PatternColor", Color) = (1, 1, 1, 1)

samplerCUBE _PatternTex;
float _PatternPower;
float4 _PatternColor;

//frag
int isFrontFace = 1;
//......
if(ndv < 0) {
    isFrontFace = 0;
}

float patternIntensity = texCUBE(_PatternTex, normal).a * isFrontFace;
patternIntensity *= pow(ndv, _PatternPower);
finalColor += patternIntensity * _PatternColor;
finalColor.a = saturate(finalColor.a);

  

 

接下来给贴图添加流动效果,用一张网格遮罩和贴图做叠加:

 

 

//Properties
_Mask ("Mask", 2D) = "black" {}
[HDR] _MaskColor ("MaskColor", Color) = (1, 1, 1, 1)

sampler2D _Mask;
float4 _Mask_ST;
float4 _MaskColor;

//frag
float mask = 0;
mask += tex2D(_Mask, i.uv * _Mask_ST.xx + _Mask_ST.zz * _Time.y).a;
mask += tex2D(_Mask, i.uv * _Mask_ST.yy + _Mask_ST.ww * _Time.y).a;
mask = saturate(mask);
finalColor += patternIntensity * mask * _MaskColor;
finalColor.a = saturate(finalColor.a);

  

视频演示1:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/12.mp4

 

视频演示2:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/13.mp4

 

七、溶解

 

接下来制作溶解效果,用像素点的y坐标控制溶解,再叠加噪声做不规则的轮廓:

//Properties
_Noise ("Noise", 2D) = "white" {}

_DissolveThreshold ("DissolveThreshold", Float) = 1
_DissolveWidth ("DissolveWidth", Float) = 0.1
[HDR] _DissolveColor ("DissolveColor", Color) = (1, 1, 1, 1)

sampler2D _Noise;
float4 _Noise_ST;

float _DissolveThreshold;
float _DissolveWidth;
float4 _DissolveColor;

//frag
if(i.localPos.y > _DissolveThreshold) {
    discard;
}
else if(i.localPos.y > _DissolveThreshold - _DissolveWidth) {
    float t = (i.localPos.y - _DissolveThreshold + _DissolveWidth) / _DissolveWidth;
    float noise = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw * _Time.y);
    noise = lerp(1, noise * (1 - t), pow(t, 0.5));
    if(noise > 0.5) {
        finalColor = _DissolveColor;
    }else {
        discard;
    }
}

  

视频演示:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/14.mp4 

 

八、颜色交互

 

接下来制作最复杂的交互功能,思路是用脚本把交互点、交互半径和交互颜色传入材质球,然后在Shader中计算像素点和交互点的距离,如果在半径内则显示相应的颜色。

新建Shield.cs脚本,这个脚本负责传递交互信息到材质球:

public class Shield : MonoBehaviour {  
    private class InteractionData {
        public Color color;
        public Vector3 interactionStartPos;
        public float timer;
    }

    public List<Material> materials;

    private List<InteractionData> interactionDatas = new List<InteractionData>();

    private void Update() {
        //......
        for (int i = 0; i < materials.Count; i++) {
            materials[i].SetInt("_InteractionNumber", interactionDatas.Count);

            if (interactionDatas.Count > 0) {
                materials[i].SetVectorArray("_InteractionStartPosArray", interactionStartPosArray);
                materials[i].SetFloatArray("_InteractionInnerRadiusArray", interactionInnerRadiusArray);
                materials[i].SetFloatArray("_InteractionOuterRadiusArray", interactionOuterRadiusArray);
                materials[i].SetFloatArray("_InteractionAlphaArray", interactionAlphaArray);
                materials[i].SetColorArray("_InteractionColorArray", interactionColorArray);
                materials[i].SetFloatArray("_DistortAlphaArray", distortAlphaArray);
            }
        }
    }

    public void AddInteractionData(Vector3 pos, Color color) {
        if (interactionDatas.Count >= 100) {
            return;
        }

        InteractionData interactionData = new InteractionData();
        interactionData.color = color;
        interactionData.interactionStartPos = pos;
        interactionDatas.Add(interactionData);
    }
}

  

再新建ShootManager.cs脚本,在点击鼠标时,做射线检测,如果碰撞到能量盾,调用交互接口:

public class ShootManager : MonoBehaviour {
    [ColorUsage(true, true)] public Color interactionColor;

    private void Update() {
        if (Input.GetMouseButtonDown(0)) {
            RaycastHit hitInfo;
            bool hited = Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hitInfo, Mathf.Infinity);
            if (hited) {
                Shield.instance.AddInteractionData(hitInfo.point, interactionColor);
            }
        }
    }
}

  

然后在Shader里计算像素点到交互点的距离:

//Properties
int _InteractionNumber;
float3 _InteractionStartPosArray[100];
float _InteractionInnerRadiusArray[100];
float _InteractionOuterRadiusArray[100];
float _InteractionAlphaArray[100];
float4 _InteractionColorArray[100];

float _DistortAlphaArray[100];

float GetInteractionIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
    float dist = distance(i.worldPos, startPos);
    if(dist > outerRadius || dist < innerRadius) {
        return 0;
    }
    else {
      float intensity = (dist - innerRadius) / (outerRadius - innerRadius);
      return intensity;
    }
}

//frag
float interactionIntensity = 0;
float4 interactionColor = 0;
for(int iii = 0; iii < _InteractionNumber; iii++) {
    float tempInteractionIntensity = GetInteractionIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _InteractionAlphaArray[iii];
    interactionIntensity += tempInteractionIntensity;

    interactionColor += _InteractionColorArray[iii] * tempInteractionIntensity;
}

interactionIntensity = saturate(interactionIntensity);

finalColor += interactionColor;
finalColor.a = saturate(finalColor.a);

  

视频演示:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/15.mp4

 

在交互区域,对贴图做提亮和扭曲:

//Properties
_DistortNormal ("DistortNormal", 2D) = "bump" {}
_DistortIntensity ("DistortIntensity", Float) = 1

sampler2D _DistortNormal;
float4 _DistortNormal_ST;
float _DistortIntensity;

float GetDistortIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
    float dist = distance(i.worldPos, startPos);
    if(dist > outerRadius) {
        return 0;
    }
    else {
        float intensity = dist / outerRadius;
        return intensity;
    }
}

//frag
float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
distortNormal *= _DistortIntensity * distortIntensity;

float distortIntensity = 0;
for(int iii = 0; iii < _InteractionNumber; iii++) {
    //......
    distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
    distortIntensity = saturate(distortIntensity);
}

float patternIntensity = texCUBE(_PatternTex, normal + distortNormal).a * isFrontFace;
patternIntensity *= pow(ndv + interactionIntensity, _PatternPower);
finalColor += patternIntensity * _PatternColor;
finalColor.a = saturate(finalColor.a);

  

视频演示:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/16.mp4

 

九、扭曲交互

 

交互功能还差最后一步。

先来做屏幕扭曲效果,原理是用一张法线贴图修改屏幕UV,然后采样_CameraOpaqueTexture。

再新建Shield_Distort.shader,把能量盾Shader里的代码复制过来,并添加以下语句:

sampler2D _CameraOpaqueTexture;
float4 _CameraOpaqueTexture_TexelSize;

  

然后重写片元着色器,修改屏幕UV,采样_CameraOpaqueTexture:

float4 frag (v2f i) : SV_Target
{
    float4 finalColor = 0;

    float distortIntensity = 0;
    for(int iii = 0; iii < _InteractionNumber; iii++) {
        distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
        distortIntensity = saturate(distortIntensity);
    }

    float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
    distortNormal *= _DistortIntensity * distortIntensity;

    i.screenPos.xyz /= i.screenPos.w;
    float2 screenUV = i.screenPos.xy;
    screenUV = (screenUV + 1) / 2;

    finalColor = tex2D(_CameraOpaqueTexture, screenUV + distortNormal.xy * _CameraOpaqueTexture_TexelSize.xy);

    return finalColor;
}

  

再把能量盾复制一份,换上新材质:

 

 

 

视频演示:

https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TAtutorial/18.mp4

 

至此,我们的能量盾就完成了!

十、源文件下载

Github:
https://github.com/MagicStones23/Unity-Shader-Tutorial-Interactable-Energy-Shield

百度网盘:
https://pan.baidu.com/s/1R58a9uPzq3pGugyFwZshEA?pwd=1111
提取码:1111

备注:工程中部分素材取自互联网,仅供学习交流使用,请勿在商业项目中使用。


这是侑虎科技第1423篇文章,感谢作者异世界的魔法石供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:https://www.zhihu.com/people/shui-guai-76-84

再次感谢异世界的魔法石的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

posted @ 2023-07-18 17:07  UWATech  阅读(124)  评论(0编辑  收藏  举报