如何使用 three.js 实现 3D 消防疏散,走廊封了怎么改道——第二课|WebGL

序

手机里的群越来越多,真正需要看的消息倒不一定多。一个通知转发三遍,再跟上几十个"收到",原消息就找不到了。大家都很积极,手机也很忙,只有事情本身还在原地。

有时候觉得,静音功能实在是个好发明。当然,该看的通知还是得看,不然第二天就要解释为什么没收到。

上篇把烟感和房间对应起来了。这篇继续往下做:二楼东侧走廊在演练中封闭,原来画好的路线要撤掉,再看能不能从另一侧楼梯出去。

一、先把能走的地方连起来

路线沿门口、转角和楼梯口连接,别从办公桌中间穿过去。配图是 AI 生成的三维场景设计图,坐标属于本次教学案例;实际通行关系应来自核对过的平面资料和预案。

js 复制代码
const nodes = new Map([
  ['room', [2, 6, -3]], ['hall', [2, 6, 0]],
  ['east', [7, 6, 0]], ['west', [-5, 6, 0]],
  ['exitE', [7, 6, 4]], ['exitW', [-5, 6, 4]]
]);
const links = [
  ['door', 'room', 'hall'], ['eastHall', 'hall', 'east'],
  ['westHall', 'hall', 'west'], ['eastStair', 'east', 'exitE'],
  ['westStair', 'west', 'exitW']
].map(([id, a, b]) => ({
  id, a, b,
  cost: new THREE.Vector3(...nodes.get(a))
    .distanceTo(new THREE.Vector3(...nodes.get(b)))
}));
const exits = new Set(['exitE', 'exitW']);

节点少的时候,这张表就能把位置关系交代清楚。路径 ID 单独保存,后面封闭的是哪一段,也就不用从一串坐标里猜。

二、东侧封闭以后,重新计算

这次用 Dijkstra 查找可到达的预案出口。封闭边不参与计算;两个出口都不可达,就返回空值,不能继续亮着上一条路线。

js 复制代码
function findExit(start, closed) {
  if (!nodes.has(start)) return null;
  const dist = new Map([[start, 0]]);
  const previous = new Map();
  const pending = new Set([start]);
  while (pending.size) {
    const current = [...pending].reduce((a, b) =>
      dist.get(a) < dist.get(b) ? a : b);
    pending.delete(current);
    if (exits.has(current)) {
      const path = [current];
      while (previous.has(path[0])) path.unshift(previous.get(path[0]));
      return path;
    }
    for (const edge of links) {
      if (closed.has(edge.id)) continue;
      const next = edge.a === current ? edge.b
        : edge.b === current ? edge.a : null;
      if (!next) continue;
      const candidate = dist.get(current) + edge.cost;
      if (candidate >= (dist.get(next) ?? Infinity)) continue;
      dist.set(next, candidate);
      previous.set(next, current);
      pending.add(next);
    }
  }
  return null;
}

const closed = new Set(['eastHall']);
const path = findExit('room', closed);

小场景这样写够用了。节点上万时再换优先队列,并把路径计算移到 Worker,别让计算和鼠标拖动抢主线程。

三、把计算结果放回三维

画线时直接使用返回的节点序列。转角保持折线,不做穿墙的圆滑插值;整条路线稍微抬高一点,避免和地面闪烁。

js 复制代码
let routeLine = null;

function drawRoute(path) {
  if (routeLine) {
    routeLine.removeFromParent();
    routeLine.geometry.dispose();
    routeLine.material.dispose();
    routeLine = null;
  }
  if (!path) return;
  const points = path.map(id => {
    const point = new THREE.Vector3(...nodes.get(id));
    point.y += 0.08;
    return point;
  });
  routeLine = new THREE.Line(
    new THREE.BufferGeometry().setFromPoints(points),
    new THREE.LineBasicMaterial({ color: 0x64b08c })
  );
  scene.add(routeLine);
}

drawRoute(path);

图里的楼梯和房门是参照物。界面除了显示路线,还应保留当前所在楼层、目的出口和本次通道状态的版本。

四、通道状态变了,再走一遍

服务端推来的状态按版本更新。下面每次先撤旧线,再计算新线,避免出现新告警到了、旧路线还在的情况。notice 对应页面中用于显示提示的一块文字区域。

js 复制代码
let routeVersion = -1;

function onPassageState(message, notice) {
  if (message.mode !== 'drill') return;
  if (!Number.isInteger(message.version)) return;
  if (message.version <= routeVersion) return;
  if (!Array.isArray(message.closedIds)) return;
  routeVersion = message.version;
  drawRoute(null);
  const blocked = new Set(message.closedIds);
  const nextPath = findExit(message.personNode, blocked);
  drawRoute(nextPath);
  notice.textContent = nextPath
    ? `演练路线 V${routeVersion} → ${nextPath.at(-1)}`
    : '当前无可用演练路线,等待人工确认';
  return nextPath;
}

最后试三种情况就很直观:只封东侧、只封西侧、两侧都封。路线应当分别改道、改道、清空。到这里,三维里画的线才跟演练状态连上了。

下一篇把报警、封闭和改道按时间记下来,做一个能暂停、能回退的演练复盘。

技术交流 1203193731@qq.com

交流微信:yeyunfeigc 

上篇: 第一课:场景与火情定位

**下篇:**第三课:事件联动与演练复盘