From 2ed2ae21280d4115a78dedce2d316994b847379f Mon Sep 17 00:00:00 2001
From: Prism <103928586+Shuang-su@users.noreply.github.com>
Date: Fri, 9 Oct 2026 12:18:21 +0800
Subject: [PATCH 1/5] fix(director): align camera feedback and stabilize
optical coverage
Correct depth quantization, calibrate Fast defocus to the actual splat kernel, and align Operator, focus feedback and timeline edits with the observed reference. Add controlled optical diagnostics and retain completed frames by rendered state.
Validation: Director 36 tests, typecheck and build; current browser and four-scene optical evidence recorded.
Refs: MF-106, MF-62
---
docs/changes/106-acg-director/plan.md | 4 +
docs/changes/106-acg-director/spec.md | 6 +
docs/changes/106-acg-director/verification.md | 32 ++
metaflow-director/diagnostics/README.md | 18 +
metaflow-director/diagnostics/fixture.ts | 62 +++
metaflow-director/diagnostics/index.html | 44 +++
metaflow-director/diagnostics/optical.ts | 360 ++++++++++++++++++
.../diagnostics/thin-lens-reference.py | 107 ++++++
metaflow-director/src/App.tsx | 39 +-
metaflow-director/src/Operator.tsx | 10 +-
metaflow-director/src/VideoTimeline.tsx | 80 ++--
metaflow-director/src/camera-chrome.css | 137 +++++--
metaflow-director/src/core/model.ts | 52 +++
metaflow-director/src/render/optics.ts | 5 +
metaflow-director/src/render/runtime.ts | 23 +-
metaflow-director/src/render/session.ts | 12 +-
metaflow-director/src/render/stable-order.ts | 8 +-
.../tests/auxiliary-presentation.test.mjs | 2 +-
metaflow-director/tests/photography.test.mjs | 61 +++
.../tests/runtime-ownership.test.mjs | 36 ++
metaflow-director/tsconfig.json | 57 +--
metaflow-director/upstream/optics-patch.mjs | 5 +-
.../upstream/precision-patch.mjs | 4 +
23 files changed, 1021 insertions(+), 143 deletions(-)
create mode 100644 metaflow-director/diagnostics/README.md
create mode 100644 metaflow-director/diagnostics/fixture.ts
create mode 100644 metaflow-director/diagnostics/index.html
create mode 100644 metaflow-director/diagnostics/optical.ts
create mode 100644 metaflow-director/diagnostics/thin-lens-reference.py
diff --git a/docs/changes/106-acg-director/plan.md b/docs/changes/106-acg-director/plan.md
index f75b326b..0b5e1983 100644
--- a/docs/changes/106-acg-director/plan.md
+++ b/docs/changes/106-acg-director/plan.md
@@ -60,3 +60,7 @@
## 2026-10-09 调整阶段 Fast 反馈
按用户最新反馈,操作中允许恢复 Fast 高斯扩散。仅在正在调整相机或显示红色光学参考时采用当前高精度投影的一次扩散预览,辅助遮罩仍从中心清晰几何生成;保持上一帧直到新合成完成。释放后回到四样本起步的圆孔径累积,静止120ms后继续所选128/256/512。Fast不进入累积、最终缓存、照片或视频。状态标为“调整预览”,不标为已收敛;单独核验响应、核切换观感、缓存、取消与完整采样输出。这项用户修订取代此前操作中只用圆孔径的限定,不改变成片模型。
+
+## 2026-10-09 持续完成
+
+沿用隔离工作区,在`codex/acg-director-final-parity`从远端main `d724479f9b73f05d41ffdf391f3e7b7a78408622`继续。先保留近同深度与大场景的修改前图像/实际GPU顺序,再改用摄影浮点深度键并检查十次重复、同深度稳定性、四个固定场景、拾取和输出。随后逐控件记录参考来源、开始/途中/结束/反向/中断,修正具体差异;受控薄透镜、Spark、GPU与CPU对照分别说明判断边界。检查通过后按既有上线授权追加PATCH、PR、预览和生产回读;历史失败及源文件保持原状。
diff --git a/docs/changes/106-acg-director/spec.md b/docs/changes/106-acg-director/spec.md
index f1a1f387..eddc8371 100644
--- a/docs/changes/106-acg-director/spec.md
+++ b/docs/changes/106-acg-director/spec.md
@@ -27,6 +27,12 @@
Fast不混入孔径累积、不保存为已收敛缓存、不进入照片或视频。输出强制清除调整、原图与红色辅助状态。内部引擎辅助帧使用独立离屏表面,最终合成器负责可见画布。测试必须分别记录持续输入响应、松手后的模型切换观感、完整采样输出及取消恢复;不能把高斯近似的速度收益称为圆孔径物理一致性。
+## 2026-10-09 持续收尾验收
+
+按用户要求继续修正具体差异,直到本次限定摄影范围的功能、视觉和运动过程都有逐项证据。新增受控诊断:相差0.003单位的透明遮挡层与10万单位远景同时存在时,排序必须遵循实际浮点深度,不能因全场景范围量化成同一档。摄影路径继续复用上游GPU radix排序;正深度按浮点位模式排序,同深度按固定entry ID稳定排序。上游原生路径保留原始格式作对照。
+
+光学检查分别登记排序、尾部覆盖、孔径积分、颜色和资产自身缺陷;受控平面使用独立薄透镜光线参照。不得将扫描数据中本来缺失的面、发丝或残影列成可以通过增加采样恢复的实现项。逐状态验收限定于公开实际保留的控件及现有浏览器,已排除功能和未运行设备不充作已完成。
+
## 2026-10-05 输入反馈与上线修正
沿用候选摄影组件;修复透明 header 阻断 Operator/Compose 命中。AF/MF 点击对焦提示及渐进/收敛反馈在取景框外,比例菜单在数字前显示真实宽高形状;手动距离和 ƒ 图标对齐已固定来源尺寸。原图对比使用自有分栏符号,仅切换预览,不改变摄影参数/历史。
diff --git a/docs/changes/106-acg-director/verification.md b/docs/changes/106-acg-director/verification.md
index bfd77ccd..8fa091ab 100644
--- a/docs/changes/106-acg-director/verification.md
+++ b/docs/changes/106-acg-director/verification.md
@@ -305,3 +305,35 @@
**本轮通过范围:** 指定兴趣区域/Compose/Operator及对焦过程、最新已显示机位交接、精细度上下调、持续Fast反馈、相同状态缓存、指定取消/撤销/重试、六视口×两主题×PHOTO/VIDEO的原生DPR2几何与截图、三档本地照片及短动态4K、四镜头四转场实际输出、正式照片/视频和发布回读。DPR2原生截图修正了历史CDP合成捕获问题,原历史失败仍保留;当时错误截图不转记为通过。
**未通过或未验证:** 全部控件、胶片和反向/打断动画组合及主观音效没有穷举;单场景计时不是四场景性能矩阵,没有旧方案成对性能基准或独立GPU计时。Safari/Edge未运行,未下载Edge。严格发丝、透明层、强前景遮挡和源扫描残影继续独立登记;Fast高斯核与圆孔径核不同,操作结束后的焦外变化不能以更多采样或局部功能通过自动验收。人物/室内/Spark/CPU浮点和独立光线追踪完整矩阵本轮未重跑。公开产品范围不包含设备Frame,设备外壳统一孔径积分不由此交付。MF-106及MF-62保持Open,不把本次发布称为完整复刻或严格物理画质通过。
+
+
+## 2026-10-09 · 持续收尾:当前来源、控件与光学因果修正
+
+本轮从 `d724479f9b73f05d41ffdf391f3e7b7a78408622` 在隔离分支 `codex/acg-director-final-parity` 实施。源模型、索引和设置只读;不改动用户主工作区及5207服务。证据位于本工作区 `.codex-work/downloads/mf-106-parity-20261009/`,中文 `index.html` / `analysis-zh.md`、参数、哈希与原始帧均可复核。此前来源及失败记录保留。
+
+### 原因与实际修正
+
+- 固定当日 ui.camera JS/CSS 来源及哈希。Operator闭合102.3047×42、展开138.5156×42;主按钮、兴趣点图标及padding按实际计算值对齐。inactive清除按钮不再隐藏可聚焦。内联滑杆28px/读数12px与刻度动画对齐;紧凑滑杆适配保留。当前来源不显示胶片数字标签,本地同步隐藏视觉标签而保留语义。时间轴/机位字体12px;片段手柄2×18、选中/hover覆盖与分格核对。
+- 兴趣区临时摄影状态为清晰AF,退出恢复原完整MF;照片Compose以观察到的220ms曲线运行,视频生成后即时开始序列,不把原站240ms移动标记误当额外相机动画。选区取消/恢复精确、一次编辑一次撤销;临时相机不重置全场景取景。
+- 点击对焦标记生命周期从输入开始1850ms,不追加异步拾取延迟。最终169帧记录可见1833.6ms,中心最大漂移约0.000061px。AF有效点击转MF,Infinity/撤销及输入请求身份继续有效。Ctrl-wheel pinch实际将底部1×改成0.5×,距离/焦点不变,一次撤销恢复完整机位。
+- 新机位在当前机位后插入,按邻接状态求值/保留端点并均匀时序;超过两机位时允许删除已选端点。时长输入卸载不会触发blur:在关闭前明确提交,Enter提交、Escape取消,回到对应机位按钮。实际外部关闭后1秒、一次撤销3秒、Enter1秒、Escape取消0.25后仍1秒。
+- 成片缓存只取实际渲染状态:镜头名称/未生效转场/AF-MF标签/控件已解析元数据及旧清晰带不再令照片失效。机位、焦点、光圈、尺寸、采样、场景版本及生效转场继续失效。最终重复原图与成片往返:渲染批次64→64→64,cacheHits1→2→3。
+- 20位归一化深度键在包含10万单位远点的场景中,会把间距0.003的前后高斯合桶。摄影路径复用上游GPU radix,改正Float32深度位模式和固定entry ID稳定排序;原生上游模式不变。受控7点只改变排序后,前绿后红遮挡恢复;10次像素完全重复。
+- Fast真实径向核经过截断/重定基线,其方差为理想高斯的约0.8387。用解析积分与100k求积验证后校准新增协方差,面积透明度补偿不变。白点Fast方差139.31→165.81,GPU512为165.57;等效半径差-8.27%→+0.07%。孔径与焦点标定不变。高斯核尾部仍不同于圆孔径,校准不是算法等同。
+
+### 本轮实际验证
+
+- Director 36/36单元、类型及固定源构建通过;Viewer144/144、类型、lint/fmt此前本轮基线通过,版本记录修改后再次执行。新增重点覆盖机位编辑、径向核矩、缓存身份、独占捕获/缩略图/导出及取消。构建保留既有Framer Motion/use-client、浏览器external及大chunk提示,无新增构建错误。
+- 390×844、649×970、1010×971、967×971、1280×720、1101×1248,两主题×PHOTO/VIDEO,共24个实际本地状态与24个来源状态,DPR2;无横向溢出,light stage border=0,双行Capture完整42px不被包裹层裁切。Operator移动展开→亮色菜单→桌面恢复:opacity1、pointer-events auto,最终残留slider0。
+- 原始取景录帧150次调焦/342次光圈,无整幅黑/白帧;相邻取景平均亮度最大变化0.00223/0.00162。光圈及调焦红色辅助由独立清晰几何按离焦量产生,结束/取消清除,不进入缓存成片或输出。默认小孔径参考较弱并非缺失。
+- 缇宝主体+环境、1770×996/DPR2,连续5秒调焦32次请求27次显示,输入→显示中位191ms/p95 401ms;光圈33次请求33次显示,中位158ms/p95 359ms。最终值175/180ms到达;孔径标定未改。取证开销包含在墙钟计时,不称GPU时间、原生帧率或四场景性能基准。
+- 强前景17709点、斜面24341点、人物1347720点、室内3703902点,固定字节/机位/焦点/背景,重新生成清晰、Fast、GPU4/8/16/32/64/128/256/512、同源CPU128/256/512及Spark2.1.0清晰/Fast。四场景各10次GPU128完全重复;GPU512对CPU Float64最大线性差分别1.363e-6/1.486e-6/3.722e-6/4.500e-6,编码最大1灰阶。此项验证求和,不能替代遮挡物理参照。
+- 独立受控平面薄透镜:Cartesian孔径求积、不调用产品高斯排序/绘制/采样,并以圆盘边缘解析CDF核对。128网格12892条视线相对解析最大线性差0.00637/平均0.000469;离焦半径29.22px。解析参照不是SOG材料/发丝真值。
+- 实际UI建立动态机位/光圈后一秒1080p/128视频:30fps、30个不同解码帧、748273字节。三档1920×1080照片耗时6.707/12.795/25.109秒;三档真实3840×2160动态视频每档8个不同帧、30fps/0.266667秒,完成耗时133.156/284.989/474.444秒,逐帧时间、哈希与完整解码记录已保存。矩阵通过产品内部捕获/时间求值/编码器运行,区别于实际UI创建的一秒镜头。
+- 暂停预览与独立输出同状态1138×640/128重复10次,最大像素差0,无变动像素;动态视频另保留首/中/尾局部及完整解码。四类转场当前真实输出和取消后重试结果随后补录。
+
+### 范围和物理解释
+
+公开摄影已排除Frame/设备、项目库、在线服务;没有恢复这些功能。教程关闭不新增Show按钮,按用户范围保持,即使来源本次某个会话又显示该入口。源人物左侧破碎行人、灯带和地面残影在清晰及Spark清晰图中已存在;与实现排序错误分开登记。薄前景透出背景的一部分视线属于有限孔径覆盖,Fast高斯尾部与圆盘不同;不能因Spark更柔和判为同参数物理真值。扫描没有真实材料/折射及逐发丝表面,不能凭增加采样宣称恢复缺失信息。
+
+当前浏览器为macOS Codex IAB/WebGPU。IAB不支持原生touch-event注入,实际移动双指、Safari/Edge及主观声音/全部设备性能未运行;未下载Edge。几何/像素/动画过程与物理/资产边界分别登记,不以终态截图代替全部反向/中断状态,也不把公开范围外研究作为本次代码失败。
diff --git a/metaflow-director/diagnostics/README.md b/metaflow-director/diagnostics/README.md
new file mode 100644
index 00000000..06d316cc
--- /dev/null
+++ b/metaflow-director/diagnostics/README.md
@@ -0,0 +1,18 @@
+# Director 光学诊断(本地)
+
+此目录是 MF-106/MF-62 的因果验收入口,不是公开摄影 UI,也不加入 Vite 的发布入口。它使用产品 `CandidateSession`、固定 SuperSplat 3.4.2 来源及摄影补丁;参考站源文件不在本目录或发布包中。
+
+- `fixture.ts` 生成七点深度排序场景和单白点场景,不读取原资产。七点包括间距 0.003 的前绿后红高斯和 100000 单位远点;它能区分范围量化造成的排序错误与同深度稳定性。
+- `optical.ts` 固定机位、背景、孔径及只读模型 URL,保存清晰、Fast、4—512 孔径阶段。同源 CPU Float64 求和检查 GPU 累积;它不是独立光学真值,也不是不含读回的性能基准。
+- `thin-lens-reference.py` 使用独立 Cartesian 圆孔径求积及圆盘边缘分布的解析积分,检查受控不透明/0.5 透明平面。它不调用高斯加载、GPU 排序、产品孔径序列或绘制。模型内叠加高斯的透明度与物理表面参数不等价,不能将其当作扫描人物的逐像素真值。
+
+开发服务准备好只读 `/data/` 以及 `/fixture/depth-ramp.ply`、`/fixture/depth-layers.ply` 后,打开 `/diagnostics/index.html`。页面将诊断方法公开为 `window.__opticalParity`,供当前页面的开发者工具取证;生产入口没有此对象。先后运行各场景,不同时占用 GPU。
+
+独立参照运行方式:
+
+```sh
+python3 metaflow-director/diagnostics/thin-lens-reference.py \
+ --output .codex-work/downloads/director-thin-lens
+```
+
+需要 NumPy 和 Pillow。只输出局部图、参数与误差统计,不保存整套浮点样本或复制大模型。模型哈希、来源构建哈希、原始录帧、当前 Spark 对照、中文说明和公开附件索引见 MF-106 的唯一验证记录;历史来源与当前结果分别标识。
diff --git a/metaflow-director/diagnostics/fixture.ts b/metaflow-director/diagnostics/fixture.ts
new file mode 100644
index 00000000..2a18a52f
--- /dev/null
+++ b/metaflow-director/diagnostics/fixture.ts
@@ -0,0 +1,62 @@
+/** Generated, deliberately overlapping planes. No published model is changed. */
+export function depthFixture(point = false) {
+ const rows: number[][] = [];
+ const add = (
+ x: number,
+ y: number,
+ z: number,
+ color: number[],
+ alpha: number,
+ sigma: number[],
+ ) =>
+ rows.push([
+ -x,
+ -y,
+ z,
+ ...color.map((c) => (c - 0.5) / 0.28209479177387814),
+ Math.log(alpha / (1 - alpha)),
+ ...sigma.map(Math.log),
+ 1,
+ 0,
+ 0,
+ 0,
+ ]);
+ // Entry order is the reverse of the required far-to-near order. A 20-bit
+ // normalized key merges the two planes when a distant environment is present.
+ for (const x of point ? [] : [-1.5, 0, 1.5]) {
+ add(x, 0, 0, [0.06, 0.96, 0.12], 0.7, [0.33, 0.55, 0.001]);
+ add(x, 0, -0.003, [0.96, 0.06, 0.12], 0.7, [0.33, 0.55, 0.001]);
+ }
+ // Invisible in the view but legitimately part of the world clipping range.
+ if (point) add(0, 0, 2.5, [1, 1, 1], 0.95, [0.006, 0.006, 0.006]);
+ else add(0, 0, -100000, [0, 0, 0], 0.00001, [0.001, 0.001, 0.001]);
+ const names = [
+ "x",
+ "y",
+ "z",
+ "f_dc_0",
+ "f_dc_1",
+ "f_dc_2",
+ "opacity",
+ "scale_0",
+ "scale_1",
+ "scale_2",
+ "rot_0",
+ "rot_1",
+ "rot_2",
+ "rot_3",
+ ];
+ const header = new TextEncoder().encode(
+ `ply\nformat binary_little_endian 1.0\ncomment MF-106 close-depth diagnostic\nelement vertex ${rows.length}\n${names.map((n) => "property float " + n).join("\n")}\nend_header\n`,
+ );
+ const data = new Uint8Array(header.length + rows.length * 56);
+ data.set(header);
+ const view = new DataView(data.buffer);
+ let offset = header.length;
+ for (const row of rows)
+ for (const value of row) {
+ view.setFloat32(offset, value, true);
+ offset += 4;
+ }
+ return data.buffer;
+}
diff --git a/metaflow-director/diagnostics/index.html b/metaflow-director/diagnostics/index.html
new file mode 100644
index 00000000..f1200d9e
--- /dev/null
+++ b/metaflow-director/diagnostics/index.html
@@ -0,0 +1,44 @@
+
+
+
Director 光学逐因子诊断
+
+ Director 光学逐因子诊断
+ 仅用于本地验收,不进入公开发布包。原模型只读;各渲染路径顺序运行。
+
+
+ 正在初始化
+
+
+
diff --git a/metaflow-director/diagnostics/optical.ts b/metaflow-director/diagnostics/optical.ts
new file mode 100644
index 00000000..2b4311a7
--- /dev/null
+++ b/metaflow-director/diagnostics/optical.ts
@@ -0,0 +1,360 @@
+import { CandidateSession, type Pose } from "../src/render/session";
+import { ApertureGpu } from "../src/render/aperture-gpu";
+import { depthFixture } from "./fixture";
+import { linearToSrgb } from "../src/render/srgb";
+import { apertureDiameter } from "../src/render/optics";
+const canvas = document.querySelector("#view")!,
+ status = document.querySelector("#status")!;
+const pose: Pose = {
+ target: [0, 0, 0],
+ yaw: 0,
+ pitch: 0,
+ roll: 0,
+ distance: 7.5,
+ fov: 50,
+ focus: 7.5,
+ focusPoint: null,
+ blur: 30,
+ dof: true,
+ nearBlur: true,
+ optics: { model: "aperture-v1", apertureScale: 1.05 },
+};
+const records: Record<
+ string,
+ {
+ url?: string;
+ pose: Pose;
+ }
+> = {
+ depth: { pose },
+ point: { pose },
+ ramp: {
+ url: "/fixture/depth-ramp.ply",
+ pose: { ...pose, focus: 7.489091396331787 },
+ },
+ foreground: {
+ url: "/fixture/depth-layers.ply",
+ pose: {
+ ...pose,
+ target: [0, 0.03099998430814077, -0.011999998525721534],
+ distance: 7.390753505706454,
+ focus: 9.190753505706454,
+ optics: { model: "aperture-v1", apertureScale: 1.0347054907989037 },
+ },
+ },
+ portrait: {
+ url: "/data/ACG/AD05/260102%20165752%20AD05%20%E8%91%AC%E9%80%81%E7%9A%84%E8%8A%99%E8%8E%89%E8%8E%B2%20%E8%8A%99%E8%8E%89%E8%8E%B2%E8%BE%9B%E7%BE%8E%E5%B0%94/260102%20165752%20AD05%20Frieren.sog",
+ pose: {
+ ...pose,
+ target: [-3.3197491396347583, 0.7199845518052846, 2.0659918219468487],
+ yaw: -39.9599571463798,
+ pitch: 6.159995695157629,
+ distance: 1.041324084470951,
+ fov: 75,
+ focus: 6.202818393707275,
+ optics: { model: "aperture-v1", apertureScale: 0.14578537182593315 },
+ },
+ },
+ interior: {
+ url: "/data/SZMG/251217%20%E5%B9%BF%E7%94%B5%E5%A4%A7%E5%8E%A66%E6%A5%BC%E6%BC%94%E6%92%AD%E5%AE%A4/SZMG6L.sog",
+ pose: {
+ ...pose,
+ target: [9.126799157443136, 0.7043933191142095, 4.3351272620044705],
+ yaw: 47.99997320223851,
+ pitch: 4.600002942218457,
+ distance: 0.4909308878176911,
+ fov: 65,
+ focus: 16.044418334960938,
+ optics: { model: "aperture-v1", apertureScale: 0.06873032429447676 },
+ },
+ },
+};
+const s = await CandidateSession.create(canvas);
+s.background = "#000000";
+s.presentation = () => {};
+let current = pose;
+async function load(label: string) {
+ const record = records[label];
+ status.textContent = "正在加载 " + label;
+ const bytes = record.url
+ ? await (await fetch(record.url)).arrayBuffer()
+ : depthFixture(label === "point");
+ const hash = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))]
+ .map((x) => x.toString(16).padStart(2, "0"))
+ .join("");
+ await s.loadScene(
+ [{ name: record.url?.split("/").pop() ?? "depth.ply", bytes }],
+ record.pose,
+ );
+ current = structuredClone(record.pose);
+ status.textContent = `已加载 ${label} · ${s.splat.numSplats} 点 · ${hash}`;
+ return { hash, points: s.splat.numSplats, pose: current };
+}
+function image(frame: { width: number; height: number; pixels: Uint8Array }) {
+ const c = document.createElement("canvas");
+ c.width = frame.width;
+ c.height = frame.height;
+ c.getContext("2d")!.putImageData(
+ new ImageData(new Uint8ClampedArray(frame.pixels), c.width, c.height),
+ 0,
+ 0,
+ );
+ return c.toDataURL("image/png");
+}
+function moments(raw: Float32Array) {
+ let mass = 0,
+ x = 0,
+ y = 0,
+ xx = 0,
+ yy = 0;
+ for (let j = 0; j < 540; j++)
+ for (let i = 0; i < 960; i++) {
+ const w = raw[(j * 960 + i) * 4];
+ mass += w;
+ x += w * i;
+ y += w * j;
+ xx += w * i * i;
+ yy += w * j * j;
+ }
+ return {
+ mass,
+ x: x / mass,
+ y: y / mass,
+ varX: xx / mass - (x / mass) ** 2,
+ varY: yy / mass - (y / mass) ** 2,
+ };
+}
+async function fastMetrics() {
+ const png = await sharp(true);
+ return { png, moments: moments((await s.read()).raw as Float32Array) };
+}
+async function sharp(fast = false) {
+ s.legacyFast = fast;
+ await s.render({ ...current, dof: fast });
+ s.legacyFast = false;
+ const display = new ApertureGpu(s.device, 960, 540, true, true);
+ display.setSource(s.scene.camera.colorTarget.colorBuffer);
+ const frame = await display.read();
+ s.device.frameStart();
+ display.present();
+ s.device.frameEnd();
+ await s.device.wgpu.queue.onSubmittedWorkDone();
+ display.destroy();
+ status.textContent = fast ? "调整预览" : "清晰原片";
+ return image(frame);
+}
+async function stages(target = 512) {
+ s.cancel();
+ const results = [];
+ const cpu = new Float64Array(960 * 540 * 4);
+ let samples = 0;
+ for (const n of [4, 8, 16, 32, 64, 128, 256, 512].filter(
+ (x) => x <= target,
+ )) {
+ const started = performance.now();
+ await s.advance(current, 960, 540, n - samples, true, undefined, (raw) => {
+ for (let i = 0; i < raw.length; i++) cpu[i] += raw[i];
+ samples++;
+ });
+ const frame = await s.readAperture(),
+ linear = await s.readApertureLinear();
+ let max = 0,
+ mae = 0;
+ for (let i = 0; i < linear.length; i++) {
+ const d = Math.abs(linear[i] - cpu[i] / samples);
+ max = Math.max(max, d);
+ mae += d;
+ }
+ const cpuPixels = new Uint8Array(frame.pixels.length);
+ for (let i = 0; i < cpuPixels.length; i += 4) {
+ const a = cpu[i + 3] / samples;
+ for (let c = 0; c < 3; c++)
+ cpuPixels[i + c] = Math.round(
+ 255 *
+ Math.max(
+ 0,
+ Math.min(
+ 1,
+ linearToSrgb(cpu[i + c] / samples / Math.max(a, 0.000001)),
+ ),
+ ),
+ );
+ cpuPixels[i + 3] = Math.round(255 * a);
+ }
+ const encodedError = (flip: boolean) => {
+ let max = 0,
+ sum = 0;
+ for (let y = 0; y < 540; y++)
+ for (let x = 0; x < 960 * 4; x++) {
+ const d = Math.abs(
+ cpuPixels[(flip ? 539 - y : y) * 960 * 4 + x] -
+ frame.pixels[y * 960 * 4 + x],
+ );
+ max = Math.max(max, d);
+ sum += d;
+ }
+ return { max, mae: sum / cpuPixels.length };
+ };
+ const direct = encodedError(false),
+ flipped = encodedError(true),
+ flip = flipped.mae < direct.mae;
+ if (flip) {
+ const row = 960 * 4;
+ for (let y = 0; y < 270; y++) {
+ const a = cpuPixels.slice(y * row, (y + 1) * row),
+ other = (539 - y) * row;
+ cpuPixels.copyWithin(y * row, other, other + row);
+ cpuPixels.set(a, other);
+ }
+ }
+ results.push({
+ n,
+ ms: performance.now() - started,
+ cpuMax: max,
+ cpuMae: mae / linear.length,
+ cpuEncoded: flip ? flipped : direct,
+ cpuReadbackFlipY: flip,
+ moments: moments(linear),
+ camera: frame.cameraSnapshot,
+ timing: frame.timing,
+ png: image(frame),
+ cpuPng: n >= 128 ? image({ ...frame, pixels: cpuPixels }) : undefined,
+ });
+ status.textContent = `成片 ${n}/${target}`;
+ }
+ return results;
+}
+async function order() {
+ const r: any = s.scene.projectedSplatRenderer;
+ const n = (await r.splatCounter.read(0, 4, new Uint32Array(1), true))[0];
+ const ids = await r.material
+ .getParameter("sortedIndices")
+ .data.read(0, n * 4, new Uint32Array(n), true);
+ const cache = await r.cacheA.read(0, 0, r.cacheA.width, r.cacheA.height, {
+ immediate: true,
+ });
+ const f = new Float32Array(cache.buffer);
+ return {
+ near: s.scene.camera.near,
+ far: s.scene.camera.far,
+ entries: [...ids].map((id: number) => ({ id, depth: f[id * 4 + 1] })),
+ };
+}
+let job: any = null;
+function start(label: string) {
+ if (job?.state === "running") throw Error("诊断任务正在执行");
+ job = { label, state: "running", started: performance.now() };
+ void (async () => {
+ try {
+ const model = await load(label),
+ clear = await sharp(),
+ fast = await fastMetrics(),
+ frames = await stages(512);
+ job = {
+ ...job,
+ state: "complete",
+ ms: performance.now() - job.started,
+ model,
+ diameter: apertureDiameter(current.optics as any, current.blur),
+ clear,
+ fast,
+ frames,
+ };
+ } catch (error) {
+ job = { ...job, state: "failed", error: String(error) };
+ }
+ })();
+ return { label, state: job.state };
+}
+function repeatStart() {
+ if (job?.state === "running") throw Error("诊断任务正在执行");
+ job = {
+ label: "repeat",
+ state: "running",
+ started: performance.now(),
+ results: [],
+ };
+ void (async () => {
+ try {
+ for (const label of ["foreground", "ramp", "portrait", "interior"]) {
+ const model = await load(label);
+ let base: Uint8Array | null = null;
+ const runs = [];
+ for (let i = 0; i < 10; i++) {
+ const started = performance.now();
+ await s.accumulate(current, 960, 540, 128);
+ const frame = await s.readAperture();
+ const hash = [
+ ...new Uint8Array(
+ await crypto.subtle.digest("SHA-256", frame.pixels),
+ ),
+ ]
+ .map((x) => x.toString(16).padStart(2, "0"))
+ .join("");
+ let max = 0,
+ changed = 0;
+ if (base)
+ for (let k = 0; k < base.length; k++) {
+ const d = Math.abs(base[k] - frame.pixels[k]);
+ max = Math.max(max, d);
+ if (d) changed++;
+ }
+ else base = new Uint8Array(frame.pixels);
+ runs.push({ i, ms: performance.now() - started, hash, max, changed });
+ status.textContent = `重复性 ${label} ${i + 1}/10`;
+ }
+ job.results.push({ label, model, runs });
+ }
+ job = { ...job, state: "complete", ms: performance.now() - job.started };
+ } catch (error) {
+ job = { ...job, state: "failed", error: String(error) };
+ }
+ })();
+ return { state: job.state };
+}
+const api = {
+ session: s,
+ records,
+ load,
+ sharp,
+ stages,
+ order,
+ image,
+ fastMetrics,
+ start,
+ repeatStart,
+ job: () => job,
+ current: () => structuredClone(current),
+};
+Object.assign(window, { __opticalParity: api });
+status.textContent = "诊断已就绪";
+document
+ .querySelectorAll("button")
+ .forEach((button) => (button.disabled = false));
+document
+ .querySelector("#load")!
+ .addEventListener(
+ "click",
+ () =>
+ void load(
+ (document.querySelector("#scene") as HTMLSelectElement).value,
+ ).catch((e) => (status.textContent = String(e))),
+ );
+document
+ .querySelector("#sharp")!
+ .addEventListener(
+ "click",
+ () => void sharp().catch((e) => (status.textContent = String(e))),
+ );
+document
+ .querySelector("#fast")!
+ .addEventListener(
+ "click",
+ () => void sharp(true).catch((e) => (status.textContent = String(e))),
+ );
+document
+ .querySelector("#aperture")!
+ .addEventListener(
+ "click",
+ () => void stages(128).catch((e) => (status.textContent = String(e))),
+ );
diff --git a/metaflow-director/diagnostics/thin-lens-reference.py b/metaflow-director/diagnostics/thin-lens-reference.py
new file mode 100644
index 00000000..c66b7c67
--- /dev/null
+++ b/metaflow-director/diagnostics/thin-lens-reference.py
@@ -0,0 +1,107 @@
+"""Independent planar thin-lens reference; no splat loader, sort or renderer.
+
+This evaluates surface rays using a Cartesian pupil quadrature, deliberately
+independent of Director's Halton sequence. It establishes pupil footprint and
+occlusion trends for controlled planes, not physical truth for scanned SOGs.
+"""
+import argparse
+import json
+import math
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+
+def decode(c):
+ c = np.asarray(c, dtype=np.float64)
+ return np.where(c <= .04045, c / 12.92, ((c + .055) / 1.055) ** 2.4)
+
+
+def encode(c):
+ return np.where(c <= .0031308, 12.92 * c,
+ 1.055 * np.maximum(c, 0) ** (1 / 2.4) - .055)
+
+
+def pupil(grid, diameter):
+ # Midpoints of equal-area Cartesian cells clipped to a circle.
+ xy = (np.arange(grid) + .5) * 2 / grid - 1
+ x, y = np.meshgrid(xy, xy)
+ inside = x * x + y * y <= 1
+ return np.column_stack((x[inside], y[inside])) * diameter * .5
+
+
+def trace(grid, width=960, height=540):
+ origin_z = 7.390753505706454 - .011999998525721534
+ origin_y = .03099998430814077
+ focus = 9.190753505706454
+ diameter = 1.0347054907989037 * math.sqrt(1 / 1.4 ** 2 - 1 / 256)
+ focal = width / (2 * math.tan(math.radians(50) * .5))
+ # Local strip crop. A far blue plane and two thin front surfaces are known
+ # geometry; source Gaussian opacity is not substituted for surface opacity.
+ crop = (190, 230, 325, 326)
+ xx, yy = np.meshgrid(np.arange(crop[0], crop[2]) + .5,
+ np.arange(crop[1], crop[3]) + .5)
+ sx = (xx - width * .5) / focal
+ sy = -(yy - height * .5) / focal
+ front_depth = origin_z - 2
+ image = np.zeros((*xx.shape, 3), dtype=np.float64)
+ back = decode([.2, .24, .3])
+ red = decode([.9, .2, .12])
+ samples = pupil(grid, diameter)
+ for ax, ay in samples:
+ # Parallel pupil offsets with rays converging at the same focal plane.
+ hit_x = sx * front_depth + ax * (1 - front_depth / focus)
+ hit_y = origin_y + sy * front_depth + ay * (1 - front_depth / focus)
+ opaque = (np.abs(hit_x + 1.3) <= .011) & (np.abs(hit_y) <= 1)
+ # The .5 layer is an explicit physical surface parameter, not a claim
+ # about the aggregate opacity of many overlapping source Gaussians.
+ transparent = (np.abs(hit_x + 1.1) <= .011) & (np.abs(hit_y) <= 1)
+ alpha = np.where(opaque, 1., np.where(transparent, .5, 0.))
+ image += back[None, None, :] * (1 - alpha[..., None]) + red * alpha[..., None]
+ image /= len(samples)
+ radius = focal * diameter * .5 * abs(1 / front_depth - 1 / focus)
+ # In this crop rays cannot reach the top/bottom ends, so a closed-form
+ # circle marginal supplies an exact independent convergence reference.
+ radius_world = diameter * .5 * abs(1 - front_depth / focus)
+ def cdf(x):
+ u = np.clip(x / radius_world, -1, 1)
+ return .5 + (u * np.sqrt(np.maximum(0, 1 - u * u)) + np.arcsin(u)) / math.pi
+ def coverage(center):
+ return cdf(center + .011 - sx * front_depth) - cdf(center - .011 - sx * front_depth)
+ alpha = coverage(-1.3) + .5 * coverage(-1.1)
+ exact = back[None, None, :] * (1 - alpha[..., None]) + red * alpha[..., None]
+ return image, exact, {"grid": grid, "samples": len(samples), "crop": crop,
+ "horizontalFov": 50, "focalPixels": focal,
+ "focus": focus, "diameter": diameter,
+ "frontDepth": front_depth, "defocusRadiusPixels": radius}
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ previous = None
+ results = []
+ for grid in [32, 64, 128]:
+ image, exact, metadata = trace(grid)
+ metadata["exactMaxLinearDifference"] = float(np.max(abs(image - exact)))
+ metadata["exactMeanLinearDifference"] = float(np.mean(abs(image - exact)))
+ if previous is not None:
+ metadata["priorMaxLinearDifference"] = float(np.max(abs(image - previous)))
+ metadata["priorMeanLinearDifference"] = float(np.mean(abs(image - previous)))
+ pixels = np.round(np.clip(encode(image), 0, 1) * 255).astype(np.uint8)
+ Image.fromarray(pixels).save(args.output / f"thin-lens-{grid}.png")
+ results.append(metadata)
+ previous = image
+ Image.fromarray(np.round(np.clip(encode(exact), 0, 1) * 255).astype(np.uint8)).save(args.output / "thin-lens-exact.png")
+ (args.output / "thin-lens-reference.json").write_text(
+ json.dumps({"scope": "controlled opaque/.5 planar surfaces; not scanned Gaussian ground truth",
+ "sequence": "independent equal-area Cartesian disk quadrature",
+ "results": results}, indent=2))
+ print(json.dumps(results[-1]))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/metaflow-director/src/App.tsx b/metaflow-director/src/App.tsx
index 783368fd..77c5b9da 100644
--- a/metaflow-director/src/App.tsx
+++ b/metaflow-director/src/App.tsx
@@ -342,7 +342,7 @@ export function App() {
playing: current.current.playing,
original: current.current.original,
peaking: current.current.peaking,
- adjusting: current.current.adjusting,
+ adjusting: current.current.adjusting || animation.current !== null,
viewRevision: intent.current,
...options,
});
@@ -606,6 +606,8 @@ export function App() {
stopMotion();
const request = ++intent.current;
setFocusMark({ x, y, id: request });
+ // The marker lifetime starts at input, independently of asynchronous picking.
+ setTimeout(() => setFocusMark((m) => (m?.id === request ? null : m)), 1850);
try {
const hit = await runtime.current.pick(x, y);
if (request !== intent.current) return;
@@ -627,10 +629,6 @@ export function App() {
setError((e as Error).message);
} finally {
if (request <= intent.current) resume();
- setTimeout(
- () => setFocusMark((m) => (m?.id === request ? null : m)),
- 1850,
- );
}
};
const pointerDown = (e: PE) => {
@@ -812,7 +810,11 @@ export function App() {
dof: false,
blur: 0,
focusPoint: null,
- controls: { ...controlsFor(from), blurAmount: 0 },
+ controls: {
+ ...controlsFor(from),
+ blurAmount: 0,
+ focusMode: "auto",
+ },
}),
{ zoom: 70 },
);
@@ -853,20 +855,17 @@ export function App() {
commit(next);
end();
setLive(true);
- animateCamera(
- from,
- target,
- false,
- mode === "video"
- ? () => {
- setLive(false);
- setPlaying(true);
- if (tutorial === "compose" || tutorial === "final-compose")
- setTutorial("watch");
- }
- : undefined,
- 240,
- );
+ if (mode === "video") {
+ // The reference starts the generated sequence immediately. Its 240ms
+ // camera-moving flag is not an additional camera animation or delay.
+ setLive(false);
+ setPlaying(true);
+ if (tutorial === "compose" || tutorial === "final-compose")
+ setTutorial("watch");
+ } else {
+ // Match the observed frame transform's 220ms cubic-bezier transition.
+ animateCamera(from, target);
+ }
setStatus("Compose");
};
const interest = async (area: InterestArea, index: number) => {
diff --git a/metaflow-director/src/Operator.tsx b/metaflow-director/src/Operator.tsx
index 7ca1263f..e6a8aec8 100644
--- a/metaflow-director/src/Operator.tsx
+++ b/metaflow-director/src/Operator.tsx
@@ -28,6 +28,8 @@ export function Operator({
// Moving between the header and compact row remounts this component. With
// initial={false}, an already-open island has no enter animation to complete.
const [settled, setSettled] = useState(active);
+ const interestLabel =
+ interestCount > 0 ? "Interest points" : "Set interest points";
useEffect(() => {
if (!active || selecting) return;
const outside = (e: PointerEvent) => {
@@ -103,13 +105,13 @@ export function Operator({
- {shot.keys.some(
- (k, i) =>
- i > 0 &&
- i < shot.keys.length - 1 &&
- Math.abs(k.time - local) < 0.015,
- ) && (
+ {shot.keys.length > 2 && (