Mục lục HLSL cho Unity

Docs / HLSL cho Unity

Thời gian trong shader: _Time, nhấp nháy khi trúng đòn và nước chảy

Shader không có hàm Update. Muốn thứ gì đó chuyển động, bạn dựa vào biến thời gian mà Unity gửi vào mỗi khung hình: _Time. Trang này dùng _Time cho hai hiệu ứng quen thuộc: quái nhấp nháy khi trúng đòn và mặt nước chảy.

Biến _Time

_Time là một float4 có sẵn khi include Core.hlsl, không cần khai báo:

Thành phầnGiá trị
_Time.xthời gian / 20
_Time.ythời gian tính bằng giây
_Time.zthời gian x 2
_Time.wthời gian x 3

Gần như lúc nào bạn cũng dùng _Time.y rồi tự nhân với tốc độ của mình. Code dễ đọc hơn khi thấy rõ _Time.y * _Speed.

half pulse = sin(_Time.y * 8.0) * 0.5 + 0.5;   // dao động 0..1, khoảng 1.3 lần mỗi giây

Lỗi hay gặp: hiệu ứng đứng im trong cửa sổ Scene dù code đúng. Scene view chỉ vẽ lại khi có thay đổi. Bật Always Refresh trong menu hiệu ứng trên thanh công cụ của Scene, hoặc bấm Play để xem.

Nhấp nháy khi trúng đòn

Người mới hay đặt cả logic trúng đòn vào shader, rồi bí vì shader không biết khi nào quái bị bắn. Chia việc cho rõ: C# biết khi nào trúng đòn, shader chỉ lo nhấp nháy ra sao. C# bật một số _Hit từ 0 lên 1 trong một lúc, shader dùng _Time để chớp trắng trong khoảng đó.

Shader "Docs/HitBlink"
{
    Properties
    {
        [MainTexture] _MainTex ("Texture", 2D) = "white" {}
        [MainColor] _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
        _FlashColor ("Flash Color", Color) = (1, 1, 1, 1)
        _BlinkSpeed ("Blinks Per Second", Float) = 12
        _Hit ("Hit", Range(0, 1)) = 0
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                half4 _BaseColor;
                half4 _FlashColor;
                float _BlinkSpeed;
                half  _Hit;
            CBUFFER_END

            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * _BaseColor;

                // frac chạy 0..1 mỗi chu kỳ, step cắt thành nửa sáng nửa tối
                half blink = step(0.5, frac(_Time.y * _BlinkSpeed));
                col.rgb = lerp(col.rgb, _FlashColor.rgb, blink * _Hit);
                return col;
            }
            ENDHLSL
        }
    }
}

Script C# gắn lên con quái, gọi Flash() khi nó mất máu:

using System.Collections;
using UnityEngine;

public class HitBlink : MonoBehaviour
{
    static readonly int HitId = Shader.PropertyToID("_Hit");

    [SerializeField] float duration = 0.3f;

    Material material;

    void Awake() => material = GetComponent<Renderer>().material;

    void OnDestroy() => Destroy(material);

    public void Flash()
    {
        StopAllCoroutines();
        StartCoroutine(FlashRoutine());
    }

    IEnumerator FlashRoutine()
    {
        material.SetFloat(HitId, 1f);
        yield return new WaitForSeconds(duration);
        material.SetFloat(HitId, 0f);
    }
}

Với _BlinkSpeed = 12 và duration = 0.3, quái chớp khoảng 3 đến 4 lần rồi trở lại bình thường. _Hit và lerp giúp shader không cần if: khi _Hit = 0, blink * _Hit luôn bằng 0.

Nước chảy bằng hai lớp UV cuộn

Cuộn một texture theo một hướng trông như băng chuyền, không giống nước. Mẹo quen thuộc: đọc cùng một texture gợn sóng hai lần, mỗi lần cuộn một hướng và một tỉ lệ khác nhau, rồi trộn lại. Hai lớp chồng nhau làm hoa văn không bao giờ lặp y hệt.

Chỉ phần khác so với shader trên:

// Properties
_WaterColor ("Water Color", Color) = (0.1, 0.45, 0.7, 1)
_FlowA ("Flow A (XY)", Vector) = (0.05, 0.02, 0, 0)
_FlowB ("Flow B (XY)", Vector) = (-0.03, 0.04, 0, 0)

// CBUFFER thêm
half4 _WaterColor;
float4 _FlowA;
float4 _FlowB;

// frag
half4 frag(Varyings input) : SV_Target
{
    float2 uvA = input.uv + _FlowA.xy * _Time.y;
    float2 uvB = input.uv * 1.7 + _FlowB.xy * _Time.y;   // tỉ lệ khác để hai lớp lệch nhau

    half rippleA = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uvA).r;
    half rippleB = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uvB).r;
    half ripple  = (rippleA + rippleB) * 0.5;

    half3 col = _WaterColor.rgb + ripple * 0.35;   // chỗ gợn sáng lên
    return half4(col, 1);
}

Texture nên là ảnh gợn sóng xám trắng, Wrap Mode đặt Repeat như ở trang Texture và UV.

Bài tập

Làm viên ngọc hồi máu nhấp nháy mềm thay vì chớp tắt: độ sáng tăng giảm đều bằng sin, sáng nhất bằng màu _GlowColor, tối nhất bằng _BaseColor, chu kỳ theo _PulseSpeed.

Xem đáp án
half4 frag(Varyings input) : SV_Target
{
    half4 tex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
    half pulse = sin(_Time.y * _PulseSpeed) * 0.5 + 0.5;   // 0..1
    half3 col = tex.rgb * lerp(_BaseColor.rgb, _GlowColor.rgb, pulse);
    return half4(col, 1);
}

Nhớ thêm _GlowColor và _PulseSpeed vào cả Properties lẫn CBUFFER.