Why isn't my pathtracing code working?
I've been hacking together a pathtracer in pure Python, just for fun, and since my previous shading-thing wasn't too pretty (Lambert's cosine law), I'm trying to implement recursive pathtracing.
My engine gives an abortive output:
My pathtracing function is defined recursively, like this:
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
And my scene (I made a custom XML description format) is this:
<?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>
I'm pretty sure that there's some fundamental flaw in my engine, but I just can't find it...
Here's my new-ish tracing function:
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.
I'm pretty sure that the pathtracing code works, as I manually casted some rays and got pretty legitimate results. The problem I'm having (now) is that the camera doesn't shoot rays through all the pixels in the image plane. I made this code to find the ray intersecting a pixel, but it's not working properly:
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 camer开发者_如何学Pythona'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())
My first question is should if test > result:
be if test < result:
? You're looking for the closest hit, not the furthest.
Second, why do you add direction*0.00001
to the hit point here n = Ray(ray.position(result) + direction * 0.00001, direction)
? That would put the start of your new ray inside the spheres. I believe then when you recursively call TracePath2
, the dot product that you multiply by would be negative, which would help explain the problem.
Edit: updated problem
This line confuses me: answer += TracePath2(n, scene, bounce_count + 1) * hit.diffuse * dp
. First of all answer
will just be Color(0.0, 0.0, 0.0)
so you could simply return racePath2(n, scene, bounce_count + 1) * hit.diffuse * dp
. But that still bothers me because I don't understand why you're multiplying the recursive call and hit.diffuse
. Something like this makes more sense to me return racePath2(n, scene, bounce_count + 1) * dp + hit.diffuse
. One more thing, you never check bounce_count
. You're never going to recurse forever in this scene anyway but if you want to render larger scenes you'll want something like this at the beginning if bounce_count > 15: return black
.
Edit 2:
The one thing I see that I still wonder about is the if-else towards the end. First of all I'm not entirely sure what that part of the code is doing. I think you're testing whether or not the ray is on the inside of the object. In that case your test would be like this inside = normal.dot(ray.direction) > 0.0
. I'm just testing against normal
instead of direction
because using the random direction in the hemisphere could give the wrong answer. Now, if you're inside the object you want to get out, but you want to stay out if you're already out? Like I said, I'm not quite sure what that part is supposed to do.
精彩评论