three.js渲染3D Slicer文件
引子
本文主要内容是如何用three.js加载3D Slicer生成的文件包,包括加载VTK文件、读取mrml文件中一些配置属性。mrml的理解比较关键,里面包含了对相机设置、背景色设置、灯光设置、vtk文件设置等等,里面配置信息很多,我做demo的文件多达1300行,但有用信息却没多少(完全理解不了)
three.js加载
初始化
function threeStart() {
camera = new THREE.PerspectiveCamera( 60, container.value.offsetWidth / container.value.offsetHeight, 0.1, 1000 );
// 设置相机位置
var cameraHeight = 600; // 相机的高度,可以根据需要调整
var lookAtPosition = new THREE.Vector3(0, 0, 0); // 设置相机的朝向中心
camera.position.set(0, cameraHeight, -10); // 将相机放置在模型上方
camera.lookAt(lookAtPosition); // 设置相机的朝向中心
// 设置场景
scene = new THREE.Scene();
// 设置背景颜色
const bgColors = options.mrmlInfo.value.globalInfo.layoutColor.split(/\s/g)
scene.background = new THREE.Color(bgColors[0], bgColors[1], bgColors[2])
scene.add( camera );
// 设置灯光
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x000000, 1 );
scene.add( hemiLight );
const dirLight = new THREE.DirectionalLight( 0xffffff, 1.5 );
dirLight.position.set( 100, 100, 100 ).normalize();
scene.add( dirLight );
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const loader = new VTKLoader();
group = new THREE.Group();
const meshPromise = options.bodyPartObj.value.map((item, i)=> {
//loader.addEventListener( 'load', function ( event ) {
return new Promise((resolve, reject) => {
console.log("正在加载", item.fileName)
loader.load( item.fileUrl, function ( geometry ) {
geometry.computeVertexNormals();
const colors = item.color.split(/\s/g)
const material = new THREE.MeshPhongMaterial( {
color: new THREE.Color(colors[0], colors[1], colors[2]),
opacity: item.opacity || 1, // 启用深度写入
depthWrite: item.depthWrite,
side: item.side || THREE.DoubleSide,
transparent: true, // 启用透明度
depthTest: true, // 启用深度测试
} );
if (item.blending) {
material.blending = item.blending
}
const mesh = new THREE.Mesh( geometry, material );
mesh.name = item.fileName
mesh.renderOrder = i
// 设置初始可见性
mesh.visible = item.visible;
group.add( mesh )
resolve(true)
});
})
})
Promise.all(meshPromise).then(() => {
loadedModel.value = true
centerGroup()
console.log("加载完了")
})
scene.add( group );
// renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio( container.value.devicePixelRatio );
renderer.setSize( container.value.offsetWidth, container.value.offsetHeight );
container.value.appendChild( renderer.domElement );
// controls = new OrbitControls( camera, renderer.domElement );
controls = new TrackballControls( camera, renderer.domElement );
controls.rotateSpeed = 5.0;
window.addEventListener( 'resize', onWindowResize );
}
模型居中问题
每个vtk文件都有位置信息,按道理加载后都会出现在相应位置,但实际上加载后部分vtk文件位置并没有相对于中心点去定位展示,这就需要重新去调整一下位置:
function centerGroup() {
var box3 = new THREE.Box3()
// 计算层级模型group的包围盒
// 模型group是加载一个三维模型返回的对象,包含多个网格模型
box3.expandByObject(group)
// 计算一个层级模型对应包围盒的几何体中心在世界坐标中的位置
var center = new THREE.Vector3()
box3.getCenter(center)
// console.log('查看几何体中心坐标', center);
// 重新设置模型的位置,使之居中。
group.position.x = group.position.x - center.x
group.position.y = group.position.y - center.y
group.position.z = group.position.z - center.z
}
窗口变动触发
/* 窗口变动触发 */
function onWindowResize() {
camera.aspect = container.value.offsetWidth / container.value.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.value.offsetWidth, container.value.offsetHeight);
}
camera.aspect = container.value.offsetWidth / container.value.offsetHeight;
: 这一行代码更新相机的宽高比,以匹配新的窗口大小。相机的宽高比定义了视景体的形状,确保在窗口大小变化时场景不会变形。camera.updateProjectionMatrix();
: 这一行代码更新相机的投影矩阵。投影矩阵定义了摄像机将3D场景投影到2D视口的方式。在窗口大小变化时,投影矩阵需要更新,以确保透视效果和视景体比例正确。renderer.setSize(container.value.offsetWidth, container.value.offsetHeight);
: 这一行代码更新渲染器的大小,确保它与新的窗口大小相匹配。渲染器负责将场景渲染到屏幕上。
整个函数 onWindowResize
会被注册到窗口的 resize
事件中,以确保在窗口大小变化时及时进行相应的更新,以保持场景的正确渲染。
控制器任意旋转问题
最开始初始化控制器是通过 new OrbitControls ,但突然发现它不能任意旋转,竟然在旋转模型上有限制,咨询了下chatgpt给我推荐了 TrackballControls。
如果你想要实现在 Three.js 中实现相机的任意旋转,
TrackballControls
是一个更灵活的选择,因为它允许用户在任意方向上自由旋转相机。相反,OrbitControls
限制了相机的上下旋转,以确保用户在垂直方向上不能翻转相机。
更新鼠标左键
function onUpdateMouseButton (id: string) {
if (!controls) return
if (id === "pan") {
// 将左键设置为平移
controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE,
};
} else if (id === "zoom") {
// 将左键设置为缩放
controls.mouseButtons = {
LEFT: THREE.MOUSE.DOLLY,
MIDDLE: THREE.MOUSE.ROTATE,
RIGHT: THREE.MOUSE.pan,
};
} else if (id === "stackScroll") {
// 将左键设置为旋转
controls.mouseButtons = {
LEFT: THREE.MOUSE.ROTATE,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.pan,
};
}
}
TrackballControls 更新左键的时候不能直接 controls.mouseButtons.LEFT = xx 去修改,需要同时更改右键和中键,否则不生效。
读取mrml文件
MRML(Markup and Resource Management Language)文件通常与3D医学图像处理软件Slicer相关联。Slicer是一个开源的医学图像处理平台,用于可视化和处理医学图像数据。
MRML文件是Slicer中用于描述场景、数据、操作和视图的XML文件。它包含了关于Slicer场景的信息,例如3D模型、图像、标记等。
它本质也是xml,利于理解我将它转成json去查看:
配合chatgpt挨个去理解每个属性的含义,挑选有用的,最终还是只能去Layout提取background,去Model提取ModelDisplay的id,可用材料属性也只有color、opacity、visible。
除却mrml中含有的属性,也有几个隐藏的属性需要去配置,例如side、depthWrith等。
material.side
在Three.js中,material.side
是用于设置材质(Material)的渲染面的属性。该属性决定了材质的渲染方式,即在渲染对象时,是渲染它的前面还是后面。
// THREE.DoubleSide: 渲染物体的正面和背面
material.side = THREE.DoubleSide;
depthWrite
部分部位是否开启深度写入(depthWrite)该属性对于实现遮挡、遮挡物体以及在透明物体上正确渲染等方面非常重要!
depthWrite
设置为true
时,表示深度信息将写入深度缓冲区。这意味着对象将影响渲染场景中的深度信息,并可能遮挡在其后的其他对象。这是默认设置。depthWrite
设置为false
时,表示深度信息不会写入深度缓冲区。对象将不会影响其他对象的深度排序,通常用于渲染半透明对象,以便正确显示它们。
大概意思就是不透明的开启,可以遮挡渲染在它后面的模型。
depthWrite: Number(obj.modelDisplay.opacity) !== 1 ? false : true
写在最后
左边是three.js渲染生成的,右侧是3D Slicer的图片,两者看起来还有差距,但也不影响查看,就先这样,后面慢慢优化