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

Python光线追踪代码RecursionError:递归深度超限问题求助

Python光线追踪项目RecursionError问题排查与修复

问题背景

开发Python光线追踪项目时,get_raycolor函数触发RecursionError: maximum recursion depth exceeded,错误来自折射材质的透射光线递归调用。调试发现某特定立方体碰撞体被重复检测上千次,递归无法终止。

相关代码片段

get_raycolor函数

def get_raycolor(ray):
    distances = []
    hit_orientation = []
    for s in Sc['scene_primitives']:
        if isinstance(s,list):
            if s[0] == 'sphere':
                a = sphere_intersect(s,ray['origin'],ray['dir'])
            elif s[0] == 'plane':
                a = plane_intersect(s,ray['origin'],ray['dir'])
            else:
                a = cuboid_intersect(s,ray['origin'],ray['dir'])
        else:
            a = s.intersect(ray['origin'], ray['dir'])
        distances.append(a[0])
        hit_orientation.append(a[1])
    nearest = np.minimum.reduce(distances)
    color = vec3(0., 0., 0.)
    for (coll, dis , orient) in zip(Sc['scene_primitives'], distances, hit_orientation):
        hit_check = (nearest != FARAWAY) & (dis == nearest)
        if np.any(hit_check):
            if isinstance(coll,list):
                if coll[0] == 'sphere':
                    color += coll[4].get_color({'origin':ray['origin'].extract(hit_check),'dir':ray['dir'].extract(hit_check),'depth':ray['depth'],'n':ray['n'].extract(hit_check),'reflections':ray['reflections'],'transmissions':ray['transmissions'],'diffuse_reflections':ray['diffuse_reflections']}, {'distance':extract(hit_check,dis),'orientation':extract(hit_check,orient),'collider':coll,'u':None,'v':None,'point':None}).place(hit_check)
                elif coll[0] == 'cuboid':
                    color += coll[5].get_color({'origin':ray['origin'].extract(hit_check),'dir':ray['dir'].extract(hit_check),'depth':ray['depth'],'n':ray['n'].extract(hit_check),'reflections':ray['reflections'],'transmissions':ray['transmissions'],'diffuse_reflections':ray['diffuse_reflections']}, {'distance':extract(hit_check,dis),'orientation':extract(hit_check,orient),'collider':coll,'u':None,'v':None,'point':None}).place(hit_check)
                else:
                    color += coll[6].get_color({'origin':ray['origin'].extract(hit_check),'dir':ray['dir'].extract(hit_check),'depth':ray['depth'],'n':ray['n'].extract(hit_check),'reflections':ray['reflections'],'transmissions':ray['transmissions'],'diffuse_reflections':ray['diffuse_reflections']}, {'distance':extract(hit_check,dis),'orientation':extract(hit_check,orient),'collider':coll,'u':None,'v':None,'point':None}).place(hit_check)
            else:
                color += coll.material.get_color({'origin':ray['origin'].extract(hit_check),'dir':ray['dir'].extract(hit_check),'depth':ray['depth'],'n':ray['n'].extract(hit_check),'reflections':ray['reflections'],'transmissions':ray['transmissions'],'diffuse_reflections':ray['diffuse_reflections']}, {'distance':extract(hit_check,dis),'orientation':extract(hit_check,orient),'collider':coll,'u':None,'v':None,'point':None}).place(hit_check)
    return color

Refractive材质类

class Refractive:
    def __init__(self, n, **kwargs):
        self.n = n
    def get_color(self, ray, hit):
        hit['point'] = (ray['origin'] + ray['dir'] * hit['distance'])
        if isinstance(hit['collider'],list):
            if hit['collider'][0] == 'sphere':
                N = (hit['point'] - hit['collider'][3]) * (1. / hit['collider'][5])*hit['orientation']
                max_depth = hit['collider'][2]
            elif hit['collider'][0] == 'cuboid':
                N = cuboid_get_Normal(hit['collider'],hit)
                max_depth = hit['collider'][8]
            else:
                N = hit['collider'][4]
                max_depth = hit['collider'][3]
        else:
            N = hit['collider'].get_Normal(hit)*hit['orientation']
            max_depth = hit['collider'].max_ray_depth
        color = vec3(0.,0.,0.)
        V = ray['dir']*-1.
        nudged = hit['point'] + N * .000001
        if ray['depth'] <max_depth:
            n1 = ray['n']
            n2 = vec3(np.where(hit['orientation']== UPWARDS, self.n.x, Sc['n'].x),np.where(hit['orientation']== UPWARDS, self.n.y, Sc['n'].y),np.where(hit['orientation']== UPWARDS, self.n.z, Sc['n'].z))
            n1_div_n2 =  vec3(np.real(n1.x), np.real(n1.y), np.real(n1.z))/vec3(np.real(n2.x), np.real(n2.y), np.real(n2.z))
            cosdeltai = V.dot(N)
            sin2deltat = (n1_div_n2)**2 * (1.-cosdeltai**2)
            vv = 1. - (n1/n2)**2 * (1.-cosdeltai**2)
            cosdeltat = vec3(np.sqrt(vv.x) , np.sqrt(vv.y) ,np.sqrt(vv.z))
            r_per = (n1*cosdeltai - n2*cosdeltat)/(n1*cosdeltai + n2*cosdeltat)
            r_par = -1.*(n1*cosdeltat - n2*cosdeltai)/(n1*cosdeltat + n2*cosdeltai) 
            F = (np.abs(r_per)**2 + np.abs(r_par)**2)/2.
            reflected_ray_dir = (ray['dir'] - N * 2. * ray['dir'].dot(N)).normalize()
            color += (get_raycolor({'origin':nudged, 'dir':reflected_ray_dir, 'depth':ray['depth'] + 1, 'n':ray['n'], 'reflections':ray['reflections'] + 1, 'transmissions':ray['transmissions'], 'diffuse_reflections':ray['diffuse_reflections']}))*F
            n1_div_n2_aver = (n1_div_n2.x + n1_div_n2.y +  n1_div_n2.z)/3
            sin2deltat = (n1_div_n2_aver)**2 * (1.-cosdeltai**2)
            non_TiR = (sin2deltat <= 1.)
            if np.any(non_TiR):
                # 错误触发行:depth未递增
                color += (get_raycolor( {'origin':(hit['point'] - N * .000001).extract(non_TiR),'dir':(ray['dir']*(n1_div_n2_aver) + N*(n1_div_n2_aver * cosdeltai - np.sqrt(1-np.clip(sin2deltat,0,1)))).normalize().extract(non_TiR),'depth':ray['depth'],'n':n2.extract(non_TiR),'reflections':ray['reflections'],'transmissions':ray['transmissions'] + 1,'diffuse_reflections':ray['diffuse_reflections']})  )*(1. - F).extract(non_TiR).place(non_TiR)
            vv = -2.*vec3(np.imag(ray['n'].x), np.imag(ray['n'].y), np.imag(ray['n'].z))*2.*np.pi/vec3(630,550,475) * 1e9* hit['distance']
            color = color *vec3(np.exp(vv.x) , np.exp(vv.y) ,np.exp(vv.z))
        return color

根本原因分析

  1. 递归终止条件失效:在透射光线的get_raycolor调用中,depth参数直接使用ray['depth']而非ray['depth'] + 1。结合立方体的max_depth=5,递归调用的depth始终为0,永远满足ray['depth'] < max_depth,导致无限递归。
  2. 潜在自相交问题:透射光线的原点偏移方向是否正确需要验证,若偏移方向错误,可能导致光线刚进入物体就再次检测到与自身相交,加剧递归次数。

修复方案

  1. 修复递归深度递增:修改透射光线的depth参数为ray['depth'] + 1,确保递归能在达到max_depth时终止:
    color += (get_raycolor( {'origin':(hit['point'] - N * .000001).extract(non_TiR),'dir':(ray['dir']*(n1_div_n2_aver) + N*(n1_div_n2_aver * cosdeltai - np.sqrt(1-np.clip(sin2deltat,0,1)))).normalize().extract(non_TiR),'depth':ray['depth'] + 1,'n':n2.extract(non_TiR),'reflections':ray['reflections'],'transmissions':ray['transmissions'] + 1,'diffuse_reflections':ray['diffuse_reflections']})  )*(1. - F).extract(non_TiR).place(non_TiR)
    
  2. 验证偏移方向:确认透射光线的原点偏移方向(hit['point'] - N * .000001)是否正确。当光线进入介质时(hit['orientation']为向下),应向物体内部偏移;若方向错误,调整为hit['point'] + N * .000001或根据法线方向动态判断。
  3. 增加全局递归限制:在get_raycolor开头添加全局最大深度检查,防止单个物体max_depth设置过大:
    GLOBAL_MAX_DEPTH = 10
    def get_raycolor(ray):
        if ray['depth'] >= GLOBAL_MAX_DEPTH:
            return vec3(0.,0.,0.)
        # 原有代码...
    

调试建议

  • 打印递归深度:在get_raycolor开头添加print(f"Recursion depth: {ray['depth']}"),确认深度是否递增。
  • 断点调试:使用pdb.set_trace()在错误触发行设置断点,查看每次递归的ray参数和max_depth值。
  • 简化场景测试:先创建仅包含单个折射立方体的场景,验证递归终止逻辑,再逐步添加其他物体。
  • 统计碰撞次数:给每个碰撞体添加计数,打印被检测的次数,确认是否存在重复检测或自相交。

代码结构优化建议

  • 用类替代列表存储场景图元:将球体、立方体、平面定义为类(如Sphere、Cuboid),统一intersect、get_normal等方法,避免大量isinstance判断,提高代码可读性。
  • 提取光线创建工具函数:将反射、透射光线的创建逻辑封装为函数,减少重复代码,比如:
    def create_reflected_ray(origin, dir, normal, depth, n, reflections):
        reflected_dir = (dir - normal * 2. * dir.dot(normal)).normalize()
        nudged_origin = origin + normal * 0.000001
        return {'origin': nudged_origin, 'dir': reflected_dir, 'depth': depth+1, 'n': n, 'reflections': reflections+1, 'transmissions':0, 'diffuse_reflections':0}
    
  • 添加类型提示:为函数参数和返回值添加类型提示,提升代码可维护性,比如:
    def get_raycolor(ray: dict[str, Any]) -> vec3:
        # 代码...
    
  • 缓存相交结果:对同一光线与场景图元的相交结果进行缓存,避免重复计算,提升性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 22:02:01