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

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

 

     Trong bài viết này chúng ta sẽ tiếp tục tìm hiểu về cấu trúc của Unlit Shader và viết mã để tùy chỉnh nó theo ý muốn.

 

     3. The Vertex Function.

     Hãy quay trở lại với khối Pass { ... } trong script file shader DemoTransparency trong phần trước, chúng ta có thể thấy các dòng khai báo định nghĩa như sau.

 
... 
     Pass 
     { 
          CGPROGRAM 
          // Dòng này nghĩa là chúng ta sẽ có 1 hàm "vertex" được gọi là "vert"
          #pragma vertex vert
          // Và 1 hàm "fragment" được gọi là "frag"
          #pragma fragment frag           
          ...
          // Cú pháp quen thuộc trong C, chúng ta đang thêm file UnityCG.cginc
          // đến file này để lấy 1 tập hợp các hàm hữu ích cho việc rendering
          // trong Unity
          #include "UnityCG.cginc"
          ...

          // Mấy cái #... này được gọi là chỉ thị tiền xử lý
     }
... 

     Tiếp theo chúng ta tiến tới 2 cấu trúc struct ở đây, có thể hiểu chúng là các đối tượng chứa thông tin

 
...
     struct appdata 
     { 
          // Struct này chứa thông tin về các đỉnh (vertices) của model 3D và
          // giá trị sẽ được truyền vào 1 mảng đóng gói dạng vector float4, giá 
          // trị các số float lưu trong vector lần lượt tương ứng X, Y, Z và W. 
          // Tiếp đến ": POSITION" là 1 kiểu ràng buộc ngữ nghĩa, cơ bản nó 
          // nói với shader về cách nó sẽ được sử dụng để vẽ ra màn hình, ở
          // đây nó mang ý nghĩa là "vị trí" 
          float4 vertex : POSITION; 

          // Tọa độ của texture
          float2 uv : TEXCOORD0; 
     }; 

     struct v2f 
     { 
          // Tọa độ của texture
          float2 uv : TEXCOORD0;

          // Liên quan đến phần fog của Lighting, không phải trọng tâm ở đây 
          UNITY_FOG_COORDS(1)
          
          // Tương tự như bên trên thì ": SV_POSITION" ở đây nghĩa là "vị trí
          // không gian màn hình" (screen-space position)
          float4 vertex : SV_POSITION; 
     };
... 

 

     Tóm lại là chúng ta sẽ lấy tọa độ các điểm trong không gian của model 3D tại không gian tọa độ cục bộ của nó rối sau đó là UV của texture, rồi truyền thông tin của appdata đến hàm v2f vert (appdata v) { ... } làm tham số để xử lý, cú pháp của cái đống này nhìn hơi lạ vì nó không phải là code c# nên kệ đi, mình cũng không có tìm hiểu sâu về nó 🤔. Ý nghĩa của v2f ở đây là “vert to frag”, trong hàm này chúng ta thực hiện 1 loạt các chuyển đổi (translate) từ không gian cục bộ của object đến không gian của màn hình, cơ bản là theo thứ tự sau: local space -> world space -> view space -> clip space -> screen space. Hãy nhìn giải đồ minh họa sau.

coordinate systems

     OK, rõ ràng là khá đau đầu với các loại không gian. Tóm lại là tất cả những thứ lằng nhằng đó sẽ được hoàn thành với câu lệnh này UnityObjectToClipPos ở trong hàm vert đó, sau đó chúng ta sẽ chuyển đổi dữ liệu của texture, nó được thực hiện với lệnh này o.uv = TRANSFORM_TEX(v.uv, _MainTex); cuối cùng là trả về dữ liệu với lệnh return o; OK tổng kết lại là trong hàm vert này chúng ta đã cơ bản dựng model vào trong 1 hệ thống tọa độ thích hợp kèm theo các dữ liệu UV tương ứng của nó, sẵn sàng để truyền dữ liệu vào hàm fragment để nó có thể được biến thành các pixel lên màn hình.

 

     4. The Fragment Function.

     Trong phần này chúng ta sẽ nói đến mục đích của hàm fragment này.

 
... 
     // SV_Target là 1 ngữ nghĩa khác giống như POSITION lúc trước,
     // SV_Target là ngữ nghĩa được sử dụng bởi DX10+ cho đầu ra là màu
     // của fragment shader. Nó là 1 render target, trong trường hợp hiện
     // tại của chúng ta nó là bộ đệm khung (frame buffer) cho màn hình
     fixed4 frag (v2f i) : SV_Target 
     { 
          // Chúng ta có 1 biến dạng mảng đóng gói (packed array) fixed4
          // mà sẽ chứa các giá trị RGBA và 1 hàm helper text2D để đọc các
          // giá trị màu từ texture của model 3D của chúng ta
          fixed4 col = tex2D(_MainTex, i.uv); 
          ... 
          return col; 
     }
... 

 

     Bây giờ chúng ta đã hiểu qua về hàm frag này rồi, giờ chúng ta sẽ tiến hành thêm 1 biến màu sắc cho shader của chúng ta để nó có thể thay đổi màu sắc của object nếu object đó không chứa 1 texture nào cả (do không chứa texture thì lấy đâu ra màu mà dùng 🙄). 

 
... 
     Properties 
     { 
          _MainTex ("Texture", 2D) = "white" {} 
          
          // Chúng ta viết thêm 1 biến _MainColor với tên hiển thị là Tint Color
          // (1, 1, 1, 1) nghĩa là màu trắng
          _MainColor("Tint Color", Color) = (1, 1, 1, 1)
     }
     SubShader 
     {
          Tags { "RenderType"="Opaque" } 
          LOD 100
 
          Pass 
          {
               ...
               sampler2D _MainTex; 
               float4 _MainTex_ST;
 
               // Để sử dụng được biến _MainColor thì chúng ta cần khai báo
               // nó ở đây nữa, tên cần phải khớp như bên trong Properties, 
               // nhớ là để sử dụng được thì biến cần phải được khai báo cả 
               // trong Properties lẫn trong phần Cg Program này
               float4 _MainColor;
               ...
               
               fixed4 frag (v2f i) : SV_Target
               {
                    // Thêm việc nhân giá trị màu ban đầu với _MainColor
                    fixed4 col = tex2D(_MainTex, i.uv) * _MainColor;
                    // apply fog
                    UNITY_APPLY_FOG(i.fogCoord, col);
                    return col;
               }
          }
     }
... 

 

     Và bùm, chúng ta đã có thành quả như hình dưới đây.

Changle color of object

     Chúng ta có thể thay * _MainColor; bằng + _MainColor; nếu muốn có hiệu ứng Additive, nhưng hãy nhớ là dấu + chỉ có tác dụng nếu model có ít nhất 1 texture trên đó, nếu không có texture nào thì object sẽ cứ trắng hếu bất kể Tint Color có là cái màu gì đi nữa. 

 

     5. Transparent Shader.

     Trong phần này chúng ta sẽ cùng biến shader thành dạng trong suốt (transparent ).

 
... 
     SubShader 
     { 
          // Chúng ta sẽ thêm code vào ở đây
          Tags { "Queue"="Transparent" "RenderType"="Transparent" } 
          LOD 100 Pass 

          // Và cả ở đây nữa
          ZWrite Off
          Blend SrcAlpha OneMinusSrcAlpha
          ...
     } 
... 

 

     Mục đích của việc này là gì? Khi chúng ta vẽ ra những thứ dạng transparent thì chúng ta cơ bản cần quan tâm tới thứ tự vẽ. Vì để vẽ ra thứ gì trong suốt nghĩa là nó nằm ở trên cùng của những thứ khác => những thứ khác cần phải được vẽ trước. Vì vậy cơ bản chúng ta cần chỉ ra Queue, link chi tiết từ Unity ở đây, phần Rendering Order – Queue tag. Sau đó chúng ta thêm ZWrite Off, từ khóa này cho chúng ta biết là không vẽ vào bộ đệm sâu (depth buffer), mình không chuyên sâu viết shader nên chúng ta có thể đọc chi tiết nội dung ở đây, chúng ta có thể set ZWrite Off thành on hoặc off để điều khiển có hay không các pixel từ đối tượng này được viết vào depth buffer, mặc định là On nếu đang vẽ ra các đối tượng đục (solid) và các hiệu ứng semi-transparent, ngược lại thì… hãy đọc chi tiết ở trong tài liệu. Cuối cùng là chúng ta chỉ ra cách các pixel này sẽ được trộn như nào (nguyên văn là blend) với Blend SrcAlpha OneMinusSrcAlpha link chi tiết từ Unity ở đây, ở trường hợp này chúng ta nói rằng chúng ta sẽ render các pixel này theo thứ tự và tại 1 điểm nhất định nào đó chúng ta sẽ trộn chúng sử dụng kênh Alpha. Nhưng mọi thứ vẫn chưa xong, đến đây chúng ta tiếp tục thêm code nữa như sau.

 
... 
     Properties 
     { 
          _MainTex ("Texture", 2D) = "white" {} 
          _MainColor("Tint Color", Color) = (1, 1, 1, 1)

          // Thêm dòng code này vào để hiển thị biến public trên editor
          // Biến _Transparency với tên trên editor là Transparency và
          // có giá trị float từ 0 -> 1, mặc định là 0.5
          _Transparency("Transparency", Range(0.0, 1.0)) = 0.5    
     }
... 

 

Nó sẽ trông như hình này

Display Transparency variable on editor

     Sau đó chúng ta thêm mấy dòng code này nữa

 
... 
          sampler2D _MainTex;
          float4 _MainTex_ST;
	  float4 _MainColor;
	  
          // Thêm dòng code này
          float _Transparency;
          ... 
          fixed4 frag (v2f i) : SV_Target 
          { 
               fixed4 col = tex2D(_MainTex, i.uv) * _MainColor;

               // Thêm dòng code này để set kênh alpha
               col.a = _Transparency;

               UNITY_APPLY_FOG(i.fogCoord, col); 
               return col; 
          }
... 

 

     Như đã giải thích với biến _MainColor bên trên, để thực sự sử dụng được biến _Transparency thì chúng ta cần khai báo cả trong phần Properties lẫn phần Cg Program, vì vậy chúng ta khai báo là float _Transparency; sau đó bên trong hàm frag chúng ta thêm dòng code để set kênh alpha cho màu của đối tượng, kênh alpha chính là giá trị của biến _Transparency. OK mọi thứ đã xong xuôi chúng ta có thể test kết quả, nếu mọi thứ đều chính xác thì kết quả sẽ như hình minh họa sau.

Transparent values

 

     Và đó là kết thúc, trong bài viết này mình đã trình bày cơ bản về cách Unity hiển thị mọi thứ ra màn hình và hướng dẫn mọi người cách viết 1 shader đơn giản, không tương tác với ánh sáng và tùy biến transparent của nó. Hẹn gặp lại mọi người trong những bài viết sau.

 

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

[Total: 0    Average: 0/5]

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 *