전체 글 (115)

04
22

 

프로젝트 진행하면서 냉동 액션 쉐이더가 패스에서 버텍스 얼마나 쓰는지 알고 싶어서 찾아보다가 디버깅해볼 수 있는 Frame Debugger을 알게되었습니다.

레퍼런스 영상은 아래와 같습니다.

https://youtu.be/w14yjBlfNeQ?si=d8jfxsjaqnliI8hU

 


그래픽스 렌더링과 프레임 디버거 기초

렌더링 (Rendering): 3D 모델 데이터(정점, 텍스처 등)를 사용해서 화면에 2D 이미지를 만들어내는 과정입니다.

게임이나 그래픽 애플리케이션에서 우리가 보는 화면은 이러한 렌더링 과정을 통해 만들어집니다.

 

CPU와 GPU: 렌더링 과정에서는 CPU와 GPU가 협력합니다.

CPU (Central Processing Unit): 어떤 물체를 어디에, 어떻게 그릴지 결정하고, 렌더링 명령(Command)을 만들어 GPU에 전달하는 역할을 주로 합니다.

GPU (Graphics Processing Unit): CPU로부터 받은 명령을 바탕으로 실제 복잡한 그래픽 연산을 수행하여 픽셀 색상을 계산하고 화면에 이미지를 그리는 역할을 합니다. 병렬 처리에 특화되어 있습니다.

 

프레임 디버거 (Frame Debugger): 특정 시점(프레임)에 CPU가 GPU에게 보낸 모든 명령들과 GPU의 상태를 캡처하여 보여주는 도구입니다. RenderDoc, PIX, Xcode Frame Debugger 등이 있습니다. 개발자는 이를 통해 렌더링 파이프라인의 각 단계를 자세히 들여다보고, 문제가 되는 부분을 찾아내거나 최적화할 부분을 파악할 수 있습니다. 버텍스가 어떻게 변환되고 그려지는지 추적하는 데 매우 유용합니다.

 

렌더 패스 (Render Pass) 이해하기

렌더링 명령의 묶음: 최신 그래픽스 API(Vulkan, DirectX 12, Metal 등)에서는 효율성을 위해 관련된 렌더링 명령들을 '렌더 패스'라는 단위로 묶어서 처리하는 경우가 많습니다.

 

렌더 패스는 "어떤 렌더 타겟(결과 이미지가 저장될 메모리 버퍼, 예를 들어 화면이나 특정 텍스처)에 그릴 것인가?" 그리고 "그 렌더 타겟을 어떻게 시작하고(예: 특정 색으로 초기화) 어떻게 끝낼 것인가?"를 정의합니다.

상태 설정: 하나의 렌더 패스 내에서는 일반적으로 동일한 렌더 타겟 설정을 공유하며, 관련된 그리기 명령(Draw Call)들이 이어서 실행됩니다.

 

SetPass Call

명령의 시작: BeginRenderPass는 CPU가 GPU에게 "이제 새로운 렌더 패스를 시작할 거야"라고 알리는 명령입니다.

GPU 준비시키기: 이 호출을 통해 CPU는 GPU에게 해당 렌더 패스에서 사용할 렌더 타겟(들), 초기화 방법(예: 배경색), 깊이/스텐실 버퍼 설정  렌더링 환경 설정을 전달합니다.

업 지시: SetPass Call이후에 오는 그리기 명령(Draw Call)들은 여기서 설정된 환경(렌더 타겟, 상태 등) 위에서 실행됩니다.

즉, "이 설정대로 다음 그리기 작업들을 수행해!"라는 지시의 시작점인 셈입니다.

 

SetPass Call 자체는 버텍스 데이터를 직접 다루는 명령은 아닙니다. 하지만 이 호출은 버텍스 셰이더를 포함한 후속 그리기 명령(Draw Call)들이 어떤 환경(렌더 타겟, 뷰포트, 기타 상태)에서 실행될지를 결정합니다.

 

디버깅의 핵심: 프레임 디버거에서 버텍스 관련 문제를 분석할 때, 문제가 발생한 그리기 명령(Draw Call)이 어떤 렌더 패스에 속해 있는지 파악하는 것이 매우 중요합니다. SetPass Call이 렌더 패스의 시작을 알리는 중요한 마커 역할을 합니다. 특정 버텍스가 이상하게 보이거나 잘못된 위치에 그려진다면, 해당 Draw Call이 속한 Pass의 설정(SetPass Call에서 지정된)부터 확인하여 렌더 타겟 설정이나 관련 상태가 올바른지 검토해야 합니다.

 

Draw Call

드로우콜은 배치(Batch)라고도 합니다. '배치(Batch) 수를 줄이는 최적화'는 결국 '화면에 무엇을 그리는가?'와 연결됩니다.

 

 

위의 사진은 유니티엔진에서 사용되는 다양한 렌더러 컴포넌트(Renderer Components)들을 볼 수 있습니다. 해당 컴포넌트들은 씬(Scene)에 있는 게임 오브젝트(Game Object)가 화면에 어떻게 그려질지를 담당하며, 렌더러 컴포넌트를 가진 게임 오브젝트들이 배칭의 대상이 됩니다.

 

렌더러와 배칭의 관계 설명

CPU가 GPU에게 내리는 그리기 명령(Draw Call, 즉 배치)은 바로 이 렌더러 컴포넌트들을 처리하기 위한 것입니다. 따라서 씬에 렌더러 컴포넌트가 많고, 각 컴포넌트가 사용하는 머티리얼(Material)이나 설정이 다르면 배치 수가 증가하게 됩니다.

 

결국, 배치 수를 줄이기 위한 최적화는 씬에 있는 이러한 렌더러 컴포넌트들을 어떻게 효율적으로 관리하는가의 문제입니다.
같은 머티리얼을 최대한 공유하도록 하고 (예: 텍스처 아틀라스 사용), 스태틱 배칭이나 GPU 인스턴싱 같은 엔진 기능을 적극적으로 활용하는 것이 중요합니다.

 

Frame Debugger 분석

Frame Debugger는 렌더링 파이프라인을 시각적으로 분석하고 최적화 포인트를 찾는 데 매우 강력한 도구입니다. 

상단탭 Window > Analysis > Frame Debugger로 창을 열 수 있습니다. 

이번에는 특정 커스텀 셰이더(`CustomFX/Ice`)를 사용하는 배치를 Frame Debugger로 분석하는 과정을 살펴보겠습니다.

위 사진은 Frame Debugger에서 특정 프레임의 렌더링 이벤트들을 보여줍니다.

왼쪽에는 이벤트 목록이 계층 구조로 나타나고, 오른쪽에는 선택된 이벤트(#71 SRP Batch)의 상세 정보가 표시됩니다.
먼저 선택된 배치(#71)의 기본 정보를 확인합니다.
RenderTarget:'_CameraNormalsTexture_...`
이 배치는 화면에 직접 그리는 것이 아니라, 카메라의 Normal 텍스처를 생성하는 패스(아마도 Deferred Shading의 G-Buffer 생성 단계나 별도의 Normal Prepass)에 그려지고 있음을 알 수 있습니다. 렌더링 결과가 의도한 텍스처에 올바르게 쓰이는지 확인하는 시작점입니다.

 

렌더링 상태 (Render States):
`ZClip: True`, `ZTest: LessEqual`, `ZWrite: On` 

-> 깊이 버퍼를 사용하고, 기존 픽셀보다 가깝거나 같은 경우에만 그리며, 자신의 깊이 값을 기록합니다. 일반적인 불투명 오브젝트의 설정입니다.

`Cull: Back`: 뒷면 컬링이 활성화되어 있습니다. 얼음 메쉬의 뒷면은 그리지 않습니다.
`Blend Color/Alpha: One / Zero`, `BlendOp Color/Alpha: Add`: 블렌딩이 비활성화된 상태(덮어쓰기)입니다.

`CustomFX/Ice` 셰이더가 반투명 효과를 의도했다면 이 부분이 잘못 설정되었을 수 있습니다.

(하지만 Normal 텍스처 생성 패스라면 불투명으로 처리하는 것이 일반적입니다.)
`ColorMask: RGBA`: 모든 컬러 채널에 쓰기 작업을 수행합니다.

이제 드로우 콜 및 배치 정보 분석해봅시다
Draw Calls: 7 이 하나의 SRP 배치는 내부적으로 7개의 개별 드로우 콜을 포함하고 있습니다. 이는 7개의 다른 오브젝트(또는 메쉬 파트)가 이 배치에서 그려진다는 의미입니다.

Vertices: 12026, Indices: 47076

이 배치에서 처리되는 총 정점 및 인덱스 수입니다. 이 수치가 과도하게 높다면 메쉬 최적화나 LOD(Level of Detail) 적용을 고려해볼 수 있습니다.

Batch cause: SRP: Node use different shader keywords
Frame Debugger는 주로 이부분을 통해 최적화할 때 판단할 수 있습니다. 

사진에서의 메시지는 SRP Batcher가 여러 오브젝트를 하나의 배치로 묶으려고 시도했지만, 각 오브젝트(또는 노드)가 서로 다른 셰이더 키워드(Shader Keywords) 조합을 사용하고 있기 때문에 이 배치가 생성되었거나 혹은 완전히 하나의 거대 배치로 묶이지 못했음을 나타냅니다.

 

분석 포인트 `Keywords` 섹션을 확장하여 어떤 키워드들이 활성화되어 있는지 확인해야 합니다. 만약 불필요하게 다양한 키워드 조합이 사용되고 있다면 (예: 머티리얼 설정 실수, 코드 로직 문제 등), 키워드 수를 줄여 배칭 효율을 개선할 여지가 있습니다. 예를 들어, 특정 기능(예: 반사 효과)을 켜고 끄는 키워드가 여러 오브젝트에서 다르게 설정되어 있다면, 이를 통일하거나 셰이더 로직을 변경하여 키워드 의존성을 줄이는 것을 고려할 수 있습니다.

위에서 우리는 이 배치의 생성 원인(Batch cause)이 "SRP: Node use different shader keywords" 임을 확인했습니다. 즉, SRP Batcher가 여러 오브젝트를 하나의 배치로 묶으려 했지만, 사용된 셰이더 키워드 조합이 달라서 현재의 배치(#71)로 나뉘었다는 뜻입니다. 이제 키워드 섹션을 자세히 살펴보겠습니다.


`_USE_OUTLINE_ON` (활성화됨, 범위: Local):

이 키워드는 이름으로 보아 외곽선(Outline) 렌더링 기능을 켜는 역할을 하는 것으로 보입니다.

범위(Scope)가 'Local'로 되어 있습니다. 이는 이 키워드가 각 머티리얼 인스턴스별로 다르게 설정될 수 있음을 의미합니다.

배칭 영향: 만약 `CustomFX/Ice` 셰이더를 사용하는 여러 오브젝트(예: `seahorse`, `merman` 등) 중 일부는 외곽선이 켜져 있고 일부는 꺼져 있다면, SRP Batcher는 이들을 하나의 배치로 묶을 수 없습니다. 키워드 조합이 다르기 때문입니다. 이것이 바로 "Node use different shader keywords" 메시지가 나타난 가장 유력한 원인일 수 있습니다.
`_WRITE_RENDERING_LAYERS` (활성화됨, 범위: Global):
이 키워드는 유니티의 렌더링 레이어(Rendering Layers) 기능과 관련된 것으로 보입니다. 프래그먼트 셰이더(fs)에서 사용되며, 범위가 'Global'입니다. 배칭 영향: 범위가 'Global'이므로, 이 키워드 자체는 보통 개별 오브젝트별로 달라지지 않습니다. 따라서 이 키워드가 직접적으로 오브젝트 간 배치를 깨뜨리는 원인일 가능성은 낮습니다. (다만, 렌더링 파이프라인 설정에 따라 간접적인 영향은 있을 수 있습니다.)

우선 `CustomFX/Ice` 셰이더를 사용하는 여러 오브젝트들이 실제로 `_USE_OUTLINE_ON` 키워드를 다르게 설정하고 있는지 확인합니다. 

사진처럼 use Outline 필드를 다 체크했더군요 
최적화 방안
    1.  키워드 통일: 만약 모든 오브젝트에 외곽선이 필요 없거나, 또는 모든 오브젝트에 동일하게 외곽선을 적용해도 된다면, 머티리얼 설정을 통일하여 `_USE_OUTLINE_ON` 키워드 상태를 같게 만들어 줍니다. 이렇게 하면 SRP Batcher가 더 많은 오브젝트를 하나의 배치로 묶어 CPU 부하를 줄일 수 있습니다.
    2.  셰이더 분리 또는 수정: 만약 오브젝트별로 외곽선 유무가 반드시 달라야 한다면, 이로 인한 배치 분리는 불가피할 수 있습니다. 하지만 이 비용을 인지하고 있어야 합니다. 경우에 따라 외곽선 기능이 없는 별도의 셰이더(`CustomFX/Ice_NoOutline` 등)를 만들어 사용하는 것을 고려해 볼 수도 있습니다.
    3.  셰이더 코드 검토: `_USE_OUTLINE_ON` 키워드가 정말 필요한 경우에만 활성화되도록 셰이더 코드나 관련 스크립트 로직을 점검합니다.

이제 Frame Debugger의 다른 항목도 살펴봅시다.

 

Meshes: seahorse, merman, FXSM_Ice_New05
이 배치에는 `seahorse`, `merman`, `FXSM_Ice_New05` 라는 이름의 메쉬들이 포함되어 있습니다. 서로 다른 메쉬들이지만 동일한 `CustomFX/Ice` 셰이더와 머티리얼 속성 (키워드 제외)을 공유하여 SRP Batcher에 의해 하나의 배치로 묶인 것으로 보입니다.

다음으로 셰이더 및 텍스처 확인해봅니다
Used Shader: CustomFX/Ice 현재 사용 중인 셰이더가 `CustomFX/Ice` 임을 보여줍니다.
LightMode: DepthNormals (Pass: DepthNormals (4))

이 배치는 `DepthNormals` 라는 LightMode 태그를 가진 셰이더 패스를 사용하고 있습니다. 이는 주로 깊이와 노멀 정보를 별도로 렌더링하는 패스에서 사용됩니다. (Deferred Shading의 G-Buffer 패스 등)
Textures: `_BackIceTex` (vs), `_NormalTex` (fs), `_VertexOffsetTex` (vs) 세 개의 텍스처가 사용됩니다.

각 텍스처가 어느 셰이더 스테이지(vs: Vertex Shader, fs: Fragment Shader)에서 사용되는지 알 수 있습니다. 텍스처 크기(512x512, 256x256)와 포맷(DXT1)도 확인 가능합니다. 텍스처 메모리 사용량이나 압축 품질이 적절한지 검토할 수 있습니다. (예: Vertex Offset 텍스처는 더 저해상도여도 괜찮을 수 있습니다.)


이 `CustomFX/Ice` 셰이더 배치를 분석한 결과, 몇 가지 포인트를 알아낼 수 있었습니다.
1.  배치 원인(Shader Keywords): 가장 주목해야 할 부분입니다. `Keywords` 섹션을 상세히 분석하여 불필요한 키워드 조합을 줄일 수 있는지 확인합니다. 이것이 배칭 성능 개선의 핵심일 수 있습니다.
2.  렌더링 상태: 현재는 불투명 오브젝트 설정이며, Normal 텍스처 생성 패스에 적합해 보입니다. 만약 다른 패스(예: Forward 렌더링)에서 반투명 효과를 의도했다면, 해당 패스의 배치를 찾아 블렌딩 설정을 확인해야 합니다.
3.  셰이더 로직 및 텍스처: `CustomFX/Ice` 셰이더 코드(특히 `DepthNormals` 패스 부분)를 검토하여 비효율적인 연산은 없는지, 텍스처 샘플링은 최적화되어 있는지 확인합니다. 사용되는 텍스처들의 해상도와 포맷이 적절한지도 평가합니다.
4.  성능 측정: Frame Debugger에서 이 배치가 소요하는 GPU 시간을 확인하여 실제 성능 병목인지 판단합니다. 병목이라면 위의 분석 포인트들을 바탕으로 최적화를 진행합니다.

이러한 단계적인 분석을 통해 Frame Debugger에서 특정 셰이더 배치의 동작을 이해하고, 문제점이나 최적화 기회를 찾아낼 수 있습니다.

다음 글에서는 배칭 기법들(스태틱 배칭, GPU 인스턴싱, 텍스처 아틀라스 등)을 실제로 어떻게 적용하는지 구체적인 예시와 함께 살펴보겠습니다. 

 

같이 읽어보시면 좋은글


https://chulin28ho.tistory.com/777

 

Depth Texture를 언제 그릴까? Depth Prepass / After Opaque

쉽게 설명해주시려고 노력해주신 고정석님께 감사 인사를 드립니다. Depth(깊이) Depth란 .. Z 버퍼라고도 불리던 그것입니다. 게임하면서 깊이값을 알아야 할 때가 있지요? 그게 depth입니다. 이게

chulin28ho.tistory.com

https://darkcatgame.tistory.com/139

 

[리메이크 예정] URP 드로우콜 최적화 이론 정리

이 포스팅에 잘못된 내용이 있고 내용도 난잡합니다, 추후 다시 정리할 예정입니다. 그 전에 맥락 참고용으로 봐주세요. Draw Call(드로우콜)이란? GPU에게 오브젝트를 화면에 그리라고 명령하는 것

darkcatgame.tistory.com

 

COMMENT
 
01
28

 

아래 공식 문서를 참고했습니다.

https://docs.unity3d.com/Packages/com.unity.physics@1.3/manual/collision-queries.html

 

Collision queries | Unity Physics | 1.3.9

Collision queries Collision queries (also known as spatial queries) are one of the most important features of any physics engine and often drive a significant amount of game logic. Unity Physics has a powerful collision query system which supports queries

docs.unity3d.com

 

Unity에서 물리 시스템은 게임 개발의 핵심 요소입니다.

오늘은 Unity의 물리 시스템 중에서 충돌 쿼리와 콜라이더 시스템에 대해 자세히 알아보겠습니다.

 

 

1. Unity 물리 시스템 개요

Unity의 물리 시스템은 크게 두 가지 방식으로 충돌을 처리합니다:

  • 콜라이더 (Collider) 시스템
  • 물리 쿼리 (Physics Query) 시스템

 

1.1 콜라이더 시스템

public class ColliderExample : MonoBehaviour
{
    void OnCollisionEnter(Collision collision)
    {
        Debug.Log("물체와 충돌했습니다!");
    }

    void OnTriggerEnter(Collider other)
    {
        Debug.Log("트리거 영역에 진입했습니다!");
    }
}

콜라이더는 게임 오브젝트에 물리적 형태를 부여하는 컴포넌트입니다. 

  • 자동 충돌 감지
  • Unity 물리 엔진과 통합
  • 지속적인 충돌 체크
  • Rigidbody와 함께 사용 시 물리 시뮬레이션 가능

 

1.2 물리 쿼리 시스템

public class PhysicsQueryExample : MonoBehaviour
{
    void Update()
    {
        // 레이캐스트
        RaycastHit hit;
        if (Physics.Raycast(transform.position, transform.forward, out hit))
        {
            Debug.Log($"레이가 {hit.collider.name}와 충돌!");
        }

        // 구체 캐스트
        Collider[] nearbyObjects = Physics.OverlapSphere(transform.position, 5f);
        foreach (var obj in nearbyObjects)
        {
            Debug.Log($"근처에 있는 오브젝트: {obj.name}");
        }
    }
}

물리 쿼리는 필요할 때 수동으로 수행하는 충돌 검사입니다. 

  • 능동적인 충돌 검사
  • 다양한 형태의 쿼리 지원 (Ray, Sphere, Box 등)
  • 레이어 마스크를 통한 필터링
  • 일회성 검사

 

쿼리에는 대표적으로 3가지가 있습니다.

 

레이캐스트 (Ray cast)

시작점, 끝점, 필터를 입력으로 사용
방향이 있는 선분의 모든 교차점을 찾습니다.


콜라이더 캐스트 (Collider cast)

콜라이더, 시작점, 끝점, 콜라이더 스케일을 입력으로 사용
주어진 경로를 따라 콜라이더를 이동시켜 충돌 검사


거리 쿼리 (Distance query)

포인트 거리 쿼리와 콜라이더 거리 쿼리로 구분
지정된 최대 반경 내의 가장 가까운 지점을 찾음

 

2. 성능 비교 분석

2.1 콜라이더 시스템 성능

public class ColliderPerformance : MonoBehaviour
{
    void Start()
    {
        // 1. 콜라이더 수에 따른 검사 횟수
        // N개의 콜라이더 = N(N-1)/2 회의 검사
        // 10개: 45회
        // 100개: 4,950회
        // 1000개: 499,500회
    }

    void Example()
    {
        // 2. 콜라이더 타입별 성능 비교
        // 단순한 순서: Sphere > Box > Capsule > Mesh
        SphereCollider sphere;    // 가장 가벼움
        BoxCollider box;          // 중간
        CapsuleCollider capsule;  // 중간
        MeshCollider mesh;        // 가장 무거움
    }
}

2.2 물리 쿼리 성능

public class QueryPerformance : MonoBehaviour
{
    [SerializeField] private LayerMask targetLayer;
    private float queryInterval = 0.1f;
    private float nextQueryTime;

    void Update()
    {
        // 최적화된 쿼리 사용
        if (Time.time >= nextQueryTime)
        {
            Physics.Raycast(transform.position, transform.forward, out hit, 100f, targetLayer);
            nextQueryTime = Time.time + queryInterval;
        }
    }
}

 

 

3. 실제 게임플레이 적용 가이드

3.1 이동 및 물리 상호작용

public class PlayerController : MonoBehaviour
{
    private CharacterController controller;
    private float groundCheckDistance = 0.2f;

    void Update()
    {
        // 이동 처리
        Vector3 moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        controller.Move(moveDirection * speed * Time.deltaTime);

        // 지면 체크
        bool isGrounded = Physics.Raycast(transform.position, Vector3.down, groundCheckDistance);

        // 점프 처리
        if (isGrounded && Input.GetButtonDown("Jump"))
        {
            // 점프 로직
        }
    }
}

3.2 전투 시스템

public class CombatSystem : MonoBehaviour
{
    [SerializeField] private float attackRadius = 2f;
    [SerializeField] private LayerMask enemyLayer;

    void ProcessMeleeAttack()
    {
        // 근접 공격 판정
        Collider[] hits = Physics.OverlapSphere(
            transform.position,
            attackRadius,
            enemyLayer
        );

        foreach (var hit in hits)
        {
            var enemy = hit.GetComponent<Enemy>();
            if (enemy != null)
            {
                enemy.TakeDamage(attackDamage);
            }
        }
    }

    void ProcessRangedAttack()
    {
        // 원거리 공격 판정
        RaycastHit hit;
        if (Physics.Raycast(firePoint.position, firePoint.forward, out hit))
        {
            var enemy = hit.collider.GetComponent<Enemy>();
            if (enemy != null)
            {
                enemy.TakeDamage(attackDamage);
            }
        }
    }
}

 

 

4. 최적화 전략

4.1 레이어 마스크 활용

public class OptimizedQueries : MonoBehaviour
{
    [SerializeField] private LayerMask interactableLayer;
    [SerializeField] private LayerMask groundLayer;

    void Update()
    {
        // 특정 레이어만 체크하여 성능 최적화
        Physics.Raycast(transform.position, transform.forward, out hit, 100f, interactableLayer);
        Physics.Raycast(transform.position, Vector3.down, out hit, groundCheckDistance, groundLayer);
    }
}

4.2 쿼리 빈도 최적화

public class QueryOptimization : MonoBehaviour
{
    private float checkInterval = 0.1f;
    private float nextCheckTime;

    void Update()
    {
        if (Time.time >= nextCheckTime)
        {
            PerformPhysicsChecks();
            nextCheckTime = Time.time + checkInterval;
        }
    }

    void PerformPhysicsChecks()
    {
        // 주기적으로 수행할 물리 검사들
        CheckEnemiesInRange();
        CheckInteractableObjects();
    }
}

 

Burst 컴파일된 Job 내에서 쿼리를 실행하면 최적의 성능을 얻을 수 있습니다.
복잡한 씬에서는 단일 레이캐스트도 스레드 호출로 전환하는 것이 좋습니다.

 

라고 공식문서에 되어있지만 이게 뭔지 몰랐습니다.

Unity.Collections - NativeArray 등을 위한 패키지
Unity.Jobs - Job System 사용을 위한 패키지
Unity.Burst - Burst 컴파일러 사용을 위한 패키지

이런 패키지들을 다운해야 적용가능한 기능이고

 

Job System: 멀티스레딩을 쉽게 구현
Burst: 코드를 더 빠른 네이티브 코드로 변환

둘을 함께 사용하면 고성능 병렬 처리 가능하다는 장점이 있습니다.
특히 많은 물리 연산이나 대규모 데이터 처리가 필요할 때 유용합니다

 

5. 결론

Unity의 물리 시스템은 상황에 따라 적절한 방식을 선택하는 것이 중요합니다.

  • 콜라이더
    • 기본적인 물리 충돌이 필요한 경우
    • 지속적인 충돌 감지가 필요한 경우
    • Rigidbody를 사용한 물리 시뮬레이션
  • 물리 쿼리
    • 공격 판정
    • 시야 체크
    • 특정 조건에서의 충돌 검사
    • 일회성 검사

두 시스템을 적절히 조합하여 사용하면 효율적이고 안정적인 게임 물리 시스템을 구축할 수 있습니다.

 

COMMENT
 
12
28

 

 

 

최근 쉐이더 분석해보던중 Ping-Pong 버퍼링을 알게되었습니다.

게임 개발에서 자주 사용되는 Ping-Pong 버퍼링(더블 버퍼링)에 대해 알아보겠습니다. 

 

Ping-Pong 버퍼링이란?

Ping-Pong 버퍼링은 두 개의 버퍼를 번갈아가며 읽기와 쓰기 작업을 수행하는 기법입니다.

마치 탁구(ping-pong)에서 공이 양쪽을 오가는 것처럼, 데이터가 두 버퍼 사이를 왔다갔다 하면서 처리됩니다.

작동 원리

 

1. 기본 구조

public class FboPingPong {
    private RenderTexture[] _buffer = new RenderTexture[2]; // 두 개의 버퍼
    private int _readTex = 0;  // 읽기 버퍼 인덱스
    private int _writeTex = 1; // 쓰기 버퍼 인덱스
}



2. 버퍼 스왑 과정

public void Swap() {
    int temp = _readTex;
    _readTex = _writeTex;
    _writeTex = temp;
}

 

 

예시

 

물감이 종이에 번지는 과정

프레임 1:
[버퍼 A] = 물감을 찍은 초기 상태
[버퍼 B] = 물감이 번진 상태를 계산

프레임 2:
[버퍼 B] = 이전에 번진 상태를 기준으로
[버퍼 A] = 더 번진 상태를 계산

 

유체 시뮬레이션


프레임 1:
[버퍼 A] = 현재 물 표면 상태
[버퍼 B] = 파동 계산 결과

 

프레임 2:
[버퍼 B] = 이전 파동 상태
[버퍼 A] = 새로운 파동 계산

코드 예시

수목화 효과

public class WatercolorEffect : MonoBehaviour {
    private FboPingPong _buffers;
    private Material _material;

    void Start() {
        // 버퍼 초기화
        _buffers = new FboPingPong(width, height);
    }

    void Update() {
        // 1. 현재 상태 읽기
        _material.SetTexture("_MainTex", _buffers.GetReadTex());
        
        // 2. 다음 상태 계산
        Graphics.Blit(_buffers.GetReadTex(), _buffers.GetWriteTex(), _material);
        
        // 3. 버퍼 스왑
        _buffers.Swap();
    }
}

 

장점


 데이터 일관성
   - 계산 중에 원본 데이터가 변경되지 않음
   - 안정적인 결과 보장

메모리 효율
   - 두 개의 버퍼만으로 복잡한 처리 가능
   - 추가 메모리 할당 최소화

성능 최적화
   - GPU 메모리 접근 최적화
   - 병렬 처리 가능

 

 

주의사항

초기화
   - 버퍼 생성 시 적절한 포맷과 크기 설정
   - 사용 후 메모리 해제 필수

동기화
   - 읽기/쓰기 작업의 순서 관리
   - 프레임 간 일관성 유지

메모리 관리
   - RenderTexture 사용 시 적절한 해제
   - 불필요한 버퍼 스왑 피하기

 

Ping-Pong 버퍼링은 게임 개발에서 복잡한 이미지 처리나 시뮬레이션을 구현할 때 필수적인 기법입니다.

특히 Unity의 셰이더 시스템에서 이 기법을 활용하면 효율적이고 안정적인 처리가 가능합니다.

이 기법을 잘 이해하고 활용하면, 물리 기반 시뮬레이션이나 고급 이미지 효과 등 다양한 기능을 구현할 수 있습니다.

 

관련 자료

https://www.reddit.com/r/FPGA/comments/xvmfrx/can_someone_explain_what_is_a_ping_pong_buffer/

 

From the FPGA community on Reddit

Explore this post and more from the FPGA community

www.reddit.com

https://discussions.unity.com/t/help-with-simple-ping-pong-buffer/945063

 

 

COMMENT
 
12
09

 

 

BFS = O(V + E)

파도타기

DFS = Θ(V + E)

미로탐험

 

위상정렬 Topoplogical Sort = Θ( |𝑉| + |𝐸| )

DFS로 만드는거, DAG 기준으로 정렬

 

크루스칼 알고리즘 =  O ( E log E) 

프림 알고리즘 = O(E log V) 

 

Dijkstra Algorithm =

Binary Heap을 사용하는 경우, O(E log V)
Fibonacci Heap을 사용하는 경우, O(V log V + E)

 

Bellman-Ford Algorithm = O(VE)

 

 
 

 

 

COMMENT