🔵前言
如果想要做web端的医学影像处理,如果不想从纯底层,例如webgl或者threejs开始做起,用好vtk.js就是必不可少的。这也是一个kitware组织开发的js库。
vtk.js的文档地址:
https://kitware.github.io/vtk-js/
机翻预警:
VTK.js是一个JavaScript库,可用于在浏览器中进行科学可视化。这个库可以通过NPM和(NPM ESM)或unpkg.com CDN来使用,所以它可以直接作为一个脚本标签导入到你的网页中。
本文会简单介绍如何在React代码中使用vtk.js渲染基础的图形。
📘准备环境
这里前端使用了umi3 的全套框架,我们先使用yarn指令获取最新的vtk.js
1
| $ yarn add @kitware/vtk.js
|
如果想在代码中使用vtk.js的代码,还需要修改webpack的配置。在umi中修改webpack的配置的方法是,使用chainWebpack,具体可以参考umi的文档,这里不多做赘述。
我们打开node_modules下vtk.js的包,发现它的目录下有一个chainWebpack的配置,可以直接粘贴过来。
这个文件的路径在 /node_modules/@kitware/vtk.js/Utilities/config/chainWebpack.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| export default (config: any) => { config.module .rule('vtk-glsl') .test(/\.glsl$/i) .include.add(/vtk\.js[/\\]Sources/) .end() .use('shader-loader') .loader('shader-loader') .end();
config.module .rule('vtk-js') .test(/\.js$/i) .include.add(/vtk\.js[/\\]Sources/) .end() .use('babel-loader') .loader('babel-loader') .end();
config.module .rule('vtk-worker') .test(/\.worker\.js$/) .include.add(/vtk\.js[/\\]Sources/) .end() .use('worker-loader') .loader('worker-loader') .options({ inline: 'no-fallback' }) .end();
config.module .rule('vtk-css') .test(/\.css$/) .exclude.add(/\.module\.css$/) .end() .include.add(/vtk\.js[/\\]Sources/) .end() .use('styles') .loader('style-loader') .loader('css-loader') .loader('postcss-loader') .end();
config.module .rule('vtk-svg') .test(/\.svg$/) .include.add(/vtk\.js[/\\]Sources/) .end() .use('raw-loader') .loader('raw-loader') .end();
config.module .rule('vtk-module-css') .test(/\.css$/) .include.add(/vtk\.js[/\\]Sources/) .add(/\.module\.css$/) .end() .use('styles') .loader('style-loader') .loader('css-loader') .options({ modules: { localIdentName: '[name]-[local]_[sha512:hash:base64:5]', }, }) .loader('postcss-loader') .end(); };
|
除了 shader-loader 是vtk.js自己就有的loader之外,我们可能还需要自行安装一些loader , 例如 babel-loader,注意部分loader的版本不要太新,否则可能可能会出现问题。
这里给出我目前的package配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| "devDependencies": { "@umijs/fabric": "^2.5.7", "@umijs/plugin-esbuild": "^1.1.0", "@umijs/preset-react": "^1.8.10", "babel-loader": "^8.2.2", "commitizen": "^4.2.4", "copy-webpack-plugin": "6", "cross-env": "^7.0.3", "css-loader": "5.2.1", "cz-conventional-changelog": "^3.3.0", "eslint": "^7.22.0", "lint-staged": "^10.5.4", "postcss-loader": "3.0.0", "prettier": "^2.2.1", "raw-loader": "4.0.2", "react-dev-inspector": "^1.5.1", "standard-version": "^9.3.0", "style-loader": "2.0.0", "webpack": "5.31.2", "webpack-cli": "4.6.0", "webpack-dev-server": "3.11.2", "webpack-merge": "5.7.3", "worker-loader": "3.0.8", "yorkie": "^2.0.0" },
|
📗现在可以写代码了
我们尝试实现一个最简单的 vtk.js的 demo
这里参考一下官方文档里面的例子,我第一次看vtk.js的文档还没有和react结合的例子,现在居然更了…
https://kitware.github.io/vtk-js/docs/vtk_react.html
我修改了部分代码,以适应现有的框架,例如UI的样式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| import { useState, useRef, useEffect, FC } from 'react';
import '@kitware/vtk.js/Rendering/Profiles/Geometry';
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource';
const Demo: React.FC<{}> = () => { const vtkContainerRef = useRef(null); const context = useRef<any | null>(null); const [coneResolution, setConeResolution] = useState(6); const [representation, setRepresentation] = useState(2);
useEffect(() => { if (!context.current) { const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ rootContainer: vtkContainerRef.current, containerStyle: { height: '640px', }, }); const coneSource = vtkConeSource.newInstance({ height: 1.0 });
const mapper = vtkMapper.newInstance(); mapper.setInputConnection(coneSource.getOutputPort());
const actor = vtkActor.newInstance(); actor.setMapper(mapper);
const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow();
renderer.addActor(actor); renderer.resetCamera(); renderWindow.render();
fullScreenRenderer.setResizeCallback(({ width, height }) => { console.log('resize'); });
context.current = { fullScreenRenderer, renderWindow, renderer, coneSource, actor, mapper, }; }
return () => { if (context.current) { const { fullScreenRenderer, coneSource, actor, mapper } = context.current; actor.delete(); mapper.delete(); coneSource.delete(); fullScreenRenderer.delete(); context.current = null; } }; }, [vtkContainerRef]);
useEffect(() => { if (context.current) { const { coneSource, renderWindow } = context.current; coneSource.setResolution(coneResolution); renderWindow.render(); } }, [coneResolution]);
useEffect(() => { if (context.current) { const { actor, renderWindow } = context.current; actor.getProperty().setRepresentation(representation); renderWindow.render(); } }, [representation]);
return ( <> <table style={{ position: 'absolute', margin: '30px', background: 'white', padding: '12px', }} > <tbody> <tr> <td> <select value={representation} style={{ width: '100%' }} onInput={(ev) => setRepresentation(Number(ev.target.value))} > <option value="0">Points</option> <option value="1">Wireframe</option> <option value="2">Surface</option> </select> </td> </tr> <tr> <td> <input type="range" min="4" max="80" value={coneResolution} onChange={(ev) => setConeResolution(Number(ev.target.value))} /> </td> </tr> </tbody> </table>
<div ref={vtkContainerRef} /> </> ); };
export default Demo;
|
📃渲染结果
📓代码解析
🔵React部分
获取网页Dom元素
1 2 3 4 5
| const vtkContainerRef = useRef(null);
...
<div ref={vtkContainerRef} />
|
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。
返回的 **ref 对象 ** 在组件的 整个生命周期内保持不变。
本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。
保存上下文的属性,这一部分值的改变不会触发重新渲染
1 2 3 4 5 6 7 8 9 10
| const context = useRef<any | null>(null);
context.current = { fullScreenRenderer, renderWindow, renderer, coneSource, actor, mapper, };
|
保存coneResolution和representation,这一部分值的改变会触发重新渲染!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const [coneResolution, setConeResolution] = useState(6); const [representation, setRepresentation] = useState(2);
...
useEffect(() => { if (context.current) { const { coneSource, renderWindow } = context.current; coneSource.setResolution(coneResolution); renderWindow.render(); } }, [coneResolution]);
useEffect(() => { if (context.current) { const { actor, renderWindow } = context.current; actor.getProperty().setRepresentation(representation); renderWindow.render(); } }, [representation]);
|
🔵vtk的渲染过程
步骤一,设置渲染窗口和渲染器
1 2 3 4 5 6 7 8 9
| const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ rootContainer: vtkContainerRef.current, containerStyle: { height: '640px', }, }); ... const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow();
|
步骤二,设置数据源
1
| const coneSource = vtkConeSource.newInstance({ height: 1.0 });
|
步骤三,为cone 设置渲染流水线
1 2 3 4 5 6 7 8 9
| const mapper = vtkMapper.newInstance(); mapper.setInputConnection(coneSource.getOutputPort());
const actor = vtkActor.newInstance(); actor.setMapper(mapper);
...
renderer.addActor(actor);
|
步骤四,重置摄像头,并渲染
1 2
| renderer.resetCamera(); renderWindow.render();
|
🔵vtk的渲染流水线
资料来源是官方的ppt
https://kitware.github.io/vtk-js/docs/tutorial.html
概要图
流水线
ConeSource提供数据
数据通过Mapper
Mapper被附加到Actor
Renderer包含许多actor
RenderWindow 包含许多渲染器
ConeSource
vtkConeSource是一个VTK过滤器
输出vtkPolyData的算法
ConeSource算法:0输入,1输出
流水线
- InputData / OutputData
- 静态:需要调用getOutputData() 来获取更新的数据
- InputConnection / OutputPort
- 响应式:响应式:获取过滤器链的输出数据将获取给定现有参数的最新数据
- 数据可以通过多个渲染管线