Shader và cách viết trong Unity – (phần 1)

Shader và cách viết một shader đơn giản trong Unity (phần 1)

 

     Trong bài viết này chúng ta sẽ tìm hiểu về các nguyên tắc cơ bản của Shader trong Unity, sau đó là hướng dẫn cách viết 1 shader đơn giản không tương tác với ánh sáng (Unlit Shader) và sau đó là điều chỉnh một số thuộc tính của nó.

 

     1. Cơ bản về cách Unity hiển thị hình ảnh lên màn hình:

 

rendering process

      Rendering Process: Dựa vào giải đồ ở mức high-level trên về quá trình xử lý đồ họa của Unity chúng ta có thể thấy các vấn đề sau.

     – Một model 3D chứa tập hợp các điểm (vertices) tọa độ trong không gian, màu sắc của chúng (vertex colours), hướng (Normals – vertex directions) và dữ liệu tọa độ để khớp texture lên model (UV data – u, v biểu thị cho các trục x, y trên texture 2D).

     – Một GameObject cần 1 thành phần Mesh Renderer kèm theo Material (có thể kèm theo nhiều material) được gắn vào. Material sử dụng 1 shader có thể kèm theo nhiều thuộc tính như textures, colors, và vân vân…

     – Shader sử dụng dữ liệu từ model 3D và từ Material để vẽ ra các điểm (pixels) lên màn hình dựa trên mã CG/HGLS.

 

     Shader:

     – Là 1 chương trình máy tính được sử dụng để phủ lên (từ sử dụng trong quá trình này trong tiếng anh gọi là shading) trong quá trình kết xuất đồ họa: tạo ra mức độ ánh sáng, bóng tối và màu sắc phù hợp trong 1 bức ảnh, tạo hiệu ứng đặc biệt hoặc làm video hậu kỳ (như xử lý hiệu ứng hình ảnh, trong Unity chúng ta hay kết hợp các hiệu ứng từ post-processing để làm mọi thứ trông có vẻ cool hơn 😎).

     – Như đã nói, shader là 1 chương trình máy tính đặc biệt được viết ra để chạy trên GPU (bộ xử lý đồ họa) bằng 1 ngôn ngữ bậc cao CG/HGLS.

     – Theo như tài liệu từ Unity thì có 3 loại shader trong Unity:

          + Surface Shaders: lựa chọn tốt nhất nếu shader cần tương tác với ánh sáng, Surface Shaders giúp bạn dễ dàng viết các shader phức tạp 1 cách gọn nhẹ, có mức độ trừu tượng cao để tương tác với hệ thống ánh sáng của Unity. Hầu hết các Surface Shaders đều tự động hỗ trợ cả 2 Rendering Path là Forward và Deferred. Đừng sử dụng Surface Shaders nếu bạn không định tương tác với ánh sáng.

          + Vertex and Fragment Shaders: lựa chọn nếu shader của bạn không cần tương tác với ánh sáng, hoặc nếu bạn cần xử lý 1 số hiệu ứng lạ mà Surface Shaders không thể xử lý được. Các chương trình Shader được viết theo cách này là cách linh hoạt nhất để tạo hiệu ứng bạn cần (ngay cả Surface Shader cũng được tự động chuyển đổi thành một loạt Vertex và Fragment Shader). Nhưng giá phải trả là bạn phải tự viết nhiều mã hơn và khó để làm nó có thể tương tác được với ánh sáng.

          + Fixed Function Shaders: là cú pháp shader kế thừa cho các hiệu ứng đơn giản. Đại khái vậy, các bạn có thể tự đọc tiếp ở đây 😅

 

     2. Cấu tạo của một Unlit Shader.

     Chúng ta sẽ khám phá cấu trúc của Unlit Shader bằng việc mở 1 dự án Unity lên, tạo 1 shader trong Unity với kiểu là “Unlit Shader”.

create an unlit shader

     Một file script mới sẽ được tạo ra với tên mặc định là “NewUnlitShader”, rename nó thành cái gì bạn thích, ví dụ “DemoTransparency”, tạo một Material đặt tên tùy ý, ví dụ “MyUnlitMaterial”. Dùng chuột chọn material, phần shader chọn Unlit > NewUnlitShader. Bạn sẽ thấy mặc định của shader chúng ta vừa tạo trông như hình dưới.

set up unlit shader on editor

     Sau đó tạo 1 khối cube và kéo material vừa tạo vào, ta sẽ thấy khối cube thay đổi như hình dưới, bên trái là standard shader mặc định khi vừa tạo ra của khối cube và bên phải là unlit shader của chúng ta.

standard shader vs unlit shader

     Double click vào file shader DemoTransparency vừa tạo để mở nó lên và chúng ta sẽ bắt đầu khám phá cấu trúc của nó. Đây là code mặc định được sinh ra trong shader.


Shader "Unlit/NewUnlitShader"
{
     Properties
     {
          _MainTex ("Texture", 2D) = "white" {}
     }
     SubShader
     {
          Tags { "RenderType"="Opaque" }
          LOD 100

          Pass
          {
               CGPROGRAM
               #pragma vertex vert
               #pragma fragment frag
               // make fog work
               #pragma multi_compile_fog

               #include "UnityCG.cginc"

               struct appdata
               {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
               };

               struct v2f
               {
                    float2 uv : TEXCOORD0;
                    UNITY_FOG_COORDS(1)
                    float4 vertex : SV_POSITION;
               };

               sampler2D _MainTex;
               float4 _MainTex_ST;

               v2f vert (appdata v)
               {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    UNITY_TRANSFER_FOG(o,o.vertex);
                    return o;
               }

               fixed4 frag (v2f i) : SV_Target
               {
                    // sample the texture
                    fixed4 col = tex2D(_MainTex, i.uv);
                    // apply fog
                    UNITY_APPLY_FOG(i.fogCoord, col);
                    return col;
               }
               ENDCG
          }
     }
}
 

 

    Những ngôn ngữ được viết ở đây được gọi là ShaderLab, nó là sự pha trộn giữa 2 ngôn ngữ khác nhau. Chúng ta sẽ bắt đầu phân tích từ trên xuống, dòng đầu tiên Shader "Unlit/NewUnlitShader" định nghĩa đường dẫn của shader mà lúc đầu chúng ta đã chọn Unlit > NewUnlitShader cho material, chúng ta không nhất thiết phải đổi tên của file shader, chỉ cần đổi tên đường dẫn của shader tại dòng trên cùng này thôi là được ví dụ giờ mình sẽ viết lại thành Shader "Unlit/DemoTransparency", bây giờ nếu quay lại editor chọn material check phần shader sẽ thấy chúng ta đã có đường dẫn mới là Unlit > DemoTransparency thay vì Unlit > NewUnlitShader như ban đầu. 

     Kế đến phần Properties { ... }, phần này định nghĩa các thuộc tính của shader, nó là các biến dạng public nên sẽ xuất hiện trên editor để chúng ta có thể thấy và tùy chỉnh trực tiếp, hiện tại trong này mới chỉ có 1 thuộc tính là _MainTex ("Texture", 2D) = "white" {}, nó sẽ lấy input là 1 texture cho material như hình minh họa cho shader ban nãy. 

 
... 
     Properties 
     {
          // _MainTex là tên thực sự của biến
          // "Texture" chỉ là tên hiển thị bên ngoài editor
          // Chúng ta có thể thay Texture thành tên khác nếu muốn
          // Ví dụ _MainTex ("Albedo Texture", 2D) = "white" {} 
          _MainTex ("Texture", 2D) = "white" {} 
     }
...

 

     Tiếp theo chúng ta đến khối SubShader { ... } , đây là nơi chứa 1 tập hợp các chỉ dẫn thực sự cho Unity về cách để vẽ (render) ra đối tượng. Trong 1 shader có thể có nhiều SubShader, chúng ta có thể viết các SubShader cho các nền tảng khác nhau như PC, mobile… để tối ưu thực thi riêng cho từng nền tảng, và ở đây theo mặc định thì chúng ta chỉ có 1 SubShader duy nhất (Mình không chuyên viết shader nên sẽ không chém nhiều 😛). 

     Tiếp theo chúng ta có các tag Tags { "RenderType"="Opaque" } , tag này sẽ nói với Unity kiểu muốn vẽ ra là gì, hiện tại chúng ta có thể thấy nó sẽ là dạng Opaque, tiếp đến là LOD 100, đây là Level of Detail, chúng ta có thể thay đổi mức độ chi tiết của shader theo ý muốn.

     Tiếp theo đến khối Pass { ... } , khối này chứa mã gọi là Cg Program, CG là viết tắt của C for Graphics. Nó là ngôn ngữ bậc cao đã được phát triển bởi Nvidia phối hợp với Microsoft cho lập trình Vertex và pixel Shader. CG dựa trên ngôn ngữ C (theo Wikipedia). Ngôn ngữ này được dùng cho lập trình GPU. Trong khối Pass này có chứa 2 hàm sau chúng ta cần lưu ý là

 
... 
     // Đây là hàm Vertex - Vertex function
     v2f vert (appdata v) 
     { 
          ...
     } 

     // Đây là hàm fragment - fragment function
     fixed4 frag (v2f i) : SV_Target 
     { 
          ... 
     }
...

     Mình có 1 biểu đồ về những gì xảy ra trong Unlit Shader như sau

Unlit Shader diagram

     Vertex function được thực thi trước và lấy tất cả dữ liệu về các đỉnh của model, sẵn sàng để vẽ, cách nó được đặt như thế nào trong thế giới, vị trí như thế nào trước camera, tóm lại chúng ta sẽ xử lý cách vẽ ra model ở đây, ví dụ để tạo hiệu ứng nhiễu màn hình của object (glitch effect), chúng ta chỉ việc di chuyển vị trí của các điểm trong không gian (vertices) của đối tượng và sau đó sẽ truyền kết quả đến thành phần trong cấu trúc struct được định nghĩa trước đó, cuối cùng là truyền nó đến hàm fragment để thực thi, ví dụ ở đây là 

 
... 
     // Định nghĩa dữ liệu trong struct 1
     struct appdata 
     { 
          ... 
     };

     // Định nghĩa dữ liệu trong struct 2
     struct v2f 
     { 
          ... 
     };

...
     // Xử lý các đỉnh của đối tượng trong hàm vert này rồi trả kết quả ra
     v2f vert (appdata v) { ... }
     
     // Hàm frag này sẽ lấy dữ liệu từ hàm vert bên trên rồi vẽ ra
     fixed4 frag (v2f i) : SV_Target { ... }
... 

 

Còn tiếp…

Bạn nghĩ sao về bài viết này?

[Total: 0    Average: 0/5]

One thought on “Shader và cách viết trong Unity – (phần 1)

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *