职责
把屏幕上的一个点,翻译成“哪一根柱子”,然后让玩家把那一格的地形换掉。
这一层是后加的(一开始只能看不能改),但它反过来替算法层抓出了一个潜伏的 bug ——
见 island-mesh 那页的「这里躺着一个只有编辑器才能发现的 bug」。
为什么不用“拾取缓冲”
通用做法是:把每一格用纯色再画一遍到一个离屏缓冲,鼠标点哪儿就读回那个像素的颜色, 按颜色编号反查格子。这叫 pick buffer,是 3D 引擎在几何不可解析时的兜底手段。
我们的场景完全解析,所以有更省的办法:
| 拾取缓冲 | 解析命中(本项目) | |
|---|---|---|
| 代价 | 每帧多画一整遍,或每次点击多画一遍 | 0 次额外绘制 |
| 精度 | 边缘像素被抗锯齿串色 → 解出第三个数 | 精确,无误差 |
| 补丁 | 要靠“回避边缘 N 像素”这类经验规则 | 不需要 |
| 深度 | 需要一个深度缓冲(或再画一遍深度) | 一个点积 |
三个面 + 一个点积
每根柱子最多只有 3 个面朝相机:顶面,和两个侧面。而“哪几个面朝相机”在正交投影下
只跟相机方向有关,是渲染器早就一帧算一次的那张 vis 表:
vis = { px: dx > 0, nx: dx < 0, py: dy > 0, ny: dy < 0, pz: dz > 0, nz: dz < 0 }
于是点选就是:把这 3 个面投影成屏幕上的四边形,各自做一次射线交叉判断, 多个面同时命中时取相机深度最大的那个:
depth = x·dir.x + y·dir.y + z·dir.z // 正交投影下深度就是一个点积
这样“前面高柱子挡住后面矮柱子的顶面”会自动得到正确答案,不需要深度缓冲。
侧面那一段把“地表层”和“地幔”合并成一个四边形,是因为它们本来就共面 —— 中间那条颜色分界线只是上色时画的,不是几何上的折。
成本:250 根柱子 × 3 个面 = 750 次测试,鼠标每动一下算一次也毫无压力 (悬停高亮就是这么来的)。
鼠标事件链
| 事件 | 做什么 |
|---|---|
pointermove(按住) |
累计位移 > 4px 就转视角;顺手把悬停框清掉 |
pointermove(松开) |
编辑态下更新 hover → 画面给那一格描个白边 |
pointerup |
拖动过就不算点击(moved > 4 才判定为拖拽),否则 paintAt |
pointercancel / pointerleave |
清悬停框 |
keydown |
1–5 换地形、0 / E 凿空、Esc 退出编辑、R 重新生成 |
进编辑态的几条约定
| 约定 | 为什么 |
|---|---|
| 进入编辑自动关掉自转 | 岛还在转的话没法瞄准 |
| 岛没成型时不给进 | 那会儿画面是 2D 网格,没有“格子”可以点 |
| 重新生成时强制退出编辑 | 整张地形都被换掉了,编辑态留着没有意义 |
| 色板出现时收起图例 | 色板上已经写了地形名,图例是同一份信息说第二遍 |
paintAt 命中同一格同一种地形时不动 |
避免无意义的整岛重算(也避免把“未改动”误报成“改动了”) |
已知的坑
| 坑 | 症状 | 原因 / 修法 |
|---|---|---|
用 hidden 属性藏色板 |
藏不住 | 这里的 display 是 flex,会盖掉 UA 样式表里的 [hidden]{display:none}。改用 .on 类控制 |
在 const edit 之前直接执行色板构建 |
整页白屏 | 暂时性死区(TDZ)。把构建调用挪到文件末尾的启动段 |
| 转视角时不清悬停框 | 会残留一个指错格子的白边 | pointermove 里 edit.hover = -1 |
| 拖拽结束也算点击 | 想转视角,结果顺手把地形改了 | moved > 4 才判为拖拽;探针有专门一条断言盯这个(拖 80px → 改动 0 格) |
| 只更新被点的那一根柱子 | 相邻柱子的底面出现台阶 | 底面由 3×3 平滑后的深度决定,改一格要整岛重挤 |
| 不加“同值跳过” | 每次移动都整岛重算 | cells[key] === brush 直接返回 false |