3、光源模型:点光源、线光源、面光源、体积光源的定义与数学描述

做照明系统建模这些年,我越来越觉得——选对光源模型,比调对参数更重要。模型选错了,后面算得再精细,结果也是错的。

这一章,咱们就把四种基本光源模型掰开揉碎讲清楚。点、线、面、体,每种都有它的数学定义、适用场景,还有我踩过的坑。

核心观点:光源模型的选择,本质上是「精度」与「计算量」的权衡。没有绝对正确的模型,只有最合适的模型。
四种光源模型知识结构 光源模型分类 点光源 线光源 面光源 体积光源 特性 各向同性辐射 无方向/无大小 特性 一维延伸辐射 长度方向均匀 特性 二维平面辐射 朗伯/非朗伯 特性 三维空间辐射 自发光/散射体 典型应用 LED单颗灯珠 白炽灯近似 典型应用 荧光灯管 LED灯带 典型应用 面板灯/格栅灯 发光墙面 典型应用 雾灯/体积光 火焰/云层

3.1 点光源——最基础的起点

点光源,说白了就是假设光源缩成一个点,向所有方向均匀发光。这是最简单的模型,也是我入行时第一个接触的。

数学定义:

点光源在空间某点 P₀ 处,向各方向辐射光通量。在距离 r 处的照度 E 为:

E = I / r²

其中 I 是光源的光强(单位:cd),r 是距离(单位:m)。

如果考虑光线方向与表面法线的夹角 θ,则:

E = I · cos(θ) / r²

这就是经典的平方反比定律。你想想看,距离翻倍,照度变成四分之一。这个规律在近场时特别明显。

我的经验:点光源模型最适合模拟单颗LED灯珠、小型白炽灯。但要注意——当光源尺寸与照射距离的比值超过1:5时,点光源的误差就开始变大了。我一般在距离大于光源尺寸10倍时才放心用点光源。

3.2 线光源——从点到线的延伸

荧光灯管、LED灯带,这些长条形的光源,用点光源算就不太准了。这时候需要线光源模型。

数学定义:

线光源可以看作无数个点光源沿一条直线排列。假设线光源长度为 L,单位长度光强为 Iₗ(单位:cd/m),则线上某微元 dl 在目标点产生的照度为:

dE = Iₗ · dl · cos(θ) / r²

整条线光源的贡献,就是对 dl 从 0 到 L 积分:

E = ∫₀ᴸ Iₗ · cos(θ) / r² · dl

这个积分有解析解,但说实话,我平时更常用数值积分。尤其是当线光源不是直线,或者有弯曲的时候。

避坑提醒:线光源模型假设线上各点发光均匀。但实际荧光灯管两端光通量会偏低。我曾经因为这个偏差,导致一个超市照明项目算出来的平均照度比实测高了12%。后来我在两端加了修正系数才校准。

3.3 面光源——二维平面辐射

面板灯、发光顶棚、格栅灯……这些是面光源的典型代表。面光源模型比点光源复杂不少,但更贴近真实。

数学定义:

面光源可以看作无数个点光源在一个二维平面上分布。假设面光源面积为 A,单位面积光强为 Iₐ(单位:cd/m²),则面上某微元 dA 在目标点产生的照度为:

dE = Iₐ · dA · cos(θ) / r²

整个面的贡献是二重积分:

E = ∬ₐ Iₐ · cos(θ) / r² · dA

这里有个关键概念——朗伯体。朗伯体表面向各方向发光亮度相同,遵循余弦定律。大多数漫反射面光源可以近似为朗伯体。

光源类型 辐射特性 数学描述 典型场景
朗伯面光源 各方向亮度相同 I(θ) = I₀ · cos(θ) 漫反射面板灯
非朗伯面光源 有方向性辐射 I(θ) = I₀ · f(θ) 格栅灯、射灯面板
关键区别:朗伯体在θ=60°时光强降为一半,而非朗伯体可能降得更快或更慢。选对模型,直接决定配光曲线的准确性。

3.4 体积光源——三维空间发光体

体积光源是最复杂的模型,也是我近几年用得越来越多的。雾灯效果、火焰、云层、半透明材质的自发光……这些都需要体积光源。

数学定义:

体积光源在三维空间 V 内连续发光。单位体积的光强密度为 Iᵥ(单位:cd/m³),则体积内某微元 dV 在目标点产生的照度为:

dE = Iᵥ · dV · cos(θ) / r²

整个体积的贡献是三重积分:

E = ∭ᵥ Iᵥ · cos(θ) / r² · dV

嗯,这个积分基本没有解析解。我都是用蒙特卡洛方法或者体素化方法来做数值计算。

实用建议:体积光源的计算量非常大。我一般只在以下情况才用:
1) 光源本身有半透明介质(如磨砂灯泡内部)
2) 需要模拟大气散射效果(如雾灯、体积光)
3) 光源尺寸与场景尺寸在同一量级(如发光天花板整体)

3.5 四种模型的对比与选择

说了这么多,到底什么时候用哪种?我整理了一个对比表,方便你快速决策。

模型 维度 计算量 精度 适用距离 典型误差来源
点光源 0D 极低 低 远场(r > 10×光源尺寸) 近场误差大,忽略光源尺寸
线光源 1D 低 中 中远场 两端发光不均匀
面光源 2D 中 高 近中远场 朗伯假设偏差
体积光源 3D 高 最高 全距离 计算资源消耗大

我个人习惯是:能用简单模型就别用复杂的。先问自己三个问题:

  1. 光源尺寸相对于照射距离,能不能忽略?能 → 点光源
  2. 光源是不是长条形?是 → 线光源
  3. 光源是不是平面发光?是 → 面光源

如果以上都不是,或者需要模拟介质内部发光,才考虑体积光源。

我曾经犯过的错:在一个博物馆照明项目中,我图省事把所有射灯都用点光源模型。结果算出来的展柜照度比实测低了18%。后来发现,射灯的反光杯让光源实际呈现面光源特性。从那以后,我每次建模前都会先看灯具的配光曲线,再决定模型。

3.6 代码实现:四种光源的照度计算

理论讲完了,咱们上点实际的。下面是一个简单的 Python 实现,演示四种光源在目标点产生的照度计算。

import numpy as np

class PointLight:
    """点光源模型"""
    def __init__(self, position, intensity):
        self.pos = np.array(position)  # 光源位置 [x, y, z]
        self.I = intensity             # 光强 (cd)
    
    def illuminance_at(self, point, normal=None):
        r_vec = np.array(point) - self.pos
        r = np.linalg.norm(r_vec)
        if r == 0:
            return 0
        E = self.I / (r ** 2)
        if normal is not None:
            cos_theta = np.dot(-r_vec / r, normal)
            E *= max(0, cos_theta)
        return E

class LineLight:
    """线光源模型(数值积分)"""
    def __init__(self, start, end, intensity_per_meter, n_segments=100):
        self.start = np.array(start)
        self.end = np.array(end)
        self.I_l = intensity_per_meter  # 单位长度光强 (cd/m)
        self.n = n_segments
    
    def illuminance_at(self, point, normal=None):
        total = 0.0
        L = np.linalg.norm(self.end - self.start)
        dl = L / self.n
        for i in range(self.n):
            t = (i + 0.5) / self.n
            p = self.start + t * (self.end - self.start)
            r_vec = np.array(point) - p
            r = np.linalg.norm(r_vec)
            if r == 0:
                continue
            dE = self.I_l * dl / (r ** 2)
            if normal is not None:
                cos_theta = np.dot(-r_vec / r, normal)
                dE *= max(0, cos_theta)
            total += dE
        return total

class AreaLight:
    """面光源模型(朗伯体,数值积分)"""
    def __init__(self, corners, intensity_per_area, nx=20, ny=20):
        self.corners = np.array(corners)  # 4个角点 [左上, 右上, 右下, 左下]
        self.I_a = intensity_per_area     # 单位面积光强 (cd/m²)
        self.nx, self.ny = nx, ny
    
    def illuminance_at(self, point, normal=None):
        total = 0.0
        # 计算面光源的局部坐标系
        vx = self.corners[1] - self.corners[0]
        vy = self.corners[3] - self.corners[0]
        dx = np.linalg.norm(vx) / self.nx
        dy = np.linalg.norm(vy) / self.ny
        ux = vx / np.linalg.norm(vx)
        uy = vy / np.linalg.norm(vy)
        dA = dx * dy
        for i in range(self.nx):
            for j in range(self.ny):
                p = self.corners[0] + (i + 0.5) * dx * ux + (j + 0.5) * dy * uy
                r_vec = np.array(point) - p
                r = np.linalg.norm(r_vec)
                if r == 0:
                    continue
                dE = self.I_a * dA / (r ** 2)
                if normal is not None:
                    cos_theta = np.dot(-r_vec / r, normal)
                    dE *= max(0, cos_theta)
                total += dE
        return total

class VolumeLight:
    """体积光源模型(蒙特卡洛采样)"""
    def __init__(self, bounds, intensity_per_volume, n_samples=5000):
        self.bounds = np.array(bounds)  # [[xmin, xmax], [ymin, ymax], [zmin, zmax]]
        self.I_v = intensity_per_volume # 单位体积光强 (cd/m³)
        self.n = n_samples
    
    def illuminance_at(self, point, normal=None):
        total = 0.0
        vol = np.prod(self.bounds[:, 1] - self.bounds[:, 0])
        for _ in range(self.n):
            p = np.array([
                np.random.uniform(self.bounds[0, 0], self.bounds[0, 1]),
                np.random.uniform(self.bounds[1, 0], self.bounds[1, 1]),
                np.random.uniform(self.bounds[2, 0], self.bounds[2, 1])
            ])
            r_vec = np.array(point) - p
            r = np.linalg.norm(r_vec)
            if r == 0:
                continue
            dE = self.I_v * vol / self.n / (r ** 2)
            if normal is not None:
                cos_theta = np.dot(-r_vec / r, normal)
                dE *= max(0, cos_theta)
            total += dE
        return total

# 使用示例
if __name__ == "__main__":
    # 点光源
    pl = PointLight([0, 0, 2], 1000)
    print(f"点光源照度: {pl.illuminance_at([1, 0, 0], [0, 0, 1]):.2f} lx")
    
    # 线光源
    ll = LineLight([-1, 0, 2], [1, 0, 2], 500)
    print(f"线光源照度: {ll.illuminance_at([0, 1, 0], [0, 0, 1]):.2f} lx")
    
    # 面光源
    al = AreaLight([[-0.5, -0.5, 2], [0.5, -0.5, 2], [0.5, 0.5, 2], [-0.5, 0.5, 2]], 200)
    print(f"面光源照度: {al.illuminance_at([0, 0, 0], [0, 0, 1]):.2f} lx")
    
    # 体积光源
    vl = VolumeLight([[-0.3, 0.3], [-0.3, 0.3], [1.8, 2.2]], 100)
    print(f"体积光源照度: {vl.illuminance_at([0, 0, 0], [0, 0, 1]):.2f} lx")
代码说明:
- 点光源直接用公式,最快。
- 线光源和面光源用数值积分,精度由分段数控制。
- 体积光源用蒙特卡洛方法,采样数越多越准,但计算量也越大。
- 实际项目中,我会根据精度需求动态调整分段数或采样数。

好了,四种光源模型就讲到这里。记住一句话:模型是工具,不是真理。选对工具,你的照明设计就成功了一半。