【免费下载链接】SpektraLab
SpektraLab — a macOS film and print simulator. SwiftUI app + a Metal/C++ render engine, linked in, no Python at run time. A fork of andreavolpato/spektrafilm.
SpektraLab 是一款运行在 macOS 上的胶片与冲印模拟器:它不靠 LUT 滤镜,而是让光线穿过 81 个波长的光谱,还原胶片染料层与相纸的真实暗房过程——颗粒、光晕、成色剂、放大机滤色,一样都不少。
如果你用过 LUT,大概都有过同一个困惑:同一个 LUT,套在天空一片绿、套在皮肤一片黄,怎么都"不对味"。这不是你调得不好,而是 LUT 从原理上就装不下"胶片"。SpektraLab 换了一条路:不去"调出"胶片的样子,而是按物理过程把一张底片冲印出来。这篇文章讲清楚 LUT 到底差在哪,以及 SpektraLab 的光谱管线是怎么把暗房搬进电脑的。
先说结论:LUT 是"滤镜",不是"胶片"
LUT(Look-Up Table,查找表)做的事,用一句话就能说完:给每个像素的 RGB 三元组,查出另一个 RGB 三元组。它是一个"逐像素查表"的函数,没有波长、没有位置、没有过程。
而真正的胶片,有三样东西 LUT 从结构上就装不下:
| LUT 装不下的东西 | 为什么 LUT 做不到 | SpektraLab 怎么做 |
|---|---|---|
| 光谱(波长) | 3 通道 RGB 已把 81 个波长压缩成 3 个数,信息丢了 | 全程携带 81 个波长做积分 |
| 空间效应 | 查表只看"这一像素的值",看不到邻居 | 颗粒、光晕、成色剂扩散都是空间过程 |
| 冲印过程 | 结果被"烤死"进一张固定表 | 放大机、相纸、显影都是可算的物理阶段 |
这三点里,最关键的是第一点和第二点。光谱丢了,就没法区分"胶片和相纸各自在不同波长上吸了多少光";空间丢了,就没法做出高亮边缘旁边那条标志性的红色光晕。
81 波长:SpektraLab 的光谱管线长什么样
在 ARCHITECTURE.md 里,SpektraLab 把整条管线画成一句链式反应:
场景光 → 相机+胶片 → 潜影 → 显影 → 负片染料密度 → 放大机+相纸 → 潜印 → 显影 → 相纸染料密度 → 扫描/观看 → 输出 RGB
颜色不是按"RGB"算的,而是380–780 nm、每 5 nm 一个采样点,共 81 个波长。一个像素在引擎内部不是一组 RGB,而是"三条染料层在 81 个波长上的密度"。整条管线分三步:
第 1 步 · 曝光:RGB → 胶片"看到的光谱"引擎用 Hanatos 2025 的光谱上采样,把输入 RGB 还原成"如果这个颜色照到胶片上,胶片会看到什么样的 81 波长光谱",再乘以胶片自己的光谱敏感度。这一步在 engine/src/core/hanatos.cpp 里完成,192×192×81 的大表在初始化时就折算成一张 192×192×3 的查找表,所以逐像素时几乎零开销。
第 2 步 · 显影:曝光 → 染料密度曝光量查胶片的特性曲线,得到青/品红/黄三条染料层的密度;接着是DIR 成色剂耦合——染料之间互相抑制并做空间扩散;最后叠上颗粒。这一整段是胶片"化学"的核心。
第 3 步 · 冲印:密度 → 光 → 响应(两次光谱积分)把负片的染料密度放进放大机。引擎把同一个积分跑两遍:第一遍用"放大机灯光 + 相纸敏感度",得到相纸上的潜影;显影成相纸染料密度后,第二遍用"CIE 标准观察者 + 观看光源",得到最终 RGB。公式写出来是这样:
$$\text{out}m = \sum{\lambda} I(\lambda),S_m(\lambda);10^{-\left(\sum_k c_k D_k(\lambda) + D_{\text{base}}(\lambda)\right)}$$
关键在于:引擎内部根本没有 RGB 这个中间空间,ProPhoto RGB 只是入口和出口的"量色标准"。这就是它和 LUT 最本质的区别——LUT 是"RGB 进、RGB 出",SpektraLab 是"光谱进、光谱出"。整条节点序列在 engine/src/pipeline/pipeline.cpp。
物理尺度:颗粒与光晕为什么"真"
LUT 的第二个死穴是空间。SpektraLab 把颗粒和光晕做成真正的空间 + 随机过程,而且按画幅以微米为单位缩放:你告诉它这张底片是 120 还是 135,颗粒、光晕、成色剂扩散就会自动按画幅变粗变细。同一张照片叫成 35 mm,颗粒就正确地更粗。
光晕(halation)是光谱模拟最"炫"也最"难"的例子。对于没有防光晕层的胶片(比如 CineStill 800T),强光会穿过乳剂层、在片基底面发生菲涅尔反射再折返回来重新曝光。引擎用一组受物理约束的高斯混合去拟合这个"后反射光晕",甚至还原出"红色光晕比绿色远 1.5–2.7 倍"的现象——这就是为什么真实扫描里,一盏日光灯旁边总有一道橙红色的边。这套拟合见 engine/src/core/halation_psf.hpp。
这种效果,任何逐像素查表的 LUT 都做不出来——它需要"这一像素的强光,去影响它周围的像素"。
放大机与相纸:真正的"暗房"
胶片模拟里最容易被忽略的,是放大机和相纸。SpektraLab 把它们当成一等公民:
- 放大机滤色:用一套可调的双折射滤色片,按 CC 色深单位去"减"放大机灯光,相当于暗房里的色温/品红/黄滤片。引擎在 engine/src/core/spectral.cpp 里把滤色片建成 81 波长上的函数,再乘到灯光上。
- 相纸自己的染料层:和胶片一样,相纸也带 81 波长的敏感度与染料密度,参与第二次光谱积分。
- 物理档位:内置 34 款实测胶片/相纸档案(25 款负片 + 9 款相纸),全部来自真实测量数据,而非"调出来的近似"。
引擎里唯一用 LUT 的地方,恰好证明它的边界
有意思的是,SpektraLab 并非完全不用 LUT。它确实用了一张 LUT,但只用在一个很窄的场景:在不同相纸之间快速切换预览。
这张表是 33×33×33、约 431 kB 的浮点表,把"负片密度 → 某个相纸的成品"直接烘焙成查表,4500 万像素下 2 毫秒就能翻一张相纸,比重新冲印(约 9–167 毫秒)快得多。但代码里特意写了一句话,恰好点破了 LUT 的天花板:
"Glare is a spatial, stochastic veiling field and cannot live in a pointwise table." (眩光是空间性的、随机的遮蔽场,装不进逐点的表里。)
也就是说,连 SpektraLab 用 LUT 的那张表,都主动丢掉了眩光——因为那是空间 + 随机的东西,点表装不下。这张 LUT 的设计边界见 engine/src/core/print_lut.hpp。
不到一秒的"重印":性能与导出
很多人担心"物理模拟 = 很慢"。SpektraLab 用 C++ 把引擎直接编译进 App(没有运行时 Python),在 M3 Max 上测得:
- 预览尺寸(长边 2560)重印约0.01 秒;
- 全分辨率(7800×5800)渲染约0.87 秒;
- 整个安装包 32 MB,其中代码约 3 MB。
导出页会先在你的目标文件原始分辨率下软打样,确认和画布逐像素一致后才落盘;导出走的是 ImageIO,色彩只做一次映射。
数字中间片(DI):给后期留空间
除了直接出成品,SpektraLab 还能导出数字中间片:按 Cineon 风格的对数编码(代码 95 = 胶片底基黑,685 = 参考白,每十度 300 码,负 gamma 0.6),逐波长去掉胶片底基。这让文件"打印密度"化、灰度精确中性,适合后续调色——而成品冲印本身不做任何印后编辑,"修图"的活儿留给别的软件。详见 engine/src/core/digital_intermediate.cpp。
快速上手:三步打开你的第一张胶片
- 打开 RAW:把文件拖进窗口即可。引擎先把 RAW 解码成线性 ProPhoto 浮点。
- 选胶片 + 选相纸:左侧 Film 列表挑负片,Print 列表挑相纸(默认已配好一对)。
- 冲印并导出:右侧 Pre-Dev / Post-Dev 调曝光、白平衡、曲线;满意后进导出页选格式与色彩空间落盘。
💡 小贴士:Post-Dev(印后:曝光/曲线/色彩平衡)是"Layer 2",只走一次绘制、几乎无延迟;Film/Print 与 Pre-Dev 则会触发一次引擎渲染。把控制项放对"层",是滑块跟不跟手的关键。
小结
- LUT 是逐像素查表:没有波长、没有空间、没有过程,所以它只能"滤镜",做不出真胶片。
- SpektraLab 按物理过程算:81 波长光谱 × 胶片/相纸染料层 × 两次光谱积分,颗粒、光晕、成色剂全部是真实的物理与空间过程。
- 快且诚实:C++ Metal 引擎编译进 App,预览重印约 0.01 秒;连它自己用的那张 LUT,都诚实地丢掉了点表装不下的眩光。
如果你想要的是"调出一个胶片味",LUT 够用;如果你想要的是"把这张底片真的冲印出来",那是 SpektraLab 这条光谱管线在做的事。
源码与文档导航
想了解内部实现,可以从这几处入手:
- 总览与色彩链:ARCHITECTURE.md、README.zh-CN.md
- 引擎 C 接口(整条管线入口):engine/include/spektrafilm/spk_engine.h
- 光谱上采样(RGB → 曝光):engine/src/core/hanatos.cpp
- 光谱常量与放大机滤色:engine/src/core/spectral.cpp
- 光晕后反射拟合:engine/src/core/halation_psf.hpp
- 胶片/相纸档案结构:engine/src/core/profile.hpp
- 冲印管线节点序列:engine/src/pipeline/pipeline.cpp
- 数字中间片(Cineon log):engine/src/core/digital_intermediate.cpp
- 实测胶片/相纸档案示例:engine/resources_product/profiles/
- 前端参数模型:modern_UI/Spektrafilm/Spektrafilm/Model/Params.swift
【免费下载链接】SpektraLab
SpektraLab — a macOS film and print simulator. SwiftUI app + a Metal/C++ render engine, linked in, no Python at run time. A fork of andreavolpato/spektrafilm.
相关推荐
AI做了什么?为什么做?:NetClaw的GAIT不可变审计轨迹完整解析
AI做了什么?为什么做?:NetClaw的GAIT不可变审计轨迹完整解析 NetClaw ("An AI agent that claws through yo
给电视"换装"的风口真的来了:开源社区在做什么、还能做什么?
给电视"换装"的风口真的来了:开源社区在做什么、还能做什么? 你有没有过这样的体验:花大几千买的智能电视,开个机要先等十几秒广告,开机后扑面而来的不是你的App
移动开发原生移动桌面应用NegPy能做什么?这款开源胶片负片处理工具的完整介绍
NegPy能做什么?这款开源胶片负片处理工具的完整介绍 NegPy 是一款免费开源的胶片负片处理工具,专为负片扫描而设计。它不只是简单地把负片反转成正片,而是用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考