在浏览器上,如何绘制每个 64-128 点的 100k 系列?

On the browser, how to plot 100k series with 64-128 points each?

我想绘制大约 120k 系列的图表,每个系列有 64 个点(下采样,如果使用实际采样率,则为 128-512 点,甚至更大)

我尝试用 dygraph 来做,但如果我使用超过 1000 个系列,它似乎很慢。

我曾尝试使用 vanilla WebGL,它绘制得非常快,但我的问题是点击鼠标而不是决定它是哪个系列 - 有什么策略吗? (我相信它叫做 unprojecting?) - 因为有 100k+ 系列,所以对每个系列使用不同的颜色而不是使用点击坐标的像素颜色来确定系列是不切实际的。还有其他策略吗?

我目前的设计将图形绘制为包含所有图形的大型 PNG 图集,加载速度很快,但在数据发生变化时,我必须在服务器上重新绘制 PNG,然后再次显示它,还有 "unprojecting" 是这里的一个问题 - 关于如何解决它的任何想法?如果可能的话?

数据已经完全降采样,进一步降采样可能会导致丢失我想向最终用户展示的细节。

绘制 120k * 64 的东西意味着可以覆盖 2700x2700 的每个像素。换句话说,您可能试图显示太多数据?这也是一个非常多的东西,而且可能很慢。

无论如何,通过 WebGL 绘图和拾取都相对容易。你可以使用任何你想要的技术来绘制你的场景。然后,单独地,当用户单击鼠标(或始终在鼠标下方)时,您会再次将整个场景绘制到屏幕外帧缓冲区,从而为每个可选择的对象赋予不同的颜色。假设默认有 32 位颜色(8 位红色、8 位绿色、8 位蓝色、8 位 alpha)可以算出 2^32-1 个东西。当然,对于其他缓冲区格式,您可以计数更高或绘制到多个缓冲区,但存储 2^32 的数据可能是更大的限制。

无论如何,这里有一个例子。这个制作了 1000 个立方体(只使用了立方体,因为这个样本已经存在)。您可以将每个立方体视为您的 "series" 中的一个,有 8 个点,尽管代码实际上是每个立方体绘制 24 个点。 (set primType = gl.TRIANGLES) 查看立方体。它将所有立方体放在同一个缓冲区中,以便单个绘制调用绘制所有立方体。这比我们使用单独的绘制调用绘制每个立方体要快得多。

重要的部分是为每个系列制作一个系列 ID。在下面的代码中,一个立方体的所有点都具有相同的 ID。

代码绘制场景两次。一次使用每个立方体的颜色,再次使用每个立方体的 ID 进入屏幕外纹理(作为帧缓冲区附件)。要知道鼠标下方是哪个立方体,我们查找鼠标下方的像素,将其颜色转换回 ID 并更新该立方体的顶点颜色以突出显示它。

const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;

const renderVS = `
attribute vec4 position;
attribute vec4 color;

uniform mat4 u_projection;
uniform mat4 u_modelView;

varying vec4 v_color;

void main() {
  gl_PointSize = 10.0;
  gl_Position = u_projection * u_modelView * position;
  v_color = color;
}
`;

const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
  gl_FragColor = v_color;
}
`;

const idVS = `
attribute vec4 position;
attribute vec4 id;

uniform mat4 u_projection;
uniform mat4 u_modelView;

varying vec4 v_id;
void main() {
  gl_PointSize = 10.0;
  gl_Position = u_projection * u_modelView * position;
  v_id = id;  // pass the id to the fragment shader
}
`;

const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
  gl_FragColor = v_id;
}
`;

// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);

// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;

// adapted from 
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
  const rnd = 1.;
  const offset = 2. / samples;
  const increment = Math.PI * (3. - Math.sqrt(5.));

  //  for i in range(samples):
  const y = ((i * offset) - 1.) + (offset / 2.);
  const r = Math.sqrt(1. - Math.pow(y ,2.));

  const phi = ((i + rnd) % samples) * increment;

  const x = Math.cos(phi) * r;
  const z = Math.sin(phi) * r;

  return [x, y, z];
}

const addCubeVertexData = (function() {
  const CUBE_FACE_INDICES = [
    [3, 7, 5, 1],  // right
    [6, 2, 0, 4],  // left
    [6, 7, 3, 2],  // ??
    [0, 1, 5, 4],  // ??
    [7, 6, 4, 5],  // front
    [2, 3, 1, 0],  // back
  ];

  const cornerVertices = [
    [-1, -1, -1],
    [+1, -1, -1],
    [-1, +1, -1],
    [+1, +1, -1],
    [-1, -1, +1],
    [+1, -1, +1],
    [-1, +1, +1],
    [+1, +1, +1],
  ];

  const faceNormals = [
    [+1, +0, +0],
    [-1, +0, +0],
    [+0, +1, +0],
    [+0, -1, +0],
    [+0, +0, +1],
    [+0, +0, -1],
  ];

  const quadIndices = [0, 1, 2, 0, 2, 3];

  return function addCubeVertexData(id, matrix, color) {
    for (let f = 0; f < 6; ++f) {
      const faceIndices = CUBE_FACE_INDICES[f];
      for (let v = 0; v < 6; ++v) {
        const ndx = faceIndices[quadIndices[v]];
        const position = cornerVertices[ndx];
        const normal = faceNormals[f];

        positions.push(...m4.transformPoint(matrix, position));
        normals.push(...m4.transformDirection(matrix, normal));
        colors.push(color);
        ids.push(id);
        timeStamps.push(-1000);
      }
    }
  };
}());

for (let i = 0; i < numCubes; ++i) {
  const direction = fibonacciSphere(numCubes, i);
  const cubePosition = v3.mulScalar(direction, radius);
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const matrix = m4.lookAt(cubePosition, target, up);
  const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
  cubeColors.push(color);
  addCubeVertexData(i + 1, matrix, color);
}

const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);

// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
  // we know each cube uses 36 vertices. If each model was different
  // we need to save the offset and number of vertices for each model
  const numVertices = 36;
  const offset = (id - 1) * numVertices;
  colorData.fill(color, offset, offset + numVertices);
}

function setCubeTimestamp(id, timeStamp) {
  const numVertices = 36;
  const offset = (id - 1) * numVertices;
  timeStampData.fill(timeStamp, offset, offset + numVertices);
}

// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: positions,
  normal: normals,
  color: new Uint8Array(colorData.buffer),
  // the colors are stored as 32bit unsigned ints
  // but we want them as 4 channel 8bit RGBA values
  id: {
    numComponents: 4,
    data: new Uint8Array((new Uint32Array(ids)).buffer),
  },
  timeStamp: {
    numComponents: 1,
    data: timeStampData,
  },
});

const lightDir = v3.normalize([3, 5, 10]);

// creates an RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl);

// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;

function getIdAtPixel(x, y, projection, view, time) {
  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, fbi);

  // no reason to render 100000s of pixels when
  // we're only going to read one
  gl.enable(gl.SCISSOR_TEST);
  gl.scissor(x, y, 1, 1);

  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);

  drawCubes(idProgramInfo, projection, view, time);

  gl.disable(gl.SCISSOR_TEST);

  const idPixel = new Uint8Array(4);
  gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
  // convert from RGBA back into ID.
  const id = (idPixel[0] <<  0) +
             (idPixel[1] <<  8) +
             (idPixel[2] << 16) +
             (idPixel[3] << 24);
  return id;
}

function drawCubes(programInfo, projection, modelView, time) {
  gl.useProgram(programInfo.program);
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  // calls gl.uniformXXX
  twgl.setUniforms(programInfo, {
    u_projection: projection,
    u_modelView: modelView,  // drawing at origin so model is identity
  });

  gl.drawArrays(primType, 0, bufferInfo.numElements);
}


function render(time) {
  time *= 0.001;
  ++frameCount;

  if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
    // resizes the texture and depth renderbuffer to
    // match the new size of the canvas.
    twgl.resizeFramebufferInfo(gl, fbi);
  }

  const fov = Math.PI * .35;
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 0.1;
  const zFar = 1000;
  const projection = m4.perspective(fov, aspect, zNear, zFar);

  const radius = 45;
  const angle = time * .2;
  const eye = [
    Math.cos(angle) * radius,
    0,
    Math.sin(angle) * radius,
  ];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const camera = m4.lookAt(eye, target, up);
  const view = m4.inverse(camera);

  if (lastHighlightedCubeId > 0) {
    // restore the last highlighted cube's color
    setCubeColor(
      lastHighlightedCubeId,
      cubeColorsAsUint32[lastHighlightedCubeId]);
    lastHighlightedCubeId = -1;
  }

  {
    const x = mousePos.x;
    const y = gl.canvas.height - mousePos.y - 1;
    highlightedCubeId = getIdAtPixel(x, y, projection, view, time);
  }

  if (highlightedCubeId > 0) {
    const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
    setCubeColor(highlightedCubeId, color);
    setCubeTimestamp(highlightedCubeId, time);
    lastHighlightedCubeId = highlightedCubeId;
  }

  highlightedCubeId = Math.random() * numCubes | 0;

  // NOTE: We could use `gl.bufferSubData` and just upload
  // the portion that changed.

  // upload cube color data.
  gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
  // upload the timestamp
  gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, null);

  gl.enable(gl.DEPTH_TEST);

  drawCubes(renderProgramInfo, projection, view, time);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  const rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  const pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / target.clientWidth;
  pos.y = pos.y * target.height / target.clientHeight;

  return pos;
}

gl.canvas.addEventListener('mousemove', (event, target) => {
  mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

上面的代码使用与 canvas 大小相同的屏幕外帧缓冲区,但它使用剪刀测试仅绘制一个像素(鼠标下方的像素)。如果没有剪刀测试,它仍然 运行 它只会更慢。

我们也可以只使用一个像素的屏幕外帧缓冲区并使用投影数学来让它工作。

const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;

const renderVS = `
attribute vec4 position;
attribute vec4 color;

uniform mat4 u_projection;
uniform mat4 u_modelView;

varying vec4 v_color;

void main() {
  gl_PointSize = 10.0;
  gl_Position = u_projection * u_modelView * position;
  v_color = color;
}
`;

const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
  gl_FragColor = v_color;
}
`;

const idVS = `
attribute vec4 position;
attribute vec4 id;

uniform mat4 u_projection;
uniform mat4 u_modelView;

varying vec4 v_id;
void main() {
  gl_PointSize = 10.0;
  gl_Position = u_projection * u_modelView * position;
  v_id = id;  // pass the id to the fragment shader
}
`;

const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
  gl_FragColor = v_id;
}
`;

// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);

// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;

// adapted from 
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
  const rnd = 1.;
  const offset = 2. / samples;
  const increment = Math.PI * (3. - Math.sqrt(5.));

  //  for i in range(samples):
  const y = ((i * offset) - 1.) + (offset / 2.);
  const r = Math.sqrt(1. - Math.pow(y ,2.));

  const phi = ((i + rnd) % samples) * increment;

  const x = Math.cos(phi) * r;
  const z = Math.sin(phi) * r;

  return [x, y, z];
}

const addCubeVertexData = (function() {
  const CUBE_FACE_INDICES = [
    [3, 7, 5, 1],  // right
    [6, 2, 0, 4],  // left
    [6, 7, 3, 2],  // ??
    [0, 1, 5, 4],  // ??
    [7, 6, 4, 5],  // front
    [2, 3, 1, 0],  // back
  ];

  const cornerVertices = [
    [-1, -1, -1],
    [+1, -1, -1],
    [-1, +1, -1],
    [+1, +1, -1],
    [-1, -1, +1],
    [+1, -1, +1],
    [-1, +1, +1],
    [+1, +1, +1],
  ];

  const faceNormals = [
    [+1, +0, +0],
    [-1, +0, +0],
    [+0, +1, +0],
    [+0, -1, +0],
    [+0, +0, +1],
    [+0, +0, -1],
  ];

  const quadIndices = [0, 1, 2, 0, 2, 3];

  return function addCubeVertexData(id, matrix, color) {
    for (let f = 0; f < 6; ++f) {
      const faceIndices = CUBE_FACE_INDICES[f];
      for (let v = 0; v < 6; ++v) {
        const ndx = faceIndices[quadIndices[v]];
        const position = cornerVertices[ndx];
        const normal = faceNormals[f];

        positions.push(...m4.transformPoint(matrix, position));
        normals.push(...m4.transformDirection(matrix, normal));
        colors.push(color);
        ids.push(id);
        timeStamps.push(-1000);
      }
    }
  };
}());

for (let i = 0; i < numCubes; ++i) {
  const direction = fibonacciSphere(numCubes, i);
  const cubePosition = v3.mulScalar(direction, radius);
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const matrix = m4.lookAt(cubePosition, target, up);
  const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
  cubeColors.push(color);
  addCubeVertexData(i + 1, matrix, color);
}

const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);

// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
  // we know each cube uses 36 vertices. If each model was different
  // we need to save the offset and number of vertices for each model
  const numVertices = 36;
  const offset = (id - 1) * numVertices;
  colorData.fill(color, offset, offset + numVertices);
}

function setCubeTimestamp(id, timeStamp) {
  const numVertices = 36;
  const offset = (id - 1) * numVertices;
  timeStampData.fill(timeStamp, offset, offset + numVertices);
}

// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: positions,
  normal: normals,
  color: new Uint8Array(colorData.buffer),
  // the colors are stored as 32bit unsigned ints
  // but we want them as 4 channel 8bit RGBA values
  id: {
    numComponents: 4,
    data: new Uint8Array((new Uint32Array(ids)).buffer),
  },
  timeStamp: {
    numComponents: 1,
    data: timeStampData,
  },
});

const lightDir = v3.normalize([3, 5, 10]);

// creates an 1x1 pixel RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl, [
  { format: gl.RGBA, type: gl.UNSIGNED_BYTE, minMag: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
  { format: gl.DEPTH_STENCIL, },
], 1, 1);

// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;

function getIdAtPixel(x, y, projectionInfo, view, time) {
  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, fbi);

  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);

  drawCubes(idProgramInfo, projectionInfo, {
    totalWidth: gl.canvas.width,
    totalHeight: gl.canvas.height,
    partWidth: 1,
    partHeight: 1,
    partX: x,
    partY: y,
  }, view, time);

  const idPixel = new Uint8Array(4);
  gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
  // convert from RGBA back into ID.
  const id = (idPixel[0] <<  0) +
             (idPixel[1] <<  8) +
             (idPixel[2] << 16) +
             (idPixel[3] << 24);
  return id;
}

function drawCubes(programInfo, projectionInfo, partInfo, modelView, time) {

  const projection = projectionForPart(projectionInfo, partInfo);

  gl.useProgram(programInfo.program);
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  // calls gl.uniformXXX
  twgl.setUniforms(programInfo, {
    u_projection: projection,
    u_modelView: modelView,  // drawing at origin so model is identity
  });

  gl.drawArrays(primType, 0, bufferInfo.numElements);
}

function projectionForPart(projectionInfo, partInfo) {
  const {fov, zNear, zFar} = projectionInfo;
  const {
    totalWidth,
    totalHeight,
    partX,
    partY,
    partWidth,
    partHeight,
  } = partInfo;
  
  const aspect = totalWidth / totalHeight;
  
  // corners at zNear for total image
  const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
  const zNearTotalBottom = -zNearTotalTop;
  const zNearTotalLeft = zNearTotalBottom * aspect;
  const zNearTotalRight = zNearTotalTop * aspect;
  
  // width, height at zNear for total image
  const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
  const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;
  
  const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth;   const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
  const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
  const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;

  return m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);
}

function render(time) {
  time *= 0.001;
  ++frameCount;

  twgl.resizeCanvasToDisplaySize(gl.canvas);

  const projectionInfo = {
    fov: Math.PI * .35,
    zNear: 0.1,
    zFar: 1000,
  };

  const radius = 45;
  const angle = time * .2;
  const eye = [
    Math.cos(angle) * radius,
    0,
    Math.sin(angle) * radius,
  ];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const camera = m4.lookAt(eye, target, up);
  const view = m4.inverse(camera);

  if (lastHighlightedCubeId > 0) {
    // restore the last highlighted cube's color
    setCubeColor(
      lastHighlightedCubeId,
      cubeColorsAsUint32[lastHighlightedCubeId]);
    lastHighlightedCubeId = -1;
  }

  {
    const x = mousePos.x;
    const y = gl.canvas.height - mousePos.y - 1;
    highlightedCubeId = getIdAtPixel(x, y, projectionInfo, view, time);
  }

  if (highlightedCubeId > 0) {
    const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
    setCubeColor(highlightedCubeId, color);
    setCubeTimestamp(highlightedCubeId, time);
    lastHighlightedCubeId = highlightedCubeId;
  }

  highlightedCubeId = Math.random() * numCubes | 0;

  // NOTE: We could use `gl.bufferSubData` and just upload
  // the portion that changed.

  // upload cube color data.
  gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
  // upload the timestamp
  gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);

  // calls gl.bindFramebuffer and gl.viewport
  twgl.bindFramebufferInfo(gl, null);

  gl.enable(gl.DEPTH_TEST);

  drawCubes(renderProgramInfo, projectionInfo, {
    totalWidth: gl.canvas.width,
    totalHeight: gl.canvas.height,
    partWidth: gl.canvas.width,
    partHeight: gl.canvas.height,
    partX: 0,
    partY: 0,
  }, view, time);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

function getRelativeMousePosition(event, target) {
  target = target || event.target;
  const rect = target.getBoundingClientRect();

  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  }
}

// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
  target = target || event.target;
  const pos = getRelativeMousePosition(event, target);

  pos.x = pos.x * target.width  / target.clientWidth;
  pos.y = pos.y * target.height / target.clientHeight;

  return pos;
}

gl.canvas.addEventListener('mousemove', (event, target) => {
  mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

请注意,在 WebGL 中绘制 POINTS 通常比绘制相同大小的 2 TRIANGLES 慢。如果我将立方体数量设置为 100k 并将 primType 设置为 TRIANGLES,它会绘制 100k 个立方体。在我的集成 GPU 上,片段 window 它 运行 的速度约为 10-20fps。当然,有那么多立方体,不可能挑出一个。如果我将半径设置为 250,我至少可以看到拾取仍在工作。