게임엔진/DOTS

유니티/DOTS2D Sprite Animation (1) - 핵심 구현 아이디어

tsyang 2023. 4. 2. 00:45

2023.03.19 - [유니티/DOTS] - 2D Sprite Animation (0) - 레퍼런스 조사

 

2D Sprite Animation (0) - 레퍼런스 조사

DOTS로 3D 애니메이션의 구현은 레퍼런스가 꽤 많다. 에셋스토어만 봐도 꽤 완성된 패키지를 팔기도 함... 반면 2D 애니메이션쪽은 쫌 레퍼런스가 적다. 그나마 있는애들도 Entites 1.0 적용 이전이라

tsyang.tistory.com

 

 

공통


 

이전 레퍼런스에서 크게 두 가지를 조사했는데,

 

https://youtu.be/t1f8ZreCuuQ

 

https://forum.unity.com/threads/1-million-animated-sprites-at-60-fps.811116/

 

1 MILLION animated sprites at 60 FPS

Hello everyone, today I want to show you what I've been working on the past few days, to celebrate my 100 stars(thank you:rolleyes:) on the...

forum.unity.com

 

코드몽키의 유튜브 강좌(이하 코드몽키)와 한 유니티 포럼에 FabrizioSpadaro 라는 사람이 쓴 글 (이하 파브리지오)임.

 

우선 화면에 sprite를 뿌려주는 핵심 메서드는 Graphics.DrawMesh를 호출하는 것이다. 

 

그리고 유니티 ECS를 활용하기로 했다면 등장하는 객체가 엄청 많을것이라는 뜻이니 GPU 인스턴싱을 활용하기 위해서 Graphics.DrawMeshInstance 혹은 Graphics.DrawMeshInstancedIndirect 을 활용해주고 있다. 참고로 둘 다 (유니티 2023에서 Graphics.RenderMeshInstanced(Indirect)로 대체됨)

 

다만!  GPU 인스턴싱 페이지의 최하단에서 볼 수 있듯 합쳐진 메시의 꼭지점이 256개 미만이라면 안 쓰는 게 낫다. 따라서 하나만 등장하는 캐릭터 등은 별도로 빼서 그냥 DrawMesh를 호출해주는 게 더 나을 수 있다. (두 레퍼런스 모두 이렇게 하지는 않음)

 

Graphics.DrawMeshInstanced의 일반 버전과 Indirect버전의 가장 큰 차이는 DrawMeshInstance는 최대 1023개의 인스턴스만 그릴 수 있고, Indirect 버전은 그릴 인스턴스 갯수를 인자로 넘겨받는다는 것이다.

 

Instance버전의 DrawMesh를 사용할 때 주의할 점은 그려지는 Mesh가 Call단위로 묶여서 컬링 및 소팅된다는 것이다. 그리고 그걸 처리해주는 Bound를 인자로 넘겨준다. (뷰 프러스텀 컬링용인듯?) 그리고 GPU 인스턴싱이 되는 Sprite 셰이더가 필요하다.

 

 

 

 

코드몽키(DrawMeshInstanced)


코드몽키의 작업물은 Graphics.DrawMeshInstanced 를 사용한다. 이 경우 1023개의 인스턴스만 그릴 수 있기 때문에 그릴 엔티티를 1023개씩 쪼개서 Draw를 호출한다. 소팅의 경우 y값을 기준으로 NativeArray를 소팅해서 넘겨주어 한 그룹 내에서도 y축을 기준으로 소팅될 수 있게 해준다. 그리고 이왕 쪼개는김에 화면을 y축을 기준으로 N개로 나누어 엔티티들을 각각의 구간에 집어넣고 소팅해준다. 이 과정에서 프러스텀 컬링도 해줌. (카메라에 들어오지 않는 구간은 Draw 호출 스킵)

 

출처 : https://youtu.be/t1f8ZreCuuQ 캡쳐

 

나쁘지는 않지만? 최선인지는 모르겠다. 특히 1023개씩 쪼개서 DrawMeshInstanced에 넘겨주는 것이 메모리 복사를 너무 많이하지 않나 싶다.

 

 

 

 

 

파브리지오 (DrawMeshInstancedIndirect)


기본 원리

파브리지오의 작업은 DrawMeshInstancedIndirect를 사용한다. 어떤 포럼을 참고하면 DrawMeshInstanced는 그냥 DrawMeshInstancedIndirect의 Wrapper라고 한다. 그렇다. 유니티가 알아서 해주는 게 없으니 훨씬 복잡하다.

 

아무튼 파브리지오 작업의 핵심은 한 메테리얼의 하나의 엔티티(BufferEntity)라는 것이다. 그리고 메테리얼 엔티티 안에 SpriteIndex (int : 몇 번째 스프라이트를 그려줄지) , SpriteMatrix (float4 : x,y 위치값, 스케일(uniform), z축회전값), SpriteColor(float4 : RGBA), 타입의 DynamicBuffer가 존재한다. 

 

그리고 메테리얼별로 (BufferEntity별로) DrawMeshInstancedIndirect를 호출한다! 

 

핵심 데이터인 RenderInformation 클래스를 보자

굉장히 많은 ComputeBuffer 필드가 존재한다. 

 

ComputeBuffer는 ComputeShader를 쓸 때 CPU가 GPU에 데이터를 넘겨줄 때 쓰는 버퍼인가보다. (잘 몰름)

 

argsBuffer는 위에서 Indirect버전의 Draw에는 몇 개를 그릴지 지정해줘야 한다고 했다. 그 역할이다. uint[] args는 그냥 중간 데이터

 

아무튼 얘네를 그냥

위와 같이 메테리얼에 넘겨주는 게 핵심이다.

 

 

 

의문

그런데 의문이 드는 게 그냥 SharedComponent로 Material을 지정하면 안 되나? 왜냐면 DynamicBuffer는 일정 사이즈가 넘어가면 Chunk 밖에 생성돼서 메모리 낭비가 심해질탠데..? (근거)

 

어쩌면 셰이더 자체의 구조때문일지도 모르겠다. 셰이더 코드를 보면 

위와같이 버퍼를 통짜로 받아오고 있다.

 

그리고 엔티티들이 아키타입별로 쪼개지면서 비효율적이 될 수도 있을 것 같고..? (왜냐면 EntityQuery를 NativeArray로 다시 불러와야 하니 이 과정에서 결국 메모리가 할당되고 복사됨)

 

 

의문2

음.. 그런데 y소팅이 잘 구현되어있는지 의심스럽고, flip이 구현되어있지 않다. (이건 그냥 uv를 뒤집어주면 될 듯?)

https://github.com/TarasMartynyuk/SpriteSheetRenderer

 

GitHub - TarasMartynyuk/SpriteSheetRenderer: A powerful Unity ECS system to render massive numbers of animated sprites.

A powerful Unity ECS system to render massive numbers of animated sprites. - GitHub - TarasMartynyuk/SpriteSheetRenderer: A powerful Unity ECS system to render massive numbers of animated sprites.

github.com

누군가 파브리지오의 작업물을 Entities 1.0 컨버팅 & flip까지 구현해둔듯

 

 

 

 

정리하며


 

위 두 작업물 모두 바로 쓰기에 묘하게 찜찜하다.

 

우선 지금으로써 딱히 그래픽쪽 최적화보다는 더 하고싶은 게 있기에 그래픽쪽은 개인적으로 우선순위가 낮다. 그러니까 그냥 누가 Fork따서 수정해준 파브리지오의 방법을 사용하거나, 

 

DrawMeshInstanced가 적어도 256개의 꼭지점 이상에서 효율적이라는 점을 근거로, 40개 이상(Quad는 삼각형 두개;  6개의 꼭지점을 가지니까)의 동일 메테리얼을 쓰는 객체가 등장할 게 아니라면 DrawMesh로 일단 애니메이션을 구현하고 Lazy하게 최적화를 진행하는 것도 좋을 것 같다.