Unity中的黑洞失真着色器

时间:2018-04-01 11:56:24

标签: c# unity3d shader hlsl

我发现着色器代码具有扭曲特定点周围空间的效果。这是一个很酷的效果,但它缺少一些动画,所以我添加了一些内容:

Shader "Marek/BlackHoleDistortion"
{
    Properties {
        _DistortionStrength ("Distortion Strength", Range(0, 10)) = 0
        _Timer("Timer", Range(0, 10)) = 0
        _HoleSize ("Hole Size", Range(0, 1)) = 0.1736101
        _HoleEdgeSmoothness ("Hole Edge Smoothness", Range(1, 4)) = 4
        _ObjectEdgeArtifactFix ("Object Edge Artifact Fix", Range(1, 10)) = 1
    }
    SubShader {
        Tags {
            "IgnoreProjector"="True"
            "Queue"="Transparent"
            "RenderType"="Transparent"
        }
        GrabPass{ }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            ZWrite Off

            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #pragma only_renderers d3d9 d3d11 glcore gles 
            #pragma target 3.0
            uniform sampler2D _GrabTexture;
            uniform float _DistortionStrength;
            uniform float _HoleSize;
            uniform float _HoleEdgeSmoothness;
            uniform float _ObjectEdgeArtifactFix;
            uniform float _Timer;

            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct VertexOutput {
                float4 pos : SV_POSITION;
                float4 posWorld : TEXCOORD0;
                float3 normalDir : TEXCOORD1;
                float4 projPos : TEXCOORD2;
            };

            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.normalDir = UnityObjectToWorldNormal(v.normal);
                o.posWorld = mul(unity_ObjectToWorld, v.vertex);
                o.pos = UnityObjectToClipPos(v.vertex);
                o.projPos = ComputeScreenPos(o.pos);

                COMPUTE_EYEDEPTH(o.projPos.z);

                return o;
            }

            float4 frag(VertexOutput i) : COLOR {
                i.normalDir = normalize(i.normalDir);

                float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
                float3 normalDirection = i.normalDir;
                float2 sceneUVs = (i.projPos.xy / i.projPos.w);
                float node_9892 = (_HoleSize * -1.0 + 1.0);
                float node_3969 = (1.0 - pow(1.0 - max(0, dot(normalDirection, viewDirection)), clamp(_DistortionStrength - _Timer, 0, _DistortionStrength)));
                float node_9136 = (length(float2(ddx(node_3969), ddy(node_3969))) * _HoleEdgeSmoothness);
                float node_4918 = pow(node_3969, 6.0);
                float node_1920 = (1.0 - smoothstep((node_9892 - node_9136), (node_9892 + node_9136), node_4918));
                float3 finalColor = (
                    lerp(
                        float4(node_1920, node_1920, node_1920, node_1920), 
                        float4(1, 1, 1, 1), 
                        pow(
                            pow(1.0 - max(0, dot(normalDirection, viewDirection)), 1.0), 
                            _ObjectEdgeArtifactFix
                        )
                    ) * tex2D(_GrabTexture, ((node_4918 * (sceneUVs.rg * _Time * -2.0 + 1.0)) + sceneUVs.rg)).rgb).rgb;

                return fixed4(finalColor, 1);
            }
            ENDCG
        }
    }

    FallBack "Diffuse"

}

现在,问题是为了使失真在一定时间后消失,我需要在等式中加入一些变量 - 这里我称之为_Timer。由于显而易见的原因,我没有使用内置的_Time - 它是一个不断增长的值,每次使用此着色器的对象生效时,我需要从0开始的东西。传递该参数的C#代码处理如下所示:

public void Update() {
    _timeElapsed += Time.deltaTime;

    _renderer.material.SetFloat("_Timer", _timeElapsed);
}

问题是 - 我可以做得更好吗?我希望这个着色器的代码更像是一个自包含的东西 - 无需将参数从cs脚本传递给它。

2 个答案:

答案 0 :(得分:4)

  

我可以做得更好吗?

简而言之,是的,不是。如果希望着色器每material行为不同,则无法避免从C#传递属性。但是,您可以通过传递Update时间并在着色器中计算start时间来避免在elapse中执行此操作。

<强> C#

void OnEnable ()
{
    _renderer.material.SetFloat("_StartTime", Time.timeSinceLevelLoad);
}

<强>着色

uniform float _StartTime;

float4 frag(VertexOutput i) : COLOR
{
   float elapse = _Time.y - _StartTime;
}

现在,虽然这将直接与您当前使用的设置相关联,但应注意访问.material属性将克隆材料(可能会破坏批处理等)。 最近引入MaterialPropertyBlock可以避免这种情况。

答案 1 :(得分:1)

  

Unity为着色器提供了一些内置值:当前对象的转换矩阵,时间等等。

     

你只需在ShaderLab中使用它们,就像你使用任何其他属性一样,唯一的区别是你不必在某处声明它 - 它们是“内置的”。

     

https://docs.unity3d.com/455/Documentation/Manual/SL-BuiltinValues.html

有一种巧妙的方法可以为您提供4种变化值,通过为每个渲染像素重新使用预乘值,可以为您节省乘法运算。有4个值可供使用。

enter image description here

 _Time.x = time / 20
 _Time.y = time
 _Time.z = time * 2
 _Time.w = time * 3

这是一个简单的例子,向您展示它是如何工作的:

Circle

Shader "Example/Circle"
{
    Properties
    {
    }
    SubShader
    {
        Cull Off 

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }



            float circle(in float2 _st, in float _radius){
                float2 dist = distance(_st,float2(0.5,0.5));
                float result = step(dist,_radius);
                return result;
            }

            fixed4 frag (v2f i) : SV_Target
            {
            float WaveTime = sin(_Time.z);
            float3 color = float3(1,1,1)*circle(i.uv,WaveTime);

            return float4( color, 1.0 );
            }
            ENDCG
        }
    }
}

在评论中,您提到要在启用时重置时间值,因此您需要使用脚本初始化时间值。

因此您应该使用自己的时间着色器:

Properties
{
    _Timer("Timer",Float) = 0
}

float WaveTime = sin(_Timer);
using System.Collections;
using UnityEngine;

public class Circle : MonoBehaviour {
    public float _timeElapsed;


    void OnEnable(){
        _timeElapsed = 0;
    }

public void Update() {
    _timeElapsed += Time.deltaTime;
    var _renderer = GetComponent<MeshRenderer>();
    _renderer.material.SetFloat("_Timer", _timeElapsed);
}
}