UNPKG

uglymol

Version:

Macromolecular Viewer for Crystallographers

786 lines (716 loc) 24 kB
import { BufferAttribute, BufferGeometry, ShaderMaterial, Object3D, Mesh, Line, LineSegments, Points, Color, Vector3, Texture } from './uthree/main.js'; import { CatmullRomCurve3 } from './uthree/extras.js'; import type { Atom } from './model'; type Num3 = [number, number, number]; const CUBE_EDGES: Num3[] = [[0, 0, 0], [1, 0, 0], [0, 0, 0], [0, 1, 0], [0, 0, 0], [0, 0, 1], [1, 0, 0], [1, 1, 0], [1, 0, 0], [1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1], [1, 0, 1], [1, 1, 1], [1, 1, 0], [1, 1, 1], [0, 1, 1], [1, 1, 1]]; function makeColorAttribute(colors: Color[]) { const col = new Float32Array(colors.length * 3); for (let i = 0; i < colors.length; i++) { col[3*i+0] = colors[i].r; col[3*i+1] = colors[i].g; col[3*i+2] = colors[i].b; } return new BufferAttribute(col, 3); } const light_dir = new Vector3(-0.2, 0.3, 1.0); // length affects brightness export const fog_pars_fragment = `#ifdef USE_FOG uniform vec3 fogColor; uniform float fogNear; uniform float fogFar; #endif`; export const fog_end_fragment = `#ifdef USE_FOG float depth = gl_FragCoord.z / gl_FragCoord.w; float fogFactor = smoothstep(fogNear, fogFar, depth); gl_FragColor.rgb = mix(gl_FragColor.rgb, fogColor, fogFactor); #endif`; const varcolor_vert = ` attribute vec3 color; varying vec3 vcolor; void main() { vcolor = color; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const unicolor_vert = ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const unicolor_frag = ` ${fog_pars_fragment} uniform vec3 vcolor; void main() { gl_FragColor = vec4(vcolor, 1.0); ${fog_end_fragment} }`; const varcolor_frag = ` ${fog_pars_fragment} varying vec3 vcolor; void main() { gl_FragColor = vec4(vcolor, 1.0); ${fog_end_fragment} }`; export function makeLines(pos: Float32Array, color: Color, linewidth: number) { const material = new ShaderMaterial({ uniforms: makeUniforms({vcolor: color}), vertexShader: unicolor_vert, fragmentShader: unicolor_frag, fog: true, linewidth: linewidth, type: 'um_lines', }); const geometry = new BufferGeometry(); geometry.setAttribute('position', new BufferAttribute(pos, 3)); return new LineSegments(geometry, material); } interface CubeOptions { color: Color; linewidth: number; } export function makeCube(size: number, ctr: Vector3, options: CubeOptions) { const pos = new Float32Array(CUBE_EDGES.length * 3); for (let i = 0; i < CUBE_EDGES.length; i++) { const coor = CUBE_EDGES[i]; pos[3*i+0] = ctr.x + size * (coor[0] - 0.5); pos[3*i+1] = ctr.y + size * (coor[1] - 0.5); pos[3*i+2] = ctr.z + size * (coor[2] - 0.5); } return makeLines(pos, options.color, options.linewidth); } export function makeMultiColorLines(pos: Float32Array, colors: Color[], linewidth: number) { const material = new ShaderMaterial({ uniforms: makeUniforms({}), vertexShader: varcolor_vert, fragmentShader: varcolor_frag, fog: true, linewidth: linewidth, type: 'um_multicolor_lines', }); const geometry = new BufferGeometry(); geometry.setAttribute('position', new BufferAttribute(pos, 3)); geometry.setAttribute('color', makeColorAttribute(colors)); return new LineSegments(geometry, material); } // A cube with 3 edges (for x, y, z axes) colored in red, green and blue. export function makeRgbBox(transform_func: (arg:Num3) => Num3, color: Color) { const pos = new Float32Array(CUBE_EDGES.length * 3); for (let i = 0; i < CUBE_EDGES.length; i++) { const coor = transform_func(CUBE_EDGES[i]); pos[3*i+0] = coor[0]; pos[3*i+1] = coor[1]; pos[3*i+2] = coor[2]; } const colors = [ new Color(0xff0000), new Color(0xffaa00), new Color(0x00ff00), new Color(0xaaff00), new Color(0x0000ff), new Color(0x00aaff), ]; for (let j = 6; j < CUBE_EDGES.length; j++) { colors.push(color); } return makeMultiColorLines(pos, colors, 1); } function double_pos(pos: Num3[]) { const double_pos = []; for (let i = 0; i < pos.length; i++) { const v = pos[i]; double_pos.push(v[0], v[1], v[2]); double_pos.push(v[0], v[1], v[2]); } return double_pos; } function double_color(color_arr: Color[]) { const len = color_arr.length; const color = new Float32Array(6*len); for (let i = 0; i < len; i++) { const col = color_arr[i]; color[6*i] = col.r; color[6*i+1] = col.g; color[6*i+2] = col.b; color[6*i+3] = col.r; color[6*i+4] = col.g; color[6*i+5] = col.b; } return color; } // draw quads as 2 triangles: 4 attributes / quad, 6 indices / quad function make_quad_index_buffer(len: number) { const index = (4*len < 65536 ? new Uint16Array(6*len) : new Uint32Array(6*len)); const vert_order = [0, 1, 2, 0, 2, 3]; for (let i = 0; i < len; i++) { for (let j = 0; j < 6; j++) { index[6*i+j] = 4*i + vert_order[j]; } } return new BufferAttribute(index, 1); } const wide_segments_vert = ` attribute vec3 color; attribute vec3 other; attribute float side; uniform vec2 win_size; uniform float linewidth; varying vec3 vcolor; void main() { vcolor = color; mat4 mat = projectionMatrix * modelViewMatrix; vec2 dir = normalize((mat * vec4(position - other, 0.0)).xy); vec2 normal = vec2(-dir.y, dir.x); gl_Position = mat * vec4(position, 1.0); gl_Position.xy += side * linewidth * normal / win_size; }`; function interpolate_vertices(segment: Atom[], smooth: number): Vector3[] { const vertices = []; for (let i = 0; i < segment.length; i++) { const xyz = segment[i].xyz; vertices.push(new Vector3(xyz[0], xyz[1], xyz[2])); } if (!smooth || smooth < 2) return vertices; const curve = new CatmullRomCurve3(vertices); return curve.getPoints((segment.length - 1) * smooth); } function interpolate_colors(colors: Color[], smooth: number) { if (!smooth || smooth < 2) return colors; const ret = []; for (let i = 0; i < colors.length - 1; i++) { for (let j = 0; j < smooth; j++) { // currently we don't really interpolate colors ret.push(colors[i]); } } ret.push(colors[colors.length - 1]); return ret; } // a simplistic linear interpolation, no need to SLERP function interpolate_directions(dirs: Num3[], smooth: number) { smooth = smooth || 1; const ret = []; let i; for (i = 0; i < dirs.length - 1; i++) { const p = dirs[i]; const n = dirs[i+1]; for (let j = 0; j < smooth; j++) { const an = j / smooth; const ap = 1 - an; ret.push(ap*p[0] + an*n[0], ap*p[1] + an*n[1], ap*p[2] + an*n[2]); } } ret.push(dirs[i][0], dirs[i][1], dirs[i][2]); return ret; } export function makeUniforms(params: Record<string, any>) { const uniforms: Record<string, {value: any}> = { fogNear: { value: null }, // will be updated in setProgram() fogFar: { value: null }, fogColor: { value: null }, }; for (const p in params) { // eslint-disable-line guard-for-in uniforms[p] = { value: params[p] }; } return uniforms; } const ribbon_vert = ` attribute vec3 color; attribute vec3 tan; uniform float shift; varying vec3 vcolor; void main() { vcolor = color; vec3 pos = position + shift * normalize(tan); gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); }`; // 9-line ribbon export function makeRibbon(vertices: Atom[], colors: Color[], tangents: Num3[], smoothness: number) { const vertex_arr = interpolate_vertices(vertices, smoothness); const color_arr = interpolate_colors(colors, smoothness); const tang_arr = interpolate_directions(tangents, smoothness); const obj = new Object3D(); const geometry = new BufferGeometry(); const pos = new Float32Array(vertex_arr.length * 3); for (let i = 0; i < vertex_arr.length; i++) { const v = vertex_arr[i]; pos[3*i+0] = v.x; pos[3*i+1] = v.y; pos[3*i+2] = v.z; } geometry.setAttribute('position', new BufferAttribute(pos, 3)); geometry.setAttribute('color', makeColorAttribute(color_arr)); const tan = new Float32Array(tang_arr); geometry.setAttribute('tan', new BufferAttribute(tan, 3)); for (let n = -4; n < 5; n++) { const material = new ShaderMaterial({ uniforms: makeUniforms({shift: 0.1 * n}), vertexShader: ribbon_vert, fragmentShader: varcolor_frag, fog: true, type: 'um_ribbon', }); obj.add(new Line(geometry, material)); } return obj; } export function makeChickenWire(data: {vertices: number[], segments: number[]}, options: {[key: string]: unknown}) { const geom = new BufferGeometry(); const position = new Float32Array(data.vertices); geom.setAttribute('position', new BufferAttribute(position, 3)); // Although almost all browsers support OES_element_index_uint nowadays, // use Uint32 indexes only when needed. const arr = (data.vertices.length < 3*65536 ? new Uint16Array(data.segments) : new Uint32Array(data.segments)); //console.log('arr len:', data.vertices.length, data.segments.length); geom.setIndex(new BufferAttribute(arr, 1)); const material = new ShaderMaterial({ uniforms: makeUniforms({vcolor: options.color}), vertexShader: unicolor_vert, fragmentShader: unicolor_frag, fog: true, linewidth: options.linewidth, type: 'um_line_chickenwire', }); return new LineSegments(geom, material); } const grid_vert = ` uniform vec3 ucolor; uniform vec3 fogColor; varying vec4 vcolor; void main() { vec2 scale = vec2(projectionMatrix[0][0], projectionMatrix[1][1]); float z = position.z; float fogFactor = (z > 0.5 ? 0.2 : 0.7); float alpha = 0.8 * smoothstep(z > 1.5 ? -10.0 : 0.01, 0.1, scale.y); vcolor = vec4(mix(ucolor, fogColor, fogFactor), alpha); gl_Position = vec4(position.xy * scale, -0.99, 1.0); }`; const grid_frag = ` varying vec4 vcolor; void main() { gl_FragColor = vcolor; }`; export function makeGrid(): LineSegments { const N = 50; const pos = []; for (let i = -N; i <= N; i++) { let z = 0; // z only marks major/minor axes if (i % 5 === 0) z = i % 2 === 0 ? 2 : 1; pos.push(-N, i, z, N, i, z); // horizontal line pos.push(i, -N, z, i, N, z); // vertical line } const geom = new BufferGeometry(); const pos_arr = new Float32Array(pos); geom.setAttribute('position', new BufferAttribute(pos_arr, 3)); const material = new ShaderMaterial({ uniforms: makeUniforms({ucolor: new Color(0x888888)}), //linewidth: 3, vertexShader: grid_vert, fragmentShader: grid_frag, fog: true, // no really, but we use fogColor type: 'um_grid', }); material.transparent = true; const obj = new LineSegments(geom, material); obj.frustumCulled = false; // otherwise the renderer could skip it return obj; } export function makeLineMaterial(options: Record<string, any>) { const uniforms = makeUniforms({ linewidth: options.linewidth, win_size: options.win_size, }); return new ShaderMaterial({ uniforms: uniforms, vertexShader: wide_segments_vert, fragmentShader: varcolor_frag, fog: true, type: 'um_line', }); } // vertex_arr and color_arr must be of the same length export function makeLineSegments(material: ShaderMaterial, vertex_arr: Num3[], color_arr?: Color[]) { // n input vertices => 2n output vertices, n triangles, 3n indexes const len = vertex_arr.length; const pos = double_pos(vertex_arr); const position = new Float32Array(pos); const other_vert = new Float32Array(6*len); for (let i = 0; i < 6 * len; i += 12) { let j = 0; for (; j < 6; j++) other_vert[i+j] = pos[i+j+6]; for (; j < 12; j++) other_vert[i+j] = pos[i+j-6]; } const side = new Float32Array(2*len); for (let k = 0; k < len; k++) { side[2*k] = -1; side[2*k+1] = 1; } const geometry = new BufferGeometry(); geometry.setAttribute('position', new BufferAttribute(position, 3)); geometry.setAttribute('other', new BufferAttribute(other_vert, 3)); geometry.setAttribute('side', new BufferAttribute(side, 1)); if (color_arr != null) { const color = double_color(color_arr); geometry.setAttribute('color', new BufferAttribute(color, 3)); } geometry.setIndex(make_quad_index_buffer(len/2)); const mesh = new Mesh(geometry, material); //mesh.userData.bond_lines = true; return mesh; } const wheel_vert = ` attribute vec3 color; uniform float size; varying vec3 vcolor; void main() { vcolor = color; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_PointSize = size; }`; // not sure how portable it is const wheel_frag = ` ${fog_pars_fragment} varying vec3 vcolor; void main() { vec2 diff = gl_PointCoord - vec2(0.5, 0.5); if (dot(diff, diff) >= 0.25) discard; gl_FragColor = vec4(vcolor, 1.0); ${fog_end_fragment} }`; export function makeWheels(atom_arr: Atom[], color_arr: Color[], size: number) { const geometry = new BufferGeometry(); const pos = new Float32Array(atom_arr.length * 3); for (let i = 0; i < atom_arr.length; i++) { const xyz = atom_arr[i].xyz; pos[3*i+0] = xyz[0]; pos[3*i+1] = xyz[1]; pos[3*i+2] = xyz[2]; } geometry.setAttribute('position', new BufferAttribute(pos, 3)); geometry.setAttribute('color', makeColorAttribute(color_arr)); const material = new ShaderMaterial({ uniforms: makeUniforms({size: size}), vertexShader: wheel_vert, fragmentShader: wheel_frag, fog: true, type: 'um_wheel', }); const obj = new Points(geometry, material); return obj; } // For the ball-and-stick rendering we use so-called imposters. // This technique was described in: // http://doi.ieeecomputersociety.org/10.1109/TVCG.2006.115 // free copy here: // http://vcg.isti.cnr.it/Publications/2006/TCM06/Tarini_FinalVersionElec.pdf // and was nicely summarized in: // http://www.sunsetlakesoftware.com/2011/05/08/enhancing-molecules-using-opengl-es-20 const sphere_vert = ` attribute vec3 color; attribute vec2 corner; uniform float radius; varying vec3 vcolor; varying vec2 vcorner; varying vec3 vpos; void main() { vcolor = color; vcorner = corner; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); vpos = mvPosition.xyz; mvPosition.xy += corner * radius; gl_Position = projectionMatrix * mvPosition; } `; // based on 3Dmol imposter shaders const sphere_frag = ` ${fog_pars_fragment} uniform mat4 projectionMatrix; uniform vec3 lightDir; uniform float radius; varying vec3 vcolor; varying vec2 vcorner; varying vec3 vpos; void main() { float sq = dot(vcorner, vcorner); if (sq > 1.0) discard; float z = sqrt(1.0-sq); vec3 xyz = vec3(vcorner.x, vcorner.y, z); vec4 projPos = projectionMatrix * vec4(vpos + radius * xyz, 1.0); gl_FragDepthEXT = 0.5 * ((gl_DepthRange.diff * (projPos.z / projPos.w)) + gl_DepthRange.near + gl_DepthRange.far); float weight = clamp(dot(xyz, lightDir), 0.0, 1.0) * 0.8 + 0.2; gl_FragColor = vec4(weight * vcolor, 1.0); ${fog_end_fragment} } `; const stick_vert = ` attribute vec3 color; attribute vec3 axis; attribute vec2 corner; uniform float radius; varying vec3 vcolor; varying vec2 vcorner; varying vec3 vpos; varying vec3 vaxis; void main() { vcolor = color; vcorner = corner; vaxis = normalize((modelViewMatrix * vec4(axis, 0.0)).xyz); vec2 normal = normalize(vec2(-vaxis.y, vaxis.x)); vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); vpos = mvPosition.xyz; mvPosition.xy += corner[1] * radius * normal; gl_Position = projectionMatrix * mvPosition; }`; const stick_frag = ` ${fog_pars_fragment} uniform mat4 projectionMatrix; uniform vec3 lightDir; uniform float radius; varying vec3 vcolor; varying vec2 vcorner; varying vec3 vpos; varying vec3 vaxis; void main() { float central = 1.0 - vcorner[1] * vcorner[1]; vec4 pos = vec4(vpos, 1.0); pos.z += radius * vaxis.z * central; vec4 projPos = projectionMatrix * pos; gl_FragDepthEXT = 0.5 * ((gl_DepthRange.diff * (projPos.z / projPos.w)) + gl_DepthRange.near + gl_DepthRange.far); float weight = length(cross(vaxis, lightDir)) * central * 0.8 + 0.2; gl_FragColor = vec4(min(weight, 1.0) * vcolor, 1.0); ${fog_end_fragment} }`; export function makeSticks(vertex_arr: Num3[], color_arr: Color[], radius: number) { const uniforms = makeUniforms({ radius: radius, lightDir: light_dir, }); const material = new ShaderMaterial({ uniforms: uniforms, vertexShader: stick_vert, fragmentShader: stick_frag, fog: true, type: 'um_stick', }); material.extensions.fragDepth = true; const len = vertex_arr.length; const pos = double_pos(vertex_arr); const position = new Float32Array(pos); const axis = new Float32Array(6*len); for (let i = 0; i < 6 * len; i += 12) { for (let j = 0; j < 6; j++) axis[i+j] = pos[i+j+6] - pos[i+j]; for (let j = 0; j < 6; j++) axis[i+j+6] = axis[i+j]; } const geometry = new BufferGeometry(); geometry.setAttribute('position', new BufferAttribute(position, 3)); const corner = new Float32Array(4*len); for (let i = 0; 2 * i < len; i++) { corner[8*i + 0] = -1; // 0 corner[8*i + 1] = -1; // 0 corner[8*i + 2] = -1; // 1 corner[8*i + 3] = +1; // 1 corner[8*i + 4] = +1; // 2 corner[8*i + 5] = +1; // 2 corner[8*i + 6] = +1; // 3 corner[8*i + 7] = -1; // 3 } geometry.setAttribute('axis', new BufferAttribute(axis, 3)); geometry.setAttribute('corner', new BufferAttribute(corner, 2)); const color = double_color(color_arr); geometry.setAttribute('color', new BufferAttribute(color, 3)); geometry.setIndex(make_quad_index_buffer(len/2)); const mesh = new Mesh(geometry, material); //mesh.userData.bond_lines = true; return mesh; } export function makeBalls(atom_arr: Atom[], color_arr: Color[], radius: number) { const N = atom_arr.length; const geometry = new BufferGeometry(); const pos = new Float32Array(N * 4 * 3); for (let i = 0; i < N; i++) { const xyz = atom_arr[i].xyz; for (let j = 0; j < 4; j++) { for (let k = 0; k < 3; k++) { pos[3 * (4*i + j) + k] = xyz[k]; } } } geometry.setAttribute('position', new BufferAttribute(pos, 3)); const corner = new Float32Array(N * 4 * 2); for (let i = 0; i < N; i++) { corner[8*i + 0] = -1; // 0 corner[8*i + 1] = -1; // 0 corner[8*i + 2] = -1; // 1 corner[8*i + 3] = +1; // 1 corner[8*i + 4] = +1; // 2 corner[8*i + 5] = +1; // 2 corner[8*i + 6] = +1; // 3 corner[8*i + 7] = -1; // 3 } geometry.setAttribute('corner', new BufferAttribute(corner, 2)); const colors = new Float32Array(N * 4 * 3); for (let i = 0; i < N; i++) { const col = color_arr[i]; for (let j = 0; j < 4; j++) { colors[3 * (4*i + j) + 0] = col.r; colors[3 * (4*i + j) + 1] = col.g; colors[3 * (4*i + j) + 2] = col.b; } } geometry.setAttribute('color', new BufferAttribute(colors, 3)); geometry.setIndex(make_quad_index_buffer(N)); const material = new ShaderMaterial({ uniforms: makeUniforms({ radius: radius, lightDir: light_dir, }), vertexShader: sphere_vert, fragmentShader: sphere_frag, fog: true, type: 'um_sphere', }); material.extensions.fragDepth = true; const obj = new Mesh(geometry, material); return obj; } /* interface LineRaycastOptions { precision: number; ray: Ray; near: number; far: number; } // based on Line.prototype.raycast(), but skipping duplicated points const inverseMatrix = new Matrix4(); const ray = new Ray(); export function line_raycast(mesh: Mesh, options: LineRaycastOptions, intersects: object[]) { const precisionSq = options.precision * options.precision; inverseMatrix.copy(mesh.matrixWorld).invert(); ray.copy(options.ray).applyMatrix4(inverseMatrix); const vStart = new Vector3(); const vEnd = new Vector3(); const interSegment = new Vector3(); const interRay = new Vector3(); const positions = mesh.geometry.attributes.position.array; for (let i = 0, l = positions.length / 6 - 1; i < l; i += 2) { vStart.fromArray(positions, 6 * i); vEnd.fromArray(positions, 6 * i + 6); const distSq = ray.distanceSqToSegment(vStart, vEnd, interRay, interSegment); if (distSq > precisionSq) continue; interRay.applyMatrix4(mesh.matrixWorld); const distance = options.ray.origin.distanceTo(interRay); if (distance < options.near || distance > options.far) continue; intersects.push({ distance: distance, point: interSegment.clone().applyMatrix4(mesh.matrixWorld), index: i, object: mesh, line_dist: Math.sqrt(distSq), // extra property, not in Three.js }); } } */ const label_vert = ` attribute vec2 uvs; uniform vec2 canvas_size; uniform vec2 win_size; uniform float z_shift; varying vec2 vUv; void main() { vUv = uvs; vec2 rel_offset = vec2(0.02, -0.3); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position.xy += (uvs + rel_offset) * 2.0 * canvas_size / win_size; gl_Position.z += z_shift * projectionMatrix[2][2]; }`; const label_frag = ` ${fog_pars_fragment} varying vec2 vUv; uniform sampler2D map; void main() { gl_FragColor = texture2D(map, vUv); ${fog_end_fragment} }`; export class Label { texture: Texture; mesh?: Mesh; constructor(text: string, options: Record<string, any>) { this.texture = new Texture(); const canvas_size = this.redraw(text, options); if (canvas_size === undefined) return; // Rectangle geometry. const geometry = new BufferGeometry(); const pos = options.pos; const position = new Float32Array([].concat(pos, pos, pos, pos)); const uvs = new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]); const indices = new Uint16Array([0, 2, 1, 2, 3, 1]); geometry.setIndex(new BufferAttribute(indices, 1)); geometry.setAttribute('position', new BufferAttribute(position, 3)); geometry.setAttribute('uvs', new BufferAttribute(uvs, 2)); const material = new ShaderMaterial({ uniforms: makeUniforms({map: this.texture, canvas_size: canvas_size, win_size: options.win_size, z_shift: options.z_shift}), vertexShader: label_vert, fragmentShader: label_frag, fog: true, type: 'um_label', }); material.transparent = true; this.mesh = new Mesh(geometry, material); } redraw(text: string, options: Record<string, any>) { if (typeof document === 'undefined') return; // for testing on node const canvas = document.createElement('canvas'); // Canvas size should be 2^N. canvas.width = 256; // arbitrary limit, to keep it simple canvas.height = 16; // font size const context = canvas.getContext('2d'); if (!context) return null; context.font = (options.font || 'bold 14px') + ' sans-serif'; //context.fillStyle = 'green'; //context.fillRect(0, 0, canvas.width, canvas.height); context.textBaseline = 'bottom'; if (options.color) context.fillStyle = options.color; context.fillText(text, 0, canvas.height); this.texture.image = canvas; this.texture.needsUpdate = true; return [canvas.width, canvas.height]; } } // Add vertices of a 3d cross (representation of an unbonded atom) export function addXyzCross(vertices: Num3[], xyz: Num3, r: number) { vertices.push([xyz[0]-r, xyz[1], xyz[2]], [xyz[0]+r, xyz[1], xyz[2]]); vertices.push([xyz[0], xyz[1]-r, xyz[2]], [xyz[0], xyz[1]+r, xyz[2]]); vertices.push([xyz[0], xyz[1], xyz[2]-r], [xyz[0], xyz[1], xyz[2]+r]); }