Three.js 做医疗数字孪生:从空场景到可漫游楼层(附最小骨架)

很多智慧医院、智慧楼宇项目,招投标阶段并不需要「全系统上线」,而是要一份看得懂、点得动、加载不崩的 3D 演示。

纯长页面、平面拓扑图,很难讲清空间关系和动线。Three.js 很适合做这一层:浏览器里打开就能漫游,客户不用装客户端。

本文不讲大而全平台,只做一个最小可演示骨架:

  1. 空场景 + 灯光 + 相机控制
  2. 加载一层建筑/科室楼层模型(或用简易几何体占位)
  3. 实现轨道式漫游,保证演示不晕、不卡

场景可以是病房楼层,也可以是门诊、后勤、机房------方法通用。适合:前端想切入医疗/楼宇可视化、需要投标 Demo、想把能力做成模板的同学。

一、先定演示目标,而不是先堆特效

投标 / 方案演示里,客户真正关心的通常是三件事:

  • 空间能不能看懂?→ 楼层轮廓、走廊、功能分区清晰
  • 能不能自己转一转?→ 旋转、缩放、漫游流畅
  • 以后能不能接数据?→ 预留点位 / 拾取扩展位(下期写)

所以第 1 版刻意做减法:

  • 不上全院多楼栋
  • 不上复杂着色器、后处理堆满
  • 一栋/一层可漫游 + 稳定流畅观感

产品化思路:先做成「简易投标 Demo」档,再迭代点位弹窗、后端对接、多楼层切换。

二、技术选型(够用就好)

  • Three.js:场景、相机、渲染
  • GLTFLoader:加载 glb/gltf(工业交付最常见)
  • OrbitControls:快速可交互
  • 可选:DRACO / Meshopt 解码(模型一大就建议上,下期展开)

浏览器端交付的好处:客户当场用手机/笔记本打开链接就能看,减少「装环境」摩擦。

三、最小项目结构

text 复制代码
ward-demo/
  index.html
  main.js
  styles.css
  models/
    ward-floor.glb

index.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>医疗数字孪生 · 楼层漫游 Demo</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div id="app">
      <canvas id="c"></canvas>
      <div class="hud">医疗数字孪生 · 楼层漫游 Demo(拖拽旋转 / 滚轮缩放)</div>
    </div>
    <script type="module" src="./main.js"></script>
  </body>
</html>

styles.css:

css 复制代码
html, body, #app { margin: 0; height: 100%; overflow: hidden; background: #0b1220; }
#c { display: block; width: 100%; height: 100%; }
.hud {
  position: fixed; left: 16px; bottom: 16px;
  padding: 8px 12px; border-radius: 8px;
  background: rgba(0,0,0,.55); color: #e8eefc; font: 13px/1.4 system-ui;
}

四、场景骨架:渲染循环 + 控制器

main.js(Three.js r160+,按你的打包方式改 import):

js 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b1220);

const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 2000);
camera.position.set(8, 6, 10);

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.target.set(0, 1.2, 0);
controls.maxPolarAngle = Math.PI * 0.49;
controls.minDistance = 2;
controls.maxDistance = 40;

scene.add(new THREE.HemisphereLight(0xb1e1ff, 0x444466, 1.0));
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
dir.position.set(5, 10, 7);
scene.add(dir);

// 占位:示意「一层功能区 + 走廊」,可替换为病房/门诊/机房等真实 GLB
function addPlaceholderFloor() {
  const floor = new THREE.Mesh(
    new THREE.BoxGeometry(20, 0.2, 12),
    new THREE.MeshStandardMaterial({ color: 0x2a3b55 })
  );
  floor.position.y = -0.1;
  scene.add(floor);

  const room = new THREE.Mesh(
    new THREE.BoxGeometry(3, 2.4, 4),
    new THREE.MeshStandardMaterial({ color: 0x6ea8fe })
  );
  room.position.set(-4, 1.2, 0);
  scene.add(room);

  const corridor = new THREE.Mesh(
    new THREE.BoxGeometry(10, 0.05, 2.2),
    new THREE.MeshStandardMaterial({ color: 0x9aa7bd })
  );
  corridor.position.set(2, 0.02, 0);
  scene.add(corridor);
}

addPlaceholderFloor();

async function loadFloorModel(url) {
  const loader = new GLTFLoader();
  const gltf = await loader.loadAsync(url);
  const root = gltf.scene;
  root.traverse((obj) => {
    if (obj.isMesh) {
      obj.castShadow = true;
      obj.receiveShadow = true;
    }
  });
  root.scale.setScalar(1);
  scene.add(root);
}

// 有模型时取消注释:
// loadFloorModel('./models/floor.glb');

function onResize() {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
}
addEventListener('resize', onResize);

function tick() {
  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();

五、从「能转」到「像投标 Demo」的 4 个细节

  1. 限制极角:避免相机钻到地板下
  2. 限制距离:防止滚轮飞到外太空
  3. 像素比封顶:setPixelRatio(Math.min(devicePixelRatio, 2)),笔记本更稳
  4. HUD 一句话:告诉客户怎么操作,减少演示冷场

如果客户要「走进去」的沉浸感,再上 PointerLock / 简易碰撞;投标第一版用 Orbit 往往更稳、更不容易晕。

六、模型从哪来?(务实路径)

  • Day 0:盒子占位,先跑通交互与录屏
  • Day 1--2:Blender 粗模 / 现成低模,走廊 + 功能区块面即可
  • 交付前:客户 CAD/设计图重拓扑,控面数,避免直接丢超模

经验原则:投标 Demo 宁可「面数少但帧率稳」,也不要「很精美但手机打开白屏」。轻量化下期专门写。

七、你可以把这个骨架产品化成什么

  • 简易投标演示 Demo:单层漫游 + 基础交互 → 适合方案汇报
  • 可对接完整 3D 页:点位弹窗、设备状态、接口联调 → 下一阶段
  • 源码模板:把灯光、控制器、加载器、HUD 收成可复用工程 → 适合重复售卖

同一套技术能力,场景可复用到病房、门诊、楼宇、园区;用标准化边界拆成不同产品,比每次从零谈需求更轻松。

八、小结与下期预告

今天完成的是数字孪生演示的「地基」:

  • 场景 / 光 / 相机 / 控制器闭环
  • 占位几何体或 GLB 可替换
  • 演示友好的限制与 HUD
相关推荐
孪生前端10 小时前
如何用配置驱动一套 Three.js 数字孪生模板
webgl
用户83134859306984 天前
Vue+Threejs实现人物奔跑场景
vue.js·webgl·three.js
元气少女小圆丶5 天前
unity发布web嵌入到前端页面的接受参数
前端·unity·webgl
WebGIS开发7 天前
WebGIS开发进阶教程①:WebGL坐标系
gis·webgl·gis开发
虚焦像素_8 天前
MapLibre GL 实战:一套引擎搞定 2D 平面与 3D 球面
webgl
山河木马10 天前
GPU自动处理专题4-填充覆盖区域(光栅化)
javascript·webgl·计算机图形学
智商偏低10 天前
WebGL 百万线段高性能 Demo(批量绘制,无循环绘制)
webgl
weixin_4368040713 天前
毒蘑菇性能测试增强版 - 毒蘑菇性能测试 - 在线 WebGL GPU 帧率压力测试工具
webgl
山河木马13 天前
GPU自动处理专题3-NDC怎么映射成屏幕像素坐标(视口变换)
javascript·webgl·图形学