MiroFish智能镜面:半透镜透光率与Boids鱼群算法实战

发布时间:2026/9/18 10:05:54
MiroFish智能镜面:半透镜透光率与Boids鱼群算法实战 客厅那面镜子挂了两年除了照人就是积灰直到我把它改成了 MiroFish——一面平时安安静静当镜子用、有人走近就有一群鱼从边缘慢慢游出来的智能镜面。它不接水、不喂食、不用换滤棉也不需要你在墙上打十几个孔走管道。整机厚度不到 6 厘米挂在玄关或者走廊尽头白天你几乎看不出它有什么特别晚上关掉主灯它就是整间屋子里最耐看的那件东西。做这个东西的念头其实很朴素。我养过几年真鱼知道草缸有多好看也知道维护有多烦换水、控温、爆藻、停电、出差三天回来满缸白点。后来我把真鱼送人了但那种盯着水里发呆的松弛感我一直想要回来。MiroFish 就是我给出的答案——用一块半透镜加一块屏幕把鱼这件事从物理世界挪到光学和代码里。它解决的从来不是怎么把鱼养好而是怎么在不承担养鱼成本的前提下保留看鱼的那份安静。这篇内容适合三类人一类是手里有树莓派、想找个能真正挂在墙上拿得出手的项目的人一类是做互动装置、展陈或者店铺软装需要低成本氛围硬件的朋友还有一类纯粹是被魔镜这个概念吸引、想搞明白半透镜到底怎么选、屏幕亮度到底要多少才够看的人。我不打算只给你一个接线上电跑起来的流程那种东西网上到处都是。我更想把光学计算、群集算法参数、散热和电源这几个真正会翻车的环节拆开讲清楚让你少走我那三个周末的弯路。1. 从一句想要一面会养鱼的镜子到方案定稿1.1 需求拆解MiroFish 到底要满足哪几件事动手之前我列了一张需求表一共五条后来发现这五条直接决定了后面所有的技术选型。第一条是日常状态必须像镜子不能是一块永远亮着的屏幕挂在墙上那样太吵而且晚上会变成客厅里的光源污染。第二条是有人靠近才唤醒走开后一段时间内自动回到镜面状态这个过程要平滑不能啪地一下亮起来。第三条是鱼群要像鱼群不能是十条鱼各游各的要有群体感、有聚散、有被惊扰后重新聚集的过程这是整个项目里最值钱的部分。第四条是整机免维护不用每天插拔电源不用管系统更新断电重启后能自己起来。第五条是外观能见人边框不能是裸露的亚克力和热熔胶得有一个像家具的壳子。这五条里第一条和第三条其实是冲突的。镜子要像镜子意味着环境光反射必须压过屏幕透射出来的光而鱼群要像鱼群又需要屏幕有足够的亮度把细节托出来。这个矛盾最后成了整个项目最核心的设计约束我在第 2 节会把它量化成一个亮度对比度的计算。很多人做魔镜失败不是代码写错了是从来没算过这笔账做完发现白天根本看不见然后草草归结为屏幕不行得换 OLED。第三条需求还带来一个隐藏约束帧率稳定性比帧率高低更重要。鱼群行为算法是靠每一帧累积微小位移来产生生命感的如果帧率在 60 和 25 之间乱跳鱼就会出现忽快忽慢的抽搐。所以后面我在渲染上做了大量减法把目标定在稳定 30 帧而不是追求 60 帧。1.2 三条可选路线为什么我最终选了透射式半透镜方案市面上能实现镜面显示的路子大致有三条我实打实评估过其中两条第三条直接放弃了。第一条是反射式把屏幕侧放在镜子背面屏幕的光打到半透镜上再反射进人眼。好处是像有真实景深你走动的时候鱼会有视差看起来真的在镜子里面的空间。坏处是屏幕不在正前方要占掉额外的框体厚度而且屏幕边缘的光会漏进视野做成大尺寸的时候漏光非常难看。小尺寸比如 20 厘米以内值得一试我这块 300×400 毫米的板子直接排除了它。第二条是透射式屏幕贴在镜子正后方光线直接穿过半透镜进入人眼半透镜同时负责反射环境光形成镜面效果。这是目前绝大多数魔镜项目用的方案结构最薄只需要屏幕和镜面之间留一段距离用来散热和走线。缺点是成像平面就贴在镜面上鱼看起来贴在玻璃上缺乏纵深。但这个问题可以完全交给渲染解决——给每条鱼一个深度值用缩放和雾化模拟出远近关系肉眼几乎分辨不出来。第三条是透明 OLED 或者透明 LCD屏幕本身就是半透的不需要额外贴镜面膜。我查完价格就关掉了页面同尺寸成本是方案二的十倍以上而且透明屏在暗场下的黑位一塌糊涂镜子那部分会发灰。对于个人项目来说完全没有性价比。最终定下来的结构是7 寸 IPS 屏 3 毫米半透亚克力 铝型材边框屏幕与镜面的间距定在 45 毫米。这 45 毫米不是随便写的它同时满足三个条件留出屏幕驱动板和走线的空间、让镜面后表面反射的重影落到视野之外、给框内空气对流留出截面。注意半透亚克力比玻璃轻、好加工、不怕摔但硬度低擦灰的时候一定要用超细纤维布加清水酒精会让表面发乌。1.3 模块划分与物料清单整机分成四块光学组件半透镜 屏幕、计算单元树莓派、感知组件人体感应 光线传感器、结构件框体 散热 电源。我在动工前把所有东西都平铺在桌子上跑通了再装的这个习惯救了我至少两次——尤其是屏幕和树莓派之间的 HDMI 时序问题装上之后拆一次要半小时。模块选型参考价格备注计算树莓派 4B 2GB约 300 元Zero 2 W 也够用但 HDMI 输出要转接头显示7 寸 IPS 1024×600400 nits约 180 元带 HDMI 驱动板支持背光调光光学3 毫米半透亚克力300×400 毫米约 80 元下单前一定问清透光率感知HC-SR501 人体红外 BH1750 光照约 25 元光照传感器用来做自适应亮度结构铝型材边框 亚克力背板约 70 元45 毫米厚度散热4010 风扇 ×1 防尘网约 15 元串联电阻降速降噪电源5V/3A 适配器 DC 母座约 40 元留足余量屏幕瞬时电流不小杂件铜柱、线夹、亚克力胶、风扇螺丝约 30 元别省装的时候很缺合计大约 740 元。这个预算下如果只是想验证效果可以先把半透镜放在桌上屏幕放后面用手电筒照着模拟环境光先看对比度够不够再决定要不要投入做壳子。我强烈建议先做这一步因为半透镜一旦按尺寸裁好就退不了货。2. 半透镜选型与亮度对比度的硬核算账2.1 透光率和反射率到底该怎么选半透镜的参数只有一个核心透光率 T。反射率 R 近似等于 1 减去 T忽略吸收所以商家标了 T 就等于标了 R。市面上常见的规格有 T10%、T30%、T50%、T70% 几档还有一些标注模糊的所谓魔镜专用膜你需要主动去问客服要实测数据很多店给不出。选型的逻辑非常反直觉很多人第一反应是透光率越高屏幕越亮那就选高的。错。透光率越高环境光反射回来的也越多镜面的镜感越弱。T70% 的板子挂上去白天你看到的是一块发灰的玻璃后面有个亮屏幕不是一面镜子。反过来 T10% 的板子镜感极强白天和普通镜子几乎没差别但需要屏幕亮度压到非常高的水平才能穿透它。我最后选的是T30%。理由是它在我预期的夜间使用场景下对比度足够同时白天的镜面感能保持在别人进来不会觉得奇怪的水平。如果你的 MiroFish 装在光照充足的客厅白天也想看到鱼那就得选 T50% 并且配一块 800 nits 以上的屏幕成本直接翻倍。透光率 T镜面感白天对屏幕亮度要求适用场景10%极强几乎等同普通镜1000 nits 以上玄关镜、只做氛围不追求白天可见30%强细看能察觉400 至 600 nits走廊、卧室、夜间为主50%中等明显能看到底色300 nits 左右白天也要求可见70%弱更像一块玻璃200 nits 即可展陈、暗环境2.2 亮度对比度计算为什么晚上才好看是设计出来的这一步是全文最值得抄作业的部分。你看到的最终画面亮度等于屏幕透射过来的光加上环境光被镜面反射回来的光。要看清鱼前者必须显著大于后者。透射亮度很好算L_view L_screen × T。屏幕标称 400 cd/m²T0.3那么透射亮度就是 120 cd/m²。反射亮度稍微绕一点用漫反射的近似公式L_refl E × R / π。E 是环境照度单位勒克斯R 是反射率0.7π 是圆周率。为什么会有这个 π因为勒克斯描述的是落到单位面积上的光通量密度而 cd/m² 描述的是单位面积朝单位立体角发出的光漫反射面把光均匀撒到半个球面上这个换算系数就是 1/π。代入几组真实的室内照度算一下环境照度 E反射亮度 L_refl透射亮度 120 cd/m² 时的对比度观感30 lux睡前只留夜灯6.7 cd/m²约 18:1鱼非常通透几乎像发光80 lux夜间主灯调暗17.8 cd/m²约 6.7:1舒适鱼和水感都清楚200 lux普通夜间客厅44.6 cd/m²约 2.7:1能看清但整体发灰400 lux白天阴天89.1 cd/m²约 1.3:1非常吃力基本看不清700 lux白天晴天室内155.9 cd/m²小于 1完全看不到鱼数字一目了然这面镜子在夜间是魔法在白天就是一面普通镜子。我一开始有点沮丧后来想通了——这恰恰是我要的行为。白天它老老实实当镜子晚上它才是 MiroFish。与其花十倍成本去跟太阳对抗不如把环境自适应做进软件里让它在照度超过 350 lux 时主动降低屏幕亮度甚至关闭背光既省电又延长屏幕寿命。提示这个计算公式里 R 取 0.7 是理想值实际亚克力表面有散射实测反射亮度会略低也就是说实际观感比算出来的好一点。别因为算出来白天看不见就放弃。2.3 自适应亮度的实现思路有了上面的模型自适应亮度就不是玄学调参了而是直接的闭式计算。我在树上挂了一个 BH1750 光照传感器每 5 秒采一次环境照度然后按目标对比度反推需要的屏幕亮度L_screen k × E × R / (π × T)其中 k 是我想要的对比度目标取 4。代入 T0.3、R0.7、π3.14化简后 L_screen ≈ 2.97 × E。也就是说环境照度 80 lux 时屏幕给到 240 cd/m² 就够了环境照度 200 lux 时需要 594 cd/m²。关键点是最后一步把计算结果往 400 这个上限夹紧再乘一个 0.75 的舒适系数。因为屏幕在最高亮度下色偏明显、发热也大除非必要不要顶满。我把最终的亮度映射到屏幕驱动板的 PWM 调光上用 0 到 100 的百分比输出实测 60% 亮度对应约 240 cd/m²正好覆盖我的主要使用场景。这里有个很容易踩的坑调光用背光 PWM不要用软件调色。很多教程教你往画面上叠一层半透明黑色遮罩来调暗那是在骗自己背光功耗一分没省屏幕该发热还是发热而且暗部细节会被压死。找屏幕驱动板上的 BL 或者 EN 引脚用 GPIO 输出 PWM 才是正解。3. 框体结构、散热与供电的实操细节3.1 厚度、视差与重影之间的取舍半透镜到屏幕的间距 d是我在结构上唯一反复调整过的参数。第一版我做了 25 毫米装上去之后正面看没问题稍微偏一点角度就能在画面里看到第二层淡淡的虚影——那是半透镜后表面反射屏幕光形成的重影。原理很简单半透镜是亚克力板前表面和后表面都会反光两侧之间隔着 3 毫米板材厚度如果有人射光打到后表面反射回来就会形成偏移的小号虚像。解决办法有两个。一是缩小间距让重影偏移量小于 1 毫米肉眼就看不出来了二是加大间距把重影推到视野边缘之外。我选的是后者把 d 加到 45 毫米同时把屏幕四周的发光区域用黑色绒布做了遮光。实测下来重影基本消失只有在很近的距离、很偏的角度才看得出来。顺便说个真实感受d 从 25 加到 45鱼看起来贴在玻璃上的感觉几乎没有变化因为透射式的成像平面天然就在屏幕那一层。真正让鱼有纵深感的是渲染里的雾化和缩放结构上折腾间距的收益远不如改这几行代码。如果你正在规划尺寸d 取 40 到 60 毫米就够了多留出来的空间全是给散热和走线的。3.2 封闭框体的散热设计这是最容易被忽略、也最容易让整个项目变短命的一环。7 寸 IPS 屏的驱动板和背光在 60% 亮度下大约耗散 3 瓦树莓派 4B 跑图形负载大约 4 瓦两个热源塞进一个 45 毫米厚、四周几乎密闭的盒子里实测内部温度稳定在 55 摄氏度以上。树莓派 CPU 会降到 75 度开始降频一降频鱼群就开始抽搐你还以为是代码问题。我的方案是下进上出的自然对流加上一个降速风扇。背板底部开一排直径 6 毫米的进气孔顶部开同样面积的一排出气孔孔外侧贴防尘网。然后装一个 4010 涡轮风扇位置贴着出气孔往外抽。关键是给风扇串一个 33 欧姆的电阻把转速从 5 伏时的满速降到大约 3.5 伏的水平。满速时风扇噪音在安静的卧室里非常明显降到 3.5 伏之后基本听不见但风量还有原来的一半左右够用了。实测改性后的效果框内温度从 55 度降到 42 度树莓派 CPU 从 72 度降到 58 度帧率再也没有掉过。如果嫌风扇麻烦还有一个零噪音方案把屏幕亮度上限压到 50%并且把树莓派限频到 1.2GHz。温度能控制在 50 度以内代价是鱼的数量要减到 120 条左右。注意风扇一定要往外抽不要往里吹。往里吹会把防尘网上的灰直接吹进框体落在半透镜内表面上擦都擦不到。3.3 供电、开关机与背光控制整机是单路 5V 供电用 5V/3A 的适配器。这里必须提醒一句不要用 USB 线从旧充电头上取电。屏幕驱动板在上电瞬间的浪涌电流会超过 2 安培劣质电源会掉压表现就是树莓派随机重启你还以为是软件崩溃。用带 DC 母座的成品适配器线径选 20AWG 以上的。背光控制我用了一个 N 沟道 MOSFETAO3400 这类就够栅极接树莓派的 GPIO18源极接地漏极接屏幕驱动板的背光使能脚。这样软件里输出 PWM 就能直接控亮度开关。注意 MOSFET 的源极必须接地而不是接负载侧这是低压侧开关的标准接法接反了要么常亮要么烧管子。休眠策略是这样的人体感应超过 3 分钟没有触发先把鱼群做一次散场动画——鱼慢慢游向画面边缘淡出——然后屏幕亮度在 2 秒内从当前值线性降到 0最后关闭背光。整个过程大约 5 秒观感上非常自然不会给人一种设备断电了的突兀感。唤醒则相反先点亮背光到低亮度鱼从边缘游进来亮度再爬到自适应目标值。状态触发条件屏幕亮度鱼群表现整机功耗休眠3 分钟无人体活动0背光关闭无约 3.2 W唤醒检测到人体活动渐亮至目标值从边缘游入约 6.5 W互动轻触镜面目标值 10%鱼群受惊散开约 7.2 W日常持续有活动自适应计算值正常巡游约 6.5 W算一笔电费按唤醒状态平均 6.5 瓦、每天有效点亮 5 小时计算一天耗电 0.033 度一个月大约 1 度电。这个数字让我在要不要常开这个问题上完全没有了心理负担。4. 鱼群算法与渲染的核心实现4.1 用 Boids 三原则搭出群体行为MiroFish 的灵魂是鱼群而鱼群最省力也最像的解法是 Boids 模型。它的思想非常朴素不给每条鱼写剧本只给三条局部规则群体行为自己涌现出来。三条规则分别是分离别撞上邻居、对齐跟邻居朝一个方向游、聚合别离群太远。我用 Python 加 NumPy 实现核心是每帧算一次两两距离矩阵。有人一听 O(N²) 就慌了但在 N 不超过 300 的场景下完全无所谓。300 条鱼的两两组合是 9 万对NumPy 用向量化算一次不到 3 毫秒30 帧每秒的要求下这点开销可以忽略。强行上空间哈希或者四叉树反而会因为 Python 的循环开销把速度拖得更慢。import numpy as np def update(boids, dt, cfg): pos boids[:, 0:2].copy() # N x 2 位置 vel boids[:, 2:4].copy() # N x 2 速度 N pos.shape[0] # 两两向量与距离对角线置大值避免自己跟自己算 d pos[:, None, :] - pos[None, :, :] dist np.linalg.norm(d, axis-1) np.fill_diagonal(dist, 1e9) # 分离只考虑分离半径内的邻居力与距离平方成反比 sep_mask (dist cfg[r_sep]) sep np.zeros_like(pos) if sep_mask.any(): inv np.where(sep_mask, 1.0 / (dist ** 2 1e-6), 0.0) sep np.einsum(ij,ijk-ik, inv, d) # 对齐与聚合只看感知半径内的邻居 nb_mask (dist cfg[r_nb]).astype(np.float64) cnt nb_mask.sum(axis1, keepdimsTrue) cnt[cnt 0] 1.0 align np.einsum(ij,jk-ik, nb_mask, vel) / cnt - vel cohesion np.einsum(ij,jk-ik, nb_mask, pos) / cnt - pos acc (cfg[w_sep] * sep cfg[w_ali] * align cfg[w_coh] * cohesion) # 转向力限幅防止单帧加速度过大导致抖动 norm np.linalg.norm(acc, axis1, keepdimsTrue) norm[norm 0] 1.0 acc acc / norm * np.minimum(norm, cfg[max_force]) vel acc * dt # 速度限幅低于下限会原地不动像死了高于上限像子弹 sp np.linalg.norm(vel, axis1, keepdimsTrue) sp[sp 0] 1.0 vel vel / sp * np.clip(sp, cfg[v_min], cfg[v_max]) pos vel * dt boids[:, 0:2] pos boids[:, 2:4] vel这段代码里有几处是我踩坑之后才加上的。第一处是分离力用距离平方的倒数而不是线性倒数因为线性倒数在邻居很近的时候力还不够大鱼会轻微重叠视觉上像穿模。第二处是转向力限幅如果不限制每一帧的加速度鱼在密集区域会突然被弹开看起来像抽筋。第三处是速度下限没有下限的话鱼在努力平衡时会越游越慢直到几乎停住像飘在水里的一具标本。参数整定我磨了很久最后稳定下来的一套值是这样的你要抄可以直接用参数取值含义与调参方向r_sep26 px分离半径。太小会重叠太大会让鱼群散成一盘沙r_nb90 px感知半径。决定群体规模太大整缸鱼会变成一团w_sep1.6分离权重。抖得厉害就加大挤成一团就减小w_ali0.9对齐权重。决定鱼群是一起朝一个方向还是各游各的w_coh0.6聚合权重。太大整群鱼会缩成球太小会散到画面外max_force90 px/s²转向力上限直接决定动作的柔与急v_min / v_max45 / 130 px/s速度区间上下限比例控制在 3 倍以内最自然4.2 边界处理为什么软墙加硬夹紧比单纯推力好用边界是最容易翻车的地方。我的第一版是用一个反向的推力把鱼往回推力的大小跟越界深度成正比。问题是鱼群在角落里聚集时几条鱼互相推挤速度会瞬间叠加到很大下一秒就贴着边界疯狂抖动看起来像触电。后来改成软墙加硬夹紧的组合。软墙负责提前预判当鱼距离边界小于 80 像素时按 (80 - 距离) / 80 的比例施加回推力越近推力越大。硬夹紧负责兜底每帧结束前如果位置真的越界了直接把位置钳回边界内同时把垂直于边界的那一维速度分量乘以 -0.6 做一次衰减反弹。这样绝大多数鱼根本不会碰到硬边界触发的都是极少数情况抖动就消失了。还有一个细节是边界要按鱼的个体半径内缩而不是按画面边缘。因为鱼贴图不是质点有长度如果按画面边缘算鱼头会伸到画面外去。我把左右各内缩 45 像素上下各内缩 30 像素。4.3 用缩放和雾化骗出纵深透射式方案的硬伤是缺纵深但这在渲染层完全能补回来。我的做法是给每条鱼额外维护一个 z 值范围 0 到 1代表离观察者的远近。z 的变化做慢速随机游走每秒最多变化 0.1这样鱼会有慢慢游远游近的感觉。然后用两条映射把 z 转成视觉信号。第一条是缩放scale 0.55 0.45 × z也就是最远的鱼是最近那条的 55% 大小。第二条是雾化给每条鱼叠一层和背景同色的透明度alpha 120 135 × z最远的鱼只有一半出头的可见度。再加上绘制顺序按 z 从远到近排远处的鱼先画近处的后画覆盖上去遮挡关系也就对了。真正省性能的关键在这里绝对不要每帧对鱼的原图做缩放。pygame.transform.smoothscale是个性能杀手30 条鱼就能把树莓派拖到 20 帧。我的做法是启动时预先生成每一条鱼、每一帧动画、每一个缩放档位的贴图缓存。缩放档位取 8 档就够z 从 0 到 1 均分配合每帧动画缓存启动时多花 1.5 秒运行时直接 blitCPU 占用能降一半以上。雾化也不用逐像素处理用surface.set_alpha()就够用效果完全能接受。提示预生成缓存的时候注意内存。50 条鱼 × 8 帧动画 × 8 个缩放档位 3200 张贴图如果每张是 64×32 像素的带透明通道 Surface大约占 26MB 内存树莓派 2GB 完全扛得住。如果要做 200 条鱼就得把动画帧数降到 4 帧。4.4 帧率控制与状态机帧率我用固定步长的思路主循环里clock.tick(30)但传给物理更新的 dt 不是真实测量的时间而是钳制在 1/30 以内的固定值。为什么要这么绕因为如果某帧因为系统调度晚了 200 毫秒你用真实 dt 去更新鱼的位移会突然跳到 100 多像素外视觉上就是闪现。钳制之后最坏情况只是那一帧鱼走得慢一点肉眼几乎察觉不到。FIXED_DT 1.0 / 30.0 while running: raw_dt clock.tick(30) / 1000.0 dt min(raw_dt, FIXED_DT) # 关键钳制防止卡顿后瞬移 handle_events() if state STATE_AWAKE: update(boids, dt, cfg) render()状态机部分我用一个简单的四状态结构SLEEP、WAKING、AWAKE、INTERACT。每个状态有自己的持续时间和亮度目标状态之间靠事件驱动跳转。人体感应触发 SLEEP 到 WAKINGWAKING 用 1.2 秒把亮度爬到位再进 AWAKE人体感应超时就反向回到 SLEEP。INTERACT 是轻触触发的临时状态持续 1.5 秒期间给鱼群加一个从触点位置发散的瞬时冲击力鱼会炸开再慢慢聚回来——这个交互带来的反馈感比想象中强很多来我家的人都会忍不住多拍两下镜子。5. 常见问题与排查实录5.1 画面发灰、看不清鱼怎么系统排查这是最高频的问题而且大部分人排查方向是错的。正确的顺序是先量环境光再算对比度最后才怀疑硬件。拿手机装个照度计应用站在镜子前测一下当时的照度如果超过 300 lux那发灰是正常物理现象不是你接错线了。如果是夜间照度只有 80 lux 仍然发灰那依次查三件事。第一件是屏幕亮度是不是真的上去了有些驱动板默认 50% 亮度你没有从软件把它拉满等于白白损失一半透射光。第二件是半透镜装在框里的时候有没有把屏幕的透光区遮住我见过有人用了比屏幕大的半透镜边缘多出来的部分反射了框内的杂光整个画面就糊了一层。第三件才轮到怀疑镜面——用手机闪光灯贴在镜面上照如果能看到明显的漫反射雾圈说明亚克力表面被擦花了只能换板子。还有一种很迷惑的现象是画面出现细密条纹或者摩尔纹这不是质量问题是半透镜到屏幕的间距太近屏幕像素栅格和亚克力表面的微小纹理形成了干涉。把间距加大到 40 毫米以上就消失了。5.2 鱼群行为异常的三种典型症状症状一鱼群缩成一个球在画面中央打转。这是聚合权重过大的经典表现。所有鱼都被吸引向群体质心质心又跟着鱼群走形成正反馈。把 w_coh 从 0.6 降到 0.3同时把感知半径 r_nb 从 90 缩到 60问题立刻缓解。症状二鱼贴着边界高频抖动。这是边界推力和转向力上限不匹配。检查 max_force 是不是设得太大或者改用前面讲的软墙加硬夹紧组合。还有一个隐蔽原因如果你的鱼数量超过了 200 条边界区域的局部密度会很高分离力会把鱼往外推推出去又被边界推回来形成极限环。这时候要么减数量要么把边界内缩距离加大。症状三某几条鱼明显比别人快或者慢。检查速度限幅的归一化步骤里有没有除零保护。速度正好为 0 的鱼归一化时会算出 NaN然后这条鱼的位置就彻底废了表现为瞬移到画面外或者卡在原地。加一句把零速度的模长置为 1 就解决了。这个问题我在真机上复现了三次才发现因为随机初始化很少正好给出零速度但鱼在急刹车平衡的时候会出现极小的速度值四舍五入之后就是 0。5.3 硬件与系统层的速查表现象最可能的原因快速验证处理办法开机黑屏风扇在转HDMI 时序不匹配接普通显示器看有没有输出config.txt 加 hdmi_cvt 指定分辨率帧率周期性掉到 15CPU 过热降频读 /sys/class/thermal 温度加风扇或限频到 1.2GHz画面亮度自己变来变去光照传感器被屏幕漏光干扰遮住传感器看是否稳定传感器开孔朝前加遮光筒走近不唤醒人体红外灵敏度太低看 GPIO 电平日志调灵敏度旋钮或换毫米波模块走开后不熄屏传感器延时旋钮设太长观察指示灯把延时旋钮拧到最短逻辑交给软件触摸没反应电容触摸受框体金属干扰换位置测试触摸铜箔与金属框之间加绝缘垫系统重启后程序没起来服务依赖没配好看 systemd 日志加上重启策略和依赖声明系统自启我用 systemd 管理核心是配上重启策略这样哪怕图形驱动偶发异常把程序搞崩了也能自己拉起来[Unit] DescriptionMiroFish Service Aftermulti-user.target [Service] ExecStart/usr/bin/python3 /home/pi/mirofish/main.py WorkingDirectory/home/pi/mirofish EnvironmentSDL_VIDEODRIVERkmsdrm EnvironmentSDL_FBDEV/dev/fb0 Restartalways RestartSec3 Userpi [Install] WantedBymulti-user.target注意用 kmsdrm 这个视频驱动需要程序有访问 DRM 设备的权限。如果日志里报权限错误把用户加进 video 和 render 用户组或者改用 X 环境下的 x11 驱动。能不装桌面环境就别装Lite 版加 kmsdrm 开机只要 12 秒装了桌面要 35 秒以上。6. 后续我打算继续折腾的方向现在这版 MiroFish 挂在我家走廊尽头已经稳定跑了小半年没死过机也没再动过参数。但有几个方向我一直惦记着。第一个是多鱼种混养现在是单一鱼种我想加进底栖的虾和小体型的群游鱼让它们有不同的行为参数——虾基本不参与群集沿缸底随机爬这样画面层次会更丰富。实现上就是给每条鱼加一个 type 字段不同 type 走不同的参数子集和边界约束。第二个是基于摄像头的头部追踪视差。前面说过透射式天生没有视差但如果用树莓派摄像头识别人脸位置然后按观察者位置给整个鱼群做一次全局的水平像素偏移就能骗出鱼在缸里不同深度的感觉。代价是要额外跑一个轻量的人脸检测我担心会把 CPU 吃满得先用低分辨率加跳帧试试。第三个是季节和天气联动。接一个天气接口外面下雨的时候画面里加一点水面波纹和水色变化晚上下雪就换成冷色调。这是个典型的投入很小、观感提升很大的改动本质上只是几组调色参数和一个网络请求但对氛围感的作用非常明显。最后分享一个小经验跟技术无关但我觉得挺重要。我做完 MiroFish 之后最大的收获不是学会了 Boids 或者搞懂了半透镜的亮度模型而是想明白了一件事个人项目不需要追求白天晚上都完美。我在白天看不到鱼这件事上纠结了整整一周甚至一度想加钱换高亮屏后来放弃了。接受它的局限、把它的长处做到极致——晚上关掉主灯的那一刻走廊里那面镜子上有一群鱼在慢慢游这个画面值回了我所有的投入。功能上的不那么全能有时候反而是设计里最清醒的部分。