Home Forums GAMES在线课程(现代计算机图形学入门)讨论区 作业3 关于深度值问题自己踩的坑和一些想法

Tagged: 

Viewing 7 reply threads
  • Author
    Posts
    • #7779 Score: 5
      SycnhronizX
      Participant
      5 pts

      新人为了准备202,所以抓紧时间开始肝101的作业来着。从昨天早上开始到现在一直在处理作业3中关于深度值的问题,到目前为止大概能够定位问题了,但是本着尽快完成的态度准备放下问题继续前进,因此在这留一点自己的想法也算是个checkpoint,供自己,也希望能为大家做个参考。由于数学水平确实捉急,说错的地方还请指教。

      太长不看的版本:
      框架缺陷1:送去光栅化的三角形w分量是写死为1的,因此在计算重心坐标时,虽然知道用非线性空间的重心坐标插值线性空间下的顶点属性不对,但从事实角度来说,作业框架将正确的计算过程退化回了错误(但据说可以近似)的方法。
      框架缺陷2:根据光栅器类的draw函数和rasterize_triangle函数,payload里面的接受的顶点位置是视空间(viewspace_position)下的数据。然而着色器中普遍有一个硬编码的eye_pos参数,也即是告诉我们当前用的是世界坐标系。而事实是着色器并没有接收到一个view矩阵的payload项,因此无法把光源转换到视空间,也无法把顶点位置转换回世界坐标系。
      遗留问题1:我在投影矩阵中保持了坐标系始终为右手系,但是通过打印深度信息,我发现深度值始终是一个正数。
      遗留问题2:和作业2相比,我在作业2中的深度测试对比的值是z_interpolate,画面正确。而在作业3中,我为了得到正确的牛,就必须在使用同样代码计算出z_interpolate的情况下,对比-z_interpolate,否则就是扭屁股对着我(前后镜像,明显是深度测试反了)。

      正文

      首先开宗明义,问题的核心是从投影空间逆推视空间的重心坐标插值。闫老师在课堂上很明确的提到了“投影空间中的对深度做插值无法得到符合投影性质的正确插值”,换言之就是线性空间(世界/观察空间)中的插值无法从非线性空间(投影空间)中按相同步骤求得。解决方法是“首先找到所求点在三维空间中的位置,然后在该点做插值,作为深度测试的深度插值”。闫老师这里说的时候诸如这个、那个之类的话佐料有点多,其实说的有点不太明确。反复回看之后我对闫老师这段话的理解是,通过某种方法找到像素对应点在线性空间中的坐标,然后通过重心坐标计算深度插值。可惜的是,课堂上并没有对于这方面的方法做一个介绍。在Google和版内搜索之后,我找到了这些结果:版内答案 CSDN博客 推导更具体的论文。概括而言,从computeBarycentric计算而来的alpha、beta和gamma并不能直接代入interpolate函数求插值,这也正是interpolate函数最后weight参数的意义。

      那么探究到的结果就到此为止了,接下来是如何在作业框架里实现。上述的介绍里面使用到了一个重要的性质就是:从视空间到投影空间,由于投影矩阵的第四行是0 0 -1(1) 0,因此可知w_clip = (-)z_view。因此,作业框架中,rasterize_triangle函数中的v[i].w()就应该代表了视空间下的深度值(可能带负号)。明白了这一点之后,按说就可以很自如得求得原空间下的各种插值了。原公式为:
      interpolated = (alpha * I_0 / z0_view + beta * I_1 / z1_view + gamma * I_2 / z2_view) / (alpha / z0_view + beta * z1_view + gamma * z2_view);
      I_i表示对应下标顶点的某种被插值计算的属性(在线性空间中的值,即深度的情况下是投影前的深度值)。当求深度时,根据alpha + beta + gamma = 1,公式退化为:
      interpolated = 1 / (alpha / z0_view + beta * z1_view + gamma * z2_view);
      一天以来困扰我的问题都是:为何我在投影时保持了右手系,可进入深度测试的值在我打印出来之后显示的都是一个正数,以及为什么作业2的时候我用z_interpolate进行深度测试就没问题,到了作业3,我就需要用-z_interpolate才能获得和图示一样的小牛,否则小牛就是屁股对着屏幕(前后镜像,很显然是深度测试结果反了)。
      结合作业2,我仔细查看了代码框架之后,发现一个有点无语的乌龙。rasterize_triangle中的z_view也就是v[i].w()的值实际上是在draw函数中对顶点缓冲区中的顶点进行组合并MVP变换后现场生成的,而在标注了Homogeneous Division注释的地方,对Vector4f中的四个值都进行了透视除法,也就是说w恒等于1,修正透视偏差的值被固定了。问题还不止这里,事实上,作业框架的Triangle类的顶点是一个Vector3f,也就是说是没有w项的。在第二次作业,以及第三次作业的更新公告里面也赫然写着auto v = t.toVector4(),这个函数表明,w的值还是1。也就是说不管那段Z=… zp=…怎么算,最终的结果还是相当于用投影空间中的alpha beta gamma直接插值了原空间的深度值。在上文提及的版内搜索到的结果里面有人提及,其实用投影空间重心坐标计算原空间的插值虽然错误,但是也不会有太大误差。对于这个说法由于我自己的数学水平实在跟不上,所以不知道到底怎么评价,但是单从作业框架这个架构来说,确实有点“我听了课,看了课外资料证明简单方法的错误性,最后你告诉我其实误差也不大,用就好了”的挫败感。

      不仅是这次了,跟着各种资料/教程已经踩下了各种坑,通过踩坑也强迫自己关注到了很多实现上的细节问题。不得不说图形学从理论到实现上跨越了许多细枝末节的问题,导致听课、记笔记、公式推导跑得一溜烟,开始敲代码就发现三步就卡住了,写好的东西也总能因为一点点小问题导致完全崩坏的结果。作业框架都是各位助教辛苦肝出来的成果,无意提什么负面意见,非常感谢大家的无私付出。摸排各种坑毕竟是个辛苦的事,写下本文就是希望对自己也对别人提供一点积极的帮助。如果能有热心的老哥来解答我的问题就更开心了。

      最后,虽然也有别人提到,但是真诚的希望论坛能加一个搜索功能,人工翻十几二十页的论坛找过往讨论真的太痛苦了。

      This post has received 5 votes up.
    • #7798 Score: 0
      star
      Participant

      当我仿照作业框架写软渲时,这些坑全都接触了一遍

    • #7936 Score: 0
      BernardLi
      Participant

      搜索功能这个太真实了, 论坛翻起来真的头疼

    • #7964 Score: 0
      云天毛
      Participant

      感谢!!我还在思考如何让我的牛转过头来

    • #8122 Score: 0
      Yijie Li
      Participant

      直接点进讨论区后可以搜索(在具体课程外)

    • #8430 Score: 0
      zombiecat
      Participant

      谢谢,帮助很大,搜索在Forum级别可以用

      • This reply was modified 3 years, 7 months ago by zombiecat.
    • #8872 Score: 1
      happyfire
      Participant
      4 pts

      很高兴你也注意到了w==1这个问题,其实作业2,作业3的框架都有问题,他们公共的问题是光栅化取三角形顶点坐标时使用了 auto v = t.toVector4(); 这样w肯定是1了。然后作业2错误更多些,在透视除法里面使用了vec /= vec.w();造成w为1。然后作业3修复了这一点,只对x,y,z除了w。但是没有用啊,因为toVector4。

      然后项目框架里面的w应该是什么含义呢?按照课程的约定,NDC使用了和ViewSpace一样的右手系,所以投影矩阵的最后一行是(0,0,1,0),这样w=Z_view。所以课程3框架的注释里面说:v[i].w() is the vertex view space depth value z. 并没有问题。而在网上找的关于OpenGL的文章里面,推导出来w=-Z_view,那是因为OpenGL的约定中,NDC为左手坐标系(near为-1,far为+1)。

      然后我们看下z插值的问题,作业2框架中这么写(作业3其实也一样):

      
      float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
      float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
      z_interpolated *= w_reciprocal;
      

      由于w为1,因此上面的w_reciprocal就是1.0/(alpha+beta+gamma),而alpha+beta+gamma==1,因此w_reciprocal为1。
      因为w为1,z_interpolated 其实就是直接使用重心坐标进行插值了(没有透视校正的效果),最后一步z_interpolated *= w_reciprocal也没有作用。最终z_interpolated 就是三个顶点的z值的插值。
      那么这个地方三个顶点的z值是什么呢?在draw中:

      
      float f1 = (50 - 0.1) / 2.0;
      float f2 = (50 + 0.1) / 2.0;
      vert.z() = vert.z() * f1 + f2;
      

      f1,f2是什么意思?因为框架中的near是0.1,far是50.(这儿near,far都是距离值,是正值。我只能这么理解吧),所以f1,f2的作用就是把clip space的z值变换到[near,far]的范围中。我LOG了一下,第一个三角形的clip space z为 -0.975379,变换后为0.714285;第二个三角形从-0.983968变换到0.50002。因为框架中一直使用的是右手坐标系,且view space的view方向为-z。因为第一个三角形的z值-0.97xx大于第二个三角形的-0.98xxx,离camera更近,所以第一个三角形应该挡住第二个三角形。但是真正做深度测试的时候,使用的是变换到[0.1,50]范围的z值。这样第一个三角形的0.7xxx由于大于第二个三角形的0.5xxx,因此如果直接使用z_interpolated 并且按照小值离camera近的逻辑去比较就会得到第二个三角形挡住第一个三角形的错误结果。为了得到正确的结果,简单的取个负修正一下确实可以,但这里面的逻辑确实搞错了。根源就在于作业2的pdf里面说的:为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。
      按照我上面的分析,反转后的正数z并不是越大离视点越远,而是正好相反。所以作业3牛画反的同学应该明白了吧,并不是你的问题。框架本身就有问题。。这个地方转成正数后反而不能用<去测试了,有点画蛇添足的感觉。

      This post has received 1 vote up.
      • #8873 Score: 2
        happyfire
        Participant
        4 pts

        需要说明的是,以上对于z值问题的分析是基于课程推导投影矩阵时的约定,即:
        view为右手系,camea看向-z;NDC为右手系;矩阵中的n和f是坐标值,即为负值。
        但是根据作业2框架给的函数参数,get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)。调用时传入的zNear和zFar分别为0.1和50, 而这个写法和OpenGL很接近,因此也可以推断作业框架使用了OpenGL的约定,即view为右手系,camea看向-z;NDC为左手系;矩阵中的n和f是距离值,即为正值。如果按照这个约定去推导投影矩阵并计算z值,得到的结果就和我之前分析的不一样了。此时就不会存在上面的问题。因此,可能框架也没问题吧,但是应该在pdf里面指出使用的约定,否则就容易引起误会,造成不必要的困扰。

        This post has received 2 votes up.
    • #11472 Score: 0
      輕山柒海
      Participant

      虽然已经时隔一年了,给后来者指个路,这里总结了相关的作业框架问题。
      《GAMES101》作业框架问题详解

Viewing 7 reply threads
  • You must be logged in to reply to this topic.