import { mat4, vec3, vec4 } from 'gl-matrix' type Geometry = { vertexBuffer: WebGLBuffer indexBuffer: WebGLBuffer indexCount: number vao: WebGLVertexArrayObject | null } /** * Represents a 3D object rendered with WebGL, including geometry, transformations, and rendering state. * Used internally by Niivue for rendering meshes and crosshairs. */ export class NiivueObject3D { static BLEND = 1 static CULL_FACE = 2 static CULL_FRONT = 4 static CULL_BACK = 8 static ENABLE_DEPTH_TEST = 16 sphereIdx: number[] = [] sphereVtx: number[] = [] renderShaders: number[] = [] isVisible = true isPickable = true vertexBuffer: WebGLVertexArrayObject indexCount: number indexBuffer: WebGLVertexArrayObject | null vao: WebGLVertexArrayObject | null mode: number glFlags = 0 id: number colorId: [number, number, number, number] modelMatrix = mat4.create() scale = [1, 1, 1] position = [0, 0, 0] rotation = [0, 0, 0] rotationRadians = 0.0 extentsMin: number[] = [] extentsMax: number[] = [] // TODO needed through NVImage furthestVertexFromOrigin?: number originNegate?: vec3 fieldOfViewDeObliqueMM?: vec3 // TODO needed through crosshairs in NiiVue mm?: vec4 constructor(id: number, vertexBuffer: WebGLBuffer, mode: number, indexCount: number, indexBuffer: WebGLVertexArrayObject | null = null, vao: WebGLVertexArrayObject | null = null) { this.vertexBuffer = vertexBuffer this.indexCount = indexCount this.indexBuffer = indexBuffer this.vao = vao this.mode = mode 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] } static generateCrosshairs = function (gl: WebGL2RenderingContext, id: number, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): NiivueObject3D { const geometry = NiivueObject3D.generateCrosshairsGeometry(gl, xyzMM, xyzMin, xyzMax, radius, sides, gap) return new NiivueObject3D(id, geometry.vertexBuffer, gl.TRIANGLES, geometry.indexCount, geometry.indexBuffer, geometry.vao) } // not included in public docs static generateCrosshairsGeometry = function (gl: WebGL2RenderingContext, xyzMM: vec4, xyzMin: vec3, xyzMax: vec3, radius: number, sides = 20, gap = 0): Geometry { const vertices: number[] = [] const indices: number[] = [] const gapX = radius * gap if (gapX <= 0) { // left-right 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) // anterior-posterior 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) // superior-inferior 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) } else { // left-right let start = vec3.fromValues(xyzMin[0], xyzMM[1], xyzMM[2]) let dest = vec3.fromValues(xyzMM[0] - gapX, xyzMM[1], xyzMM[2]) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) start = vec3.fromValues(xyzMM[0] + gapX, xyzMM[1], xyzMM[2]) dest = vec3.fromValues(xyzMax[0], xyzMM[1], xyzMM[2]) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) // anterior-posterior start = vec3.fromValues(xyzMM[0], xyzMin[1], xyzMM[2]) dest = vec3.fromValues(xyzMM[0], xyzMM[1] - gapX, xyzMM[2]) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) start = vec3.fromValues(xyzMM[0], xyzMM[1] + gapX, xyzMM[2]) dest = vec3.fromValues(xyzMM[0], xyzMax[1], xyzMM[2]) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) // superior-inferior start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMin[2]) dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] - gapX) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) start = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMM[2] + gapX) dest = vec3.fromValues(xyzMM[0], xyzMM[1], xyzMax[2]) NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, false) } // console.log('i:',indices.length / 3, 'v:',vertices.length / 3); const vertexBuffer = gl.createBuffer() if (vertexBuffer === null) { throw new Error('could not instantiate vertex buffer') } gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW) // index buffer allocated in parent class const indexBuffer = gl.createBuffer() if (indexBuffer === null) { throw new Error('could not instantiate index buffer') } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW) const 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 } } static getFirstPerpVector = function (v1: vec3): vec3 { const 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 } static subdivide = function (verts: number[], faces: number[]): void { // Subdivide each triangle into four triangles, pushing verts to the unit sphere""" let nv = verts.length / 3 let nf = faces.length / 3 const n = nf const vNew = vec3.create() const nNew = vec3.create() for (let faceIndex = 0; faceIndex < n; faceIndex++) { // setlength(verts, nv + 3); const fx = faces[faceIndex * 3 + 0] const fy = faces[faceIndex * 3 + 1] const fz = faces[faceIndex * 3 + 2] const vx = vec3.fromValues(verts[fx * 3 + 0], verts[fx * 3 + 1], verts[fx * 3 + 2]) const vy = vec3.fromValues(verts[fy * 3 + 0], verts[fy * 3 + 1], verts[fy * 3 + 2]) const 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 } } static weldVertices = function (verts: number[], faces: number[]): number[] { // unify identical vertices const 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(); const 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 const x = verts[v] const y = verts[v + 1] const 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); const nf = faces.length for (let f = 0; f < nf; f++) { faces[f] = remap[faces[f]] } const vtx = verts.slice(0, nUnique * 3 - 1) for (let i = 0; i < nv - 1; i++) { const v = i * 3 const r = remap[i] * 3 vtx[r] = verts[v] vtx[r + 1] = verts[v + 1] vtx[r + 2] = verts[v + 2] } return vtx } static makeSphere = function (vertices: number[], indices: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0]): void { let 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([ const 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 ] NiivueObject3D.subdivide(vtx, idx) NiivueObject3D.subdivide(vtx, idx) vtx = NiivueObject3D.weldVertices(vtx, idx) for (let i = 0; i < vtx.length; i++) { vtx[i] = vtx[i] * radius } const 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++ } const 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) } static makeCylinder = function (vertices: number[], indices: number[], start: vec3, dest: vec3, radius: number, sides = 20, endcaps = true): void { if (sides < 3) { sides = 3 } // prism is minimal 3D cylinder const v1 = vec3.create() vec3.subtract(v1, dest, start) vec3.normalize(v1, v1) // principle axis of cylinder const v2 = NiivueObject3D.getFirstPerpVector(v1) // a unit length vector orthogonal to v1 // Get the second perp vector by cross product const 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 } const idx0 = Math.floor(vertices.length / 3) // first new vertex will be AFTER previous vertices const idx = new Uint32Array(num_f * 3) const vtx = new Float32Array(num_v * 3) function setV(i: number, vec3: vec3): void { vtx[i * 3 + 0] = vec3[0] vtx[i * 3 + 1] = vec3[1] vtx[i * 3 + 2] = vec3[2] } function setI(i: number, a: number, b: number, c: number): void { idx[i * 3 + 0] = a + idx0 idx[i * 3 + 1] = b + idx0 idx[i * 3 + 2] = c + idx0 } const startPole = 2 * sides const destPole = startPole + 1 if (endcaps) { setV(startPole, start) setV(destPole, dest) } const pt1 = vec3.create() const pt2 = vec3.create() for (let i = 0; i < sides; i++) { const c = Math.cos((i / sides) * 2 * Math.PI) const 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) } static makeColoredCylinder = function ( vertices: number[], indices: number[], colors: number[], start: vec3, dest: vec3, radius: number, rgba255 = [192, 0, 0, 255], sides = 20, endcaps = false ): void { let nv = vertices.length / 3 NiivueObject3D.makeCylinder(vertices, indices, start, dest, radius, sides, endcaps) nv = vertices.length / 3 - nv const 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) } static makeColoredSphere = function (vertices: number[], indices: number[], colors: number[], radius: number, origin: vec3 | vec4 = [0, 0, 0], rgba255 = [0, 0, 192, 255]): void { let nv = vertices.length / 3 NiivueObject3D.makeSphere(vertices, indices, radius, origin) nv = vertices.length / 3 - nv const 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) } }