感觉好久没写学习文章,主要还是在忙工作和生活。国庆节过后2019年的时间就没剩下多少了,为自己为亲人奋斗吧。
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。
这次内容需要用到上一节的深度纹理(Depth Texture),传送门在这。深度纹理是以深度值为主要存储内容的纹理对象,其原理是:光源角度指向观察目标 的(投影矩阵*视图矩阵)光源矩阵空间,在这个光源空间下开启深度测试,保存其所有观察对象的遮挡情况。根据遮挡情况,再进行阴影映射(Shadow Mapping)。
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。(这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment)
枯燥的理论学习部分结束,根据以上理论我们不难发现,阴影的实现是一种通用算法,只要有光源的存在+物体对象=阴影映射。以下以shader代码为例进一步掌握阴影的实现。
#version 320 es
in vec3 position;
in vec3 normal;
in vec2 uv;out VS_OUT {
vec3 FragPosWorldSpace;
vec3 Normal;
vec2 TexCoords;
vec4 FragPosLightSpace;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
vs_out.TexCoords = uv; // 纹理uv坐标传递到片元着色器,进行插值
vs_out.Normal = transpose(inverse(mat3(model))) * normal;
vs_out.FragPosWorldSpace = vec3(model * vec4(position, 1.0));
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPosWorldSpace, 1.0);
}
顶点着色器内容比较好理解,使用结构体VS_OUT把所有相关的输出整合起来;
法向量要经过法向量矩阵的变换(模型矩阵的逆矩阵的转置);
FragPosWorldSpace = 世界坐标系下的绝对位置坐标,留着往下片元着色器使用;
lightSpaceMatrix就是上文说的光源矩阵空间,光源矩阵空间*绝对位置坐标,就是光源空间下的顶点位置了;
接下看重点——片元着色器。
#version 320 es
precision mediump float;
uniform vec3 _lightColor;
uniform sampler2D _texture;
uniform sampler2D _shadowMap;
uniform vec3 _lightPos;
uniform vec3 _viewPos;in VS_OUT {
vec3 FragPosWorldSpace;
vec3 Normal;
vec2 TexCoords;
vec4 FragPosLightSpace;
} fs_in;out vec4 fragColor;
float ShadowCalculation(vec4 fragPosLightSpace)
{
[ ... ]
}
void main()
{
vec3 color = texture(_texture, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = _lightColor;
// 周围环境 ambient
vec3 ambient = 0.5 * color;
// 漫反射 diffuse
vec3 lightDir = normalize(_lightPos - fs_in.FragPosWorldSpace);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * lightColor;
// 阴影失真
//float bias = max(0.01 * (1.0 - dot(normal, lightDir)), 0.0005);
// 计算阴影
float shadow = ShadowCalculation(fs_in.FragPosLightSpace);
vec3 lighting = (ambient + (1.0 - shadow) * diffuse) * color;
fragColor = vec4(lighting, 1.0f);
}
片元着色器使用Blinn-Phong光照模型渲染场景。接着计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0。然后,diffuse颜色会乘以这个阴影元素。由于阴影不会是全黑的(由于散射),我们把ambient分量从乘法中剔除。
要检查一个片元是否在阴影中,首先要先把光空间的片元位置转换为裁切空间的标准化设备坐标(白话意思就是:3维空间转回变成屏幕2维坐标,好和深度纹理进行比较)。当我们在顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围-w到w转为-1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里,我们必须自己做透视除法:
// 执行透视除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
上面的projCoords的xyz分量都是[-1,1](下面会指出这对于远平面之类的点才成立),而为了和深度贴图的深度相比较,z分量需要变换到[0,1];为了作为从深度贴图中采样的坐标,xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。
projCoords = projCoords * 0.5 + 0.5;
有了这些投影坐标,我们就能从深度纹理中采样得到0到1的结果,从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。我们将得到光的位置视野下最近的深度:
float closestDepth = texture(shadowMap, projCoords.xy).r;
为了得到片元的当前深度,我们简单获取投影向量的z坐标,它等于来自光的透视视角的片元的深度。
float currentDepth = projCoords.z;
实际的对比就是简单检查currentDepth是否大于closetDepth,如果是,那么片元就在被遮挡的背影中。
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
完整的shadowCalculation函数是这样的:
float ShadowCalculation(vec4 fragPosLightSpace)
{
// 执行透视除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// 变换到[0,1]的范围
projCoords = projCoords * 0.5 + 0.5;
// 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标)
float closestDepth = texture(shadowMap, projCoords.xy).r;
// 取得当前片元在光源视角下的深度
float currentDepth = projCoords.z;
// 检查当前片元是否在阴影中
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
return shadow;
}
关键的理论部分学习已经结束,接下来就是实际操作。我打算做一个定点的光源,照射到原本的绿草地和正方体,并呈现出阴影效果。光源位置以一个白色小正方体模拟出位置。
void ShadowFBORender::surfaceCreated(ANativeWindow *window)
{
if (mEglCore == NULL) {
mEglCore = new EglCore(NULL, FLAG_TRY_GLES3);
}
mWindowSurface = new WindowSurface(mEglCore, window, true);
mWindowSurface->makeCurrent();
char res_name[250]={0};
sprintf(res_name, "%s%s", res_path, "land.jpg");
GLuint land_texture_id = TextureHelper::createTextureFromImage(res_name);
sprintf(res_name, "%s%s", res_path, "test.jpg");
GLuint texture_cube_id = TextureHelper::createTextureFromImage(res_name);
// 带阴影效果的正方体
cubeShadow.init(CELL::float3(1,1,1));
cubeShadow.setSurfaceTexture(texture_cube_id);
// 带阴影效果的草地板
landShadow.init(10, -1);
landShadow.setSurfaceTexture(land_texture_id);
// 实际的光源位置
mLightPosition = CELL::real3(5, 5, 2);
// 模拟光源位置的小正方体
lightPositionCube.init(CELL::real3(0.15f,0.15f,0.15f), 0);
lightPositionCube.mModelMatrix.translate(mLightPosition);
}
接下来实现绘制渲染的流程。
void ShadowFBORender::renderOnDraw(double elpasedInMilliSec)
{
mWindowSurface->makeCurrent();
matrix4 cProj(mCamera3D.getProject());
matrix4 cView(mCamera3D.getView());
mLightProjectionMatrix = CELL::perspective(45.0f, (float)mViewWidth/(float)mViewHeight, 0.1f, 30.0f);
mLightViewMatrix = CELL::lookAt(mLightPosition, CELL::real3(0,0,0), CELL::real3(0,1.0,0));
// Note.绘制深度纹理
renderDepthFBO();
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
glViewport(0,0, mViewWidth, mViewHeight);
// 绘制模拟光源位置的小正方体
lightPositionCube.render(mCamera3D);
// 绘制带阴影效果的地板
landShadow.setShadowMap(depthFBO.getDepthTexId());
landShadow.render(cProj,cView, mLightPosition, mLightProjectionMatrix, mLightViewMatrix);
// 绘制带阴影效果的正方体
cubeShadow.setShadowMap(depthFBO.getDepthTexId());
cubeShadow.render(cProj,cView, mLightPosition, mLightProjectionMatrix, mLightViewMatrix);
mWindowSurface->swapBuffers();
}
void ShadowFBORender::renderDepthFBO()
{
depthFBO.begin();
{
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
//glEnable(GL_CULL_FACE);
//glCullFace(GL_FRONT);
landShadow.render(mLightProjectionMatrix,mLightViewMatrix,
mLightPosition,
mLightProjectionMatrix,mLightViewMatrix);
cubeShadow.render(mLightProjectionMatrix,mLightViewMatrix,
mLightPosition,
mLightProjectionMatrix,mLightViewMatrix);
//glCullFace(GL_BACK);
//glDisable(GL_CULL_FACE);
}
depthFBO.end();
}
和之前不同的地方是LandShadow / CubeShadow对象的绘制方法,以前是传入Camera3D对象,而在绘制深度纹理方法renderDepthFBO中,传入参数也有差别。主要是深度纹理需要的在光空间下的深度值。而真实渲染的时候,是摄像机视野空间的效果。以LandShadow为例看看具体代码。
class LandShadow {
public:
struct V3N3T2 {
float x, y, z; //位置坐标
float nx, ny, nz; //法向量
float u,v; //纹理坐标
};
public:
V3N3T2 _data[6];
CELL::matrix4 _modelMatrix;
GLuint _texId;
GLuint _ShadowMapId;
IlluminateWithShadow sprogram;
void setShadowMap(GLuint texId) {
_ShadowMapId = texId;
}
void setSurfaceTexture(GLuint texId) {
_texId = texId;
}
void init(const float size, const float y_pos)
{
float gSizeX = 10;
float gSizeZ = 10;
V3N3T2 verts[] =
{
{-gSizeX, y_pos, -gSizeZ, 0,1,0, 0.0f, 0.0f}, // left far
{ gSizeX, y_pos, -gSizeZ, 0,1,0, size, 0.0f}, // right far
{ gSizeX, y_pos, gSizeZ, 0,1,0, size, size}, // right near
{-gSizeX, y_pos, -gSizeZ, 0,1,0, 0.0f, 0.0f}, // left far
{ gSizeX, y_pos, gSizeZ, 0,1,0, size, size}, // right near
{-gSizeX, y_pos, gSizeZ, 0,1,0, 0.0f, size} // left near
};
memcpy(_data, verts, sizeof(verts));
_modelMatrix.identify();
sprogram.initialize();
}
void render(matrix4 currentProjectionMatrix, matrix4 currentViewMatrix,
real3& lightPos,
matrix4 lightProjectionMatrix, matrix4 lightViewMatrix)
{
sprogram.begin();
// 加载材质纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _texId);
glUniform1i(sprogram._texture, 0);
// 加载阴影深度测试的纹理
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, _ShadowMapId);
glUniform1i(sprogram._shadowMap, 1);
// 用于对象顶点坐标的空间转换
glUniformMatrix4fv(sprogram._projection, 1, GL_FALSE, currentProjectionMatrix.data());
glUniformMatrix4fv(sprogram._view, 1, GL_FALSE, currentViewMatrix.data());
glUniformMatrix4fv(sprogram._model, 1, GL_FALSE, _modelMatrix.data());
glUniform3f(sprogram._lightColor, 1.0f, 1.0f, 1.0f);
glUniform3f(sprogram._lightPos, lightPos.x, lightPos.y, lightPos.z);
// 光源空间矩阵
matrix4 lightSpaceMatrix = lightProjectionMatrix * lightViewMatrix;
glUniformMatrix4fv(sprogram._lightSpaceMatrix, 1, GL_FALSE, lightSpaceMatrix.data());
// 绘制
glVertexAttribPointer(static_cast(sprogram._position), 3, GL_FLOAT, GL_FALSE,
sizeof(LandShadow::V3N3T2), &_data[0].x);
glVertexAttribPointer(static_cast(sprogram._normal), 3, GL_FLOAT, GL_FALSE,
sizeof(LandShadow::V3N3T2), &_data[0].nx);
glVertexAttribPointer(static_cast(sprogram._uv), 2, GL_FLOAT, GL_FALSE,
sizeof(LandShadow::V3N3T2), &_data[0].u);
glDrawArrays(GL_TRIANGLES, 0, 6);
sprogram.end();
}
};
第一个参数和第二个参数,是用于对象的顶点具体位置渲染使用的;第三参数是光源位置;
第四个参数和第五个参数,是用于计算出光源空间矩阵的对象顶点位置,进而方便顶点所在片元的阴影判断。
以上代码,如果没有很大的区别,运行的效果大概是这样的:
为啥是这样的呢,这种现象是因为存在很严重的阴影失真 和 多余的阴影遮挡测试。怎么解决?请听下回分解!