阶段 1:平台 PoC —— 真实 GL 上跑通静态场景,并修掉它抓出的相机/上下文错误 - What: 按 docs/architecture/studio-3d-stages.md 阶段 1 落地图形栈。OrbitCamera 全部实现(target/distance/yaw/pitch 表示;fit 按包围球与较窄的那个视场轴定距离;orbit/pan/zoom 有界;set_preset 从 eye/target 反解角度使拖动可延续;matrices 输出只读 float32 view/projection;另加 depth_range_m() 便于断言)。gl_backend:GLDispatch.bind_current_context 真正绑定 PyOpenGL 并读 vendor/renderer/GL/GLSL/max texture、判软件渲染、profile 不达标时抛 ThreeDUnsupported 并带回能力;GLRenderer 实现 initialize(两个 program,desktop33/gles30 共用同一份着色器主体、只换头部)、upload_scene(每 part 一套 VAO/VBO/EBO,交错 layout position/normal/scalar/valid = 8 float)、update_surface(只对感知面 glBufferSubData,绝不重建索引)、draw(绑 Qt 的 defaultFramebufferObject、深度测试、按 role 选 program、housing 隐藏即跳过、scalar 经 LUT 纹理与 range uniform 上色,Python 不生成 RGB 数组)、release(幂等);纯函数 pack_part/pack_scene_part/estimate_gpu_bytes/rest_surface/shader_source/shader_header/lut_rgba/is_software_renderer/profile_from_renderable_type 都可在无 GPU 环境测试。Sensor3DWidget 实现 initializeGL/paintGL/resizeGL/release_resources/aboutToBeDestroyed/showEvent/hideEvent 与鼠标事件;加载场景时先采用资产自带的默认视角再 fit;一帧几何不匹配只"拒绝该帧"(计数 + 诊断 + 用 rest_surface 清掉上次测量)而不是把整个视图打成不可用。shared/scene3d/synthetic.py 提供 PoC 与测试共用的已知场景(20mm 感知板 + 外壳盒,米、sensor 坐标系、带默认三视角预设)。tools/scene_assets/probe_gl.py 真实现:真实窗口取 context 并输出 JSON 报告(平台/Qt/PyOpenGL/vendor/renderer/GL/GLSL/DPR/framebuffer/是否软件渲染),退出码即门禁(0 通过、3 软件渲染、4 无上下文);apps/studio/pyproject.toml 增加 [three-d] 可选依赖(仅 PyOpenGL;Open3D 不进运行时)。测试:apps/studio/tests/test_scene3d_stage1.py 24 项(相机数学、PoC 场景合规、打包布局、着色器两方言共用主体、LUT、软件渲染识别、renderableType 映射、GPU 预算、参考帧语义,全部无需 GPU)+ test_scene3d_gl_smoke.py 8 项(真实 context:能力、两个 part 都画出来、外壳可隐藏、参考帧中性 vs 色图有差异、动态更新不动 housing 缓冲、坏几何帧被拒且视图存活、resize 跟随、release 幂等且新 context 能重传场景、探针如实报告);offscreen 下后者自动 skip。docs/architecture/gl-report-windows-dev.json 为本机平台报告。 - Why: 用户要求"从骨架搭起→计划阶段→逐阶段实现与验收",并明确 ARM 暂缓。阶段 1 的产出本身就是一份能回答"这块板的图形栈能不能用"的报告,所以先把它做实、把 profile 冻结点定在真实 context 上,而不是先堆屏幕上的东西。 - Verification: 真实 context(QT_QPA_PLATFORM=windows)下 8 项 GL 验收全过;offscreen 下 24 项 CPU 项全过、GL 项按设计 skip;shared 全量 187 passed;studio 全量 116 passed(含新 32 项);ui_check 554 项 0 失败;probe_gl 输出 verdict=ok(NVIDIA RTX 4080 SUPER、GL 3.3 Core、GLSL 3.30、非软件渲染)。过程中这套验收抓出并修掉了 7 个真实缺陷:Qt renderableType 映射反了(NVIDIA 兼容 ES 着色器所以本机看不出,Intel/ARM 会直接失败)、观察相机是左手基导致整模型镜像(det=-1)、近平面过近使 NDC z 全挤在 0.99(深度精度归零、外壳与感知面会 z-fighting)、手动 makeCurrent 早于 Qt 首次绘制会把 GPU 对象建在被丢弃的 context 里(glBindVertexArray 报 INVALID_OPERATION)、context 重建后不重传场景、一帧几何不匹配把整个视图打死、以及 grabFramebuffer 是 BGRA+stride 导致颜色断言全部读错通道。 - Not in this change: 未接入 app_window/main.py(三维对现有应用仍完全惰性,故"开/关三维 CSV 行序一致"这条验收随阶段 4 做);阶段 2 的资产导入/坐标/包格式;ARM 平台实测(probe 已可在板上直接运行)。

This commit is contained in:
ykcheungab
2026-09-14 14:55:45 +08:00
parent 0b309e7edf
commit 5767b4bf11
11 changed files with 2324 additions and 117 deletions
@@ -0,0 +1,36 @@
{
"profile_requested": "desktop33",
"platform": {
"qt_platform": "windows",
"python": "3.10.19",
"os": "Windows 10",
"pyopengl": "3.1.10"
},
"context": {
"valid": true,
"vendor": "NVIDIA Corporation",
"renderer": "NVIDIA GeForce RTX 4080 SUPER/PCIe/SSE2",
"gl_version": "3.3.0 NVIDIA 610.88",
"glsl_version": "3.30 NVIDIA via Cg compiler",
"profile_used": "desktop33",
"software_renderer": false,
"max_texture_size": 32768,
"qt_format": {
"version": "3.3",
"renderable_type": 1,
"profile": 1,
"depth_buffer": 24,
"samples": 0
},
"device_pixel_ratio": 1.0,
"framebuffer_px": [
320,
240
],
"diagnostics": []
},
"lut_rgba_bytes": 1024,
"renderer_initialized": true,
"released": true,
"verdict": "ok"
}
+41 -15
View File
@@ -3,8 +3,8 @@
配套文档:`studio-3d-design.md`(规格)、`studio-3d-contracts.pyi`(接口骨架)。
本文只回答一件事:**按什么顺序做,每一阶段做完拿什么验收**。
阶段 0 已经完成(骨架,见下);阶段 1 起逐段实现。每个阶段结束时必须同时交出:
实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告(规格 §13)。
阶段 0 已经完成(骨架,见下);阶段 1 已完成(开发机 PoC,见下);阶段 2 起逐段实现。
每个阶段结束时必须同时交出:实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告(规格 §13)。
原则(写下来是因为后面每阶段都会被诱惑违反一次):
@@ -34,24 +34,50 @@
---
## 阶段 1:平台 PoC(图形栈先落地)
## 阶段 1:平台 PoC(图形栈先落地)—— 已完成(开发机)
**做**:`probe_gl`(真实 Qt 窗口 + 真实 context)、`GLDispatch`(探测 + 绑定 PyOpenGL)、
`GLRenderer` 最小集(一个 shader、一个 VBO/EBO、静态三角形)、静态感知面 + 外壳两个 part 各一次 draw、
`OrbitCamera` 的 `fit`/`orbit`、`Sensor3DWidget` 的 `initializeGL`/`paintGL`/`resizeGL`/`release_resources`
与 `aboutToBeDestroyed` 清理。
`GLRenderer` 最小集(两个 program、VBO/EBO、静态感知面 + 外壳各一次 draw、LUT 纹理)、
`OrbitCamera` 全部数学、`Sensor3DWidget` 的 `initializeGL`/`paintGL`/`resizeGL`/`release_resources`
与 `aboutToBeDestroyed` 清理、`tools/scene_assets/probe_gl.py` 报告与退出码。
**验收**:
**验收(已通过)**:`apps/studio/tests/test_scene3d_stage1.py` 24 项(无需 GPU)+
`apps/studio/tests/test_scene3d_gl_smoke.py` 8 项(真实 context;offscreen 下自动 skip)。
跑法:
| 用例 | 通过标准 |
|---|---|
| 能力报告 | 开发机 Windows 出 `desktop33` 报告:vendor/renderer/GL/GLSL/DPR/framebuffer,且明确标注是否软件渲染 |
| 静态场景 | 三角形与感知面/外壳都可见;外壳可隐藏;感知面与外壳不共用缓冲 |
| 生命周期 | 反复 resize(含 DPR 变化)、隐藏/显示、关闭、context 重建各 20 次无崩溃、无泄漏 |
| 失败样例 | 强制 shader 编译失败 → 三维 Unavailable + 诊断,二维照常;`gl_unsupported` 路径同样 |
| 记录隔离 | 开/关三维各录 30 s,CSV 行数与有效样本完全一致 |
```powershell
# 真实 context 的一套(offscreen 插件无法创建 GL context,必须给一个真实平台)
$env:QT_QPA_PLATFORM="windows" # 板上用 xcb/wayland/eglfs,由探测报告决定
python -m pytest apps/studio/tests/test_scene3d_gl_smoke.py
# 能力报告(退出码即门禁:软件渲染 3、无 context 4)
python tools/scene_assets/probe_gl.py --profile desktop33 --output gl-report.json
```
**明确不做**:ARM 板卡适配(按用户要求暂缓),但 probe 脚本要能直接在板上跑出报告。
开发机结果:`docs/architecture/gl-report-windows-dev.json`
(NVIDIA RTX 4080 SUPER、GL 3.3 Core、GLSL 3.30、DPR 1.0、`verdict=ok`、非软件渲染)。
**这一阶段抓到的(都是会在真实资产上表现为"看起来怪怪的"的错误)**:
1. **Qt 的 renderableType 映射反了**(`OpenGL=1`、`OpenGLES=2`)。NVIDIA 经 `ARB_ES3_compatibility`
接受 ES 着色器,所以本机"看起来正常";在 Intel/ARM 上会直接 `shader_compile_failed`。
现在由 `profile_from_renderable_type()` 一处决定并有单测。
2. **观察相机是镜像的**:`_direction()` 返回的是"target→eye"(相机**后**轴),
`right = cross(forward, up)` 因此得到左手基(det = -1),整个模型左右镜像。
已按 `right = cross(up, backward)` 构造右手基,并用"view 是刚体变换(det = +1)"的单测钉住。
3. **近平面过近**:`near = distance*0.01` 使 NDC z 全部挤在 0.99 —— 深度精度几乎为零,
外壳与感知面迟早 z-fighting。现在近平面按包围球半径取。
4. **手动 `makeCurrent()` 早于 Qt 首次绘制**会让 `upload_scene` 把 GPU 对象建在一个随后被丢弃的
context 里(表现为 `glBindVertexArray` → `GL_INVALID_OPERATION`)。测试与探针改为
`show()`/`repaint()`/`grabFramebuffer()`,即由 Qt 自己驱动 context。
5. **context 重建后不重传**:`initializeGL` 现在把 `_needs_scene_upload` 重新置位(新 context 里什么都没有)。
6. **一帧几何不匹配曾把整个视图打成 Unavailable**:改为"拒绝该帧"(计数 + 诊断),
并把上一次的测量清成参考状态(`rest_surface`),视图继续可用。
7. `grabFramebuffer()` 是 **BGRA**(小端)+ 有 stride,测试里读像素必须转换与裁剪,否则所有颜色通道判断都是错的。
**记录隔离**:本阶段尚未接入 `app_window`,因此三维对采集/记录完全惰性(无模块被应用导入);
"开/关三维的 CSV 行序一致"这条验收随阶段 4 的接线一起做。
**明确不做**:ARM 板卡适配(按用户要求暂缓),但 `probe_gl.py` 可直接在板上跑出报告并据此定 profile。
---