• CSharpGL(44)用ShadowMapping方式画物体的影子


    CSharpGL(44)用ShadowMapping方式画物体的影子

    在(前文)已经实现了渲染到纹理(Render To Texture)的功能,在此基础上,本文记录画物体的影子的方式之一——shadow mapping。

    下载

    CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL

    开始

    如图所示,在蓝色背景下,有一个金色的茶壶(Teapot模型)和一块灰色的地面(4个顶点组成的正方形),空中有一个立方体(代表光源的位置)发出光,照到茶壶和地面上,地面显示出了茶壶的影子,茶壶身上也显示出了壶柄和壶盖的部分影子。这就是shadow mapping的效果。

    本文仅以聚光灯类型的光源为例。其他类型的光源暂时先不做了,还有别的东西要弄。

    原理

    一个fragment为何会是阴影的所在地?因为有物体挡在了此fragment与光源之间。那么OpenGL如何反映这种(光源-物体-fragment)之间的遮挡关系?方法就是以光源的位置为摄像机的位置,渲染一遍场景,此时的深度图就能够反映这个关系:深度图上的数据,就是距离光源最近的fragment的深度值(即,如果某fragment的深度值大于此值,那么他就在阴影中,否则就是被光源照射到了)。

    下面是上图的深度图。(由于我设定茶壶是一直在旋转的,所以茶壶的角度可能不吻合)

    白色的部分,代表深度值为1,是最远的,颜色越黑,代表深度值越接近0,即最近的。

    我初次见到这种深度图的时候,误以为直接把这个图贴到地面上就完事了,然而看看最后的效果图,又不是这样,十分不解。现在才知道不是直接贴,而是以此为依据,判定fragment是否在阴影内,从而计算此fragment的颜色值。

    获取Depth texture

    为了获取深度图(Depth Texture),我们需要使用(上文)提到到Render To Teture技术:创建Framgbuffer,把Texture绑定到此Framebuffer的depth component上。然后以光源的位置为摄像机的位置,渲染整个场景。所以光源下的每个能够产生阴影的结点,都要有这样一个vertex shader(不需要fragment shadedr):

     1 #version 330
     2 
     3 uniform mat4 mvpMatrix;
     4 
     5 layout (location = 0) in vec4 position;;
     6 
     7 void main(void)
     8 {
     9     gl_Position = mvpMatrix * position;
    10 }

    哪里是影子?

    然后就依据深度图来判定fragment是不是在影子里。

    Vertex shader

    在shadow mapping的渲染过程中,由于需要和【摄像机在光源时,顶点在eye space里的坐标】比较,所以在vertex shader里要手动计算此坐标(shadow_coord)。

     1 #version 330
     2 
     3 uniform mat4 model_matrix;
     4 uniform mat4 view_matrix;
     5 uniform mat4 projection_matrix;
     6 
     7 uniform mat4 shadow_matrix;
     8 
     9 layout (location = 0) in vec4 position;
    10 layout (location = 1) in vec3 normal;
    11 
    12 out VS_FS_INTERFACE
    13 {
    14     vec4 shadow_coord;
    15     vec3 world_coord;
    16     vec3 eye_coord;
    17     vec3 normal;
    18 } vertex;
    19 
    20 void main(void)
    21 {
    22     vec4 world_pos = model_matrix * position;
    23     vec4 eye_pos = view_matrix * world_pos;
    24     vec4 clip_pos = projection_matrix * eye_pos;
    25     
    26     vertex.world_coord = world_pos.xyz;
    27     vertex.eye_coord = eye_pos.xyz;
    28     vertex.shadow_coord = shadow_matrix * world_pos;
    29     vertex.normal = normalize(mat3(view_matrix * model_matrix) * normal);
    30     
    31     gl_Position = clip_pos;
    32 }

    Fragment shader

    在fragment shader里,其他方面与一般的光照计算相同,只有在判定此fragment属于阴影内时,才会削弱光照对它的影响。sampler2DShadow是比sampler2D更适合做shadow mapping的纹理采样器类型,它指向的,就是上一步得到的深度图(depth texture)。可见,纹理是纹理,采样器是采样器,换个采样器,仍旧可以对相同的纹理采样,然而得到的数据是不同的。

     1 #version 330
     2 
     3 uniform sampler2DShadow depth_texture;
     4 uniform vec3 light_position;
     5 
     6 uniform vec3 material_ambient;
     7 uniform vec3 material_diffuse;
     8 uniform vec3 material_specular;
     9 uniform float material_specular_power;
    10 
    11 layout (location = 0) out vec4 color;
    12 
    13 in VS_FS_INTERFACE
    14 {
    15     vec4 shadow_coord;
    16     vec3 world_coord;
    17     vec3 eye_coord;
    18     vec3 normal;
    19 } fragment;
    20 
    21 void main(void)
    22 {
    23     vec3 N = normalize(fragment.normal);
    24     vec3 L = normalize(light_position - fragment.eye_coord);
    25     vec3 R = reflect(L, N);
    26     vec3 E = normalize(fragment.eye_coord);
    27     float NdotL = dot(N, L);
    28     float EdotR = dot(E, R);
    29     float diffuse = max(NdotL, 0.0);
    30     float specular = max(pow(EdotR, material_specular_power), 0.0);
    31     float f = textureProj(depth_texture, fragment.shadow_coord);
    32     
    33     color = vec4(material_ambient + f * (material_diffuse * diffuse + material_specular * specular), 1.0);
    34 }

    似乎忘了记录一下如何实现经典的光照模型,等下一篇吧。

    总结

    最近突然发现了多次遍历的妙用。

    解析obj格式的模型文件,可以用多次遍历的方式:

    第一次遍历,只记录顶点数目、索引数目等统计值;

    第二次遍历,根据上次的统计值,直接创建固定长度的数组,既不浪费空间,又避免了动态数组的可能反复分配空间的问题;

    第三次遍历,对上次的数据计算法线;

    第四次遍历,计算模型的体积和中心位置。

    渲染场景,也可以用多次遍历的方式:

    第一次遍历,根据场景的树结构,依次更新各个结点的位置;

    第二次遍历,对支持shadow mapping的结点,更新其depth texture;

    第三次遍历,渲染场景到窗口。

    这样代码就能拆分开来,还能根据情况选择是否使用其中某些遍历步骤。比如解析obj文件,如果不需要计算法线,我可以跳过第三次遍历;比如渲染场景,如果没有shadow mapping结点,我可以跳过第二次遍历。

    联想到编译器也是采用的多次遍历的方式,

    第一次遍历,词法分析;

    第二次遍历,语法分析;

    第三次遍历,语义分析;

    第四次遍历,优化……

  • 相关阅读:
    SSM中shiro的基本使用
    TortoiseGit小乌龟 git管理工具
    vux用法
    vue webpack打包
    vue2.0 watch
    vue2.0 $emit $on组件通信
    简单工具 & 杂技
    html基础问题总结
    Node应用进程管理器pm2的使用
    node express 登录拦截器 request接口请求
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/CSharpGL-44-shadow-mapping.html
Copyright © 2020-2023  润新知