쿼드 코어와 고기 그래픽 카드가 있어도 Minecraft의 놀라운 대형 세계는 탐색 속도가 매우 느리다는 것을 알았습니다.
Minecraft의 속도 저하는 다음과 같습니다.
- 공간 분할 및 메모리 관리가 기본 C ++에서 더 빠르기 때문에 Java.
- 약한 세계 분할.
나는 두 가지 가정 모두에서 틀릴 수 있습니다. 그러나 이것은 큰 복셀 세계를 관리하는 가장 좋은 방법에 대해 생각하게했습니다. 이 블록은 세계의 어떤 부분에 존재할 수있는 진정한 3D 세계입니다, 그것은 큰 차원 배열 기본적으로 [x][y][z]
세계 각 블록이 유형이, (즉 BlockType.Empty = 0
, BlockType.Dirt = 1
등)
이런 종류의 세계를 제대로 작동 시키려면 다음이 필요하다고 가정합니다.
- 모든 큐브를 분리하기 위해 다양한 나무 ( oct / kd / bsp )를 사용하십시오. 삼각형 레벨이 아닌 큐브 레벨마다 파티션을 나눌 수 있기 때문에 oct / kd가 더 나은 옵션 인 것 같습니다.
- 일부 알고리즘을 사용하면 현재 볼 수있는 블록을 파악할 수 있습니다. 사용자에게 더 가까운 블록은 블록을 난독 처리하여 렌더링 할 수 없게되므로
- 블록 오브젝트 자체를 가볍게 유지하여 나무에서 빠르게 추가하고 제거하십시오.
나는 더이없는 것 같아요 바로 이에 대한 대답은,하지만 난 주제에 대한 사람들의 의견을보고 관심이있을 것이다. 대규모 복셀 기반 세계에서 어떻게 성능을 향상 시키겠습니까?
답변
Java 대 C ++과 관련하여 두 가지 모두에서 voxel 엔진을 작성했습니다 (위의 C ++ 버전). 또한 2004 년부터 보셀 엔진을 작성해 왔습니다 (보그가 아니었을 때). 🙂 C ++ 성능이 훨씬 뛰어나다는 것을 망설이지 않고 말할 수는 있지만 코딩하기가 더 어렵습니다. 계산 속도에 관한 것이 아니라 메모리 관리에 관한 것입니다. 복셀 세계의 데이터만큼 많은 데이터를 할당 / 할당 해제 할 때 C (++)가 이길 언어입니다. 하나목표에 대해 생각해야합니다. 성능이 최우선 순위라면 C ++를 사용하십시오. 최신 성능을 발휘하지 않고 게임을 작성하려는 경우 Minecraft에서 입증 한 바와 같이 Java를 사용할 수 있습니다. 사소한 / 가장자리가 많지만 일반적으로 Java가 (잘 작성된) C ++보다 약 1.75-2.0 배 느리게 실행될 것으로 예상 할 수 있습니다. 제대로 작동하지 않는 오래된 버전의 엔진이 여기 에서 작동하는 것을 볼 수 있습니다 (EDIT : newer version here ). 청크 생성은 느리게 보일 수 있지만 3D 보로 노이 다이어그램을 체적으로 생성하여 무차별 강제 방법으로 CPU의 표면 법선, 조명, AO 및 그림자를 계산한다는 점을 명심하십시오. 나는 다양한 기술을 시험해 보았고 다양한 캐싱 및 인스 턴싱 기술을 사용하여 약 100 배 빠른 청크 생성을 얻을 수 있습니다.
나머지 질문에 대답하기 위해 성능을 향상시키기 위해 할 수있는 일이 많이 있습니다.
- 캐싱. 가능하면 데이터를 한 번 계산해야합니다. 예를 들어 조명을 장면에 굽습니다. 동적 조명 (후 처리로 화면 공간에서)을 사용할 수 있지만 조명에서 베이킹하면 삼각형의 법선을 통과하지 않아도됩니다.
-
비디오 카드에 가능한 적은 데이터를 전달하십시오. 사람들이 잊어 버리는 경향은 GPU에 더 많은 데이터를 전달할수록 시간이 더 걸린다는 것입니다. 단색과 정점 위치를 전달합니다. 주야간주기를 원한다면 컬러 그레이딩을하거나 태양이 서서히 변할 때 장면을 다시 계산할 수 있습니다.
-
GPU로 데이터를 전달하는 것은 비용이 많이 들기 때문에 어떤면에서는 더 빠른 소프트웨어로 엔진을 작성할 수 있습니다. 소프트웨어의 장점은 GPU에서는 불가능한 모든 종류의 데이터 조작 / 메모리 액세스를 수행 할 수 있다는 것입니다.
-
배치 크기로 재생하십시오. GPU를 사용하는 경우 전달하는 각 정점 배열의 크기에 따라 성능이 크게 달라질 수 있습니다. 따라서 청크 크기로 놀아보십시오 (청크를 사용하는 경우). 64x64x64 청크가 꽤 잘 작동한다는 것을 알았습니다. 어쨌든 청크를 입방체로 유지하십시오 (직사각 프리즘 없음). 이를 통해 코딩 및 변환과 같은 다양한 작업이 쉬워지고 경우에 따라 성능이 향상됩니다. 모든 차원의 길이에 대해 하나의 값만 저장하는 경우 계산 중에 스왑되는 레지스터가 2 개 줄어 듭니다.
-
표시 목록 (OpenGL 용)을 고려하십시오. 비록 그들이 “오래된”방식이지만 더 빠를 수 있습니다. 표시 목록을 변수로 구워야합니다 … 표시 목록 작성 작업을 실시간으로 호출하면 불필요하게 느려집니다. 표시 목록이 얼마나 빠릅니까? 정점 별 속성 대 상태 만 업데이트합니다. 즉, 최대 6 개의면을 통과 한 다음 1 개의 색상 (복셀의 각 정점에 대한 색상)을 전달할 수 있습니다. GL_QUADS와 3 차 복셀을 사용하는 경우 복셀 당 최대 20 바이트 (160 비트)를 절약 할 수 있습니다! (알파가없는 15 바이트, 일반적으로 사물을 4 바이트로 정렬하려는 경우)
-
“청크”또는 데이터 페이지를 렌더링하는 무차별 방식을 사용합니다. 이는 일반적인 기술입니다. octrees와는 달리, 데이터를 읽고 / 처리하는 것이 훨씬 쉽고 빠르지 만, 메모리에 덜 친숙합니다 (그러나 요즘에는 $ 200- $ 300에 64 기가 바이트의 메모리를 얻을 수 있습니다). 분명히, 전세계에 하나의 거대한 배열을 할당 할 수는 없습니다 (복셀 당 32 비트 int가 사용되는 경우 1024x1024x1024 복셀 세트는 4 기가 바이트의 메모리입니다). 따라서 뷰어와의 근접성에 따라 많은 작은 배열을 할당 / 할당 해제합니다. 데이터를 할당하고 필요한 표시 목록을 얻은 다음 데이터를 덤프하여 메모리를 절약 할 수도 있습니다. 이상적인 조합은 octrees와 arrays의 하이브리드 접근법을 사용하는 것입니다. 세계의 절차 생성, 조명 등을 수행 할 때 데이터를 배열에 저장합니다.
-
먼 곳까지 렌더링 … 클리핑 된 픽셀은 시간이 절약됩니다. gpu는 깊이 버퍼 테스트를 통과하지 않으면 픽셀을 던집니다.
-
뷰포트에서 청크 / 페이지 만 렌더링합니다 (자체 설명). GPU가 뷰포트 외부에서 폴 지온을 클립하는 방법을 알고 있더라도이 데이터를 전달하는 데 여전히 시간이 걸립니다. 나는 이것을위한 가장 효율적인 구조가 무엇인지 모르지만 ( “부끄럽게도, BSP 트리를 작성한 적이 없다”) 청크 단위로 간단한 레이 캐스트조차도 성능을 향상시킬 수 있으며 분명히 관찰 절두체에 대한 테스트는 시간을 절약.
-
명백한 정보이지만 초보자에게는 : 표면에없는 모든 단일 다각형을 제거합니다. 즉, 복셀이 6 개의면으로 구성된 경우 절대 렌더링되지 않는면을 제거합니다 (다른 복셀을 만지고 있음).
-
프로그래밍에서 수행하는 모든 작업의 일반적인 규칙 : CACHE LOCALITY! 캐시를 로컬에 유지할 수 있다면 (소량의 시간에도 큰 차이가 생길 수 있습니다.) 이는 동일한 메모리 영역에서 데이터를 합리적으로 유지하고 메모리 영역을 너무 자주 처리하도록 전환하지 않음을 의미합니다. 이상적으로는 스레드 당 하나의 청크에 대해 작업하고 해당 메모리를 스레드 전용으로 유지하십시오. 이는 CPU 캐시에만 적용되지 않습니다. 캐시 계층 구조는 다음과 같습니다 (가장 느리거나 빠른) : 네트워크 (클라우드 / 데이터베이스 / 등) -> 하드 드라이브 (아직 SSD가없는 경우 SSD를 얻음), 램 (삼중 채널 또는 RAM이없는 경우 더 큰 RAM을 얻음), CPU 캐시 (들), 레지스터. 후자이며, 필요 이상으로 바꾸지 마십시오.
-
스레딩. 해. Voxel 월드는 스레딩에 매우 적합합니다. 각 부분은 (대부분) 다른 부품과 독립적으로 계산할 수 있습니다. 스레딩 루틴.
-
문자 / 바이트 데이터 형식을 사용하지 마십시오. 또는 반바지. 일반 소비자에게는 최신 AMD 또는 Intel 프로세서가있을 것입니다 (아마도). 이 프로세서에는 8 비트 레지스터가 없습니다. 바이트를 32 비트 슬롯에 넣은 다음 메모리에서 다시 변환하여 바이트를 계산합니다. 컴파일러는 모든 종류의 부두를 수행 할 수 있지만 32 또는 64 비트 숫자를 사용하면 가장 예측 가능하고 빠른 결과를 얻을 수 있습니다. 마찬가지로 “bool”값은 1 비트를 사용하지 않습니다. 컴파일러는 종종 bool에 전체 32 비트를 사용합니다. 데이터에 대해 특정 유형의 압축을 시도하고 싶을 수 있습니다. 예를 들어, 8 개의 복셀이 모두 같은 유형 / 색상 인 경우 단일 숫자 (2 ^ 8 = 256 조합)로 저장할 수 있습니다. 그러나 이것의 결과에 대해 생각해야합니다. 메모리를 많이 절약 할 수 있습니다. 작은 압축 시간으로도 성능이 저하 될 수 있습니다. 그로 인해 소량의 추가 시간도 세계의 크기와 입방체로 확장되기 때문입니다. 레이 캐스트 계산을 상상해보십시오. 레이 캐스트의 모든 단계에 대해 압축 해제 알고리즘을 실행해야합니다 (한 번에 8 개의 복셀 계산을 일반화하는 현명한 방법이 아니라면).
-
Jose Chavez가 언급했듯이 플라이급 디자인 패턴이 유용 할 수 있습니다. 비트 맵을 사용하여 2D 게임에서 타일을 나타내는 것처럼 여러 3D 타일 (또는 블록) 유형으로 세계를 만들 수 있습니다. 이것의 단점은 텍스처의 반복이지만, 함께 맞는 분산 텍스처를 사용하여이를 개선 할 수 있습니다. 경험상 어디에서나 인스 턴싱을 활용하려고합니다.
-
지오메트리를 출력 할 때 쉐이더에서 정점 및 픽셀 처리를 피하십시오. 복셀 엔진에는 필연적으로 많은 삼각형이 있으므로 간단한 픽셀 쉐이더조차도 렌더링 시간을 크게 줄일 수 있습니다. 버퍼로 렌더링하는 것이 좋습니다. 그런 다음 픽셀 셰이더를 포스트 프로세스로 사용하십시오. 그렇게 할 수 없다면 정점 셰이더에서 계산을 시도하십시오. 다른 계산은 가능한 정점 데이터로 구워 져야합니다. 모든 지오메트리 (예 : 그림자 매핑 또는 환경 매핑)를 다시 렌더링해야하는 경우 추가 패스가 매우 비쌉니다. 때로는 더 풍부한 디테일을 위해 역동적 인 장면을 포기하는 것이 좋습니다. 게임에 수정 가능한 장면 (예 : 파괴 가능한 지형)이있는 경우 사물이 파괴 될 때 항상 장면을 다시 계산할 수 있습니다. 재 컴파일은 비싸지 않으며 1 초도 걸리지 않습니다.
-
루프를 풀고 배열을 평평하게 유지하십시오! 이 작업을 수행하지 마십시오 :
for (i = 0; i < chunkLength; i++) { for (j = 0; j < chunkLength; j++) { for (k = 0; k < chunkLength; k++) { MyData[i][j][k] = newVal; } } } //Instead, do this: for (i = 0; i < chunkLengthCubed; i++) { //figure out x, y, z index of chunk using modulus and div operators on i //myData should have chunkLengthCubed number of indices, obviously myData[i] = newVal; }
편집 : 더 광범위한 테스트를 통해 이것이 잘못 될 수 있음을 발견했습니다. 시나리오에 가장 적합한 사례를 사용하십시오. 일반적으로 배열은 평평해야하지만 경우에 따라 다중 색인 루프를 사용하는 것이 더 빠를 수 있습니다
편집 2 : 다중 인덱스 루프를 사용할 때 다른 방법이 아닌 z, y, x 순서를 반복하는 것이 가장 좋습니다. 컴파일러가 이것을 최적화 할 수는 있지만 그렇게하면 놀랄 것입니다. 이를 통해 메모리 액세스 및 로컬 효율성이 극대화됩니다.
for (k < 0; k < volumePitch; k++) {
for (j = 0; j < volumePitch; j++) {
for (i = 0; i < volumePitch; i++) {
myIndex = k*volumePitch*volumePitch + j*volumePitch + i;
}
}
}
- 때로는 가정, 일반화 및 희생을해야합니다. 가장 좋은 방법은 대부분의 세계가 완전히 정적 인 것으로 가정하고 수천 프레임마다 만 변경하는 것입니다. 세계의 애니메이션 부분의 경우 별도의 패스로 수행 할 수 있습니다. 또한 대부분의 세계가 완전히 불투명하다고 가정하십시오. 투명한 객체는 별도의 패스로 렌더링 할 수 있습니다. 텍스처는 x 단위로만 변하거나 개체를 x 단위로만 배치 할 수 있다고 가정합니다. 무한한 세상의 유혹처럼 고정 된 세계 크기를 가정하면 예측할 수없는 시스템 요구 사항으로 이어질 수 있습니다. 예를 들어, 위의 바위에서 보로 노이 패턴 생성을 단순화하기 위해 모든 보로 노이 중심점이 약간의 오프셋 (즉, 기하학적 해시를 암시 함)으로 균일 한 격자에 놓여 있다고 가정했습니다. 랩핑되지 않은 (가장자리가있는) 월드를 가정하십시오. 이는 사용자 경험에 최소 비용으로 래핑 좌표계에 의해 도입 된 많은 복잡성을 단순화 할 수 있습니다.
내 사이트 에서 구현에 대한 자세한 내용을 읽을 수 있습니다
답변
Minecraft가 더 효율적으로 수행 할 수있는 작업이 많이 있습니다. 예를 들어 Minecraft는 약 16×16 타일의 전체 세로 기둥을로드하고 렌더링합니다. 많은 타일을 불필요하게 보내고 렌더링하는 것이 매우 비효율적이라고 생각합니다. 그러나 나는 언어 선택이 중요하다고 생각하지 않습니다.
Java는 매우 빠를 수 있지만이 데이터 지향적 인 무언가에 대해 C ++은 배열에 액세스하고 바이트 내에서 작업하는 데 드는 오버 헤드가 현저히 줄어 큰 이점이 있습니다. 반면 Java의 모든 플랫폼에서 스레딩을 수행하는 것이 훨씬 쉽습니다. OpenMP 또는 OpenCL을 사용할 계획이 아니라면 C ++에서는 그 편리함을 찾을 수 없습니다.
이상적인 시스템은 약간 더 복잡한 계층 구조입니다.
타일 은 단일 유형으로, 재료 유형 및 조명과 같은 정보를 유지하기 위해 약 4 바이트입니다.
세그먼트 는 32x32x32 타일 블록입니다.
- 전체면이 솔리드 블록 인 경우 6 개면 각각에 대해 플래그가 설정됩니다. 그러면 렌더러가 해당 세그먼트 뒤의 세그먼트를 막을 수 있습니다. Minecraft는 현재 폐색 테스트를 수행하지 않는 것 같습니다. 그러나 저급 카드에 대량의 다각형을 렌더링하는 것보다 비용이 많이 들지만 더 나은 하드웨어 폐색 컬링을 사용할 수 있다는 언급이있었습니다 .
- 세그먼트는 액티비티 (플레이어, NPC, 물 물리, 나무 성장 등) 중 메모리에만로드됩니다. 그렇지 않으면 디스크에서 클라이언트로 직접 압축되어 전송됩니다.
섹터 는 16x16x8 세그먼트 블록입니다.
- 섹터는 각 수직 열에 대해 가장 높은 세그먼트를 추적하므로 해당 세그먼트보다 높은 세그먼트는 빠르게 비워 질 수 있습니다.
- 또한 아래쪽 폐색 된 세그먼트를 추적하므로 표면에서 렌더링해야하는 모든 세그먼트를 빠르게 잡을 수 있습니다.
- 섹터는 다음에 각 세그먼트를 업데이트해야 할 때 (물리, 나무 성장 등) 추적합니다. 이러한 방식으로 각 부문에 로딩하는 것만으로도 세상을 활기차게 유지할 수 있고 작업을 완료 할 수있을 정도로 긴 세그먼트에만 로딩 할 수 있습니다.
- 모든 엔티티 위치는 섹터를 기준으로 추적됩니다. 이렇게하면지도 센터에서 매우 멀리 여행 할 때 Minecraft에있는 부동 소수점 오류가 방지됩니다.
세계 는 무한한 분야의지도가 될 것입니다.
- 세계는 섹터와 다음 업데이트를 관리 할 책임이 있습니다.
- 세계는 선구 적으로 플레이어에게 잠재적 인 경로를 따라 세그먼트를 보냅니다. Minecraft는 클라이언트가 요청한 세그먼트를 반응 적으로 보내 지연을 유발합니다.
답변
내 2 코어에서도 Minecraft가 매우 빠릅니다. 약간의 서버 지연이 있지만 Java는 제한 요소가 아닌 것 같습니다. 로컬 게임이 더 나은 것처럼 보이므로 비효율적 인 부분을 가정하겠습니다.
귀하의 질문에 관해서는, Notch (Minecraft 저자)는 기술에 대해 얼마 동안 블로그를 작성했습니다. 특히, 세계는 “청크 (chunks)”에 저장됩니다 (때때로 세계가 아직 채워지지 않아 누락 된 경우). 따라서 첫 번째 최적화는 청크를 볼 수 있는지 여부를 결정하는 것입니다. .
당신이 짐작했듯이 청크 내에서 앱은 다른 블록에 의해 가려 지는지 여부에 따라 블록을 볼 수 있는지 여부를 결정해야합니다.
또한 블록 FACES가 있으며, 보이지 않는 것으로 간주 될 수 있습니다 (예 : 다른 블록이 얼굴을 덮고 있음) 또는 카메라가 가리키는 방향 (카메라가 북쪽을 향하는 경우)으로 볼 수 있습니다. 모든 블록의 북쪽 얼굴을 보지 마십시오!)
일반적인 기술에는 별도의 블록 객체를 유지하는 것이 아니라 각 블록마다 하나의 프로토 타입 블록과 함께이 블록이 어떻게 사용자 정의 될 수 있는지를 설명하기위한 최소의 데이터 세트와 함께 블록 유형의 “청크”가 포함됩니다. 예를 들어, 맞춤형 화강암 블록은 없지만 (물론) 물에는 각 측면을 따라 얼마나 깊이 있는지를 알려주는 데이터가있어 흐름 방향을 계산할 수 있습니다.
렌더링 속도, 데이터 크기 또는 무엇을 최적화 하려는지 확실하지 않습니다. 도움이 될 것입니다.
답변
다음은 일반적인 정보와 조언에 대한 몇 마디입니다. 음, 경험이 많은 Minecraft modder로 제공 할 수 있습니다 (적어도 부분적으로 지침을 제공 할 수 있음).
Minecraft가 느린 이유는 의심스럽고 낮은 수준의 디자인 결정과 관련이 많습니다. 예를 들어, 블록을 위치 지정으로 참조 할 때마다 게임은 약 7 if 문을 사용하여 좌표가 유효하지 않은지 확인합니다 . 또한 캐시 룩업과 erm, 바보 같은 유효성 검사 문제를 우회하기 위해 ‘청크'(게임이 작동하는 16x16x256 블록 단위)를 가져온 다음 블록을 직접 참조 할 수있는 방법이 없습니다 (즉, 각 블록 참조에는 내 모드에서는 블록 배열을 직접 잡고 변경하는 방법을 만들었습니다. 이로 인해 대규모 던전 생성이 재생 불가능한 게으른 속도에서 눈에 띄게 빠른 속도로 향상되었습니다.
편집 : 다른 범위에서 변수를 선언하면 성능이 향상되었다는 주장이 제거되었지만 실제로는 그렇지 않습니다. 나는이 결과를 내가 실험하고있는 다른 것과 혼동 시켰을 때 (특히, 이중으로 통합하여 폭발 관련 코드에서 이중 및 부유 사이의 캐스트를 제거하는 것은 … 상당히 이것은 큰 영향을 미쳤다!)
또한, 내가 많은 시간을 소비하는 영역은 아니지만 Minecraft의 대부분의 성능 초크는 렌더링 문제입니다 (게임 시간의 약 75 %가 내 시스템에서 사용됩니다). 걱정이 멀티 플레이어에서 더 많은 플레이어를 지원하는 경우 (서버는 아무것도 렌더링하지 않음) 크게 신경 쓰지 않지만 모든 컴퓨터에서 재생할 수있는 정도에 중요합니다.
따라서 어떤 언어를 선택하든 구현 / 낮은 수준의 세부 사항에 매우 친밀한 태도를 취하십시오. 이와 같은 프로젝트의 작은 세부 사항조차도 모든 차이를 만들 수 있기 때문입니다. 포인터? “”가능합니다! 코드가 적고 인라이닝의 이점이 있기 때문에 작업중인 프로젝트 중 하나에 엄청난 차이가 생겼습니다.)
높은 수준의 디자인을 어렵게 만들기 때문에 그 대답을 싫어하지만 성능이 문제가되는 것은 고통스러운 진실입니다. 도움이 되었기를 바랍니다.
또한 Gavin의 대답은 반복하고 싶지 않은 세부 사항 (그리고 훨씬 더! 그는 나보다 주제에 대해 더 잘 알고 있음)을 다루며 대부분 그에 동의합니다. 프로세서와 더 짧은 가변 크기에 대한 그의 의견을 실험해야 할 것입니다. 들어 본 적이 없습니다. 그것이 사실이라는 것을 스스로에게 증명하고 싶습니다!
답변
우선 데이터를 어떻게로드 할 것인지 생각해야합니다. 필요한 경우 맵 데이터를 메모리로 스트리밍하는 경우 렌더링 할 수있는 항목에 대한 자연적인 한계가 분명히 있으며 이는 이미 렌더링 성능 업그레이드입니다.
이 데이터로 무엇을하는지에 달려 있습니다. GFX 성능을 위해 클리핑 을 사용 하여 숨겨진 개체, 너무 작아서 볼 수없는 개체 등을 클리핑 할 수 있습니다.
그렇다면 그래픽 성능 기술을 찾고 있다면 인터넷에서 산을 찾을 수 있다고 확신합니다.
답변
볼거리는 플라이급 디자인 패턴입니다. 나는 대부분의 답변 이이 디자인 패턴을 어떤 식 으로든 참조한다고 생각합니다.
Minecraft가 각 블록 유형의 메모리를 최소화하기 위해 사용하는 정확한 방법을 모르지만 이것은 게임에서 사용할 수있는 방법입니다. 아이디어는 프로토 타입 객체와 같이 모든 블록에 대한 정보를 보유하는 객체를 하나만 갖는 것입니다. 유일한 차이점은 각 블록의 위치입니다.
그러나 위치조차도 최소화 할 수 있습니다. 토지 블록이 한 유형 인 경우 해당 위치의 크기를 하나의 위치 데이터와 함께 하나의 거대한 블록으로 저장하지 않겠습니까?
분명히 알 수있는 유일한 방법은 직접 구현을 시작하고 성능을 위해 메모리 테스트를 수행하는 것입니다. 어떻게 진행되는지 알려주세요!