Hello all,
I’m pretty new to threejs
so please forgive any dumb questions, I’ve been lurking on this forum for a while and everyone is always so helpful, so I’m looking for a solution for my performance issues.
I’m trying to apply this 3d texture example to an XCT (x-ray computed tomography) scan that I’ve converted into the NRRD file format. The dimensions are quite large: [351, 410, 610]
and the data array is also large: Float32Array (87785100)
which I’m sure is causing the performance issues.
I am adding ArcballControls
to the mesh, and rotating the mesh is very slow and laggy. From my research it appears that this is gated by the shaders, since the performance improves when I decrease the size of the container <div>
.
What I’ve tried:
- deleting “normal” attribute from the geometry
- using Buffer geometry
- setting
texture.needsUpdate
to false after updating it once - compressing the nrrd file to the maximum level I can
- adding “high-performance” and antialias to the
WebGLRenderer
- lowering the camera frustum height
Code:
import * as THREE from "three";
import { GUI } from "GUI";
import { ArcballControls } from "ArcballControls";
import { NRRDLoader } from "NRRDLoader";
import { VolumeRenderShader1 } from "VolumeShader";
let load_img = "assets/1537646449_159.nrrd"
const cm_gray = "assets/cm_gray.png"
const cm_viridis = "assets/cm_viridis.png"
let camera, scene, renderer, controls, container, material, volconfig, cmtextures;
container = document.getElementById("canvas");
let contain_w = container.offsetWidth
let contain_h = container.offsetHeight
init();
function init() {
scene = new THREE.Scene();
// Create renderer
let AA = true
if ( window.devicePixelRatio > 1 ) {
AA = false
}
renderer = new THREE.WebGLRenderer({powerPreference:"high-performance", antialias: AA});
renderer.setPixelRatio( container.devicePixelRatio );
renderer.setSize( contain_w, contain_h );
container.appendChild( renderer.domElement );
// Create camera (The volume renderer does not work very well with perspective yet)
const h = 256; // frustum height
const aspect = contain_w / contain_h;
camera = new THREE.OrthographicCamera( - h * aspect / 2, h * aspect / 2, h / 2, - h / 2, 1, 1000 );
camera.position.set( 0, 0, -1 );
camera.up.set( 0, 0, 1 ); // In our data, z is up
// The gui for interaction
volconfig = { clim1: 0, clim2: 1, renderstyle: 'iso', isothreshold: 0.15, colormap: 'viridis' };
const gui = new GUI();
gui.add( volconfig, 'clim1', 0, 1, 0.01 ).onChange( updateUniforms );
gui.add( volconfig, 'clim2', 0, 1, 0.01 ).onChange( updateUniforms );
gui.add( volconfig, 'colormap', { gray: 'gray', viridis: 'viridis' } ).onChange( updateUniforms );
gui.add( volconfig, 'renderstyle', { mip: 'mip', iso: 'iso' } ).onChange( updateUniforms );
gui.add( volconfig, 'isothreshold', 0, 4000, 10 ).onChange( updateUniforms );
let mesh
// Load the data ...
new NRRDLoader().load( load_img, 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.Data3DTexture( volume.data, volume.xLength, volume.yLength, volume.zLength );
texture.format = THREE.RedFormat;
texture.type = THREE.FloatType;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.unpackAlignment = 1;
texture.needsUpdate = true;
texture.needsUpdate = false;
// Colormap textures
cmtextures = {
viridis: new THREE.TextureLoader().load( cm_viridis, render ),
gray: new THREE.TextureLoader().load( cm_gray, render )
};
// Material
const shader = VolumeRenderShader1;
const uniforms = THREE.UniformsUtils.clone( shader.uniforms );
uniforms[ 'u_data' ].value = texture;
uniforms[ 'u_size' ].value.set( volume.xLength, volume.yLength, volume.zLength );
uniforms[ 'u_clim' ].value.set( volconfig.clim1, volconfig.clim2 );
uniforms[ 'u_renderstyle' ].value = volconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
uniforms[ 'u_renderthreshold' ].value = volconfig.isothreshold; // For ISO renderstyle
uniforms[ 'u_cmdata' ].value = cmtextures[ volconfig.colormap ];
material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
side: THREE.BackSide // The volume shader uses the backface as its "reference point"
} );
// THREE.Mesh
const geometry = new THREE.BoxBufferGeometry( volume.xLength, volume.yLength, volume.zLength );
console.log(geometry)
geometry.translate( volume.xLength / 2 - 0.5, volume.yLength / 2 - 0.5, volume.zLength / 2 - 0.5 );
geometry.deleteAttribute("normal");
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
render();
} );
// Create controls
controls = new ArcballControls( camera, renderer.domElement, mesh );
controls.addEventListener( 'change', render );
controls.target.set( 1, 1, 1 );
controls.minZoom = 0.1;
controls.maxZoom = 20;
controls.enablePan = true;
controls.update();
window.addEventListener( 'resize', onWindowResize );
}
function updateUniforms() {
material.uniforms[ 'u_clim' ].value.set( volconfig.clim1, volconfig.clim2 );
material.uniforms[ 'u_renderstyle' ].value = volconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO
material.uniforms[ 'u_renderthreshold' ].value = volconfig.isothreshold; // For ISO renderstyle
material.uniforms[ 'u_cmdata' ].value = cmtextures[ volconfig.colormap ];
render();
}
function onWindowResize() {
renderer.setSize( contain_w, contain_h );
const aspect = contain_w / contain_h;
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 );
}
Any help you all could provide would be huge, I’m just so green when it comes to both javascript and threejs that I’ve no clue what I can do if my specific issue hasn’t been answered on SO or here before lol.