플레이어 이동에 반응하는 인터랙티브 식생 셰이더(Foliage Bend) 구현 가이드
버텍스 셰이더와 글로벌 벡터를 활용해 플레이어 이동에 자연스럽게 눕고 휘어지는 인터랙티브 식생(Foliage Bend)의 원리와 최적화 구현 단계를 소개합니다.

핵심 요약
인터랙티브 식생(Interactive Foliage)은 버텍스 셰이더(Vertex Shader) 단계에서 플레이어의 월드 좌표와 풀 버텍스 간의 거리를 계산하여 버텍스를 외곽으로 밀어내는 기술입니다. 메시 버텍스 컬러(Vertex Color)의 R 채널을 높이 마스크로 활용해 뿌리를 바닥에 고정하고 CPU의 Shader.SetGlobalVector로 플레이어 위치를 전달하면 추가적인 물리 연산 비용 없이 대규모 잔디 렌더링에 반응형 벤딩 효과를 구현할 수 있습니다.
왜 단순한 바람 애니메이션만으로는 식생이 어색할까?
오픈월드나 3D 환경에서 바람에 흔들리는 풀은 생동감을 주는 기본 요소입니다. 대부분의 엔진에서는 사인파(Sine Wave)나 노이즈 텍스처(Noise Texture)를 이용해 버텍스 셰이더에서 잔디를 주기적으로 흔드는 버텍스 디스플레이스먼트(Vertex Displacement) 방식을 사용합니다.
그러나 캐릭터가 풀숲을 헤치고 지나갈 때 캐릭터의 발과 몸이 풀을 그대로 관통하면 시각적 몰입감이 크게 떨어집니다. 반대로 모든 풀에 물리 콜라이더(Collider)와 리지드바디(Rigidbody)를 붙여 물리 시뮬레이션을 수행하면 수천 개의 드로우콜과 물리 연산 부하로 인해 프레임레이트가 급락합니다.
이 문제를 해결하는 가장 효율적인 테크니컬 아트 접근법이 바로 셰이더 기반 인터랙티브 벤딩(Interactive Foliage Bending)입니다. 물리 엔진을 거치지 않고 오직 셰이더의 버텍스 단계에서 플레이어의 거리에 따라 위치 오프셋을 계산하므로 GPU 인스턴싱(GPU Instancing)을 그대로 유지하면서 고성능 상호작용을 완성할 수 있습니다.
위 수식에서 는 버텍스의 월드 좌표, 는 플레이어의 월드 좌표, 는 밀려나는 영향 반경(Radius), 는 풀의 높이 마스크(0.0~1.0)를 의미합니다. 플레이어와 버텍스 사이의 거리가 가까울수록 강한 밀림 벡터()가 생성됩니다.

상호작용 식생 셰이더 구현 방식은 어떻게 나뉠까?
게임의 규모와 상호작용 주체 수에 따라 식생 벤딩을 구현하는 아키텍처는 크게 두 가지로 분류됩니다.
| 구분 | 글로벌 벡터(Global Vector) 방식 | 렌더 텍스처(Render Texture) 트레일 방식 |
|---|---|---|
| 주요 원리 | C#에서 단일/소수 좌표를 Shader.SetGlobalVector로 전달 | 플레이어와 몬스터들의 궤적을 탑다운 카메라 렌더 텍스처에 기록 |
| 장점 | 구현이 매우 단순하고 메모리 사용량이 거의 없음 | 수십 명의 엔티티 상호작용 및 밟힌 풀 복원 잔상 표현 가능 |
| 단점 | 반응 가능한 인터랙터(Interactor) 수가 소수로 제한됨 | 렌더 타깃 메모리 및 탑다운 뎁스/페인팅 패스 필요 |
| 적합한 프로젝트 | 싱글플레이어 게임, 모바일 액션 RPG | 대규모 MMO, 궤적 유지가 중요한 오픈월드 환경 |
싱글플레이어나 소수 엔티티 위주의 게임이라면 연산 비용이 가장 적은 글로벌 벡터 방식이 가장 실용적입니다.
flowchart LR
A["플레이어 위치 갱신 (C# Update)"] --> B["Shader.SetGlobalVector 전달"]
B --> C["버텍스 셰이더 (Vertex Shader)"]
C --> D["버텍스 컬러(R채널) 높이 마스크 추출"]
D --> E["플레이어 거리 기반 밀림 오프셋 계산"]
E --> F["최종 버텍스 월드 포지션 적용"]
3단계로 구현하는 Global Vector 기반 Foliage Bend 셰이더
1단계: 3D 풀 메시의 버텍스 컬러 마스크 준비
풀이 밀려날 때 뿌리 부분까지 밀리면 잔디가 공중에 뜨거나 바닥에서 뽑히는 현상이 발생합니다. 이를 방지하기 위해 3D 모델링 툴(Blender, Maya 등)에서 풀 메시의 버텍스 컬러를 칠해야 합니다.
- Root(뿌리 버텍스): 검은색()
- Tip(풀 끝 버텍스): 흰색()
셰이더 내부에서는 버텍스 컬러의 R 채널을 가져와 높이 가중치()로 곱해줍니다.
2단계: C# 인터랙터 위치 전달 스크립트 작성
플레이어 캐릭터의 트랜스폼 좌표를 매 프레임 전역 셰이더 프로퍼티로 바인딩합니다.
using UnityEngine;
[ExecuteAlways]
public class FoliageInteractor : MonoBehaviour
{
[Header("Interaction Settings")]
[SerializeField] private float pushRadius = 1.5f;
[SerializeField] private float pushStrength = 1.0f;
private static readonly int PlayerPosID = Shader.PropertyToID("_PlayerPosition");
private static readonly int InteractionParamsID = Shader.PropertyToID("_FoliageInteractionParams");
private void Update()
{
Vector3 playerPos = transform.position;
// W 성분에 반경(Radius)을 함께 패킹하여 전달
Shader.SetGlobalVector(PlayerPosID, new Vector4(playerPos.x, playerPos.y, playerPos.z, pushRadius));
Shader.SetGlobalVector(InteractionParamsID, new Vector4(pushStrength, 0, 0, 0));
}
}
3단계: HLSL 버텍스 변위 함수 구현
Unity URP(Universal Render Pipeline) 환경의 HLSL 버텍스 셰이더 내부에 버텍스 오프셋 함수를 추가합니다.
// C#에서 설정한 전역 변수
float4 _PlayerPosition; // xyz: 위치, w: 영향 반경(Radius)
float4 _FoliageInteractionParams; // x: 밀림 강도(Strength)
// 버텍스 변위 계산 함수
float3 ApplyFoliageBend(float3 worldPos, float heightMask)
{
float3 playerPos = _PlayerPosition.xyz;
float radius = max(_PlayerPosition.w, 0.001);
float strength = _FoliageInteractionParams.x;
// 플레이어에서 버텍스로 향하는 수평 벡터 계산
float3 pushDir = worldPos - playerPos;
pushDir.y = 0.0;
// Y축 높이 차이로 인한 왜곡을 방지하기 위해 수평 거리 위주로 계산
float distance = length(pushDir);
// 거리 감쇠: 중심에 가까울수록 1, 반경 끝에서는 0
float attenuation = 1.0 - saturate(distance / radius);
// 급격한 선형 꺾임을 완화하기 위한 부드러운 감쇠 곡선(Smoothstep)
attenuation = smoothstep(0.0, 1.0, attenuation);
// 정규화된 밀림 방향 (완전 수평 + 약간의 하향 압력 부여)
float3 normalizedDir = normalize(float3(pushDir.x, -0.2, pushDir.z));
// 최종 오프셋: 방향 * 감쇠 * 강도 * 높이 마스크
float3 bendOffset = normalizedDir * (attenuation * strength * heightMask);
return worldPos + bendOffset;
}

풀이 바닥을 뚫고 내려가지 않게 보정하는 테크닉
1. 구형 회전(Spherical Rotation) 근사
버텍스를 단순히 선형 방향으로 더하면 풀의 전체 길이가 늘어나는 현상(Mesh Stretching)이 발생합니다. 본래 잔디는 줄기 길이를 유지하며 회전해야 합니다.
수평 이동 거리()가 커질 때 피타고라스 정리에 기반하여 Y 좌표를 일정 비율 낮춰주면 길이가 늘어나는 느낌을 억제할 수 있습니다.
2. 바람 노이즈와 인터랙션의 부드러운 블렌딩
바람 애니메이션과 인터랙션 오프셋이 단순 가산되면 캐릭터가 풀을 밟고 있을 때도 풀이 심하게 파들거릴 수 있습니다. 인터랙션 감쇠값()이 높을 때는 바람 강도를 줄이도록 가중치 보간(Lerp)을 적용하는 것이 자연스럽습니다.
// 바람에 의한 흔들림 계산
float3 windOffset = CalculateWind(worldPos, heightMask);
// 플레이어 근처에서는 바람의 영향력을 줄이고 벤딩 효과를 우선 적용
float3 finalWind = lerp(windOffset, float3(0, 0, 0), attenuation);
float3 finalWorldPos = ApplyFoliageBend(worldPos + finalWind, heightMask);
자주 묻는 질문 (FAQ)
Q1. 수많은 풀 인스턴스에 적용할 때 드로우콜이나 성능 저하는 없나요?
글로벌 벡터 방식은 머티리얼 속성을 인스턴스마다 변경하지 않고 전역 셰이더 상수 버퍼(Constant Buffer)를 공유합니다. 따라서 GPU 인스턴싱(GPU Instancing) 및 SRP 배처(SRP Batcher) 배칭이 깨지지 않아 수만 개의 잔디를 렌더링해도 추가 드로우콜이 발생하지 않습니다.
Q2. 플레이어 외에 다수의 몬스터나 투사체에도 반응하게 하려면 어떻게 하나요?
상호작용 객체가 4~8개 이내라면 Vector4[] 배열을 Shader.SetGlobalVectorArray로 전달하여 반복문으로 누적 연산할 수 있습니다. 상호작용 객체가 수십 개 이상이거나 밟고 지나간 잔디가 서서히 일어나는 트레일 효과가 필요하다면 렌더 텍스처 기반의 버텍스 디스플레이스먼트 기법으로 전환해야 합니다.
Q3. 풀이 밀려난 뒤 복원될 때 통통 튀는 탄성을 주려면 어떻게 해야 할까요?
셰이더 내부에서 감쇠 진동 수식인 형태의 시간 기반 감쇠 함수를 사용하거나 렌더 텍스처 트레일 방식에서 텍스처 페이드아웃 곡선에 스프링 노이즈를 결합하여 자연스러운 탄성 복원 연출을 구현할 수 있습니다.
요약 및 권장 적용 가이드
- 버텍스 컬러 마스크: 3D 풀 에셋의 Root(0.0)와 Tip(1.0)에 높이 가중치를 정확히 베이크합니다.
- 글로벌 프로퍼티 전달: CPU 스크립트에서 위치와 영향 반경을
Shader.SetGlobalVector로 전달합니다. - 부드러운 감쇠와 마스킹: 버텍스 셰이더에서
smoothstep감쇠와 높이 마스크를 적용해 물리 연산 없이 고성능 인터랙티브 환경을 완성합니다.



