Tìm hiểu về OpenGL ES 2.0 (phần cuối)
Ở phần trước chúng ta đã cùng nhau tìm hiểu một ví dụ nho nhỏ(vẽ một hình tam giác) sử dụng OpenGL ES 2.0 và phân tích cụ thể cách mà framework đó thực hiện từng bước để vẽ được một hình tam giác.Trong bài viết tiếp theo ngay sau đây chúng ta cùng tìm hiểm các kiến thức liên quan đến các kĩ thuật ...
Ở phần trước chúng ta đã cùng nhau tìm hiểu một ví dụ nho nhỏ(vẽ một hình tam giác) sử dụng OpenGL ES 2.0 và phân tích cụ thể cách mà framework đó thực hiện từng bước để vẽ được một hình tam giác.Trong bài viết tiếp theo ngay sau đây chúng ta cùng tìm hiểm các kiến thức liên quan đến các kĩ thuật dựng hình tiên tiến như Per-Fragment Lighting, Environment mapping, Particle system with point sprites...
I.Per-Fragment Lighting
1.Lighting with a Normal Map
Một normal map là một kết cấu 2D lưu trữ mỗi texel một vector.Các kênh màu đỏ đại diện cho các thành phần x, kênh xanh thành phần y, và các kênh màu xanh thành phần z. Đối với một bản đồ bình thường được lưu trữ như GL_RGB8 với dữ liệu GL_UNSIGNED_BYTE, tất cả các giá trị ở trong khoảng [0, 1].Thông thường, những giá trị này cần phải được thu nhỏ và ưu tiên đặt trong shader để remap về các giá trị trong đoạn [-1, 1]. Các khối fragment shader code cho thấy làm thế nào để lấy được dữ liệu từ normal map.
// Fetch the tangent space normal from normal map vec3 normal = texture2D(s_bumpMap, v_texcoord).xyz; // Scale and bias from [0, 1] to [-1, 1] and normalize normal = normalize(normal * 2.0 – 1.0);
Như bạn có thể thấy, một mã nhỏ của shader code sẽ lấy giá trị màu từ một bản đồ kết cấu(texture map) và sau đó nhân kết quả với hai rồi trừ đi một. Các Kết quả là các giá trị được rescaled vào trong đoạn [-1, 1] và nằm trong đoạn [0, 1]. Ngoài ra, nếu các dữ liệu trong normal map của bạn không được chuẩn hóa, bạn cũng cần phải chuẩn hóa các kết quả trong fragment shader. Bước này có thể được bỏ qua nếu normal map của bạn có chứa tất cả các vectơ đơn vị.
Vấn đề quan trọng khác cần giải quyết với mỗi đoạn ánh sáng là không gian nào được sử dụng để lưu trữ cấu trúc của chúng.Để giảm thiểu tính toán trong fragment shader, chúng ta không muốn phải chuyển đổi kết quả của từ normal map. Một cách để làm điều này là lưu trữ không gian thực trong normal map của bạn.Đó là các normal vectơ trong các normal map mà chúng đều đại diện cho một không gian thực normal vector. Sau đó, ánh sáng và hướng vectơ có thể được chuyển đổi vào không gian thực trong vertex shader và có thể được sử dụng trực tiếp với các giá trị lấy từ các normal map. Đáng kể nhất là các đối tượng phải được giả định là tĩnh(static) vì việc không được chuyển đổi có thể xảy ra trên một đối tượng Một vấn đề quan trọng nữa là các bề mặt giống nhau theo các hướng khác nhau trong không gian sẽ không thể chia sẻ cùng nhau texels trong normal map, có thể dẫn đến kết quả là các map lớn hơn nhiều.
2.Lighting Shaders
Một khi chúng ta có không gian tiếp tuyến của normal map và vectơ không gian tiếp tuyến được thiết lập, chúng ta có thể tiến hành xử lí với mỗi mảnh ánh sáng(per-fragment lighting).Đầu tiên, chúng ta hãy nhìn vào ví dụ dưới đây:
uniform mat4 u_matViewInverse; uniform mat4 u_matViewProjection; uniform vec3 u_lightPosition; uniform vec3 u_eyePosition; varying vec2 v_texcoord; varying vec3 v_viewDirection; varying vec3 v_lightDirection; attribute vec4 a_vertex; attribute vec2 a_texcoord0; attribute vec3 a_normal; attribute vec3 a_binormal; attribute vec3 a_tangent; void main(void) { // Transform eye vector into world space vec3 eyePositionWorld = (u_matViewInverse * vec4(u_eyePosition, 1.0)).xyz; // Compute world space direction vector vec3 viewDirectionWorld = eyePositionWorld - a_vertex.xyz; // Transform light position into world space vec3 lightPositionWorld = (u_matViewInverse * vec4(u_lightPosition, 1.0)).xyz; // Compute world space light direction vector vec3 lightDirectionWorld = lightPositionWorld - a_vertex.xyz; // Create the tangent matrix mat3 tangentMat = mat3(a_tangent, a_binormal, a_normal); // Transform the view and light vectors into tangent space v_viewDirection = viewDirectionWorld * tangentMat; v_lightDirection = lightDirectionWorld * tangentMat; // Transform output position gl_Position = u_matViewProjection * a_vertex; // Pass through texture coordinate v_texcoord = a_texcoord0.xy; }
Chúng ta có hai mẫu ma trận mà chúng ta cần để làm đầu vào cho vertex shader: u_matViewInverse và u_matViewProjection.Các u_matViewInverse chứa các nghịch đảo của ma trận điểm. Ma trận này được sử dụng để chuyển đổi vector ánh sáng và vector mắt (được view trong space) vào không gian thực.Bốn trạng thái đầu tiên trong main function được sử dụng để thực hiện chuyển đổi này và tính toán vector ánh sáng, xem vector trong world space. Bước tiếp theo trong shader là tạo ra một ma trận tiếp tuyến. Các không gian tiếp xúc với các đỉnh được lưu trữ trong ba thuộc tính đỉnh đó là: a_normal, a_binormal, và a_tangent.Ba vectơ xác định ba trục tọa độ của không gian tiếp tuyến cho mỗi đỉnh. Chúng ta xây dựng một ma trận 3 × 3 lưu trữ các vec tơ để tạo thành tiếp tuyến ma trận tangentMat.
Bước tiếp theo là chuyển đổi view và định hướng vectơ vào không gian tiếp tuyến bằng cách nhân chúng với các ma trận tangentMat. Hãy nhớ rằng, mục đích đặt ra ở đây là để có được cái nhìn và hướng vector vào không gian giống như các normals trong không gian tiếp tuyến normal map. Bằng cách chuyển đổi này trong vertex shader, chúng ta tránh làm bất kỳ biến đổi nào trong fragment shader. Cuối cùng, chúng ta tính toán vị trí đầu ra cuối cùng và đặt nó trong gl_Position và thông qua các kết cấu phối hợp cùng với các fragment shader trong v_texcoord.
Bây giờ chúng ta có view và hướng vector theo quan điểm không gian và kết cấu phối hợp được validate tới các fragment shader. Bước tiếp theo là những fragment được thắp sáng bằng cách sử dụng fragment shader show như trong ví dụ ngay dưới đây
precision mediump float; uniform vec4 u_ambient; uniform vec4 u_specular; uniform vec4 u_diffuse; uniform float u_specularPower; uniform sampler2D s_baseMap; uniform sampler2D s_bumpMap; varying vec2 v_texcoord; varying vec3 v_viewDirection; varying vec3 v_lightDirection; void main(void) { // Fetch basemap color vec4 baseColor = texture2D(s_baseMap, v_texcoord); // Fetch the tangent-space normal from normal map vec3 normal = texture2D(s_bumpMap, v_texcoord).xyz; // Scale and bias from [0, 1] to [-1, 1] and normalize normal = normalize(normal * 2.0 - 1.0); // Normalize the light direction and view direction vec3 lightDirection = normalize(v_lightDirection); vec3 viewDirection = normalize(v_viewDirection); // Compute N.L float nDotL = dot(normal, lightDirection); // Compute reflection vector vec3 reflection = (2.0 * normal * nDotL) - lightDirection; // Compute R.V float rDotV = max(0.0, dot(reflection, viewDirection)); // Compute Ambient term vec4 ambient = u_ambient * baseColor; // Compute Diffuse term vec4 diffuse = u_diffuse * nDotL * baseColor; // Compute Specular term vec4 specular = u_specular * pow(rDotV, u_specularPower); // Output final color gl_FragColor = ambient + diffuse + specular; }
Phần đầu tiên của fragment shader là một loạt các mẫu thống nhất được khai báo định nghĩa cho màu sắc môi trường xung quanh, đổ bóng, phản chiếu. Những giá trị được lưu trữ trong các mẫu dạng u_ambient, u_diffuse, và u_specular. Shader cũng được cấu hình với hai bộ lấy mẫu, s_baseMap và s_bumpMap, chúng được ràng buộc với một bản đồ màu sắc cơ bản và với normal map tương ứng.
Phần đầu tiên của fragment shader lấy các màu cơ bản từ base map và các giá trị từ normap.Như đã mô tả ở trên, normal vector lấy từ các bản đồ kết cấu quy mô sau đó chuyển đổi thành véc tơ đơn vị với các thành phần trong đoạn [-1, 1] .Tiếp theo, các light vector và view vector được chuẩn hóa và được lưu trữ trong light-Direction và viewDirection.Lý do mà bình thường là cần thiết là vì cách các biến khác nhau được nội suy.Các biến khác nhau được nội suy tuyến tính từ nguyên thủy. Khi nào nội suy tuyến tính được thực hiện giữa hai vectơ, kết quả có thể trở thành không còn ở dạng chuẩn hóa từ nội suy nữa. Để bù đắp cho hiện tượng này, các vectơ phải được chuẩn hóa trong fragment shader
II.Environment Mapping Kỹ thuật vẽ tiếp theo chúng ta cùng tìm hiểu liên quan đến kỹ thuật trước-là thực hiện lập enviroment map sử dụng một cubemap. Ví dụ được trình bày ở phần dưới đây:
uniform mat4 u_matViewInverse; uniform mat4 u_matViewProjection; uniform vec3 u_lightPosition; uniform mat4 u_eyePosition; varying vec2 v_texcoord; varying vec3 v_lightDirection; varying vec3 v_normal; varying vec3 v_binormal; varying vec23 v_tangent; attribute vec4 a_vertex; attribute vec2 a_texcoord0; attribute vec3 a_normal; attribute vec3 a_binormal; attribute vec 3 a_tangent; void main(void) { // Transform light position into world space vec3 lightPositionWorld = (u_matViewInverse * vec4(u_lightPosition, 1.0)).xyz; // Compute world-space light direction vector vec3 lightDirectionWorld = lightPositionWorld - a_vertex.xyz; // Pass the world-space light vector to the fragment shader v_lightDirection = lightDirectionWorld; // Transform output position gl_Position = u_matViewProjection * a_vertex; // Pass through other attributes v_texcoord = a_texcoord0.xy; v_normal = a_normal; v_binormal = a_binormal; v_tangent = a_tangent; }
Vertex shader trong ví dụ này là gần tương tự như trong ví dụ trước trước. Sự khác biệt chính là thay vì chuyển các vector hướng ánh sáng vào không gian tiếp tuyến, chúng tôi giữ vector ánh sáng trong không gian thực.Lý do chúng ta phải làm điều này là bởi vì chúng ta muốn lấy từ cubemap sử dụng một sự phản ánh không gian vector thế giới thực. Như vậy, thay vì chuyển các vector ánh sáng thành không gian tiếp tuyến, chúng ta sẽ chuyển đổi các normal vector tiếp xúc với không gian thế giới thực. Để làm như vậy, vertex shader phải pass normal, binormal, và tiếp tuyến validate vào fragment shader để một ma trận tiếp tuyến có thể được xây dựng.
III.Kết luận
Hy vọng loạt bài về tiềm hiểu OpenGL ES 2.0 sẽ giúp chúng ta có hướng tiếp cận tốt để tìm hiểu về framework này.