为什么我的路径追踪代码不工作?

1 投票
1 回答
505 浏览
提问于 2025-04-16 15:01

我最近在用纯Python编写一个路径追踪器,纯粹是为了好玩。之前的阴影效果做得不太好(参考了朗伯余弦定律),所以我想尝试实现递归路径追踪。

我的引擎输出的结果很糟糕:

这里插入图片描述

我的路径追踪函数是递归定义的,像这样:

def TracePath2(ray, scene, bounce_count):
  result = 100000.0
  hit = False

  answer = Color(0.0, 0.0, 0.0)

  for object in scene.objects:
    test = object.intersection(ray)

    if test and test < result:
      result = test
      hit = object

  if not hit:
    return answer

  if hit.emittance:
    return hit.diffuse * hit.emittance

  if hit.diffuse:
    direction = RandomDirectionInHemisphere(hit.normal(ray.position(result)))

    n = Ray(ray.position(result), direction)
    dp = direction.dot(hit.normal(ray.position(result)))
    answer += TracePath2(n, scene, bounce_count + 1) * hit.diffuse * dp

  return answer

我的场景(我自己做了一个自定义的XML描述格式)是这样的:

<?xml version="1.0" ?>

<scene>
  <camera name="camera">
    <position x="0" y="-5" z="0" />
    <direction x="0" y="1" z="0" />

    <focalplane width="0.5" height="0.5" offset="1.0" pixeldensity="1600" />
  </camera>

  <objects>
    <sphere name="sphere1" radius="1.0">
      <material emittance="0.9" reflectance="0">
        <diffuse r="0.5" g="0.5" b="0.5" />
      </material>

      <position x="1" y="0" z="0" />
    </sphere>

    <sphere name="sphere2" radius="1.0">
      <material emittance="0.0" reflectance="0">
        <diffuse r="0.8" g="0.5" b="0.5" />
      </material>

      <position x="-1" y="0" z="0" />
    </sphere>
  </objects>
</scene>

我很确定我的引擎里有一些根本性的错误,但我就是找不到...


这是我新写的追踪函数:

def Trace(ray, scene, n):
  if n > 10: # Max raydepth of 10. In my scene, the max should be around 4, since there are only a few objects to bounce off, but I agree, there should be a cap.
    return Color(0.0, 0.0, 0.0)

  result = 1000000.0 # It's close to infinity...
  hit = False

  for object in scene.objects:
    test = object.intersection(ray)

    if test and test < result:
      result = test
      hit = object

  if not hit:
    return Color(0.0, 0.0, 0.0)

  point = ray.position(result)

  normal = hit.normal(point)
  direction = RandomNormalInHemisphere(normal) # I won't post that code, but rest assured, it *does* work.

  if direction.dot(ray.direction) > 0.0:
    point = ray.origin + ray.direction * (result + 0.0000001) # We're going inside an object (for use when tracing glass), so move a tad bit inside to prevent floating-point errors.
  else:
    point = ray.origin + ray.direction * (result - 0.0000001) # We're bouncing off. Move away from surface a little bit for same reason.

  newray = Ray(point, direction)

  return Trace(newray, scene, n + 1) * hit.diffuse + Color(hit.emittance, hit.emittance, hit.emittance) # Haven't implemented colored lights, so it's a shade of gray for now.

我很确定路径追踪的代码是有效的,因为我手动发射了一些光线,得到了相当不错的结果。现在我遇到的问题是,相机没有通过图像平面上的所有像素发射光线。我写了这段代码来找到与像素相交的光线,但它没有正常工作:

origin = scene.camera.pos                 # + 0.5 because it      # 
                                          # puts the ray in the   # This calculates the width of one "unit"
                                          # *middle* of the pixel # 
worldX = scene.camera.focalplane.width - (x + 0.5)                * (2 * scene.camera.focalplane.width / scene.camera.focalplane.canvasWidth)
worldY = scene.camera.pos.y - scene.camera.focalplane.offset # Offset of the imaging plane is know, and it's normal to the camera's direction (directly along the Y-axis in this case).
worldZ = scene.camera.focalplane.height - (y + 0.5)               * (2 * scene.camera.focalplane.height / scene.camera.focalplane.canvasHeight)

ray = Ray(origin, (scene.camera.pos + Point(worldX, worldY, worldZ)).norm())

1 个回答

4

我第一个问题是,if test > result: 这个判断是不是应该改成 if test < result:? 因为你是想找最接近的结果,而不是最远的。

第二个问题是,为什么在这里要加上 direction*0.00001 到命中的点 n = Ray(ray.position(result) + direction * 0.00001, direction)? 这样做会把你新光线的起点放在球体里面。我觉得当你递归调用 TracePath2 时,你乘的点积会是负数,这可能就是问题的原因。

编辑:更新了问题

这行代码让我困惑:answer += TracePath2(n, scene, bounce_count + 1) * hit.diffuse * dp。首先,answer 最开始就是 Color(0.0, 0.0, 0.0),所以你可以直接 return racePath2(n, scene, bounce_count + 1) * hit.diffuse * dp。但这让我还是很困惑,因为我不明白你为什么要把递归调用和 hit.diffuse 相乘。对我来说,像这样更有道理 return racePath2(n, scene, bounce_count + 1) * dp + hit.diffuse。还有一点,你从来没有检查过 bounce_count。虽然在这个场景中你不会无限递归,但如果你想渲染更大的场景,开头加上 if bounce_count > 15: return black 会比较好。

编辑 2:

我还有一个问题是关于最后的 if-else 部分。首先,我不太确定那部分代码在干什么。我觉得你是在测试光线是否在物体内部。如果是这样,你的测试应该是 inside = normal.dot(ray.direction) > 0.0。我用 normal 来测试,而不是 direction,因为在半球内使用随机方向可能会得到错误的结果。现在,如果你在物体内部,你想要出去,但如果你已经在外面了,就想要保持在外面?就像我说的,我不太确定那部分代码到底是想实现什么。

撰写回答