Liên quan đến Java vs C ++, tôi đã viết một công cụ voxel ở cả hai (phiên bản C ++ được hiển thị ở trên). Tôi cũng đã viết động cơ voxel từ năm 2004 (khi chúng không thịnh hành). :) Tôi có thể nói với một chút do dự rằng hiệu năng C ++ vượt trội hơn nhiều (nhưng nó cũng khó mã hóa hơn). Nó ít hơn về tốc độ tính toán, và nhiều hơn về quản lý bộ nhớ. Thật tuyệt vời, khi bạn đang phân bổ / xử lý nhiều dữ liệu như những gì trong thế giới voxel, C (++) là ngôn ngữ để đánh bại. Tuy nhiên, bạn nên nghĩ về mục tiêu của mình. Nếu hiệu suất là ưu tiên cao nhất của bạn, hãy đi với C ++. Nếu bạn chỉ muốn viết một trò chơi mà không có hiệu năng vượt trội, Java hoàn toàn có thể chấp nhận được (bằng chứng là Minecraft). Có nhiều trường hợp tầm thường / cạnh, nhưng nói chung, bạn có thể mong đợi Java chạy chậm hơn khoảng 1,75-2,0 lần so với (được viết tốt) C ++. Bạn có thể thấy một phiên bản cũ hơn, tối ưu hóa cho động cơ của tôi đang hoạt động ở đây (EDIT: phiên bản mới hơn tại đây ). Mặc dù thế hệ chunk có vẻ chậm, nhưng hãy nhớ rằng nó đang tạo ra các sơ đồ voronoi 3D, tính toán các quy tắc bề mặt, ánh sáng, AO và bóng trên CPU bằng các phương pháp vũ phu. Tôi đã thử các kỹ thuật khác nhau và tôi có thể có được thế hệ chunk nhanh hơn khoảng 100 lần bằng cách sử dụng các kỹ thuật lưu trữ và lưu trữ khác nhau.
Để trả lời phần còn lại của câu hỏi của bạn, có rất nhiều điều bạn có thể làm để cải thiện hiệu suất.
- Bộ nhớ đệm. Bất cứ nơi nào bạn có thể, bạn nên tính toán dữ liệu một lần. Ví dụ, tôi nướng ánh sáng vào khung cảnh. Nó có thể sử dụng ánh sáng động (trong không gian màn hình, như một quá trình hậu kỳ), nhưng nướng trong ánh sáng có nghĩa là tôi không phải vượt qua các quy tắc cho các hình tam giác, có nghĩa là ....
Truyền càng ít dữ liệu vào card màn hình càng tốt. Một điều mọi người có xu hướng quên là bạn truyền càng nhiều dữ liệu vào GPU thì càng mất nhiều thời gian. Tôi vượt qua trong một màu duy nhất và một vị trí đỉnh. Nếu tôi muốn thực hiện chu kỳ ngày / đêm, tôi chỉ có thể thực hiện phân loại màu hoặc tôi có thể tính toán lại cảnh khi mặt trời dần thay đổi.
Vì việc truyền dữ liệu tới GPU rất tốn kém, có thể viết một công cụ trong phần mềm nhanh hơn ở một số khía cạnh. Ưu điểm của phần mềm là nó có thể thực hiện tất cả các loại thao tác dữ liệu / truy cập bộ nhớ mà đơn giản là không thể có trên GPU.
Chơi với kích thước lô. Nếu bạn đang sử dụng GPU, hiệu suất có thể thay đổi đáng kể dựa trên mức độ lớn của từng mảng đỉnh bạn vượt qua. Theo đó, chơi xung quanh với kích thước của các khối (nếu bạn sử dụng khối). Tôi đã thấy rằng các khối 64x64x64 hoạt động khá tốt. Không có vấn đề gì, giữ cho khối của bạn khối (không có lăng kính hình chữ nhật). Điều này sẽ làm cho mã hóa và các hoạt động khác nhau (như biến đổi) dễ dàng hơn, và trong một số trường hợp, hiệu suất cao hơn. Nếu bạn chỉ lưu trữ một giá trị cho độ dài của mọi thứ nguyên, hãy nhớ rằng hai thanh ghi ít bị tráo đổi trong quá trình tính toán.
Xem xét danh sách hiển thị (đối với OpenGL). Mặc dù chúng là cách "cũ", chúng có thể nhanh hơn. Bạn phải nướng một danh sách hiển thị thành một biến ... nếu bạn gọi các hoạt động tạo danh sách hiển thị trong thời gian thực, nó sẽ chậm một cách vô duyên. Làm thế nào là một danh sách hiển thị nhanh hơn? Nó chỉ cập nhật trạng thái, so với các thuộc tính trên mỗi đỉnh. Điều này có nghĩa là tôi có thể vượt qua tối đa sáu mặt, sau đó một màu (so với một màu cho mỗi đỉnh của voxel). Nếu bạn đang sử dụng GL_QUADS và voxel khối, điều này có thể tiết kiệm tới 20 byte (160 bit) cho mỗi voxel! (15 byte không có alpha, mặc dù thông thường bạn muốn giữ mọi thứ được căn chỉnh 4 byte.)
Tôi sử dụng phương pháp brute-force để hiển thị "khối" hoặc các trang dữ liệu, đây là một kỹ thuật phổ biến. Không giống như octrees, việc đọc / xử lý dữ liệu dễ dàng hơn / nhanh hơn nhiều, mặc dù ít thân thiện với bộ nhớ hơn (tuy nhiên, ngày nay bạn có thể nhận được 64 gigabyte bộ nhớ với giá 200 - 300 đô la) ... không phải người dùng trung bình có điều đó. Rõ ràng, bạn không thể phân bổ một mảng lớn cho toàn thế giới (một bộ voxels 1024x1024x1024 là 4 gigabyte bộ nhớ, giả sử int 32 bit được sử dụng cho mỗi voxel). Vì vậy, bạn phân bổ / dealloc nhiều mảng nhỏ, dựa trên sự gần gũi của chúng với người xem. Bạn cũng có thể phân bổ dữ liệu, lấy danh sách hiển thị cần thiết, sau đó kết xuất dữ liệu để tiết kiệm bộ nhớ. Tôi nghĩ rằng sự kết hợp lý tưởng có thể là sử dụng cách tiếp cận hỗn hợp giữa octrees và mảng - lưu trữ dữ liệu trong một mảng khi thực hiện quá trình tạo thế giới, chiếu sáng, v.v.
Kết xuất gần đến xa ... một pixel bị cắt là tiết kiệm thời gian. Gpu sẽ ném một pixel nếu nó không vượt qua bài kiểm tra bộ đệm sâu.
Chỉ hiển thị các đoạn / trang trong chế độ xem (tự giải thích). Ngay cả khi gpu biết cách cắt các polgyons bên ngoài khung nhìn, việc truyền dữ liệu này vẫn mất thời gian. Tôi không biết cấu trúc hiệu quả nhất cho việc này sẽ là gì ("đáng xấu hổ", tôi chưa bao giờ viết cây BSP), nhưng ngay cả một chương trình phát sóng đơn giản trên cơ sở từng đoạn có thể cải thiện hiệu suất và rõ ràng việc kiểm tra chống lại sự thất vọng khi xem tiết kiệm thời gian.
Thông tin rõ ràng, nhưng đối với người mới: xóa mọi đa giác không có trên bề mặt - tức là nếu một voxel bao gồm sáu mặt, hãy xóa các mặt không bao giờ được hiển thị (đang chạm vào một voxel khác).
Như một quy tắc chung của tất cả mọi thứ bạn làm trong lập trình: CACHE ĐỊA PHƯƠNG! Nếu bạn có thể giữ mọi thứ trong bộ nhớ cache (ngay cả trong một khoảng thời gian nhỏ, nó sẽ tạo ra sự khác biệt lớn. Điều này có nghĩa là giữ cho dữ liệu của bạn đồng nhất (trong cùng một vùng bộ nhớ) và không chuyển đổi các vùng bộ nhớ để xử lý quá thường xuyên. , lý tưởng, làm việc trên một khối trên mỗi luồng và giữ bộ nhớ đó dành riêng cho luồng. Điều này không chỉ áp dụng cho bộ đệm CPU. Hãy nghĩ về hệ thống phân cấp bộ đệm như thế này (chậm nhất đến nhanh nhất): mạng (đám mây / cơ sở dữ liệu / v.v.) -> ổ cứng (có ổ SSD nếu bạn chưa có), ram (lấy kênh gấp ba hoặc RAM lớn hơn nếu bạn chưa có), bộ đệm CPU, đăng ký. Hãy cố gắng giữ dữ liệu của bạn kết thúc sau, và không trao đổi nó nhiều hơn bạn phải.
Luồng. Làm đi. Thế giới Voxel rất phù hợp để phân luồng, vì mỗi phần có thể được tính toán (phần lớn) độc lập với các phần khác ... Tôi đã thấy một sự cải tiến gần gấp 4 lần (trên Core i7 4 lõi, trong thế hệ thủ tục khi tôi viết thói quen cho luồng.
Không sử dụng các kiểu dữ liệu char / byte. Hoặc quần short. Người tiêu dùng trung bình của bạn sẽ có bộ xử lý AMD hoặc Intel hiện đại (có thể như bạn). Các bộ xử lý này không có thanh ghi 8 bit. Họ tính toán byte bằng cách đặt chúng vào khe 32 bit, sau đó chuyển đổi chúng trở lại (có thể) trong bộ nhớ. Trình biên dịch của bạn có thể thực hiện tất cả các loại voodoo, nhưng sử dụng số 32 hoặc 64 bit sẽ mang lại cho bạn kết quả dễ đoán nhất (và nhanh nhất). Tương tự, giá trị "bool" không mất 1 bit; trình biên dịch thường sẽ sử dụng 32 bit đầy đủ cho một bool. Nó có thể hấp dẫn để thực hiện một số loại nén trên dữ liệu của bạn. Ví dụ: bạn có thể lưu trữ 8 voxels dưới dạng một số (2 ^ 8 = 256 kết hợp) nếu tất cả chúng đều cùng loại / màu. Tuy nhiên, bạn phải suy nghĩ về sự phân nhánh của việc này - nó có thể tiết kiệm rất nhiều bộ nhớ, nhưng nó cũng có thể cản trở hiệu suất, ngay cả với thời gian giải nén nhỏ, bởi vì ngay cả lượng nhỏ thời gian thêm đó cũng cân đối với kích thước của thế giới của bạn. Hãy tưởng tượng tính toán một raycast; đối với mỗi bước của raycast, bạn sẽ phải chạy thuật toán giải nén (trừ khi bạn đưa ra một cách thông minh để khái quát hóa phép tính cho 8 voxels trong một bước tia).
Như Jose Chavez đã đề cập, mẫu thiết kế fly trọng có thể hữu ích. Giống như bạn sẽ sử dụng một bitmap để thể hiện một ô trong trò chơi 2D, bạn có thể xây dựng thế giới của mình từ một số loại hình xếp (hoặc khối) 3D. Nhược điểm của điều này là sự lặp lại của kết cấu, nhưng bạn có thể cải thiện điều này bằng cách sử dụng kết cấu phương sai phù hợp với nhau. Như một quy tắc tự nhiên, bạn muốn sử dụng sự vận động bất cứ nơi nào bạn có thể.
Tránh xử lý đỉnh và pixel trong shader khi xuất hình học. Trong một công cụ voxel chắc chắn bạn sẽ có nhiều hình tam giác, do đó, ngay cả một trình đổ bóng pixel đơn giản cũng có thể giảm đáng kể thời gian kết xuất của bạn. Tốt hơn là kết xuất vào bộ đệm, sau đó bạn tạo pixel shader làm hậu xử lý. Nếu bạn không thể làm điều đó, hãy thử thực hiện các phép tính trong trình tạo bóng đỉnh của bạn. Các tính toán khác nên được đưa vào dữ liệu đỉnh nếu có thể. Các đường chuyền bổ sung trở nên rất tốn kém nếu bạn phải kết xuất lại tất cả các hình dạng (như ánh xạ bóng hoặc ánh xạ môi trường). Đôi khi tốt hơn là từ bỏ một cảnh năng động để ủng hộ các chi tiết phong phú hơn. Nếu trò chơi của bạn có các cảnh có thể sửa đổi (tức là địa hình có thể phá hủy), bạn luôn có thể tính toán lại cảnh đó khi mọi thứ bị phá hủy. Việc biên dịch lại không tốn kém và sẽ mất dưới một giây.
Hủy bỏ các vòng lặp của bạn và giữ cho mảng phẳng! Đừng làm điều này:
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;
}
EDIT: Thông qua thử nghiệm rộng rãi hơn, tôi đã thấy điều này có thể sai. Sử dụng trường hợp hoạt động tốt nhất cho kịch bản của bạn. Nói chung, các mảng phải bằng phẳng, nhưng sử dụng các vòng lặp đa chỉ số thường có thể nhanh hơn tùy theo trường hợp