光线追踪中玻璃球体折射效果实现故障排查求助
玻璃球体折射效果实现问题排查与修复
问题概述
实现简易光线追踪器时,玻璃球体的折射效果始终无法正常工作。经初步排查,refracted函数在光线离开球体(向量与法线夹角大于90°)时行为异常,且出球后射线原点仍处于球内,导致后续追踪出错。仅需实现两次折射(入射+出射)效果,无需复杂逻辑。
原代码
import numpy as np import matplotlib.pyplot as plt def normalize(vector): # normalize vector return vector / np.linalg.norm(vector) def refracted(vector, norm, n1, n2): # simulates vector refraction n = n1 / n2 cosI = -np.dot(norm, vector) sinT2 = n ** 2 * (1 - cosI ** 2) cosT = np.sqrt(1 - sinT2) return n * vector + (n * cosI - cosT) * norm def sphere_intersect(center, radius, ray_origin, ray_direction, glass): b = 2 * np.dot(ray_direction, ray_origin - center) c = np.linalg.norm(ray_origin - center) ** 2 - radius ** 2 delta = b ** 2 - 4 * c if delta > 0: t1 = (-b + np.sqrt(delta)) / 2 t2 = (-b - np.sqrt(delta)) / 2 if t1 > 0 and t2 > 0 and not glass: # returns min distance to intersection return min(t1, t2) if glass: # returns cords of intersection return ray_origin + max(t1, t2) * ray_direction return None def nearest_intersected_object(objects, ray_origin, ray_direction, glass=False): distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction, glass) for obj in objects] nearest_object = None min_distance = np.inf for index, distance in enumerate(distances): if distance and distance < min_distance: min_distance = distance nearest_object = objects[index] return nearest_object, min_distance width = 300 height = 200 max_depth = 10 camera = np.array([0, 0, 1]) ratio = float(width) / height screen = (-1, 1 / ratio, 1, -1 / ratio) # screen cords light = {'position': np.array([1, 3, 4])} objects = [ {'center': np.array([0.0, 0, 0.3]), 'radius': 0.35, 'type': 'glass'}, {'center': np.array([0, -9000, 0]), 'radius': 9000 - 0.7, 'color': np.array([0.9, 0.9, 0.9]), 'type': 'sphere'}, ] image = np.zeros((height, width, 3)) for i, y in enumerate(np.linspace(screen[1], screen[3], height)): for j, x in enumerate(np.linspace(screen[0], screen[2], width)): # screen and start vector pixel = np.array([x, y, 0]) ray_origin = camera ray_direction = normalize(pixel - ray_origin) # intersection nearest_object, min_distance = nearest_intersected_object(objects, ray_origin, ray_direction) if nearest_object is None: continue # ray_goes_to_infinity intersection = ray_origin + min_distance * ray_direction normal_to_surface = normalize(intersection - nearest_object['center']) shifted_point = intersection + 1e-5 * normal_to_surface # if for type 'glass' if nearest_object['type'] == 'glass': # 1st refraction ray_origin = shifted_point ray_direction = refracted(ray_direction, normal_to_surface, 1, 1.5) # 2nd refraction min_distance = sphere_intersect(nearest_object['center'], nearest_object['radius'], ray_origin, ray_direction, True) normal_to_surface = normalize(ray_origin + min_distance * ray_direction - nearest_object['center']) intersection = ray_origin + min_distance * ray_direction ray_origin = intersection ray_direction = refracted(ray_direction, normal_to_surface, 1, 1.5) # floor nearest_object = objects[1] if nearest_object is None: continue # ray_goes_to_infinity color = np.zeros(3) color += nearest_object['color'] # chess_floor_to_check_refractions if nearest_object['type'] == 'sphere' and (round(intersection[0]) % 2 == 0 and round(intersection[2]) % 2 != 0 or round(intersection[0]) % 2 != 0 and round( intersection[2]) % 2 == 0): color = np.zeros(3) image[i, j] = np.clip(color, 0, 1) print(f'{"%.1f" % (i/height * 100)}% complete') plt.imsave('shitty_balls.png', image)
核心问题分析
- 折射函数的法线方向错误:当光线从球内射出时,法线应指向球内(与入射光线方向同侧),但原代码始终计算指向球外的法线,导致
cosI符号错误,折射公式计算完全失效。 - 第二次折射的折射率参数搞反:光线出球时是从玻璃(折射率1.5)进入空气(折射率1),但原代码仍传入
n1=1, n2=1.5,参数完全颠倒。 - 出球后未偏移射线原点:出球时直接将交点作为新的射线原点,未做微小偏移,导致射线可能再次与球体相交,出现“原点仍在球内”的问题。
- sphere_intersect函数逻辑混乱:玻璃模式下直接返回交点,而非距离,导致
nearest_intersected_object的距离比较逻辑失效。
修复后的代码
import numpy as np import matplotlib.pyplot as plt def normalize(vector): return vector / np.linalg.norm(vector) def refracted(vector, norm, n1, n2): n = n1 / n2 # 计算入射光线与法线的夹角余弦,自动适配法线方向 cosI = np.dot(norm, vector) # 如果cosI为正,说明法线与入射光线同向(光线在介质内向外射),反转法线 if cosI > 0: norm = -norm cosI = -cosI sinT2 = n ** 2 * (1 - cosI ** 2) # 处理全反射(这里我们只处理折射,暂时忽略全反射) if sinT2 > 1: return None cosT = np.sqrt(1 - sinT2) return normalize(n * vector + (n * cosI - cosT) * norm) def sphere_intersect(center, radius, ray_origin, ray_direction): # 统一返回距离,玻璃逻辑在外部处理 oc = ray_origin - center b = 2 * np.dot(ray_direction, oc) c = np.linalg.norm(oc) ** 2 - radius ** 2 delta = b ** 2 - 4 * c if delta < 0: return None, None # 无交点 sqrt_delta = np.sqrt(delta) t1 = (-b + sqrt_delta) / 2 t2 = (-b - sqrt_delta) / 2 # 筛选正的距离,避免精度问题导致的自相交 t_vals = [t for t in [t1, t2] if t > 1e-5] if not t_vals: return None, None # 返回最近和最远的交点距离 t_min = min(t_vals) t_max = max(t_vals) if len(t_vals) == 2 else t_min return t_min, t_max def nearest_intersected_object(objects, ray_origin, ray_direction): nearest_obj = None min_t = np.inf for obj in objects: t_min, _ = sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) if t_min and t_min < min_t: min_t = t_min nearest_obj = obj return nearest_obj, min_t width = 300 height = 200 camera = np.array([0, 0, 1]) ratio = float(width) / height screen = (-1, 1 / ratio, 1, -1 / ratio) objects = [ {'center': np.array([0.0, 0, 0.3]), 'radius': 0.35, 'type': 'glass'}, {'center': np.array([0, -9000, 0]), 'radius': 9000 - 0.7, 'color': np.array([0.9, 0.9, 0.9]), 'type': 'sphere'}, ] image = np.zeros((height, width, 3)) for i, y in enumerate(np.linspace(screen[1], screen[3], height)): for j, x in enumerate(np.linspace(screen[0], screen[2], width)): pixel = np.array([x, y, 0]) ray_origin = camera ray_direction = normalize(pixel - ray_origin) nearest_obj, min_t = nearest_intersected_object(objects, ray_origin, ray_direction) if nearest_obj is None: continue if nearest_obj['type'] == 'glass': # 第一次折射:空气→玻璃 entry_point = ray_origin + min_t * ray_direction entry_normal = normalize(entry_point - nearest_obj['center']) # 偏移原点避免自相交 ray_origin = entry_point + 1e-5 * entry_normal ray_direction = refracted(ray_direction, entry_normal, 1.0, 1.5) if ray_direction is None: # 全反射,直接返回黑色 image[i, j] = np.zeros(3) continue # 第二次折射:玻璃→空气,找到出球点 _, exit_t = sphere_intersect(nearest_obj['center'], nearest_obj['radius'], ray_origin, ray_direction) if exit_t is None: image[i, j] = np.zeros(3) continue exit_point = ray_origin + exit_t * ray_direction exit_normal = normalize(exit_point - nearest_obj['center']) # 偏移原点到球外 ray_origin = exit_point - 1e-5 * exit_normal ray_direction = refracted(ray_direction, exit_normal, 1.5, 1.0) if ray_direction is None: image[i, j] = np.zeros(3) continue # 追踪折射后的光线到地面 floor_obj = objects[1] floor_t, _ = sphere_intersect(floor_obj['center'], floor_obj['radius'], ray_origin, ray_direction) if floor_t is None: image[i, j] = np.zeros(3) continue intersection = ray_origin + floor_t * ray_direction nearest_obj = floor_obj else: # 普通球体/地面处理 intersection = ray_origin + min_t * ray_direction # 生成棋盘格颜色 color = nearest_obj['color'].copy() if nearest_obj['type'] == 'sphere': x_round = round(intersection[0]) z_round = round(intersection[2]) if (x_round % 2 == 0 and z_round % 2 != 0) or (x_round % 2 != 0 and z_round % 2 == 0): color = np.zeros(3) image[i, j] = np.clip(color, 0, 1) print(f'{"%.1f" % (i/height * 100)}% complete') plt.imsave('glass_ball.png', image)
关键修改说明
- refracted函数优化:自动判断法线方向,确保
cosI计算正确,同时处理全反射情况。 - sphere_intersect函数重构:统一返回最近和最远交点距离,避免玻璃模式下的逻辑混乱。
- 修正折射率参数:出球时传入
n1=1.5, n2=1.0,符合玻璃→空气的折射场景。 - 射线原点偏移:出球时将原点向球外偏移微小距离,彻底解决原点在球内的问题。
- 玻璃球逻辑拆分:将两次折射的逻辑清晰分离,避免变量复用导致的错误。
内容的提问来源于stack exchange,提问作者Элемент
相关产品推荐
相关产品推荐

