# 준비
기본적인 머테리얼을 생성합니다.
Unlit Shader를 생성합니다.
이것은 조명없이 텍스처만 표시하는 간단한 셰이더입니다.
유니티의 에셋스토어에서 무료로 제공하는 Space Robot Kyle 에셋을 임포트합니다.
# 간단한 Unlit Shader
기본적인 언릿 셰이더를 생성하고 더 단순화한 뒤 코멘트를 붙였습니다.
Shader "Unlit/NewUnlitShader"
{
Properties
{
// 인스펙터에 타일링/오프셋 설정이 보이지 않습니다.
[NoScaleOffset]
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert // 버텍스 셰이더는 vert
#pragma fragment frag // 픽셀(프래그먼트) 셰이더는 frag
struct appdata // 버텍스 셰이더의 input
{
float4 vertex : POSITION; // 버텍스 위치
float2 uv : TEXCOORD0; // 텍스처 위치
};
struct v2f // 버텍스 셰이더의 output ( 버텍스 >> 픽셀 )
{
float2 uv : TEXCOORD0; // 텍스처 위치
float4 vertex : SV_POSITION; // 잘라낸 공간 위치
};
sampler2D _MainTex; // 이 텍스처를 샘플링합니다.
// 버텍스 셰이더
// 각 버텍스마다 실행됩니다.
v2f vert (appdata v)
{
v2f o;
// 클립 공간으로 변환
// 투영행렬으로 곱하기
o.vertex = UnityObjectToClipPos(v.vertex);
// 텍스처 좌표를 전달합니다.
o.uv = v.uv;
return o;
}
// 픽셀 셰이더 (fixed4 타입으로 리턴)
// 각 픽셀마다 실행됩니다.
fixed4 frag (v2f i) : SV_Target // SV_Target은 셰이더 시멘틱이라 합니다.
{
// 텍스처를 샘플링하고 리턴합니다.
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
버텍스 셰이더가 오브젝트의 위치 및 텍스처 좌표를 클립 공간으로 보내고,
픽셀 셰이더가 클립 공간의 데이터를 픽셀로 바꾸어 보내는 흐름이 보입니다.
# 더 간단한 컬러 셰이더
더 간단하게 단순화해봤습니다.
실제로 사용할 만한 셰이더는 아니지만, 셰이더의 흐름을 이해하는데 도움이 될 것입니다.
Shader "Unlit/SingleColor"
{
Properties
{
// 인스펙터에서 색상 설정. 기본 흰색.
_Color("Main Color", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 버텍스 셰이더
// 이번에는 appdata 구조체를 사용하지 않았습니다.
// 리턴 타입도 v2f 구조체가 아닌, 단일 출력인 float4 입니다.
float4 vert(float4 vertex : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertex);
};
// 머테리얼의 색상
float4 _Color;
// 픽셀 셰이더
// 별다른 입력이 없고, 그대로 리턴합니다.
fixed4 frag() : SV_Target
{
return _Color;
}
ENDCG
}
}
}
셰이더의 입출력으로 구조체를 사용할 수도 있고, 수동으로 사용할 수도 있습니다.
# Normal 셰이더
월드 공간에 메시 노멀을 표시하는 셰이더입니다.
Shader "Unlit/WorldSpaceNormals"
{
// 이번에는 프로퍼티가 없습니다.
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 월드 노멀 헬퍼를 추가합니다.
#include "UnityCG.cginc"
struct v2f {
// 텍스처 좌표를 사용한 월드 노멀을 출력하기 위함
half3 worldNormal : TEXCOORD0;
float4 pos : SV_POSITION;
};
// 버텍스 셰이더 : 객체 공간의 노멀도 입력으로 사용할 수 있습니다.
v2f vert(float4 vertex : POSITION, float4 normal : NORMAL)
{
v2f o;
// "UnityCG.cginc"가 가지고 있는 함수들을 사용합니다.
o.pos = UnityObjectToClipPos(vertex);
o.worldNormal = UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 c = 0;
// 노말은 -1 ~ 1사이의 xyz 값을 갖는 3D 벡터값입니다.
// 색상으로 표시하려면 0 ~ 1 사이에 값을
// rgb 구성요소에 대입해줍니다.
c.rgb = i.worldNormal * 0.5 + 0.5;
return c;
}
ENDCG
}
}
}
정규화된 벡터를 컬러로 시각화하는 간단한 방법입니다.
-1 ~ 1 사이값이기 때문에
0.5를 곱하면 -0.5 ~ 0.5 사이 값이 되고
0.5를 더하면 0 ~ 1 사이 값이 됩니다.
# 월드-공간 노멀을 사용한 환경 반사
Shader "Unlit/SkyReflection"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldRefl : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert(float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
// 버텍스의 월드 공간 위치를 계산합니다.
float3 worldPos = mul(unity_ObjectToWorld, vertex).xyz;
// 카메라 방향을 계산합니다.
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 월드 공간 노멀을 계산합니다.
float3 worldNormal = UnityObjectToWorldNormal(normal);
// 월드 공간 반사 벡터입니다.
o.worldRefl = reflect(-worldViewDir, worldNormal);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 반사 벡터를 사용해 기본 큐브맵을 샘플링합니다.
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
// DecodeHDR 로 반사 프로브 데이터에서 실제 컬러값을 얻을 수 있습니다.
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
씬에서 반사 소스로 스카이박스가 사용될 때,
스카이박스 데이터를 가지고 있는 '기본' 반사프로브가 생성됩니다.
반사 프로브는 내부적으로 큐브맵 텍스쳐입니다.
# 노멀맵이 있는 환경 반사
위에 있는 셰이더를 살짝 수정합니다.
Shader "Unlit/SkyReflection Per Pixel"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldPos : TEXCOORD0;
half3 worldNormal : TEXCOORD1;
float4 pos : SV_POSITION;
};
v2f vert(float4 vertex : POSITION, float3 normal : NORMAL)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
o.worldNormal = UnityObjectToWorldNormal(normal);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 카메라 방향과 반사 벡터 계산
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, i.worldNormal);
// 큐브맵 샘플링 및 컬러값 얻기
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
화면을 보기에는 크게 달라진 점이 없습니다.
하지만 이전 셰이더에서 버텍스에서 계산한 것을
화면의 모든 픽셀에서 계산하는 것으로 바뀌었습니다.
그리고 "탄젠트 공간"에 대해 알아야하는데,
노멀 맵 텍스처는 대부분 모델의 표면을 따르는 것이라 볼 수 있습니다.
이번 셰이더는 탄젠트 공간 기반 벡터를 알아야 하고
텍스처로부터 노멀 벡터를 읽어야하고,
이 벡터를 월드 공간으로 변환한 후
셰이더에서 계산을 해주어야 합니다.
Shader "Unlit/SkyReflection Per Pixel"
{
Properties{
// 머테리얼의 노멀맵 텍스처입니다.
// 기본값은 평평한 표면의 더미값입니다.
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
half3 worldPos : TEXCOORD0;
// 탄젠트 공간에서 월드 공간으로 변환하는
// 3*3 회전 행렬을 갖는 세 벡터입니다.
half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
// 노멀맵을 위한 텍스처 좌표
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};
v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}
sampler2D _BumpMap;
fixed4 frag(v2f i) : SV_Target
{
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
return c;
}
ENDCG
}
}
}
# 다양한 텍스처를 추가하기
여기에 처음 셰이더에서 사용했었던 베이스 컬러 텍스처를 추가하면 로봇이 사실적으로 표현됩니다.
위 이미지는 오클루전 텍스처를 실제로 사용하지 않았습니다.
Shader "Unlit/More Textures"
{
Properties{
// 베이스 컬러 텍스처, 오클루전 텍스처를 추가합니다.
_MainTex("Base texture", 2D) = "white" {}
_OcclusionMap("Occlusion", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float3 worldPos : TEXCOORD0;
half3 tspace0 : TEXCOORD1;
half3 tspace1 : TEXCOORD2;
half3 tspace2 : TEXCOORD3;
float2 uv : TEXCOORD4;
float4 pos : SV_POSITION;
};
v2f vert(float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
o.worldPos = mul(unity_ObjectToWorld, vertex).xyz;
half3 wNormal = UnityObjectToWorldNormal(normal);
half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
half tangentSign = tangent.w * unity_WorldTransformParams.w;
half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
o.uv = uv;
return o;
}
// 위 프로퍼티를 위한 텍스처 변수입니다.
sampler2D _MainTex;
sampler2D _OcclusionMap;
sampler2D _BumpMap;
fixed4 frag(v2f i) : SV_Target
{
half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
half3 worldNormal;
worldNormal.x = dot(i.tspace0, tnormal);
worldNormal.y = dot(i.tspace1, tnormal);
worldNormal.z = dot(i.tspace2, tnormal);
half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
half3 worldRefl = reflect(-worldViewDir, worldNormal);
half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
half3 skyColor = DecodeHDR(skyData, unity_SpecCube0_HDR);
fixed4 c = 0;
c.rgb = skyColor;
// 베이스컬러 텍스처와 오클루전 텍스처를 곱해줍니다.
fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;
fixed occlusion = tex2D(_OcclusionMap, i.uv).r;
c.rgb *= baseColor;
c.rgb *= occlusion;
return c;
}
ENDCG
}
}
}
'게임 개발 > 유니티' 카테고리의 다른 글
[유니티] Input.GetKey() 한글로 입력해도 동작하게 하고 싶다면 (0) | 2023.01.16 |
---|---|
[유니티/셰이더] 순차적 체커보드 패턴 Procedural checkerboard pattern (1) | 2022.12.24 |
[유니티] Photon Fusion 103 간단 요약 (0) | 2022.12.22 |
[유니티] Photon Fusion 102 간단 요약 (1) | 2022.12.20 |
[유니티] 게임 패널에 GUI 띄우기 (0) | 2022.12.15 |