第三章 光源模型编程:点光源、平行光源、面光源的数学模型与Python实现

做光学系统集成,光源模型是绕不开的第一道坎。

我记得刚入行那会儿,带我的老师傅跟我说过一句话:「你连光都描述不清楚,后面整个光路设计就是瞎蒙。」这话糙理不糙。光源的数学模型,说白了就是给光一个「身份证」——它从哪来、往哪去、能量怎么分布。

这一章,咱们就动手把三种最常用的光源模型用Python写出来。点光源、平行光源、面光源,一个都别落下。

3.1 点光源模型

点光源是最基础的光源模型。它假设光从一个点向四面八方均匀发射。

数学上怎么描述?很简单。假设光源位置在 P0 = (x0, y0, z0),那么空间中任意一点 P = (x, y, z) 接收到的光线方向就是:

direction = P - P0
# 然后归一化
direction_norm = direction / ||direction||

这里有个坑,我踩过。归一化的时候如果 PP0 重合,分母就是0。嗯,要加个判断。

注意:点光源的辐照度随距离平方衰减。也就是说,距离光源2米处的光强只有1米处的1/4。这个在物理仿真里必须考虑。

来看代码实现:

import numpy as np

class PointLight:
    def __init__(self, position, intensity=1.0):
        self.position = np.array(position, dtype=float)
        self.intensity = intensity
    
    def get_direction(self, point):
        """计算从光源到目标点的方向向量"""
        vec = np.array(point, dtype=float) - self.position
        dist = np.linalg.norm(vec)
        if dist < 1e-10:
            return np.zeros(3), 0.0  # 重合点,返回零向量
        direction = vec / dist
        # 辐照度随距离衰减
        irradiance = self.intensity / (4 * np.pi * dist**2)
        return direction, irradiance

我个人习惯把位置和强度分开管理。这样后期如果要加光源阵列,改起来方便。

3.2 平行光源模型

平行光源模拟的是太阳光这类远距离光源。光线之间互相平行,没有发散角。

数学上更简单——只需要一个方向向量 D = (dx, dy, dz)。所有光线都沿着这个方向走。

你想想看,平行光源没有位置概念。所以它的辐照度不随距离变化。这一点和点光源完全不同。

class ParallelLight:
    def __init__(self, direction, intensity=1.0):
        # 归一化方向向量
        self.direction = np.array(direction, dtype=float)
        self.direction = self.direction / np.linalg.norm(self.direction)
        self.intensity = intensity
    
    def get_direction(self, point=None):
        """平行光方向与目标点无关"""
        return self.direction, self.intensity

注意看,get_direction 方法里我加了个 point=None 的默认参数。这样做是为了和点光源保持接口一致。我在项目中吃过接口不统一的亏,后来所有光源类都统一了 get_direction(point) 这个接口。

小技巧:平行光源的方向向量通常指向光源方向,而不是光线传播方向。比如太阳在头顶,方向向量就是 (0, 1, 0)。这个约定在光线追踪里很常见。

3.3 面光源模型

面光源就复杂多了。它不是一个点,而是一个发光面。比如LED面板、荧光屏都属于面光源。

面光源的数学模型可以看作无数个点光源的集合。但实际计算时,我们不可能真的去算每一个点。常用的方法是蒙特卡洛采样。

假设面光源是一个矩形,长宽分别为 widthheight,中心在 center,法线方向为 normal

class AreaLight:
    def __init__(self, center, width, height, normal, intensity=1.0):
        self.center = np.array(center, dtype=float)
        self.width = width
        self.height = height
        self.normal = np.array(normal, dtype=float)
        self.normal = self.normal / np.linalg.norm(self.normal)
        self.intensity = intensity
        
        # 计算局部坐标系
        # 这里我选了一个与法线不平行的向量做叉积
        if abs(self.normal[0]) < 0.9:
            self.u = np.cross(self.normal, [1, 0, 0])
        else:
            self.u = np.cross(self.normal, [0, 1, 0])
        self.u = self.u / np.linalg.norm(self.u)
        self.v = np.cross(self.normal, self.u)
    
    def sample_point(self):
        """在面光源上随机采样一个发光点"""
        u_offset = (np.random.random() - 0.5) * self.width
        v_offset = (np.random.random() - 0.5) * self.height
        sample = self.center + u_offset * self.u + v_offset * self.v
        return sample
    
    def get_direction(self, point, num_samples=16):
        """从目标点看向面光源的多个采样点"""
        directions = []
        total_irradiance = 0.0
        
        for _ in range(num_samples):
            sample = self.sample_point()
            vec = sample - np.array(point, dtype=float)
            dist = np.linalg.norm(vec)
            if dist < 1e-10:
                continue
            direction = vec / dist
            # 面光源的辐照度与采样点处的发光角度有关
            cos_theta = abs(np.dot(direction, self.normal))
            irradiance = self.intensity * cos_theta / (dist**2)
            directions.append((direction, irradiance / num_samples))
            total_irradiance += irradiance / num_samples
        
        return directions, total_irradiance

这里我用了16个采样点。实际项目中,采样数量要根据精度要求来调。我曾经做过一个项目,采样数从16调到64,结果差异不到1%,但计算时间翻了两倍。所以别盲目追求高采样。

核心要点:面光源的辐照度计算要考虑两个因素——距离衰减和发光角度衰减。发光面法线方向的光最强,侧面看过去就暗了。这就是为什么LED面板正面看很亮,侧面看就一般。

3.4 光源参数化控制

写到这里,你可能已经发现了——三种光源有很多共同参数。比如位置、强度、方向。把这些参数统一管理,就是参数化控制的核心思想。

我建议用一个基类来定义公共接口:

class BaseLight:
    def __init__(self):
        self.name = ""
        self.enabled = True
        self.color = (1.0, 1.0, 1.0)  # RGB
    
    def get_direction(self, point):
        raise NotImplementedError
    
    def set_intensity(self, value):
        self.intensity = max(0.0, value)
    
    def toggle(self):
        self.enabled = not self.enabled

然后让三种光源都继承这个基类。这样做的好处是,你在系统集成时可以用统一的方式控制所有光源。

举个例子,假设你要做一个多光源场景:

class LightManager:
    def __init__(self):
        self.lights = []
    
    def add_light(self, light):
        self.lights.append(light)
    
    def compute_total_irradiance(self, point):
        total = 0.0
        for light in self.lights:
            if not light.enabled:
                continue
            _, irradiance = light.get_direction(point)
            total += irradiance
        return total

你看,不管是什么类型的光源,到了 LightManager 这里都一样处理。这就是参数化控制的威力。

实战建议:在做系统集成时,把光源参数放到配置文件里。比如用YAML或JSON格式。这样调试时改参数不用重新编译代码。我现在的项目都是这么干的,效率提升很明显。

3.5 三种光源的对比总结

光源类型 数学模型 关键参数 辐照度衰减 典型应用
点光源 单点发射,球面波 位置、强度 1/r² 小灯泡、LED单颗
平行光源 方向一致,平面波 方向、强度 无衰减 太阳光、准直光
面光源 面内多点发射 位置、尺寸、法线、强度 1/r² + 角度衰减 LED面板、显示屏

最后说一句。光源模型这东西,理论看着简单,但真正集成到光学系统里,坑不少。比如点光源和面光源的接口统一问题,我当年改了三版才满意。别急,慢慢来,代码写多了自然就顺手了。

下一章咱们聊聊光线追迹的基础算法。到时候这些光源模型就要真正派上用场了。