OpenGL - Làm thế nào để vẽ các họa tiết có hiệu suất cao như vậy


7

Tôi đang tự hỏi làm thế nào để vẽ hình học đơn giản với kết cấu có thể ăn quá nhiều hiệu suất (dưới 60 khung hình / giây)? Ngay cả card đồ họa tốt của tôi (GTX 960) cũng có thể "chỉ" vẽ tối đa 1000 họa tiết. Hoạ tiết tôi đang sử dụng là tất cả sức mạnh của 2 kết cấu và không vượt quá kích thước 512x512. Tôi thậm chí chỉ lọc với GL_NEAREST.
Các sprite chính họ được tạo ngẫu nhiên trong kích thước. Vì vậy, không có 1000 quads toàn màn hình, sẽ không có trường hợp sử dụng thực sự.

Tôi đang vẽ các họa tiết của mình theo đợt, nghĩa là tôi có một bộ đệm đỉnh động và bộ đệm chỉ mục tĩnh. Tôi cập nhật bộ đệm đỉnh mỗi khung hình glBufferSubDatamột lần và sau đó vẽ mọi thứ với `` glDrawElements`. Tôi có khoảng 5 kết cấu khác nhau mà tôi liên kết một lần trên mỗi khung hình dẫn đến 5 cuộc gọi rút thăm. Để kết xuất, tôi chỉ sử dụng một shader bị ràng buộc khi ứng dụng khởi động.
Vì vậy, tôi có 5 liên kết kết cấu, 5 cuộc gọi vẽ và một cập nhật bộ đệm đỉnh cho mỗi khung hình không thực sự nhiều.

Đây là một ví dụ với một kết cấu:

val shaderProgram = ShaderProgram("assets/default.vert", "assets/default.frag")
val texture = Texture("assets/logo.png")
val sprite = BufferSprite(texture)
val batch = BufferSpriteBatch()

val projView = Matrix4f().identity().ortho2D(0f, 640f, 0f, 480f)

fun setup() {
    glEnable(GL_TEXTURE)
    //glColorMask(true, true, true, true)
    //glDepthMask(false)

    glUseProgram(shaderProgram.program)
    texture.bind()

    batch.begin()
        for(i in 1..1000)
            batch.draw(sprite)
    batch.update()
}

fun render() {
    glClear(GL_COLOR_BUFFER_BIT)

    stackPush().use { stack ->
        val mat = stack.mallocFloat(16)
        projView.get(mat)
        val loc = glGetUniformLocation(shaderProgram.program, "u_projView")
        glUniformMatrix4fv(loc, false, mat)

        batch.flush()
    }

}

Các batch.draw()puts phương pháp sprites đỉnh dữ liệu trong một khía cạnh cpu đệm và batch.update()cập nhật mọi thứ để các gpu với glBufferSubData. Và thiết lập spritebatch trông như sau:

glBindBuffer(GL_ARRAY_BUFFER, tmpVbo)
            glBufferData(GL_ARRAY_BUFFER, vertexData, GL_STATIC_DRAW)
            glEnableVertexAttribArray(0)
            glEnableVertexAttribArray(1)
            glEnableVertexAttribArray(2)
            glVertexAttribPointer(0, 2, GL_FLOAT, false, 24 * sizeof(Float), 0)
            glVertexAttribPointer(1, 4, GL_FLOAT, false, 24 * sizeof(Float), 2.toLong() * sizeof(Float))
            glVertexAttribPointer(2, 2, GL_FLOAT, false, 24 * sizeof(Float), 6.toLong() * sizeof(Float))

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tmpEbo)
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)

Tôi đã định hình chương trình của mình trước, nhưng việc cập nhật bộ đệm đỉnh và tất cả các hình học chiếm khoảng 10% tổng thời gian trên mỗi khung. Nhưng việc hoán đổi bộ đệm chiếm phần còn lại của thời gian khung 90%.

Vì vậy, tôi đang hỏi, làm thế nào các trò chơi AAA lớn như vậy có thể hiển thị cảnh của chúng với hàng triệu đỉnh, nếu vẽ pixel là một nhiệm vụ tốn thời gian như vậy? Tôi biết rằng họ có rất nhiều tối ưu hóa trong mã, nhưng vẫn còn.


3
Bao gồm những gì chính xác bạn đang làm rất có thể sẽ cho phép câu trả lời được cá nhân hóa nhiều hơn. Ngay bây giờ không rõ liệu bạn đang hỏi về cách nó thường được thực hiện (cơ thể) hoặc tại sao trường hợp cụ thể của bạn thực hiện kém (tiêu đề). Đánh giá từ nhận xét của bạn, bạn đang ở sau tùy chọn 1 nhưng cung cấp thông tin khá mơ hồ về giải pháp hiện tại của bạn.
wonderra

Vui lòng bao gồm một ví dụ tối thiểu, đầy đủ và có thể kiểm chứng . Theo nhận xét cuối cùng, nếu chúng ta không biết chính xác những gì bạn đang làm, điều tốt nhất chúng tôi có thể làm là đưa ra những phỏng đoán có giáo dục.
Pikalek

Làm thế nào để các chương trình OpenGL khác thực hiện trên máy tính của bạn? Bạn có thể thử một cái gì đó như Quake hoặc Quake II - họ vẽ hơn 1000 đa giác trên mỗi khung và sẽ cho bạn ý tưởng về loại hiệu suất mà bạn nên mong đợi.
Maximus Minimus

Với GTX 960 đã đề cập, các chương trình khác hoạt động rất tốt. Tất cả vào lúc tối thiểu 60 khung hình / giây. Nhưng tôi không thể nhìn vào mã của họ một cách đáng tiếc và xem cách quản lý bộ đệm và bộ nhớ hoạt động ở đó.
mrdlink

Câu trả lời:


16

GPU của bạn có thể có thể hiển thị thậm chí 100 nghìn sprite mà không gặp vấn đề gì, nhưng bạn cần phải làm điều đó một cách thông minh. Sprites và hình học khác phải được cung cấp cho GPU theo lô được nhóm theo cùng một kết cấu, đổ bóng và chế độ hòa trộn.

Các trò chơi AAA lớn đang giảm thiểu các cuộc gọi rút thăm được phát hành cho GPU. Các cuộc gọi vẽ thường rất tốn kém , vì vậy nhiều thao tác vẽ tương tự được nhóm lại với nhau và gửi đến GPU theo từng đợt. Mỗi chế độ đổ bóng, kết cấu hoặc pha trộn mới thay đổi trong khi kết xuất kết quả trong một lệnh gọi riêng. Ngoài ra, atlase Texture được sử dụng để giảm các cuộc gọi vẽ (nhiều hình ảnh trên một kết cấu).


3
Vâng cái này. Có một cách đúng và một cách sai để vẽ các họa tiết, và cách sai là rất OO, sử dụng một lớp sprite, mỗi đối tượng sprite chứa bộ đệm đỉnh của nó, tính toán lại vị trí của nó trên CPU từng khung và cập nhật bộ đệm đỉnh đó cho từng khung , do đó, có rất nhiều thay đổi trạng thái và chi phí đồng bộ hóa; ném vào một số liên kết và bạn đã nhân đôi số lượng thay đổi trạng thái và bạn bắt đầu thấy 1000 sprite có thể kéo GPU hiện đại đến đầu gối như thế nào. Mã của OP có thể có một số hoặc tất cả những gì tôi vừa mô tả; nó đủ phổ biến
Maximus Minimus

Nhưng đó chính xác là những gì tôi đang làm. Tôi chỉ có một bộ đệm đỉnh cho tất cả các họa tiết phát hành một lệnh gọi cho mỗi kết cấu. Bất kỳ tôi chỉ có 5 kết cấu khác nhau và chỉ có một shader.
mrdlink

1
@mrdlink nếu bạn muốn phản hồi về việc cải thiện phương pháp cụ thể của mình, bao gồm các mẫu mã trong câu hỏi của bạn là cách tốt nhất để đảm bảo mọi người hiểu trò chơi của bạn hiện đang làm gì. Đặc biệt là nếu bạn dành thời gian để chỉnh sửa chúng thành các phần có liên quan tối thiểu, vì vậy mọi người không cần phải xem qua hàng trăm dòng trước khi họ có thể lượm lặt được sự hiểu biết đó.
DMGregory

@mrdlink Đặt kết cấu trong một tập bản đồ duy nhất. Bạn nên liên kết kết cấu một lần mỗi khung hình, cho TẤT CẢ các họa tiết đang được kết xuất

Tôi cảm thấy như bài viết là một chút sai lệch. Một cuộc gọi bốc thăm không có nghĩa là thay đổi trạng thái, cũng không có nghĩa là sự suy giảm hiệu suất. Bạn có thể vẽ một số đối tượng trong các cuộc gọi vẽ riêng biệt mà không thay đổi trạng thái. Trong trường hợp các lệnh gọi Sprite Batching draw trở nên rõ ràng hơn về hiệu năng vì lý do duy nhất để một bộ trộn sprite tạo ra một drawcall mới là nếu kết cấu hoặc shader đã thay đổi (hoặc nếu bộ đệm đầy, nhưng không thay đổi về kết cấu hoặc shader tho) . Số lượng Drawcall có thể gây hiểu nhầm. Nếu bạn sử dụng Renderdoc, bạn có thể thấy Unity / Unreal làm điều đó như thế nào.
Sidar

1

Cách bạn sắp xếp các sprite của bạn có thể là tối ưu. Nếu bạn đang sử dụng glDrawElements()để kết xuất một loạt nhiều sprite, thì điều đó chỉ có nghĩa là bạn đang lưu trữ 4 đỉnh trên một quad trong VBO của mình (nếu không tôi không thể thấy glDrawElements()một mình có thể tạo ra nhiều sprite như thế nào . Tôi có thể sai trường hợp nào cảm thấy tự do để sửa cho tôi).

Giải pháp phù hợp cũng phụ thuộc vào trường hợp sử dụng của bạn - nó không nhất thiết giống nhau đối với hệ thống hạt so với kết xuất 2D chung cho trò chơi.

Vấn đề là: chúng ta không cần chỉ số và chúng ta cũng không cần lưu trữ 4 vị trí đỉnh trên mỗi góc.
Bằng cách sử dụng ít bộ nhớ hơn, chúng tôi sẽ giảm lượng dữ liệu để cập nhật trên mỗi khung hình và giảm số lượng truy cập bộ nhớ, nên được coi là chậm.

Những gì tôi sẽ làm là instance Rendering .
Về cơ bản, vấn đề của bạn có thể được mô tả là hiển thị một lưới bốn mặt, nhưng 1000 lần với mỗi cài đặt khác nhau (bao gồm các biến đổi và thông tin cần thiết cho tra cứu kết cấu).
Ngoài ra, nếu bạn biết rằng các quads của bạn luôn phải đối mặt với màn hình, bạn thậm chí có thể đủ khả năng gửi ít thông tin hơn tới GPU (ví dụ: các vị trí như vec2, xoay như một float, v.v.).

Đây là một mã giả rất thô cho kết xuất đồ họa. Hướng dẫn chuyên sâu và giải thích về kỹ thuật này có sẵn rộng rãi và tôi rất khuyên bạn nên tìm kiếm một số.


// When setting up attrib pointers.
// See https://www.khronos.org/opengl/wiki/Vertex_Specification#Instanced_arrays
glVertexAttribDivisor(attribQuadCenter​, 1);
glVertexAttribDivisor(attribQuadScale​, 1);
glVertexAttribDivisor(attribTextureUnit​, 1);
glVertexAttribPointer(attribQuadCenter, ....);
glVertexAttribPointer(attribQuadScale, ....);
glVertexAttribIPointer(attribTextureUnit, .....);

glBufferSubData(...) // Supply all positions, scales and texture unit values.

// Rendering
for(int i=0 ; i<5 ; ++i) {
    glActiveTexture(GL_TEXTURE0 + i);
    glBindTexture(GL_TEXTURE_2D, textures[i]);
}
// Render absolutely all sprites in a single draw call.
glBindVertexArray(quad_vao);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 1000);

Một kỹ thuật khác bạn có thể xem xét là Point Sprites .
Sprites Point liên quan đến việc "vẽ" một đỉnh trên mỗi sprite; mỗi đỉnh sau đó được mở rộng thành một hình tứ giác không gian màn hình và bạn có thể điều chỉnh diện mạo của nó bằng cách sử dụng trình đổ bóng mảnh, được cho các tọa độ chuẩn hóa trong hình tứ giác đó (ví dụ: thực hiện tra cứu kết cấu).
Kích thước của quad không gian màn hình có thể được ghi vào trong shader đỉnh (nơi bạn cũng có thể chia nó cho z), như được giải thích trong bài viết được liên kết.

Một loạt những thứ khác để thử và hồ sơ:

  • Hãy thử kiểm tra độ sâu. Nếu bạn có đủ khả năng, việc kích hoạt nó sẽ giúp bạn tăng sức mạnh.
  • Xem xét loại bỏ các sprite lớn mà máy ảnh không thể nhìn thấy. Tuy nhiên, tôi nghi ngờ điều này không đáng làm trừ khi bạn làm việc với hơn 10 nghìn sprite hoặc hơn thế.
  • Gọi glBufferSubData () mọi khung để cập nhật dữ liệu cho mọi sprite có thể bị chậm và quy mô kém; Việc chuyển bộ nhớ từ CPU sang GPU rất tốn kém, đó là lý do tại sao chúng ta hiện có bộ đệm đỉnh thay vì đường ống chức năng cố định cũ.
    Nếu trường hợp sử dụng của bạn cho chính nó sử dụng, bạn có thể muốn sử dụng Compute Shader để cập nhật trực tiếp VBO bằng GPU (điều này có liên quan nhiều hơn một chút và có những tài nguyên trực tuyến tuyệt vời sẽ giải thích điều này tốt hơn tôi có thể).

Tôi nghĩ vấn đề không phải là cập nhật bộ đệm đỉnh hoặc đồng bộ hóa với cpu, gpu và trình điều khiển. Tôi đã thử vẽ mọi thứ với một VBO tĩnh và nó vẫn hoạt động như vậy. PointSprites không phải là lựa chọn, vì tôi không muốn các họa tiết của mình là bậc hai.
mrdlink

1
@mrdlink Như những người khác đã nói, tôi nghĩ rằng chúng tôi sẽ được hưởng lợi từ việc xem một số mã và có thể là một ảnh chụp màn hình, vì vậy chúng tôi có thể cung cấp các đề xuất tốt hơn. Bao nhiêu tổng không gian màn hình làm những sprite bao gồm? Có cần pha trộn không? Bạn đang sử dụng trình điều khiển độc quyền? Đây là những gì tôi sẽ thử tiếp theo: giảm kích thước kết cấu xuống còn 32x32 và xem liệu nó có cải thiện được gì không (nếu có, hãy cân nhắc sử dụng glGenerateMipmap () với kết cấu 512x512). Ngoài ra, nếu bạn chưa làm như vậy, bạn nên thử bật tính năng loại bỏ mặt sau. Và, tôi đoán là tôi đang cạn kiệt ý tưởng ở đây. :)
Yoan Lecoq ngày

Đối với 1000 sprite, điều này rất khó có thể là một nút cổ chai.
Maximus Minimus

@MaximusMinimus Tôi đồng ý với điều đó. Tôi vẫn chưa có kinh nghiệm khi nói đến sự hoàn hảo trong API đồ họa. :)
Yoan Lecoq ngày
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.