NGL@1.0.0-beta.7 Home Manual Reference Source Gallery

src/surface/volume.js

/**
 * @file Volume
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import { Vector3, Box3, Matrix3, Matrix4 } from '../../lib/three.es6.js'

import { WorkerRegistry, ColormakerRegistry } from '../globals.js'
import { defaults } from '../utils.js'
import WorkerPool from '../worker/worker-pool.js'
import { VolumePicker } from '../utils/picker.js'
import {
  uniformArray, serialArray,
  arrayMin, arrayMax, arraySum, arrayMean, arrayRms
} from '../math/array-utils'
import MarchingCubes from './marching-cubes.js'
import { laplacianSmooth, computeVertexNormals } from './surface-utils.js'
import {
  applyMatrix4toVector3array, applyMatrix3toVector3array
} from '../math/vector-utils.js'
import { m3new, m3makeNormal } from '../math/matrix-utils.js'
import Surface from './surface.js'

function VolumeSurface (data, nx, ny, nz, atomindex) {
  var mc = new MarchingCubes(data, nx, ny, nz, atomindex)

  function getSurface (isolevel, smooth, box, matrix, contour, wrap) {
    const sd = mc.triangulate(isolevel, smooth, box, contour, wrap)
    if (smooth && !contour) {
      laplacianSmooth(sd.position, sd.index, smooth, true)
      sd.normal = computeVertexNormals(sd.position, sd.index)
    }
    if (matrix) {
      applyMatrix4toVector3array(matrix, sd.position)
      if (sd.normal) {
        const normalMatrix = m3new()
        m3makeNormal(normalMatrix, matrix)
        applyMatrix3toVector3array(normalMatrix, sd.normal)
      }
    }
    return sd
  }

  this.getSurface = getSurface
}
VolumeSurface.__deps = [
  laplacianSmooth, computeVertexNormals, MarchingCubes,
  applyMatrix4toVector3array, applyMatrix3toVector3array,
  m3new, m3makeNormal
]

WorkerRegistry.add('surf', function func (e, callback) {
  const a = e.data.args
  const p = e.data.params
  if (a) {
    /* global self */
    self.volsurf = new VolumeSurface(a[0], a[1], a[2], a[3], a[4])
  }
  if (p) {
    const sd = self.volsurf.getSurface(
            p.isolevel, p.smooth, p.box, p.matrix, p.contour, p.wrap
        )
    const transferList = [ sd.position.buffer, sd.index.buffer ]
    if (sd.normal) transferList.push(sd.normal.buffer)
    if (sd.atomindex) transferList.push(sd.atomindex.buffer)
    const data = {
      sd: sd,
      p: p
    }
    callback(data, transferList)
  }
}, [ VolumeSurface ])

/**
 * Volume
 */
class Volume {
  /**
   * Make Volume instance
   * @param {String} name - volume name
   * @param {String} path - source path
   * @param {Float32array} data - volume 3d grid
   * @param {Integer} nx - x dimension of the 3d volume
   * @param {Integer} ny - y dimension of the 3d volume
   * @param {Integer} nz - z dimension of the 3d volume
   * @param {Int32Array} atomindex - atom indices corresponding to the cells in the 3d grid
   */
  constructor (name, path, data, nx, ny, nz, atomindex) {
    this.name = name
    this.path = path

    this.matrix = new Matrix4()
    this.normalMatrix = new Matrix3()
    this.inverseMatrix = new Matrix4()
    this.center = new Vector3()
    this.boundingBox = new Box3()

    this.setData(data, nx, ny, nz, atomindex)
  }

  get type () { return 'Volume' }

  /**
   * set volume data
   * @param {Float32array} data - volume 3d grid
   * @param {Integer} nx - x dimension of the 3d volume
   * @param {Integer} ny - y dimension of the 3d volume
   * @param {Integer} nz - z dimension of the 3d volume
   * @param {Int32Array} atomindex - atom indices corresponding to the cells in the 3d grid
   * @return {undefined}
   */
  setData (data, nx, ny, nz, atomindex) {
    this.nx = nx || 1
    this.ny = ny || 1
    this.nz = nz || 1

    this.data = data || new Float32Array(1)
    this.setAtomindex(atomindex)

    delete this._position

    delete this._min
    delete this._max
    delete this._mean
    delete this._rms

    if (this.worker) this.worker.terminate()
  }

  /**
   * Set statistics, which can be different from the data in this volume,
   * if this volume is a slice of a bigger volume
   * @param {Number|undefined} min - minimum value of the whole data set
   * @param {Number|undefined} max - maximum value of the whole data set
   * @param {Number|undefined} mean - average value of the whole data set
   * @param {Number|undefined} rms - sigma value of the whole data set
   */
  setStats (min, max, mean, rms) {
    this._min = min
    this._max = max
    this._mean = mean
    this._rms = rms
  }

  /**
   * set transformation matrix
   * @param {Matrix4} matrix - 4x4 transformation matrix
   * @return {undefined}
   */
  setMatrix (matrix) {
    this.matrix.copy(matrix)

    const bb = this.boundingBox
    const v = this.center  // temporary re-purposing

    const x = this.nx - 1
    const y = this.ny - 1
    const z = this.nz - 1

    bb.makeEmpty()

    bb.expandByPoint(v.set(x, y, z))
    bb.expandByPoint(v.set(x, y, 0))
    bb.expandByPoint(v.set(x, 0, z))
    bb.expandByPoint(v.set(x, 0, 0))
    bb.expandByPoint(v.set(0, y, z))
    bb.expandByPoint(v.set(0, 0, z))
    bb.expandByPoint(v.set(0, y, 0))
    bb.expandByPoint(v.set(0, 0, 0))

    bb.applyMatrix4(this.matrix)
    bb.getCenter(this.center)

    // make normal matrix

    const me = this.matrix.elements
    const r0 = new Vector3(me[0], me[1], me[2])
    const r1 = new Vector3(me[4], me[5], me[6])
    const r2 = new Vector3(me[8], me[9], me[10])
    const cp = new Vector3()
    //        [ r0 ]       [ r1 x r2 ]
    // M3x3 = [ r1 ]   N = [ r2 x r0 ]
    //        [ r2 ]       [ r0 x r1 ]
    const ne = this.normalMatrix.elements
    cp.crossVectors(r1, r2)
    ne[ 0 ] = cp.x
    ne[ 1 ] = cp.y
    ne[ 2 ] = cp.z
    cp.crossVectors(r2, r0)
    ne[ 3 ] = cp.x
    ne[ 4 ] = cp.y
    ne[ 5 ] = cp.z
    cp.crossVectors(r0, r1)
    ne[ 6 ] = cp.x
    ne[ 7 ] = cp.y
    ne[ 8 ] = cp.z

    this.inverseMatrix.getInverse(this.matrix)
  }

  /**
   * set atom indices
   * @param {Int32Array} atomindex - atom indices corresponding to the cells in the 3d grid
   * @return {undefined}
     */
  setAtomindex (atomindex) {
    this.atomindex = atomindex
  }

  getBox (center, size, target) {
    if (!target) target = new Box3()

    target.set(center, center)
    target.expandByScalar(size)
    target.applyMatrix4(this.inverseMatrix)

    target.min.round()
    target.max.round()

    return target
  }

  _getBox (center, size) {
    if (!center || !size) return

    if (!this.__box) this.__box = new Box3()
    const box = this.getBox(center, size, this.__box)
    return [ box.min.toArray(), box.max.toArray() ]
  }

  _makeSurface (sd, isolevel, smooth) {
    const name = this.name + '@' + isolevel.toPrecision(2)
    const surface = new Surface(name, '', sd)
    surface.info.isolevel = isolevel
    surface.info.smooth = smooth
    surface.info.volume = this

    return surface
  }

  getSurface (isolevel, smooth, center, size, contour, wrap) {
    isolevel = isNaN(isolevel) ? this.getValueForSigma(2) : isolevel
    smooth = defaults(smooth, 0)

    //

    if (this.volsurf === undefined) {
      this.volsurf = new VolumeSurface(
        this.data, this.nx, this.ny, this.nz, this.atomindex
      )
    }

    const box = this._getBox(center, size)
    const sd = this.volsurf.getSurface(
      isolevel, smooth, box, this.matrix.elements, contour, wrap
    )

    return this._makeSurface(sd, isolevel, smooth)
  }

  getSurfaceWorker (isolevel, smooth, center, size, contour, wrap, callback) {
    isolevel = isNaN(isolevel) ? this.getValueForSigma(2) : isolevel
    smooth = smooth || 0

    //

    if (window.Worker) {
      if (this.workerPool === undefined) {
        this.workerPool = new WorkerPool('surf', 2)
      }

      const msg = {}
      const worker = this.workerPool.getNextWorker()

      if (worker.postCount === 0) {
        msg.args = [
          this.data, this.nx, this.ny, this.nz, this.atomindex
        ]
      }

      msg.params = {
        isolevel: isolevel,
        smooth: smooth,
        box: this._getBox(center, size),
        matrix: this.matrix.elements,
        contour: contour,
        wrap: wrap
      }

      worker.post(msg, undefined,
        e => {
          const sd = e.data.sd
          const p = e.data.p
          callback(this._makeSurface(sd, p.isolevel, p.smooth))
        },
        e => {
          console.warn(
            'Volume.getSurfaceWorker error - trying without worker', e
          )
          const surface = this.getSurface(isolevel, smooth, center, size, contour, wrap)
          callback(surface)
        }
      )
    } else {
      const surface = this.getSurface(isolevel, smooth, center, size, contour, wrap)
      callback(surface)
    }
  }

  getValueForSigma (sigma) {
    return this.mean + defaults(sigma, 2) * this.rms
  }

  getSigmaForValue (value) {
    return (defaults(value, 0) - this.mean) / this.rms
  }

  get position () {
    if (!this._position) {
      const nz = this.nz
      const ny = this.ny
      const nx = this.nx
      const position = new Float32Array(nx * ny * nz * 3)

      let p = 0
      for (let z = 0; z < nz; ++z) {
        for (let y = 0; y < ny; ++y) {
          for (let x = 0; x < nx; ++x) {
            position[ p + 0 ] = x
            position[ p + 1 ] = y
            position[ p + 2 ] = z
            p += 3
          }
        }
      }

      applyMatrix4toVector3array(this.matrix.elements, position)
      this._position = position
    }

    return this._position
  }

  getDataAtomindex () {
    return this.atomindex
  }

  getDataPosition () {
    return this.position
  }

  getDataColor (params) {
    const p = params || {}
    p.volume = this
    p.scale = p.scale || 'Spectral'
    p.domain = p.domain || [ this.min, this.max ]

    const colormaker = ColormakerRegistry.getScheme(p)

    const n = this.position.length / 3
    const array = new Float32Array(n * 3)

    // var atoms = p.structure.atoms;
    // var atomindex = this.atomindex;

    for (let i = 0; i < n; ++i) {
      colormaker.volumeColorToArray(i, array, i * 3)
      // a = atoms[ atomindex[ i ] ];
      // if( a ) colormaker.atomColorToArray( a, array, i * 3 );
    }

    return array
  }

  getDataPicking () {
    const picking = serialArray(this.position.length / 3)
    return new VolumePicker(picking, this)
  }

  getDataSize (size, scale) {
    const data = this.data
    const n = this.position.length / 3
    let array

    switch (size) {
      case 'value':
        array = new Float32Array(data)
        break

      case 'abs-value':
        array = new Float32Array(data)
        for (let i = 0; i < n; ++i) {
          array[ i ] = Math.abs(array[ i ])
        }
        break

      case 'value-min': {
        array = new Float32Array(data)
        const min = this.min
        for (let i = 0; i < n; ++i) {
          array[ i ] -= min
        }
        break
      }

      case 'deviation':
        array = new Float32Array(data)
        break

      default:
        array = uniformArray(n, size)
        break
    }

    if (scale !== 1.0) {
      for (let i = 0; i < n; ++i) {
        array[ i ] *= scale
      }
    }

    return array
  }

  get min () {
    if (this._min === undefined) {
      this._min = arrayMin(this.data)
    }
    return this._min
  }

  get max () {
    if (this._max === undefined) {
      this._max = arrayMax(this.data)
    }
    return this._max
  }

  get sum () {
    if (this._sum === undefined) {
      this._sum = arraySum(this.data)
    }
    return this._sum
  }

  get mean () {
    if (this._mean === undefined) {
      this._mean = arrayMean(this.data)
    }
    return this._mean
  }

  get rms () {
    if (this._rms === undefined) {
      this._rms = arrayRms(this.data)
    }
    return this._rms
  }

  clone () {
    const vol = new Volume(
      this.name,
      this.path,

      this.data,

      this.nx,
      this.ny,
      this.nz,

      this.atomindex
    )

    vol.matrix.copy(this.matrix)
    vol.header = Object.assign({}, this.header)

    return vol
  }

  dispose () {
    if (this.workerPool) this.workerPool.terminate()
  }
}

export default Volume

export {
    VolumeSurface
}