Kỹ Thuật Tính Toán Vector Ánh Sáng và Normal Mapping trong Unity Shader

Trong quy trình kết xuất đồ họa của Unity3D, việc xử lý chính xác hướng ánh sáng và ứng dụng bản đồ pháp tuyến (Normal Map) đóng vai trò then chốt trong việc nâng cao chất lượng hình ảnh. Bài viết này sẽ đi sâu vào cơ chế hoạt động của vector ánh sáng và kỹ thuật Normal Mapping, đồng thời cung cấp các đoạn mã HLSL được cấu trúc lại để áp dụng vào thực tế.

1. Cơ Chế Tính Toán Vector Ánh Sáng

Việc xác định hướng ánh sáng đòi hỏi phải tính toán góc giữa vector pháp tuyến của bề mặt vật thể và vector chỉ hướng từ bề mặt đến nguồn sáng. Dựa trên mô hình phản xạ khuếch tán (Lambert), cường độ ánh sáng nhận được tỷ lệ thuận với cosin của góc này, được tính thông qua tích vô hướng (dot product). Thay vì tính toán thủ công trong hàm surf như cách tiếp cận cơ bản, chúng ta có thể tách biệt logic ánh sáng bằng cách định nghĩa một mô hình chiếu sáng tùy chỉnh (Custom Lighting Model) để mã nguồn trở nên modular và dễ bảo trì hơn.

Shader "Rendering/DirectionalIllumination"
{
    Properties
    {
        _DiffuseMap ("Diffuse Map", 2D) = "white" {}
        _EmitLightPos ("Emissive Light Coord", Vector) = (5, 5, 5, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }
        
        CGPROGRAM
        #pragma surface surf NoLighting
        
        struct SurfaceData
        {
            float2 uv_DiffuseMap;
            float3 worldPosition;
            float3 worldNormalVector;
        };

        sampler2D _DiffuseMap;
        float4 _EmitLightPos;

        half4 LightingNoLighting(SurfaceOutput s, half3 lightDir, half atten)
        {
            return half4(s.Albedo, s.Alpha);
        }

        void surf (SurfaceData IN, inout SurfaceOutput o)
        {
            float3 directionToLight = normalize(_EmitLightPos.xyz - IN.worldPosition);
            float lambertFactor = saturate(dot(IN.worldNormalVector, directionToLight));
            
            float4 texColor = tex2D(_DiffuseMap, IN.uv_DiffuseMap);
            o.Albedo = texColor.rgb * lambertFactor;
            o.Normal = IN.worldNormalVector;
        }
        ENDCG
    }
    Fallback "Unlit/Texture"
}

Trong đoạn Shader trên, hàm LightingNoLighting được sử dụng để vô hiệu hóa hệ thống chiếu sáng mặc định, cho phép chúng ta toàn quyền kiểm soát toán tử tích vô hướng giữa pháp tuyến và hướng sáng ngay trong hàm surf. Hàm này chỉ chịu trách nhiệm lấy dữ liệu màu sắc và tính toán vật lý ánh sáng cục bộ, giúp tách rời logic kết xuất texture khỏi pipeline ánh sáng tiêu chuẩn.

2. Kỹ Thuật Normal Mapping

Normal Mapping cho phép mô phỏng các chi tiết bề mặt lồi lõm (như vết nứt, nếp gấp) mà không cần phải tăng số lượng đa giác của mesh. Kỹ thuật này lưu trữ thông tin vector pháp tuyến trong không gian tiếp tuyến (Tangent Space) dưới dạng dữ liệu màu sắc RGB. Khi giải mã, các giá trị này được chuyển đổi lại thành vector pháp tuyến thực tế để đánh lừa bộ nhớ đệm ánh sáng, tạo ra ảo giác về độ sâu bề mặt.

Shader "Rendering/SurfaceNormalMapping"
{
    Properties
    {
        _AlbedoTex ("Albedo Texture", 2D) = "white" {}
        _NormalTex ("Normal Texture", 2D) = "bump" {}
        _BumpStrength ("Normal Intensity", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        
        CGPROGRAM
        #pragma surface surf StandardLambert
        
        struct Input
        {
            float2 uv_AlbedoTex;
            float2 uv_NormalTex;
        };

        sampler2D _AlbedoTex;
        sampler2D _NormalTex;
        float _BumpStrength;

        half4 LightingStandardLambert(SurfaceOutput s, half3 lightDir, half atten)
        {
            half diff = max(0, dot(s.Normal, lightDir));
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * diff;
            c.a = 1.0;
            return c;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            o.Albedo = tex2D(_AlbedoTex, IN.uv_AlbedoTex).rgb;
            float3 tangentNormal = UnpackScaleNormal(tex2D(_NormalTex, IN.uv_NormalTex), _BumpStrength);
            o.Normal = tangentNormal;
        }
        ENDCG
    }
    Fallback "Standard"
}

Đoạn mã sử dụng hàm UnpackScaleNormal để giải nén dữ liệu từ texture normal map, đồng thời áp dụng biến _BumpStrength để kiểm soát độ sâu của chi tiết bề mặt một cách linh hoạt. Việc gán trực tiếp vào o.Normal cho phép engine tự động thực hiện các phép biến đổi ma trận cần thiết từ không gian tiếp tuyến sang không gian thế giới trước khi đưa vào hàm tính toán ánh sáng.

3. Tích Hợp Vector Ánh Sáng và Normal Map

Để đạt được hiệu ứng chân thực nhất, hướng ánh sáng và Normal Map cần được kết hợp chặt chẽ. Khi bề mặt có độ lồi lõm ảo từ Normal Map, hướng ánh sáng phải được tính toán dựa trên các vector pháp tuyến mới này thay vì pháp tuyến hình học gốc. Điều này tạo ra các vùng sáng tối vi mô (micro-shadows) trên bề mặt vật liệu, giúp vật thể tương tác tự nhiên với môi trường.

Shader "Rendering/IntegratedIllumination"
{
    Properties
    {
        _MainColorMap ("Color Map", 2D) = "white" {}
        _DetailNormalMap ("Detail Normal", 2D) = "bump" {}
        _LocalLightPos ("Local Light Position", Vector) = (0, 5, 0, 1)
        _NormalFactor ("Normal Factor", Range(0.1, 2.0)) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        
        CGPROGRAM
        #pragma surface surf CustomPointLight
        
        struct Input
        {
            float2 uv_MainColorMap;
            float2 uv_DetailNormalMap;
            float3 worldPos;
        };

        sampler2D _MainColorMap;
        sampler2D _DetailNormalMap;
        float4 _LocalLightPos;
        float _NormalFactor;

        half4 LightingCustomPointLight(SurfaceOutput s, half3 lightDir, half atten)
        {
            half NdotL = saturate(dot(s.Normal, lightDir));
            half4 res;
            res.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten;
            res.a = s.Alpha;
            return res;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            o.Albedo = tex2D(_MainColorMap, IN.uv_MainColorMap).rgb;
            o.Normal = UnpackScaleNormal(tex2D(_DetailNormalMap, IN.uv_DetailNormalMap), _NormalFactor);
        }
        ENDCG
    }
    Fallback "Diffuse"
}

Shader tích hợp này sử dụng mô hình CustomPointLight để tính toán ánh sáng điểm với độ suy giảm (attenuation). Hàm surf giải mã Normal Map và gán vào thuộc tính Normal. Unity sẽ tự động sử dụng các pháp tuyến chi tiết này khi truyền dữ liệu vào hàm LightingCustomPointLight, đảm bảo rằng mọi gờ nổi hay vết lõm đều tương tác chính xác với nguồn sáng cục bộ, mang lại chiều sâu thị giác tối ưu cho vật thể.

Thẻ: unity3d shader HLSL Normal Mapping Lighting

Đăng vào ngày 26 tháng 9 lúc 17:31