[셰이더 프로그래밍 입문] Part2 | 셰이더 프로그래밍 응용 (Chapter9~10)

jungizz_·2023년 5월 19일
0

Shader Programming

목록 보기
5/5
post-thumbnail

Chapter 9 | UV 애니메이션과 울렁효과

🆕 HLSL

  • cos() - 코사인 함수

◾ UV 애니메이션

  • 표면에 텍스처를 입힌 뒤에 텍스처를 이동시켜 움직이는 효과를 만든다.

📝 기초설정

  • 5장 Diffuse/SpecularMapping 프로젝트를 불러오고 셰이더의 이름을 UVAnimation으로 수정한다.
  • 모델을 Torus.3ds로 설정한다.(원환체)
  • 셰이더는 GPU상에서 실행되며 시간을 구하는 함수가 없으므로 CPU에서 시간 값을 계산한 뒤 전역변수로 넘겨 받아야 한다.
  • 셰이더에 Float 변수 gTime을 생성하고, Time0_X로 변수 시맨틱을 설정한다.

📝 정점셰이더

  • 정점셰이더 기법이므로 픽셀셰이더는 수정할 것이 없다.
float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

float4 gWorldLightPosition;
float4 gWorldCameraPosition;

float gTime; //시간변수 선언 

struct VS_INPUT{
   . . .
};

struct VS_OUTPUT{
   . . .
};


VS_OUTPUT vs_main(VS_INPUT Input){
   . . .
   
   Output.mUV = Input.mUV + float2(gTime * 0.25f, 0); //U좌표에만 시간변수를 더하기(0.25를 곱해 속도 조절)
   return Output;
}

◾ 울렁효과

  • 물결 효과처럼 울렁이는 효과
  • 정점의 위치가 위 아래로 울렁이는 것이므로 cos그래프를 사용해 Y값을 적당히 증감해준다.
  • cos그래프의 x축을 경과한 시간으로 설정해 결과 값을 정점의 Y위치가 되도록 한다.

📝 정점셰이더

. . .

float gTime;

//속도를 조절하는 변수들
float gWaveHeight;
float gSpeed;
float gWaveFrequency;
float gUVSpeed;

struct VS_INPUT{
   . . .
};

struct VS_OUTPUT{
   . . .
};


VS_OUTPUT vs_main(VS_INPUT Input){
   VS_OUTPUT Output;
   
   //[시간+정점마다 높낮이를 다르게 해주기 위한 UV좌표]를 매개변수로 하는 cos값 
   float cosTime = gWaveHeight * cos(gTime*gSpeed + Input.mUV.x*gWaveFrequency);
   Input.mPosition.y += cosTime;
   
   Output.mPosition = mul(Input.mPosition, gWorldMatrix);
   
   . . .
   
   Output.mUV = Input.mUV + float2(gTime * gUVSpeed, 0); //U좌표에만 시간변수를 더하기
   return Output;
}
  • 속도 조절 변수들을 셰이더에 추가하고 값을 아래와 같이 설정한다. (Float)

    gWaveHeight: 3
    gSpeed: 2
    gWaveFrequency : 10
    gUVSpeed: 0.25

왜 잘리는 부분이 보일까??


Chapter 10 | 그림자 기법을 평정한 그림자매핑

◾ 그림자매핑

  • 얇은 점선 기준으로 빛을 가로막는 물체가 있는 b, c, e에 그림자가 진다.
  • 굵은 점선 기준으로 2번 점선이 A물체와 만날 때는 그림자가 없고, B물체와 만나는 부분에 물체 A 때문에 그림자가 생긴다. 또한, 3번 점선은 물체B의 영향을 받지 않는다.
  • 입사광선을 가로막는 첫 번째 물체가 그림자를 만든다. 이것을 활용한 기법이 그림자매핑이다.

1. 그림자 만들기 (그림자맵)

  • 빛을 가로막는 첫번째 물체의 깊이(광원으로부터의 깊이)를 저장한다.
  • 카메라를 빛의 위치에 두고 물체의 깊이를 그리면 된다.
  • 그리는 것은 픽셀셰이더이므로 물체를 이루는 각 픽셀마다 깊이를 그린다. 하지만 그려진 깊이 값을 활용해 그림자 기법을 적용하기 위해 값을 읽을 수 있는 방법이 필요하다.
  • 곧바로 화면에 그리지 않고 렌더타깃 텍스처 위에 그려 저장한 뒤, 픽셀셰이더에서 읽어와 계산하는 것이다. 이 텍스처를 그림자맵ShadowMap이라고 한다.

2. 그림자 입히기

  • 위 그림에 장면 카메라를 추가하면 아래와 같다.

✔️점선2와 물체B가 만나는 픽셀을 그리는 경우

  • 물체B의 깊이(현재 깊이): 0.5
  • 그림자 맵에서 물체B의 값(그림자 깊이): 0.2 (A가 첫번째 물체)
  • 현재깊이 > 그림자깊이 이므로 그림자가 생겨야한다는 것을 알아낸다.

📝 기초설정

  • DirectX 셰이더를 생성하고 이름을 ShadowMapping으로 설정한다.
  • 모델을 Torus.3ds로 수정하고, Model 변수의 이름도 Torus로 변경한다.

1. 그림자 만들기 셰이더

  • Pass0의 이름을 CreateShadow로 변경한다. (패스를 여러개 사용하기 때문)

📝 정점셰이더

1. 입력데이터

struct VS_INPUT 
{
   float4 mPosition : POSITION;
};

2. 출력데이터

  • 각 픽셀의 깊이 값을 반환해야하므로 위치 정보만 필요하지만 픽셀셰이더에서 POSITION 시맨틱 변수를 바로 접근할 수 없기 때문에 TEXCOORD1 시맨틱으로 위치정보를 전달한다.
struct VS_OUTPUT 
{
   float4 mPosition : POSITION;
   float4 mClipPosition: TEXCOORD1; 
};

3. 전역변수

  • 물체를 월드공간으로 변환한 뒤, 광원에 위치에서 물체들을 바라본 뷰 공간으로 변환한다. (광원-뷰공간)
  • 그 뒤, 투영공간으로 변환하여 2D표면에 물체를 그린다. (광원-투영공간)
  • 렌더몽키에 위 행렬을 추가해준다. matViewProjectionMatrix를 삭제하고 셰이더에 float4x4변수 gWorldMatrix를 생성하고 World 변수 시맨틱을 대입한다.
  • 나머지 두 행렬은 알맞는 변수 시맨틱이 없으므로 gLightProjectionMatrix은 Projection 변수 시맨틱으로, gLightViewMatrix는 정점셰이더에서 만든다. (좋은방법은 아님)
  • gLightViewMatrix를 위해 광원의 위치 float4 gWorldLightPosition를 만들고 (500, 500, -500, 1)로 설정한다.
float4x4 gWorldMatrix;
float4x4 gLightViewMatrix; //광원-뷰공간
float4x4 gLightProjectionMatrix; //광원-투영공간

float4 gWorldLightPosition; //광원의 위치

4. 함수

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;
   
   //광원-뷰행렬 생성 (억지로 만든거라 이해필요 X)
   float4x4 lightViewMatrix = gLightViewMatrix ;
   
   float3 dirZ = -normalize ( gWorldLightPosition. xyz );
   float3 up = float3 ( 0, 1, 0 );
   float3 dirX = cross ( up, dirZ );
   float3 dirY = cross ( dirZ, dirX );
   
   lightViewMatrix = float4x4 (
   float4 (dirX, - dot ( gWorldLightPosition.xyz, dirX )),
   float4 (dirY, - dot ( gWorldLightPosition.xyz, dirY )),
   float4 (dirZ, - dot ( gWorldLightPosition.xyz, dirZ )),
   float4 (0, 0, 0, 1));
   lightViewMatrix = transpose ( lightViewMatrix );
   
   
   //공간변환 (물체공간 > 월드공간 > 광원-뷰공간 > 광원-투영공간)
   Output.mPosition = mul ( Input.mPosition, gWorldMatrix );
   Output.mPosition = mul ( Output.mPosition, lightViewMatrix );
   Output.mPosition = mul ( Output.mPosition, gLightProjectionMatrix );
   
   Output.mClipPosition = Output.mPosition ;
   
   return Output ;   
}
  • 마우스 드래그를 해도 위치가 변하지 않는 카메라 고정상태이다.

📝 픽셀셰이더

1. 입력 데이터

struct PS_INPUT{
   float4 mClipPosition: TEXCOORD1;
};

2. 함수

  • 깊이 값z을 반환하는데, 원근투영을 하면 2D 이미지 위에 그려지는 물체들의 XY좌표 값이 잘못되므로 올바른 XY값을 구하기 위해 w로 나눈다. (책 p.216 이해 잘 안됨!)
  • w로 나누면 XY값의 범위는 -1~1, Z는 0~1이 된다.
float4 ps_main(PS_INPUT Input) : COLOR
{   
   float depth = Input.mClipPosition.z / Input.mClipPosition.w;
   return float4(depth.xxx, 1);
}

📝 렌더타깃 설정

  • renderTexture 텍스처를 생성하고 이름을 ShadowMap으로 설정한다.
  • ShadowMap을 더블클릭하여 포맷을 R32F로 수정한다. 충분한 많은 깊이를 표현하기 위해 32비트 값으로 바꾸는 것이다.
  • CreateShadow패스에 ShadowMap 렌더타겟을 추가하고 더블클릭하여 Clear color를 하얀색으로 수정한다. (그림자를 받지 않는 부분을 하얀색으로 설정)

2. 그림자 입히기 셰이더 - 원환체

  • 현재 그리는 픽셀의 깊이(광원으로부터의 깊이)를 그림자맵에서 가져온 깊이와 비교하는 것이다.

📝 기초설정

  • 셰이더에 Pass를 추가하고 이름을 ApplyShadowTorus로 설정한다.
  • Pass안에 Model 변수를 Torus로 설정한다.

📝 정점셰이더

1. 입력데이터

  • StreamMapping에 Normal을 추가한다. (FLOAT3, Index = 0)
struct VS_INPUT
{
   float4 mPosition : POSITION ;
   float3 mNormal : NORMAL ; //난반사광 계산을 위한 법선
};

2. 출력데이터

struct VS_OUTPUT
{
   float4 mPosition : POSITION ;
   float4 mClipPosition : TEXCOORD1 ;
   float mDiffuse : TEXCOORD2 ; //난반사광 결과
};

3. 전역변수

  • 광원으로부터의 깊이를 구하기 위해 그림자 만들 때와 동일한 변수 필요
  • 난반사광을 구하기 위해 장면을 렌더링할 때 사용하는 카메라에서 뷰행렬과 투영행렬 필요하므로 합쳐서 gViewProjectionMatrix 행렬을 셰이더에 추가하고 ViewProjection 시맨틱을 대입한다.
//광원으로부터의 깊이를 구하기 위한 변수
float4x4 gWorldMatrix ;
float4x4 gLightViewMatrix ;
float4x4 gLightProjectionMatrix ;

float4 gWorldLightPosition ;

//뷰행렬과 투영행렬
float4x4 gViewProjectionMatrix ;

4. 함수

  • 공간변환을 두 번 해야한다.
  1. 물체를 그리기 위한 공간변환
  2. 물체의 깊이(광원으로부터의 깊이)를 구하는 공간변환
VS_OUTPUT vs_main ( VS_INPUT Input ) {
   VS_OUTPUT Output ;
   
   //광원-뷰행렬
   float4x4 lightViewMatrix = gLightViewMatrix ;
   
   float3 dirZ = - normalize ( gWorldLightPosition.xyz );
   float3 up = float3 ( 0 , 1 , 0 );
   float3 dirX = cross ( up , dirZ );
   float3 dirY = cross ( dirZ , dirX );
   
   lightViewMatrix = float4x4 (
      float4 ( dirX , - dot ( gWorldLightPosition.xyz , dirX )),
      float4 ( dirY , - dot ( gWorldLightPosition.xyz , dirY )),
      float4 ( dirZ , - dot ( gWorldLightPosition.xyz , dirZ )),
      float4 ( 0 , 0 , 0 , 1 ));
   lightViewMatrix = transpose ( lightViewMatrix );


   //1. 물체를 그리기 위한 공간변환
   float4 worldPosition = mul ( Input.mPosition , gWorldMatrix );
   Output.mPosition = mul ( worldPosition , gViewProjectionMatrix );
   
   
   //2. 깊이를 구하기 위한 공간변환
   Output.mClipPosition = mul ( worldPosition , lightViewMatrix );
   Output.mClipPosition = mul ( Output.mClipPosition , gLightProjectionMatrix );
   
   
   //3. 난반사광 구하기
   float3 lightDir = normalize ( worldPosition.xyz - gWorldLightPosition . xyz );
   float3 worldNormal = normalize ( mul ( Input.mNormal , ( float3x3 ) gWorldMatrix ));
   Output.mDiffuse = dot (- lightDir , worldNormal );
   
   return Output ;
}

📝 픽셀셰이더

1. 전역변수

  • ApplyShadowTorus패스에 ShadowMap 텍스처 오브젝트를 추가하고 이름을 ShadowSampler으로 설정한다. CreateShadow패스가 그린 그림자맵 텍스처를 읽어오는 것이다.
  • 물체의 색을 전역변수로 설정하기 위해 myColor변수를 생성하고 이름을 gObjectColor로 변경한다.
sampler2D ShadowSampler ;
float4 gObjectColor ;

2. 입력데이터

struct PS_INPUT
{
   float4 mClipPosition : TEXCOORD1 ;
   float mDiffuse : TEXCOORD2 ;
};

3. 함수

float4 ps_main ( PS_INPUT Input ) : COLOR
{
   float3 rgb = saturate ( Input mDiffuse ) * gObjectColor ; //난반사광 값을 물체에 색상에 곱하기
   
   float currentDepth = Input.mClipPosition.z / Input.mClipPosition.w ; //현재 픽셀의 깊이
  • 그림자맵에서 빛을 가렸던 물체의 깊이를 가져오기 위해 tex2D()함수를 사용해 텍스처를 샘플링하는건 알겠는데, UV좌표는 어떤걸 사용할까?
  • 아래 그림의 그림자맵을 텍스처로 생각하면 UV좌표 범위는 (0, 0)~(1, 1)이지만 / 투영공간 좌표계로 생각하면 XY좌표 범위는 (-1, 1)~(1, -1)이다.
  • 각 픽셀의 XY좌표를 UV좌표로 변환해야하여 사용하기 위해 아래 공식을 사용한다. (현재 픽셀의 XY좌표가 있으면 그림자맵의 uv좌표도 구할 수 있게 된 것)
   //그림자맵의 uv좌표 구하기
   float2 uv = Input.mClipPosition.xy / Input.mClipPosition.w ; 
   uv.y = - uv.y ;
   uv = uv * 0.5 + 0.5 ;
   
   float shadowDepth = tex2D ( ShadowSampler , uv ).r ; //uv로 그림자 깊이 구하기
   
   //그림자맵은 R32F텍스처이므로 r채널만 불러오면 그림자의 깊이를 얻을 수 있다.
   //그림자의 깊이와 현재 깊이를 비교하여 그림자 유무를 확인한다.
   if ( currentDepth > shadowDepth + 0.0000125f ) {
      rgb *= 0.5f ; //그림자(조명을 20% 줄이기)
   }
   
   return ( float4 ( rgb , 1.0f ) );
}
  • 그림자의 깊이와 현재 깊이를 비교할 때 shadowDepth에 0.0000125를 더한 이유는 부동소수점 에러 문제를 해결하기 위해 'A와 B의 값이 완전히 똑같아야 같은 걸로 본다'가 아닌 'A와 B의 값 차이가 매우 작으면 같은 걸로 본다.' 라고 정의하기 위해서이다.

  • 그림자 맵의 크기가 작아 각져보이므로 ShadowMap의 크기를 2048로 설정한다.

3. 그림자 입히기 셰이더 - 평면

  • 평면을 추가하여 그림자 효과를 더 관찰한다.
  • ApplyShadowTorus패스를 복사하여 이름을 ApplyShaodwDisc로 수정한다.
  • 셰이더에 Disc평면 모델을 추가하고 새로 만든 패스에도 적용한다.
  • 평면의 위치와 크기를 조절하기 위해 4x4행렬gWorldMatrix을 생성하고 아래와 같이 설정한다.
profile
( •̀ .̫ •́ )✧

0개의 댓글