问题
我最近发现了一个很棒的小型纯Python光线跟踪脚本from this link,并为了更方便的功能而扩展了一点点。然而,有时它会扭曲物体的形状,我想知道有光线追踪/ 3D经验的人是否可能对可能导致它的原因有任何线索?
一些信息
我测试的场景包括一个地面平面,顶部有三个彩色球体。当相机从上方/角度和一定距离俯视场景时,它会产生美观的场景(参见前两张照片);然而,当相机靠近地面并靠近物体时,球体最终会改变它们的形状并变成椭圆形,好像它们被拉向天空一样(见第三张图片)。请注意,第三张照片中带有扭曲球体的相机有点颠倒,这是因为我还在弄清楚如何控制相机并且不确定如何旋转它#34;当发生这种情况时直立;它似乎自动地朝向球体/光源所在的一般区域看,并且只有当我改变一些参数时它才会朝不同的方向看。
我仍在尝试破译并了解我发现的原始代码中的内容以及我的代码所基于的内容,因此我不知道但是它可能是关于光线跟踪的方法或方法由原作者拍摄。我已经附加了我的模块脚本的整个代码,当你按F5时,如果有人接受挑战,它应该运行。图像渲染需要PIL,如果你想玩相机的位置,只需看看Camera类,并在" normaltest"中更改它的选项。功能。
更新
有人指出,在运行脚本时,它不会在第三张图像中重现问题。我现在已经更改了正常测试功能的摄像头位置,以便重现问题(请参阅新的第四张图像,看看它应该是什么样子)。如果您想知道为什么光线似乎是从球体中射出的,那么我将光源放置在所有光源之间的某处。
我开始认为问题在于相机而我完全不了解它。
还要尝试将相机从当前的z坐标2增加到5的az,这是一个非常小的变化,但它会使失真看起来更好(尽管仍然很差),因此接近地面或角度的变化它附带似乎发挥了一些作用。
"""
Pure Python ray-tracer :)
taken directly from http://pastebin.com/f8f5ghjz with modifications
another good one alternative at http://www.hxa.name/minilight/
some more equations for getting intersection with other 3d geometries, https://www.cl.cam.ac.uk/teaching/1999/AGraphHCI/SMAG/node2.html#SECTION00023200000000000000
"""
#IMPORTS
from math import sqrt, pow, pi
import time
import PIL,PIL.Image
#GEOMETRIES
class Vector( object ):
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
def dot(self, b):
return self.x*b.x + self.y*b.y + self.z*b.z
def cross(self, b):
return (self.y*b.z-self.z*b.y, self.z*b.x-self.x*b.z, self.x*b.y-self.y*b.x)
def magnitude(self):
return sqrt(self.x**2+self.y**2+self.z**2)
def normal(self):
mag = self.magnitude()
return Vector(self.x/mag,self.y/mag,self.z/mag)
def __add__(self, b):
return Vector(self.x + b.x, self.y+b.y, self.z+b.z)
def __sub__(self, b):
return Vector(self.x-b.x, self.y-b.y, self.z-b.z)
def __mul__(self, b):
assert type(b) == float or type(b) == int
return Vector(self.x*b, self.y*b, self.z*b)
class Sphere( object ):
def __init__(self, center, radius, color):
self.c = center
self.r = radius
self.col = color
def intersection(self, l):
q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
if q < 0:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
else:
d = -l.d.dot(l.o - self.c)
d1 = d - sqrt(q)
d2 = d + sqrt(q)
if 0 < d1 and ( d1 < d2 or d2 < 0):
return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
elif 0 < d2 and ( d2 < d1 or d1 < 0):
return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
else:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
def normal(self, b):
return (b - self.c).normal()
class Cylinder( object ):
"just a copy of sphere, needs work. maybe see http://stackoverflow.com/questions/4078401/trying-to-optimize-line-vs-cylinder-intersection"
def __init__(self, startpoint, endpoint, radius, color):
self.s = startpoint
self.e = endpoint
self.r = radius
self.col = color
def intersection(self, l):
q = l.d.dot(l.o - self.c)**2 - (l.o - self.c).dot(l.o - self.c) + self.r**2
if q < 0:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
else:
d = -l.d.dot(l.o - self.c)
d1 = d - sqrt(q)
d2 = d + sqrt(q)
if 0 < d1 and ( d1 < d2 or d2 < 0):
return Intersection(l.o+l.d*d1, d1, self.normal(l.o+l.d*d1), self)
elif 0 < d2 and ( d2 < d1 or d1 < 0):
return Intersection(l.o+l.d*d2, d2, self.normal(l.o+l.d*d2), self)
else:
return Intersection( Vector(0,0,0), -1, Vector(0,0,0), self)
def normal(self, b):
return (b - self.c).normal()
class LightBulb( Sphere ):
pass
class Plane( object ):
"infinite, no endings"
def __init__(self, point, normal, color):
self.n = normal
self.p = point
self.col = color
def intersection(self, l):
d = l.d.dot(self.n)
if d == 0:
return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
else:
d = (self.p - l.o).dot(self.n) / d
return Intersection(l.o+l.d*d, d, self.n, self)
class Rectangle( object ):
"not done. like a plane, but is limited to the shape of a defined rectangle"
def __init__(self, point, normal, color):
self.n = normal
self.p = point
self.col = color
def intersection(self, ray):
desti = ray.dest.dot(self.n)
if desti == 0:
#??
return Intersection( vector(0,0,0), -1, vector(0,0,0), self)
else:
desti = (self.p - ray.orig).dot(self.n) / desti
return Intersection(ray.orig+ray.desti*desti, desti, self.n, self)
class RectangleBox( object ):
"not done. consists of multiple rectangle objects as its sides"
pass
class AnimatedObject( object ):
def __init__(self, *objs):
self.objs = objs
def __iter__(self):
for obj in self.objs:
yield obj
def __getitem__(self, index):
return self.objs[index]
def reverse(self):
self.objs = [each for each in reversed(self.objs)]
return self
#RAY TRACING INTERNAL COMPONENTS
class Ray( object ):
def __init__(self, origin, direction):
self.o = origin
self.d = direction
class Intersection( object ):
"keeps a record of a known intersection bw ray and obj?"
def __init__(self, point, distance, normal, obj):
self.p = point
self.d = distance
self.n = normal
self.obj = obj
def testRay(ray, objects, ignore=None):
intersect = Intersection( Vector(0,0,0), -1, Vector(0,0,0), None)
for obj in objects:
if obj is not ignore:
currentIntersect = obj.intersection(ray)
if currentIntersect.d > 0 and intersect.d < 0:
intersect = currentIntersect
elif 0 < currentIntersect.d < intersect.d:
intersect = currentIntersect
return intersect
def trace(ray, objects, light, maxRecur):
if maxRecur < 0:
return (0,0,0)
intersect = testRay(ray, objects)
if intersect.d == -1:
col = vector(AMBIENT,AMBIENT,AMBIENT)
elif intersect.n.dot(light - intersect.p) < 0:
col = intersect.obj.col * AMBIENT
else:
lightRay = Ray(intersect.p, (light-intersect.p).normal())
if testRay(lightRay, objects, intersect.obj).d == -1:
lightIntensity = 1000.0/(4*pi*(light-intersect.p).magnitude()**2)
col = intersect.obj.col * max(intersect.n.normal().dot((light - intersect.p).normal()*lightIntensity), AMBIENT)
else:
col = intersect.obj.col * AMBIENT
return col
def gammaCorrection(color,factor):
return (int(pow(color.x/255.0,factor)*255),
int(pow(color.y/255.0,factor)*255),
int(pow(color.z/255.0,factor)*255))
#USER FUNCTIONS
class Camera:
def __init__(self, cameraPos, zoom=50.0, xangle=-5, yangle=-5):
self.pos = cameraPos
self.zoom = zoom
self.xangle = xangle
self.yangle = yangle
def renderScene(camera, lightSource, objs, imagedims, savepath):
imgwidth,imgheight = imagedims
img = PIL.Image.new("RGB",imagedims)
#objs.append( LightBulb(lightSource, 0.2, Vector(*white)) )
print "rendering 3D scene"
t=time.clock()
for x in xrange(imgwidth):
#print x
for y in xrange(imgheight):
ray = Ray( camera.pos, (Vector(x/camera.zoom+camera.xangle,y/camera.zoom+camera.yangle,0)-camera.pos).normal())
col = trace(ray, objs, lightSource, 10)
img.putpixel((x,imgheight-1-y),gammaCorrection(col,GAMMA_CORRECTION))
print "time taken", time.clock()-t
img.save(savepath)
def renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat):
"NOTE: savepath should not have file extension, but saveformat should have a dot"
time = 0
while True:
print "time",time
timesavepath = savepath+"_"+str(time)+saveformat
objs = []
objs.extend(staticobjs)
objs.extend([animobj[time] for animobj in animobjs])
renderScene(camera, lightSource, objs, imagedims, timesavepath)
time += 1
#SOME LIGHTNING OPTIONS
AMBIENT = 0.05 #daylight/nighttime
GAMMA_CORRECTION = 1/2.2 #lightsource strength?
#COLORS
red = (255,0,0)
yellow = (255,255,0)
green = (0,255,0)
blue = (0,0,255)
grey = (120,120,120)
white = (255,255,255)
purple = (200,0,200)
def origtest():
print ""
print "origtest"
#BUILD THE SCENE
imagedims = (500,500)
savepath = "3dscene_orig.png"
objs = []
objs.append(Sphere( Vector(-2,0,-10), 2, Vector(*green)))
objs.append(Sphere( Vector(2,0,-10), 3.5, Vector(*red)))
objs.append(Sphere( Vector(0,-4,-10), 3, Vector(*blue)))
objs.append(Plane( Vector(0,0,-12), Vector(0,0,1), Vector(*grey)))
lightSource = Vector(0,10,0)
camera = Camera(Vector(0,0,20))
#RENDER
renderScene(camera, lightSource, objs, imagedims, savepath)
def normaltest():
print ""
print "normaltest"
#BUILD THE SCENE
"""
the camera is looking down on the surface with the spheres from above
the surface is like looking down on the xy axis of the xyz coordinate system
the light is down there together with the spheres, except from one of the sides
"""
imagedims = (200,200)
savepath = "3dscene.png"
objs = []
objs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
objs.append(Sphere( Vector(-2, -2, 1), 1, Vector(*blue)))
objs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
objs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*grey)))
lightSource = Vector(-2.4, -3, 2)
camera = Camera(Vector(-19,-19,2), zoom=2.0, xangle=-30, yangle=-30)
#RENDER
renderScene(camera, lightSource, objs, imagedims, savepath)
def animtest():
print ""
print "falling ball test"
#BUILD THE SCENE
imagedims = (200,200)
savepath = "3d_fallball"
saveformat = ".png"
staticobjs = []
staticobjs.append(Sphere( Vector(-4, -2, 1), 1, Vector(*red)))
staticobjs.append(Sphere( Vector(-2, -4, 1), 1, Vector(*green)))
staticobjs.append(Plane( Vector(0,0,0), Vector(0,0,1), Vector(*purple)))
animobjs = []
fallingball = AnimatedObject(Sphere( Vector(-2, -2, 20), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 15), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 9), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 5), 1, Vector(*yellow)),
Sphere( Vector(-2, -2, 1), 1, Vector(*yellow)))
animobjs.append(fallingball)
lightSource = Vector(-4,-4,10)
camera = Camera(Vector(0,0,30))
#RENDER
renderAnimation(camera, lightSource, staticobjs, animobjs, imagedims, savepath, saveformat)
#RUN TESTS
#origtest()
normaltest()
#animtest()
答案 0 :(得分:0)
问题的关键可能就在这一行:
ray = Ray( camera.pos,
(Vector(
x/camera.zoom+camera.xangle,
y/camera.zoom+camera.yangle,
0)
-camera.pos)
.normal())
光线被定义为从XY平面上的相机位置(但是定义的那个点)开始的线条,即通过xangle和yangle参数进行缩放和移位。
这不是通常实施透视投影的方式。这更像是倾斜/移位相机。典型的透视变换会使您投射到平面上的平面保持与从相机穿过图片中心的光线。
使用这段代码你有两个选择:重写这个,或者总是使用xangle,yangle,camera.pos.x和camera.pos.y == 0.否则你会得到不可思议的结果。
说实话,这是完全合法的观点。这不是你用普通相机所见到的。