OpenGL.Shader:11-阴影实现 - 定向光阴影

.:11-阴影实现 - 定向光阴影
感觉好久没写学习文章,主要还是在忙工作和生活 。国庆节过后2019年的时间就没剩下多少了,为自己为亲人奋斗吧 。
一、阴影映射的理论
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了 。阴影能够使场景看起来真实得多 , 并且可以让观察者获得物体之间的空间位置关系 。
这次内容需要用到上一节的深度纹理(Depth ) , 传送门在这 。深度纹理是以深度值为主要存储内容的纹理对象,其原理是:光源角度指向观察目标 的(投影矩阵*视图矩阵)光源矩阵空间,在这个光源空间下开启深度测试,保存其所有观察对象的遮挡情况 。根据遮挡情况,再进行阴影映射( ) 。
阴影映射( )背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了 。假设有一个地板,在光源和它之间有一个大盒子 。由于光源处向光线方向看去 , 可以看到这个盒子,但看不到地板的一部分 , 这部分就应该在阴影中了 。(这里的所有蓝线代表光源可以看到的 。黑线代表被遮挡的)
枯燥的理论学习部分结束,根据以上理论我们不难发现,阴影的实现是一种通用算法,只要有光源的存在+物体对象=阴影映射 。以下以代码为例进一步掌握阴影的实现 。
# 320 es
in vec3;
in vec3;
in vec2uv;
out{
vec3 ;
vec3 ;
vec2 ;
vec4 ;
} ;
mat4 ;
mat4 view;
mat4 model;
mat4 ;
void main()
=* view * model * vec4(, 1.0f);
. = uv; // 纹理uv坐标传递到片元着色器,进行插值
. = ((mat3(model))) * ;
. = vec3(model * vec4(, 1.0));
. =* vec4(., 1.0);
顶点着色器内容比较好理解 , 使用结构体把所有相关的输出整合起来;
法向量要经过法向量矩阵的变换(模型矩阵的逆矩阵的转置);

OpenGL.Shader:11-阴影实现 - 定向光阴影

文章插图
= 世界坐标系下的绝对位置坐标,留着往下片元着色器使用;
就是上文说的光源矩阵空间,光源矩阵空间*绝对位置坐标 , 就是光源空间下的顶点位置了;
接下看重点——片元着色器 。
# 320 es
float;
vec3;
;
;
vec3;
vec3;
in{
vec3 ;
vec3 ;
vec2 ;
vec4 ;
} fs_in;
outvec4;
float (vec4 )
[ ... ]
void main()
vec3 color = (, fs_in.).rgb;
vec3= (fs_in.);
vec3= ;
// 周围环境
vec3= 0.5 * color;
// 漫反射
vec3= ( - fs_in.);
float diff = max(dot(, ), 0.0);
vec3= diff * ;
// 阴影失真
//float bias = max(0.01 * (1.0 - dot(, )), 0.0005);
// 计算阴影
float= (fs_in.);
vec3= ( + (1.0 - ) * ) * color;
= vec4(, 1.0f);
片元着色器使用Blinn-Phong光照模型渲染场景 。接着计算出一个值,当在阴影中时是1.0,在阴影外是0.0 。然后,颜色会乘以这个阴影元素 。由于阴影不会是全黑的(由于散射),我们把分量从乘法中剔除 。
要检查一个片元是否在阴影中,首先要先把光空间的片元位置转换为裁切空间的标准化设备坐标(白话意思就是:3维空间转回变成屏幕2维坐标,好和深度纹理进行比较) 。当我们在顶点着色器输出一个裁切空间顶点位置到时,自动进行一个透视除法 , 将裁切空间坐标的范围-w到w转为-1到1 , 这要将x、y、z元素除以向量的w元素来实现 。由于裁切空间的并不会通过传到像素着色器里,我们必须自己做透视除法:
【OpenGL.Shader:11-阴影实现 - 定向光阴影】// 执行透视除法
vec3= .xyz / .w;
上面的的xyz分量都是[-1,1](下面会指出这对于远平面之类的点才成立),而为了和深度贴图的深度相比较,z分量需要变换到[0,1];为了作为从深度贴图中采样的坐标,xy分量也需要变换到[0,1] 。所以整个向量都需要变换到[0,1]范围 。
=* 0.5 + 0.5;
有了这些投影坐标,我们就能从深度纹理中采样得到0到1的结果,从第一个渲染阶段的坐标直接对应于变换过的NDC坐标 。我们将得到光的位置视野下最近的深度:
float= (, .xy).r;
为了得到片元的当前深度,我们简单获取投影向量的z坐标,它等于来自光的透视视角的片元的深度 。
float= .z;
实际的对比就是简单检查是否大于,如果是,那么片元就在被遮挡的背影中 。
float=>? 1.0 : 0.0;
完整的函数是这样的:
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();}
和之前不同的地方是 / 对象的绘制方法,以前是传入对象,而在绘制深度纹理方法中 , 传入参数也有差别 。主要是深度纹理需要的在光空间下的深度值 。而真实渲染的时候,是摄像机视野空间的效果 。以为例看看具体代码 。
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;IlluminateWithShadowsprogram;voidsetShadowMap(GLuint texId) {_ShadowMapId = texId;}voidsetSurfaceTexture(GLuint texId) {_texId = texId;}voidinit(const float size, const float y_pos){floatgSizeX = 10;floatgSizeZ = 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();}voidrender(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();}};
第一个参数和第二个参数,是用于对象的顶点具体位置渲染使用的;第三参数是光源位置;
第四个参数和第五个参数,是用于计算出光源空间矩阵的对象顶点位置,进而方便顶点所在片元的阴影判断 。
以上代码,如果没有很大的区别,运行的效果大概是这样的:
为啥是这样的呢,这种现象是因为存在很严重的阴影失真 和 多余的阴影遮挡测试 。怎么解决?请听下回分解!