这条链路可行的关键,是让 Codex 通过 Blender MCP 生成可检查的 Blender 场景,再把模型交给 React 与 Three.js 完成医疗数据和交互联动。Codex 拆解任务并根据反馈继续修改,MCP 把指令落实到 Blender,React 管理医疗状态与 DataV 面板,Three.js 负责画布渲染、拾取和镜头。各环节通过模型、业务 ID 和共享状态衔接。
本文从掘金热榜文章《Three.js 医院三维大屏实战:Codex + Blender MCP + React 从建模到交互全流程》切入。现有材料没有该文正文,因此不复述其中无法核对的命令、版本和性能数字。下文依据可核对的资料与实际搭建顺序,说明建模、导出、加载、联动、排障和验收。
Codex、Blender MCP、React 与 Three.js 各自负责什么?
完整链路涉及多个工具,需要按环节划分职责:
Codex
│ 任务拆解、执行反馈、下一轮修改清单
▼
Blender MCP
│ 查询场景、创建对象、设置变换、材质、保存与导出
▼
Blender
│ 建筑、楼层、科室、床位、设备、材质与层级
▼
glTF + 业务清单
│
▼
React 状态层 ─────────────► DataV 图表与详情面板
│
▼
Three.js 场景、材质、显隐、相机与射线拾取
│
└──► 浏览器报错、点击结果、筛选结果 ──► 下一轮 Codex 修正
| 环节 | 主要职责 | 不应承担的职责 |
|---|---|---|
| Codex | 拆解需求、生成操作步骤、读取执行结果、整理修改清单 | 不把聊天回复当作 Blender 场景已经改变的证据 |
| Blender MCP | 把查询与修改动作落到 Blender,并返回可检查的状态 | 不负责 React 页面渲染,也不直接处理医疗业务数据 |
| Blender | 生成几何体、层级、材质、UV、灯光和导出文件 | 不承担实时状态刷新和前端筛选 |
| React | 管理模拟数据、REST 或 WebSocket 更新、选中项、筛选条件和组件生命周期 | 不在每次状态变化时重建整个 Three.js 场景 |
| Three.js | 加载模型、渲染画面、射线拾取、相机控制、材质与显隐更新 | 不直接充当业务状态数据库 |
React、DataV、Three.js 与 Blender 的组合已有公开实践参考。 Codex 与 Blender MCP 的边界则应通过实际任务逐步验证,而不是依赖一句“AI 自动建模”的概括。
第一次测试也不应该直接搭建整座医院。更合适的最小闭环是:创建名为 MCP_SMOKE_TEST 的测试物体,保存 Blender 场景,导出一个 glTF 文件,在 React 中加载它,点击后回传同一个对象名称。只有这条链路完全闭合,才值得继续加入楼层、床位和设备。
开工前怎样记录环境并跑通最小闭环?
版本基线必须来自当前执行环境,不能从旧文章中拼接。现有资料里的示例同时出现过 Three.js 0.91.0、0.161.0 和 Blender 2.93.6,它们对应不同项目环境,只能作为对照,不能直接当作本次配置。
先采集版本和依赖:
node --version
npm --version
codex --version
codex mcp --help
blender --version
npm ls --depth=0
npm ls three
如果 Blender 没有加入系统路径,就直接运行桌面安装包,并从“关于”页面记录版本。MCP 服务也应记录实际使用的包版本、提交号或配置修订号,因为不同服务与 Codex 客户端的配置入口并不由主题名称决定。
| 记录项 | 需要留下的信息 |
|---|---|
| 操作系统 | 系统版本、显卡、浏览器版本 |
| Node 与前端 | Node、包管理器、React、Three.js、Vite 或其他构建工具版本 |
| Blender 与 MCP | Blender 版本、MCP 服务版本、连接方式、实际端口 |
| Codex | 客户端版本、启用的 MCP 服务名称、任务目录 |
| 前端服务 | 开发地址、端口、API 与 WebSocket 地址 |
| 文件目录 | .blend、导出模型、纹理、清单和构建产物位置 |
| 权限 | Blender 写入目录、MCP 网络权限、前端静态资源读取权限 |
| 样例数据 | 数据来源、字段、状态数量和更新频率 |
建议按以下顺序启动:
- 启动 Blender,确认当前 MCP 桥接服务已经连接。
- 启动 Codex,通过 MCP 查询 Blender,确认能读到场景,而不是只收到“连接成功”。
- 启动 React 开发服务,在浏览器中验证空白场景或静态测试模型。
- 最后执行创建、保存、导出和点击回传。
开发服务可以这样启动:
npm run dev -- --host 127.0.0.1
端口应以终端实际输出为准,不要把某个开发端口写进文档或部署配置。MCP 连接若走本机网络,优先限制在回环地址,不应直接暴露给局域网或公网。
项目目录最好把源文件、模型和业务清单分开:
medical-dashboard/
├ src/
│ ├ components/
│ │ ├ HospitalScene.jsx
│ │ ├ DetailPanel.jsx
│ │ └ FilterPanel.jsx
│ ├ hooks/
│ │ ├ useHospitalState.js
│ │ └ useSceneSelection.js
│ └ data/
│ └ hospital-fixture.json
├ public/
│ └ models/
│ └ hospital/
│ ├ hospital.gltf
│ ├ hospital.bin
│ ├ textures/
│ └ hospital-manifest.json
└─ scene/
├ hospital.blend
└ exports/
模拟数据先使用匿名对象,避免在模型、截图和日志里带入患者姓名、住院号等信息:
[
{
"id": "bed-3f-w-012",
"name": "BED_3F_W_012",
"type": "bed",
"floor": "3F",
"department": "west",
"status": "occupied"
}
]
已有 Blender 模型、React 页面或 DataV 大屏怎么接入?
接入已有项目无需推倒重来,具体方式取决于现有资产。
| 现有资产 | 接入方式 | 优先检查 |
|---|---|---|
| 已有 Blender 模型 | 先用 MCP 查询层级、命名、单位和材质,再补业务 ID 与导出设置 | 比例、轴向、轴心、重复材质、无用骨骼、外部纹理 |
| 已有 React 页面 | 将 Three.js 封装成独立组件,由现有页面提供模型地址和业务状态 | React 严格模式下的重复初始化、组件卸载后的资源释放 |
| 已有 DataV 大屏 | 把选中项和筛选条件提升到共同的 React 状态层 | 图表是否自行维护了另一份筛选状态 |
| 已有 Three.js 场景 | 保留现有相机、控制器和加载逻辑,只接入新的医院模型 | Three.js 版本差异、拾取对象列表、状态更新入口 |
最容易出问题的是让 DataV 和 Three.js 各自维护一套筛选条件。正确关系应当是:
React filters
├─► 场景对象显隐与材质
└─► DataV 数据源
Three.js 点击事件只更新 selectedId,DataV 不直接操作 Three.js 对象,场景也不直接改写图表数据。这样才能保证复位、楼层筛选、科室筛选和图表联动使用同一套状态。
Codex 通过 Blender MCP 第一步能验证什么?
第一步不是让 Codex 一次生成完整医院,而是验证每个原子动作是否真的作用到了 Blender。
| 动作 | 检查方式 | 通过标准 |
|---|---|---|
| 查询场景 | MCP 返回对象、集合、父子关系与变换 | 返回内容与 Blender 界面一致 |
| 创建物体 | 在指定 Collection 中新建测试网格 | 对象名称、类型和位置正确 |
| 重命名 | 修改对象名称 | MCP 查询和 Blender 界面显示同一名称 |
| 设置父级 | 将测试对象挂到指定空物体或父对象 | 移动父级时子对象跟随 |
| 调整变换 | 修改位置、旋转或缩放 | 世界坐标、包围盒与视觉位置符合预期 |
| 赋予材质 | 添加或替换材质 | 对象材质槽和预览颜色正确 |
| 保存场景 | 保存 .blend |
文件存在,重新打开后修改仍在 |
| 导出模型 | 输出 glTF 及依赖文件 | 文件完整,React 能加载并拾取对象 |
可以先给 Codex 一份边界明确的检查任务:
先只读查询当前 Blender 场景,返回 Collection、对象名称、父级、位置、缩放和材质。
不要创建、删除、重命名或导出任何对象。
确认查询结果可信后,再执行单步修改:
在 Hospital 场景的 Test Collection 中创建一个名为 MCP_SMOKE_TEST 的立方体。
不删除现有对象,不修改其他对象,不立即导出。
完成后重新查询场景,并返回新对象的父级、位置和材质。
每轮只改一个变量,比让 Codex 连续执行“建模、材质、灯光、导出、接入 React”更容易定位问题。一个实用的闭环是:
Codex 生成动作
→ MCP 执行
→ 查询 Blender 状态
→ React 加载或浏览器操作
→ 收集报错、截图与点击结果
→ Codex 修正
→ 重新导出并复测
如果 Codex 会话不能直接读取浏览器控制台,就需要把错误文本、Network 面板中的失败请求或拾取结果回填到下一轮任务。Blender MCP 返回“执行成功”并不等于页面正确,最终证据仍应来自场景查询、文件结果和浏览器行为。
医院场景怎样拆到楼层、科室和设备?
医院模型适合按“建筑结构”和“业务对象”两条线组织。Blender 本身覆盖建模、UV、贴图、动画与材质制作,模型进入 Three.js 前的准备流程也有系统资料可参考。
推荐层级如下:
HOSPITAL
├─ Building
│ ├─ BuildingShell
│ └─ Entrances
├─ Floors
│ ├─ Floor_01
│ ├─ Floor_02
│ └─ Floor_03
│ ├─ Department_West
│ │ ├─ Ward_W01
│ │ │ ├─ BED_3F_W_012
│ │ │ └─ BED_3F_W_013
│ │ └─ Device_Group
│ └─ Department_ICU
├─ AlarmAreas
└─ SharedEquipment
这里要区分三种结构:
| 结构 | 用途 | 推荐做法 |
|---|---|---|
| Collection | 方便按楼层、科室或用途选择、隐藏和导出 | 名称保持稳定 |
| Empty 或父对象 | 承载移动、旋转和楼层显隐 | 业务对象尽量归入统一父级 |
| 网格对象 | 真实可见的建筑、床位和设备 | 绑定唯一 businessId |
对象显示名称与业务 ID 应分开。显示名称可以调整,业务 ID 一旦进入接口就不能随意变化,也不要使用 Blender 临时 UUID 或数组下标。命名可以采用:
BED_3F_W_012
DEV_3F_ICU_ECG_007
WARD_3F_NORTH_01
对应清单则记录稳定的业务关系:
{
"version": 1,
"objects": [
{
"id": "bed-3f-w-012",
"name": "BED_3F_W_012",
"type": "bed",
"floor": "3F",
"department": "west",
"pickable": true
}
]
}
建模有两条可行路线。
第一条是使用 Blender 基础几何体搭建低多边形医院。建筑外壳、楼层、科室边界、床位和普通设备先用方体、圆柱与简单曲线表达,适合大屏中远距离观察,也便于控制面数和材质数量。
第二条是导入外部设备模型。外部模型可以提供更细的监护仪、呼吸机或检查设备,但导入后需要检查比例、轴向、轴心、重复几何、UV、材质和纹理。关键设备可以保留细节,重复设备应共享网格与材质,并通过实例化或 LOD 控制运行时成本。
告警区域可以建模成半透明边界或覆盖层,但不要把“红色告警”烘焙进模型。颜色、闪烁和文字属于运行时状态,应由医疗数据在 React 与 Three.js 运行时更新。
glTF 导出前怎样统一坐标、单位和业务 ID?
glTF 适合作为主要交换格式。Three.js 对 glTF 等模型格式提供支持,Blender 模型进入 Three.js 交互展示也有对应实践。
导出前至少检查以下项目:
| 项目 | 需要确认的内容 |
|---|---|
| 单位 | 全项目采用同一现实尺度,例如以米建模;不要一处使用米、一处使用厘米 |
| 轴向 | 记录 Blender 场景轴向、导出轴向和运行时观察结果,不在多个环节重复猜测转换 |
| 原点与轴心 | 建筑以地面中心为参考,设备以合理安装点或几何中心为参考 |
| 缩放 | 检查非单位缩放、负缩放和未应用变换,避免导出后比例异常 |
| 选择范围 | 只导出需要进入浏览器的 Collection,避免把辅助物体一并交付 |
| 材质 | 检查透明材质、双面材质、发光材质和重复材质 |
| 纹理 | 使用相对路径或打包方式,核对文件名大小写与实际目录 |
| 动画 | 保留确有需要的骨骼或动画,删除无关动画数据 |
| 业务 ID | 通过对象自定义属性或旁路清单交付,不能只依赖显示名称 |
| LOD | 明确主模型和低细节模型分别用于哪些视距或屏幕尺寸 |
glTF 导出后,浏览器通常需要同时取得模型、缓冲区与纹理。可以保留便于检查的分离结构:
hospital.gltf
hospital.bin
textures/
floor_basecolor.png
device_metal.png
hospital-manifest.json
如果使用打包文件,则要确认打包结果没有改变业务 ID、材质名称或纹理引用。导出完成后,还应把文件重新导入 Blender 做一次往返检查:对象数量、比例、主要材质和层级是否仍然正确。
参考资料列出的 glTF Pipeline、纹理压缩、LOD 和实例化都可以进入候选优化清单,但不应在上线前机械地全部启用。先用渲染统计确认瓶颈,再决定处理顺序。
模型导出后为什么在 React 里加载失败?
加载失败通常不是 Three.js 本身的问题,而是文件、路径、坐标或生命周期中的某一环断了。按下面顺序排查,比直接修改相机和缩放更有效。
| 现象 | 优先检查 | 修正方向 |
|---|---|---|
| 模型请求 404 | modelUrl、Vite base、静态目录 |
统一资源根路径 |
.bin 或纹理 404 |
glTF 中的相对路径 | 同步移动全部依赖文件 |
| CORS 报错 | 请求地址与页面是否同源 | 优先同源静态部署,必要时配置服务端代理 |
| MIME 类型错误 | Web 服务器对 glTF、bin、图片的配置 | 使用正确的静态资源响应类型 |
| JSON 或解析错误 | 文件是否完整、格式是否被二次转换 | 重新导出,不手工修改二进制内容 |
| 模型加载但不可见 | 包围盒、相机位置、裁剪面、灯光 | 根据模型包围盒设置相机与阴影范围 |
| 模型位置偏移 | 导出原点、轴心、运行时变换 | 统一坐标约定,避免重复偏移 |
| 材质发黑或丢失 | 灯光、材质通道、纹理路径 | 检查材质与灯光,不直接替换成临时颜色 |
| 组件卸载后报错 | 动画循环、事件监听和资源释放 | 在 React 清理函数中完整释放 |
React 中的基础场景可以按下面的方式组织。示例只负责初始化、加载、适配与释放,选中状态和医疗数据在后续章节单独接入。
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
function disposeMaterial(material) {
if (!material) return;
for (const value of Object.values(material)) {
if (value?.isTexture) value.dispose();
}
material.dispose();
}
function disposeModel(root) {
root.traverse((object) => {
object.geometry?.dispose();
const materials = Array.isArray(object.material)
? object.material
: [object.material];
materials.forEach(disposeMaterial);
});
}
export function HospitalScene({ modelUrl }) {
const hostRef = useRef(null);
const [phase, setPhase] = useState('loading');
useEffect(() => {
const host = hostRef.current;
if (!host) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07131f);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';
host.appendChild(renderer.domElement);
const hemisphere = new THREE.HemisphereLight(0xffffff, 0x334155, 1.2);
const keyLight = new THREE.DirectionalLight(0xffffff, 2);
keyLight.position.set(20, 30, 10);
keyLight.castShadow = true;
scene.add(hemisphere, keyLight);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
let root = null;
let disposed = false;
const resize = () => {
const width = host.clientWidth;
const height = host.clientHeight;
if (!width || !height) return;
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
const loader = new GLTFLoader();
loader.load(
modelUrl,
(gltf) => {
if (disposed) {
disposeModel(gltf.scene);
return;
}
root = gltf.scene;
root.traverse((object) => {
if (object.isMesh) {
object.castShadow = true;
object.receiveShadow = true;
}
});
scene.add(root);
const box = new THREE.Box3().setFromObject(root);
const sphere = box.getBoundingSphere(new THREE.Sphere());
const radius = Math.max(sphere.radius, 1);
controls.target.copy(sphere.center);
camera.position.copy(sphere.center).add(
new THREE.Vector3(radius, radius * 0.6, radius)
);
camera.near = Math.max(radius / 1000, 0.01);
camera.far = radius * 100;
camera.updateProjectionMatrix();
controls.update();
setPhase('ready');
},
undefined,
(error) => {
console.error('Hospital model failed to load:', error);
if (!disposed) setPhase('error');
}
);
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
return () => {
disposed = true;
renderer.setAnimationLoop(null);
observer.disconnect();
controls.dispose();
if (root) disposeModel(root);
renderer.dispose();
renderer.domElement.remove();
};
}, [modelUrl]);
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
{phase === 'loading' && <div>模型加载中</div>}
<div
ref={hostRef}
style={{ position: 'absolute', inset: 0 }}
/>
{phase === 'error' && (
<div role="alert">
模型加载失败,已切换到静态数据视图
</div>
)}
</div>
);
}
加载状态和错误兜底不能只输出到控制台。模型失败时,页面仍应保留筛选器、静态统计和告警列表,避免医疗大屏变成一块空白画布。
医疗状态怎样与三维对象一一对应?
先用模拟数据完成映射,再接入接口。正常、占用、维护和告警可以映射为不同颜色,但不应只依赖颜色。色觉差异、远距离观看和视频截图都会降低单一颜色编码的可辨识度,最好同时提供标签、图标或状态文案。
| 状态 | 建议视觉 | 业务含义 |
|---|---|---|
normal |
稳定的绿色或中性色 | 对象当前正常 |
occupied |
蓝色或青色,并显示占用标签 | 床位正在使用 |
maintenance |
黄色或琥珀色,并显示维护标记 | 对象暂不可用 |
alarm |
红色,并配合闪烁或告警标识 | 对象需要处置 |
颜色属于项目约定,不是资料中规定的医疗标准。实际落地时还应结合医院现有标识规范。
模型加载完成后,先建立业务 ID 与三维对象的映射:
const objectById = new Map();
root.traverse((object) => {
const id =
object.userData.businessId ||
manifestByName.get(object.name)?.id;
if (id) {
objectById.set(id, object);
}
});
状态更新时只修改目标对象:
const STATUS_COLORS = {
normal: 0x38c793,
occupied: 0x2f80ed,
maintenance: 0xf2b84b,
alarm: 0xeb5757
};
function applyStatus(object, status) {
if (!object || !STATUS_COLORS[status]) return;
const materials = Array.isArray(object.material)
? object.material
: [object.material];
for (const material of materials) {
material?.color.setHex(STATUS_COLORS[status]);
}
}
如果多个床位共享同一个材质实例,直接修改材质会让它们一起变色。可交互对象应使用独立材质副本,或者改用 InstancedMesh 的实例颜色通道。
数据接入可以分三步:
- 使用本地 fixture 验证 ID、状态和材质映射。
- 换成 REST 轮询,确认接口失败、空数据和部分字段缺失时的行为。
- 数据频率较高时再接入 WebSocket,并处理断线重连和重复消息。
REST 轮询的关键是替换旧状态,而不是追加状态:
useEffect(() => {
const controller = new AbortController();
let stopped = false;
const load = async () => {
const response = await fetch('/api/hospital-state', {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Hospital state request failed: ${response.status}`);
}
const nextState = await response.json();
if (!stopped) setRecords(nextState);
};
load();
const timer = window.setInterval(load, pollMs);
return () => {
stopped = true;
controller.abort();
window.clearInterval(timer);
};
}, [pollMs]);
状态推送也不应触发整棵场景重建。React 保存业务状态,Three.js 在 effect 中比较前后差异,只更新目标对象的颜色、显隐或实例属性。
点击、筛选和镜头联动如何形成完整交互?
Three.js 的射线拾取负责判断用户点中了哪个业务对象,React 负责保存选择结果。指针坐标必须根据画布实际矩形换算,不能直接使用页面坐标:
const pointer = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
function updatePointer(event, canvas) {
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
function pick(event, camera, pickables) {
updatePointer(event, event.currentTarget);
raycaster.setFromCamera(pointer, camera);
return raycaster.intersectObjects(pickables, false)[0] ?? null;
}
如果模型使用父级 Empty 承载业务对象,命中网格后还要沿父级链查找 businessId。交互建议保持一条清晰路径:
| 操作 | Three.js | React | DataV |
|---|---|---|---|
| 悬停 | 更新射线结果、鼠标样式和提示位置 | 记录 hoveredId |
不触发 |
| 点击 | 停止继续传播选择逻辑 | 设置 selectedId |
高亮对应业务记录 |
| 楼层筛选 | 设置楼层分组显隐 | 更新 filters.floor |
使用过滤后的数据 |
| 科室筛选 | 设置科室分组显隐 | 更新 filters.department |
使用过滤后的数据 |
| 告警筛选 | 只显示告警对象 | 更新 filters.alarmOnly |
只统计告警数据 |
| 镜头聚焦 | 平移相机与控制器目标 | 保存焦点对象 | 联动详情面板 |
| 复位 | 恢复初始相机和控制器目标 | 清空筛选与选择 | 恢复完整图表 |
镜头动画可以接入 GSAP;参考资料也把 GSAP 用于复杂相机路径与时间轴动画。
gsap.to(camera.position, {
x: target.position.x,
y: target.position.y,
z: target.position.z,
duration: 0.6,
ease: 'power2.out'
});
gsap.to(controls.target, {
x: target.position.x,
y: target.position.y,
z: target.position.z,
duration: 0.6,
ease: 'power2.out'
});
更稳妥的实现是让相机围绕目标计算一个偏移位置,而不是直接飞到对象原点。对于内部床位,镜头目标可以取床位中心;对于楼层筛选,则可以取该楼层所有可见对象的包围盒中心。
高亮也不建议永久改写共享材质。可以为选中对象克隆材质、设置单独的选中态材质,或者使用辅助框。详情面板只根据 selectedId 从业务状态中读取数据,不从 Three.js 对象里拼装医疗信息。
1920×1080 大屏怎样适配?
固定设计尺寸适合会议室或指挥中心的大屏。已有 Three.js 医疗监控示例直接按 1920×1080 建立画布,并通过固定相机、灯光和雾效组织场景。
常见做法是保留一个 1920×1080 的设计舞台,再按容器比例整体缩放:
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
function fitStage(container, stage) {
const scale = Math.min(
container.clientWidth / DESIGN_WIDTH,
container.clientHeight / DESIGN_HEIGHT
);
stage.style.transform =
`translate(-50%, -50%) scale(${scale})`;
}
.stage-host {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.stage {
position: absolute;
left: 50%;
top: 50%;
width: 1920px;
height: 1080px;
transform-origin: center center;
}
这种方式能保持 DataV 面板、Three.js 画布与三维文字的位置关系,不会在不同屏幕上逐个拉伸。浏览器比例不是 16∶9 时,应保留留白,而不是强行铺满。
如果页面需要同时适配桌面、笔记本和不同分辨率大屏,则应使用 CSS Grid 配合 ResizeObserver,让 Three.js 画布跟随面板尺寸变化。此时要同步更新相机宽高比和渲染器尺寸。
固定舞台和自适应布局都需要处理以下问题:
- 固定设计尺寸不等于固定渲染分辨率。
- 高像素密度屏幕会放大实际渲染像素数量,需要限制
renderer.setPixelRatio()。 - Three.js 画布、CSS 面板和拾取坐标必须使用同一套布局。
- 缩放后的指针位置仍应通过
getBoundingClientRect()换算。 - 全屏切换后要触发一次 resize,并重新检查 DataV 图表尺寸。
大屏卡顿、错位或内存上涨怎么排?
性能排查应从渲染统计、React 更新和资源生命周期三条线同时进行。只看模型面数不够,因为大量低面数网格同样可能制造很高的 draw calls。
可以在稳定帧读取 Three.js 统计:
console.table({
calls: renderer.info.render.calls,
triangles: renderer.info.render.triangles,
geometries: renderer.info.memory.geometries,
textures: renderer.info.memory.textures
});
| 现象 | 重点测量 | 常见修正 |
|---|---|---|
| 帧率低但模型面数不高 | draw calls、材质数量、阴影 | 合并静态几何、减少透明材质、限制阴影范围 |
| 大量床位拖慢场景 | 实例数量与重复网格 | 使用 InstancedMesh |
| 首次加载慢 | glTF、纹理与请求瀑布图 | 纹理压缩、分块或渐进加载 |
| 缩放时明显卡顿 | 像素比、阴影和高精度纹理 | 控制渲染像素比,按距离降低细节 |
| 筛选后帧率短暂下降 | 是否重建模型和材质 | 只更新目标对象属性 |
| 点击位置偏移 | 指针坐标与画布矩形 | 使用 getBoundingClientRect() 换算 |
| 内存持续上涨 | geometry、texture、材质、事件监听 | 卸载时释放资源并解绑事件 |
| 多次进入页面后越来越卡 | 是否重复创建 renderer | renderer 只初始化一次并完整清理 |
参考资料列出的 LOD、InstancedMesh、glTF Pipeline、纹理压缩、渐进加载和 Points 配合 ShaderMaterial,都可以按实际瓶颈选择使用。 但这些方法解决的是不同问题:LOD 主要控制几何细节,实例化主要减少重复对象调用,纹理压缩主要降低资源体积,渐进加载改善的是加载过程,Points 则适合不承担实体表面的点状数据。
React 侧还要检查两个常见问题:一是把 Three.js 对象直接放进 state,导致每次业务更新都触发大范围渲染;二是 effect 依赖数组频繁变化,使场景被反复销毁和重建。场景对象应保存在 ref 中,业务状态只保存 ID、筛选条件和必要的显示参数。
资源释放至少包括:
geometry.dispose();
material.dispose();
texture.dispose();
renderer.dispose();
controls.dispose();
同时移除 resize、pointermove、click 和 WebSocket 监听器。阴影贴图、渲染目标以及后处理 Pass 使用的资源也要单独检查。
最终怎样验收并安全上线?
验收不能只写“页面正常”或“效果不错”,每项都应留下能够复查的证据。
| 验收模块 | 测试动作 | 通过标准与证据 |
|---|---|---|
| MCP 最小闭环 | 查询、创建、重命名、保存、导出 | 返回结果与 Blender 场景一致,导出文件存在 |
| 模型加载 | 刷新 React 页面 | 模型出现,Network 中模型及纹理请求成功 |
| ID 映射 | 点击床位和设备 | 回传 ID 与业务清单一致 |
| 状态更新 | 切换正常、占用、维护和告警数据 | 目标对象更新,其他对象不受影响 |
| 悬停与点击 | 划过楼层、科室和设备 | 提示、选中态与详情面板正确 |
| 楼层与科室筛选 | 组合多个筛选条件 | 场景显隐与 DataV 数据一致 |
| 镜头聚焦 | 点击设备并复位 | 镜头到达目标,复位恢复初始视角 |
| DataV 同步 | 选择对象并切换图表条件 | 图表与场景使用同一筛选结果 |
| 错误兜底 | 临时使用无效模型地址 | 页面显示错误或静态视图,不出现空白页 |
| 全屏适配 | 在不同窗口比例下切换全屏 | 面板不重叠,拾取位置不偏移 |
| 资源释放 | 多次进入并离开场景页面 | 不重复创建 renderer,内存不持续增长 |
构建与本地复查可以这样执行:
npm run build
npm run preview -- --host 127.0.0.1
非功能项也要留下原始记录:
- 构建命令的退出状态和产物目录。
- 浏览器控制台错误与警告。
- Network 中失败或异常的模型、纹理和接口请求。
- 固定浏览器、窗口尺寸、像素比和采样时长下的帧率记录。
- JavaScript 堆、GPU 资源和 Three.js geometry、texture 数量。
- glTF、bin、纹理、脚本与总资源体积。
- 模型面数、材质数、draw calls 和初始加载时间。
安全检查应围绕演示数据和部署边界展开。样例数据只保留床位、设备、楼层、科室和状态,不包含患者姓名、住院号、诊断或联系方式;接口与静态资源部署不能携带密钥;Blender MCP 只开放给受信任的本机进程;访问日志不记录完整业务载荷。React 与 Three.js 组合已有实践可供参考,但生产环境仍需单独完成权限和数据检查。
现有材料没有附上这次构建的终端输出、浏览器控制台、帧率、内存或资源体积,因此本文不把这些项目写成已经通过的数值。建模、导出、加载、状态映射、交互、性能和部署检查已有明确顺序,最终发布状态仍须由实际环境中的构建产物、浏览器复查和运行记录确认。
[email protected]
。
我们承诺在核实信息后,将第一时间删除相关内容或断开链接。






















暂无评论内容