實(shí)時(shí)陰影在3D游戲中基本上已經(jīng)得到了廣泛的應(yīng)用。實(shí)時(shí)的陰影效果能給游戲場(chǎng)景的真實(shí)度加分不少铡原。
陰影和光照的關(guān)系基本上是相輔相成的纱新。對(duì)陰影的渲染主要用到幀緩沖對(duì)象(Framebuffer Object),及其相關(guān)的繪制到紋理的技術(shù)捆愁。本來(lái)打算先寫一篇OpenGL ES繪制到紋理的相關(guān)文章的割去,后來(lái)發(fā)現(xiàn)通用的繪制到紋理的技術(shù)和實(shí)時(shí)陰影有很多共通點(diǎn),因此就放在一篇文章里描述要來(lái)的方便一些昼丑。
首先講一下實(shí)時(shí)陰影算法的原理吧呻逆。主要利用到OpenGL ES在坐標(biāo)變換之后的坐標(biāo)點(diǎn)的深度值。通俗的來(lái)講菩帝,OpenGL ES在經(jīng)過(guò)世界坐標(biāo)變換之后咖城,會(huì)生成每個(gè)坐標(biāo)點(diǎn)轉(zhuǎn)換后的深度值,然后這些深度值經(jīng)過(guò)深度檢測(cè)(DEPTH_TEST)和坐標(biāo)歸一化(轉(zhuǎn)換為-1呼奢,1之間的坐標(biāo))宜雀,最終得到的點(diǎn)的Z值既是我們?cè)谄聊簧纤芸吹降狞c(diǎn)。
那么握础,思路就出來(lái)了——
1.變換觀察點(diǎn)
我們將觀察點(diǎn)放在光源處辐董,也就是從光源點(diǎn)作為觀察點(diǎn),那么最終歸一化之后禀综,將會(huì)得到一個(gè)Za值简烘,然后將這個(gè)深度值寫入已經(jīng)綁定紋理的幀緩沖(Framebuffer Object)他匪,渲染到紋理。
2.重置觀察點(diǎn)并比較深度值
從真正的觀察點(diǎn)進(jìn)行坐標(biāo)變換夸研,也會(huì)得到一個(gè)Zb值邦蜜,從剛才幀緩沖中綁定的紋理中,采樣獲得第1步計(jì)算出的深度值Z亥至。我們比較這兩個(gè)Z值悼沈,如果Zb > Za則表示該坐標(biāo)點(diǎn)不在陰影內(nèi)(正常光照顯示),否則該點(diǎn)在陰影內(nèi)(顯示環(huán)境光+(漫反射+鏡面反射)×陰影系數(shù))(注:這里攝像頭朝Z-方向)姐扮。
看下代碼的關(guān)鍵步驟吧絮供。
創(chuàng)建深度幀緩沖并綁定紋理
// 創(chuàng)建并綁定一個(gè)紋理對(duì)象
glGenTextures(1, &shadowmap_tex);
glBindTexture(GL_TEXTURE_2D, shadowmap_tex);
// 設(shè)置紋理對(duì)象的數(shù)據(jù),因?yàn)槭亲鳛殇秩灸繕?biāo)茶敏,因此這時(shí)沒(méi)有數(shù)據(jù)
// 使用GL_DEPTH_COMPONENT16和GL_DEPTH_COMPONENT選項(xiàng)表明作為深度紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, g_glstate.swidth, g_glstate.sheight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
// 設(shè)置過(guò)濾選項(xiàng)壤靶,沒(méi)什么特別的
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 創(chuàng)建并綁定幀緩沖對(duì)象
glGenFramebuffers(1, &shadowmap_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, shadowmap_fbo);
// 將創(chuàng)建的紋理對(duì)象綁定到幀緩沖區(qū)
// GL_DEPTH_ATTACHMENT參數(shù)表明作為深度緩沖
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowmap_tex, 0);
設(shè)置觀察點(diǎn)為點(diǎn)光源坐標(biāo)
void QGLSL_SetUniformMat4v(const char *name, const char *uniform, const mat4 *in) {
GLint loc = QGLSL_GetUniformLoc(shader, uniform);
if (loc >= 0) {
glUniformMatrix4fv(loc, 1, GL_FALSE, in->m);
}
}
...
QGLSL_SetUniform1v(shadow_shader, "isPassLight", 1);
QGLSL_SetUniformMat4v(shadow_shader, "model", &model);
QGLSL_SetUniformMat4v(shadow_shader, "view", &lit_view);
QGLSL_SetUniformMat4v(shadow_shader, "projection", &lit_projection);
QGLSL_SetUniformMat4v(shadow_shader, "light_view", &lit_view);
QGLSL_SetUniformMat3v(shadow_shader, "norm_mat", &norm_mat);
上面函數(shù)就不一一介紹了,大致就是激活陰影shader程序惊搏,然后逐一設(shè)置常量贮乳。
綁定幀緩沖區(qū)繪制到紋理
// 激活并清空幀緩沖
glBindFramebuffer(GL_FRAMEBUFFER, shadowmap_fbo);
glViewport(0, 0, g_glstate.swidth, g_glstate.sheight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT);
// 設(shè)置偏移,減少陰影計(jì)算時(shí)的失真
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.5f, 20.0f);
// 渲染場(chǎng)景
QUnit_shadow_mapping_DrawScene();
綁定默認(rèn)緩沖區(qū)恬惯,重置觀察點(diǎn)并繪制到系統(tǒng)緩沖
// 重置觀察點(diǎn)到相機(jī)位置
QGLSL_SetUniform1v(shadow_shader, "isPassLight", 0);
QGLSL_SetUniformMat3v(shadow_shader, "norm_mat", &norm_mat);
QGLSL_SetUniformMat4v(shadow_shader, "model", &model);
QGLSL_SetUniformMat4v(shadow_shader, "view", &camera_view);
QGLSL_SetUniformMat4v(shadow_shader, "light_view", &lit_view);
QGLSL_SetUniformMat4v(shadow_shader, "projection", &camera_projection);
// 解除剛才綁定的緩沖區(qū)對(duì)象向拆,默認(rèn)繪制到系統(tǒng)離屏緩沖
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 關(guān)閉偏移
// glCullFace(GL_BACK);
glDisable(GL_POLYGON_OFFSET_FILL);
// 使用shader
QGLSL_Active(shadow_shader);
// 設(shè)置紋理對(duì)象(做采樣對(duì)比Z值繪制陰影時(shí)使用)
QGLSL_SetUniform1v(shadow_shader, "s_shadowmap", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, shadowmap_tex);
最終繪制結(jié)果如下:
Shader代碼以及重要的幾步工作
// vertex shader
precision highp float;
attribute vec3 vert_pos;
attribute vec3 vert_norm;
varying vec3 v_pos;
varying vec3 v_norm;
varying vec4 v_shadowcoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 light_view;
uniform mat3 norm_mat;
void main(void) {
const mat4 bias = mat4(0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.5, 0.5, 0.5, 1.0);
v_norm = normalize(norm_mat * vert_norm);
v_pos = vec3(view * model * vec4(vert_pos, 1.0));
v_shadowcoord = bias * projection * light_view * model * vec4(vert_pos, 1.0);
gl_Position = projection * view * model * vec4(vert_pos, 1.0);
}
在vertex shader source中很重要的一個(gè)東西就是這個(gè)bias矩陣,經(jīng)過(guò)MLP(區(qū)別于MVP酪耳,L指的是Light)變換后的xyz值是0-1之間的浓恳,需要將其歸一化為-1~1之間的值,通過(guò)矩陣運(yùn)算很容易實(shí)現(xiàn)碗暗。
// fragment shader source
precision highp float;
struct Light {
vec3 pos;
vec3 la;
vec3 ld;
vec3 ls;
};
uniform Light light;
struct Material {
vec3 ka;
vec3 kd;
vec3 ks;
float shininess;
};
uniform Material material;
// uniform sampler2DShadow s_shadowmap;
uniform sampler2D s_shadowmap;
varying vec3 v_pos;
varying vec3 v_norm;
varying vec4 v_shadowcoord;
uniform int isPassLight;
void main(void) {
if (1 == isPassLight) {
return;
}
vec3 s = normalize(light.pos - v_pos);
vec3 r = reflect(-s, v_norm);
vec3 v = normalize(-v_pos.xyz);
float sDotN = max(dot(s, v_norm), 0.0);
vec3 ambient = light.la * material.ka;
vec3 diffuse = light.ld * material.kd * sDotN;
vec3 specular = vec3(0.0);
if (sDotN > 0.0) {
specular = light.ls * material.ks * pow(max(dot(r, v), 0.0), material.shininess);
}
// 這里用作矯正偏移的
vec2 poissonDisk[4] = vec2[](
vec2(-0.94201624, -0.39906216),
vec2(0.94558609, -0.76890725),
vec2(-0.0941848101, -0.92938870),
vec2(0.34495938, 0.29387760)
);
vec4 shadow_coord = v_shadowcoord.xyzw / v_shadowcoord.w;
float shadow = 1.0;
for (int i = 0; i < 4; ++i) {
// 采樣深度紋理颈将,然后比較z值
vec4 tex_col = texture2D(s_shadowmap, shadow_coord.xy + poissonDisk[i]/700.0);
if (tex_col.z < shadow_coord.z) {
shadow -= 0.2;
}
}
gl_FragColor = vec4(ambient + (diffuse + specular) * shadow, 1.0);
}
至此,全部完成言疗。