[그래픽스] three.js에서의 다양한 렌더링 최적화 batching과 LOD
카테고리: graphics
태그: deep dive
🚀Introduction
오늘은 three.js에서의 최적화에 대해 다루어보고자 한다. 회사에서 3D 웹 에디터를 만들면서 알게된 지식을 공유하고자 한다. 저번글의 내용(instancing과 drawCall)을 일부 이해했으며, 그래픽파이프라인을 (나처럼)찍먹정도 해봤다는 가정하에 웹 관점에서 작성해보도록 하겠다.
⭐️merging
unity에서는 batching이라고 하고, three.js에서는 merging이라고 한다. static Batching의 경우 멈춰있는 건물들을 하나의 drawCall로 렌더링 시켜준다. 어차피 안움직이니까?!? 어떻게 이루어지는걸까? 원래의 렌더링을 생각 해 보고 merging이 적용된 렌더링을 비교해보자.
위와같은 3D이미지가 렌더링된다고 가정 해 보자. 그렇다면 drawCall은 몇번이나 일어날까? 최적화가 되지 않았다는 가정하에, object수만큼 drawCall이 일어난다.
그런데 이상하지 않은가? 이 많은 명령들을 한번에 뭉쳐서 전달 해 주면 되지 않을까? batching의 핵심이 이것이다. 뭉쳐서 전달해 주는 것이다. 아래 그림을 보자.
어떤어떤오브젝트들을 그리면 되는지 한번에 뭉쳐서 전달 해 준다. 이와같이 명령들을 합쳐서(merging) GPU에게 전달 해 준다. 당연하게도 명령들을 뭉치는(합치는) 과정에서 CPU의 연산이 들어간다. 결국 CPU의 연산과 메모리를 사용해서 DrawCall을 줄여내는 것이다. 움직이지 않는 물체들에 대해서는 static batching
이 들어가게 된다. 단 한번의 연산만 하고 나면 1번의 drawCall을 가져갈 수 있겠지만, 움직이는 물체들에 대해서는 dynamic batching이 들어간다. 즉 어떤 물체를 렌더링 할지 다시 계산하며 버퍼들을 하나로 합치는 연산이 매 프레임마다 이루어 진다.
이처럼 메모리가 많이 필요하다는 단점 외에도, 같이 merging된 오브젝트들은 culling시키기가 어렵다는 문제가 있어서 적절하게 사용되어야 한다. 예를들면 아파트를 1동부터 10동까지 merging시킨다면, 1동 아파트는 머리 일부분만 보여도 친구들과 묶여있기 때문에 (culling시키지 못하고)1동 아파트 전체를 렌더링 시킬수 밖에 없는 것이다. unity등에서는 automatic batching이 이루어져, 적절한 최적화가 자동으로 이루어진다. 하지만 three에서는 이와같은 최적화가 없기 때문에 직접 구현해 주어야 한다.(하…)
구현해주기만 하면, 당연하게도 drawCall은 1이 된다(뭉친것들 기준). 그렇다 해볼만 한 주제이다.
그렇다면 어떤 방식으로 구현할 수 있을까? BufferGeometryUtils
의 mergeGeometries
를 사용해야한다. 내부라이브러리 는 이런식으로 구현되어있다.
var mergedGeo = new THREE.BufferGeometryUtils();
for ( var i = 0; i < 10000; i++ ) {
var mesh = new THREE.Mesh( cubeGeo, material );
//+적절한 포지션에 생성해주기
mesh.updateMatrix();
mergedGeo.mergeGeometries( mesh.geometry, mesh.matrix );
}
참고로 기존방식인 merge는 deprecated되었다. 바뀐지 얼마 안되었기 때문에, 기존 Three.Geometry().merge()
로 작성된 레퍼런스들이 있다면, Three.bufferGeometryUtils().mergeGeometries()
로 바꿔서 이용하면 될 것 같다.
three-drei
에서는 훨씬 직관적인 방식으로 표현되어있다. 큐브2개와 구2개를 merging시키는 코드이다.
<Merged meshes={[box, sphere]}>
{(Box, Sphere) => (
<>
<Box position={[-2, -2, 0]} color="red" />
<Box position={[-3, -3, 0]} color="tomato" />
<Sphere scale={0.7} position={[2, 1, 0]} color="green" />
<Sphere scale={0.7} position={[3, 2, 0]} color="teal" />
</>
)}
</Merged>
⭐️LOD
LOD, Level Of Detail이라는 기술이다.
개념 자체는 instancing이나 batching보다 훨씬 간단하다. 멀리있는 오브젝트는 대충 그려주고, 가까이 있는 오브젝트는 자세히 그려주는 것이다. 배틀그라운드를 플레이 할 때, 저~멀리 있는 나무 혹은 플레이어들이 고밀도 폴리곤으로 그려질 필요는 없기 때문이다. 그냥 위의 사진만 봐도 어떤 느낌인지 바로 알 수 있으리라 생각한다.
멀리있는걸 대충그린다 => 렌더링할게 줄어든다 => 빨라진다 => 사람들이 좋아한다
게임에서 멀리에는 왜 항상 안개가 있을까? LOD
를 해둔 오브젝트들이 갑자기 튀어나오거나, 폴리곤이 갑자기 확 늘어나게 되면 뜬금없는 느낌이 들 수 있기 때문에, 멀리있는 물체들은 안개등으로 자연스럽게 가려두고, LOD를 점진적으로 적용시키며 안개를 뚫고 나오도록 자연스럽게 구현하기 위함이다. 게임에 안개가 많은 이유가 있었다! 갑툭튀 방지용이였던 것이다.
그렇다면 어떤 방식으로 구현할 수 있을까?
three.js
에서는 아래와 같은 방식으로, [0, 10, 20] 거리에 따라 다른 퀄리티를 보여줄 수 있다.
const lod = new THREE.LOD();
for( let i = 0; i < 3; i++ ) {
const geometry = new THREE.IcosahedronGeometry( 10, 3 - i )
const mesh = new THREE.Mesh( geometry, material );
lod.addLevel( mesh, i * 10 );
}
scene.add( lod );
three-fiber(R3F)
에서는 훨씬 직관적인 방식으로 표현 가능하다
<Detailed distances={[0, 10, 20]}>
<mesh geometry={high} />
<mesh geometry={mid} />
<mesh geometry={low} />
<Detailed/>
기술 적용 후 가장 빠르게 wow-point를 줄 수 있다는 점에서 흥미로운 것 같다. 급하게 포인트를 주고싶은 데모 시연에서는 짧은 거리들에 대해서만 high-polygon을 잘 적용시키면, 꽤 근사한 데모가 웹에서 가능 하다.
⭐️마무리
three.js에서의 merging(unity의 batching)과 LOD에 대해 알아보았다. 두 로직 모두 적절히 사용하면 훨씬 좋은 결과를 보여줄 수 있을 것이다. (건물과 창문 1000개를 merging시키면 drawCall이 1/1000으로 줄어든다!!) unity등에서는 자동적으로 batching(automatic batching
)을 해주지만 three.js는 여러 이유(나도 잘 모름…)로 인해 수동으로 구현해 주어야 한다. 단순 에셋을 활용하는 것 이상의 three활용이 필요할 때는, 좋은 성능을 위해 리소스를 투입할 만한 작업들이라 생각된다.
댓글 남기기