# 4-2 [Light Vector]

Ricon·2024년 8월 13일

URP Shader Basic

목록 보기
15/17

Light Vector를 활용한 Lighting 구현

Lighting.hlsl에는 라이트 정보가 정의되어 있다. 이를 활용하여 간단한 라이팅 연산을 구현할 수 있다.

Light GetMainLight()
{
	Light light;
    light.direction = _MainLightPosition.xyz;
    light.distanceAttenuation = unity_LightData.z;
    light.shadowAttenuation = 1.0f;
    light.color = _MainLightColor.rgb;
    return light;
}

우선 면의 방향을 계산하기 위해 Normal값이 필요함.
Vertex Buffer에서 Normal값을 읽어온다.

struct VertexInput
{
	float4 vertex	: POSITION;
	float3 normal	: NORMAL;
};

Pixel Shader에 전달한 보간기를 선언

struct VertexOutput
{
	float4 vertex	: SV_POSITION;
	float3 normal	: NORMAL;
};

이제 Vertex Shader에서 Vertex위치를 Local에서 World로 공간 변환을 해주듯이, Normal도 Local에서 World로 변환해준다.
다만, Vertex 위치의 변환과 달리 Normal의 변환은 균등 스케일(Uniform Scale)의 경우와 아닌 경우를 구분해서 사용한다.

이런 현상이 발생하는 이유는 스케일이 적용된 경우에 Normal의 방향이 바뀌기 때문.
0.5 스케일이 적용된 경우 노멀의 방향이 바뀌는 이미지를 참고.

SpaceTransforms.hlsl의 노멀 변환과 관련된 함수는 다음과 같다.

float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize = true)
{
	#ifdef UNITY_ASSUME_UNIFORM_SCALING
    return TransformObjectToWorldDir(normalOS, doNormalize);
    #else
    	//Normal need to multiply by inverse transpose
        float3 normalWS = mul(normalOS, (float3x3)GetWorldToObjectMatrix());
        if(doNormalize)
        	return SafeNormalize(normalWS);
        return normalWS;
	#endif
}

Vertex Shader에서 계산해준다.

VertexOutput vert(VertexInput v)
{
	VertexOutput o;
	o.vertex = TransformObjectToHClip(v.vertex.xyz);
	o.normal = TransformObjectToWorldNormal(v.normal);
	o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

	return o;
}

이제 Pixel Shader에서 Light와 Mesh의 Normal이 이루는 각도에 따라 Light가 반영되는 정도를 구한다.
우선은 lighting.hlsl에 정의된 변수를 사용하지 않고 유니티에서 light 벡터와 light컬러를 직접 불러온다.

half4 frag(VertexOutput i) : SV_TARGET
{
	// _MainLightPosition은 라이트 벡터를 읽어온다.
	float3 light = _MainLightPosition.xyz;

	//Mesh의 기본 색상은 White로 선언하고
	float4 color = float4(1, 1, 1, 1);

	//얻어진 노멀과 라이트를 내적해서 면과 라이트가 같은 방향이면 1, 90도로 어긋나면 0을 적용한다.
	color.rgb *= saturate(dot(i.normal, light)) * _MainLightColor.rgb;

	return color;
}

Saturate 함수
saturate(x) = x가 0이하이면 0을, 1이상이면 1을 return.(0~1사이의 값으로 Clamp)

color.rgb *= saturate(dot(i.normal, light)) * _MainLightColor.rgb;

이 방식이 lambert light의 기본이 됨.


lighting.hlsl에 사용된 구조체를 사용하면 아래와 같이 사용한다.

half4 frag(VertexOutput i) : SV_Target
{
	Light lit = GetMainLight();
    float4 color = float4(1, 1, 1, 1);
    color.rgb *= saturate(dot(i.normal, lit.direction)) * lit.color;
    
    return color;
}

또한 lighting.hlsl에는 lambert lighting이 다음과 같이 선언되어 있음

half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal)
{
	half NdotL = saturate(dot(normal, lightDir));
    return lightColor * NdotL;
}
half4 frag(VertexOutput i) : SV_Target
{
	float4 color = float4(1, 1, 1, 1);
    color.rgb *= LightingLambert(_MainLightColor.rgb, _MainLightPosition.xyz, i.normal); 
}

전체 쉐이더 코드는 다음과 같다

// Shader 시작. 셰이더의 폴더와 이름을 여기서 결정합니다.
Shader "URPTraining/20_Light Vector"
{
	Properties
	{
		// Properties Block : 셰이더에서 사용할 변수를 선언하고 이를 material inspector에 노출시킵니다
		_TintColor("Test Color", Color) = (1, 1, 1, 1)
		_Intensity("Range Sample", Range(0, 1)) = 0.5
		_MainTex("Main Texture", 2D) = "white"{}
	}

	SubShader
	{
		Tags
		{
			//Render type과 Render Queue를 여기서 결정합니다.
			"RenderPipeline" = "UniversalPipeline"
			"RederType" = "Opaque"
			"Queue" = "Geometry"
		}
		Pass
		{
			Name "Universal Forward"
			Tags { "LightMode" = "UniversalForward"}

			HLSLPROGRAM
			#pragma prefer_hlslcc gles
			#pragma exclude_renderers d3d11_9x
			#pragma vertex vert
			#pragma fragment frag

			//cg shader는 .cginc를 hlsl shader는 .hlsl을 include하게 됩니다.
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

			//vertex buffer에서 읽어올 정보를 선언합니다. 
			struct VertexInput
			{
				float4 vertex	: POSITION;
				float2 uv		: TEXCOORD0;
				float3 normal	: NORMAL;
			};

			//보간기를 통해 버텍스 셰이더에서 픽셀 셰이더로 전달할 정보를 선언합니다.
			struct VertexOutput
			{
				float4 vertex	: SV_POSITION;
				float2 uv		: TEXCOORD0;
				float3 normal	: NORMAL;
			};

			half4 _TintColor;
			float _Intensity;

			float4 _MainTex_ST;
			Texture2D _MainTex;
			SamplerState sampler_MainTex;

			//버텍스 셰이더
			VertexOutput vert(VertexInput v)
			{
				VertexOutput o;
				o.vertex = TransformObjectToHClip(v.vertex.xyz);
				o.normal = TransformObjectToWorldNormal(v.normal);
				o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

				return o;
			}
			
			//픽셀 셰이더
			half4 frag(VertexOutput i) : SV_TARGET
			{

				/*
				// _MainLightPosition은 라이트 벡터를 읽어온다.
				float3 light = _MainLightPosition.xyz;

				//Mesh의 기본 색상은 White로 선언하고
				float4 color = float4(1, 1, 1, 1);

				//얻어진 노멀과 라이트를 내적해서 면과 라이트가 같은 방향이면 1, 90도로 어긋나면 0을 적용한다.
				color.rgb *= saturate(dot(i.normal, light)) * _MainLightColor.rgb;

				return color;
				*/

				
				Light lit = GetMainLight();
				float4 color = float4(1, 1, 1, 1);
				color.rgb *= saturate(dot(i.normal, lit.direction)) * lit.color;

				return color;
				

				/*
				float4 color = float4(1, 1, 1, 1);
				color.rgb *= LightingLambert(_MainLightColor.rgb, _MainLightPosition.xyz, i.normal);
				return color;
				*/
			}

			ENDHLSL
		}
	}
}

Vertex Shader에서 구하기

VertexOutput vert(VertexInput v)
{
	VertexOutput o;
	o.vertex = TransformObjectToHClip(v.vertex.xyz);
	o.normal = TransformObjectToWorldNormal(v.normal);
	//o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;

	float3 light = _MainLightPosition.xyz;
	o.light = saturate(dot(o.normal, light)) * _MainLightColor.rgb;

	return o;
}

half4 frag(VertexOutput i) : SV_TARGET
{
	float4 color = float4(1, 1, 1, 1);
	color.rgb *= i.light;

	return color;
}

이를 Vertex Lighting이라고 한다. Pixel수가 늘어나는 요즘 플랫폼에서는 상대적으로 Vertex Lighting이 연산에 좀 더 가벼운 편이나, Vertex 갯수에 따라 Lighting의 퀄리티가 좌우되므로 주의해서 사용해야한다.

0개의 댓글