计算反射/折射的屏幕空间是否需要二阶偏导数?

问题描述

我编写了一个基本的光线追踪器来跟踪屏幕空间。每个片段都有一个关联的像素半径。当射线 dirdistance 被一些 eye 挤压时撞击几何体时,我计算命中的法向量 N,并将其与另外四条射线组合。在伪代码中:

def distance := shortestdistancetoSurface(sdf,eye,dir,pixelRadius)
def p := eye + dir * distance
def N := estimatenormal(sdf,p)
def glance := distance * glsl.dot(dir,N)
def dx := (dirPX / glsl.dot(dirPX,N) - dirNX / glsl.dot(dirNX,N)) * glance
def dy := (dirPY / glsl.dot(dirPY,N) - dirNY / glsl.dot(dirNY,N)) * glance

这里,dirPXdirNXdirPYdirNY 是光线,它们在每个屏幕空间中的像素半径偏移了 dir四个方向,但仍瞄准同一个参考点。这给出了 dxdy,它们是像素上的偏导数,表示当光线穿过屏幕空间时命中沿几何体表面移动的速率。

因为我跟踪屏幕空间,所以我可以使用 pre-filtered samplers,as discussed by Inigo Quilez。他们看起来很棒。但是,现在我想添加反射(和折射),这意味着我需要递归,我不确定如何计算这些光线和跟踪屏幕空间。

本质问题是,为了确定几何体上某个位置反射的光是什么颜色的,我不仅需要采集点样本,还需要检查被反射的整个屏幕空间。我可以使用偏导数为我提供几何上的四个新点,它们近似于一个椭圆,该椭圆是屏幕上原始像素的投影:

def px := dx * pixelRadius
def py := dy * pixelRadius
def ppx := p + px
def pNX := p - px
def pPY := p + py
def pNY := p - py 

我可以通过将椭圆变成圆形来计算近似的像素半径。我知道这会破坏某些理想的各向异性模糊,但是没有作弊的光线追踪器是什么?

def nexTradius := (glsl.length(dx) * glsl.length(dy)).squareRoot() * pixelRadius

然而,我不知道在哪里将这些点反映到几何体中;我不知道将它们的光线聚焦在哪里。如果我必须选择焦点,那么它将是任意的,并且取决于几何体反射其自身图像的位置,然后这可能会任意模糊或出现反射图像的波纹。

我需要取二阶偏导数吗?我可以像一阶导数一样近似它们,然后我可以用它们来调整正常的N,稍有变化,只需就像命中 p。然后法线引导椭圆的焦点,并将其映射到近似的圆锥截面。我担心三件事:

  1. 我担心做一些额外的向量加法和乘法的成本,这可能可以忽略不计;
  2. 还有关于精度的损失,在做这些廉价的衍生产品时已经很糟糕,在多次反射时是否会损失太多;
  3. 最后,我应该如何处理屏幕空间爆炸的情况;当我有一个镜像球体时,我应该如何在反射空间的大楔形上进行采样,例如将棋盘格图案平均为灰色?

虽然这并不令人担心,但我只是不知道如何取四个向量并快速为它们拟合一个令人信服的圆锥体,但这可能只是花一些时间在白板上做代数的问题。

编辑:在 John Amanatides 1984 年的论文 Ray Tracing with Cones 中,确实计算了曲率信息,并用于将估计的锥体拟合到反射光线上。在 Homan Igehy 1999 年的论文 Tracing Ray Differentials 中,只使用了一阶导数,而明确忽略了二阶导数。

还有其他选择吗?我已经尝试在一次反射后丢弃像素半径并仅获取点样本,它们看起来很糟糕,有很多混叠和噪音。也许存在可以基于每种材料计算的视场或景深近似值。像往常一样,多重采样会有所帮助,但我想要一个分析解决方案,这样我就不会不必要地浪费这么多 cpu

sdf一个 signed distance function,我正在做球体追踪;同样的例程计算距离和法线。glsl 是 GLSL 标准库。)

解决方法

我不会接受我自己的答案,但我会解释我做了什么,以便我现在可以放下这个问题。我最终采用了类似于 Amanatides 的方法,计算每条射线周围的锥体。

每次计算法向量时,我也会计算平均曲率。法向量是使用众所周知的技巧计算的。在我的问题中,让 p 成为 SDF 的零,让 epsilon 成为用于 numerical differentiation 的合理偏移量,然后让 vpvn是向量,其分量是 p 附近的 SDF 的评估,但在每个分量处受到 epsilon 的扰动。在伪代码中:

def justX := V(1.0,0.0,0.0)
def justY := V(0.0,1.0,0.0)
def justZ := V(0.0,1.0)
def vp := V(sdf(p + justX * epsilon),sdf(p + justY * epsilon),sdf(p + justZ * epsilon))
def vn := V(sdf(p - justX * epsilon),sdf(p - justY * epsilon),sdf(p - justZ * epsilon))

现在,通过巧妙地滥用 finite difference coefficients,我们可以同时计算一阶和二阶导数。根据假设,第三个系数 sdf(p) 已经为零。这给出了我们的法线向量 N,即梯度,以及我们的平均曲率 H,即 Laplacian

def N := glsl.normalize(vp - vn)
def H := sum(vp + vn) / (epsilon * epsilon)

我们可以从平均曲率估计高斯曲率。虽然平均曲率告诉我们的锥体膨胀或收缩多少,但高斯曲率始终为非负值,并衡量向锥体的相交区域添加了多少额外区域 (spherical excess)。用K代替H给出高斯曲率,代入后:

def K := H * H

现在我们准备调整片段宽度的计算。让我们假设,除了屏幕空间中的 pixelRadius 和从相机到几何体的 distance 之外,我们还有 dradius,即像素半径随距离的变化率。我们可以像以前一样用点积计算一个扫视因子,三角函数类似:

def glance := glsl.dot(dir,N).abs().reciprocal()
def fradius := (pixelRadius + dradius * distance) * glance * (1.0 + K)
def fwidth := fradius * 2.0

现在我们有 fwidth 就像在 GLSL 中一样。最后,当需要进行反射时,我们需要通过将二阶导数的曲率积分到一阶导数中来调整半径的变化:

def dradiusNew := dradius + H * fradius

结果令人满意。这些片段可能有点太大了;很难判断某些东西是否过于模糊或只是适当地抗锯齿。我想知道 Amanatides 是否使用了一组不同的方程来处理曲率?我在白板上花了太长时间,最终得出了令人愉快的简单操作。

A mirrored sphere hovering above and reflecting a checkerboard pattern which covers the ground

这张图片没有经过超采样;每个像素有一个片段,一个射线和一个锥体。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...