import { Sphere } from "./WebGL/shapes";
import { Vector3, Matrix4, XYZ } from "./WebGL/math";
import { VolumetricMaterial, Mesh, Texture, Object3D, Material } from "./WebGL";
import { CC } from "./colors";
import { GLShape } from "./GLShape";
import { AtomSelectionSpec } from "specs";
import { GLViewer } from "GLViewer";
/**
* VolumetricRenderer style specification
*/
export interface VolumetricRendererSpec {
/** list of objects containing @color, @opacity and @value properties to specify color per voxel data value */
transferfn?: { color: unknown; opacity: unknown; value: unknown }[];
/** number of times to sample each voxel approximately (default 5) */
subsamples?: number;
/** coordinates around which to include data; use viewer.selectedAtoms() to convert an AtomSelectionSpec to coordinates */
coords?: XYZ[];
/** selection around which to include data */
selection?: AtomSelectionSpec;
/** distance around coords to include data [default = 2.0] */
seldist?: number;
};
/**
* A GLVolumetricRender is a "shape" for representing volumetric data as a density distribution.
*
* @class
*
* @param {VolumeData} data - volumetric data
* @param {VolumetricRenderSpec} spec - specification of volumetric render
* @returns {$3Dmol.GLShape}
*/
export class GLVolumetricRender {
static interpolateArray(data: string | any[], fitCount: number) {
function linearInterpolate(before: number, after: number, atPoint: number) {
return before + (after - before) * atPoint;
}
var newData = [];
var springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0]; // for new allocation
for (var i = 1; i < fitCount - 1; i++) {
var tmp = i * springFactor;
var before = Math.floor(tmp);
var after = Math.ceil(tmp);
var atPoint = tmp - before;
newData[i] = linearInterpolate(data[before], data[after], atPoint);
}
newData[fitCount - 1] = data[data.length - 1]; // for new allocation
return newData;
}
hidden = false;
boundingSphere = new Sphere();
shapePosition: any;
renderedShapeObj: any = null;
shapeObj: any = null;
geo: any;
subsamples = 5.0;
data: any = null;
transferfunctionbuffer: any = [];
min: number = 0;
max: number = 0;
extent: any;
maxdepth: number;
texmatrix: any;
minunit: any;
constructor(data: { matrix: { elements: any; }; size: XYZ;
unit: XYZ; origin: XYZ; data: number[]; getIndex: (arg0: number, arg1: number, arg2: number) => number; },
spec: VolumetricRendererSpec, viewer?: GLViewer) {
spec = spec || {};
var transferfn = Object.assign([], spec.transferfn);
this.subsamples = spec.subsamples || 5.0;
let TRANSFER_BUFFER_SIZE = 256;
// arrange points based on position property
transferfn.forEach(function (a: { value: any; }) { a.value = parseFloat(a.value); });
transferfn.sort(function (a: { value: number; }, b: { value: number; }) { return a.value - b.value; });
this.min = transferfn[0].value;
if (transferfn.length == 0) transferfn.push(transferfn[0]); //need at least two
this.max = transferfn[transferfn.length - 1].value;
// create and fill an array of interpolated values per 2 colors
var pos1, pos2, color1, color2, R, G, B, A, alpha1, alpha2;
for (let i = 0; i < transferfn.length - 1; i++) {
color1 = CC.color(transferfn[i].color);
color2 = CC.color(transferfn[i + 1].color);
alpha1 = transferfn[i].opacity;
alpha2 = transferfn[i + 1].opacity;
pos1 = Math.floor((transferfn[i].value - this.min) * TRANSFER_BUFFER_SIZE / (this.max - this.min));
pos2 = Math.floor((transferfn[i + 1].value - this.min) * TRANSFER_BUFFER_SIZE / (this.max - this.min));
if (pos1 == pos2)
continue;
R = GLVolumetricRender.interpolateArray([color1.r * 255, color2.r * 255], pos2 - pos1);
G = GLVolumetricRender.interpolateArray([color1.g * 255, color2.g * 255], pos2 - pos1);
B = GLVolumetricRender.interpolateArray([color1.b * 255, color2.b * 255], pos2 - pos1);
A = GLVolumetricRender.interpolateArray([alpha1 * 255, alpha2 * 255], pos2 - pos1);
for (let j = 0; j < R.length; j++) {
this.transferfunctionbuffer.push(R[j]);
this.transferfunctionbuffer.push(G[j]);
this.transferfunctionbuffer.push(B[j]);
this.transferfunctionbuffer.push(A[j]); // opacity will be added later
}
}
this.transferfunctionbuffer = new Uint8ClampedArray(this.transferfunctionbuffer);
//need to create transformation matrix that maps model points into
//texture space
// need extent (bounding box dimensions), maxdepth (box diagonal),
// texmatrix (conversion from model to texture coords), minunit,
// possibly non-orthnormal basis if matrix
if (data.matrix) {
//figure out bounding box of transformed grid
let start = new Vector3(0, 0, 0);
let end = new Vector3(data.size.x, data.size.y, data.size.z);
let unit = new Vector3(1, 1, 1);
start.applyMatrix4(data.matrix);
end.applyMatrix4(data.matrix);
unit.applyMatrix4(data.matrix).sub(start);
this.extent = [[start.x, start.y, start.z], [end.x, end.y, end.z]];
//check all corners, these may not be the farthest apart
for (let i = 1; i < 7; i++) {
end.x = (i & 1) ? data.size.x : 0;
end.y = (i & 2) ? data.size.y : 0;
end.z = (i & 4) ? data.size.z : 0;
end.applyMatrix4(data.matrix);
this.extent[0][0] = Math.min(this.extent[0][0], end.x);
this.extent[0][1] = Math.min(this.extent[0][1], end.y);
this.extent[0][2] = Math.min(this.extent[0][2], end.z);
this.extent[1][0] = Math.max(this.extent[1][0], end.x);
this.extent[1][1] = Math.max(this.extent[1][1], end.y);
this.extent[1][2] = Math.max(this.extent[1][2], end.z);
}
let xoff = end.x - start.x;
let yoff = end.y - start.y;
let zoff = end.z - start.z;
this.maxdepth = Math.sqrt(xoff * xoff + yoff * yoff + zoff * zoff);
this.minunit = Math.min(Math.min(unit.x, unit.y), unit.z);
//invert onto grid, then scale by grid dimensions to get
//normalized texture coordinates
this.texmatrix = new Matrix4().identity().scale({ x: data.size.x, y: data.size.y, z: data.size.z });
this.texmatrix = this.texmatrix.multiplyMatrices(data.matrix, this.texmatrix);
this.texmatrix = this.texmatrix.getInverse(this.texmatrix);
} else {
this.texmatrix = new Matrix4().identity();
let xoff = data.unit.x * data.size.x;
let yoff = data.unit.y * data.size.y;
let zoff = data.unit.z * data.size.z;
//scale doesn't apply to the translation vector, so preapply it
this.texmatrix.makeTranslation(-data.origin.x / xoff, -data.origin.y / yoff, -data.origin.z / zoff);
this.texmatrix.scale({ x: 1.0 / xoff, y: 1.0 / yoff, z: 1.0 / zoff });
this.minunit = Math.min(Math.min(data.unit.x, data.unit.y), data.unit.z);
//need the bounding box so we can intersect with rays
this.extent = [[data.origin.x, data.origin.y, data.origin.z],
[data.origin.x + xoff, data.origin.y + yoff, data.origin.z + zoff]];
this.maxdepth = Math.sqrt(xoff * xoff + yoff * yoff + zoff * zoff);
}
//use GLShape to construct box
var shape = new GLShape({});
shape.addBox({
corner: { x: this.extent[0][0], y: this.extent[0][1], z: this.extent[0][2] },
dimensions: {
w: this.extent[1][0] - this.extent[0][0],
h: this.extent[1][1] - this.extent[0][1],
d: this.extent[1][2] - this.extent[0][2]
}
});
this.geo = shape.finalize();
this.boundingSphere.center = new Vector3(
(this.extent[0][0] + this.extent[1][0]) / 2.0,
(this.extent[0][1] + this.extent[1][1]) / 2.0,
(this.extent[0][2] + this.extent[1][2]) / 2.0
);
this.boundingSphere.radius = this.maxdepth / 2;
if (spec.coords === undefined && spec.selection !== undefined) {
if(viewer) {
spec.coords = viewer.selectedAtoms(spec.selection) as XYZ[];
} else {
console.log('Need to provide viewer to volumetric renderer if selection specified.');
}
}
// volume selectivity based on given coords and distance
if (spec.coords !== undefined && spec.seldist !== undefined) {
let mask = new Uint8Array(data.data.length);
//for each coordinate
let d = spec.seldist;
let d2 = d * d;
for (let i = 0, n = spec.coords.length; i < n; i++) {
let c = spec.coords[i];
let minx = c.x - d, miny = c.y - d, minz = c.z - d;
let maxx = c.x + d, maxy = c.y + d, maxz = c.z + d;
if (data.getIndex(minx, miny, minz) >= 0 || data.getIndex(maxx, maxy, maxz) >= 0) {
//bounding box overlaps grid
//iterate over the grid points in the seldist bounding box
//minunit may be inefficient if axes have very different units. oh well.
for (let x = minx; x < maxx; x += this.minunit) {
for (let y = miny; y < maxy; y += this.minunit) {
for (let z = minz; z < maxz; z += this.minunit) {
let idx = data.getIndex(x, y, z);
if (idx >= 0 && !mask[idx]) {
//if not already masked, check distance
let distsq = (x - c.x) * (x - c.x) + (y - c.y) * (y - c.y) + (z - c.z) * (z - c.z);
if (distsq < d2) {
mask[idx] = 1;
}
}
}
}
}
}
}
//any place mask is zero, make infinite in data
for (let i = 0, n = data.data.length; i < n; i++) {
if (mask[i] == 0) data.data[i] = Infinity;
}
}
this.data = data;
}
/**
* Initialize webgl objects for rendering
* @param {Object3D} group
*
*/
globj(group: { remove: (arg0: any) => void; add: (arg0: any) => void; }) {
if (this.renderedShapeObj) {
group.remove(this.renderedShapeObj);
this.renderedShapeObj = null;
}
if (this.hidden)
return;
this.shapeObj = new Object3D();
var material = null;
var texture = new Texture(this.data, true);
var transfertexture = new Texture(this.transferfunctionbuffer, false);
texture.needsUpdate = true;
transfertexture.needsUpdate = true;
transfertexture.flipY = false;
material = new VolumetricMaterial({
transferfn: transfertexture,
transfermin: this.min,
transfermax: this.max,
map: texture,
extent: this.extent,
maxdepth: this.maxdepth,
texmatrix: this.texmatrix,
unit: this.minunit,
subsamples: this.subsamples,
});
var mesh = new Mesh(this.geo, (material as Material));
this.shapeObj.add(mesh);
this.renderedShapeObj = this.shapeObj.clone();
group.add(this.renderedShapeObj);
};
removegl(group: { remove: (arg0: any) => void; }) {
if (this.renderedShapeObj) {
// dispose of geos and materials
if (this.renderedShapeObj.geometry !== undefined)
this.renderedShapeObj.geometry.dispose();
if (this.renderedShapeObj.material !== undefined)
this.renderedShapeObj.material.dispose();
group.remove(this.renderedShapeObj);
this.renderedShapeObj = null;
}
this.shapeObj = null;
};
get position() {
return this.boundingSphere.center;
}
get x() {
return this.boundingSphere.center.x;
}
get y() {
return this.boundingSphere.center.y;
}
get z() {
return this.boundingSphere.center.z;
}
}