光线追踪器相机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()
问题原因
- 方向向量归一化精度问题:当
look_at距离look_from极远时,cameraFwd = (look_at - look_from).normalize()的方向几乎固定(比如look_at.x极大时,cameraFwd趋近于vec3(1,0,0)),相机旋转时方向变化极小,视角几乎不变。 - 焦平面定位错误:当前
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
相关产品推荐
相关产品推荐

