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

Kar*_*gat 5 python 3d graphics raytracing

问题

我最近在这个链接中找到了一个很棒的纯Python-ray光线跟踪脚本,并为了更方便的功能而扩展了一点.然而,有时它会扭曲物体的形状,我想知道有光线追踪/ 3D经验的人是否可能有任何线索可能导致它?

一些信息

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

在此输入图像描述

在此输入图像描述

在此输入图像描述

在此输入图像描述

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

更新

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

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

  • 相机选项变焦,偏光和偏光可能不符合他们的名字所暗示的; 这就是我根据他们改变它们时所做的事情来命名它们的方式.最初它们不是变量,而是在计算中必须手动更改的常量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()
Run Code Online (Sandbox Code Playgroud)

Rom*_*nka 0

您问题的关键可能是这一行:

ray = Ray( camera.pos, 
     (Vector(
        x/camera.zoom+camera.xangle,
        y/camera.zoom+camera.yangle,
        0)
     -camera.pos)
     .normal())
Run Code Online (Sandbox Code Playgroud)

射线被定义为从摄像机位置(但是该点已定义)穿过 XY 平面的一条线,该线由 xangle 和 yangle 参数进行缩放和移动。

这不是透视投影通常的实现方式。这更像是倾斜/移位相机。典型的透视变换将使您投影到的平面与从相机穿过图片中心的光线垂直。

使用此代码,您有两个选择:要么重写此代码,要么始终使用 xangle、yangle、camera.pos.x 和camera.pos.y == 0。否则您会得到不稳定的结果。

正确地说,这是完全合理的观点。这不是您使用典型相机所看到的。