opengl 学习 之 16 lesson

简介

阴影贴图,在tutorial15中,我们学会了去创建了光照贴图,使用的是静态光源。它产生了很好的阴影,但是它不可以处理动态光照。

link

http://www.opengl-tutorial.org/uncategorized/2017/06/07/website-update/

http://www.opengl-tutorial.org/cn/intermediate-tutorials (还有中文版在一直没有察觉)

https://zhuanlan.zhihu.com/p/144025113 (知乎大佬)

基础的阴影贴图算法

基础的阴影贴图算法由两部分组成。第一,这个场景场景是由点光源渲染的。只要计算每个碎片的深度。第二,场景以普通的方式渲染,用而外的测试去看当前的片段是否在阴影中。

是否在阴影中的测试十分简单,如果当前的采样比光存储的阴影贴图中对应的点远。表示场景有一个对象比当前要渲染出来的片段离光更近。

渲染阴影贴图

在当前教程中,我们将会去只考虑直线光源 (类似于太阳光,所有的光线是平行投射的)。因此,渲染阴影贴图我们要用正交投影(TIPS:还有一个是透视投影,一共两个投影?)一个正交矩阵,就像一个透视投影举证,除了没有透视考虑在内。(正常,先得到正交矩阵才能得到透视矩阵)。一个对象将会看起来一样无论它离摄像头是远是近。

设置渲染对象和MVP矩阵

在教程14中,你知道了如何取渲染场景进入纹理中为了在渲染中读取它?(其实14并没有特别懂)。

我们这里使用一个1024x1024 16位的深度纹理去包含阴影贴图。16位通常足够生成阴影贴图了。我们使用深度纹理,而不是一个深度的渲染矩阵,因为我们将要去在之后采样他。

// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
GLuint FramebufferName = 0;
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName); // Depth texture. Slower than a depth buffer, but you can sample it later in your shader
GLuint depthTexture;
glGenTextures(1, &depthTexture);
glBindTexture(GL_TEXTURE_2D, depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT16, 1024, 1024, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0); glDrawBuffer(GL_NONE); // No color buffer is drawn to. // Always check that our framebuffer is ok
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;

模型视图投影矩阵渲染沉降从点光源的位置:

  • 投影矩阵是一个正交矩阵将包含坐标轴空间的所有的东西,X的空间范围(-10,10),Y的空间范围(-10,10),Z的空间范围是(-10,20)。这些值被设置然后所有可以看到的东西都会被看到。
  • 视图矩阵旋转这个世界,光线朝向是-Z
  • 模型矩阵可以是任何你想要的
glm::vec3 lightInvDir = glm::vec3(0.5f,2,2);

 // Compute the MVP matrix from the light's point of view
glm::mat4 depthProjectionMatrix = glm::ortho<float>(-10,10,-10,10,-10,20);
glm::mat4 depthViewMatrix = glm::lookAt(lightInvDir, glm::vec3(0,0,0), glm::vec3(0,1,0));
glm::mat4 depthModelMatrix = glm::mat4(1.0);
glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrix * depthModelMatrix; // Send our transformation to the currently bound shader,
// in the "MVP" uniform
glUniformMatrix4fv(depthMatrixID, 1, GL_FALSE, &depthMVP[0][0])

渲染器

在此过程使用渲染器十分简单。顶点着色器是一个传递着色器,简单计算顶点的坐标在齐次坐标系中:

#version 330 core

// Input vertex data, different for all executions of this shader.
layout(location = 0) in vec3 vertexPosition_modelspace; // Values that stay constant for the whole mesh.
uniform mat4 depthMVP; void main(){
gl_Position = depthMVP * vec4(vertexPosition_modelspace,1);
}

片段着色器也很简单:它简单的写入片段的深度

#version 330 core

// Ouput data
layout(location = 0) out float fragmentdepth; void main(){
// Not really needed, OpenGL does it anyway
fragmentdepth = gl_FragCoord.z;
}

渲染阴影贴图是两倍快对于普通的渲染,因为只有第分辨率的深度被写入了,而不是深度和颜色;内存带宽通常是最大的变现问题在GPU中。

使用阴影贴图

基础渲染器

现在我们回到我们通常的渲染器,对于每个片段我们计算的,我们必须测试他是否在阴影贴图后面。

为了做这个,我们需要去计算当前的片段位置在相同的空间我们曾经创建阴影贴图的地方。所以我们需要去转换它一次使用普通的MVP矩阵,另一次使用深度MVP矩阵。

但是这有一个小窍门。深度MVP矩阵乘以顶点坐标会得到齐次结果,结果在[-1,1]之间;但是纹理采样在[0,1] 之间。

举个例子,一个片段在屏幕的中间会是(0,0) 在齐次坐标;但是因为被采样在纹理坐标,UVs是(0.5,0.5)。

这可以被修复通过扭转得到的坐标直接应用于片段着色器,但是更困难去乘以齐次坐标通过以下的矩阵,简单的除2(不太懂,但是例子好像有点懂如下,先将得到[-1,1] / 2 得到 [-0.5, 0.5],然后平移得到[0,1])

glm::mat4 biasMatrix(
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
);
glm::mat4 depthBiasMVP = biasMatrix*depthMVP;

现在我们可以卸下顶点着色器。和之前一样但是,我们现在处处两个坐标而不是1个:

  • gl_Position 是顶点的坐标通过当前摄像头看到的坐标
  • ShadowCoord是顶点的坐标我们通过上一个摄像头看到的(光源的位置)
// Output position of the vertex, in clip space : MVP * position
gl_Position = MVP * vec4(vertexPosition_modelspace,1); // Same, but with the light's view matrix
ShadowCoord = DepthBiasMVP * vec4(vertexPosition_modelspace,1);

碎片着色器也很简单

  • 纹理(阴影贴图,阴影坐标).z 是距离在管线和最近的物体
  • 阴影坐标是距离在管线和当前碎片着色器

...所以如果当前的随便比最近的物体元,这意味着我们在阴影中:

float visibility = 1.0;
if ( texture( shadowMap, ShadowCoord.xy ).z < ShadowCoord.z){
visibility = 0.5;
}

我们仅仅需要去使用知识去修改我们的着色器。当然,环境色不需要求改,因为环境光的目的在生活中是模拟一些入射关键机试我们在阴影之中,否则阴影中将会变为纯黑色。

color =
// Ambient : simulates indirect lighting
MaterialAmbientColor +
// Diffuse : "color" of the object
visibility * MaterialDiffuseColor * LightColor * LightPower * cosTheta+
// Specular : reflective highlight, like a mirror
visibility * MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5);

result

问题

Shaow acne :阴影交错

猜测,因为管线贴图的分辨率不是特别大。所以上图 lightmap pixel 代表的距离,导致了一段黑的更接近光(其实我认为黑的应该是亮的,图中是否绘制错误了呢),一段黄的比light远,应该绘制为黑色(我觉得图中画反了)。我们增加一个偏移:

float bias = 0.005;
float visibility = 1.0;
if ( texture( shadowMap, ShadowCoord.xy ).z < ShadowCoord.z-bias){
visibility = 0.5;
}

修改 simple里面的面片着色器

 float bias = 0.005;
float visibility = texture( shadowMap, vec3(ShadowCoord.xy, (ShadowCoord.z - bias)/ShadowCoord.w) );

发现边缘还是不是特别好,发现tutorial里面也是这种效果。

但是,你发现因为我们的偏移,虚假在底面和墙上变得更糟糕了。更进一步,偏移0.005看起来在地面上足够了,但是不太足够在曲线曲面上:一些伪像存在圆柱和球上。

一个基本的方法是根据倾斜度修改偏移:

float bias = 0.005*tan(acos(cosTheta)); // cosTheta is dot( n,l ), clamped between 0 and 1
bias = clamp(bias, 0,0.01);

这个实验就先不做了~

阴影交错现象消失了,即使是在曲线和曲面上。

另一个敲门,可能会产生效果也可能不会产生效果基于集合,仅仅去渲染背面在渲染贴图。这使我们有一个更好的集合,看下一章(Perter Panning)。哦这个窍门是计算物体背面的渲染贴图,同时增加集合的厚度。

当我们渲染影音贴图的时候,剔除前面的三角形:

        // We don't use bias in the shader, but instead we draw back faces,
// which are already separated from the front faces by a small distance
// (if your geometry is made this way)
glCullFace(GL_FRONT); // Cull front-facing triangles -> draw only back-facing triangles

当我们渲染长的的时候,正常渲染(剔除背面)

 glCullFace(GL_BACK); // Cull back-facing triangles -> draw only front-facing triangles

Peter Panning(让墙起飞的现象)

我们没有了阴影交错了,但是我们任然有错误的底面渲染,让墙开起来仿佛在飞翔。事实上,增加偏移让它看起来更加糟糕。

这个很容易修复:简单避免很薄的集合体,这有两个优势:

  • 第一,它解决了Perter Panning:
  • 第二,他可以开启背面去除当渲染光照贴图,因为现在,这有一个多边形墙面对着光,将会咬合另一边。

缺点是你有更多的三角形面片需要被渲染(两倍时间)

Aliasing(混叠)

即使用了这两个敲门你也会注意到,球的边界上仍然有一些点亮边上点暗。

PCF

最简单的方法是提升这个去改变阴影贴图采样器类型为sample2DShadow.结果是当你采样阴影贴图,硬件将会采样周围的纹理,作比较,然后返回一个float值在[0,1]之间带有一个双线性过滤对于比较的结果。

举个例子,0.5代表2个采样在阴影,两个在光线之中。

表示这不和但采样滤波深度贴图!比较通常返回true或者false;PCF给出要给插值对于4个“true or false".

正如你看到的,阴影边界开始变得顺滑,但是阴影贴图纹理依旧可见。

Poisson Sampling(泊松采样)

一个简单的方法去解决这个是去赛扬阴影贴图N次而不是一次。使用PCF混合,这将产生很好的结果,及时用一个小一点的N。这有代码用了四次泊松采样。

for (int i=0;i<4;i++){
if ( texture( shadowMap, ShadowCoord.xy + poissonDisk[i]/700.0 ).z < ShadowCoord.z-bias ){
visibility-=0.2;
}
}

poissonDisk 是一个静态数组定义,举个例子如下所示

vec2 poissonDisk[4] = vec2[](
vec2( -0.94201624, -0.39906216 ),
vec2( 0.94558609, -0.76890725 ),
vec2( -0.094184101, -0.92938870 ),
vec2( 0.34495938, 0.29387760 )
);

生成的片段着色器汇编的更加亮或者更加暗依据阴影贴图的采样次数。

700这个常数定一个了样本分布的范围。

分层泊松采样

我们可以移除这个条纹通过选择不同的采样对于每个像素。这有两个主要的方法:分层泊松或者旋转泊松。分层泊松选择不同的样本;旋转泊松总是使用相同的采样,但是带有一个随机的旋转,所以他们看起来不同。在这个教程中我将只会解释分层泊松。

和之前的版本的位移不同我们将使用index用一个随机的值

for (int i=0;i<4;i++){
int index = // A random number between 0 and 15, different for each pixel (and each i !)
visibility -= 0.2*(1.0-texture( shadowMap, vec3(ShadowCoord.xy + poissonDisk[index]/700.0, (ShadowCoord.z-bias)/ShadowCoord.w) ));
}

我们可以生成一个随机的数字,返回一个随机的数字从[0,1]:

float dot_product = dot(seed4, vec4(12.9898,78.233,45.164,94.673));
return fract(sin(dot_product) * 43758.5453);

在我们的例子中,seed4将是i的组合和...其他东西。我们可以使用gl_FragCoord像素的位置在屏幕上,或者世界坐标系的位置:

        //  - A random sample, based on the pixel's screen location.
// No banding, but the shadow moves with the camera, which looks weird.
int index = int(16.0*random(gl_FragCoord.xyy, i))%16;
// - A random sample, based on the pixel's position in world space.
// The position is rounded to the millimeter to avoid too much aliasing
//int index = int(16.0*random(floor(Position_worldspace.xyz*1000.0), i))%16;

完美的噪音比条纹稍微令人接受。

其他提升的方法

Early bailing

Spot lights

......

opengl 学习 之 16 lesson的更多相关文章

  1. OpenGL学习之路(一)

    1 引子 虽然是计算机科班出身,但从小对几何方面的东西就不太感冒,空间想象能力也较差,所以从本科到研究生,基本没接触过<计算机图形学>.为什么说基本没学过呢?因为好奇(尤其是惊叹于三维游戏 ...

  2. OpenGL学习之路(三)

    1 引子 这些天公司一次次的软件发布节点忙的博主不可开交,另外还有其它的一些事也占用了很多时间.现在坐在电脑前,在很安静的环境下,与大家分享自己的OpenGL学习笔记和理解心得,感到格外舒服.这让我回 ...

  3. OpenGL学习之路(四)

    1 引子 上次读书笔记主要是学习了应用三维坐标变换矩阵对二维的图形进行变换,并附带介绍了GLSL语言的编译.链接相关的知识,之后介绍了GLSL中变量的修饰符,着重介绍了uniform修饰符,来向着色器 ...

  4. OpenGL学习之windows下安装opengl的glut库

    OpenGL学习之windows下安装opengl的glut库 GLUT不是OpenGL所必须的,但它会给我们的学习带来一定的方便,推荐安装.  Windows环境下的GLUT下载地址:(大小约为15 ...

  5. OpenGL学习进程(10)第七课:四边形绘制与动画基础

        本节是OpenGL学习的第七个课时,下面以四边形为例介绍绘制OpenGL动画的相关知识:     (1)绘制几种不同的四边形: 1)四边形(GL_QUADS) OpenGL的GL_QUADS图 ...

  6. OpenGL学习进程(7)第五课:点、边和图形(二)边

    本节是OpenGL学习的第五个课时,下面介绍OpenGL边的相关知识: (1)边的概念: 数学上的直线没有宽度,但OpenGL的直线则是有宽度的.同时,OpenGL的直线必须是有限长度,而不是像数学概 ...

  7. OpenGL学习进程(6)第四课:点、边和图形(一)点

    本节是OpenGL学习的第四个课时,下面介绍OpenGL点的相关知识:     (1)点的概念:     数学上的点,只有位置,没有大小.但在计算机中,无论计算精度如何提高,始终不能表示一个无穷小的点 ...

  8. OpenGL学习笔记3——缓冲区对象

    在GL中特别提出了缓冲区对象这一概念,是针对提高绘图效率的一个手段.由于GL的架构是基于客户——服务器模型建立的,因此默认所有的绘图数据均是存储在本地客户端,通过GL内核渲染处理以后再将数据发往GPU ...

  9. OpenGL学习进程(12)第九课:矩阵乘法实现3D变换

    本节是OpenGL学习的第九个课时,下面将详细介绍OpenGL的多种3D变换和如何操作矩阵堆栈.     (1)3D变换: OpenGL中绘制3D世界的空间变换包括:模型变换.视图变换.投影变换和视口 ...

  10. OpenGL学习进程(11)第八课:颜色绘制的详解

        本节是OpenGL学习的第八个课时,下面将详细介绍OpenGL的颜色模式,颜色混合以及抗锯齿.     (1)颜色模式: OpenGL支持两种颜色模式:一种是RGBA,一种是颜色索引模式. R ...

随机推荐

  1. SpringBoot接口 - 统一异常处理

    为什么要统一异常处理 如果不统一处理异常,程序开发时就需要在controller层写大量重复的Valid代码, 比如下面这个样子: @Slf4j @RestController public clas ...

  2. 在 MySQL 中建索引时需要注意哪些事项?

    在 MySQL 中建索引时需要注意哪些事项 索引在 MySQL 中是提升查询性能的关键,但不当的索引设计可能会导致性能下降或资源浪费.因此,在建索引时需要综合考虑性能.存储成本和业务需求. 1. 确定 ...

  3. 国内首个「混合推理模型」Qwen3深夜开源,盘点它的N种对接方式!

    今日凌晨,通义千问团队正式开源了 Qwen3 大模型,并且一口气发布了 8 个型号,其中包括 0.6B.1.7B.4B.8B.14B.32B 以及 30B-A3B 和 235B-A22B,使用者可以根 ...

  4. Java编程--单例(Singleton)设计模式

    单例设计模式 一个类只有一个实例,根据创建的时机又分为懒汉式和饿汉式,它们的区别主要体现在实例的创建时机和线程安全性上. 饿汉式(Eager Initialization): 特点: 在类加载时就创建 ...

  5. termux添加ll命令

    cd ~ vim .bashrc 添加如下内容 alias ll="ls -l" 保存退出 :wq source .bashrc 参考:https://www.cnblogs.co ...

  6. 关闭windows10 Alt+Tab开打edge选项卡

    发现最近更新的windows10会使用快捷键Alt+Tab打开Edge的选项卡,很不适应,可喜的是微软提供了关闭的方法. 设置⚙->系统->多任务处理->Alt+Tab 设置为仅打开 ...

  7. 为什么重写equals一定也要重写hashCode方法?

    简要回答 这个是针对set和map这类使用hash值的对象来说的 只重写equals方法,不重写hashCode方法: 有这样一个场景有两个Person对象,可是如果没有重写hashCode方法只重写 ...

  8. 递归神经网络 RNN 原理(上)

    前篇对于 RNN 前奏, 或者说是 NLP 的基础, 语言模型 (Language Model) 有了一点认识. LM 的应用场景为 在词库中, 搜索出 符合当前给定 句子的 下一个单词, 的所有可能 ...

  9. RPC实战与核心原理之时钟轮

    时钟轮在RPC中的应用 回顾 在分布式环境下,RPC 框架自身以及服务提供方的业务逻辑实现,都应该对异常进行合理地封装,让使用方可以根据异常快速地定位问题:而在依赖关系复杂且涉及多个部门合作的分布式系 ...

  10. AutoCAD中的Deep Clone

    AutoCAD中的Deep Clone 所谓Deep clone是指将实体从一个dwg文件拷贝至另一个dwg文件,类似于Ctr+C,CtrV,而普通的实体的Copy()方法,是在单个dwg文件中输入命 ...