ImageEffect ScreenSpaceSnow

Have you ever wondered how much time does it take to apply snow toall of the textures in your game? Probably a lot of times. We’d like toshow you how to create an Image Effect (screen-space shader) thatwill immediately change the season of your scene in Unity.



How does it work?

In the images above you can see two screenshots presenting the same scene.The only difference is that in the second one I enabled snow effect on thecamera. No changes to any of the textures has been made. How could that be?

The theory is really simple. The assumption is that there should be a snowwhenever a rendered pixel’s normal is facing upwards (ground, roofs, etc.)Also there should be a gentle transition between a snow texture and originaltexture if pixel’s normal is facing any other direction (pine trees, walls).

Gettingthe required data

For presented effect to work it requires at least two things:

·        Rendering path set to deferred (For some reason Icouldn’t get forward rendering to work correctly with this effect. The depthshader was just rendered incorrectly. If you have any idea why thatcould be, please leave a message in the comments section.)

·        Camera.depthTextureMode set to DepthNormals

Since the second option can be easily set by the image effect scriptitself, the first option can cause a problem if your game is already usinga forward rendering path.

Setting Camera.depthTextureMode to DepthNormalswill allow us to read screen depth (how far pixels are located from thecamera) and normals (facing direction).

Now if you’ve never created an Image Effect before,you should know that these are build from at least one script and at least oneshader. Usually this shader instead of rendering 3D object, rendersfull-screen image out of given input data. In our case the input data isan image rendered by the camera and some properties set up bythe user.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

using UnityEngine;

using System.Collections;

 

[ExecuteInEditMode]

public class ScreenSpaceSnow : MonoBehaviour

{

 

         public Texture2D SnowTexture;

 

         public Color SnowColor = Color.white;

 

         public float SnowTextureScale = 0.1f;

 

         [Range(0, 1)]

         public float BottomThreshold = 0f;

         [Range(0, 1)]

         public float TopThreshold = 1f;

 

         private Material _material;

 

         void OnEnable()

         {

                   // dynamically create a material that will use our shader

                   _material = new Material(Shader.Find("TKoU/ScreenSpaceSnow"));

 

                   // tell the camera to render depth and normals

                   GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;

         }

 

         void OnRenderImage(RenderTexture src, RenderTexture dest)

         {

                   // set shader properties

                   _material.SetMatrix("_CamToWorld", GetComponent<Camera>().cameraToWorldMatrix);

                   _material.SetColor("_SnowColor", SnowColor);

                   _material.SetFloat("_BottomThreshold", BottomThreshold);

                   _material.SetFloat("_TopThreshold", TopThreshold);

                   _material.SetTexture("_SnowTex", SnowTexture);

                   _material.SetFloat("_SnowTexScale", SnowTextureScale);

 

                   // execute the shader on input texture (src) and write to output (dest)

                   Graphics.Blit(src, dest, _material);

         }

}

It’s only the basic setup, it will not generate a snow for you.Now the real fun begins…

The shader

Our snow shader should be an unlit shader – we don’t want to apply anylight information to it since on screen-space there’s no light. Here’s thebasic template:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

Shader "TKoU/ScreenSpaceSnow"

{

         Properties

         {

                   _MainTex ("Texture", 2D) = "white" {}

         }

         SubShader

         {

                   // No culling or depth

                   Cull Off ZWrite Off ZTest Always

 

                   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 = mul(UNITY_MATRIX_MVP, v.vertex);

                                     o.uv = v.uv;

                                     return o;

                            }

                           

                            fixed4 frag (v2f i) : SV_Target

                            {

                                     // the magic happens here

                            }

                            ENDCG

                   }

         }

}

Note that if you create a new unlit unity shader(Create->Shader->Unlit Shader) you get mostly the same code.

Let’s now focus only on the important part – the fragment shader.First, we need to capture all the data passed by ScreenSpaceSnow script:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

sampler2D _MainTex;

sampler2D _CameraDepthNormalsTexture;

float4x4 _CamToWorld;

 

sampler2D _SnowTex;

float _SnowTexScale;

 

half4 _SnowColor;

 

fixed _BottomThreshold;

fixed _TopThreshold;

 

 

half4 frag (v2f i) : SV_Target

{

        

}

Don’t worry if you don’t know why we need all this data yet. I willexplain it in detail in a moment.

Finding out whereto snow

As I explained before, we’d like to put the snow on surfaces that arefacing upwards. Since we’re set up on the camera that is set to generatedepth-normals texture, now we are able to access it. For this case thereis

1

sampler2D _CameraDepthNormalsTexture;

in the code. Why is it called that way? You can learn aboutit in Unity documentation:

Depth texturesare available for sampling in shaders as global shader properties. By declaringa sampler called _CameraDepthTexture you will be able to sample the main depth texturefor the camera.

_CameraDepthTexture always refers to the camera’sprimary depth texture.

Now let’s start with getting the normal:

1

2

3

4

5

6

7

8

9

10

half4 frag (v2f i) : SV_Target

{

         half3 normal;

         float depth;

 

         DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);

         normal = mul( (float3x3)_CamToWorld, normal);

 

         return half4(normal, 1);

}

Unity documentation says that depth and normals are packed in 16 bitseach. In order to unpack it, we need to call DecodeDepthNormal asabove seen above.

Normals retrieved in this way are camera-space normals. That means that ifwe rotate the camera then normals’ facing will also change. We don’t wantthat, and that’s why we have to multiply it by _CamToWorld matrixset in the script before. It will convert normals from camera to worldcoordinates so they won’t depend on camera’s perspective no more.

In order for shader to compile it has to return something, so I set up thereturn statement as seen above. To see if our calculations are correctit’s a good idea to preview the result.


We’re rendering this as RGB. In Unity Y is facing the zenith by default.That means that green color is showing the value of Y coordinate. So far, sogood!

Now let’s convert it to snow amount factor.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

half4 frag (v2f i) : SV_Target

{

         half3 normal;

         float depth;

 

         DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);

         normal = mul( (float3x3)_CamToWorld, normal);

 

         half snowAmount = normal.g;

         half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;

         snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

 

         return half4(snowAmount, snowAmount, snowAmount, 1);

}

We should be using the G channel, of course. Now, this may be enough,but I like to push it a little further to be able to configure bottom andtop threshold of the snowy area. It will allow to fine-tune how much snow thereshould be on the scene.


Snow texture

Snow may not look real without a texture. This is the most difficultpart – how to apply a texture on 3D objects if you have only a 2D image(we’re working on screen-space, remember)? One way is to find out the pixel’sworld position. Then we can use X and Z world coordinates as texturecoordinates.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

half4 frag (v2f i) : SV_Target

{

         half3 normal;

         float depth;

 

         DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);

         normal = mul( (float3x3)_CamToWorld, normal);

 

         // find out snow amount

         half snowAmount = normal.g;

         half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;

         snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

 

         // find out snow color

         float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);

         float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;

         float4 wpos = mul(_CamToWorld, float4(vpos, 1));

         wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;

 

         half3 snowColor = tex2D(_SnowTex, wpos.xz * _SnowTexScale * _ProjectionParams.z) * _SnowColor;

 

         return half4(snowColor, 1);

}

Now here’s some math that is not a subject of this article. All you needto know is that vpos is a viewport position, wpos isa world position received by multiplying _CamToWorld matrix by viewportposition and it’s converted to a valid world position by dividing by the farplane (_ProjectionParams.z). Finally, we’re calculating the snow colorusing XZ coordinates multiples by _SnowTexScale configurableparameter and far plane to get sane value. Phew…


Merging it!

It’s time to finally merge it all together!

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

half4 frag (v2f i) : SV_Target

{

         half3 normal;

         float depth;

 

         DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);

         normal = mul( (float3x3)_CamToWorld, normal);

 

         // find out snow amount

         half snowAmount = normal.g;

         half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;

         snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

 

         // find out snow color

         float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);

         float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;

         float4 wpos = mul(_CamToWorld, float4(vpos, 1));

         wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;

 

         wpos *= _SnowTexScale * _ProjectionParams.z;

         half4 snowColor = tex2D(_SnowTex, wpos.xz) * _SnowColor;

 

         // get color and lerp to snow texture

         half4 col = tex2D(_MainTex, i.uv);

         return lerp(col, snowColor, snowAmount);

}

Here we’re getting the original color and lerping from it to snowColor using snowAmount.


The final touch: let’s set _TopThreshold value to 0.6:


Voila!

Summary

Here’s a full scene result. Looking nice?



Feel free to download the shader here and use it in your project!

 

Shader "TKoU/ScreenSpaceSnow"

{

       Properties

       {

              _MainTex("Texture", 2D) = "white" {}

       }

       SubShader

       {

              //No culling or depth

              CullOff ZWrite Off ZTest Always

 

              Pass

              {

                     CGPROGRAM

                     #pragmavertex vert

                     #pragmafragment frag

                    

                     #include"UnityCG.cginc"

 

                     structappdata

                     {

                            float4vertex : POSITION;

                            float2uv : TEXCOORD0;

                     };

 

                     structv2f

                     {

                            float2uv : TEXCOORD0;

                            float4vertex : SV_POSITION;

                     };

 

                     v2fvert (appdata v)

                     {

                            v2fo;

                            o.vertex= UnityObjectToClipPos(v.vertex);

                            o.uv= v.uv;

                            returno;

                     }

                    

                     sampler2D_MainTex;

                     sampler2D_CameraDepthNormalsTexture;

                     float4x4_CamToWorld;

 

                     sampler2D_SnowTex;

                     float_SnowTexScale;

 

                     half4_SnowColor;

 

                     fixed_BottomThreshold;

                     fixed_TopThreshold;

                    

 

                     half4frag (v2f i) : SV_Target

                     {

                            half3 normal;

                            floatdepth;

 

                            DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture,i.uv), depth, normal);

                            normal= mul((float3x3)_CamToWorld, normal);

 

                            //find out snow amount

                            halfsnowAmount = normal.g;

                            halfscale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;

                            snowAmount= saturate((snowAmount - _BottomThreshold) * scale);

 

                            //find out snow color

                            float2p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);

                      float3 vpos = float3((i.uv * 2 - 1) /p11_22, -1) * depth;

                      float4 wpos = mul(_CamToWorld,float4(vpos, 1));

                      wpos += float4(_WorldSpaceCameraPos, 0)/ _ProjectionParams.z;

 

                      half4 snowColor = tex2D(_SnowTex,wpos.xz * _SnowTexScale * _ProjectionParams.z) * _SnowColor;

 

                      // get color and lerp to snow texture

                      half4 col = tex2D(_MainTex, i.uv);

                            returnlerp(col, snowColor, snowAmount);

                     }

                     ENDCG

              }

       }

}

 

 

 

using UnityEngine;

using System.Collections;

 

[ExecuteInEditMode]

public class ScreenSpaceSnow :MonoBehaviour

{

 

       publicTexture2D SnowTexture;

 

       publicColor SnowColor = Color.white;

 

       publicfloat SnowTextureScale = 0.1f;

 

       [Range(0,1)]

       publicfloat BottomThreshold = 0f;

       [Range(0,1)]

       publicfloat TopThreshold = 1f;

 

       privateMaterial _material;

 

       voidOnEnable()

       {

              //dynamically create a material that will use our shader

              _material= new Material(Shader.Find("TKoU/ScreenSpaceSnow"));

 

              //tell the camera to render depth and normals

              GetComponent<Camera>().depthTextureMode|= DepthTextureMode.DepthNormals;

       }

 

       voidOnRenderImage(RenderTexture src, RenderTexture dest)

       {

              //set shader properties

              _material.SetMatrix("_CamToWorld",GetComponent<Camera>().cameraToWorldMatrix);

              _material.SetColor("_SnowColor",SnowColor);

              _material.SetFloat("_BottomThreshold",BottomThreshold);

              _material.SetFloat("_TopThreshold",TopThreshold);

              _material.SetTexture("_SnowTex",SnowTexture);

              _material.SetFloat("_SnowTexScale",SnowTextureScale);

 

              //execute the shader on input texture (src) and write to output (dest)

              Graphics.Blit(src,dest, _material);

       }

}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值