Dies ist das Repository meines kleinen Portfolios.
Im Hintergrund läuft eine Planetensimulation, geschrieben in JavaScript und Three.js.
Die zu sehenden Texturen stammen von:
https://www.solarsystemscope.com/textures/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
672 lines
13 KiB
672 lines
13 KiB
import { |
|
FileLoader, |
|
Loader, |
|
Matrix4, |
|
Vector3 |
|
} from 'three'; |
|
import * as fflate from '../libs/fflate.module.js'; |
|
import { Volume } from '../misc/Volume.js'; |
|
|
|
class NRRDLoader extends Loader { |
|
|
|
constructor( manager ) { |
|
|
|
super( manager ); |
|
|
|
} |
|
|
|
load( url, onLoad, onProgress, onError ) { |
|
|
|
const scope = this; |
|
|
|
const loader = new FileLoader( scope.manager ); |
|
loader.setPath( scope.path ); |
|
loader.setResponseType( 'arraybuffer' ); |
|
loader.setRequestHeader( scope.requestHeader ); |
|
loader.setWithCredentials( scope.withCredentials ); |
|
loader.load( url, function ( data ) { |
|
|
|
try { |
|
|
|
onLoad( scope.parse( data ) ); |
|
|
|
} catch ( e ) { |
|
|
|
if ( onError ) { |
|
|
|
onError( e ); |
|
|
|
} else { |
|
|
|
console.error( e ); |
|
|
|
} |
|
|
|
scope.manager.itemError( url ); |
|
|
|
} |
|
|
|
}, onProgress, onError ); |
|
|
|
} |
|
|
|
parse( data ) { |
|
|
|
// this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X |
|
|
|
let _data = data; |
|
|
|
let _dataPointer = 0; |
|
|
|
const _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0; |
|
|
|
const _littleEndian = true; |
|
|
|
const headerObject = {}; |
|
|
|
function scan( type, chunks ) { |
|
|
|
if ( chunks === undefined || chunks === null ) { |
|
|
|
chunks = 1; |
|
|
|
} |
|
|
|
let _chunkSize = 1; |
|
let _array_type = Uint8Array; |
|
|
|
switch ( type ) { |
|
|
|
// 1 byte data types |
|
case 'uchar': |
|
break; |
|
case 'schar': |
|
_array_type = Int8Array; |
|
break; |
|
// 2 byte data types |
|
case 'ushort': |
|
_array_type = Uint16Array; |
|
_chunkSize = 2; |
|
break; |
|
case 'sshort': |
|
_array_type = Int16Array; |
|
_chunkSize = 2; |
|
break; |
|
// 4 byte data types |
|
case 'uint': |
|
_array_type = Uint32Array; |
|
_chunkSize = 4; |
|
break; |
|
case 'sint': |
|
_array_type = Int32Array; |
|
_chunkSize = 4; |
|
break; |
|
case 'float': |
|
_array_type = Float32Array; |
|
_chunkSize = 4; |
|
break; |
|
case 'complex': |
|
_array_type = Float64Array; |
|
_chunkSize = 8; |
|
break; |
|
case 'double': |
|
_array_type = Float64Array; |
|
_chunkSize = 8; |
|
break; |
|
|
|
} |
|
|
|
// increase the data pointer in-place |
|
let _bytes = new _array_type( _data.slice( _dataPointer, |
|
_dataPointer += chunks * _chunkSize ) ); |
|
|
|
// if required, flip the endianness of the bytes |
|
if ( _nativeLittleEndian != _littleEndian ) { |
|
|
|
// we need to flip here since the format doesn't match the native endianness |
|
_bytes = flipEndianness( _bytes, _chunkSize ); |
|
|
|
} |
|
|
|
if ( chunks == 1 ) { |
|
|
|
// if only one chunk was requested, just return one value |
|
return _bytes[ 0 ]; |
|
|
|
} |
|
|
|
// return the byte array |
|
return _bytes; |
|
|
|
} |
|
|
|
//Flips typed array endianness in-place. Based on https://github.com/kig/DataStream.js/blob/master/DataStream.js. |
|
|
|
function flipEndianness( array, chunkSize ) { |
|
|
|
const u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength ); |
|
for ( let i = 0; i < array.byteLength; i += chunkSize ) { |
|
|
|
for ( let j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) { |
|
|
|
const tmp = u8[ k ]; |
|
u8[ k ] = u8[ j ]; |
|
u8[ j ] = tmp; |
|
|
|
} |
|
|
|
} |
|
|
|
return array; |
|
|
|
} |
|
|
|
//parse the header |
|
function parseHeader( header ) { |
|
|
|
let data, field, fn, i, l, m, _i, _len; |
|
const lines = header.split( /\r?\n/ ); |
|
for ( _i = 0, _len = lines.length; _i < _len; _i ++ ) { |
|
|
|
l = lines[ _i ]; |
|
if ( l.match( /NRRD\d+/ ) ) { |
|
|
|
headerObject.isNrrd = true; |
|
|
|
} else if ( l.match( /^#/ ) ) { |
|
} else if ( m = l.match( /(.*):(.*)/ ) ) { |
|
|
|
field = m[ 1 ].trim(); |
|
data = m[ 2 ].trim(); |
|
fn = _fieldFunctions[ field ]; |
|
if ( fn ) { |
|
|
|
fn.call( headerObject, data ); |
|
|
|
} else { |
|
|
|
headerObject[ field ] = data; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if ( ! headerObject.isNrrd ) { |
|
|
|
throw new Error( 'Not an NRRD file' ); |
|
|
|
} |
|
|
|
if ( headerObject.encoding === 'bz2' || headerObject.encoding === 'bzip2' ) { |
|
|
|
throw new Error( 'Bzip is not supported' ); |
|
|
|
} |
|
|
|
if ( ! headerObject.vectors ) { |
|
|
|
//if no space direction is set, let's use the identity |
|
headerObject.vectors = [ ]; |
|
headerObject.vectors.push( [ 1, 0, 0 ] ); |
|
headerObject.vectors.push( [ 0, 1, 0 ] ); |
|
headerObject.vectors.push( [ 0, 0, 1 ] ); |
|
|
|
//apply spacing if defined |
|
if ( headerObject.spacings ) { |
|
|
|
for ( i = 0; i <= 2; i ++ ) { |
|
|
|
if ( ! isNaN( headerObject.spacings[ i ] ) ) { |
|
|
|
for ( let j = 0; j <= 2; j ++ ) { |
|
|
|
headerObject.vectors[ i ][ j ] *= headerObject.spacings[ i ]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
//parse the data when registred as one of this type : 'text', 'ascii', 'txt' |
|
function parseDataAsText( data, start, end ) { |
|
|
|
let number = ''; |
|
start = start || 0; |
|
end = end || data.length; |
|
let value; |
|
//length of the result is the product of the sizes |
|
const lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) { |
|
|
|
return previous * current; |
|
|
|
}, 1 ); |
|
|
|
let base = 10; |
|
if ( headerObject.encoding === 'hex' ) { |
|
|
|
base = 16; |
|
|
|
} |
|
|
|
const result = new headerObject.__array( lengthOfTheResult ); |
|
let resultIndex = 0; |
|
let parsingFunction = parseInt; |
|
if ( headerObject.__array === Float32Array || headerObject.__array === Float64Array ) { |
|
|
|
parsingFunction = parseFloat; |
|
|
|
} |
|
|
|
for ( let i = start; i < end; i ++ ) { |
|
|
|
value = data[ i ]; |
|
//if value is not a space |
|
if ( ( value < 9 || value > 13 ) && value !== 32 ) { |
|
|
|
number += String.fromCharCode( value ); |
|
|
|
} else { |
|
|
|
if ( number !== '' ) { |
|
|
|
result[ resultIndex ] = parsingFunction( number, base ); |
|
resultIndex ++; |
|
|
|
} |
|
|
|
number = ''; |
|
|
|
} |
|
|
|
} |
|
|
|
if ( number !== '' ) { |
|
|
|
result[ resultIndex ] = parsingFunction( number, base ); |
|
resultIndex ++; |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
|
const _bytes = scan( 'uchar', data.byteLength ); |
|
const _length = _bytes.length; |
|
let _header = null; |
|
let _data_start = 0; |
|
let i; |
|
for ( i = 1; i < _length; i ++ ) { |
|
|
|
if ( _bytes[ i - 1 ] == 10 && _bytes[ i ] == 10 ) { |
|
|
|
// we found two line breaks in a row |
|
// now we know what the header is |
|
_header = this.parseChars( _bytes, 0, i - 2 ); |
|
// this is were the data starts |
|
_data_start = i + 1; |
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
// parse the header |
|
parseHeader( _header ); |
|
|
|
_data = _bytes.subarray( _data_start ); // the data without header |
|
if ( headerObject.encoding.substring( 0, 2 ) === 'gz' ) { |
|
|
|
// we need to decompress the datastream |
|
// here we start the unzipping and get a typed Uint8Array back |
|
_data = fflate.gunzipSync( new Uint8Array( _data ) );// eslint-disable-line no-undef |
|
|
|
} else if ( headerObject.encoding === 'ascii' || headerObject.encoding === 'text' || headerObject.encoding === 'txt' || headerObject.encoding === 'hex' ) { |
|
|
|
_data = parseDataAsText( _data ); |
|
|
|
} else if ( headerObject.encoding === 'raw' ) { |
|
|
|
//we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header |
|
const _copy = new Uint8Array( _data.length ); |
|
|
|
for ( let i = 0; i < _data.length; i ++ ) { |
|
|
|
_copy[ i ] = _data[ i ]; |
|
|
|
} |
|
|
|
_data = _copy; |
|
|
|
} |
|
|
|
// .. let's use the underlying array buffer |
|
_data = _data.buffer; |
|
|
|
const volume = new Volume(); |
|
volume.header = headerObject; |
|
// |
|
// parse the (unzipped) data to a datastream of the correct type |
|
// |
|
volume.data = new headerObject.__array( _data ); |
|
// get the min and max intensities |
|
const min_max = volume.computeMinMax(); |
|
const min = min_max[ 0 ]; |
|
const max = min_max[ 1 ]; |
|
// attach the scalar range to the volume |
|
volume.windowLow = min; |
|
volume.windowHigh = max; |
|
|
|
// get the image dimensions |
|
volume.dimensions = [ headerObject.sizes[ 0 ], headerObject.sizes[ 1 ], headerObject.sizes[ 2 ] ]; |
|
volume.xLength = volume.dimensions[ 0 ]; |
|
volume.yLength = volume.dimensions[ 1 ]; |
|
volume.zLength = volume.dimensions[ 2 ]; |
|
|
|
// Identify axis order in the space-directions matrix from the header if possible. |
|
if ( headerObject.vectors ) { |
|
|
|
const xIndex = headerObject.vectors.findIndex( vector => vector[ 0 ] !== 0 ); |
|
const yIndex = headerObject.vectors.findIndex( vector => vector[ 1 ] !== 0 ); |
|
const zIndex = headerObject.vectors.findIndex( vector => vector[ 2 ] !== 0 ); |
|
|
|
const axisOrder = []; |
|
axisOrder[ xIndex ] = 'x'; |
|
axisOrder[ yIndex ] = 'y'; |
|
axisOrder[ zIndex ] = 'z'; |
|
volume.axisOrder = axisOrder; |
|
|
|
} else { |
|
|
|
volume.axisOrder = [ 'x', 'y', 'z' ]; |
|
|
|
} |
|
|
|
// spacing |
|
const spacingX = new Vector3().fromArray( headerObject.vectors[ 0 ] ).length(); |
|
const spacingY = new Vector3().fromArray( headerObject.vectors[ 1 ] ).length(); |
|
const spacingZ = new Vector3().fromArray( headerObject.vectors[ 2 ] ).length(); |
|
volume.spacing = [ spacingX, spacingY, spacingZ ]; |
|
|
|
|
|
// Create IJKtoRAS matrix |
|
volume.matrix = new Matrix4(); |
|
|
|
const transitionMatrix = new Matrix4(); |
|
|
|
if ( headerObject.space === 'left-posterior-superior' ) { |
|
|
|
transitionMatrix.set( |
|
- 1, 0, 0, 0, |
|
0, - 1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1 |
|
); |
|
|
|
} else if ( headerObject.space === 'left-anterior-superior' ) { |
|
|
|
transitionMatrix.set( |
|
1, 0, 0, 0, |
|
0, 1, 0, 0, |
|
0, 0, - 1, 0, |
|
0, 0, 0, 1 |
|
); |
|
|
|
} |
|
|
|
|
|
if ( ! headerObject.vectors ) { |
|
|
|
volume.matrix.set( |
|
1, 0, 0, 0, |
|
0, 1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1 ); |
|
|
|
} else { |
|
|
|
const v = headerObject.vectors; |
|
|
|
const ijk_to_transition = new Matrix4().set( |
|
v[ 0 ][ 0 ], v[ 1 ][ 0 ], v[ 2 ][ 0 ], 0, |
|
v[ 0 ][ 1 ], v[ 1 ][ 1 ], v[ 2 ][ 1 ], 0, |
|
v[ 0 ][ 2 ], v[ 1 ][ 2 ], v[ 2 ][ 2 ], 0, |
|
0, 0, 0, 1 |
|
); |
|
|
|
const transition_to_ras = new Matrix4().multiplyMatrices( ijk_to_transition, transitionMatrix ); |
|
|
|
volume.matrix = transition_to_ras; |
|
|
|
} |
|
|
|
volume.inverseMatrix = new Matrix4(); |
|
volume.inverseMatrix.copy( volume.matrix ).invert(); |
|
volume.RASDimensions = new Vector3( volume.xLength, volume.yLength, volume.zLength ).applyMatrix4( volume.matrix ).round().toArray().map( Math.abs ); |
|
|
|
// .. and set the default threshold |
|
// only if the threshold was not already set |
|
if ( volume.lowerThreshold === - Infinity ) { |
|
|
|
volume.lowerThreshold = min; |
|
|
|
} |
|
|
|
if ( volume.upperThreshold === Infinity ) { |
|
|
|
volume.upperThreshold = max; |
|
|
|
} |
|
|
|
return volume; |
|
|
|
} |
|
|
|
parseChars( array, start, end ) { |
|
|
|
// without borders, use the whole array |
|
if ( start === undefined ) { |
|
|
|
start = 0; |
|
|
|
} |
|
|
|
if ( end === undefined ) { |
|
|
|
end = array.length; |
|
|
|
} |
|
|
|
let output = ''; |
|
// create and append the chars |
|
let i = 0; |
|
for ( i = start; i < end; ++ i ) { |
|
|
|
output += String.fromCharCode( array[ i ] ); |
|
|
|
} |
|
|
|
return output; |
|
|
|
} |
|
|
|
} |
|
|
|
const _fieldFunctions = { |
|
|
|
type: function ( data ) { |
|
|
|
switch ( data ) { |
|
|
|
case 'uchar': |
|
case 'unsigned char': |
|
case 'uint8': |
|
case 'uint8_t': |
|
this.__array = Uint8Array; |
|
break; |
|
case 'signed char': |
|
case 'int8': |
|
case 'int8_t': |
|
this.__array = Int8Array; |
|
break; |
|
case 'short': |
|
case 'short int': |
|
case 'signed short': |
|
case 'signed short int': |
|
case 'int16': |
|
case 'int16_t': |
|
this.__array = Int16Array; |
|
break; |
|
case 'ushort': |
|
case 'unsigned short': |
|
case 'unsigned short int': |
|
case 'uint16': |
|
case 'uint16_t': |
|
this.__array = Uint16Array; |
|
break; |
|
case 'int': |
|
case 'signed int': |
|
case 'int32': |
|
case 'int32_t': |
|
this.__array = Int32Array; |
|
break; |
|
case 'uint': |
|
case 'unsigned int': |
|
case 'uint32': |
|
case 'uint32_t': |
|
this.__array = Uint32Array; |
|
break; |
|
case 'float': |
|
this.__array = Float32Array; |
|
break; |
|
case 'double': |
|
this.__array = Float64Array; |
|
break; |
|
default: |
|
throw new Error( 'Unsupported NRRD data type: ' + data ); |
|
|
|
} |
|
|
|
return this.type = data; |
|
|
|
}, |
|
|
|
endian: function ( data ) { |
|
|
|
return this.endian = data; |
|
|
|
}, |
|
|
|
encoding: function ( data ) { |
|
|
|
return this.encoding = data; |
|
|
|
}, |
|
|
|
dimension: function ( data ) { |
|
|
|
return this.dim = parseInt( data, 10 ); |
|
|
|
}, |
|
|
|
sizes: function ( data ) { |
|
|
|
let i; |
|
return this.sizes = ( function () { |
|
|
|
const _ref = data.split( /\s+/ ); |
|
const _results = []; |
|
|
|
for ( let _i = 0, _len = _ref.length; _i < _len; _i ++ ) { |
|
|
|
i = _ref[ _i ]; |
|
_results.push( parseInt( i, 10 ) ); |
|
|
|
} |
|
|
|
return _results; |
|
|
|
} )(); |
|
|
|
}, |
|
|
|
space: function ( data ) { |
|
|
|
return this.space = data; |
|
|
|
}, |
|
|
|
'space origin': function ( data ) { |
|
|
|
return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' ); |
|
|
|
}, |
|
|
|
'space directions': function ( data ) { |
|
|
|
let f, v; |
|
const parts = data.match( /\(.*?\)/g ); |
|
return this.vectors = ( function () { |
|
|
|
const _results = []; |
|
|
|
for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { |
|
|
|
v = parts[ _i ]; |
|
_results.push( ( function () { |
|
|
|
const _ref = v.slice( 1, - 1 ).split( /,/ ); |
|
const _results2 = []; |
|
|
|
for ( let _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) { |
|
|
|
f = _ref[ _j ]; |
|
_results2.push( parseFloat( f ) ); |
|
|
|
} |
|
|
|
return _results2; |
|
|
|
} )() ); |
|
|
|
} |
|
|
|
return _results; |
|
|
|
} )(); |
|
|
|
}, |
|
|
|
spacings: function ( data ) { |
|
|
|
let f; |
|
const parts = data.split( /\s+/ ); |
|
return this.spacings = ( function () { |
|
|
|
const _results = []; |
|
|
|
for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) { |
|
|
|
f = parts[ _i ]; |
|
_results.push( parseFloat( f ) ); |
|
|
|
} |
|
|
|
return _results; |
|
|
|
} )(); |
|
|
|
} |
|
|
|
}; |
|
|
|
export { NRRDLoader };
|
|
|