Slow Performance for 3D Texture Volume Shader

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.