Overlay 3D images

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>

did you find a solution for this?