@niivue/niivue
Version:
minimal webgl2 nifti image viewer
204 lines (185 loc) • 8.45 kB
text/typescript
/**
* Volume texture management functions for handling GPU textures and texture operations.
*/
import { NVImage, NiiDataType, NiiIntentCode } from '@/nvimage'
import { Shader } from '@/shader'
import { log } from '@/logger'
// Texture unit constants
const TEXTURE9_ORIENT = 33993
/**
* Prepare layer data by extracting the appropriate frame for 4D volumes
* @param overlayItem - Volume to prepare
* @returns Image data for the current frame
*/
export function prepareLayerData(overlayItem: NVImage): any {
let img = overlayItem.img
if (overlayItem.frame4D > 0 && overlayItem.frame4D < overlayItem.nFrame4D!) {
img = overlayItem.img!.slice(overlayItem.frame4D * overlayItem.nVox3D!, (overlayItem.frame4D + 1) * overlayItem.nVox3D!)
}
return img
}
/**
* Result of checking image size limits
*/
export interface ImageSizeLimits {
isAboveMax2D: boolean
isAboveMax3D: boolean
}
/**
* Check if image dimensions exceed hardware texture size limits
* @param params - Parameters containing hdr and size limits
* @returns Object indicating if image exceeds 2D or 3D limits
*/
export function checkImageSizeLimits(params: { hdr: any; max2D: number; max3D: number }): ImageSizeLimits {
const { hdr, max2D, max3D } = params
const isAboveMax2D = hdr.dims[1] > max2D || hdr.dims[2] > max2D
const isAboveMax3D = hdr.dims[1] > max3D || hdr.dims[2] > max3D || hdr.dims[3] > max3D
return { isAboveMax2D, isAboveMax3D }
}
/**
* Parameters for creating a 2D RGBA texture from large scalar image
*/
export interface Create2DRGBATextureParams {
hdr: any
img: any
overlayItem: NVImage
crosshairPos: number[]
frac2vox: (frac: number[]) => number[]
colormap: (name: string) => Uint8ClampedArray
}
/**
* Create 2D RGBA texture data for large scalar images
* @param params - Texture creation parameters
* @returns RGBA texture data for current slice
*/
export function create2DRGBATextureData(params: Create2DRGBATextureParams): Uint8Array {
const { hdr, img, overlayItem, crosshairPos, frac2vox, colormap } = params
const nPix = hdr.dims[1] * hdr.dims[2]
const vox = frac2vox(crosshairPos)
const z = Math.min(Math.max(vox[2], 0), hdr.dims[3] - 1)
const zOffset = z * nPix
const img2D = new Uint8Array(nPix * 4)
const img2D_U32 = new Uint32Array(img2D.buffer)
const opacity = Math.floor(overlayItem.opacity * 255)
const scale = (255 * hdr.scl_slope) / (overlayItem.cal_max - overlayItem.cal_min)
const intercept = (255 * (hdr.scl_inter - overlayItem.cal_min)) / (overlayItem.cal_max - overlayItem.cal_min)
const cmap = new Uint8Array(colormap(overlayItem.colormap))
const cmap_U32 = new Uint32Array(cmap.buffer)
let j = -1
for (let i = 0; i < nPix; i++) {
const v = img[i + zOffset] * scale + intercept
const v255 = Math.round(Math.min(255, Math.max(0, v))) // Clamp to 0..255
img2D_U32[i] = cmap_U32[v255]
img2D[(j += 4)] = opacity
}
return img2D
}
/**
* Parameters for texture creation based on datatype
*/
export interface TextureCreationParams {
gl: WebGL2RenderingContext
hdr: any // NIfTI header
img: any // Image data
overlayItem: NVImage
layer: number
colormapTexture: WebGLTexture | null
volumes: NVImage[]
orientShaderAtlasU: Shader
orientShaderAtlasI: Shader
orientShaderU: Shader
orientShaderI: Shader
orientShaderF: Shader
orientShaderRGBU: Shader
orientShaderPAQD: Shader
paqdTexture: WebGLTexture | null
atlasOutline: number
atlasActiveIndex: number
backDims: number[]
}
/**
* Select appropriate shader based on image datatype and intent
* @param params - Texture creation parameters
* @returns Selected shader
*/
export function selectOrientShader(params: TextureCreationParams): Shader {
const { hdr, overlayItem, layer, volumes, orientShaderAtlasU, orientShaderAtlasI, orientShaderU, orientShaderI, orientShaderF, orientShaderRGBU, orientShaderPAQD } = params
let orientShader = orientShaderU
if (hdr.datatypeCode === NiiDataType.DT_UINT8) {
if (hdr.intent_code === NiiIntentCode.NIFTI_INTENT_LABEL) {
orientShader = orientShaderAtlasU
}
} else if (hdr.datatypeCode === NiiDataType.DT_INT16) {
orientShader = orientShaderI
if (hdr.intent_code === NiiIntentCode.NIFTI_INTENT_LABEL) {
orientShader = orientShaderAtlasI
}
} else if (hdr.datatypeCode === NiiDataType.DT_FLOAT32 || hdr.datatypeCode === NiiDataType.DT_FLOAT64) {
orientShader = orientShaderF
} else if (hdr.datatypeCode === NiiDataType.DT_RGB24) {
orientShader = orientShaderRGBU
} else if (hdr.datatypeCode === NiiDataType.DT_UINT16) {
if (hdr.intent_code === NiiIntentCode.NIFTI_INTENT_LABEL) {
orientShader = orientShaderAtlasU
}
} else if (hdr.datatypeCode === NiiDataType.DT_RGBA32) {
orientShader = orientShaderRGBU
if (overlayItem.colormapLabel) {
orientShader = orientShaderPAQD
let firstPAQD = true
for (let l = 0; l < layer; l++) {
const isRGBA = volumes[l].hdr.datatypeCode === NiiDataType.DT_RGBA32
const isLabel = !!volumes[l].colormapLabel
if (isRGBA && isLabel) {
firstPAQD = false
}
}
if (!firstPAQD) {
log.warn(`Current version only one probabilistic atlas (PAQD) at a time`)
}
}
}
return orientShader
}
/**
* Create and configure a 3D texture based on datatype
* @param params - Texture creation parameters
* @returns Configured WebGL texture
*/
export function create3DTextureByDatatype(params: TextureCreationParams): WebGLTexture | null {
const { gl, hdr, img } = params
const tempTex3D = gl.createTexture()
gl.activeTexture(TEXTURE9_ORIENT)
gl.bindTexture(gl.TEXTURE_3D, tempTex3D)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
// Configure texture based on datatype
if (hdr.datatypeCode === NiiDataType.DT_UINT8) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R8UI, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RED_INTEGER, gl.UNSIGNED_BYTE, img)
} else if (hdr.datatypeCode === NiiDataType.DT_INT16) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R16I, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RED_INTEGER, gl.SHORT, img)
} else if (hdr.datatypeCode === NiiDataType.DT_FLOAT32) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R32F, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RED, gl.FLOAT, img)
} else if (hdr.datatypeCode === NiiDataType.DT_FLOAT64) {
const img32f = Float32Array.from(img)
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R32F, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RED, gl.FLOAT, img32f)
} else if (hdr.datatypeCode === NiiDataType.DT_RGB24) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGB8UI, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RGB_INTEGER, gl.UNSIGNED_BYTE, img)
} else if (hdr.datatypeCode === NiiDataType.DT_UINT16) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R16UI, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RED_INTEGER, gl.UNSIGNED_SHORT, img)
} else if (hdr.datatypeCode === NiiDataType.DT_RGBA32) {
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGBA8UI, hdr.dims[1], hdr.dims[2], hdr.dims[3])
gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, hdr.dims[1], hdr.dims[2], hdr.dims[3], gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, img)
}
return tempTex3D
}