<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>技术 on 诺亚的小站</title><link>https://www.noahland.cn/categories/%E6%8A%80%E6%9C%AF/</link><description>Recent content in 技术 on 诺亚的小站</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 13 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.noahland.cn/categories/%E6%8A%80%E6%9C%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>雨夜便利店：一个 Three.js 单文件场景的写实化渲染重构</title><link>https://www.noahland.cn/p/rainy-store-render/</link><pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.noahland.cn/p/rainy-store-render/</guid><description>&lt;img src="https://www.noahland.cn/p/rainy-store-render/images/overview.jpg" alt="Featured image of post 雨夜便利店：一个 Three.js 单文件场景的写实化渲染重构" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;在线试玩 → &lt;a class="link" href="https://www.noahland.cn/rainy-store/" &gt;www.noahland.cn/rainy-store/&lt;/a&gt;&lt;/strong&gt;&#10;浏览器直接打开即可（推荐 Chrome/Edge + 独显），鼠标拖动环绕视角。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;&lt;img alt="雨夜便利店 全景" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="720" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.noahland.cn/p/rainy-store-render/images/overview.jpg" srcset="https://www.noahland.cn/p/rainy-store-render/images/overview_hu_e39c9b76ebcce27c.jpg 800w, https://www.noahland.cn/p/rainy-store-render/images/overview.jpg 1280w" width="1280"&gt;&lt;/p&gt;&#10;&lt;p&gt;雨天的深夜，街角亮着灯的便利店——这是我基于店长测试Agent产生的渲染项目修改而来：把一个 Three.js r184 的卡通风格单文件场景，重构成 PBR 写实管线，同时尝试保持：&lt;strong&gt;成品必须仍是单个 HTML 文件，零外部依赖，双击就能开&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="近景" class="gallery-image" data-flex-basis="402px" data-flex-grow="167" height="560" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.noahland.cn/p/rainy-store-render/images/closeup.jpg" srcset="https://www.noahland.cn/p/rainy-store-render/images/closeup_hu_ef3632f72c59fc32.jpg 800w, https://www.noahland.cn/p/rainy-store-render/images/closeup.jpg 940w" width="940"&gt;&lt;/p&gt;&#10;&lt;h2 id="从卡通到写实差距在哪"&gt;从卡通到写实，差距在哪&#10;&lt;/h2&gt;&lt;p&gt;店长测试产生的原场景（baseline）是 &lt;code&gt;MeshToonMaterial&lt;/code&gt; 四阶卡通渐变 + 描边后处理，可爱：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;baseline&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;目标&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;材质&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MeshToonMaterial + 顶点色&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;全 PBR（albedo/roughness/normal/ORM）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;阴影&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;完全没有&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;烘焙阴影贴图&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;后处理&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Outline + Bloom&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SSAO + SSGI + SSR + SSS + Bloom&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;纹理&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3 张 2048 Canvas 图集&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;全部程序化生成，禁止手绘位图&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;img alt="改造前的卡通风格 baseline" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="720" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://www.noahland.cn/p/rainy-store-render/images/baseline.jpg" srcset="https://www.noahland.cn/p/rainy-store-render/images/baseline_hu_a1637873836e7010.jpg 800w, https://www.noahland.cn/p/rainy-store-render/images/baseline.jpg 1280w" width="1280"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;改造前的卡通风格 baseline。图集资源全部复用，我仅修改了整条渲染管线。&lt;/em&gt;&lt;/p&gt;&#10;&lt;h2 id="几条关键技术决策"&gt;几条关键技术决策&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;吃「静态场景」的红利。&lt;/strong&gt; 光源位置 100% 静态，全场只有路灯闪烁强度和信号灯颜色两处动态 → 阴影贴图只烘焙一次，&lt;code&gt;renderer.shadowMap.autoUpdate = false&lt;/code&gt;，之后每帧零成本。几何也基本静态 → SSGI 走低频更新 + 时域累积 + 重投影，不在每帧全量 raymarch。这两条是在 4060 上跑得动 SSGI 的前提。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;所有画质 Pass 一律半分辨率。&lt;/strong&gt; SSAO / SSGI / SSR / SSS 全部 ≤ 0.5× 渲染，配套空间降噪；SSGI 另配时域累积。每帧全场景渲染次数硬约束 ≤ 2 次（GBuffer prepass + 主渲染）。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;纹理零手绘。&lt;/strong&gt; 没有额外的纯手绘材质贴图，优先级是「复用现有图集 → 公式程序化生成 → 参考公开技术实现」。Roughness/Metal/AO 由公式生成 ORM 图烘焙内联，法线贴图由高度函数 Sobel 求导生成，一切能离线烘焙的全离线。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;自适应降级而不是砍画质。&lt;/strong&gt; 沿用帧时长统计，超预算时按「关 SSGI 时域累积 → 降 renderScale → 砍雨粒子数」的顺序降；首帧黑屏时逐级降级（关 MSAA → 关 Bloom → 关 SSGI）再展示画面。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;反射的分面处理。&lt;/strong&gt; 路面和人行道分开各渲一张反射 RT，反射查询点按反射面高度修正，再配逐面几何剔除——根治了抬高面倒影错位和矮墙底面镜面错位这类经典 bug。&lt;/p&gt;&#10;&lt;h2 id="性能红线"&gt;性能红线&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;值&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;显卡&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RTX 4060 Laptop&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;帧率&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30–60 FPS（低于 30 不达标）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;主渲染分辨率&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;renderScale 0.75（可调）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;网络请求&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;场景本身不大——13 个 Mesh 承载整条街，Draw Call 个位数——压力全在屏幕空间管线上。真正难的不是把效果做出来，难的是在 30 FPS 的预算里把效果做出来。&lt;/p&gt;&#10;&lt;h2 id="为什么是单文件"&gt;为什么是单文件&#10;&lt;/h2&gt;&lt;p&gt;这是项目最有性格的约束：禁止 CDN、禁止 fetch、所有纹理运行时 Canvas 生成或构建期内联为 data URI，最终产物 &lt;code&gt;file://&lt;/code&gt; 双击即开。这样就构造出了一个可以随手发给朋友双击打开的雨夜。&lt;/p&gt;&#10;&lt;h2 id="后续"&gt;后续&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;代码整理后若店长同意，我会开源到 GitHub，届时补上链接&lt;/li&gt;&#10;&lt;li&gt;待办清单：SSS 改进、雨滴打在玻璃上的折射、低配模式更多档位&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果你打开 demo 觉得卡，&lt;code&gt;?q=low&lt;/code&gt; 可以切低配。我自己手机反正是没抗住。&lt;/p&gt;&#10;</description></item><item><title>熊导游：一个按键就能唤起 AI 讲解的景区导览设备</title><link>https://www.noahland.cn/p/travelbear-ai-guide/</link><pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.noahland.cn/p/travelbear-ai-guide/</guid><description>&lt;p&gt;最近和几个同学做了一个叫「熊导游」（TravelBear）的项目：一台树莓派 4B 上的 AI 景区导览设备。按一下按键，摄像头拍下眼前的展品，定位模块告诉它你在哪，云端大模型生成一段讲解，然后用语音播报出来。&lt;/p&gt;&#10;&lt;p&gt;整个流程听起来简单，真要把每一环都做实，还是踩了不少坑。项目开源在 &lt;a class="link" href="https://github.com/RichLi4307/Travelbear" target="_blank" rel="noopener"&#10; &gt;GitHub&lt;/a&gt;，这里记录几个最有意思的技术决策。&lt;/p&gt;&#10;&lt;h2 id="整体架构"&gt;整体架构&#10;&lt;/h2&gt;&lt;p&gt;设备端五个模块：定位、识图、LLM、TTS、按键状态机。主程序用有限状态机控制状态流转，&lt;code&gt;trigger()&lt;/code&gt; 是按键入口，支持打断——讲解员讲到一半再按一下就能换题。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;识图&lt;/strong&gt;：Qwen-VL-Max，置信度低于 0.05 的画面直接丢弃，不让模型瞎编&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;LLM&lt;/strong&gt;：阿里云 qwen-plus，实测 2.4 秒生成 127 字讲解，语速刚好&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;TTS&lt;/strong&gt;：小米 MiMo，云端兜底，实测尾音完整&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;ASR&lt;/strong&gt;：本来想做语音问答，冻结了——设备定位是「一按一讲」，先把核心体验做扎实&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="最难的问题展厅里-gps-不可信"&gt;最难的问题：展厅里 GPS 不可信&#10;&lt;/h2&gt;&lt;p&gt;室外定位用的是 NEO-M8N GPS 模块，本地围栏两档判定（景区级宽松、点位级严格），未命中再走高德在线反解兜底。这套方案在室外工作得很好——实测 9 星、HDOP 2.87，准确命中「上海大学（宝山校区）」。&lt;/p&gt;&#10;&lt;p&gt;但展厅里完全是另一回事：GPS 会漏进弱 fix，漂移几十米，青铜器和陶瓷的讲解能讲串。&lt;/p&gt;&#10;&lt;p&gt;我们的解法是给每个展位布一个 ESP32-C3 的 iBeacon 蓝牙信标，树莓派板载蓝牙扫描 RSSI，中值滤波加阈值过滤后映射到展位。关键是&lt;strong&gt;仲裁策略&lt;/strong&gt;：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;BLE 信标新鲜命中无条件最高优先——信标固定在展位的米级物理证据前，GPS（包括点位级）不插嘴；信标信号消失 10 秒后才落回 GPS。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;这个策略有 9 条行为测试锁死（&lt;code&gt;test_ble_priority.py&lt;/code&gt;），消除了展厅内 GPS 漏入导致的讲解串味。&lt;/p&gt;&#10;&lt;h2 id="状态"&gt;状态&#10;&lt;/h2&gt;&lt;p&gt;截至 2026-09-28，硬件全链路已经实测闭环：GNSS 室外定位、BLE 室内定位、识图、LLM、TTS、按键热键、服务锁、打断全部通过真实验收。剩下的是现场工作：信标进馆布点、阈值标定和供电方案。&lt;/p&gt;&#10;&lt;p&gt;从「按一下键」到「听见讲解」，全链路延迟压在可接受范围内——对一个靠电池和 4G 跑的设备来说，每一毫秒的云端往返都是省出来的。&lt;/p&gt;&#10;</description></item><item><title>给树莓派 3.5 寸屏写了个 Rust 系统仪表盘</title><link>https://www.noahland.cn/p/pi-dashboard-rust/</link><pubDate>Sun, 20 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.noahland.cn/p/pi-dashboard-rust/</guid><description>&lt;p&gt;之前给树莓派 4B 买外壳时，顺手给配了一块微雪 3.5 寸 TFT 屏。最初简简单单用 Python 写了个系统仪表盘,显示 CPU、温度、内存、磁盘、IP、Docker 容器和 Tailscale 状态，但监控的数据越来越多，很快摸到性能天花板，最后我不得不用 Rust 重写了整个东西（&lt;a class="link" href="https://github.com/RichLi4307/pi-dashboard" target="_blank" rel="noopener"&#10; &gt;pi-dashboard&lt;/a&gt;，v0.2.2）。&lt;/p&gt;&#10;&lt;h2 id="为什么-python-不够"&gt;为什么 Python 不够&#10;&lt;/h2&gt;&lt;p&gt;起初我只是计划做一个0.5FPS的监控屏幕程序，后面发现监控速度太慢，很多程序都是脉冲式的负载。为了更客观反应小派的实际硬件状态，我打起了rust的主意。&lt;/p&gt;&#10;&lt;h2 id="rust-版做额外做的几件事"&gt;Rust 版做额外做的几件事&#10;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;脏区刷新&lt;/strong&gt;。核心思路是每次渲染都先保留上一帧画面，只把画面里变化的矩形区域写入帧缓冲。静止状态下 SPI 写入量远小于全屏，动态画面下的流畅度立刻不一样了。为此自己实现了 RGB565 帧缓冲层和一套几何原语。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;字体渲染&lt;/strong&gt;。因为要监测的数据很多，但屏幕太小，字体必须和像素足够对齐才能保证清晰度。为此我写了一个字体级 baseline 锚定的文本引擎，现在多行混排稳定得多。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;SPI 超频到 48MHz&lt;/strong&gt;。微雪屏官方建议频率偏保守，考虑到我的脏区刷新的平均带宽需求并不大，所以树莓派和这块屏的配合在 48MHz 下稳定，主循环跑到 15 FPS。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;裸解析触摸事件&lt;/strong&gt;。彻底重写触摸协议和校准方式。现在不依赖任何触摸框架，直接读 &lt;code&gt;input_event&lt;/code&gt;,在保持轻量的同时保持了可控性。现在支持点击容器列表翻页，但是重启按钮还是有点难按。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;双通道指标采集&lt;/strong&gt;。考虑不同监控指标的变化速度差异显著，我让CPU 这类变得快的指标走快通道，磁盘、IP 这类变得慢的走慢通道,同时保证帧率与低功耗。&lt;/p&gt;&#10;&lt;h2 id="最有意思的部分让聊天机器人也能看屏"&gt;最有意思的部分：让聊天机器人也能看屏&#10;&lt;/h2&gt;&lt;p&gt;仪表盘内置了一个 Unix Domain Socket 的 IPC server，配套写了 &lt;a class="link" href="https://github.com/RichLi4307/pi-dashboard-mcp" target="_blank" rel="noopener"&#10; &gt;pi-dashboard-mcp&lt;/a&gt;——一个 MCP server，让 AstrBot（我部署的聊天机器人）可以直接查询系统状态、截屏、切换显示模式。&lt;/p&gt;&#10;&lt;p&gt;也就是说，机器人不用问人「服务器现在什么状态」，它自己看一眼屏幕就知道了。&lt;/p&gt;&#10;&lt;h2 id="尾巴"&gt;尾巴&#10;&lt;/h2&gt;&lt;p&gt;这个仪表盘是树莓派时代的遗物——2026 年 9 月底我把整套服务迁到了一台小新笔记本上，树莓派正式退役。项目代码和这段折腾的记录都留在 GitHub 和本站里，权当纪念。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;树莓派退役之后这块屏我还留着。虽然有些硬件退役了，但它教会我的东西会一直跟着我。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>