Skip to content

改訂前の参考解説

このページは改訂前の教材です。現在の授業の回番号・内容は、後期の授業計画で確認してください。

第16回 法線と基本的なランバート照明

4 / 4 Textureの色と照明の合成 ​

このステップの目的

このステップでは、テクスチャの色とランバート照明を組み合わせ、頂点入力から色の出力までの処理を整理します。

表面の色へ明るさを掛ける ​

第15回では、テクスチャから取得した色にTintを掛けました。このステップでは、その色へ照明の効果を組み合わせます。

まずは光の色と補助的な明るさをマテリアルから指定できるように、ShaderLabのPropertiesへ次の項目を追加してください。

shaderlab
_LightDir ("Direction To Light (World)", Vector) = (0, 1, 0, 0)
_LightColor ("Light Color", Color) = (1, 1, 1, 1)
_Ambient ("Constant Fill", Range(0, 1)) = 0.1

続いて、HLSL側の既存のUnityPerMaterialにも、対応する値の宣言を追加してください。Range(0, 1)と指定することで、Inspectorに0〜1の操作範囲を持つスライダー形式の入力欄が用意されます。

hlsl
float4 _LightDir;
float4 _LightColor;
float _Ambient;

宣言を追加したら、Frag() の normalWS、lightDirWS、diffuse の計算を残し、白黒で返していた return 行を次の3行に置き換えます。

hlsl
float4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _Tint;
float3 lighting = _LightColor.rgb * diffuse + _Ambient;
return float4(albedo.rgb * lighting, albedo.a);

ここでalbedoは、この例で使う表面の色(テクスチャカラーとTintの積)です。_LightColor.rgb * diffuseは、光の色に面の向きによる明るさを掛け合わせた結果を表します。

_Ambientは、すべての面へ同じ量を加える補助的な明るさ(定数アンビエント)です。値を0にすると、直接光が当たらない面は黒くなります。少し値を加えて底上げしておくことで、暗い面でもテクスチャの模様を確認しやすくなります。

この定数は、周囲の物体から届く光やライトベイクを計算した結果ではありません。現実の間接光を再現するには別の仕組みが必要ですが、ここでは計算式の役割を見分けるために一定の値を使用しています。

Shader Graphにも同じ合成を加える ​

Shader Graphにも同様に、光の色を指定するColorプロパティと、補助的な明るさを指定するFloatプロパティを追加しましょう。

  1. 光の色とSaturateの結果をMultiplyで掛け合わせます。
  2. その結果へAddで補助的な明るさを加えます。
  3. 第15回と同じテクスチャの色とTintの積を用意します。
  4. 表面の色と照明の結果をMultiplyで掛け合わせ、UnlitのBase Colorへ接続します。

二つのマテリアルでは、テクスチャ、Tiling、Offset、Tint、光の方向、光の色、補助的な明るさの各パラメータをそろえます。まずは光の色を白、補助的な明るさを0にして比較してみましょう。

出力を戻しながら原因を調べる ​

意図した表示にならない場合は、最終的な色だけを見続けず、確認用の出力へ一時的に戻して原因を調べましょう。

確認する表示調べる内容
UVをRGとして表示するテクスチャを読む座標が正しく届いているか確認します。
テクスチャとTintだけを表示する表面の色と画像の指定が正しいか確認します。
法線をRGBとして表示する法線の向きと座標変換が正しく行われているか確認します。
diffuseだけを白黒で表示する光の方向の設定と内積の計算が正しいか確認します。

この確認方法は、シェーダーの途中結果を色へ置き換える方法です。このように処理を一つずつ確かめることで、修正すべき箇所を確実に絞り込めます。

確認用の出力を試した後は、上の3行によるテクスチャと照明の合成へ戻します。HLSLとShader Graphで同じ入力値を設定し、表面の色と明暗が対応することを確認してください。

参考実装 ​

次の例は、この回で追加した処理を含むシェーダー全体の完全な実装です。頂点の法線がどこで変換され、どこで明るさの計算に使われているかを確認してみましょう。

shaderlab
Shader "Course/LambertStudy"
{
    Properties
    {
        _BaseMap ("Texture", 2D) = "white" {}
        _Tint ("Tint", Color) = (1, 1, 1, 1)
        _LightDir ("Direction To Light (World)", Vector) = (0, 1, 0, 0)
        _LightColor ("Light Color", Color) = (1, 1, 1, 1)
        _Ambient ("Constant Fill", Range(0, 1)) = 0.1
    }

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

        Pass
        {
            Tags { "LightMode" = "SRPDefaultUnlit" }

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float3 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float3 normalOS : NORMAL;
            };

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

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _Tint;
                float4 _LightDir;
                float4 _LightColor;
                float _Ambient;
            CBUFFER_END

            Varyings Vert(Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS);
                output.uv = input.uv * _BaseMap_ST.xy + _BaseMap_ST.zw;
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                return output;
            }

            float4 Frag(Varyings input) : SV_Target
            {
                float3 normalWS = normalize(input.normalWS);
                float3 lightDirWS = normalize(_LightDir.xyz);
                float diffuse = saturate(dot(normalWS, lightDirWS));

                float4 albedo =
                    SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _Tint;
                float3 lighting = _LightColor.rgb * diffuse + _Ambient;
                return float4(albedo.rgb * lighting, albedo.a);
            }
            ENDHLSL
        }
    }
}

この例は、不透明な表面の角度による明暗を学ぶための実装です。返したAlphaは透明度としては使用されません。また、このシェーダーは影の判定、金属の反射、鏡面反射、Scene内のライトとの自動連携を実装していません。

これらの機能を追加すると、必要な入力やパスも増えます。まずは今回の入力・座標変換・補間・色の計算をしっかりと区別し、それぞれの追加機能がどこへ関わるかを考えられるようにしておきましょう。

チェックリスト
  • 表面の色と照明の明るさを別々に確認できていますか。
  • 定数の補助光と、計算された間接光を区別して理解できていますか。
  • 法線の入力から最終色の出力までをコード上でたどることができますか。
  • 同じ値を設定したShader GraphとHLSLを比較できていますか。