Post

2026-04-18~19-TA04

2026-04-18~19-TA04

프로시듀얼 셰이더 응용

과제

프로시듀얼 셰이더(Procedural Shader) 개념을 응용하여 나만의 패턴 직접 구현하기

이번 과제에서는 외부 이미지 텍스처를 사용하지 않고, 오직 수학적 계산만으로 무늬를 만들어내는 프로시듀얼 셰이더를 활용해 볼 예정입니다.

특히 스타크래프트 2의 핵 미사일 이펙트나 FPS 게임의 조준선, 바닥 장판 공격 또는 범위 및 경고 표시를 프로시듀얼 방식으로 구현해 보고자 합니다.

프로시듀얼 셰이더의 핵심 프로시듀얼 방식이란, 수학적인 계산을 통해 화면에 0(검정)과 1(하양)로 이루어진 마스크(모양)를 깎아내는 과정입니다.

    
사진사진사진사진




1. 머티리얼 세팅

사진

  • Blend Mode (블렌드 모드): Opaque -> Translucent
    • 변경 이유: 배경의 땅바닥을 완전히 가리지 않고, 우리가 만든 장판 모양만 화면에 띄운 채 그 외의 배경은 비쳐 보이게 만들기 위함입니다.
  • Shading Model (셰이딩 모델): Default Lit -> Unlit (빛의 영향을 받지 않음)
    • 변경 이유: 타겟팅 장판은 맵의 조명이나 그림자에 가려져 어두워지면 안 됩니다. 어떤 환경에서든 플레이어의 눈에 잘 띄어야 하므로 Unlit으로 설정합니다.


Opacity (불투명도) 기능의 원리

블렌드 모드를 Translucent로 변경하면 머티리얼 노드에서 Opacity 핀이 활성화됩니다.

이 Opacity 핀에 들어가는 데이터 값에 따라 투명도가 결정됩니다.

  • 0 (검은색): 투명해져서 배경이 그대로 비쳐 보입니다.
  • 1 (흰색): 불투명해져서 우리가 지정한 색상이 화면에 출력됩니다.




2. 원형 그라데이션 만들기

프로시듀얼로 원을 그리기 위해서는 화면 정중앙에서부터 거리가 얼마나 떨어져 있는가를 구해야 합니다.

중심점 이동하기 (Subtract 노드)

기본적으로 화면의 UV 좌표는 왼쪽 위 모서리가 (0, 0)으로 시작합니다. 하지만 우리가 만들 장판(원)은 화면 정중앙에 있어야 합니다.

  • 해결 방법: TexCoord(UV)에서 0.5를 빼줍니다. (Subtract 노드 사용)
  • 결과: 좌표계가 이동하여, 이제 화면의 정중앙이 (0, 0)이 됩니다. (범위: -0.5 ~ +0.5)

중앙으로부터의 거리 구하기 (VectorLength 노드)

정중앙이 (0,0)이 되었다면, 이제 화면상의 모든 픽셀들이 중앙으로부터 얼만큼 떨어져 있는지(거리)를 하나의 숫자로 알아내야 합니다. 이때 사용하는 것이 바로 VectorLength 노드입니다.

  • VectorLength 노드의 역할: 픽셀까지의 ‘직선거리’를 계산해 줍니다.
    • 중앙점 (0,0)의 거리는 0 (검은색)
    • 모서리로 갈수록 거리가 멀어짐 ~1 (점점 하얀색으로 변함)
  • 결과: 이 결과를 머티리얼의 색상(Emissive Color)에 연결해 보면, 중심은 새카맣고 밖으로 갈수록 하얗게 퍼지는 방사형 그라데이션이 나타납니다.

참고: VectorLength (V2 vs V3)의 차이

  • V2 (Vector 2): 2D 평면(X, Y 혹은 U, V)에서의 거리를 구합니다. 지금처럼 화면상에 2D 장판(원)을 그릴 때 사용합니다.
  • V3 (Vector 3): 3D 공간(X, Y, Z)에서의 거리를 구합니다. 3D 입체적인 구(Sphere) 형태의 이펙트나 구형 마스크를 만들 때 사용합니다.




3. 테두리 구현

중심점으로부터의 거리(VectorLength)를 구했다면, 이제 이 값을 이용해 경계선이 있는 을 만들고, 그 속을 파내어 범위 공격 표시의 테두리를 구현해보겠습니다.


If 노드를 이용해 “큰 원” 만들기

사진

경계선이 뚜렷한 큰 원을 만들어야 합니다. 이때 If 노드를 사용하여 특정 거리(반지름)를 기준으로 안쪽과 바깥쪽을 나눕니다.

  • If 노드의 역할 및 핀(Pin) 설명:
    • A (비교 대상): VectorLength의 결과값 (중앙으로부터 각 픽셀까지의 거리)
    • B (기준치/반지름): 우리가 지정한 장판의 전체 크기 (예: 0.5)
    • A < B (원 안쪽): 거리가 0.5보다 작다면 1 (흰색/불투명)을 출력합니다.
    • A >= B (원 바깥쪽): 거리가 0.5보다 크거나 같다면 0 (검은색/투명)을 출력합니다.
  • 요약: “중심에서 거리가 0.5 미만인 곳만 하얗게 칠해라”라는 논리입니다. 이를 통해 바깥의 그라데이션이 날아가고 완벽하게 꽉 찬 원이 만들어집니다.

사진

Subtract 노드를 이용해 속 파내기 (큰 원 - 작은 원)

큰 원을 만들었다면, 이번엔 테두리만 남기기 위해 작은 원을 구현해 파내는 작업이 필요합니다.

  • 두 개의 원 준비하기:
    • 위에서 만든 If 노드 세트를 사용하여, B 값을 0.45로 설정한 “조금 더 작은 원”을 하나 더 만듭니다. (이 B 값이 0.5에 가까워질수록 테두리 선이 얇아집니다.)

사진

  • Subtract (빼기) 연산하기:
    • A 핀: 큰 원 (반지름 0.5)
    • B 핀: 작은 원 (반지름 0.45)
  • 원리 요약 및 결과: 반지름이 0.5인 큰 원의 영역(1)에서, 반지름이 0.45인 작은 원의 영역(1)을 뺍니다.
    • 겹치는 안쪽 중심부: 1 - 1 = 0
    • 안 겹치는 테두리 영역(0.45~0.5 구간): 1 - 0 = 1 (테두리만 남음)
    • 애초에 원 밖의 영역: 0 - 0 = 0 (그대로)

결과

사진




4. 다중 원 마스크 만들기

3번의 원리를 응용하여, 여러 개의 링이 겹쳐진 형태의 다중 동심원 마스크를 만들어 보겠습니다.


크기가 다른 추가 테두리 생성하기

이전에 만들었던 링 세트(If 노드 2개 + Subtract 노드 1개)를 그대로 복사하여, 파라미터 값만 다르게 설정한 새로운 링 마스크를 만듭니다.

  • 서로 다른 크기의 링 세팅 (예시):
    • 첫 번째 링: OuterRadius_01 (0.35), InnerRadius_01 (0.3)
    • 두 번째 링: OuterRadius_02 (0.5), InnerRadius_02 (0.45)
    • 결과적으로 작은 링 1개와 그 바깥을 감싸는 더 큰 링 1개가 각각 따로 존재하게 됩니다.


Add 노드를 이용하여 두 링 합치기

각각 따로 만들어진 링 마스크 결과값을 하나의 캔버스로 합쳐야 합니다. 이때 사용하는 것이 Add (더하기) 노드입니다.

  • 합성 원리: * 링의 테두리 영역은 1, 배경은 0입니다.
    • 이 두 마스크를 더하면(Add), 하얀색 동심원 두 개가 한 마스크에 고스란히 담기게 됩니다.
    • 즉, 0과 1로 이루어진 마스크들이 산술적으로 더해지면서, 검은색 도화지 위에 여러 개의 하얀 동심원이 동시에 나타납니다.

결과

사진




5. 십자선 구현

원형 링에 이어, 조준점 느낌을 살려줄 ‘십자선’ 모양의 마스크를 UV 좌표를 통해 그려보겠습니다.


중심점 잡고 축 분리하기

기본적으로 UV 좌표는 (U, V) 또는 (X, Y)라는 두 개의 숫자가 묶인 세트입니다. 여기서 선을 그리기 위해 각 축의 데이터를 따로 떼어내는 과정이 필요합니다.

  • 원점 이동: 원을 그릴 때와 동일하게 TexCoord에서 0.5를 빼주어 화면 중앙을 (0,0)으로 맞춥니다.
  • 원점으로 이동한 값을 Component Mask와 연결해줍니다.
  • Component Mask의 기능과 이유: 묶여있는 2D 데이터에서 내가 원하는 특정 축(채널)의 데이터만 쏙 뽑아내는 필터 역할을 합니다. 언리얼에서는 X축을 R(Red), Y축을 G(Green)로 표현합니다.
    • R 체크 (세로선 생성): R은 X축을 뜻하기 때문에 X축 데이터만 가져옵니다. “가로 위치가 0에 가까운 곳”을 찾으면, 위아래 전체로 이어지기 때문에 결과적으로 세로선이 그려집니다.
    • G 체크 (가로선 생성): G는 Y축을 뜻하기 때문에 Y축 데이터만 가져옵니다. “세로 위치가 0에 가까운 곳”을 찾으면, 좌우 전체로 이어지기 때문에 가로선이 그려집니다.


대칭 만들기

두께가 일정한 선명한 선을 그리려면 Abs(절댓값) 노드가 필요합니다.

  • Abs를 사용하는 결정적 이유: 중심이 0이기 때문에 왼쪽은 -0.5, 오른쪽은 0.5입니다. 만약 Abs 없이 선 두께를 주려고 If 노드(A < 0.01)에 바로 연결하면 왼쪽 영역(-0.1, -0.5 등)은 모두 음수 영역이라 0.01보다 작기 때문에, 화면의 왼쪽 절반이 하얗게 칠해지는 문제가 일어납니다.
  • 적용 결과: Abs를 씌우면 음수(-0.5)가 양수(0.5)로 바뀝니다. 즉, 중심(0)을 기준으로 양쪽 모두 동일하게 0 ~ 0.5의 대칭적인 양수 값을 갖게 됩니다.
  • 이를 통해 If 노드(A < 0.01)를 통과시켰을 때, 정중앙에서부터 양옆으로 딱 0.01만큼의 얇은 구역만 하얗게 칠해진 완벽한 대칭의 선이 완성됩니다.


세로선과 가로선 결합하기

만들어진 세로선(R) 마스크와 가로선(G) 마스크를 하나로 합쳐 십자선을 만듭니다. 두 값을 합칠 때는 Add가 아닌 Max를 사용하는 것이 마스크 합성의 정석입니다.

  • Add (더하기)의 문제점: 두 마스크의 값을 단순히 더합니다.
    • 세로선(1)과 가로선(1)이 교차하는 정중앙은 1 + 1 = 2가 되어버립니다.
    • 마스크는 투명(0)과 불투명(1)으로만 이루어져야 하는데, 2가 되면 나중에 색상을 곱할 때 이상하게 나오는 오류가 발생합니다.
  • Max (최댓값)의 장점: 두 값 중 ‘더 큰 값’ 하나만 가져옵니다.
    • 교차점에서도 Max(1, 1) = 1이 됩니다.
    • 덕분에 선이 겹치는 중앙 부분도 값이 1로 유지되어, 전체 마스크가 순수한 01로 깔끔하게 떨어집니다.

결과

사진




6. 심장박동 이펙트

게임의 경고 장판처럼, 전체 마스크의 크기가 심장 박동처럼 조절되게 만듭니다.


Time 노드

  • 기능: 머티리얼이 로드된 순간부터 흐르는 시간(초, Seconds)을 float(실수) 값으로 끊임없이 반환합니다. (예: 0.1, 0.2 … 1.5 … 100.3 …)
  • 블루프린트의 Tick과 연관성 * 언리얼 엔진의 Tick은 매 프레임마다 CPU에서 Delta Time을 누적하여 시간을 계산한다면, Time 노드는 GPU 상에서 매 프레임 픽셀/버텍스마다 독립적으로 흐르는 시간을 계산합니다.
    • CPU의 간섭 없이 GPU 혼자서 돌릴 수 있게 해주는 타이머입니다.


Sine 노드

  • 기능: 삼각함수 사인 그래프 ($y = \sin(x)$)와 동일한 기능을 합니다.
  • 왜 파동이 생기는가?
    • Time 노드의 숫자는 $0 \rightarrow 100 \rightarrow 1000$으로 영원히 커지기만 하는 선형 데이터입니다.
    • 하지만 이 커지기만 하는 Time 값($x$)을 Sine 노드에 집어넣으면, 삼각함수의 주기성 원리에 의해 출력값($y$)은 영원히 커지지 않고 $-1.0$에서 $1.0$ 사이의 값만을 반복하게 됩니다.
    • 곡선의 형태를 띠기 때문에 커질 때와 작아질 때속도가 감속/가속되어 자연스러운 심장 박동 템포가 만들어집니다.


RemapValueRange 노드 (데이터의 선형 보간 변환기)

  • 기능: 어떤 데이터의 범위를 내가 원하는 새로운 범위의 비율로 늘리거나 압축해서 변환해 주는 노드입니다. 프로그래밍에서 자주 쓰이는 Map 또는 Lerp 함수와 같은 원리입니다.
  • 각 핀의 의미와 작동 원리
    • Input Low (입력 최소치): -1.0 -> “들어올 원본 데이터(Sine)의 최솟값은 -1이야.”
    • Input High (입력 최대치): 1.0 -> “들어올 원본 데이터의 최댓값은 1이야.”
    • Target Low (목표 최소치): 1.0 -> “원본이 -1일 때, 나는 1.0(장판의 기본 크기)으로 바꿔서 내보낼게.”
    • Target High (목표 최대치): 1.1 -> “원본이 1일 때, 나는 1.1(10% 팽창한 크기)로 바꿔서 내보낼게.”
  • 작동
    • 만약 Sine 값이 0 (중간값)이라면? 변환기는 1.01.1의 정확히 중간인 1.05를 뱉어냅니다.
  • 사용하는 경우
    • 센서값, 조이스틱 입력값, 삼각함수 값 등 고정된 범위의 데이터를 다른 수치로 사용해야 할 때 쓰입니다.
    • 장판 시스템에서는 거리에 곱할 배율이 필요했습니다. -1을 곱하면 거리가 뒤집히고 0을 곱하면 장판이 소멸하기 때문에, 크기가 유지되면서 커지도록(1.0 ~ 1.1) 배율 수치로 변환해 준 것입니다.

결과

사진




7. Opacity 마스크 결합

Emissive에는 색 데이터를 넣었지만, 투명도를 관장하는 Opacity 핀에는 색상 데이터가 들어가면 안 됩니다. 투명도가 비정상적으로 렌더링될 수 있습니다.

  • 핵심 원리: Opacity는 오직 어디를 보여주고(1), 어디를 뚫을 것인가(0)를 판단하는 순수한 흑백 정보만 필요로 합니다.
  • 마스크 전용 합성: 색상을 입히기 전 단계로 돌아가서, 마스크들만 따로 합쳐줍니다.
    • 원본 링 마스크 합본 Add 원본 십자선 마스크
  • 최종 연결: 이렇게 하얀색 뼈대만 모두 모인 Add 결과를 메인 노드의 Opacity 핀에 꽂아줍니다.

결과

사진




8. 원 및 십자선 색상

색상을 합치기 전에, 각각 독립적으로 완성된 흑백 마스크 결과물들에 먼저 고유의 색상을 곱해줍니다.

  • 원의 Subtract 결과값과 십자선이 합쳐진 부분 Max 노드에 원하는 색을 만들어 Multiply 해주기
  
사진사진

결과

사진




최종 결과

사진

뭔가 아직 어설픈 느낌이 나지만 텍스처 데이터 없이 직접 구현해 본 게 좋은 경험이었습니다.

This post is licensed under CC BY 4.0 by the author.