three.js渲染3D Slicer文件

Aditya2024-01-31图像渲染相关Three

引子

本文主要内容是如何用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);
}
  1. camera.aspect = container.value.offsetWidth / container.value.offsetHeight;: 这一行代码更新相机的宽高比,以匹配新的窗口大小。相机的宽高比定义了视景体的形状,确保在窗口大小变化时场景不会变形。
  2. camera.updateProjectionMatrix();: 这一行代码更新相机的投影矩阵。投影矩阵定义了摄像机将3D场景投影到2D视口的方式。在窗口大小变化时,投影矩阵需要更新,以确保透视效果和视景体比例正确。
  3. 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的图片,两者看起来还有差距,但也不影响查看,就先这样,后面慢慢优化

Last Updated 2024/12/27 11:36:49