Một cách tiếp cận bán phổ biến là tạo ra những gì tôi gọi là các thành phần đổ bóng , tương tự như những gì tôi nghĩ bạn đang gọi các mô-đun.
Ý tưởng tương tự như một biểu đồ xử lý hậu kỳ. Bạn viết các đoạn mã shader bao gồm cả đầu vào cần thiết, đầu ra được tạo và sau đó là mã để thực sự hoạt động trên chúng. Bạn có một danh sách biểu thị các shader nào sẽ được áp dụng trong mọi tình huống (liệu tài liệu này có cần một thành phần ánh xạ vết, cho dù thành phần bị hoãn hoặc chuyển tiếp được bật hay không, v.v.).
Bây giờ bạn có thể lấy biểu đồ này và tạo mã shader từ nó. Điều này chủ yếu có nghĩa là "dán" mã chunk vào vị trí, với biểu đồ đã đảm bảo chúng theo thứ tự cần thiết, và sau đó dán vào đầu vào / đầu ra của shader là phù hợp (trong GLSL, điều này có nghĩa là xác định "toàn cầu" của bạn trong , ra, và các biến thống nhất).
Điều này không giống như một cách tiếp cận ubershader. Ubershader là nơi bạn đặt tất cả mã cần thiết cho mọi thứ vào một bộ đổ bóng, có thể sử dụng #ifdefs và đồng phục và muốn bật và tắt các tính năng khi biên dịch hoặc chạy chúng. Cá nhân tôi coi thường cách tiếp cận ubershader, nhưng một số động cơ AAA khá ấn tượng sử dụng chúng (đặc biệt là Crytek nghĩ đến).
Bạn có thể xử lý các khối shader theo nhiều cách. Cách tiên tiến nhất - và hữu ích nếu bạn có kế hoạch hỗ trợ GLSL, HLSL và bảng điều khiển - là viết một trình phân tích cú pháp cho ngôn ngữ đổ bóng (có thể gần với HLSL / Cg hoặc GLSL như bạn có thể hiểu "tối đa" ) sau đó có thể được sử dụng cho các bản dịch từ nguồn tới nguồn. Một cách tiếp cận khác là chỉ gói các khối shader trong các tệp XML hoặc tương tự, ví dụ:
<shader name="example" type="pixel">
<input name="color" type="float4" source="vertex" />
<output name="color" type="float4" target="output" index="0" />
<glsl><![CDATA[
output.color = vec4(input.color.r, 0, 0, 1);
]]></glsl>
</shader>
Lưu ý với cách tiếp cận đó, bạn có thể tạo nhiều phần mã cho các API khác nhau hoặc thậm chí phiên bản phần mã (để bạn có thể có phiên bản GLSL 1.20 và phiên bản GLSL 3.20). Biểu đồ của bạn thậm chí có thể tự động loại trừ các khối shader không có phần mã tương thích để bạn có thể có được sự xuống cấp nửa duyên dáng trên phần cứng cũ (vì vậy một cái gì đó như ánh xạ bình thường hoặc bất cứ thứ gì chỉ bị loại trừ trên phần cứng cũ không thể hỗ trợ mà không cần lập trình viên làm một loạt các kiểm tra rõ ràng).
Mẫu XMl sau đó có thể tạo ra một cái gì đó tương tự (xin lỗi nếu đây là GLSL không hợp lệ, đã được một thời gian kể từ khi tôi tự mình chịu đựng API đó):
layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;
struct Input {
vec4 color;
};
struct Output {
vec4 color;
}
void main() {
Input input;
input.color = input_color;
Output output;
// Source: example.shader
#line 5
output.color = vec4(input.color.r, 0, 0, 1);
output_color = output.color;
}
Bạn có thể thông minh hơn một chút và tạo mã "hiệu quả" hơn, nhưng thành thật mà nói, bất kỳ trình biên dịch shader nào không phải là tào lao sẽ loại bỏ các phần thừa từ mã được tạo ra cho bạn. Có thể GLSL mới hơn cho phép bạn đặt tên tệp trong #line
các lệnh ngay bây giờ, nhưng tôi biết các phiên bản cũ hơn rất thiếu và không hỗ trợ điều đó.
Nếu bạn có nhiều khối, các đầu vào của chúng (không được cung cấp dưới dạng đầu ra bởi một tổ tiên trong cây) được nối vào khối đầu vào, cũng như các đầu ra và mã chỉ được nối. Một công việc phụ nhỏ được thực hiện để đảm bảo các giai đoạn khớp với nhau (đỉnh so với đoạn) và bố cục đầu vào thuộc tính đỉnh "chỉ hoạt động". Một lợi ích tuyệt vời khác với cách tiếp cận này là bạn có thể viết các chỉ số ràng buộc thuộc tính và thống nhất rõ ràng không được hỗ trợ trong các phiên bản cũ hơn của GLSL và xử lý chúng trong thư viện liên kết / tạo shader của bạn. Tương tự như vậy, bạn có thể sử dụng siêu dữ liệu trong việc thiết lập VBO và glVertexAttribPointer
các cuộc gọi của mình để đảm bảo tính tương thích và mọi thứ "chỉ hoạt động".
Thật không may, không có thư viện API chéo tốt như thế này. Cg khá gần, nhưng nó có hỗ trợ tào lao cho OpenGL trên thẻ AMD và có thể cực kỳ chậm nếu bạn sử dụng bất kỳ tính năng tạo mã cơ bản nhất nào. Khung hiệu ứng DirectX cũng hoạt động nhưng tất nhiên không hỗ trợ cho bất kỳ ngôn ngữ nào ngoài HLSL. Có một số thư viện không đầy đủ / lỗi cho GLSL bắt chước các thư viện DirectX nhưng được đưa ra trạng thái của chúng lần cuối cùng tôi kiểm tra tôi chỉ viết riêng cho mình.
Cách tiếp cận ubershader chỉ có nghĩa là xác định các chỉ thị tiền xử lý "nổi tiếng" cho các tính năng nhất định và sau đó biên dịch lại cho các vật liệu khác nhau với cấu hình khác nhau. ví dụ: đối với bất kỳ tài liệu nào có bản đồ bình thường, bạn có thể xác định USE_NORMAL_MAPPING=1
và sau đó trong ubershader giai đoạn pixel của bạn chỉ cần có:
#if USE_NORMAL_MAPPING
vec4 normal;
// all your normal mapping code
#else
vec4 normal = normalize(in_normal);
#endif
Một vấn đề lớn ở đây là xử lý vấn đề này đối với HLSL được biên dịch trước, trong đó bạn cần biên dịch trước tất cả các kết hợp đang sử dụng. Ngay cả với GLSL, bạn cần có khả năng tạo chính xác một khóa của tất cả các chỉ thị tiền xử lý đang sử dụng để tránh biên dịch lại / bộ đệm ẩn giống nhau. Sử dụng đồng phục có thể làm giảm độ phức tạp nhưng không giống như đồng phục tiền xử lý không làm giảm số lượng lệnh và vẫn có thể có một số tác động nhỏ đến hiệu suất.
Nói rõ hơn, cả hai cách tiếp cận (cũng như chỉ viết thủ công một tấn biến thể của shader) đều được sử dụng trong không gian AAA. Sử dụng bất cứ điều gì làm việc tốt nhất cho bạn.