9/28/15

Rendering pipeline trong OpenGL

Rendering pipeline là các bước theo thứ tự tuần tự mà opengl sẽ thực hiện để biến các vertex thành hình ảnh hiển thị ra màng hình.
Rendering pipeline sẽ tiến hành xử lý dữ liệu thô của vertex kết hợp với các bước lọc và test để cho ra hình ảnh cuối cùng trên màng hình. Rendering pipeline như 1 đường ống kéo dài, chia làm nhiều phân đoạn liên tiếp nhau và tại mỗi phần ta có thể enable, disable hoặc cấu hình nó để cho ra hình ảnh đúng với ý đồ của ta.
Bên dưới là sơ đồ đầy đủ của Rendering pipeline:
Phần tiếp theo chúng ta sẽ tìm hiểu sơ lượt về các thành phần của Rendering pipeline:

Vertex stream

Vertex stream là 1 mảng các vertex được gửi từ RAM sang GPU. Mỗi vertex sẽ chứa các attribute như position, normal, uv và color. Các gia trị attribute như là các biến số của mỗi vertex, nghĩa là các vertex có thể có các giá trị attribute khác nhau, đây cũng là điểm khác biệt với uniform.

Vertex shader

Cho phép chúng ta có thể áp dụng các phép biến đổi lên trên các vertex được truyền vô pipeline. Thực ra ở đây chúng ta sẽ sử dụng 1 đoạn code để định nghĩa 1 vertex shader, mỗi vertex shader sẽ như 1 chương trình hoàn chỉnh gồm biến số, vòng lặp,...và 1 hàm main. Vertex shader sẽ nhận các giá trị của vertex truyền vào như các attribute và uniform, dựa vào đó áp dụng các phép biến đổi cần thiết để cho ra kết quả như mong muốn. Ở phần này chúng ta có thêm khái niệm uniform, uniform cũng tương tự như attribute nhưng giá trị của chúng sẽ không thay đổi xuyên suốt pipeline, có thể hiểu đơn giản chúng như là các hằng số dùng chung cho tất cả các vertex khi truyền vào pipeline. Khi vertex shader nhận các giá trị attribte và uniform thì nó sẽ xử lý và giữ các giá trị này lại, để truyền các giá trị này cho các phân đoạn tiếp theo của pipeline(fragment shader) thì chúng ta cần sử dụng từ khóa varying, cụ thể như thế nào chúng ta sẽ tìm hiểu ở các phần tiếp theo về lập trình shader.

Index stream and Primitive assembly

Primitive assembly, đầu ra của vertex shader sẽ kết hợp với vertex connectivity(quy định cách các vertex kết hợp với nhau để tạo ra 1 primitive) sẽ tạo ra 1 primitive. Primitive có thể là: triangle, triangle_strip, quad, quad_strip, line...Thông dụng nhất vẩn là triangle và triangle_strip. Có thể hiểu đơn giản là ở bước này opengl sẽ cố gắng nối các điểm rời rạc thành các hình thù cơ bản như điểm, tam giác, tứ giác...
Dưới đây là ví dụ về tạo 2 tam giác từ 4 vertex:
Index stream, nó là 1 mảng các indices chứa thứ tự nối các đỉnh lại để tạo ra 1 primitive hoàn chỉnh. Nhờ cách này mà ta có thể giảm lượng dữ liệu truyền từ RAM sang GPU, nếu theo cách bình thường ta phải đẩy vertex sang GPU mỗi lần vẽ, còn theo cách sử dụng indices thì ta chỉ cần đẩy 1 lần các vertex sang GPU và sau đó truyền indices mỗi lần vẽ để opengl vẽ hình ta mong muốn như thế thì lưu lượng dữ liệu truyền giữa RAM và GPU sẽ giảm đáng kể.
Clipping, ở bước này opengl sẽ cắt toàn bộ các vertex nằm ngoài viewport(mặt định là toàn bộ cửa sổ render của chương trình), các vertex có liên quan đến vertex bị cắt bỏ sẽ được điều chỉnh lại để tạo ra các primitive mới sao cho khớp, và hình ảnh tạo ra trông giống như đang bị cắt thật sự vậy. Hình bên dưới là 1 ví dụ về clipping, hình chử nhật là viewport và người dùng chỉ thấy được các đoạn màu xanh.
Backface culling, theo lý thuyết nếu nó được enable thì nó sẽ xóa tất cả các primitive có normal tạo góc tù với view vector. Để hiểu đơn giản, chúng ta chia 1 primitive làm 2 mặt là mặt trước(front) và mặt sau(back), backface culling sẽ quyết định mặt nào sẽ được vẽ ra màng hình. Mặt trước hoặc mặt sau của 1 primitive được xác định bằng vector normal hoặc đơn giản là dựa vào thứ tự các đỉnh tạo ra primitive đó, nếu theo thứ tự ngược chiều kim đồng hồ thì mặt đó là mặt trước ngược lại sẽ là mặt sau.

Rasterization and Interpolation

Rasterization, ở bước này opengl sẽ nhận tọa độ của các vertex(hoặc là nhận primitive) để sinh ra các fragment(cung cấp thông tin để sinh ra pixel hiển thị lên màng hình).
Interpolation, theo lý thuyết nó sẽ tính toán các biến đổi cho mỗi fragment, hủy bỏ các biến không được sử dụng trong fragment shader. Các attribute không được sử dụng sẽ được truyền vào giá trị mặt định, các uniform không được sử dụng sẽ không được gán giá trị location(location sẽ là -1), những biến đổi không sử dụng sẽ không được tính toán.

Fragment shader

Cho phép chúng ta điều khiển màu sắt của các fragment, ngoài ra còn tạo ra giá trị alpha cho fragment để tạo hiệu ứng trong suốt. Ngoài ra chúng ta còn có thể sử dụng texture để áp các texture(thực chất là các hình ảnh) lên bề mặt của vật thể. Các fragment shader được lập trình bằng các đoạn code tương tự như vertex shader, fragment shader là 1 chương trình hoàn chỉnh và nó sẽ nhận các giá trị attribute, uniform được truyền từ vertex shader để xử lý fragment.

Buffer

Trước khi đi vào tìm hiểu các giai đoạn test của opengl trong pipeline chúng ta sẽ tìm hiểu qua 1 số buffer trong opengl như color buffer, stencil buffer, z buffer
Color buffer(pixel buffer), đây là bộ đệm chứa nội dung giá trị của màu cho pixel, nó được cấu thành từ 2 bộ đệm là front buffer và back buffer chúng được gọi là double buffer. Nội dung trong front buffer sẽ được hiển thị ra màng hình còn nội dung trong back buffer sẽ như là nội dung chuẩn bị trước để chuẩn bị hiển thị. Sau khi chuẩn bị xong back buffer, ta có thể swap 2 buffer hoặc copy nội dung của back buffer cho front buffer để vẽ ra screen.
Stencil buffer, nó là 1 extra buffer lưu giữ các giá trị nguyên cho mỗi pixel trên màng hình và nó được sử dụng cho stencil test. Chúng ta có thể hiểu đơn giản thế này, giả sử stencil buffer của chúng ta là 1 mảng các giá trị 0 và 1, opengl sẽ "áp" mảng này vào "ảnh" vị trí nào có giá trị 0 sẽ bị loại bỏ ra khỏi ảnh và chỉ giữ lại các vị trí có giá trị 1.
Depth buffer(Z buffer), buffer này lưu giữ độ "sâu" của các fragment nghĩa là lưu giữ giá trị z của fragment. Chú ý rằng depth buffer chỉ lưu giữ 1 giá trị độ sâu cho tất cả các fragment có cùng vị trí, cách lưu giữ như sau: đầu tiên nó so sánh giá trị độ sâu của fragment với giá trị đã có trong depth buffer, nếu giá trị này gần hơn với người nhìn thì nó sẽ ghi đè lên giá trị cũ trong depth buffer. Nhưng chuyện gì sẽ xảy ra nếu 2 hay nhiều fragment(primitive) có cùng 1 giá trị depth, đó là cả 1 vấn đề và người ta gọi vấn đề đó là z-fighting, để tránh z-fighting ta phải tăng độ chính xác cho depth buffer(16 bit thường sẽ xảy ra z-fighting, 24 và 32 bit sẽ hiếm xảy ra hơn, còn 8 bit thì hầu như không sử dụng vì xác xuất xảy ra z-fighting rất cao).

Testing

Các fragment sẽ trải qua 1 loạt các bước test ở giai đoạn này trước khi được chuyển thành pixel ra màng hình. Các bước test này sẽ được chúng ta enable/disable tùy thuộc vào từng hoàn cảnh cụ thể. Nếu 1 fragment vược qua được tất cả các bước test thì nó sẽ trở thành 1 pixel và giá trị của nó sẽ được ghi vào các bộ đệm. Các bước test lần lược bao gồm:
  • Depth test: opengl sẽ kiểm tra độ sâu của fragment hiện tại với độ sâu trong depth buffer, nếu nó gần với người nhìn hơn thì nó sẽ vược qua được bước test này.
  • Alpha test: chỉ được áp dụng với chế độ màu RGBA, nó sẽ so sánh giá trị alpha của fragment với 1 giá trị hằng số cho trước, kết quả so sánh sẽ quyết định fragment có vược qua được bước test này không.
  • Stencil test: kiểm tra tọa độ của fragment với stencil buffer, nghĩa là ta sẽ kiểm tra fragment với 1 vùng render(hình dạng tùy ý), nếu nó nằm ngoài vùng render đó thì fragment sẽ không vược qua được bước test này. Có thể hiểu đơn giản là ta sẽ áp 1 mặt nạ vô hình để lấy ra được hình ảnh cần thiết.
  • Scissor test: kiểm tra xem fragment đó có nằm ngoài vùng render(hình chữ nhật) hay không, nếu nằm ngoài thì nó sẽ bị loại bỏ và các bước test tiếp theo sẽ không cần phải thực hiện với nó nữa.
Thứ tự các bước test lần lược như sau: Scissor test -> Alpha test -> Stencil test -> Depth test.

Blending

Ở bước này hiệu ứng trong suốt sẽ được áp dụng và màu cuối cùng của các fragment sẽ được tính toán sau đó lưu vào color buffer. Sau bước này thì fragment chính thức trở thành pixel.

Như thế chúng ta đã tìm hiểu sơ lượt về Rendering pipeline cũng như các bước xử lý bên trong của nó. Tùy vào từng hoàng cảnh mà ta có thể enable/disable các chức năng test, lập trình lại toàn bộ các vertex shader và fragment shader hoặc cấu hình các bước khác của pipeline để cho ra các hình ảnh cũng như hiệu ứng như mong muốn.

0 nhận xét :

Post a Comment