Voxel / Minecraft 유형 게임의 렌더링 속도를 개선하려면 어떻게해야합니까?


35

나는 내 자신의 Minecraft 복제본 (Java로도 작성 됨)을 작성 중입니다. 그것은 지금 잘 작동합니다. 40 미터의 시거리로 MacBook Pro 8,1에서 60 FPS를 쉽게 칠 수 있습니다. (인텔 i5 + 인텔 HD 그래픽 3000). 그러나 70 미터에 시거리를두면 15-25 FPS에 도달합니다. 실제 마인 크래프트에서는 아무 문제없이 시선을 멀리 (= 256m) 놓을 수 있습니다. 내 질문은 내 게임을 개선하기 위해 어떻게해야합니까?

내가 구현 한 최적화 :

  • 로컬 청크 만 메모리에 보관하십시오 (플레이어의 시청 거리에 따라 다름)
  • 절두체 컬링 (처음 덩어리에 이어 블록에)
  • 블록의 실제 보이는 면만 그리기
  • 보이는 블록을 포함하는 청크 당 목록 사용. 보이는 덩어리는이 목록에 추가됩니다. 이들이 보이지 않으면이 목록에서 자동으로 제거됩니다. 블록은 이웃 블록을 만들거나 파괴함으로써 보이게됩니다.
  • 업데이트 블록이 포함 된 청크 당 목록 사용. 보이는 차단 목록과 동일한 메커니즘입니다.
  • new게임 루프 내에서 거의 문장을 사용하지 마십시오. (가비지 수집기가 호출 될 때까지 내 게임은 약 20 초 동안 실행됩니다)
  • 현재 OpenGL 통화 목록을 사용하고 있습니다. ( glNewList(), glEndList(), glCallList()) 블록의 종류의 각면.

현재 나는 어떤 종류의 조명 시스템도 사용하고 있지 않습니다. VBO에 대해 이미 들었습니다. 그러나 나는 그것이 무엇인지 정확히 모른다. 그러나 나는 그들에 대해 약간의 연구를 할 것입니다. 성능이 향상됩니까? VBO를 구현하기 전에 glCallLists()통화 목록 목록 을 사용 하고 전달 하려고 합니다. 대신 천 번 사용하십시오 glCallList(). (실제 MineCraft는 VBO를 사용하지 않는다고 생각하기 때문에 이것을 시도하고 싶습니다. 맞습니까?)

성능을 향상시키는 다른 요령이 있습니까?

VisualVM 프로파일 링은 이것을 보여주었습니다 (시각 거리가 70 미터 인 33 프레임에 대해서만 프로파일 링).

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

40 미터 (246 프레임)로 프로파일 링 :

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

참고 : 다른 스레드에서 청크를 생성하기 때문에 많은 메소드와 코드 블록을 동기화하고 있습니다. 게임 루프 에서이 작업을 수행 할 때 객체의 잠금을 얻는 것이 성능 문제라고 생각합니다 (물론 게임 루프 만 있고 새로운 청크가 생성되지 않는 시간에 대해 이야기하고 있습니다). 이게 옳은 거니?

편집 : 일부 synchronised블록과 다른 약간의 개선 사항을 제거한 후 . 성능은 이미 훨씬 나아졌습니다. 70 미터의 새로운 프로파일 링 결과는 다음과 같습니다.

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

나는 이것이 selectVisibleBlocks문제 라는 것이 분명하다고 생각합니다 .

미리 감사드립니다!
마틴

업데이트 : 몇 가지 추가 개선 사항 (각 루프 대신 for 루프를 사용하고 루프 외부의 변수를 버퍼링하는 등)을 보았을 때 시야 거리 60을 꽤 잘 실행할 수 있습니다.

가능한 빨리 VBO를 구현할 것이라고 생각합니다.

PS : 모든 소스 코드는 GitHub에서 사용 가능합니다 :
https://github.com/mcourteaux/CraftMania


2
40m의 프로필 샷을 제공하여 다른 것보다 더 빠르게 확대되는 것을 볼 수 있습니까?
James

어쩌면 너무 지정되었을 수도 있지만 고려한다면 3D 게임의 속도를 높이는 방법을 묻는 것입니다. 그러나 제목은 ppl을 두려워 할 수 있습니다.
구스타보 메이 엘

@Gtoknu : 제목으로 무엇을 제안 하시겠습니까?
Martijn Courteaux

5
누구에게 물어 보느냐에 따라 일부 사람들은 Minecraft도 그렇게 빠르지 않다고 말합니다.
thedaian

"3D 게임 속도를 높일 수있는 기술"과 같은 것이 훨씬 나을 것이라고 생각합니다. 무언가를 생각하되 "최고"라는 단어를 사용하거나 다른 게임과 비교하려고하지 마십시오. 우리는 그들이 어떤 게임에서 무엇을 사용하는지 정확하게 말할 수 없습니다.
구스타보 메이 엘

답변:


15

개별 블록에서 절두체 컬링을 언급하고 있습니다. 대부분의 렌더링 청크는 완전히 보이거나 완전히 보이지 않아야합니다.

마인은 블록이 주어진 청크 수정하고, 표시 목록 / 정점 버퍼 (I 그것을 사용하는 몰라) 재 구축 그래서을 수행 . 보기가 변경 될 때마다 표시 목록을 수정하면 표시 목록의 이점을 얻지 못합니다.

또한 세계 높이 청크를 사용하는 것 같습니다. Minecraft는 로드 및 저장과 달리 표시 목록에 입방체 16 × 16 × 16 청크를 사용합니다. 그렇게하면 개별 청크를 풀어야 할 이유가 훨씬 줄어 듭니다.

(참고 : 나는 Minecraft의 코드를 조사하지 않았습니다.이 모든 정보는 내가 플레이 할 때 Minecraft의 렌더링을 관찰 한 정보 나 결론입니다.)


더 일반적인 조언 :

렌더링은 CPU와 GPU의 두 프로세서에서 실행됩니다. 프레임 속도가 충분하지 않은 경우 하나 또는 다른 하나가 제한 리소스입니다 . 프로그램이 CPU 바운드 또는 GPU 바운드 (스왑 또는 스케줄링 문제가 없다고 가정)입니다.

프로그램이 100 % CPU에서 실행 중이고 완료 할 다른 작업이없는 경우 CPU가 너무 많은 작업을 수행하고 있습니다. GPU가 더 많은 작업을 수행하는 대신 작업을 단순화하려고합니다 (예 : 컬링 감소). 귀하의 설명에 따라 귀하의 문제라고 생각합니다.

반면에 GPU가 한계이면 슬프게도 일반적으로 편리한 0 % -100 %로드 모니터가없는 경우 더 적은 데이터를 전송하거나 더 적은 픽셀을 채울 필요가있는 방법에 대해 생각해야합니다.


2
훌륭한 참고 자료, 위키에 언급 된 내용에 대한 연구가 저에게 매우 도움이되었습니다! +1
Gustavo Maciel

@OP : 보이는 면만 렌더링합니다 ( 블록이 아님). 병리학 적이지만 단조로운 16x16x16 청크에는 거의 800 개의 가시면이 있고 포함 된 블록에는 24,000 개의 가시면이 있습니다. 일단 그렇게하면 Kevin의 대답에는 다음으로 중요한 개선 사항이 포함됩니다.
AndrewS

@KevinReid 성능 디버깅에 도움이되는 몇 가지 프로그램이 있습니다. 예를 들어 AMD GPU PerfStudio는 CPU 또는 GPU가 바인딩되어 있고 GPU에 어떤 구성 요소가 바인딩되어 있는지 알려줍니다 (텍스처 vs 프래그먼트 대 버텍스 등) 그리고 Nvidia에도 비슷한 것이 있다고 확신합니다.
akaltar

3

Vec3f.set를 그렇게 많이 부르는 것은 무엇입니까? 각 프레임을 처음부터 렌더링 할 대상을 구축하는 경우 속도를 높이고 싶은 곳이 분명합니다. 나는 OpenGL 사용자가 많지 않고 Minecraft가 렌더링하는 방법에 대해 잘 모르지만 사용하는 수학 함수가 지금 당신을 죽이고있는 것 같습니다 (그들에게 얼마나 많은 시간과 시간을 보내는 지보십시오) 그들은 전화로 수천 컷으로 사망합니다).

이상적으로는 월드로 분할되어 함께 렌더링 할 버텍스 버퍼 오브젝트를 만들고 여러 프레임에서 재사용 할 수 있도록 그룹화 할 수 있습니다. VBO가 변경하는 세상이 어떻게 든 사용자가 편집하는 것처럼 VBO 만 수정하면됩니다. 그런 다음 메모리 소비를 줄이기 위해 보이는 범위에서 VBO를 생성 / 파기 할 수 있습니다. VBO가 모든 프레임이 아닌 생성 된 대로만 적중합니다.

프로필에서 "호출"횟수가 정확하면 엄청나게 많은 일을 엄청나게 많이 부르는 것입니다. (Vec3f.set에 10 천만 건의 전화 ...)


나는이 방법을 수많은 것들에 사용한다. 단순히 벡터의 세 값을 설정합니다. 이것은 매번 새로운 객체를 할당하는 것보다 훨씬 낫습니다.
Martijn Courteaux

2

여기 내 설명 (나의 실험에서)이 적용됩니다.

복셀 렌더링의 경우 사전 제작 된 VBO 또는 지오메트리 쉐이더가 더 효율적입니까?

Minecraft와 코드는 고정 함수 파이프 라인을 사용합니다. 내 자신의 노력은 GLSL과 함께했지만 요점은 일반적으로 적용 가능합니다.

(메모리에서) 나는 스크린 절두체보다 반 블록 큰 절두체를 만들었습니다. 그런 다음 각 덩어리의 중심점을 테스트했습니다 ( 마인 크래프트에는 16 * 16 * 128 블록이 있음 ).

각각의 얼굴은 요소 배열 VBO에 걸쳐 있습니다 (청크의 많은 얼굴은 'full'이 될 때까지 동일한 VBO를 공유합니다 malloc. 가능하면 같은 VBO에서 동일한 질감을 가진 얼굴 )와 북쪽의 정점 인덱스 면, 남쪽면 등이 혼합되어 있지 않고 인접 해 있습니다. 내가 그릴 때, 나는 glDrawRangeElements북쪽면에 대해, 법선이 이미 투영되고 정규화 된 유니폼을 사용합니다. 그런 다음 남쪽면 등을 수행하므로 법선은 VBO에 없습니다. 각 청크마다 볼 수있는 면만 방출하면됩니다. 예를 들어 화면 중앙의 면만 왼쪽과 오른쪽을 그려야합니다. 이것은 GL_CULL_FACE응용 프로그램 수준에서 간단 합니다.

가장 큰 속도 인 iirc는 각 청크를 다각형 처리 할 때 내부면을 컬링하는 것이 었습니다.

또한 중요하다 텍스처 아틀라스 관리 및 질감으로 얼굴을 정렬 및 다른 덩어리에서 동일 VBO에서 같은 질감으로 얼굴을 넣어. 너무 많은 텍스처 변경을 피하고 텍스처를 기준으로면을 정렬하는 등의 방법으로 스팬 수가 최소화 glDrawRangeElements됩니다. 인접한 동일한 타일면을 더 큰 직사각형으로 병합하는 것도 큰 문제였습니다. 위에서 언급 한 다른 답변에서 병합에 대해 이야기합니다.

분명히 표시 된 청크 만 다각형 화하고 오랫동안 표시되지 않은 청크를 버리고 편집 된 청크를 다시 다각형 화 할 수 있습니다 (이것은 렌더링과 비교하여 드문 경우입니다).


나는 절두체 최적화에 대한 아이디어를 좋아합니다. 그러나 설명에서 "블록"과 "청크"라는 용어를 섞지 않습니까?
Martijn Courteaux

아마 그렇습니다. 블록 덩어리는 영어로 된 블록 블록입니다.
Will

1

모든 비교 ( BlockDistanceComparator) 는 어디 에서 오는가? 정렬 함수에서 가져온 경우 기수 정렬로 대체 할 수 있습니까 (무조건 더 빠르며 비교 기반이 아님)?

정렬 자체가 그렇게 나쁘지 않더라도 타이밍을 보면 relativeToOrigincompare함수 마다 함수가 두 번 호출됩니다 . 해당 데이터는 모두 한 번 계산해야합니다. 보조 구조를 정렬하는 것이 더 빠릅니다. 예 :

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

그런 다음 pseudoCode에서

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

그것이 유효한 Java 구조체가 아니라면 죄송합니다 (저학년 이후 Java를 만지지 않았습니다).


나는 이것이 재미 있다고 생각한다. 자바에는 구조체가 없습니다. Java 세계에는 이와 같은 것이 있지만 데이터베이스와 관련이 있지만 전혀 같은 것은 아닙니다. 그들은 공개 멤버들과 함께 최종 수업을 만들 수 있습니다.
Theraot

1

VBO와 CULL을 사용하지만 거의 모든 게임에 적용됩니다. 당신이이 플레이어가 볼 수, 경우에만 큐브를 렌더링하고 싶지 블록이 특정의 방법으로 접촉하는 경우가 블록과 메이크업의 정점을 추가 (의이 지하 때문에 당신이 볼 수없는 덩어리를 가정 해 봅시다) 거의 "더 큰 블록"또는 귀하의 경우 덩어리입니다. 이를 욕심 메시라고하며 성능을 크게 향상시킵니다. 게임 (voxel 기반)을 개발 중이며 욕심 많은 메시 알고리즘을 사용합니다.

다음과 같이 모든 것을 렌더링하는 대신 :

세우다

다음과 같이 렌더링됩니다.

render2

이것의 단점은 초기 월드 빌드에서 또는 플레이어가 블록을 제거 / 추가하는 경우 청크 당 더 많은 계산을 수행해야한다는 것입니다.

거의 모든 유형의 복셀 엔진은 우수한 성능을 위해 이것을 필요로합니다.

블록면이 다른 블록면에 닿아 있는지 확인하고, 그렇다면 : 하나 (또는 ​​0)의 블록면으로 만 렌더링합니다. 청크를 정말 빠르게 렌더링 할 때 비용이 많이 듭니다.

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}

1
그리고 그만한 가치가 있습니까? LOD 시스템이 더 적절한 것 같습니다.
MichaelHouse

0

코드가 객체와 함수 호출에서 익사하는 것처럼 보입니다. 숫자를 측정하면 인라인이 발생하지 않는 것 같습니다.

Vec3f에서는 다른 Java 환경을 찾거나 자신이 설정 한 설정을 엉망으로 만들 수 있지만 코드를 빠르고 간단하게 만드는 간단한 방법이지만 Vec3f에서는 내부적으로 중지하는 것이 좋습니다. 코딩 OOO *. 모든 방법을 자체적으로 작성하고 다른 방법을 호출하지 마십시오.

편집 : 모든 곳에서 오버 헤드가 있지만 렌더링하기 전에 블록을 주문하는 것이 성능을 저하시키는 것으로 보입니다. 정말 필요한가요? 그렇다면 루프를 통해 시작하여 각 블록의 원점까지의 거리를 계산 한 다음 그 순서대로 정렬해야합니다.

* 과도한 객체 지향


그렇습니다. 메모리는 절약되지만 CPU는 손실됩니다! 따라서 실시간 게임에서는 OOO가 그리 좋지 않습니다.
구스타보 메이 엘

샘플링이 아닌 프로파일 링을 시작하자마자 JVM이 일반적으로 수행하는 인라인이 사라집니다. 그것은 양자 이론과 비슷합니다. 결과를 바꾸지 않으면 서 무언가를 측정 할 수 없습니다 : p
Michael

@Gtoknu 일반적으로 사실은 아니지만, 일부 수준의 OOO에서는 함수 호출이 인라인 코드보다 많은 메모리를 차지하기 시작합니다. 메모리의 중단 점 근처에 문제가있는 코드의 좋은 부분이 있다고 말하고 싶습니다.
aaaaaaaaaaaa

0

수학 연산을 비트 연산자로 분류 할 수도 있습니다. 을 가지고 있다면 128 / 16비트 연산자를 만드십시오 128 << 4. 이것은 당신의 문제에 많은 도움이 될 것입니다. 물건을 최고 속도로 작동 시키려고하지 마십시오. 60 정도의 속도로 게임을 업데이트하고 다른 것들을 위해 그것을 분해하십시오. 엔터티에 대해 약 20의 업데이트 속도를 수행 할 수 있습니다. 그리고 세계 업데이트 및 생성을위한 10과 같은 것.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.