阶段 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:
@@ -0,0 +1,168 @@
|
||||
"""A known scene, in metres, in the sensor frame — for the PoC and for tests.
|
||||
|
||||
Not an asset pipeline and not shipped to customers: it is the geometry the
|
||||
platform PoC draws and the CPU tests reason about, so "the surface is at z≈0 and
|
||||
the housing sits behind it" is a fact both can rely on instead of re-deriving
|
||||
triangles in every test.
|
||||
|
||||
Two parts, as the design requires of every scene: a sensing plate (dynamic) and a
|
||||
housing box (static, hideable). Coordinates are right-handed metres; the plate
|
||||
faces +Z, so a normal check has an unambiguous answer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
CameraPreset,
|
||||
ExpectedGeometry,
|
||||
FrameIdentity,
|
||||
Material,
|
||||
MeshPart,
|
||||
SceneAssets,
|
||||
SurfaceBinding,
|
||||
freeze,
|
||||
)
|
||||
|
||||
#: The plate is 20 mm x 20 mm, sampled 3x3: small enough to read in a test, big
|
||||
#: enough to have an interior vertex that a bilinear tap can move.
|
||||
PLATE_HALF_M = 0.01
|
||||
GRID = 3
|
||||
|
||||
|
||||
def _plate() -> MeshPart:
|
||||
step = (2.0 * PLATE_HALF_M) / (GRID - 1)
|
||||
positions = np.zeros((GRID * GRID, 3), dtype=np.float32)
|
||||
for row in range(GRID):
|
||||
for col in range(GRID):
|
||||
positions[row * GRID + col] = (
|
||||
-PLATE_HALF_M + col * step,
|
||||
-PLATE_HALF_M + row * step,
|
||||
0.0,
|
||||
)
|
||||
normals = np.tile(np.array([0.0, 0.0, 1.0], dtype=np.float32), (GRID * GRID, 1))
|
||||
triangles = []
|
||||
for row in range(GRID - 1):
|
||||
for col in range(GRID - 1):
|
||||
a = row * GRID + col
|
||||
b = a + 1
|
||||
c = a + GRID
|
||||
d = c + 1
|
||||
triangles.append((a, b, d))
|
||||
triangles.append((a, d, c))
|
||||
return MeshPart(
|
||||
part_id="sensing_surface",
|
||||
role="sensing",
|
||||
positions_sensor_m=freeze(positions, dtype=np.float32),
|
||||
normals_sensor=freeze(normals, dtype=np.float32),
|
||||
triangles=freeze(np.asarray(triangles, dtype=np.uint32), dtype=np.uint32),
|
||||
material=Material((0.85, 0.85, 0.88, 1.0), "unlit_scalar", True),
|
||||
lod_id="poc",
|
||||
)
|
||||
|
||||
|
||||
def _housing() -> MeshPart:
|
||||
"""An open box sitting behind the plate: static, grey, never measured."""
|
||||
h = PLATE_HALF_M * 1.6
|
||||
depth = PLATE_HALF_M * 0.8
|
||||
corners = np.array(
|
||||
[
|
||||
(-h, -h, -depth), (h, -h, -depth), (h, h, -depth), (-h, h, -depth),
|
||||
(-h, -h, 0.0), (h, -h, 0.0), (h, h, 0.0), (-h, h, 0.0),
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
faces = [
|
||||
(0, 1, 2), (0, 2, 3), # back
|
||||
(4, 6, 5), (4, 7, 6), # front, wound the other way
|
||||
(0, 4, 5), (0, 5, 1), # bottom
|
||||
(3, 2, 6), (3, 6, 7), # top
|
||||
(0, 3, 7), (0, 7, 4), # left
|
||||
(1, 5, 6), (1, 6, 2), # right
|
||||
]
|
||||
normals = np.zeros_like(corners)
|
||||
for triangle in faces:
|
||||
a, b, c = (corners[index] for index in triangle)
|
||||
normal = np.cross(b - a, c - a)
|
||||
length = float(np.linalg.norm(normal))
|
||||
if length > 0:
|
||||
normal = normal / length
|
||||
for index in triangle:
|
||||
normals[index] += normal
|
||||
lengths = np.linalg.norm(normals, axis=1, keepdims=True)
|
||||
normals = normals / np.where(lengths > 0, lengths, 1.0)
|
||||
return MeshPart(
|
||||
part_id="housing",
|
||||
role="housing",
|
||||
positions_sensor_m=freeze(corners, dtype=np.float32),
|
||||
normals_sensor=freeze(normals.astype(np.float32), dtype=np.float32),
|
||||
triangles=freeze(np.asarray(faces, dtype=np.uint32), dtype=np.uint32),
|
||||
material=Material((0.25, 0.26, 0.28, 1.0), "diffuse", True),
|
||||
lod_id="poc",
|
||||
)
|
||||
|
||||
|
||||
def _binding() -> SurfaceBinding:
|
||||
"""Identity binding: each plate vertex samples its own field cell."""
|
||||
count = GRID * GRID
|
||||
indices = np.full((count, 4), -1, dtype=np.int32)
|
||||
weights = np.zeros((count, 4), dtype=np.float32)
|
||||
for index in range(count):
|
||||
indices[index, 0] = index
|
||||
weights[index, 0] = 1.0
|
||||
return SurfaceBinding(
|
||||
part_id="sensing_surface",
|
||||
geometry_id="poc-geometry",
|
||||
field_domain_id="poc-domain",
|
||||
shape_hw=(GRID, GRID),
|
||||
sample_indices=freeze(indices, dtype=np.int32),
|
||||
sample_weights=freeze(weights, dtype=np.float32),
|
||||
)
|
||||
|
||||
|
||||
def poc_scene() -> SceneAssets:
|
||||
"""The synthetic scene: one sensing plate, one housing box, one preset."""
|
||||
plate = _plate()
|
||||
housing = _housing()
|
||||
points = np.concatenate(
|
||||
[np.asarray(plate.positions_sensor_m), np.asarray(housing.positions_sensor_m)]
|
||||
)
|
||||
presets = (
|
||||
# The asset's own default view: a three-quarter view of the plate's face,
|
||||
# with the housing visible behind it. Face-on would hide the depth, and
|
||||
# looking along Y would show the plate edge-on.
|
||||
CameraPreset(
|
||||
"default",
|
||||
(0.03, -0.03, 0.06),
|
||||
(0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 1.0),
|
||||
0.6,
|
||||
),
|
||||
CameraPreset("front", (0.0, 0.0, 0.08), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), 0.6),
|
||||
CameraPreset("side", (0.08, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 1.0), 0.6),
|
||||
CameraPreset("top", (0.0, 0.0, 0.09), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), 0.6),
|
||||
)
|
||||
return SceneAssets(
|
||||
asset_id="poc-scene",
|
||||
asset_version="0.0.0",
|
||||
asset_hash="poc-hash",
|
||||
geometry_id="poc-geometry",
|
||||
parts=(plate, housing),
|
||||
binding=_binding(),
|
||||
camera_presets=presets,
|
||||
bounds_sensor_m=freeze(
|
||||
np.stack([points.min(axis=0), points.max(axis=0)]).astype(np.float32),
|
||||
dtype=np.float32,
|
||||
),
|
||||
field_domain_id="poc-domain",
|
||||
shape_hw=(GRID, GRID),
|
||||
)
|
||||
|
||||
|
||||
def poc_identity(*, generation: int = 1) -> FrameIdentity:
|
||||
return FrameIdentity("poc", 1, 0, 0.0, generation=generation)
|
||||
|
||||
|
||||
def poc_expected() -> ExpectedGeometry:
|
||||
return ExpectedGeometry("finger", "poc", "poc-geometry", "poc-domain", (GRID, GRID))
|
||||
Reference in New Issue
Block a user