UNPKG

@chinhui/niivue

Version:

minimal webgl2 nifti image viewer

411 lines (390 loc) 11.5 kB
import { mat4, vec3 } from "gl-matrix"; /** * @class NiivueObject3D * @type NiivueObject3D * @typedef NiivueObject3D * @property {Shader[]} renderShaders * @property {boolean} isVisible * @property {WebGLVertexArrayObject} vertexBuffer * @property {number} indexCount * @property {WebGLVertexArrayObject} indexBuffer * @property {WebGLVertexArrayObject} textureCoordinateBuffer * @property {number} mode * @description Object rendered with WebGL * @constructor * @param {number} id * @param {WebGLVertexArrayObject} vertexBuffer * @param {number} mode * @param {number} indexCount * @param {WebGLVertexArrayObject} indexBuffer * @param {WebGLVertexArrayObject} textureCoordinateBuffer **/ export var NiivueObject3D = function ( id, vertexBuffer, mode, indexCount, indexBuffer = null, vao = null ) { this.BLEND = 1; this.CULL_FACE = 2; this.CULL_FRONT = 4; this.CULL_BACK = 8; this.ENABLE_DEPTH_TEST = 16; this.sphereIdx = []; this.sphereVtx = []; this.renderShaders = []; this.isVisible = true; this.isPickable = true; this.vertexBuffer = vertexBuffer; this.indexCount = indexCount; this.indexBuffer = indexBuffer; this.vao = vao; this.mode = mode; this.glFlags = 0; this.id = id; this.colorId = [ ((id >> 0) & 0xff) / 255.0, ((id >> 8) & 0xff) / 255.0, ((id >> 16) & 0xff) / 255.0, ((id >> 24) & 0xff) / 255.0, ]; this.modelMatrix = mat4.create(); this.scale = [1, 1, 1]; this.position = [0, 0, 0]; this.rotation = [0, 0, 0]; this.rotationRadians = 0.0; this.extentsMin = []; this.extentsMax = []; }; NiivueObject3D.generateCrosshairs = function ( gl, id, xyzMM, xyzMin, xyzMax, radius, sides = 20 ) { let geometry = this.generateCrosshairsGeometry( gl, xyzMM, xyzMin, xyzMax, radius, sides ); return new NiivueObject3D( id, geometry.vertexBuffer, gl.TRIANGLES, geometry.indexCount, geometry.indexBuffer, geometry.vao ); }; // not included in public docs NiivueObject3D.generateCrosshairsGeometry = function ( gl, xyzMM, xyzMin, xyzMax, radius, sides = 20 ) { let vertices = []; let indices = []; let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2]); let dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2]); NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides); start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2]); dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2]); NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides); start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2]); dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2]); NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides); //console.log('i:',indices.length / 3, 'v:',vertices.length / 3); let vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // index buffer allocated in parent class let indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW ); let vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); //vertex position: 3 floats X,Y,Z gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.bindVertexArray(null); // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl return { vertexBuffer, indexBuffer, indexCount: indices.length, vao, }; }; NiivueObject3D.getFirstPerpVector = function (v1) { let v2 = vec3.fromValues(0.0, 0.0, 0.0); if (v1[0] === 0.0) v2[0] = 1.0; else if (v1[1] === 0.0) v2[1] = 1.0; else if (v1[2] === 0.0) v2[2] = 1.0; else { // If xyz is all set, we set the z coordinate as first and second argument . // As the scalar product must be zero, we add the negated sum of x and y as third argument v2[0] = v1[2]; //scalp = z*x v2[1] = v1[2]; //scalp = z*(x+y) v2[2] = -(v1[0] + v1[1]); //scalp = z*(x+y)-z*(x+y) = 0 vec3.normalize(v2, v2); } return v2; }; NiivueObject3D.subdivide = function (verts, faces) { //Subdivide each triangle into four triangles, pushing verts to the unit sphere""" let nv = verts.length / 3; let nf = faces.length / 3; let n = nf; let vNew = vec3.create(); let nNew = vec3.create(); for (let faceIndex = 0; faceIndex < n; faceIndex++) { //setlength(verts, nv + 3); let fx = faces[faceIndex * 3 + 0]; let fy = faces[faceIndex * 3 + 1]; let fz = faces[faceIndex * 3 + 2]; let vx = vec3.fromValues( verts[fx * 3 + 0], verts[fx * 3 + 1], verts[fx * 3 + 2] ); let vy = vec3.fromValues( verts[fy * 3 + 0], verts[fy * 3 + 1], verts[fy * 3 + 2] ); let vz = vec3.fromValues( verts[fz * 3 + 0], verts[fz * 3 + 1], verts[fz * 3 + 2] ); vec3.add(vNew, vx, vy); vec3.normalize(nNew, vNew); verts.push(...nNew); vec3.add(vNew, vy, vz); vec3.normalize(nNew, vNew); verts.push(...nNew); vec3.add(vNew, vx, vz); vec3.normalize(nNew, vNew); verts.push(...nNew); //Split the current triangle into four smaller triangles: let face = [nv, nv + 1, nv + 2]; faces.push(...face); face = [fx, nv, nv + 2]; faces.push(...face); face = [nv, fy, nv + 1]; faces.push(...face); faces[faceIndex * 3 + 0] = nv + 2; faces[faceIndex * 3 + 1] = nv + 1; faces[faceIndex * 3 + 2] = fz; nf = nf + 3; nv = nv + 3; } }; NiivueObject3D.weldVertices = function (verts, faces) { //unify identical vertices let nv = verts.length / 3; //yikes: bubble sort! TO DO: see Surfice for more efficient solution let nUnique = 0; //first vertex is unique //var remap = new Array(); let remap = new Int32Array(nv); for (let i = 0; i < nv - 1; i++) { if (remap[i] !== 0) continue; //previously tested remap[i] = nUnique; let v = i * 3; let x = verts[v]; let y = verts[v + 1]; let z = verts[v + 2]; for (let j = i + 1; j < nv; j++) { v += 3; if (x === verts[v] && y === verts[v + 1] && z === verts[v + 2]) remap[j] = nUnique; } nUnique++; //another new vertex } //for i if (nUnique === nv) return verts; //console.log('welding vertices removed redundant positions ', nv, '->', nUnique); let nf = faces.length; for (let f = 0; f < nf; f++) faces[f] = remap[faces[f]]; let vtx = verts.slice(0, nUnique * 3 - 1); for (let i = 0; i < nv - 1; i++) { let v = i * 3; let r = remap[i] * 3; vtx[r] = verts[v]; vtx[r + 1] = verts[v + 1]; vtx[r + 2] = verts[v + 2]; } return vtx; }; NiivueObject3D.makeSphere = function ( vertices, indices, radius, origin = [0, 0, 0] ) { let vtx = []; let idx = []; if (this.sphereVtx !== undefined) { //only generate unit sphere once... vtx = this.sphereVtx.slice(); idx = this.sphereIdx.slice(); } else { vtx = [ 0.0, 0.0, 1.0, 0.894, 0.0, 0.447, 0.276, 0.851, 0.447, -0.724, 0.526, 0.447, -0.724, -0.526, 0.447, 0.276, -0.851, 0.447, 0.724, 0.526, -0.447, -0.276, 0.851, -0.447, -0.894, 0.0, -0.447, -0.276, -0.851, -0.447, 0.724, -0.526, -0.447, 0.0, 0.0, -1.0, ]; //let idx = new Uint16Array([ idx = [ 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 1, 7, 6, 11, 8, 7, 11, 9, 8, 11, 10, 9, 11, 6, 10, 11, 6, 2, 1, 7, 3, 2, 8, 4, 3, 9, 5, 4, 10, 1, 5, 6, 7, 2, 7, 8, 3, 8, 9, 4, 9, 10, 5, 10, 6, 1, ]; this.subdivide(vtx, idx); this.subdivide(vtx, idx); vtx = this.weldVertices(vtx, idx); this.sphereVtx = vtx.slice(); this.sphereIdx = idx.slice(); } for (let i = 0; i < vtx.length; i++) vtx[i] = vtx[i] * radius; let nvtx = vtx.length / 3; let j = 0; for (let i = 0; i < nvtx; i++) { vtx[j] = vtx[j] + origin[0]; j++; vtx[j] = vtx[j] + origin[1]; j++; vtx[j] = vtx[j] + origin[2]; j++; } let idx0 = Math.floor(vertices.length / 3); //first new vertex will be AFTER previous vertices for (let i = 0; i < idx.length; i++) idx[i] = idx[i] + idx0; indices.push(...idx); vertices.push(...vtx); }; NiivueObject3D.makeCylinder = function ( vertices, indices, start, dest, radius, sides = 20, endcaps = true ) { if (sides < 3) sides = 3; //prism is minimal 3D cylinder let v1 = vec3.create(); vec3.subtract(v1, dest, start); vec3.normalize(v1, v1); //principle axis of cylinder let v2 = NiivueObject3D.getFirstPerpVector(v1); //a unit length vector orthogonal to v1 // Get the second perp vector by cross product let v3 = vec3.create(); vec3.cross(v3, v1, v2); //a unit length vector orthogonal to v1 and v2 vec3.normalize(v3, v3); let num_v = 2 * sides; let num_f = 2 * sides; if (endcaps) { num_f += 2 * sides; num_v += 2; } let idx0 = Math.floor(vertices.length / 3); //first new vertex will be AFTER previous vertices let idx = new Uint16Array(num_f * 3); let vtx = new Float32Array(num_v * 3); function setV(i, vec3) { vtx[i * 3 + 0] = vec3[0]; vtx[i * 3 + 1] = vec3[1]; vtx[i * 3 + 2] = vec3[2]; } function setI(i, a, b, c) { idx[i * 3 + 0] = a + idx0; idx[i * 3 + 1] = b + idx0; idx[i * 3 + 2] = c + idx0; } let startPole = 2 * sides; let destPole = startPole + 1; if (endcaps) { setV(startPole, start); setV(destPole, dest); } let pt1 = vec3.create(); let pt2 = vec3.create(); for (let i = 0; i < sides; i++) { let c = Math.cos((i / sides) * 2 * Math.PI); let s = Math.sin((i / sides) * 2 * Math.PI); pt1[0] = radius * (c * v2[0] + s * v3[0]); pt1[1] = radius * (c * v2[1] + s * v3[1]); pt1[2] = radius * (c * v2[2] + s * v3[2]); vec3.add(pt2, start, pt1); setV(i, pt2); vec3.add(pt2, dest, pt1); setV(i + sides, pt2); let nxt = 0; if (i < sides - 1) nxt = i + 1; setI(i * 2, i, nxt, i + sides); setI(i * 2 + 1, nxt, nxt + sides, i + sides); if (endcaps) { setI(sides * 2 + i, i, startPole, nxt); setI(sides * 2 + i + sides, destPole, i + sides, nxt + sides); } } indices.push(...idx); vertices.push(...vtx); }; NiivueObject3D.makeColoredCylinder = function ( vertices, indices, colors, start, dest, radius, rgba255 = [192, 0, 0, 255], sides = 20, endcaps = false ) { let nv = vertices.length / 3; this.makeCylinder(vertices, indices, start, dest, radius, sides, endcaps); nv = vertices.length / 3 - nv; let clrs = []; for (let i = 0; i < nv * 4 - 1; i += 4) { clrs[i] = rgba255[0]; clrs[i + 1] = rgba255[1]; clrs[i + 2] = rgba255[2]; clrs[i + 3] = rgba255[3]; } colors.push(...clrs); }; NiivueObject3D.makeColoredSphere = function ( vertices, indices, colors, radius, origin = [0, 0, 0], rgba255 = [0, 0, 192, 255] ) { let nv = vertices.length / 3; this.makeSphere(vertices, indices, radius, origin); nv = vertices.length / 3 - nv; let clrs = []; for (let i = 0; i < nv * 4 - 1; i += 4) { clrs[i] = rgba255[0]; clrs[i + 1] = rgba255[1]; clrs[i + 2] = rgba255[2]; clrs[i + 3] = rgba255[3]; } colors.push(...clrs); };