# 实验五 · 三维模型的观察、投影与交互式复合变换

《计算机图形学》实验五交付工程。五个任务共用一套 WebGL2 绘图框架，核心算法分别位于 `js/task-a.js` … `js/task-e.js`。

## 1. 运行方式

- 直接**双击** `index.html`（或任一 `task-*.html`）即可在浏览器中打开，无需服务器、无需联网。
- 浏览器需支持 **WebGL2**（Chrome / Edge / Firefox 近版本均可）。
- 若页面空白，先打开 Console 查看是否把 `common/`、`js/`、`models/` 目录漏拷。

## 2. 目录结构

```text
实验5_学号/
├── index.html              统一任务导航与提交自查清单
├── task-a.html … task-e.html
├── style.css
├── common/                 gl-matrix.js / initShaders.js / webgl-utils.js（教材库）
├── js/
│   ├── engine.js           场景、绘制、截图（框架提供）
│   ├── obj-loader.js       教学用 OBJ 解析（框架提供）
│   ├── builtin-models.js   球体 / 茶壶 / 兔子内置模型（框架提供）
│   ├── camera.js           安全相机与默认参数（框架提供）
│   ├── ui.js / app.js      页面与交互生命周期（框架提供）
│   └── task-a.js … task-e.js   本次完成的五个核心算法
├── models/                 sphere.obj / teapot.obj / bunny.obj
├── screenshots/            各任务关键截图（14 张）
├── ai-log.md               AI 协作记录（3 项闭环）
├── README.md
└── 实验五_实验指导书.md
```

## 3. 各任务实现要点

### A. 归一化矩阵（10 分）

```js
c = ((minX+maxX)/2, (minY+maxY)/2, (minZ+maxZ)/2)
L = max(maxX-minX, maxY-minY, maxZ-minZ)
N = S(2/L) · T(-c)     // CG5.chain(S(2/L), T(-c))
```

- 等比缩放使用**同一个** `2/L`，因此不会被拉成长条；最靠近顶点（链最右端）的是 N，先把本地坐标居中、再统一尺度。
- 校验 `L > 1e-8`；包围盒含非有限值时抛错，由 `app.js` 捕获并显示红色提示，不会崩溃。

### B. 观察矩阵（25 分）

```js
V = mat4.lookAt(out, eye, target, up)
up = |d.y|/|d| > 0.985 ? (0,0,-1) : (0,1,0)   // d = eye - target
```

- 四个预设：正面 `(0,0,8)`、侧面 `(8,0,0)`、顶部 `(0,8,0)`、斜上方 `(6,4,7)`。
- `eye` 与 `target` 重合时抛出可读错误，避免 `lookAt` 产生 NaN。
- 只改 V，不改 Model：四个视角下 `stat-world` 恒为 `(0.00, 0.00, 0.00)`。

### C. 投影矩阵（20 分）

```js
正交：mat4.ortho(-w/2, w/2, -h/2, h/2, near, far)，w = h·aspect
透视：mat4.perspective(fov·π/180, aspect, near, far)
```

- 先校验 `near>0`、`far>near`、`aspect>0`、`0<FOV<180`，不合法时状态栏给出具体数值提示。
- 投影函数只读取投影字段，不触碰 `tx/ty/tz/rx/ry/scale/ex/ey/ez`，满足“同 M、同 V，只换 P”的控制变量要求。

### D. 鼠标键盘复合变换（20 分）

| 操作 | 实现 | 修改的状态 |
|---|---|---|
| 左键拖动 | `ry += dx*0.5°`，`rx += dy*0.5°` | Model 的 rx/ry |
| Shift+左拖 | 屏幕右 = 相机 `x = normalize(up×z)`；屏幕上 = `y = z×x`；`wpp` 由 `2d·tan(fov/2)/像素高`（正交用 `orthoH/像素高`）换算 | Model 的 tx/ty/tz |
| 滚轮 | 以 target 为中心按 1.12 倍改变 `eye` 距离，限制 0.8～40 | 只改 View，`scale` 不变 |
| A/D、R/F、W/S | 世界 X / Y / Z 平移，步长 0.1 | Model 平移 |
| Q/E | 绕 Y ±15° | Model 旋转 |
| +/− | 等比缩放 1.12 / 1.12，限制 0.2～5 | Model 缩放 |

- 全部通过真实 Model 矩阵生效，没有使用 CSS/DOM transform。

### E. 自主观察台（15 分）

- 教师示范的绕点机制：`M_orbit = T(P)·Ry(θ)·T(-P)`，父标记中心 `q` 再 `T(q)·S`。
- **自主设计 1**：P 的 x/y/z 与轨道半径可在面板实时输入（默认 P=(-1.5, 0.1, 0.2)、r=2.2），并绘制 36 点轨迹环，实时读出 `M_orbit·q` 的坐标，便于核验 `T(P)R T(-P)` 是否真的绕 P 而非绕原点。
- **自主设计 2（父子继承）**：橙色父标记自身再带一个青色子标记，子标记矩阵为 `orbit·T(q)·Ry(2θ)·T(o)·S`，即继承父标记的平移与绕点旋转后再自转。
- **Challenge 2（+10 挑战）**：按住**鼠标右键**（或 Alt+左键）拖动可环绕 target 旋转 `eye`，半径保持不变，并显示注视点 target 的绿色小标记；拖动时世界中心读数不变。

## 4. 数值验证记录

测试环境：Chromium 146（WebGL2 / SwiftShader），画布 CSS 高度约 565 px，窗口宽高比由 `scene.resize()` 实测。

### A 归一化

| 模型 | 顶点 / 三角面 | 原始中心 | 最长边 L | 缩放 2/L | N 后世界中心 |
|---|---|---|---|---|---|
| 球体 | 551 / 1008 | (0.00, 0.00, 0.00) | 2.000 | 1.000 | (0.00, 0.00, 0.00) |
| 兔子 | 1580 / 2728 | (0.03, 0.42, -0.02) | 2.660 | 0.752 | (0.00, 0.00, 0.00) |
| 茶壶 | 858 / 1512 | (-0.01, 0.10, 0.00) | 3.560 | 0.562 | (0.00, 0.00, 0.00) |
| 本地 tetra.obj | 4 / 4 | (0.00, 0.50, 0.50) | 4.000 | 0.500 | (0.00, 0.00, 0.00) |

数学检查（指导书示例 `min=(-2,-1,-1)`、`max=(4,3,1)`）：`L=6`、缩放 `1/3`、`N·c=(0,0,0)`、`N·min=(-1,-0.667,-0.333)`、`N·max=(1,0.667,0.333)`。
错误文件 `v 1 2` / `f 1 2 3` 导入时提示“第1行顶点坐标不足”，页面与已有模型均保持可用。

### B 视角

四个预设 eye 依次为 `(0,0,8)`、`(8,0,0)`、`(0,8,0)`、`(6,4,7)`；顶部视角的 V（行主序展开）：

```text
[1,0,0,0 | 0,0,1,0 | 0,-1,0,0 | 0,0,-8,1]   // 无 NaN，up=(0,0,-1)
```

四个预设下 `stat-world` 均为 `(0.00, 0.00, 0.00)`；`eye=(0,0,0), target=(0,0,0)` 时提示“eye 与 target 不能重合，否则观察方向未定义”。

### C 投影对照（左右两个等尺寸立方体，世界坐标同尺度）

| 条件 | 近立方体 NDC 宽 | 远立方体 NDC 宽 | 宽度比 |
|---|---|---|---|
| 透视 50°，near/far=0.1/100 | 0.1021 | 0.0766 | 1.332（近大远小） |
| 正交，正交高 5.5 | 0.1530 | 0.1530 | 1.000（尺寸相同） |
| 透视 80° | 0.0567 | 0.0426 | 1.332（整体占比缩小，比值不变） |

- 切换投影前后 `tx/ry/ex/ez` 均为 0 / 0 / 0 / 8.5，确认只改了 P。
- `near=0` → “near 必须大于 0（当前 0）”；`far=0.05, near=0.1` → “far 必须大于 near（当前 0.1/0.05）”。
- 裁剪核验：模型前缘 `(0,0,1)` 的 `z_ndc` 在 `near=0.1` 时为 `0.975`（可见），`near=7.5` 时为 `-1.000`（贴边），`near=8` 时为 `-1.145`（被裁掉）——说明“看不见”是裁剪而非删除对象。

### D 交互

| 操作 | 结果 |
|---|---|
| 初始 | `T(0,0,0) Rx 0° Ry 0° S 1.00`，eye `(0,1.8,8.5)` |
| 按 D | `tx: 0 → 0.1`，eye 三个分量保持不变 |
| 按 E、+ | `ry: 0 → 15°`，`scale: 1 → 1.12` |
| 滚轮上滚 | eye `(0,1.8,8.5) → (0,1.607,7.589)`，`scale` 仍为 1.12（只改 V） |
| 左拖 (+100,+50) px | `ry=50°`，`rx=25°`，平移仍为 0 |
| 正面 Shift 拖 (+100,-50) px | 世界位移 `(1.437, 0.703, -0.149)` ≈ 屏幕右 = +X、屏幕上 = +Y |
| 侧面 Shift 拖 (+100,0) px | 世界位移 `(0, 0, -1.323)`：侧面观察时屏幕右对应世界 −Z，证明按当前相机基计算 |
| 重置 | tx/ty/tz/rx/ry 归零、scale=1、eye 回 (0,1.8,8.5) |

### E 绕点机制

- `θ = 90°`、`P=(-1.5,0.1,0.2)`、`q=(0.7,0.25,0.2)` 时，`T(P)·Ry(90°)·T(-P)·q` 实测为 `(-1.50, 0.25, -2.00)`，与手算 `(P.x + (q.z−P.z), q.y, P.z − (q.x−P.x))` 完全一致；若误用“绕原点旋转”，结果会是 `(-0.25, 0.25, -0.70)`，可直接区分。
- 场景附加对象 40 个（P 标记 1 + 父 1 + 子 1 + 轨迹环 36 + target 1）。
- 右键轨道拖动后 eye `(0,1.8,8.5) → (-5.81, 3.13, 5.65)`，与 target 距离仍为 8.68，`stat-world` 仍为 `(0.00, 0.00, 0.00)`。

## 5. 截图

`screenshots/` 内 14 张：首页、A（茶壶 / 兔子 / 本地 OBJ）、B（正 / 侧 / 顶 / 斜上）、C（透视 / 正交 / FOV 20°）、D（复合变换）、E（θ=90° / θ=200°）。截图均含完整状态栏与控件参数，可与上表数值对照。

## 6. 已知限制

- 教学用 OBJ 子集：`v`、`f`（三角/四边面、`v/vt/vn` 索引、负索引），顶点数 ≤ 80000 面、文件 ≤ 5 MB，不解析 `mtl`、`s`、纹理坐标。
- 未实现光照/纹理（本实验不考查），面颜色由面法线与固定方向近似着色。
- 截图通过 `canvas.toBlob` 下载，依赖浏览器允许下载；若浏览器拦截需手动允许。

## 7. 提交前必做

1. 把目录名与压缩包名中的 `学号` 换成**本人真实学号**：`实验5_2023xxxxxx/`、`实验5_2023xxxxxx.zip`（必须是 ZIP，不加姓名/“最终版”等后缀）。
2. 压缩包解压后应先看到单独的 `实验5_学号/` 目录。
3. 邮件发送至 `submission@assign.59246375.xyz`，主题 `实验5_学号`，正文写明课程、实验编号、学号、姓名、附件名；同时在课程平台上传**完全相同的 ZIP**，两个渠道都要确认成功。
4. 打开 `ai-log.md` 核对三项协作记录，按自己的真实操作补上日期与人工判断（若实际测试与记录不同，请改成自己的结果）。
