Vista热成像相机

Vista热成像相机

该模组为相机添加了热成像模式,可通过CC:T访问扩展功能。

Vista 热成像摄像机

为 Vista 取景器添加热成像视图和 CC:Tweaked 外设。

Vista 已经提供了一个摄像机方块(取景器),可以将它看到的画面传输到电视上。这个模组在此基础上添加了两样东西。第一,热成像模式:将一块染色玻璃放入取景器的镜片槽位,画面就会变成热成像图像,Sable 结构(载具、飞行器、任何作为子层级建造的东西)在深灰色世界中以白热状态呈现。第二,为 CC:Tweaked 添加了一个 thermal_camera 外设,让计算机可以瞄准摄像机、将其稳定地对准移动中的载具、锁定其他移动结构并跟踪它们,以及读取角度、距离和速度。

画面永远不会传到 Lua。电视照常显示画面;计算机获取的是数字。

这是为我自己世界中的一辆坦克建造的。摄像机安装在炮塔上,计算机驱动炮塔和火炮,乘员通过电视观看画面。这个外设的大部分功能都是因为那辆坦克在某个时候需要它们才存在的。

前置要求

  • Minecraft 1.21.1,NeoForge 21.1.248 或更新版本
  • Vista 5.4.4+ 及其库 Moonlight 3.5.2+
  • CC:Tweaked 1.115+
  • Sable 2.0.3+(可选)。没有 Sable 时,摄像机就是一个固定的取景器:热成像视图、准星、叠加文字和方块测距仪仍然可用,但没有东西可以稳定对准,也没有东西可以跟踪。

已在 Distant Horizons 3.3.1 和 Sodium 0.8.13 下测试。两者都是可选的,模组会在它们存在时调整渲染方式。

无需计算机的热成像视图

右键点击取景器打开其镜片槽位(通常放置染色玻璃板的那个槽位),放入一块染色玻璃。就这样。所有观看此摄像机的电视画面都会切换为热成像。取出玻璃后恢复正常。

热成像图像的样子:

  • Sable 子层级渲染为纯白色,不受光照或纹理影响。
  • 其他一切(地形、建筑、天空)都是深灰色。夜晚看起来和白天一样。
  • 云和雾保持各自的灰色调,因此仍然能分辨出云和雾,只是暗淡。
  • 雨和雪不绘制。
  • Distant Horizons 的 LOD 以与附近区块相同的灰色绘制,因此地平线不会变成一条亮带。

Vista 自带的彩色镜片仍然有效;模组只改变镜片为染色玻璃时发生的情况。

外设

每个取景器同时也是一个 thermal_camera 外设。在它旁边放一台计算机,或者通过有线调制解调器访问它:

local cam = peripheral.find("thermal_camera")
cam.setThermal(true)          -- 与染色玻璃相同,但通过 Lua 控制
cam.setReticle(true)          -- 画面中央的黑色准星
cam.setOverlayText("READY")   -- 画面底部的文字

计算机可以做的事情简要列表:

  • 以绝对角度瞄准摄像机,或以增量微调
  • 改变缩放
  • 将摄像机相对于它所安装的载具(Sable)进行稳定,可以完全在世界坐标系中,或仅俯仰稳定而航向跟随载具
  • 将其指向一个世界坐标,让它锁定那里最近的 Sable 结构,然后自动跟踪该结构
  • 在自动跟踪之上添加操作员修正(提前量、仰角),而不丢失锁定
  • 读取被跟踪结构的位置、包围盒、距离、角度误差和速度
  • 列出当前在摄像机视野内的所有结构
  • 沿视线发射测距仪,获取它击中的方块
  • 在画面上绘制准星和最多 256 个字符的叠加文字
  • 随时低成本地读取摄像机真实的世界方向

为什么摄像机会自行移动

Vista 中取景器的万向架并不知道它站在一辆移动的载具上。如果你设置一个朝向,而载具转向了,摄像机也会跟着转。因此当计算机连接时,这个模组会在服务端每 tick 运行一个小型控制器。它为摄像机保留一个“保持”方向,并按照你要求的坐标系重新应用它:

  • setStabilized(false):保持的角度相对于方块本地。摄像机是刚性的。
  • setStabilized(true):保持的角度在世界坐标系中。载具可以转向、俯仰或翻滚,摄像机保持看向同一个方向。
  • setStabilized(true, true):仅俯仰。航向跟随载具(当摄像机安装在一个已由独立电机旋转的炮塔上时很有用),俯仰保持在世界坐标系中,这样当车体摇晃时地平线保持水平。

在这些模式之间切换不会使摄像机突然抖动;保持的角度是根据摄像机当时实际指向的位置重新计算的。进入和退出自动跟踪也是如此。

当计算机第一次连接时,保持方向取自方块放置时的朝向,因此摄像机不会在上电时突然转向南方。

如果没有连接计算机,控制器完全不运行。此时摄像机的行为与普通的 Vista 取景器完全相同,包括热成像镜片。

提示、锁定、跟踪

cue(x, y, z) 告诉摄像机转向一个世界坐标点(每 tick 最大转动 6 度,因此看起来像万向架而不是瞬移)。当它看向那里时,会在其视线 3 度锥角内搜索 Sable 结构。最接近视线的那个会被锁定。从那时起,摄像机自动跟踪该结构:每 tick 它重新计算到结构瞄准点的方位并应用,同样受转动限制。

瞄准点默认不是结构的中心。setAimHeightFraction(f) 选择结构包围盒内的高度,0 为底部,0.5 为中心,1 为顶部。默认为 0.5。getTrack() 返回同一个点,因此火炮控制器和摄像机对“目标”的位置达成一致。

锁定时,correct(dYaw, dPitch) 通过一个持续偏移(限制在 20 度内)将摄像机的视线偏离结构。结构会偏离准星该偏移量,而跟踪在底层继续进行。这就是操作员如何对移动目标施加提前量,同时自动化持续保持跟踪。clearCorrection() 将其归零,任何手动瞄准命令(setAimnudgecueunlock)会同时丢弃锁定和修正。

当结构从世界中移除时,锁定会自动丢弃。仅距离远不会中断锁定。

摄像机会从搜索中排除它所站立的结构,因此坦克上的摄像机不会锁定那辆坦克。

测距仪

rangefind() 从摄像机沿其当前视线发射一条射线,以 0.5 方块的步长前进,直到击中一个有碰撞形状的世界方块,最远 1500 方块。它返回击中点、方块位置和距离,如果射线离开已加载区域或什么都没击中则返回 nil。它不会强制加载区块,也看不到 Sable 结构(它们存在于 Sable 自己的网格中,不在世界方块空间里;对它们使用 getContacts())。

API 参考

角度单位为弧度。偏航遵循 Minecraft 实体约定:0 为南方(+Z),正值转向西方(-X)。俯仰负值向上,正值向下。速度单位为方块每 tick。坐标为世界坐标,如果摄像机在子层级上,已从 Sable 网格中投影出来。

“主线程”列对性能很重要。这些调用在服务端线程上运行,调用的计算机每次都要等待下一个服务端 tick。其他调用立即返回。如果你每秒轮询主线程函数二十次,你的程序大部分时间都在等待。参见下面的性能说明。

函数 主线程 作用
setAim(yaw, pitch) 在当前坐标系(本地、世界或混合,取决于 setStabilized)中的绝对瞄准。丢弃锁定、提示和修正。
nudge(dYaw, dPitch) 相对瞄准。副作用与 setAim 相同。
getAimWorld() 返回摄像机实际指向的世界坐标系中的 yaw, pitch。开销低;可以随意调用。
setZoom(level) / getZoom() 是 / 否 Vista 缩放级别,与取景器 GUI 使用的相同整数。
setStabilized(on [, pitchOnly]) / isStabilized() / isPitchOnly() 是 / 否 / 否 稳定模式,见上文。随方块持久保存。
setAimHeightFraction(f) / getAimHeightFraction() 摄像机在被锁定结构包围盒内的瞄准位置。
cue(x, y, z) 转向一个世界坐标点并尝试锁定那里最近的结构。
unlock() 丢弃锁定或提示。摄像机停留在原地。
isLocked() 当前是否锁定了某个结构。
correct(dYaw, dPitch) 锁定时添加到操作员偏移。如果未锁定则返回 false
clearCorrection() / getCorrection() 是 / 否 将偏移归零 / 以 yaw, pitch 读取它。
getTrack() 被锁定结构的状态,见下表。
getContacts() 视野内的每个 Sable 结构,最远 1500 方块,无特定顺序。
rangefind() 沿视线的方块测距仪。
setThermal(on) / isThermal() 是 / 否 从 Lua 控制热成像视图。当镜片槽位中有染色玻璃时 isThermal() 也为 true;玻璃无法从 Lua 覆盖。
setReticle(on) / isReticle() 是 / 否 画面中央的黑色准星。
setOverlayText(s) / getOverlayText() 是 / 否 画面底部的文字,行以 \n 分隔,最多 256 个字符,UTF-8。
getDebug() 控制器知道的一切:自身位置和坐标系、当前和保持的角度、半 FOV,以及所有子层级及其方位的未过滤列表。用于调试设置,不用于控制循环。

getTrack() 返回一个表:

含义
locked 如果没有锁定任何东西则为 false;此时其他键不存在
sid 结构 id(字符串),在结构的生命周期内稳定
x, y, z 世界坐标中的瞄准点(水平方向为包围盒中心,垂直方向为高度的 aimFrac
ymin, ymax, aimFrac 包围盒高度范围及使用的比例
range 从摄像机到瞄准点的距离
yawErr, pitchErr 到瞄准点的方位角减去摄像机当前方向
aimYaw, aimPitch 摄像机当前指向的位置(世界坐标)
corrYaw, corrPitch 生效的操作员偏移
vx, vy, vz 瞄准点的速度,方块每 tick,在最近的服务端 tick 上平滑
velSamples 该估计使用了多少 tick;当此值为 3 或更多时可以信任它

getContacts() 返回一个表列表,包含 sidxyz(结构中心)、yminymaxrangeyawErrpitchErr

rangefind() 返回 rangexyz(击中点)和 bxbybz(方块位置),或 nil

示例

锁定最接近准星的结构,并在跟踪时打印其距离:

local cam = peripheral.find("thermal_camera")
if not cam then error("no thermal_camera attached") end

cam.setStabilized(true)
cam.setReticle(true)

-- 选取最接近画面中心的接触目标
local best, bestErr
for _, c in ipairs(cam.getContacts()) do
  local err = c.yawErr ^ 2 + c.pitchErr ^ 2
  if not bestErr or err < bestErr then best, bestErr = c, err end
end
if not best then error("nothing in view") end

cam.cue(best.x, best.y, best.z)

while true do
  local t = cam.getTrack()          -- 主线程:每 5 tick 一次就足够了
  if not t.locked then
    cam.setOverlayText("LOST")
    break
  end
  local speed = math.sqrt(t.vx ^ 2 + t.vy ^ 2 + t.vz ^ 2) * 20   -- 方块/tick -> 方块/秒
  cam.setOverlayText(("RANGE %d m  V %.1f m/s"):format(math.floor(t.range + 0.5), speed))
  sleep(0.25)
end

对摄像机正在看的任何东西进行激光测距:

local r = cam.rangefind()
if r then
  print(("block %d %d %d at %.1f m"):format(r.bx, r.by, r.bz, r.range))
else
  print("no hit")
end

性能说明

主线程调用是昂贵的部分。每次调用都是到服务端 tick 的一次往返,因此每 tick 调用 getTrack() 的循环只能以其可能速度的一小部分运行。对我有效的方法:

  • 每 3 到 5 tick 调用一次 getTrack()getContacts(),并在样本之间用 vx/vy/vz 外推。
  • 当你只需要知道摄像机指向哪里时,使用 getAimWorld()(非主线程)。
  • 在调用 setOverlayText() 之前先在 Lua 侧比较叠加文字。模组也会跳过未改变的文字,但该调用仍然消耗一个 tick。
  • setAim() 置于一个小阈值之后,而不是每 tick 发送它。

控制器本身(稳定和跟踪)在服务端 tick 上运行,不消耗计算机资源。

限制

  • getContacts() 不检查视线。山后面的结构如果在锥角内仍然会被列出。
  • rangefind() 无法击中 Sable 结构。
  • 提示仅按角度选择。在 3 度锥角内最接近视线的任何东西都会被锁定,即使它是几方块外的一个小结构而你本意是它后面的那个。如果这很重要,在提示之前先在 Lua 侧按 range 过滤 getContacts()
  • 热成像图像是一种渲染技巧,不是温度模型。Sable 结构是热的,其他一切都是冷的,这就是全部规则。

致模组包作者

客户端和服务端都需要这个模组。外设和控制器是服务端的;热成像渲染是客户端的,需要 vista_thermal_camera.mixins.json 中的 mixin,它们针对 Vista 5.4.x、Sable 的渲染调度器,以及(仅在存在时)Sodium 的云渲染器和 Distant Horizons。

鸣谢

Vista 和 Moonlight 由 MehVahdJukaar 制作。CC:Tweaked 由 SquidDev 制作。Sable 由 RyanHCode 制作。这个模组是三者之间的粘合剂;困难的部分没有一样是我的。

MIT 许可证。随你怎么用,保留版权声明。