Featured image of post Shadow 1

Shadow 1

Giới thiệu về shadow trong Unity

Shadow hay còn gọi là bóng thường được sử dụng nhằm mục đích khiến giúp tạo chiều sâu cho khung hình cũng như liên kết nhân vật vào không gian.

Shadow sử dụng trong 2D thường là shadow fake (ở dạng sprite cố định tùy game mà có thể thêm thắt việc thay đổi scale của sprite fake shadow để trông thật hơn).

Còn trong môi trường 3D chúng ta có thể hiển thị shadow của vật thể lên một bề mặt.

  • Tạo một scene trống ở mode 3D (có sẵn camera và lighting), light được tạo sẵn là Directional Light và bật Shadow Type được đặt sẵn là Soft Shadows, nếu bạn tạo Light bằng tay mà không thấy có shadow hãy nhớ đổi shadow type của light từ No Shadows sang giá trị khác

  • Tạo background sử dụng Image, Render Mode của canvas được đặt thành Screen Space - Camera

  • Tạo một Cube mặc định (thuộc tính Cast Shadows được đặt là On)

Như bạn thấy chúng ta không thể hiển thị shadow của Cube lên background ở dạng UI

  • Tạo một tấm Plane để nhận shadow từ Cube như bạn thấy chúng ta có tùy chọn Receive ShadowsLighting của Mesh Renderer Khi Receive Shadows được bật thì Mesh Renderer sẽ có thể nhận được shadow từ vật thể khác chiếu lên nó, còn nếu Receive Shadows bị tắt thì sẽ không có shadow nào hiển thị được trên Mesh Renderer này cả.

Ở đây Plane không cần hiển thị shadow của nó lên vật thể khác vì vậy chúng ta chuyển Cast Shadows của nó thành Off

Nhưng vấn đề ở đây là shadow của Cube chỉ được chiếu lên Plane nếu chúng ta tắt Mesh Renderer của Plane để nó không chắn tầm hiển thị của background thì chúng ta lại không thể nhìn thấy shadow của cube được nữa.

Vậy phải làm thế nào?

Làm thế nào mà Plane trong suốt mà vẫn nhận được shadow?

Hmmm chúng ta sẽ cần viết shader tùy chỉnh cho Plane để thực hiện được việc này.

  • Đầu tiên chúng ta cần tạo một shader Unlit

Đặt tên nó là ReceiveShadow

Ban đầu nó trông như thế này

 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
48
49
50
51
52
53
54
55
56
57
58
Shader "Unlit/ReceiveShadow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

Chúng ta sẽ loại bỏ phần xử lý fog trong shader này

 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
48
49
50
51
52
Shader "Pancake/ReceiveShadow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

Chúng ta chỉ chỉnh sửa color và không cần thay đổi texture nên Properties sẽ chỉ khai báo _Color thay vì khai báo _MainTex

Đoạn shader bên trên sẽ thay đổi thành

 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
Shader "Unlit/ReceiveShadow"
{
    Properties
    {
        _Color ("Color", Color) = (0, 0, 0, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

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

            fixed4 frag (v2f i) : SV_Target
            {
                return _Color;
            }
            ENDCG
        }
    }
}

Vì là shader unlit nên nó không bị ảnh hưởng bởi các yếu tố về ánh sáng, giờ chúng ta sẽ sửa lại nó để nó hỗ trợ nhận ánh sáng

1
2
3
4
        Tags {
            "RenderType"="Transparent"
            "LightMode"="ForwardBase"
        }

Thêm vào Tags "LightMode"="ForwardBase" để cho phép nhận ánh sáng

Chúng ta sẽ pha trộn màu ở kênh alpha cho SubShader bằng câu lệnh Blend

1
Blend SrcAlpha OneMinusSrcAlpha // Traditional transparency

Ngoài câu lệnh blend này chúng ta có một số cú pháp phổ biến như sau

1
2
3
4
5
6
Blend SrcAlpha OneMinusSrcAlpha // Traditional transparency
Blend One OneMinusSrcAlpha // Premultiplied transparency
Blend One One // Additive
Blend OneMinusDstColor One // Soft additive
Blend DstColor Zero // Multiplicative
Blend DstColor SrcColor // 2x multiplicative

Xem thêm thông tin về cách hoạt động của Blend ở đây

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            ...
    }

Việc triển khai hỗ trợ việc receive shadows sẽ yêu cầu biên dịch đường truyền ánh sáng base thành một số biến thể để xử lý directional light without shadowsdirectional light with shadows một cách chính xác

  • #pragma multi_compile_fwdbase chỉ định shader này là một shader biến thể của của multi_compile_fwdbase
  • #include "AutoLight.cginc" bao gồm thư viện AutoLight.cginc để sử dụng các hỗ trợ về ánh sáng từ thư viện này ( SHADOW_COORDS, TRANSFER_SHADOW, SHADOW_ATTENUATION)

Giá trị truyền từ vertex sang fragment là

1
2
3
4
            struct v2f {
                float4 pos : SV_POSITION;
                SHADOW_COORDS(0)
            };

Ở đây thì SHADOW_COORDS là một macro của thư viện AutoLight.cginc nó sẽ đưa dữ liệu của shadow vào trong TEXCOORD0

trong hàm xử lý vertex chúng ta có

1
2
3
4
5
6
            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                TRANSFER_SHADOW(o)
                return o;
            }

Hàm TRANSFER_SHADOW(o) được gọi để tính toán dữ liệu shadow

Trong hàm xử lý fragment chúng ta có

1
2
3
4
5
            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = _Color;
                col.a *= 1 - LIGHT_ATTENUATION(i);
                return col;
            }

LIGHT_ATTENUATION(i) tính toán độ suy giảm ánh sáng do sử dụng blend theo alpha nên độ biến đổi ánh sáng phụ thuộc vào alpha

Shadow càng thì giá trị càng gần 1 vì vậy cần đảo ngược giá trị của LIGHT_ATTENUATION

Shader hoàn chỉnh sẽ trông như này

 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 "Unit/ReceiveShadow" {
    Properties {
        _Color ("Color", Color) = (0, 0, 0, 1)
    }
    SubShader {
        Tags {
            "RenderType"="Transparent"
            "LightMode"="ForwardBase"
        }
        Blend SrcAlpha OneMinusSrcAlpha
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                SHADOW_COORDS(0)
            };

            fixed4 _Color;

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

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = _Color;
                col.a *= 1 - LIGHT_ATTENUATION(i);
                return col;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}
  • Tạo material từ shader vừa viết và gắn nó cho Plane lúc này Plane sẽ hứng được shadow của Cube

Nhưng khoan kết quả vẫn không như chúng ta mong đợi background bị cắt mất phần của tấm plane. Shader của tấm plane ZWrite không bị tắt để có thể nhận shadow, do đó các đối tượng như image luôn được draw sau các đối tượng opaque và nó sẽ bị bỏ qua sau khi vẽ plane.

Vì vậy chúng ta sẽ thay đổi Render Queue của Image background

  • Tạo một shader UI/Default và chỉnh Render Queue của nó thành 1950 (nhỏ hơn 2000)

Cuối cùng kết quả của chúng ta thu được như sau:

yenmoc
Lượt nghé thăm