들어가며
최근 고객사의 요청이 있던 부분과 관련 된 Edge Fusion은 Unity URP에서 오브젝트 간 경계를 자연스럽게 블렌딩하는 포스트 프로세싱 효과입니다. 블렌더에서 구현 된 것을 이 전에 본적이 있어서 구현 준비를 했습니다만 Kronnect 에서 어제 릴리스를 했습니다. 본 글에서는 이를 구성하는 3가지 핵심 알고리즘에 대해 살펴보겠습니다.
1. ObjectID 패스를 통한 오브젝트 식별
문제 정의
일반적인 렌더링 과정에서는 최종 화면에 색상 정보만 남게 되며 오브젝트 정보는 소실됩니다. Edge Fusion이 엣지를 정확하게 찾기 위해서는 각 픽셀이 어떤 오브젝트에 속하는지 식별할 수 있어야 합니다.
해결책: ObjectID Texture 생성
EdgeFusionRenderPass는 별도의 렌더 패스를 통해 모든 오브젝트를 고유 ID로 렌더링하는 방식을 사용합니다.
ObjectID 생성 방법
// 1단계: 오브젝트 위치를 기반으로 고유값 생성
float3 p = TransformObjectToWorld(float3(1, 1, 1));
float objectID = dot(p, 1.0); // x + y + z
// 2단계: Instancing ID 추가
#if UNITY_ANY_INSTANCING_ENABLED
objectID += unity_InstanceID;
#endif
// 3단계: 커스텀 ID가 있으면 덮어쓰기
if (_CustomObjectId > 0.5)
objectID = _CustomObjectId;
// 4단계: 정수로 변환
output.objectID = floor(objectID);
ObjectID Texture 구조
단순히 ID만 저장하는 것이 아니라, 4개 채널에 여러 정보를 패킹하여 저장합니다.
return float4(packedR, rawDepth, normalVS.xy);
채널별 의미:
- R 채널: ObjectID와 Radius를 인코딩 (정수부=ID, 소수부=radius)
- G 채널: 원시 깊이값 (카메라 Z 버퍼)
- BA 채널: View Space Normal의 XY 성분 (Intra-Object Fusion용)
원리 이해
오브젝트 A의 World Position: (5, 10, 3)
→ objectID = 5 + 10 + 3 = 18
오브젝트 B의 World Position: (2, 7, 1)
→ objectID = 2 + 7 + 1 = 10
같은 위치의 인스턴스:
→ objectID = 18 + InstanceID(1) = 19
중요성
이러한 방식을 통해 셰이더는 픽셀 단위로 인접한 픽셀이 동일한 오브젝트에 속하는지 여부를 판단할 수 있으며, 이를 통해 엣지를 감지할 수 있습니다.
// Blend Pass에서 사용 예시
float myObjectID = UnpackObjectId(objectIDTexture[currentPixel].r);
float neighborObjectID = UnpackObjectId(objectIDTexture[neighborPixel].r);
if (myObjectID != neighborObjectID) {
// 다른 오브젝트 = 엣지 발견
}
2. 방사형 샘플링과 Binary Search를 활용한 정밀한 엣지 위치 탐색
문제 정의
상하좌우 4방향만 체크하는 단순한 방식으로는 대각선 방향의 엣지를 놓칠 수 있습니다.
1단계: 방사형 샘플링 (Radial Sampling)
현재 픽셀을 중심으로 원형으로 여러 방향을 샘플링하는 방식을 사용합니다.
// EdgeFusionBlendPass.hlsl (의사코드)
for (int i = 0; i < sampleCount; i++) {
// 360도를 sampleCount로 나눔
float angle = (i / sampleCount) * TWO_PI;
// 방향 벡터 계산
float2 direction = float2(cos(angle), sin(angle));
// 현재 픽셀로부터 radius만큼 떨어진 위치 샘플링
float2 sampleUV = currentUV + direction * radius;
// 해당 위치의 ObjectID 확인
float neighborObjectID = SampleObjectID(sampleUV);
if (neighborObjectID != myObjectID) {
// 엣지 발견
}
}
샘플링 시각화
sampleCount = 8인 경우:
7
6 ↑ 0
\|/
5 ←--[나]--→ 1
/|\
4 ↓ 2
3
8방향으로 샘플링
각도: 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°
sampleCount에 따른 정밀도
- sampleCount = 4 (Very Low): 낮은 정밀도, 높은 성능
- sampleCount = 24 (High): 높은 정밀도, 낮은 성능
- sampleCount = 32 (Very High): 최고 정밀도, 최저 성능
2단계: Binary Search를 통한 정밀화
방사형 샘플링을 통해 엣지가 존재하는 방향을 파악할 수 있지만, 정확한 거리는 알 수 없습니다. 이를 해결하기 위해 이진 탐색 기법을 적용합니다.
// 1. 초기 범위 설정
float minDist = 0.0; // 내 위치
float maxDist = radius; // 최대 샘플링 거리
// 2. 이진 탐색 반복 (binarySearchSteps 횟수만큼)
for (int step = 0; step < binarySearchSteps; step++) {
// 중간 지점 샘플링
float midDist = (minDist + maxDist) * 0.5;
float2 midUV = currentUV + direction * midDist;
float midObjectID = SampleObjectID(midUV);
if (midObjectID == myObjectID) {
// 아직 내 오브젝트 영역
minDist = midDist;
} else {
// 다른 오브젝트 영역
maxDist = midDist;
}
}
// 3. 최종 엣지 거리
float edgeDistance = (minDist + maxDist) * 0.5;
Binary Search 과정 시각화
binarySearchSteps = 3 예시:
Step 0: 초기 범위
[나:5]================================[이웃:12]
0m 0.1m
↓ 중간 체크 (0.05m)
[나:5]================|===============[이웃:12]
(ID=5, 아직 내 영역)
→ minDist = 0.05m
Step 1: 범위 좁히기
[나:5]========|========[이웃:12]
0.05m 0.075m 0.1m
↓ 중간 체크
[나:5]====|=====[이웃:12]
(ID=12, 지나침)
→ maxDist = 0.075m
Step 2: 더 좁히기
[나:5]==|==[이웃:12]
0.05 0.0625 0.075
↓ 중간 체크
[나:5]=|=[이웃:12]
(ID=5, 아직 내 영역)
→ minDist = 0.0625m
최종: 엣지는 약 0.0625m ~ 0.075m 사이
→ 평균: 0.06875m
정밀도 비교
- binarySearchSteps = 2: ±0.025m 오차
- binarySearchSteps = 5: ±0.003m 오차
- binarySearchSteps = 8: ±0.0004m 오차 (0.4mm)
최적화: Early Exit Hits
모든 방향을 검사하지 않고 충분한 수의 엣지를 발견하면 조기 종료하는 최적화 기법을 사용합니다.
int edgeHitCount = 0;
for (int i = 0; i < sampleCount; i++) {
// 샘플링 과정
if (foundEdge) {
edgeHitCount++;
// 충분한 엣지를 발견하면 중단
if (edgeHitCount >= earlyExitHits) {
break;
}
}
}
효과:
- earlyExitHits = 1: 첫 엣지 발견 시 즉시 종료 (최고 성능)
- earlyExitHits = 5: 5개 엣지 발견 후 종료 (균형)
- earlyExitHits = 32: 모든 샘플 검사 (최고 품질)
3. 거리 기반 블렌딩을 통한 자연스러운 경계 처리
문제 정의
단순히 50:50 비율로 색상을 혼합할 경우 부자연스러운 결과가 발생합니다. 거리에 따라 부드럽게 감쇠(falloff)시키는 처리가 필요합니다.
Falloff 함수 (감쇠 곡선)
// 엣지까지의 거리 정규화
float normalizedDistance = edgeDistance / radius;
// 0.0 = 엣지 바로 위
// 1.0 = 최대 블렌딩 거리
// 감쇠 곡선 계산 (smoothstep)
float falloff = 1.0 - normalizedDistance;
falloff = smoothstep(0.0, 1.0, falloff);
// 0에 가까울수록 블렌딩 약함
// 1에 가까울수록 블렌딩 강함
감쇠 곡선 시각화

블렌딩 공식
여러 방향에서 발견한 엣지들을 가중 평균 방식으로 블렌딩합니다.
// 1. 각 방향의 가중치 계산
float totalWeight = 0.0;
float3 blendedColor = float3(0, 0, 0);
for (each edgeDirection) {
float dist = edgeDistances[i];
float weight = CalculateFalloff(dist, radius);
// 엣지 너머의 색상 샘플링
float3 neighborColor = SampleColor(edgePositions[i]);
// 가중치 누적
blendedColor += neighborColor * weight;
totalWeight += weight;
}
// 2. 정규화
if (totalWeight > 0) {
blendedColor /= totalWeight;
}
// 3. 원본 색상과 블렌딩
float3 originalColor = SampleColor(currentPixel);
float3 finalColor = lerp(originalColor, blendedColor, intensity * globalFalloff);
실제 적용 예시
상황: 빨간 큐브와 파란 구가 맞닿아 있는 경계선 근처 픽셀 분석
1. ObjectID 확인
→ 내 ID = 5 (빨간 큐브)
2. 방사형 샘플링 (8방향)
- 0° (→): ID=5 (동일, 엣지 없음)
- 45° (↗): ID=5 (동일)
- 90° (↑): ID=12 (상이, 엣지 발견, 거리=0.03m)
- 135° (↖): ID=12 (상이, 엣지 발견, 거리=0.04m)
- 180° (←): ID=5 (동일)
- 나머지 방향도 검사
3. Binary Search로 정밀화
- 90° 방향 엣지: 정확히 0.028m
- 135° 방향 엣지: 정확히 0.037m
4. 가중치 계산 (radius=0.05m)
- 90° 가중치: 1.0 - (0.028/0.05) = 0.44
- 135° 가중치: 1.0 - (0.037/0.05) = 0.26
5. 블렌딩
- 90° 위치의 파란색: RGB(0, 0, 255) * 0.44
- 135° 위치의 파란색: RGB(0, 0, 255) * 0.26
- 가중 평균 계산
- 원본 빨간색과 혼합
6. 최종 색상
RGB(255, 0, 0) → RGB(180, 0, 75) (약간 보라빛)
→ 경계가 부드럽게 처리됨
추가 기능
Shadow Protection
// 그림자 영역 감지
float shadow = 1.0 - saturate(luminance(originalColor) / threshold);
// 그림자에서는 블렌딩 약화
blendStrength *= (1.0 - shadow * shadowProtection);
적용 이유: 그림자 경계는 실제 오브젝트 경계가 아니므로, 블렌딩 시 부자연스러운 결과가 발생할 수 있습니다.
Noise
// 3D 노이즈 텍스처 샘플링
float noise = tex3D(noiseTex, worldPos * noiseScale);
// 블렌딩 반경에 노이즈 추가
float adjustedRadius = radius * (1.0 + noise * noiseIntensity);
효과: 기계적이지 않은 자연스러운 변화를 연출합니다.
Max Screen Radius
// 화면 공간에서 radius 계산
float screenRadius = WorldRadiusToScreenRadius(radius, depth);
// 최대값 제한
screenRadius = min(screenRadius, maxScreenRadius * screenHeight);
적용 이유: 먼 거리의 오브젝트에서 과도한 블렌딩이 발생하는 것을 방지합니다.
기술적 우수성
Edge Fusion의 기술적 이점
- 엣지 영역만 정확하게 타겟팅
- 오브젝트 인식 기반 처리
- 거리에 따른 자연스러운 감쇠
- 디테일을 유지하면서 경계만 부드럽게 처리
성능과 품질의 균형
Quality Preset 비교
| Preset | Sample Count | Binary Search | Early Exit | 권장 용도 |
|---|---|---|---|---|
| Very Low | 4 | 2 | 1 | 모바일, 저사양 환경 |
| Low | 8 | 4 | 2 | 일반 게임 |
| Medium | 16 | 5 | 3 | 균형잡힌 설정 |
| High | 24 | 7 | 4 | 고품질 게임 |
| Very High | 32 | 8 | 5 | 시네마틱, 스크린샷 |
최적화 권장사항
- maxBlendDistance 설정을 통해 먼 거리의 오브젝트를 처리 대상에서 제외
- maxScreenRadius를 활용하여 화면 공간 제한 적용
- earlyExitHits 값을 낮춰 조기 종료 빈도 증가
- Quality Preset을 통한 일괄 조정
마치며
Edge Fusion은 다음 3단계의 알고리즘을 통해 자연스러운 엣지 블렌딩을 구현합니다.
- ObjectID 패스: 각 오브젝트를 고유 ID로 식별
- 방사형 샘플링과 Binary Search: 정밀한 엣지 위치 탐색
- 거리 기반 블렌딩: 부드러운 감쇠 곡선을 통한 자연스러운 경계 처리
이러한 기술들의 조합을 통해 디테일을 유지하면서도 경계를 부드럽게 처리하는 고품질 렌더링 효과를 달성할 수 있습니다.
'UNITY3D' 카테고리의 다른 글
| Greedy Meshing(그리디 메싱) 이론, 처리 구조, 활용 이유 및 Voxel Chunk(복셀 청크) (0) | 2025.09.27 |
|---|---|
| Voxel 게임의 최적화 기술 및 최신 Voxel 렌더링 최적화 트렌드 분석 (0) | 2025.09.26 |
| URP 기술적 개선 제안 (2025.09.15 추가) (0) | 2025.09.15 |
| Unity 6.2.1 HDRP vs URP 셰이더 코드 비교 연구 (1) | 2025.09.07 |
| MagicaCloth2 Dynamic Optimizer (0) | 2025.07.22 |