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
  2.  
  3. uniform mat4 mvpMatrix;
  4.  
  5. layout (location = ) 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
  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 = ) in vec4 position;
  10. layout (location = ) 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
  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 = ) 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结点,我可以跳过第二次遍历。

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

第一次遍历,词法分析;

第二次遍历,语法分析;

第三次遍历,语义分析;

第四次遍历,优化……

CSharpGL(44)用ShadowMapping方式画物体的影子的更多相关文章

  1. CSharpGL(48)用ShadowVolume画模型的影子

    CSharpGL(48)用ShadowVolume画模型的影子 在Per-Fragment Operations & Tests阶段,有一个步骤是模版测试(Stencil Test).依靠这一 ...

  2. 利用Tkinter和matplotlib两种方式画饼状图

    当我们学习python的时候,总会用到一些常用的模块,接下来我就详细讲解下利用两种不同的方式画饼状图.首先利用[Tkinter]中的canvas画布来画饼状图: from tkinter import ...

  3. 转载 用ShadowVolume画模型的影子

    阅读目录(Content) Shadow Volume 包围盒 动态生成包围盒 判断 多光源下的阴影 总结 问题 CSharpGL(48)用ShadowVolume画模型的影子 回到顶部(go to ...

  4. unity3d 使用GL 方式画线

    这个是画线部分 private Vector3[] linePoints; public int m_LineCount; public int m_PointUsed; public void Re ...

  5. PHP中使用GD库方式画时钟

    <!--demo.html中内容--> <body> <img id="time" src="test.php" /> &l ...

  6. [U3D 画起重机,绑脚本和控制它运动的基本操作]

    之前在学习Unity3D,不知为何网上的教学资源真是少啊...我某段时间还卡在不知如何让物体绑个脚本自动运动.. 之所以要学习U3D是因为导师让我做的IOS项目里有个需要模拟起重机,从而控制真实起重机 ...

  7. 用drawRect的方式实现一个尺子

    用drawRect的方式实现了一个尺子选择器,demo在这里:https://github.com/Phelthas/LXMRulerView 效果如图:   如果不考虑复用的问题,我感觉最简单的实现 ...

  8. 利用border-radious画图形

    今天才发现,border-radius可以画很多图形,下面跟我来看一下吧: 在设有宽和高的情况下画一个圆: #div1{ /*宽高相等,圆角范围为高或宽的一半或以上*/ background-colo ...

  9. 手把手教你使用startuml画用例图

    转自:http://www.2cto.com/os/201502/377091.html 最近准备研究下volley的源码,但看了网上一些大牛的博客都是配合图这样看起来更直观,分析起来逻辑也很好,什么 ...

随机推荐

  1. html模板中的数字保留两位小数

    <script> //html模板中的数字保留两位小数 function formatCurrency(num) { num = num.toString().replace(/\$|\, ...

  2. jquery判断对象是否显示或隐藏

    if($('a.specail2').is(":visible")){ /**jquery判断对象是否显示或隐藏**/ $('a.one').hide(); $('a.specai ...

  3. Ionic2+ 环境搭建

    ionic2+官方guide 基础环境安装 nodejs安装 ionic,cordova安装 npm install -g ionic cordova 项目创建 ionic start MyIonic ...

  4. VB6获取Chrome地址栏的URL信息

    上篇写到了获取IE8浏览器URL的一般方法,那这篇就写下chrome的URL怎么获取.事实上,早期的chrome版本可以通过跟IE8差不多方式获取到URL信息.但是,现在chrome的控件都是Dire ...

  5. spa单页面应用(angular)

    本篇文章是对单页面的一个简单的基本逻辑操作,这个方法可以搭建基本的单页面的逻辑结构. 简单理解:单页面应用,锚点值切换,一个路由对应一个页面. 路由:此时会创建一个信息保存路由的信息,之后对页面a标签 ...

  6. Elasticsearch学习随笔(二)-- Index 和 Doc 查询新建API总结

    本文着重总结Elasticsearch的常见API了,进行分析. Index API 初始化Index,设置shards和replica PUT http://localhost:9200/firew ...

  7. MySQL实现自动使用uuid作为主键以及解决不能调用触发器的一点思路

    这里使用触发程序实现此功能. 触发程序语法如下: Create trigger <tri_name> {before|after} {insert|update|delete} On &l ...

  8. nodejs 实践:express 最佳实践 (一)

    express 最佳实践 (一) 最近,一直在使用 nodejs 做项目,对 nodejs 开发可以说深有体会. 先说说 nodejs 在业务中的脚色,, 在 web同构 方面, nodejs 的优势 ...

  9. [问题解决]linux sudo xxx:command not found

    题外话 软件的安装在linux下主要分为两种.一种是通过包管理器例如ubuntu的apt-get xxx,另一种是自己手动安装.通过包管理器安装的,基本开箱即用,无需配置,但是存在一个问题,有时候无法 ...

  10. MySQL 启动参数说明及性能优化建议

    [mysqld] port = 3306 serverid = 1 socket = /tmp/mysql.sock skip-name-resolve #禁止MySQL对外部连接进行DNS解析,使用 ...