I am trying to create an overlay between CT scan, radiation dosage and anatomical segmentations used in radiotherapy treatment. Here is my sample code. I am having difficulty applying transperancy to textures. I am using VolumeRenderShader1 from ‘./jsm/shaders/VolumeShader.js’. I have uploaded the sample data here. Please update dataLocation
to point to the correct location. Thanks!
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - volume rendering example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://github.com/cerr/CERR" target="_blank" rel="noopener">cerr/omt/0617</a> - Safe vs Risky dose distributions
</div>
<div id="inset"></div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { GUI } from './jsm/libs/dat.gui.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { NRRDLoader } from './jsm/loaders/NRRDLoader.js';
import { VolumeRenderShader1 } from './jsm/shaders/VolumeShader.js';
import { WEBGL } from './jsm/WebGL.js';
import { XYZLoader } from './jsm/loaders/XYZLoader.js';
import { STLLoader } from './jsm/loaders/STLLoader.js';
if ( WEBGL.isWebGL2Available() === false ) {
document.body.appendChild( WEBGL.getWebGL2ErrorMessage() );
}
let renderer,
scene,
camera,
controls,
material1,
material2,
material0,
volconfig,
scanconfig,
doseconfig,
structconfig,
points,
rtObj,
cmtextures;
const dataLocation = 'models/nrrd/cerr_lung_example/';
const structurefiles = ["Lung_IPSI.stl","Lung_CONTRA.stl","LIVER.stl",
"HEART.stl","ESOPHAGUS.stl"];
const risk_dose = 'FINALHETERO.nrrd'
const safe_dose = 'FINALHETERO.nrrd'
const scan_name = 'scan.nrrd'
init();
function init() {
rtObj = new THREE.Object3D();
scene = new THREE.Scene();
// Create renderer
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
// Create camera (The volume renderer does not work very well with perspective yet)
const h = 512; // frustum height
const aspect = window.innerWidth / window.innerHeight;
camera = new THREE.OrthographicCamera( - h * aspect / 2, h * aspect / 2, h / 2, - h / 2, 1, 1000 );
camera.position.set( 0, 0, 128 );
camera.up.set( 0, 0, 1 ); // In our data, z is up
// Create controls
controls = new OrbitControls( camera, renderer.domElement );
controls.addEventListener( 'change', render );
controls.target.set( 164, 164, 228 );
controls.minZoom = 0.5;
controls.maxZoom = 4;
controls.update();
// scene.add( new AxesHelper( 128 ) );
// Lighting is baked into the shader a.t.m.
// let dirLight = new DirectionalLight( 0xffffff );
// The gui for interaction
const gui = new GUI();
scanconfig = {clim1: -500, clim2: 1000, renderstyle: 'iso', isothreshold: 200, colormap: 'gray'};
doseconfig = {dose: risk_dose, clim1: 0, clim2: 85, renderstyle: 'iso', isothreshold: 60, colormap: 'viridis'};
//const strname = 'Lung_IPSI_visible'
//structconfig = { Lung_IPSI_visible: true };
structconfig = {};
for (var i = 0; i < structurefiles.length; i++) {
var strlen = structurefiles[i].length
var keyStr = structurefiles[i].substring(0,strlen-4) + "_visible"
structconfig[keyStr] = false;
}
const scanSettings = gui.addFolder( 'Scan settings' ),
doseSettings = gui.addFolder( 'Dose settings' ),
structureSettings = gui.addFolder( 'Structure settings' );
scanSettings.add(scanconfig, 'clim1', -1000, 1500, 5).onChange( updateUniforms1 );
scanSettings.add(scanconfig, 'clim2', -1000, 1500, 5 ).onChange( updateUniforms1 );
scanSettings.add(scanconfig, 'colormap', { gray: 'gray', viridis: 'viridis' } ).onChange( updateUniforms1 );
scanSettings.add(scanconfig, 'renderstyle', { mip: 'mip', iso: 'iso' } ).onChange( updateUniforms1 );
scanSettings.add(scanconfig, 'isothreshold', -1000, 1500, 10 ).onChange( updateUniforms1 );
doseSettings.add(doseconfig, 'dose', { Safe: safe_dose, Risky: risk_dose } ).onChange( replaceDose );
doseSettings.add(doseconfig, 'clim1', 0, 85, 1 ).onChange( updateUniforms2 );
doseSettings.add(doseconfig, 'clim2', 0, 85, 1 ).onChange( updateUniforms2 );
doseSettings.add(doseconfig, 'colormap', { gray: 'gray', viridis: 'viridis' } ).onChange( updateUniforms2 );
doseSettings.add(doseconfig, 'renderstyle', { mip: 'mip', iso: 'iso' } ).onChange( updateUniforms2 );
doseSettings.add(doseconfig, 'isothreshold', 0, 85, 1 ).onChange( updateUniforms2 );
const globalPlane = new THREE.Plane( new THREE.Vector3( 0, 0, 1 ), 0.1 );
const globalPlanes = [ globalPlane ],
Empty = Object.freeze( [] );
renderer.clippingPlanes = Empty; // GUI sets it to globalPlanes
renderer.localClippingEnabled = true;
const folderGlobal = gui.addFolder( 'Global Clipping' ),
propsGlobal = {
get 'Enabled'() {
return renderer.clippingPlanes !== Empty;
},
set 'Enabled'( v ) {
renderer.clippingPlanes = v ? globalPlanes : Empty;
},
get 'Plane'() {
return globalPlane.constant;
},
set 'Plane'( v ) {
globalPlane.constant = v;
}
};
folderGlobal.add( propsGlobal, 'Enabled' );
folderGlobal.add( propsGlobal, 'Plane', 0, 200 );
// Material
const shader1 = VolumeRenderShader1;
material0 = new THREE.ShaderMaterial( {
vertexShader: shader1.vertexShader,
fragmentShader: shader1.fragmentShader,
transparent: true,
opacity: 0.2,
depthWrite: false,
side: THREE.BackSide // The volume shader uses the backface as its "reference point"
} );
// Load the data ...
//new NRRDLoader().load( "models/nrrd/stent.nrrd", function ( volume ) {
//new NRRDLoader().load( "models/nrrd/scan.nrrd", function ( volume ) {
new NRRDLoader().load( dataLocation + scan_name, function ( volume ) {
// Texture to hold the volume. We have scalars, so we put our data in the red channel.
// THREEJS will select R32F (33326) based on the THREE.RedFormat and THREE.FloatType.
// Also see https://www.khronos.org/registry/webgl/specs/latest/2.0/#TEXTURE_TYPES_FORMATS_FROM_DOM_ELEMENTS_TABLE
// TODO: look the dtype up in the volume metadata
const texture = new THREE.DataTexture3D( volume.data, volume.xLength, volume.yLength, volume.zLength );
texture.format = THREE.RedFormat;
texture.type = THREE.FloatType;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.unpackAlignment = 4;
//texture.center = [volume.RASDimensions[0]/2, volume.RASDimensions[1]/2, volume.RASDimensions[2]/2];
// Colormap textures
cmtextures = {
viridis: new THREE.TextureLoader().load( 'textures/cm_viridis.png', render ),
gray: new THREE.TextureLoader().load( 'textures/cm_gray.png', render )
};
// Material
//const shader1 = VolumeRenderShader1;
const uniforms1 = THREE.UniformsUtils.clone( shader1.uniforms );
uniforms1[ "u_data" ].value = texture;
uniforms1[ "u_size" ].value.set(volume.RASDimensions[0], volume.RASDimensions[1], volume.RASDimensions[2])
uniforms1[ "u_clim" ].value.set( scanconfig.clim1, scanconfig.clim2 );
uniforms1[ "u_renderstyle" ].value = scanconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
uniforms1[ "u_renderthreshold" ].value = scanconfig.isothreshold; // For ISO renderstyle
uniforms1[ "u_cmdata" ].value = cmtextures[ scanconfig.colormap ];
material1 = material0.clone();
material1.uniforms = uniforms1;
// THREE.Mesh
//const geometry1 = new THREE.BoxBufferGeometry( volume.xLength, volume.yLength, volume.zLength );
const geometry1 = new THREE.BoxBufferGeometry( volume.RASDimensions[0], volume.RASDimensions[1], volume.RASDimensions[2] );
geometry1.translate( volume.RASDimensions[0] / 2, volume.RASDimensions[1] / 2 , volume.RASDimensions[2] / 2 );
//geometry1.translate( volume.xLength / 2 - 0.5, volume.yLength / 2 - 0.5, volume.zLength / 2 - 0.5 );
//geometry1.center(volume.RASDimensions[0] / 2, volume.RASDimensions[1] / 2 , volume.RASDimensions[2] / 2);
const mesh1 = new THREE.Mesh( geometry1, material1 );
//scene.add( mesh1 );
mesh1.name = "scan";
rtObj.add(mesh1);
scene.add(rtObj);
//gui.add( volume, "upperThreshold", volume.min, volume.max, 1 ).name( "Upper Threshold" ).onChange( function () {
// volume.repaintAllSlices();
//} );
//const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
//const cube = new THREE.Mesh( geometry1, material );
//cube.visible = false;
//const box = new THREE.BoxHelper( cube );
//scene.add( box );
const lighta = new THREE.AmbientLight( {color: 0x404040, intensity: 1000} ); // soft white light
scene.add( lighta );
//render();
} );
const colors = [0x0C7BDC,0xFFC20A,0x26DCC3,
0xFBB4E3,0xE363D9,0x955FA8,0xB7D628,0x99FBC5,
0x4A3139, 0x324058, 0xC5CA22, 0x6A7C76, 0x415E7C,
0x7C416A, 0x8013D2, 0xD1F5B5];
for (var i = 0; i < structurefiles.length; i++) {
const strlen = structurefiles[i].length;
const strnam = structurefiles[i].substring(0,strlen-4); // + "_visible"
const loader = new STLLoader();
const colorVal = colors[i];
loader.load( dataLocation + structurefiles[i], function ( geometry ) {
const materialStr = new THREE.MeshLambertMaterial( { wireframe: true, morphTargets: false, side: THREE.DoubleSide, color: colorVal, transparent: true, opacity: 0.3} );
geometry.u_size = 2;
const mesh = new THREE.Mesh( geometry, materialStr );
structureSettings.add(structconfig, strnam+"_visible").name(strnam).onChange( function () {
//gui.add( visibilityControl, "visible" ).name( "Lung IPSI" ).onChange( function () {
mesh.visible = structconfig[strnam+"_visible"];
render();
//renderer.render( scene, camera );
} );
mesh.name = "structure";
mesh.visible = false;
rtObj.add(mesh)
} );
};
// add dose
replaceDose()
render();
window.addEventListener( 'resize', onWindowResize, false );
document.body.appendChild( renderer.domElement );
}
function replaceDose(){
//doseconfig.dose
const dosefile = dataLocation + doseconfig.dose; // "Bottom_10_cindices.nrrd";
// Load the data ...
//new NRRDLoader().load( "models/nrrd/stent.nrrd", function ( volume ) {
new NRRDLoader().load( dosefile, function ( volume ) {
// Texture to hold the volume. We have scalars, so we put our data in the red channel.
// THREEJS will select R32F (33326) based on the THREE.RedFormat and THREE.FloatType.
// Also see https://www.khronos.org/registry/webgl/specs/latest/2.0/#TEXTURE_TYPES_FORMATS_FROM_DOM_ELEMENTS_TABLE
// TODO: look the dtype up in the volume metadata
const texture = new THREE.DataTexture3D( volume.data, volume.xLength, volume.yLength, volume.zLength );
texture.format = THREE.RedFormat;
texture.type = THREE.FloatType;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.unpackAlignment = 4;
// Colormap textures
cmtextures = {
viridis: new THREE.TextureLoader().load( 'textures/cm_viridis.png', render ),
gray: new THREE.TextureLoader().load( 'textures/cm_gray.png', render )
};
// Material
const shader2 = VolumeRenderShader1;
const uniforms2 = THREE.UniformsUtils.clone( shader2.uniforms );
uniforms2[ "u_data" ].value = texture;
uniforms2[ "u_size" ].value.set( volume.RASDimensions[0], volume.RASDimensions[1], volume.RASDimensions[2] );
uniforms2[ "u_clim" ].value.set( doseconfig.clim1, doseconfig.clim2 );
uniforms2[ "u_renderstyle" ].value = doseconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
uniforms2[ "u_renderthreshold" ].value = doseconfig.isothreshold; // For ISO renderstyle
uniforms2[ "u_cmdata" ].value = cmtextures[ doseconfig.colormap ];
material2 = material0.clone();
material2.uniforms = uniforms2;
// THREE.Mesh
//const geometry2 = new THREE.BoxBufferGeometry( volume.xLength, volume.yLength, volume.zLength );
const geometry2 = new THREE.BoxBufferGeometry( volume.RASDimensions[0], volume.RASDimensions[1], volume.RASDimensions[2] );
geometry2.translate( volume.RASDimensions[0] / 2, volume.RASDimensions[1] / 2 , volume.RASDimensions[2] / 2 );
//geometry2.translate( volume.xLength / 2 - 0.5, volume.yLength / 2 - 0.5, volume.zLength / 2 - 0.5 );
//geometry2.center();
const mesh2 = new THREE.Mesh( geometry2, material2 );
mesh2.name = 'dose'
//scene.add( mesh2 );
rtObj.remove(rtObj.getObjectByName("dose"))
rtObj.add(mesh2);
rtObj.getObjectByName("dose").renderOrder = 1;
rtObj.getObjectByName("structure").renderOrder = 2;
rtObj.getObjectByName("scan").renderOrder = 0;
render();
} );
}
function updateUniforms1() {
material1.uniforms[ "u_clim" ].value.set( scanconfig.clim1, scanconfig.clim2 );
material1.uniforms[ "u_renderstyle" ].value = scanconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
material1.uniforms[ "u_renderthreshold" ].value = scanconfig.isothreshold; // For ISO renderstyle
material1.uniforms[ "u_cmdata" ].value = cmtextures[ scanconfig.colormap ];
render();
}
function updateUniforms2() {
material2.uniforms[ "u_clim" ].value.set( doseconfig.clim1, doseconfig.clim2 );
material2.uniforms[ "u_renderstyle" ].value = doseconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
material2.uniforms[ "u_renderthreshold" ].value = doseconfig.isothreshold; // For ISO renderstyle
material2.uniforms[ "u_cmdata" ].value = cmtextures[ doseconfig.colormap ];
render();
}
function onWindowResize() {
renderer.setSize( window.innerWidth, window.innerHeight );
const aspect = window.innerWidth / window.innerHeight;
const frustumHeight = camera.top - camera.bottom;
camera.left = - frustumHeight * aspect / 2;
camera.right = frustumHeight * aspect / 2;
camera.updateProjectionMatrix();
render();
}
function render() {
renderer.render( scene, camera );
}
</script>
</body>
</html>