Co
concode
Sign In
DevLog

프레임이 떨어져요..

Concode
Dec 21, 20256m ago

문제 상황: PC에서 프레임 드랍 발생?

잔디가 잔뜩 배치된 씬을 구성한 뒤, 에디터(Mac M2 환경)에서 눈에 띄는 프레임 드랍이 발생했다.
잔디 오브젝트는 6개의 custom sprite로 구성된 비교적 단순한 구조였다.
겨우 이 정도인데 프레임이 떨어진다고?
Profiler (Analysis > Profiler > Render) 를 확인했다.
SetPass Calls: 5.1k
Draw Calls: 5.2k
Batches: 5232
Triangles: 66.9k
Vertices: 63.8k

(Dynamic Batching): 0
(Static Batching): 0
(Instancing): 0

Shadow Casters: 1802
수치만 봐도 상황은 심각했다.

핵심 문제 요약

1.
Draw Calls / SetPass Calls 과다
•
매 프레임 5천 번 이상 드로우 → PC 에디터에서도 부담
•
모바일 환경에서는 사실상 치명적
2.
배칭 & 인스턴싱이 전부 0
•
Static Batching / GPU Instancing이 전혀 잡히지 않음
•
구조적으로 "아무 것도 묶을 수 없는 상태"일 가능성
3.
Shadow Casters 1802개
•
그림자 캐스터 수가 과도하게 많음

1차 개선: 메시 컴바인 (Mesh Combine)

배칭이나 인스턴싱을 적용하기 전에, 가장 단순하고 확실한 방법부터 적용했다.

접근 방식

•
잔디 하나당 6개로 나뉜 sprite를 하나의 combined mesh로 통합
•
잔디 오브젝트 기준으로 드로우콜 1/6 감소
메시를 합치는 코드는 GPT를 활용해 빠르게 작성했다.
예전처럼 에셋스토어나 GitHub를 뒤질 필요가 없어 생산성이 크게 올라간 느낌이었다.

결과

SetPass Calls: 2.0k
Draw Calls: 2.1k
:
Shadow Casters: 796

개선 효과

•
Draw Calls: 5.2k → 2.1k
•
Shadow Casters: 1802 → 796
이 단계만으로도 체감 성능이 크게 좋아졌다.

그림자 처리 최적화

그림자는 생각보다 훨씬 비싼 연산이다. URP 설정을 중심으로 정리했다.

주요 포인트

•
Cascading Shadow 수는 최소화
◦
가능하면 1~2개
•
Shadow Max Distance는 작을수록 좋음
•
카메라 밖 오브젝트도 그림자를 그리는 문제
◦
청크 단위로 inactive
◦
또는 Renderer.enabled = false 처리

청크(Chunk) 시스템 적용

자체 제작한 Voxel 스타일 타일 시스템은 처음부터 청크 개념을 염두에 두고 설계했지만,
실제로 on/off 하는 로직은 사용하지 않고 있었다.

개선 내용

•
카메라 위치 기준으로 청크 활성 / 비활성 처리
•
월드 오브젝트도 청크 시스템에 포함

Active 방식별 특징

방식
장점
단점
GameObject Inactive
스크립트도 멈춤 → 성능 최적
GC 발생 가능
재활성 시 스파이크 가능
Renderer.enabled
렌더링 부하 감소
스크립트는 계속 실행됨
CPU 부하 여전

배칭 vs 인스턴싱 개념 정리

둘 다 CPU 부담을 줄이는 목적이지만, 접근 방식은 완전히 다르다.
구분
배칭 (Batching)
인스턴싱 (Instancing)
핵심
여러 메쉬를 하나로 묶음
같은 메쉬를 여러 번 그림
처리 위치
CPU
GPU
메쉬
서로 달라도 가능
완전히 동일해야 함
머티리얼
같아야 함
같아야 함
오브젝트별 데이터
거의 불가
float / color 등 가능
텍스처 변경
❌
❌
대표 예
SRP Batcher
GPU Instancing
이 프로젝트는 같은 오브젝트를 대량으로 배치하는 구조(잔디 등)라
GPU Instancing이 핵심 목표였다.

인스턴싱이 깨졌던 이유 (프로젝트 기준)

1) Material 직접 접근

•
Material.SetXXX 호출 시 즉시 인스턴싱 깨짐
•
반드시 MaterialPropertyBlock(MPB) 사용 필요

2) MPB + SetTexture 문제

•
MPB를 써도 SetTexture 호출 시 인스턴싱이 깨짐
•
같은 텍스처를 넣어도 깨진다
•
기존엔 텍스처 단위로 Material 만든게 아니라, 템플릿 Material를 하나만 두고 Mpb에서 SetTexture 하는 구조

해결 방법

•
방법1: Material 분리 (수백개 이상의 Material을 만드는 노가다 필요)
•
방법2: 텍스처별 Material 캐싱 (수백개의 머트리얼을 안만들고 기존 구조 유지)
public Material ReadyMaterial(Material template, Sprite sprite) {
    // 생략

    // 해시키 생성
    var key = template.GetHashCode();
    if (sprite != null) {
        key ^= sprite.texture.GetHashCode() << 2;
    }
    
    // 캐싱된 머트리얼 반납
    if (materials.TryGetValue(key, out var data)) {
        data.referenceCount++;
        return data.material;
    }

    // 텍스처별 매터리얼 생성
    var mat = new Material(template);

    if (sprite != null) {
	    // 메인 텍스처 세팅
        mat.SetTexture(MainTex, sprite.texture);

	    // 세컨더리 텍스처 세팅
        var secondaryTextureCount = sprite.GetSecondaryTextureCount();
        if (secondaryTextureCount > 0) {
            // ..
            foreach (SecondarySpriteTexture second in secondaryTextureBuffer) {
                // ..
                mat.SetTexture(pid, second.texture);
            }
        }
    }

	// 텍스처 캐싱
    materials.Add(key, new MaterialData {
        referenceCount = 1,
        material = mat,
    });

    return mat;
}

3) Mesh 객체 공유 문제

•
인스턴싱은 "값이 같은 Mesh"가 아니라 "같은 Mesh 객체" 여야 한다
•
meshFilter.sharedMesh에 값만 같은 Mesh를 넣어도 인스턴싱 실패

4) 자체 Sprite System 구조적 문제

•
스프라이트 시트 애니메이션을 위해 Mesh UV를 직접 수정
•
이 방식은 Mesh 공유가 불가능 → 인스턴싱 불가

대안 검토

•
Shader에 UV 프로퍼티로 전달 방식
•
하지만 모든 쉐이더 수정은 비용 과다 → 보류
다행히 인스턴싱이 필요한 오브젝트 대부분은 스프라이트 애니메이션이 없는 잔디류였기 때문에,
해당 오브젝트에 한해서
Mesh 공유 방식으로 구조 수정
(총 20여 종의 자체제작 Custom Sprite Renderer 수정)

7. 최종 결과

SetPass Calls: 135
Draw Calls: 409
Batches: 409
Triangles: 28.6k
Vertices: 40.8k

(Instancing)
Batched Draw Calls: 1.5k
Batches: 307
Triangles: 23.2k
Vertices: 29.8k

Shadow Casters: 676

최종 성능 개선 요약

•
Draw Calls: 5.2k → 409
•
Instancing: 0 → 1.5k
•
Shadow Casters: 1802 → 676

8. 기타 전역 성능 최적화

URP Render Scale

•
픽셀 게임 특성상 약간 낮춰도 위화감 적음
•
renderScale = 0.5
◦
렌더 타겟 크기 = 1/4
◦
성능 개선 효과 매우 큼
•
단, 앞선 최적화가 없으면 퀄리티만 떨어질 뿐 프레임은 그대로

Point Light Shadow

•
모바일에서 특히 비용이 큼
•
옵션으로 켜고 끌 수 있게 설정 제공

마무리하며

이번 작업을 통해 PC 환경이라고 해서 성능을 안일하게 생각하면 안 된다는 걸 다시 느꼈다.
에디터에서 이미 부담이 되는 구조는, 모바일에서는 더 큰 문제가 된다.
또한 Frame Debugger와 Profiler는 필수 도구라는 걸 체감했다.
막연한 감이 아니라 수치를 통해 문제 지점을 명확히 확인할 수 있었고, 인스턴싱이 왜 깨지는지 까지 알려준다.
덕분에 어디부터 손대야 할지 판단하기 쉬웠다.
마지막으로 ChatGPT의 도움으로 분석 방향을 잡는 데 많은 도움이 됐다. 프로파일러 결과를 기반으로 핵심 병목을 빠르게 짚을 수 있었고, 시행착오를 줄이며 효율적으로 최적화를 진행할 수 있었다.
Co
'concode' 구독하기
사이트를 구독하면 새 포스트 등 최신 업데이트를 알림과 메일로 가장 먼저 받아보실 수 있습니다.
Slashpage에 가입하고 'concode'을 구독하세요!
구독
👍
6