• opengl 学习 之 16 lesson


    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

    ......

    Hope is a good thing,maybe the best of things,and no good thing ever dies.----------- Andy Dufresne
  • 相关阅读:
    蓝桥杯基础练习 高精度加法
    int和string的相互装换 (c++)
    蓝桥杯基础练习 十六进制转八进制
    markdown笔记
    hdu1384Intervals(差分约束)
    【Android】Android Studio 设置
    【调试】pthread.c:(.text+0xa2):对‘pthread_create’未定义的引用
    【STM32】开拓者MiniSTM32RBT6开发板原理图
    [小技巧]解决Word转PDF时,图片变模糊的问题
    Java十六进制字符串转换说明
  • 原文地址:https://www.cnblogs.com/eat-too-much/p/14092711.html
Copyright © 2020-2023  润新知