Python光线跟踪会在靠近相机时扭曲对象形状

时间:2014-04-02 19:18:39

标签: python graphics 3d raytracing

问题

我最近发现了一个很棒的小型纯Python光线跟踪脚本from this link,并为了更方便的功能而扩展了一点点。然而,有时它会扭曲物体的形状,我想知道有光线追踪/ 3D经验的人是否可能对可能导致它的原因有任何线索?

一些信息

我测试的场景包括一个地面平面,顶部有三个彩色球体。当相机从上方/角度和一定距离俯视场景时,它会产生美观的场景(参见前两张照片);然而,当相机靠近地面并靠近物体时,球体最终会改变它们的形状并变成椭圆形,好像它们被拉向天空一样(见第三张图片)。请注意,第三张照片中带有扭曲球体的相机有点颠倒,这是因为我还在弄清楚如何控制相机并且不确定如何旋转它#34;当发生这种情况时直立;它似乎自动地朝向球体/光源所在的一般区域看,并且只有当我改变一些参数时它才会朝不同的方向看。

enter image description here

enter image description here

enter image description here

enter image description here

我仍在尝试破译并了解我发现的原始代码中的内容以及我的代码所基于的内容,因此我不知道但是它可能是关于光线跟踪的方法或方法由原作者拍摄。我已经附加了我的模块脚本的整个代码,当你按F5时,如果有人接受挑战,它应该运行。图像渲染需要PIL,如果你想玩相机的位置,只需看看Camera类,并在" normaltest"中更改它的选项。功能。

更新

有人指出,在运行脚本时,它不会在第三张图像中重现问题。我现在已经更改了正常测试功能的摄像头位置,以便重现问题(请参阅新的第四张图像,看看它应该是什么样子)。如果您想知道为什么光线似乎是从球体中射出的,那么我将光源放置在所有光源之间的某处。

我开始认为问题在于相机而我完全不了解它。

  • 相机选项缩放,Xangle和yangle可能不会像他们的名字所暗示的那样;这就是我根据他们改变它们时所做的事情来命名它们的方式。最初它们不是变量,而是在计算中必须手动更改的常量nr。具体来说,它们用于通过renderScene函数中第218行的场景定义和生成光线。
  • 例如,有时当我更改变焦值时,它也会改变相机的方向和位置。
  • 有点奇怪的是,在原始代码中,相机被定义为没有方向的点(xangle和yangle变量最初只是静态nrs,没有定义它们的选项),并且几乎总是开始自动向对象看。
  • 我无法找到一种方法来旋转" /倾斜相机本身。

还要尝试将相机从当前的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()

1 个答案:

答案 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.否则你会得到不可思议的结果。

说实话,这是完全合法的观点。这不是你用普通相机所见到的。