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

光线追踪中玻璃球体折射效果实现故障排查求助

玻璃球体折射效果实现问题排查与修复

问题概述

实现简易光线追踪器时,玻璃球体的折射效果始终无法正常工作。经初步排查,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)

核心问题分析

  1. 折射函数的法线方向错误:当光线从球内射出时,法线应指向球内(与入射光线方向同侧),但原代码始终计算指向球外的法线,导致cosI符号错误,折射公式计算完全失效。
  2. 第二次折射的折射率参数搞反:光线出球时是从玻璃(折射率1.5)进入空气(折射率1),但原代码仍传入n1=1, n2=1.5,参数完全颠倒。
  3. 出球后未偏移射线原点:出球时直接将交点作为新的射线原点,未做微小偏移,导致射线可能再次与球体相交,出现“原点仍在球内”的问题。
  4. 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,提问作者Элемент

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 07:20:41