AI 协作日志
学生 Web 实验作品平台
学号:202412211401001 · 来源:ai-log.md
# 实验三 AI协作与核验记录
AI工具:豆包(Doubao),用于讨论矩阵次序、排查动画漂移问题与比较截图实现方案。
## 协作1:矩阵与变换次序(Task B)
- **我的目标和预先预测**:
在点 C=(0.50, 0.25, 0) 处放置一个正方形,分别用 Rz(45°)·T(C) 和 T(C)·Rz(45°) 计算其中心位置。
我预测:
- `R·T(C)`:先把正方形平移到 C,再绕世界原点旋转 45°,所以正方形中心会从 C 旋转到一个新的位置(公转),不再停在 C。
- `T(C)·R`:先在局部原点旋转,再平移到 C,所以正方形中心仍在 C,只是自身姿态变了(自转)。
数值上我手算了 C 旋转 45° 后的坐标:x'=0.5·cos45°−0.25·sin45°≈0.177,y'=0.5·sin45°+0.25·cos45°≈0.530。
- **关键提示词(原文)**:
“WebGL 列向量约定下,模型矩阵 M = R·T 与 M = T·R 有什么区别?为什么 R·T(C) 会让图形绕世界原点公转,而 T(C)·R 会让图形绕自身中心自转?请用齐次矩阵乘法的‘右侧先作用于列向量’规则解释。”
- **AI建议摘要**:
AI 确认了列向量约定下,矩阵乘法是从右往左作用于顶点:`M·p = A·(B·p)` 表示先做 B 再做 A。
- `R·T(C)·p`:先 `T(C)` 把局部原点平移到世界坐标 C,再 `R` 把这个已平移的位置绕世界原点旋转 → 中心轨迹是绕原点的圆弧(公转)。
- `T(C)·R·p`:先 `R` 在局部坐标系内旋转顶点,再 `T(C)` 把整个旋转后的图形搬到 C → 中心始终在 C,只有姿态变了(自转)。
- **我的计算或测试方法**:
用 Node.js 直接调用实现好的 `transforms.js`,对局部原点 (0,0,0,1) 分别乘以两个矩阵:
```js
const Mw = multiply(R('Z',45°), T(0.5,0.25,0)); // world pivot
const Ms = multiply(T(0.5,0.25,0), R('Z',45°)); // self pivot
transformPoint(Mw,[0,0,0,1]);
transformPoint(Ms,[0,0,0,1]);
```
- **实际结果与证据**:
- `R·T(C)` 作用于局部原点得到 `(0.177, 0.530, 0, 1)`,与我手算完全一致 → 中心离开了 C,公转成立。
- `T(C)·R` 作用于局部原点得到 `(0.500, 0.250, 0, 1)`,仍在 C → 自转成立。
- 在 task-b.html 页面切换“世界原点 / 对象自身中心”下拉框,肉眼确认:world 模式下正方形绕画布中心划出圆弧,self 模式下正方形在原地转动。
- **采纳、拒绝或修正的结论**:
完全采纳 AI 解释,并在 `bItem()` 与 `modelFor()` 中按此规则实现:world 用 `multiply(R, T(c))`,self 用 `multiply(T(c), R)`。我自己的手算预测与运行结果一致,说明矩阵列主序存储和乘法次序没有被 gl-matrix 的列主序格式搞反。
---
## 协作2:故障诊断与修复——三角形往复平移漂移(Task C)
- **错误现象、触发步骤**:
我最初实现三角形往复平移时,第一反应是“每帧把当前位置加上 dx”,即:
```js
obj.curX += obj.dxPerFrame; // 逐帧累加
```
运行几个周期后,三角形没有回到初始中心,而是整体慢慢向某个方向偏移(漂移)。即使最后反向,也回不到原点。
- **我先检查了什么**:
1. 先看 `amplitude` 和 `directionDeg` 有没有被错误地每帧重新读取;
2. 再看 sin 函数的相位有没有写反;
3. 最后意识到问题出在“位移基准”——我用了一个不断变化的 `curX` 作为累加起点。
- **关键提示词(原文)**:
“WebGL 动画中让一个三角形沿 X 轴在 [−0.3, +0.3] 之间往复运动。如果每帧执行 `currentX += dx`,为什么运行几个周期后会逐渐漂移?正确做法应该怎样基于时间计算位置?”
- **AI列出的候选原因**:
1. **浮点累加误差**:每帧加上一个小浮点数,几百帧后误差累积,且反向时误差不会抵消,导致整体平移。
2. **相位与周期不同步**:如果每帧固定加一个角度增量,帧率波动(掉帧/高刷屏)会让实际速度不一致。
3. **基准点选择错误**:应该始终以“初始中心 C”为基准,而不是“上一帧位置”。
4. 正确做法:`位移 = amplitude · sin(2π · elapsed / period)`,然后 `model = T(Cx + 位移·cosθ, Cy + 位移·sinθ)`,每帧根据绝对时间 `elapsed` 重新计算,不依赖上一帧状态。
- **我逐项测试的结果**:
- 把 `elapsed` 设为 0、0.75、1.5、2.25、3(三角形周期 3 秒),分别计算 `amplitude·sin(2π·t/3)`:
- t=0 → 0
- t=0.75 → 0.3·sin(π/2) = +0.3
- t=1.5 → 0.3·sin(π) = 0
- t=2.25 → 0.3·sin(3π/2) = −0.3
- t=3 → 0.3·sin(2π) = 0
- 这组值正好在 0→+0.3→0→−0.3→0 之间连续往返,且任何时刻都能由 elapsed 唯一确定,与上一帧无关。
- **真正原因、修改位置和复测结论**:
原因是“逐帧累加当前位置”。修改位置在 `transforms.js` 的 `modelFor()` 的 `translate` 分支:
```js
const delta = obj.amplitude * Math.sin(TAU * t / period);
return T(c[0] + delta*Math.cos(rad), c[1] + delta*Math.sin(rad), c[2]);
```
复测:运行 5 个周期后,三角形仍精确经过初始中心,屏幕上看不到整体漂移;暂停 2 秒再继续,因为 elapsed 是累计时间而非帧计数,位置也不跳变。
---
## 协作3:方案比较与实现核验——WebGL 截图为什么会得到空白图(Task E)
- **待解决的具体设计问题**:
点击“保存PNG截图”后,最初实现只调用了 `canvas.toBlob(...)`,但在某些浏览器下得到的是一张全黑/全透明图片。需要比较两种截图方案并选出可靠的一种。
- **关键提示词(原文)**:
“用 WebGL 绘制的 canvas 调用 toDataURL / toBlob 截图时,为什么经常得到空白图?preserveDrawingBuffer:true 起什么作用?截图前为什么还要主动调用一次 draw()?请比较‘依赖自动保留’与‘截图前主动重绘’两种方案。”
- **两种方案及取舍依据**:
| 方案 | 做法 | 优点 | 风险 |
|---|---|---|---|
| 方案A | 只依赖 `preserveDrawingBuffer:true`,直接 toBlob | 代码简单 | 浏览器在合成后可能已清空 drawing buffer;rAF 异步调度下时序不稳定,容易截到上一帧或空帧 |
| 方案B | 截图事件中**先同步调用一次 `this.draw()`**,紧接着在同一段代码里 `toBlob` 取像素 | 保证 drawing buffer 里就是当前画面;时序确定 | 多一次绘制,开销可忽略(单画布几个对象) |
AI 解释:WebGL 默认在每帧合成后会清空 drawing buffer(为了性能)。`preserveDrawingBuffer:true` 告诉浏览器不要立刻清空,但这只是“允许保留”,不是“保证你点击时 buffer 还在”。最稳妥的做法是在读取像素前主动重绘一次,让 buffer 内容确定。
- **我最终采用的实现**:
在 `savePNG()` 中:
```js
this.draw(); // 1. 主动重绘,确保 drawing buffer 是本帧
this.canvas.toBlob(blob => {
// 2. 取 Blob,创建临时 <a download> 触发下载
// 3. URL.revokeObjectURL 清理
}, 'image/png');
```
同时 `Graphics` 构造时已设置 `preserveDrawingBuffer:true`,双重保险。
- **测试与反思**:
- 在 task-e.html 中先添加若干对象(圆、三角形、正方形、四面体),点击“保存PNG截图”,打开下载的 PNG,确认:
1. 图片尺寸 = canvas 内部尺寸(800×600);
2. 背景色为设置的浅色 `#f4f8ff`,不是黑色;
3. 所有对象都在图中,没有被截断或错位。
- 把画布改成 960×540 后再截图,确认 PNG 尺寸也变成 960×540,且圆没有被拉成椭圆。
- 反思:实验指导书 3.3 节强调“世界 X 范围随宽高比变化”,截图尺寸与 canvas 内部像素一致,而不是 CSS 显示尺寸——这正是因为 `canvas.width/height` 是绘图缓冲分辨率,`getBoundingClientRect()` 只是 CSS 布局尺寸,两者不能混用。
---
## 其他使用AI的记录
- **三角波函数**:我问“如何写一个周期为 1、在 [0,1] 之间连续往复、t=0 时输出 0、t=0.5 时输出 1 的三角波?”AI 给出 `f(t) = 1 - |2·frac(t) - 1|`,我用 Node.js 验证 t=0/0.25/0.5/0.75/1 输出 0/0.5/1/0.5/0,与实验指导书自测点完全一致,直接采用。
- **鼠标坐标映射**:对照实验指导书 4.D 节给出的公式 `xWorld = (2u−1)·(w/h)`、`yWorld = 1−2v`,我用 800×600 画布手工验证点击中心 (400,300) 得 (0,0)、点击 (200,150) 得 (−0.667, +0.5),与公式计算结果一致后才写入 `addFromMouse()`。