You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

光线追踪器相机Look-At异常:旋转范围受限?

光线追踪器相机Look-At功能异常问题

开发光线追踪器时遇到相机Look-At功能异常:当将look_at设置为vec3(100., 0.25, -3.)时,其表现与vec3(1000., 0.25, -3.)几乎一致,无法实现围绕场景的360度平滑旋转。

问题代码

from PIL import Image, ImageFilter
import numpy as np

class Scene():
    def __init__(self, ambient_color = vec3(0.01, 0.01, 0.01), n = vec3(1.0,1.0,1.0)) :
        self.scene_primitives = []
        self.collider_list = []
        self.shadowed_collider_list = []
        self.Light_list = []
        self.importance_sampled_list = []
        self.ambient_color = ambient_color
        self.n = n
        self.importance_sampled_list = []
        self.bvh_root = None
    def add(self, collider):
        self.collider_list.append(collider)

    def build_bvh(self):
        # Build BVH for the scene
        self.bvh_root = build_bvh(self.collider_list)

    def get_raycolor(self, ray):
        distances, hit_orientation = zip(*[s.intersect(ray.origin, ray.dir) for s in self.collider_list])

        nearest = np.minimum.reduce(distances)
        color = vec3(0., 0., 0.)

        for (coll, dis, orient) in zip(self.collider_list, distances, hit_orientation):
            hit_check = (nearest != FARAWAY) & (dis == nearest)
            if np.any(hit_check):
                color += coll.assigned_primitive.material.get_color(
                    self, ray.extract(hit_check),
                    Hit(extract(hit_check, dis), extract(hit_check, orient), coll.assigned_primitive.material, coll,
                        coll.assigned_primitive)).place(hit_check)

        return color
    def add_Camera(self, look_from, look_at, **kwargs):
        self.camera = Camera(look_from, look_at, **kwargs)
    def add_PointLight(self, pos, color):
        self.Light_list += [PointLight(pos, color)]
    def add_DirectionalLight(self, Ldir, color):
        self.Light_list += [DirectionalLight(Ldir.normalize() , color)]
    def add(self,primitive, importance_sampled = False):
        self.scene_primitives += [primitive]
        self.collider_list += primitive.collider_list
        if importance_sampled == True:
            self.importance_sampled_list += [primitive]
        if primitive.shadow == True:
            self.shadowed_collider_list += primitive.collider_list
    def add_Background(self, img, light_intensity = 0.0, blur =0.0 , spherical = False):
        primitive = None
        if spherical == False:
            primitive = SkyBox(img, light_intensity = light_intensity, blur = blur)
        else:
            primitive = Panorama(img, light_intensity = light_intensity, blur = blur)
        self.scene_primitives += [primitive]
        self.collider_list += primitive.collider_list
    def render(self, samples_per_pixel, progress_bar = False):
        if self.bvh_root is None:
            self.build_bvh()
        color_RGBlinear = vec3(0.,0.,0.)
        if progress_bar:
            for i in range(samples_per_pixel):
                color_RGBlinear += get_raycolor(self.camera.get_ray(self.n), scene = self)
                print(i)
        else:
            for i in range(samples_per_pixel):
                color_RGBlinear += get_raycolor(self.camera.get_ray(self.n), scene = self)
        color_RGBlinear = color_RGBlinear/samples_per_pixel
        rgb_linear = color_RGBlinear.to_array()
        rgb = np.where( rgb_linear <= 0.00304,  12.92 * rgb_linear,  1.055 * np.power(rgb_linear, 1.0/2.4) - 0.055)
        rgb_max =  np.amax(rgb, axis=0)  + 0.00001
        intensity_cutoff = 1.0
        color = np.where(rgb_max > intensity_cutoff, rgb * intensity_cutoff / (rgb_max), rgb)
        img_RGB = []
        for c in color:
            img_RGB += [Image.fromarray((255 * np.clip(c, 0, 1).reshape((self.camera.screen_height, self.camera.screen_width))).astype(np.uint8), "L") ]
        return Image.merge("RGB", img_RGB)
class Camera():
    def __init__(self, look_from, look_at, screen_width = 400 ,screen_height = 300,  field_of_view = 90., aperture = 0., focal_distance = 1.):
        self.screen_width = screen_width
        self.screen_height = screen_height
        self.aspect_ratio = float(screen_width) / screen_height
        self.look_from = look_from
        self.look_at = look_at
        self.camera_width = np.tan(field_of_view * np.pi / 180 / 2.) * 2.
        self.camera_height = self.camera_width / self.aspect_ratio
        self.cameraFwd = (look_at - look_from).normalize()
        self.cameraRight = (self.cameraFwd.cross(vec3(0., 1., 0.))).normalize()
        self.cameraUp = self.cameraRight.cross(self.cameraFwd)
        self.lens_radius = aperture / 2.
        self.focal_distance = focal_distance
        self.near = .1
        self.far = 100.
        self.x = np.linspace(-self.camera_width / 2., self.camera_width / 2., self.screen_width)
        self.y = np.linspace(self.camera_height / 2., -self.camera_height / 2., self.screen_height)
        xx, yy = np.meshgrid(self.x, self.y)
        self.x = xx.flatten()
        self.y = yy.flatten()
    def get_ray(self,n):
        x = self.x + (np.random.rand(len(self.x)) - 0.5) * self.camera_width / (self.screen_width)
        y = self.y + (np.random.rand(len(self.y)) - 0.5) * self.camera_height / (self.screen_height)
        ray_origin = self.look_from + self.cameraRight * x * self.near + self.cameraUp * y * self.near
        return Ray(origin=ray_origin, dir=(self.look_from + self.cameraUp * y * self.focal_distance +self.cameraRight * x * self.focal_distance +self.cameraFwd * self.focal_distance - ray_origin).normalize(), depth=0, n=n, reflections=0, transmissions=0, diffuse_reflections=0)

使用示例

camera_position = vec3(2.5 * np.sin(angle), 0.25, 2.5 * np.cos(angle) - 1.5)
look_at = vec3(0., 0.25, -3.) # 此处设为10时表现正常,但设为100、1000等大值时表现一致,不符合预期
Sc.add_Camera(look_from=camera_position, look_at=look_at, screen_width=width, screen_height=height)
Sc.build_bvh()
Sc.render(samples_per_pixel=1).show()

问题原因

  1. 方向向量归一化精度问题:当look_at距离look_from极远时,cameraFwd = (look_at - look_from).normalize()的方向几乎固定(比如look_at.x极大时,cameraFwd趋近于vec3(1,0,0)),相机旋转时方向变化极小,视角几乎不变。
  2. 焦平面定位错误:当前get_ray方法中,射线目标点是沿cameraFwd方向固定focal_distance距离的平面,而非聚焦到look_at点所在的平面。当look_at很远时,这个焦平面与look_at的位置偏差极大,导致视角无法跟随look_at变化。

解决方案

1. 修正相机初始化逻辑

在Camera.__init__中计算相机到look_at的距离,并处理方向向量平行的边界情况:

class Camera():
    def __init__(self, look_from, look_at, screen_width = 400 ,screen_height = 300,  field_of_view = 90., aperture = 0., focal_distance = None):
        self.screen_width = screen_width
        self.screen_height = screen_height
        self.aspect_ratio = float(screen_width) / screen_height
        self.look_from = look_from
        self.look_at = look_at
        
        # 计算相机到look_at的距离,若未指定focal_distance则用该值
        self.look_at_distance = (look_at - look_from).length()
        self.focal_distance = focal_distance if focal_distance is not None else self.look_at_distance
        
        self.camera_width = np.tan(field_of_view * np.pi / 180 / 2.) * 2.
        self.camera_height = self.camera_width / self.aspect_ratio
        
        # 计算相机坐标系,避免forward与up向量平行导致cross结果为零向量
        self.cameraFwd = (look_at - look_from).normalize()
        up = vec3(0.,1.,0.)
        if abs(self.cameraFwd.dot(up)) > 0.999:
            up = vec3(1.,0.,0.)
        self.cameraRight = (self.cameraFwd.cross(up)).normalize()
        self.cameraUp = self.cameraRight.cross(self.cameraFwd)
        
        self.lens_radius = aperture / 2.
        self.near = .1
        self.far = 100.
        self.x = np.linspace(-self.camera_width / 2., self.camera_width / 2., self.screen_width)
        self.y = np.linspace(self.camera_height / 2., -self.camera_height / 2., self.screen_height)
        xx, yy = np.meshgrid(self.x, self.y)
        self.x = xx.flatten()
        self.y = yy.flatten()

2. 修正射线生成逻辑

让射线指向look_at所在的焦平面,而非固定距离的平面:

def get_ray(self,n):
    x = self.x + (np.random.rand(len(self.x)) - 0.5) * self.camera_width / (self.screen_width)
    y = self.y + (np.random.rand(len(self.y)) - 0.5) * self.camera_height / (self.screen_height)
    
    # 计算射线原点(考虑光圈采样)
    ray_origin = self.look_from
    if self.lens_radius > 0:
        u, v = np.random.rand(2)
        r = self.lens_radius * np.sqrt(u)
        theta = 2 * np.pi * v
        offset = self.cameraRight * r * np.cos(theta) + self.cameraUp * r * np.sin(theta)
        ray_origin += offset
    
    # 计算焦平面上的目标点:以look_at为中心,添加横向/纵向偏移
    target = self.look_at + self.cameraRight * x * (self.focal_distance / self.near) + self.cameraUp * y * (self.focal_distance / self.near)
    
    # 计算射线方向
    ray_dir = (target - ray_origin).normalize()
    
    return Ray(origin=ray_origin, dir=ray_dir, depth=0, n=n, reflections=0, transmissions=0, diffuse_reflections=0)

说明

  • 修正后,相机无论look_at距离远近,都会正确朝向目标点,焦平面始终聚焦到look_at所在位置,旋转时视角可实现360度平滑变化。
  • 补充了镜头光圈的正确采样逻辑,同时处理了相机朝向与up向量平行的边界情况,避免方向向量计算错误。

内容的提问来源于stack exchange,提问作者FrostDream

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 22:43:10