Làm cách nào tôi có thể cải thiện tốc độ kết xuất của trò chơi loại Voxel / Minecraft?


35

Tôi đang viết bản sao Minecraft của riêng mình (cũng được viết bằng Java). Nó hoạt động tuyệt vời ngay bây giờ. Với khoảng cách xem 40 mét, tôi có thể dễ dàng đạt 60 FPS trên MacBook Pro 8.1. (Intel i5 + Intel HD Graphics 3000). Nhưng nếu tôi đặt khoảng cách xem trên 70 mét, tôi chỉ đạt 15-25 FPS. Trong Minecraft thực, tôi có thể đặt khoảng cách xem ở xa (= 256m) mà không gặp vấn đề gì. Vì vậy, câu hỏi của tôi là tôi nên làm gì để làm cho trò chơi của tôi tốt hơn?

Những tối ưu hóa tôi đã thực hiện:

  • Chỉ giữ các khối cục bộ trong bộ nhớ (tùy thuộc vào khoảng cách xem của người chơi)
  • Frustum culling (Đầu tiên trên các khối, sau đó trên các khối)
  • Chỉ vẽ các mặt thực sự có thể nhìn thấy của các khối
  • Sử dụng danh sách trên mỗi đoạn có chứa các khối có thể nhìn thấy. Chunks trở nên hữu hình sẽ thêm chính nó vào danh sách này. Nếu chúng trở nên vô hình, chúng sẽ tự động bị xóa khỏi danh sách này. Các khối trở nên (trong) có thể nhìn thấy bằng cách xây dựng hoặc phá hủy một khối lân cận.
  • Sử dụng danh sách trên mỗi đoạn có chứa các khối cập nhật. Cơ chế tương tự như danh sách khối có thể nhìn thấy.
  • Sử dụng gần như không có newtuyên bố trong vòng lặp trò chơi. (Trò chơi của tôi chạy khoảng 20 giây cho đến khi Trình thu gom rác được gọi)
  • Hiện tại tôi đang sử dụng danh sách cuộc gọi OpenGL. ( glNewList(), glEndList(), glCallList()) Cho mỗi bên của một loại khối.

Hiện tại tôi thậm chí không sử dụng bất kỳ loại hệ thống chiếu sáng. Tôi đã nghe nói về VBO. Nhưng tôi không biết chính xác nó là gì. Tuy nhiên, tôi sẽ làm một số nghiên cứu về chúng. Họ sẽ cải thiện hiệu suất? Trước khi triển khai VBO, tôi muốn thử sử dụng glCallLists()và vượt qua danh sách danh sách cuộc gọi. Thay vì sử dụng ngàn lần glCallList(). (Tôi muốn thử điều này, vì tôi nghĩ rằng MineCraft thực sự không sử dụng VBO. Đúng không?)

Có những thủ thuật khác để cải thiện hiệu suất?

Cấu hình VisualVM cho tôi thấy điều này (chỉ lược tả 33 khung hình, với khoảng cách xem là 70 mét):

nhập mô tả hình ảnh ở đây

Hồ sơ với 40 mét (246 khung):

nhập mô tả hình ảnh ở đây

Lưu ý: Tôi đang đồng bộ hóa rất nhiều phương thức và khối mã, bởi vì tôi đang tạo các khối trong một luồng khác. Tôi nghĩ rằng việc có được một khóa cho một đối tượng là một vấn đề hiệu năng khi thực hiện nhiều điều này trong một vòng lặp trò chơi (tất nhiên, tôi đang nói về thời gian khi chỉ có vòng lặp trò chơi và không có đoạn mới nào được tạo ra). Thê nay đung không?

Chỉnh sửa: Sau khi loại bỏ một số synchronisedkhối và một số cải tiến nhỏ khác. Hiệu suất đã tốt hơn nhiều. Dưới đây là kết quả hồ sơ mới của tôi với 70 mét:

nhập mô tả hình ảnh ở đây

Tôi nghĩ rằng nó là khá rõ ràng đó selectVisibleBlockslà vấn đề ở đây.

Cảm ơn trước!
Martijn

Cập nhật : Sau một số cải tiến bổ sung (như sử dụng các vòng lặp thay cho từng vòng, đệm các biến bên ngoài các vòng lặp, v.v ...), giờ đây tôi có thể chạy khoảng cách xem 60 khá tốt.

Tôi nghĩ rằng tôi sẽ triển khai VBO càng sớm càng tốt.

PS: Tất cả mã nguồn có sẵn trên GitHub:
https://github.com/mcourteaux/CraftMania


2
Bạn có thể cho chúng tôi chụp hồ sơ ở độ cao 40m để chúng tôi có thể thấy những gì có thể tăng lên nhanh hơn so với cái khác không?
James

Có thể quá cụ thể, nhưng nếu bạn xem xét, chỉ là hỏi các kỹ thuật về cách tăng tốc trò chơi 3D, nghe có vẻ thú vị. Nhưng tiêu đề có thể sợ ppl.
Gustavo Maciel

@Gtoknu: Bạn đề nghị làm tiêu đề gì?
Martijn Courteaux

5
Tùy thuộc vào người bạn hỏi, một số người sẽ nói rằng Minecraft thực sự cũng không nhanh như vậy.
thedaian

Tôi nghĩ một cái gì đó như "Những kỹ thuật nào có thể tăng tốc trò chơi 3D" sẽ tốt hơn rất nhiều. Hãy nghĩ về điều gì đó, nhưng cố gắng không sử dụng từ "tốt nhất" hoặc cố gắng so sánh với một số trò chơi khác. Chúng tôi không thể nói chính xác những gì họ sử dụng trên một số trò chơi.
Gustavo Maciel

Câu trả lời:


15

Bạn đề cập đến việc làm nản lòng trên các khối riêng lẻ - hãy thử ném nó ra. Hầu hết các khối kết xuất nên hoàn toàn có thể nhìn thấy hoặc hoàn toàn vô hình.

Minecraft chỉ xây dựng lại một danh sách hiển thị / bộ đệm đỉnh (tôi không biết nó sử dụng cái gì) khi một khối được sửa đổi trong một đoạn nhất định, và tôi cũng vậy . Nếu bạn đang sửa đổi danh sách hiển thị bất cứ khi nào chế độ xem thay đổi, bạn sẽ không nhận được lợi ích của danh sách hiển thị.

Ngoài ra, bạn dường như đang sử dụng chunk chiều cao thế giới. Lưu ý rằng Minecraft sử dụng các khối 16 × 16 × 16 hình khối cho danh sách hiển thị của nó, không giống như tải và lưu. Nếu bạn làm điều đó, thậm chí còn ít lý do hơn để làm nản lòng từng khối riêng lẻ.

(Lưu ý: Tôi chưa kiểm tra mã của Minecraft. Tất cả thông tin này là tin đồn hoặc kết luận của riêng tôi khi quan sát kết xuất của Minecraft khi tôi chơi.)


Lời khuyên chung hơn:

Hãy nhớ rằng kết xuất của bạn thực thi trên hai bộ xử lý: CPU và GPU. Khi tốc độ khung hình của bạn không đủ, thì cái này hay cái kia là tài nguyên giới hạn - chương trình của bạn bị ràng buộc bởi CPU hoặc bị ràng buộc bởi GPU (giả sử nó không bị tráo đổi hoặc gặp sự cố về lịch trình).

Nếu chương trình của bạn đang chạy với 100% CPU (và không có nhiệm vụ nào khác không hoàn thành), thì CPU của bạn đang làm quá nhiều việc. Bạn nên cố gắng đơn giản hóa nhiệm vụ của nó (ví dụ: loại bỏ ít hơn) để đổi lấy việc GPU làm được nhiều hơn. Tôi nghi ngờ đây là vấn đề của bạn, đưa ra mô tả của bạn.

Mặt khác, nếu GPU là giới hạn (đáng buồn thay, thường không có màn hình tải 0% -100% tiện lợi) thì bạn nên suy nghĩ về cách gửi ít dữ liệu hơn hoặc yêu cầu nó lấp đầy ít pixel hơn.


2
Tài liệu tham khảo tuyệt vời, nghiên cứu của bạn về nó được đề cập trên wiki của bạn rất hữu ích cho tôi! +1
Gustavo Maciel

@OP: chỉ hiển thị khuôn mặt có thể nhìn thấy (không phải khối ). Một khối 16x16x16 bệnh lý nhưng đơn điệu sẽ có gần 800 khuôn mặt có thể nhìn thấy, trong khi các khối chứa sẽ có 24.000 khuôn mặt có thể nhìn thấy. Khi bạn đã thực hiện điều đó, câu trả lời của Kevin chứa những cải tiến quan trọng nhất tiếp theo.
AndrewS

@KevinReid Có một số chương trình giúp gỡ lỗi hiệu năng. Chẳng hạn, GPU PerfStudio của AMD cho bạn biết nếu CPU hoặc GPU của nó bị ràng buộc và trên GPU thì thành phần nào bị ràng buộc (kết cấu so với mảnh so với đỉnh, v.v.) Và tôi chắc chắn rằng Nvidia cũng có thứ tương tự.
akaltar 7/07/2015

3

Cái gì gọi Vec3f.set rất nhiều? Nếu bạn đang xây dựng những gì bạn muốn hiển thị từ đầu mỗi khung thì đó chắc chắn là nơi bạn muốn bắt đầu tăng tốc nó. Tôi không phải là người dùng OpenGL và tôi không biết nhiều về cách Minecraft kết xuất, nhưng có vẻ như các hàm toán học bạn đang sử dụng đang giết chết bạn ngay bây giờ (hãy nhìn xem bạn dành bao nhiêu thời gian cho chúng và số lần họ được gọi - cái chết bởi một ngàn vết cắt gọi họ).

Lý tưởng nhất là thế giới của bạn sẽ được phân chia để bạn có thể nhóm các thứ để kết xuất lại với nhau, xây dựng Đối tượng bộ đệm Vertex và sử dụng lại chúng trên nhiều khung. Bạn chỉ cần sửa đổi VBO nếu thế giới mà nó đại diện thay đổi bằng cách nào đó (như người dùng chỉnh sửa nó). Sau đó, bạn có thể tạo / hủy VBO cho những gì bạn thể hiện khi nó xuất hiện trong phạm vi hiển thị để giảm mức tiêu thụ bộ nhớ, bạn chỉ thực hiện cú đánh khi VBO được tạo chứ không phải mọi khung hình.

Nếu số lần "gọi" là chính xác trong hồ sơ của bạn, thì bạn đang gọi rất nhiều thứ rất nhiều lần. (10 triệu cuộc gọi đến Vec3f.set ... ouch!)


Tôi sử dụng phương pháp này cho hàng tấn thứ. Nó chỉ đơn giản là đặt ba giá trị cho vectơ. Điều này tốt hơn nhiều so với việc phân bổ mỗi lần một đối tượng mới.
Martijn Courteaux

2

Mô tả của tôi (từ thử nghiệm của riêng tôi) ở đây có thể áp dụng:

Để kết xuất voxel, cái gì hiệu quả hơn: VBO được tạo sẵn hoặc một shader hình học?

Minecraft và mã của bạn có thể sử dụng đường ống chức năng cố định; những nỗ lực của riêng tôi đã có với GLSL nhưng ý chính thường được áp dụng, tôi cảm thấy:

(Từ bộ nhớ) Tôi đã tạo ra một nỗi thất vọng lớn hơn nửa khối so với màn hình. Sau đó tôi đã kiểm tra các điểm trung tâm của mỗi khối ( minecraft có 16 * 16 * 128 khối ).

Các mặt trong mỗi mặt đều trải dài trong một VBO mảng phần tử (nhiều mặt từ các khối chia sẻ cùng một VBO cho đến khi nó đầy '; hãy nghĩ như thế malloc, những mặt có cùng kết cấu trong cùng VBO nếu có thể) và các chỉ số đỉnh ở phía bắc các mặt, các mặt phía nam và như vậy là liền kề chứ không phải là hỗn hợp. Khi tôi vẽ, tôi làm một glDrawRangeElementscho các mặt phía bắc, với hình bình thường đã được chiếu và chuẩn hóa, trong một bộ đồng phục. Sau đó, tôi làm các mặt phía nam và như vậy, vì vậy các quy tắc không có trong bất kỳ VBO nào. Đối với mỗi đoạn, tôi chỉ phải phát ra các khuôn mặt sẽ hiển thị - ví dụ, chỉ những người ở giữa màn hình cần vẽ hai bên trái và phải; Điều này là đơn giản GL_CULL_FACEở cấp độ ứng dụng.

Tốc độ lớn nhất, iirc, đã loại bỏ các khuôn mặt bên trong khi đa giác hóa từng đoạn.

Cũng quan trọng là quản lý kết cấu và sắp xếp các khuôn mặt theo kết cấu và đặt các khuôn mặt có cùng kết cấu trong cùng một vbo như các khuôn từ các khối khác. Bạn muốn tránh quá nhiều thay đổi kết cấu và sắp xếp các khuôn mặt theo kết cấu và như vậy sẽ giảm thiểu số lượng nhịp trong glDrawRangeElements. Sáp nhập các mặt gạch cùng liền kề thành hình chữ nhật lớn hơn cũng là một vấn đề lớn. Tôi nói về việc hợp nhất trong câu trả lời khác được trích dẫn ở trên.

Rõ ràng là bạn chỉ đa giác hóa các khối đã từng nhìn thấy được, bạn có thể loại bỏ các khối đó không thể nhìn thấy trong một thời gian dài và bạn tái lập các khối đa giác được chỉnh sửa (vì đây là trường hợp hiếm gặp so với kết xuất chúng).


Tôi thích ý tưởng tối ưu hóa sự thất vọng của bạn. Nhưng không phải bạn trộn lẫn các thuật ngữ "chặn" và "chunk" trong lời giải thích của bạn sao?
Martijn Courteaux

chắc là đúng. Một khối các khối là một khối các khối bằng tiếng Anh.
Sẽ

1

Tất cả các so sánh của bạn ( BlockDistanceComparator) đến từ đâu? Nếu đó là từ một chức năng sắp xếp, điều đó có thể được thay thế bằng một loại cơ số (nhanh hơn không có triệu chứng và không dựa trên so sánh)?

Nhìn vào thời gian của bạn, ngay cả khi bản thân việc sắp xếp không tệ, relativeToOriginchức năng của bạn sẽ được gọi hai lần cho mỗi comparechức năng; tất cả dữ liệu đó nên được tính một lần. Nó sẽ nhanh hơn để sắp xếp một cấu trúc phụ trợ, vd

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

và sau đó trong mã giả

// 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 ]

Xin lỗi nếu đó không phải là một cấu trúc Java hợp lệ (tôi đã không chạm vào Java kể từ khi chưa tốt nghiệp) nhưng hy vọng bạn hiểu ý tưởng đó.


Tôi thấy điều này thú vị. Java không có cấu trúc. Chà, có một thứ gọi là như thế trong thế giới Java nhưng nó phải làm với cơ sở dữ liệu, không giống với tất cả. Họ có thể tạo ra một lớp cuối cùng với các thành viên công cộng, tôi đoán rằng nó hoạt động.
Theraot

1

Vâng sử dụng VBO và CULL phải đối mặt, nhưng điều đó phù hợp với mọi trò chơi. Những gì bạn muốn làm là chỉ hiển thị khối nếu trình phát hiển thị cho người chơi, nếu các khối được chạm theo một cách cụ thể (giả sử một đoạn mà bạn không thể nhìn thấy vì nó nằm dưới lòng đất), bạn thêm các đỉnh của khối và tạo nó gần giống như một "khối lớn hơn", hoặc trong trường hợp của bạn - một khối. Điều này được gọi là chia lưới tham lam và nó làm tăng đáng kể hiệu suất. Tôi đang phát triển một trò chơi (dựa trên voxel) và nó sử dụng thuật toán chia lưới tham lam.

Thay vì kết xuất mọi thứ như thế này:

kết xuất

Nó ám chỉ nó như thế này:

kết xuất2

Nhược điểm của việc này là bạn phải thực hiện nhiều phép tính hơn cho mỗi đoạn trên bản dựng thế giới ban đầu hoặc nếu người chơi xóa / thêm một khối.

hầu như bất kỳ loại động cơ voxel nào cũng cần điều này để có hiệu suất tốt.

Những gì nó làm là kiểm tra xem mặt khối có chạm vào mặt khối khác hay không và nếu có: chỉ hiển thị dưới dạng một (hoặc không) mặt khối. Đó là một liên lạc đắt tiền khi bạn đang kết xuất khối rất nhanh.

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
Và điều đó có xứng đáng không? Có vẻ như một hệ thống LOD sẽ phù hợp hơn.
MichaelHouse

0

Có vẻ như mã của bạn đang chìm trong các đối tượng và các lệnh gọi hàm. Đo các con số, có vẻ như không có bất kỳ điều gì xảy ra.

Bạn có thể cố gắng tìm một môi trường Java khác hoặc đơn giản là làm rối tung các cài đặt của môi trường Java mà bạn có, nhưng cách đơn giản và đơn giản để tạo mã của bạn, không nhanh, nhưng một cách nhanh chóng, ít nhất là chậm trong nội bộ trong Vec3f để dừng lại mã hóa *. Làm cho mọi phương thức tự chứa, đừng gọi bất kỳ phương thức nào khác chỉ để thực hiện một số nhiệm vụ cấp độ.

Chỉnh sửa: Mặc dù có nhiều chi phí ở khắp mọi nơi nhưng dường như việc đặt hàng các khối trước khi kết xuất là trình ăn hiệu suất kém nhất. Điều đó có thực sự cần thiết? Nếu vậy, có lẽ bạn nên bắt đầu bằng cách đi qua một vòng lặp và tính toán khoảng cách từng khối để xuất xứ, và sau đó sắp xếp theo đó.

* Hướng đối tượng quá mức


Đúng, bạn sẽ tiết kiệm bộ nhớ, nhưng mất CPU! Vì vậy, OOO không quá tốt trong các trò chơi thời gian thực.
Gustavo Maciel

Ngay khi bạn bắt đầu định hình (và không chỉ lấy mẫu), mọi nội tuyến mà JVM thường biến mất. Nó giống như lý thuyết lượng tử, không thể đo lường một cái gì đó mà không đưa ra kết quả: p
Michael

@Gtoknu Điều đó không đúng, ở một mức độ nào đó, các lệnh gọi hàm bắt đầu chiếm nhiều bộ nhớ hơn mã nội tuyến. Tôi muốn nói có một phần tốt của mã được đề cập là xung quanh điểm hòa vốn cho bộ nhớ.
aaaaaaaaaaaa

0

Bạn cũng có thể thử chia nhỏ các phép toán thành các toán tử bitwise. Nếu bạn có 128 / 16, hãy thử tạo một toán tử bitwise : 128 << 4. Điều này sẽ giúp rất nhiều với các vấn đề của bạn. Đừng cố làm mọi thứ chạy hết tốc lực. Làm cho bản cập nhật trò chơi của bạn ở mức 60 hoặc một cái gì đó, và thậm chí phá vỡ nó cho những thứ khác, nhưng bạn sẽ phải phá hủy và đặt voxels hoặc bạn sẽ phải tạo một danh sách việc cần làm, sẽ giảm fps của bạn. Bạn có thể thực hiện tỷ lệ cập nhật khoảng 20 cho các thực thể. Và một cái gì đó như 10 cho cập nhật thế giới và hoặc thế hệ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.