C ++에서 복셀 엔진에 복셀 저장


9

재미 있기 때문에 작은 복셀 엔진을 만들려고 노력하고 있지만 실제 복셀을 저장하는 가장 좋은 방법을 찾기 위해 고심하고 있습니다. 나는 어떤 종류의 덩어리가 필요하다는 것을 알고 있으므로 전 세계를 메모리에 담을 필요가 없으며 합리적인 성능으로 렌더링해야한다는 것을 알고 있습니다.

나는 octrees에 대해 읽었고 그것이 이해하는 것부터 1 큐브로 시작하고 그 큐브에서 8 큐브가 더 될 수 있으며 8 큐브 모두 다른 8 큐브가 될 수 있습니다. 그러나 이것이 내 voxel 엔진에 적합하지 않다고 생각합니다. 복셀 큐브 / 아이템은 모두 같은 크기입니다.

따라서 다른 옵션은 16 * 16 * 16 크기의 배열을 만들고 하나의 청크를 만들고 항목으로 채우는 것입니다. 품목이없는 부품의 값은 0입니다 (0 = 공기). 그러나 이것이 많은 메모리를 낭비하고 빠르지는 않을까 걱정됩니다.

그런 다음 다른 옵션은 각 청크에 대한 벡터이며 큐브로 채 웁니다. 그리고 큐브는 청크에서 그 위치를 유지합니다. 이렇게하면 메모리가 절약되지만 (공기 블록 없음) 특정 위치에서 큐브를 찾는 속도가 훨씬 느려집니다.

그래서 나는 정말로 좋은 해결책을 찾을 수 없으며 누군가 나를 도울 수 있기를 바랍니다. 그래서 무엇을 사용하고 왜?

그러나 또 다른 문제는 렌더링입니다. OpenGL을 사용하여 각 청크를 읽고 GPU로 보내는 것은 쉽지만 매우 느립니다. 청크 당 하나의 메쉬를 생성하는 것이 더 좋지만, 한 블록을 깰 때마다 약간의 눈에 띄는 딸꾹질을 일으키는 데 약간의 시간이 걸릴 수있는 전체 청크를 다시 빌드해야한다는 것을 의미합니다. 그래서 더 힘들 것입니다. 그러면 큐브를 어떻게 렌더링합니까? 청크 당 하나의 정점 버퍼에 모든 큐브를 만들고 렌더링하여 다른 스레드에 넣거나 다른 방법이 있습니까?

감사!


1
큐브를 렌더링하려면 인스 턴싱을 사용해야합니다. learnopengl.com/Advanced-OpenGL/Instancing 에서 자습서를 찾을 수 있습니다 . 큐브를 저장하는 경우 : 하드웨어에 대한 메모리 제한이 있습니까? 16 ^ 3 큐브는 너무 많은 메모리가 아닌 것 같습니다.
Turms

@Turms 댓글 주셔서 감사합니다! 나는 강한 메모리 제약이 없으며 일반적인 PC 일뿐입니다. 그러나 가장 많은 덩어리가 50 % 공기이고 세계가 매우 크다면 상당히 많은 메모리가 낭비되어야한다고 생각했습니다. 그러나 아마 당신이 말한 것 같지는 않습니다. 정적 블록 수로 16 * 16 * 16 청크를 사용해야합니까? 또한 인스 턴싱을 사용해야한다고 말해야합니까? 내 생각은 각 덩어리에 대해 메쉬를 생성하는 것이 었습니다. 그렇게하면 모든 보이지 않는 삼각형을 제거 할 수 있기 때문입니다.

6
Turms가 설명하는 것처럼 큐브에 인스턴스화를 사용하지 않는 것이 좋습니다. 이것은 드로우 콜을 줄이지 만 오버 드로우 및 숨겨진 얼굴에는 아무 것도하지 않습니다. 사실 모든 큐브 작업을위한 인스턴스화를 위해 손을 그 문제를 해결하지 못하도록 묶습니다. 동일해야합니다-일부 큐브의 숨겨진면을 삭제하거나 동일 평면상의면을 더 큰 단일 다각형으로 병합 할 수 없습니다.
DMGregory

최고의 복셀 엔진을 선택하는 것은 어려울 수 있습니다. 스스로에게 물어야 할 가장 큰 질문은 "보셀에서 어떤 작업을 수행해야합니까?"입니다. 작업을 안내합니다. 예를 들어, 어떤 보셀이 oct-tree에서 어디에 있는지 알아내는 것이 얼마나 어려운지 염려됩니다. Oct-tree 알고리즘은 트리를 걸을 때 (종종 재귀적인 방식으로) 필요에 따라이 정보를 생성 할 수있는 문제에 적합합니다. 비용이 너무 비싼 특정 문제가 있으면 다른 옵션을 볼 수 있습니다.
Cort Ammon

또 다른 큰 질문은 복셀이 얼마나 자주 업데이트되는지입니다. 일부 알고리즘은 데이터를 효율적으로 저장하기 위해 데이터를 사전 처리 할 수 ​​있으면 좋지만 데이터가 입자 기반 유체 시뮬레이션에있을 수 있으므로 데이터가 지속적으로 업데이트되는 경우 효율성이 떨어집니다.
Cort Ammon

답변:


23

블록을 위치와 값으로 저장하는 것은 실제로 매우 비효율적입니다. 사용하는 구조체 나 객체로 인한 오버 헤드가 없어도 블록 당 4 개의 고유 한 값을 저장해야합니다. "고정 배열로 블록을 저장하는"방법 (앞서 설명한 방법)을 통해서만 사용하는 것이 합리적 일 것입니다.이 방법은 블록의 1/4만이 단단하고 다른 방법으로는 다른 최적화 방법을 사용하지 않습니다. 계정.

옥트리는 복셀 기반 게임에 실제로 유용합니다. 큰 블록 (예 : 동일한 블록의 패치)으로 데이터를 저장하는 것을 전문으로하기 때문입니다. 이를 설명하기 위해 쿼드 트리 (기본적으로 2d에서 octrees)를 사용했습니다.

이것은 1024 값과 같은 32x32 타일을 포함하는 시작 세트입니다. 여기에 이미지 설명을 입력하십시오

이것을 1024 개의 개별 값으로 저장하는 것은 비효율적 인 것처럼 보이지 않지만 Terraria 와 같은 게임과 유사한 맵 크기에 도달하면 화면 로딩에 몇 초가 걸립니다. 그리고 3 차원으로 늘리면 시스템의 모든 공간을 사용하기 시작합니다.

쿼드 트리 (또는 3D의 옥트리)가 상황을 도울 수 있습니다. 하나를 만들려면 타일에서 이동하여 그룹화하거나 하나의 거대한 셀에서 이동하여 타일에 도달 할 때까지 분할합니다. 시각화하기가 더 쉽기 때문에 첫 번째 방법을 사용하겠습니다.

따라서 첫 번째 반복에서는 모든 것을 2x2 셀로 그룹화하고 셀에 동일한 유형의 타일 만 포함 된 경우 타일을 삭제하고 유형을 저장하면됩니다. 한 번의 반복 후에, 우리의 맵은 다음과 같이 보일 것입니다 :

여기에 이미지 설명을 입력하십시오

빨간색 선은 우리가 저장하는 것을 표시합니다. 각 제곱은 1 값입니다. 이로 인해 크기가 1024 값에서 439로 감소하여 57 % 감소했습니다.

그러나 당신은 진언 을 알고 있습니다. 한 걸음 더 나아가서 셀로 그룹화 해 봅시다 :

여기에 이미지 설명을 입력하십시오

이것은 저장된 값의 양을 367로 줄였습니다. 원래 크기의 36 %에 불과합니다.

청크 내부의 모든 4 개의 인접한 셀 (3d에서 8 개의 인접 블록)이 하나의 셀 안에 저장되어 본질적으로 청크를 하나의 큰 셀로 변환 할 때까지이 분할을 수행해야합니다.

여기에는 주로 충돌을 수행 할 때 다른 이점이 있지만 단일 블록이 견고한 지 여부 만 신경 쓰는 별도의 octree를 만들 수 있습니다. 이렇게하면 청크 내부의 모든 블록에 대한 충돌을 검사하는 대신 셀에 대해 수행 할 수 있습니다.


답장을 보내 주셔서 감사합니다! octree 's가 갈 길인 것 같습니다. (내 voxel 엔진이 3D이기 때문에) 그래도 물어보고 싶은 질문이 있습니다 : 마지막 그림은 마인 크래프트를 원하기 때문에 검은 부분이 더 큰 사각형을 보여줍니다. 복셀 지형을 수정할 수있는 엔진이라면 블록이있는 모든 것을 동일한 크기로 유지하는 것이 좋습니다. 그렇지 않으면 일이 매우 복잡해 지므로 가능합니까? (나는 여전히 빈 / 공기 슬롯을 단순화 할 것입니다.) 옥트리를 프로그래밍하는 방법에 대한 튜토리얼이 있습니까? 감사!

7
@ appmaker1358 전혀 문제 없습니다. 플레이어가 큰 블록을 수정하려고하면 그 순간 에 작은 블록으로 나눕니다 . 16.16x16 값의 "rock"을 저장할 필요가 없습니다. 더 이상 사실이 될 때까지 "이 전체 덩어리는 단단한 바위입니다"라고 말할 수 있습니다.
DMGregory

1
@ appmaker1358 DMGregory가 말했듯이 octree에 저장된 데이터를 업데이트하는 것은 비교적 쉽습니다. 모든 하위 셀에 단일 유형의 블록 만 포함될 때까지 변경이 발생한 셀을 나누기 만하면됩니다. 다음은 quadtree가있는 대화식 예제입니다 . 하나를 생성하는 것도 간단합니다. 청크를 완전히 포함하는 하나의 큰 셀을 만든 다음 모든 리프 셀 (자식이없는 셀)을 재귀 적으로 살펴보고 해당하는 지형의 일부에 여러 유형의 블록이 포함되어 있는지 확인합니다. cell
Bálint

@ appmaker1358 더 큰 문제는 실제로 그 반대입니다. octree가 하나의 블록으로 나뭇잎으로 가득 차지 않게하여 Minecraft 스타일 게임에서 쉽게 발생할 수 있습니다. 그러나 문제에 대한 많은 해결책이 있습니다. 적절하다고 생각되는 것을 선택하는 것입니다. 그리고 많은 건물이 진행될 때만 실제 문제가됩니다.
Luaan

옥트리가 반드시 최선의 선택은 아닙니다. 여기 흥미로운 글이 있습니다 : 0fps.net/2012/01/14/an-analysis-of-minecraft-like-engines
Polygnome

7

옥트리는 사용자가 설명하는 문제를 정확하게 해결하기 위해 존재하므로 검색 시간이 길지 않고 희박한 데이터를 밀도있게 저장할 수 있습니다.

복셀의 크기가 같다는 사실은 옥트리의 깊이가 고정되어 있음을 의미합니다. 예. 16x16x16 청크의 경우 최대 5 단계의 트리가 필요합니다.

  • 청크 루트 (16x16x16)
    • 첫 번째 계층 옥탄트 (8x8x8)
      • 두 번째 계층 Octant (4x4x4)
        • 3 계층 옥탄트 (2x2x2)
          • 단일 복셀 (1x1x1)

즉, 청크의 특정 위치에 복셀이 있는지 확인하려면 최대 5 단계를 수행해야합니다.

  • 청크 루트 : 전체 청크가 동일한 값입니까 (예 : 모든 공기)? 그렇다면 우리는 끝났습니다. 그렇지 않다면 ...
    • 첫 번째 계층 :이 위치를 모두 포함하는 옥탄트가 모두 같은 값입니까? 그렇지 않다면 ...
      • 두 번째 계층...
        • 세 번째 계층 ...
          • 이제 우리는 단일 복셀을 다루고 있으며 그 값을 반환 할 수 있습니다.

최대 4096 개의 복셀 배열을 통해 1 %의 방법으로 스캔하는 것보다 훨씬 짧습니다!

이렇게하면 값이 모두 공기이든 모든 암석이든 다른 값이든 상관없이 동일한 값의 전체 옥타 트가있는 곳이면 어디에서든 데이터를 압축 할 수 있습니다. 옥탄트에는 단일 복셀 리프 노드의 한계까지 더 세분화 해야하는 혼합 값이 포함되어 있습니다.


청크의 자식을 처리 하기 위해 일반적으로 다음과 같이 Morton order로 진행 합니다.

  1. X-Y-Z-
  2. X-Y-Z +
  3. X-Y + Z-
  4. X-Y + Z +
  5. X + Y- Z-
  6. X + Y- Z +
  7. X + Y + Z-
  8. X + Y + Z +

따라서 Octree 노드 탐색은 다음과 같습니다.

GetOctreeValue(OctreeNode node, int depth, int3 nodeOrigin, int3 queryPoint) {
    if(node.IsAllOneValue)
        return node.Value;

    int childIndex =  0;
    childIndex += (queryPoint.x > nodeOrigin.x) ? 4 : 0;
    childIndex += (queryPoint.y > nodeOrigin.y) ? 2 : 0;
    childIndex += (queryPoint.z > nodeOrigin.z) ? 1 : 0;

    OctreeNode child = node.GetChild(childIndex);

    return GetOctreeValue(
                child, 
                depth + 1,
                nodeOrigin + childOffset[depth, childIndex],
                queryPoint
    );
}

답장을 보내 주셔서 감사합니다! 옥트리가가는 길인 것 같습니다. 그러나 2 가지 질문이 있지만 octree는 배열을 통한 스캔보다 빠릅니다. 정확합니다. 그러나 배열이 정적 일 수 있으므로 필요한 큐브의 위치를 ​​계산할 수 있으므로 그렇게 할 필요가 없습니다. 왜 스캔해야합니까? 두 번째 질문은 octree (1x1x1)의 마지막 계층에서 어떤 큐브가 어디에 있는지 어떻게 알 수 있습니까? 올바로 이해하면 octree 노드에 8 개의 노드가 더 있으므로 어떤 노드가 어느 3d 위치에 속하는지 어떻게 알 수 있습니까? ? (또는 저 자신을 기억해야합니까?)

예, 귀하는 이미 16x16x16 복셀의 전체 배열을 다루었으며 청크 당 4K 메모리 풋 프린트 (각 복셀 ID가 바이트라고 가정)를 과도하게 거부하는 것처럼 보였습니다. 언급 한 검색은 위치가있는 복셀 목록을 저장할 때 제공되며 목록을 스캔하여 대상 위치에서 복셀을 찾도록합니다. 여기서 4096은리스트 길이의 상한입니다. 일반적으로 그 길이보다 작지만 일반적으로 해당 octree 검색보다 더 깊습니다.
DMGregory
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.