在 Three.js 中高效渲染数以万计的可变大小/颜色/位置的球体?

2023-12-20

这个问题是从我的上一个问题中提炼出来的,我发现使用积分会导致问题:https://stackoverflow.com/a/60306638/4749956 https://stackoverflow.com/a/60306638/4749956

为了解决这个问题,您需要使用四边形而不是点来绘制点。有很多方法可以做到这一点。将每个四边形绘制为单独的网格或精灵,或者将所有四边形合并到另一个网格中,或者在每个点需要一个矩阵的情况下使用 InstancedMesh,或者编写自定义着色器来处理点(请参阅本文的最后一个示例)

我一直在试图找出这个答案。我的问题是

什么是“实例化”?合并几何图形和实例化有什么区别?而且,如果我要执行其中任何一项,我将使用什么几何形状以及如何改变颜色?我一直在看这个例子:

https://github.com/mrdoob/ Three.js/blob/master/examples/webgl_instancing_performance.html https://github.com/mrdoob/three.js/blob/master/examples/webgl_instancing_performance.html

我发现对于每个球体,您都会有一个几何体,该几何体将应用位置和大小(比例?)。那么底层几何体是单位半径的 SphereBufferGeometry 吗?但是,如何应用颜色呢?

另外,我读到了有关自定义着色器方法的内容,它的意义有些模糊。但是,看起来更复杂。性能会比上面更好吗?


根据您之前的问题...

首先,实例化是一种告诉 Three.js 多次绘制相同几何图形但为每个“实例”更改更多内容的方法。 IIRC Three.js 唯一支持开箱即用的是为每个实例设置不同的矩阵(位置、方向、比例)。除此之外,例如具有不同的颜色,您必须编写自定义着色器。

实例化允许您要求系统通过一个“询问”而不是每个事物一个“询问”来绘制许多事物。这意味着它最终会更快。你可以把它想象成任何东西。如果想要 3 个汉堡,你可以要求别人给你做 1 个。当他们完成后,你可以要求他们再做一个。当他们完成后,你可以要求他们做第三个。这比一开始就要求他们做 3 个汉堡要慢得多。这不是一个完美的类比,但它确实指出了一次要求多件事情比一次要求多件事情效率低。

合并网格是另一种解决方案,遵循bad与上面的类比,合并网格就像制作一个 1 磅重的大汉堡而不是三个 1/3 磅的汉堡。翻转一个较大的汉堡并将配料和面包放在一个大汉堡上比翻转 3 个小汉堡要快一些。

至于哪个是最适合您的解决方案取决于。在您的原始代码中,您只是使用点绘制纹理四边形。点总是在屏幕空间中绘制它们的四边形。另一方面,默认情况下,网格在世界空间中旋转,因此,如果您创建四边形或合并的四边形集的实例并尝试旋转它们,它们将旋转并且不会像点那样面向相机。如果您使用球体几何体,那么您会遇到这样的问题:您不是只计算每个四边形 6 个顶点并在其上绘制一个圆,而是计算每个球体 100 或 1000 个顶点,这比每个四边形 6 个顶点要慢。

因此,它再次需要一个自定义着色器来保持点面向相机。

要通过实例化短版本来实现这一点,您需要决定每个实例重复哪些顶点数据。例如,对于纹理四边形,我们需要 6 个顶点位置和 6 个 uv。对于这些你做正常的BufferAttribute

然后您决定哪些顶点数据对于每个实例是唯一的。在你的情况下,大小、颜色和点的中心。对于每一个我们都做了一个InstancedBufferAttribute

我们将所有这些属性添加到InstancedBufferGeometry作为最后一个参数,我们告诉它有多少个实例。

在绘制的时候你可以这样想

  • for each instance
    • 将 size 设置为 size 属性中的下一个值
    • 将颜色设置为颜色属性中的下一个值
    • 将 center 设置为 center 属性中的下一个值
    • 调用顶点着色器 6 次,将位置和 uv 设置为其属性中的第 n 个值。

通过这种方式,您可以多次使用相同的几何图形(位置和 uv),但每次都会更改一些值(大小、颜色、中心)。

body {
  margin: 0;
}
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
#info {
  position: absolute;
  right: 0;
  bottom: 0;
  color: red;
  background: black;
}
<canvas id="c"></canvas>
<div id="info"></div>
<script type="module">
// Three.js - Picking - RayCaster w/Transparency
// from https://threejsfundamentals.org/threejs/threejs-picking-gpu.html

import * as THREE from "https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.module.js";

function main() {
  const infoElem = document.querySelector("#info");
  const canvas = document.querySelector("#c");
  const renderer = new THREE.WebGLRenderer({ canvas });

  const fov = 60;
  const aspect = 2; // the canvas default
  const near = 0.1;
  const far = 200;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 30;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0);
  const pickingScene = new THREE.Scene();
  pickingScene.background = new THREE.Color(0);

  // put the camera on a pole (parent it to an object)
  // so we can spin the pole to move the camera around the scene
  const cameraPole = new THREE.Object3D();
  scene.add(cameraPole);
  cameraPole.add(camera);

  function randomNormalizedColor() {
    return Math.random();
  }

  function getRandomInt(n) {
    return Math.floor(Math.random() * n);
  }

  function getCanvasRelativePosition(e) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };
  }

  const textureLoader = new THREE.TextureLoader();
  const particleTexture =
    "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/sprites/ball.png";

  const vertexShader = `
    attribute float size;
    attribute vec3 customColor;
    attribute vec3 center;

    varying vec3 vColor;
    varying vec2 vUv;

    void main() {
        vColor = customColor;
        vUv = uv;
        vec3 viewOffset = position * size ;
        vec4 mvPosition = modelViewMatrix * vec4(center, 1) + vec4(viewOffset, 0);
        gl_Position = projectionMatrix * mvPosition;
    }
`;

  const fragmentShader = `
    uniform sampler2D texture;
    varying vec3 vColor;
    varying vec2 vUv;

    void main() {
        vec4 tColor = texture2D(texture, vUv);
        if (tColor.a < 0.5) discard;
        gl_FragColor = mix(vec4(vColor.rgb, 1.0), tColor, 0.1);
    }
`;

  const pickFragmentShader = `
    uniform sampler2D texture;
    varying vec3 vColor;
    varying vec2 vUv;

    void main() {
      vec4 tColor = texture2D(texture, vUv);
      if (tColor.a < 0.25) discard;
      gl_FragColor = vec4(vColor.rgb, 1.0);
    }
`;

  const materialSettings = {
    uniforms: {
      texture: {
        type: "t",
        value: textureLoader.load(particleTexture)
      }
    },
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    blending: THREE.NormalBlending,
    depthTest: true,
    transparent: false
  };

  const createParticleMaterial = () => {
    const material = new THREE.ShaderMaterial(materialSettings);
    return material;
  };

  const createPickingMaterial = () => {
    const material = new THREE.ShaderMaterial({
      ...materialSettings,
      fragmentShader: pickFragmentShader,
      blending: THREE.NormalBlending
    });
    return material;
  };

  const geometry = new THREE.InstancedBufferGeometry();
  const pickingGeometry = new THREE.InstancedBufferGeometry();
  const colors = [];
  const sizes = [];
  const pickingColors = [];
  const pickingColor = new THREE.Color();
  const centers = [];
  const numSpheres = 30;

  const positions = [
    -0.5, -0.5,
     0.5, -0.5,
    -0.5,  0.5,
    -0.5,  0.5,
     0.5, -0.5,
     0.5,  0.5,
  ];

  const uvs = [
     0, 0,
     1, 0,
     0, 1,
     0, 1,
     1, 0,
     1, 1,
  ];

  for (let i = 0; i < numSpheres; i++) {
    colors[3 * i] = randomNormalizedColor();
    colors[3 * i + 1] = randomNormalizedColor();
    colors[3 * i + 2] = randomNormalizedColor();

    const rgbPickingColor = pickingColor.setHex(i + 1);
    pickingColors[3 * i] = rgbPickingColor.r;
    pickingColors[3 * i + 1] = rgbPickingColor.g;
    pickingColors[3 * i + 2] = rgbPickingColor.b;

    sizes[i] = getRandomInt(5);

    centers[3 * i] = getRandomInt(20);
    centers[3 * i + 1] = getRandomInt(20);
    centers[3 * i + 2] = getRandomInt(20);
  }

  geometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 2)
  );
  geometry.setAttribute(
    "uv",
    new THREE.Float32BufferAttribute(uvs, 2)
  );
  geometry.setAttribute(
    "customColor",
    new THREE.InstancedBufferAttribute(new Float32Array(colors), 3)
  );
  geometry.setAttribute(
    "center",
    new THREE.InstancedBufferAttribute(new Float32Array(centers), 3)
  );
  geometry.setAttribute(
    "size",
    new THREE.InstancedBufferAttribute(new Float32Array(sizes), 1));

  const material = createParticleMaterial();
  const points = new THREE.InstancedMesh(geometry, material, numSpheres);

  // setup geometry and material for GPU picking
  pickingGeometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 2)
  );
  pickingGeometry.setAttribute(
    "uv",
    new THREE.Float32BufferAttribute(uvs, 2)
  );
  pickingGeometry.setAttribute(
    "customColor",
    new THREE.InstancedBufferAttribute(new Float32Array(pickingColors), 3)
  );
  pickingGeometry.setAttribute(
    "center",
    new THREE.InstancedBufferAttribute(new Float32Array(centers), 3)
  );
  pickingGeometry.setAttribute(
    "size",
    new THREE.InstancedBufferAttribute(new Float32Array(sizes), 1)
  );

  const pickingMaterial = createPickingMaterial();
  const pickingPoints = new THREE.InstancedMesh(pickingGeometry, pickingMaterial, numSpheres);

  scene.add(points);
  pickingScene.add(pickingPoints);

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  class GPUPickHelper {
    constructor() {
      // create a 1x1 pixel render target
      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
      this.pixelBuffer = new Uint8Array(4);
    }
    pick(cssPosition, pickingScene, camera) {
      const { pickingTexture, pixelBuffer } = this;

      // set the view offset to represent just a single pixel under the mouse
      const pixelRatio = renderer.getPixelRatio();
      camera.setViewOffset(
        renderer.getContext().drawingBufferWidth, // full width
        renderer.getContext().drawingBufferHeight, // full top
        (cssPosition.x * pixelRatio) | 0, // rect x
        (cssPosition.y * pixelRatio) | 0, // rect y
        1, // rect width
        1 // rect height
      );
      // render the scene
      renderer.setRenderTarget(pickingTexture);
      renderer.render(pickingScene, camera);
      renderer.setRenderTarget(null);
      // clear the view offset so rendering returns to normal
      camera.clearViewOffset();
      //read the pixel
      renderer.readRenderTargetPixels(
        pickingTexture,
        0, // x
        0, // y
        1, // width
        1, // height
        pixelBuffer
      );

      const id =
        (pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];

      infoElem.textContent = `You clicked sphere number ${id}`;

      return id;
    }
  }

  const pickHelper = new GPUPickHelper();

  function render(time) {
    time *= 0.001; // convert to seconds;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    cameraPole.rotation.y = time * 0.1;

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);

  function onClick(e) {
    const pickPosition = getCanvasRelativePosition(e);
    const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
  }

  function onTouch(e) {
    const touch = e.touches[0];
    const pickPosition = getCanvasRelativePosition(touch);
    const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
  }

  window.addEventListener("mousedown", onClick);
  window.addEventListener("touchstart", onTouch);
}

main();
</script>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 Three.js 中高效渲染数以万计的可变大小/颜色/位置的球体? 的相关文章

  • sqlParameters vs string.Format 哪个更能提高速度?

    sqlParameters vs string Format 哪个更能提高速度 在不承担安全性的情况下 cmd CommandText INSERT INTO tbl test VALUES a b SqlParameter SqlPara
  • Android:RunOnUiThread 与 AsyncTask

    我相信 Google 建议开发人员使用 AsyncTask 但是 我想知道它与使用 new Thread 然后调用 RunOnUiThread 在性能和内存效率方面有何不同 使用 RunOnUithread 的示例 some code 1
  • 网站性能衡量

    我需要一个免费的工具来测量网站的性能 并且不需要对代码 jsp asp 页面 进行任何更改 感谢所有帮助 对于绩效衡量 我建议您YSlow http developer yahoo com yslow 它是一个 Firefox 插件 集成了
  • MPI Alltoallv 还是更好的单独发送和接收? (表现)

    我有许多进程 大约 100 到 1000 个 每个进程都必须将一些数据发送到其他一些进程 比如大约 10 个 通常 但并非总是必要 如果 A 发送到 B B 也会发送到 A 每个进程都知道它必须从哪个进程接收多少数据 所以我可以用MPI A
  • 构建可扩展 Web 应用程序的书籍? (数据库性能/调优、网络、一般性能等)[关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 从计算机科学专业毕业并作为一名从事 Web 应用程序的软件工程师进入 现实世界 后 我对如何正确扩展 W
  • Google BigQuery:检索每行的最后版本

    我有一个 Google BigQuery 表 其中包含所有版本的资源 每次创建 更新 删除资源时 都会添加一个新行 并递增版本号 该数字将是添加行时的时间戳 ID ResourceID Action Count Timestamp ABC
  • 主键删除需要多长时间?

    画一个简单的表结构 Table1 Table2 ID lt ID Name gt Table1ID Name Table1有几百万行 例如 350 万行 我通过主键发出删除 DELETE FROM Table1 WHERE ID 100 中
  • JavaScript:字符串连接性能低下? Array.join('')?

    我读过如果我有一个for循环 我不应该使用字符串连接 因为它很慢 例如 for i 0 i lt 10000000 i str a 相反 我应该使用Array join 因为它更快 var tmp for i 0 i lt 10000000
  • 在二维平面中找到距离 P 点最近的 K 个点

    资料来源 亚马逊面试问题 解决方案1制作大小为 K 的堆并按最小距离收集点O NLogK 复杂 解决方案2 取大小为 N 的数组并按距离排序 应该使用QuickSort 霍尔修改 取前 K 点作为答案 这太复杂了 NlogN 但可以优化到近
  • Three.js - 如何翻译几何图形

    我有一个脚本 可以定位各种宽度 高度和深度的立方体 并且正在努力根据 xAxis yAxis 和 zAxis 也有所不同 将它们准确地排列起来 var geometry new THREE BoxGeometry width height
  • 数百个空闲线程的影响

    我正在考虑使用可能数百个线程来实现通过网络管理设备的任务 这是一个在带有 Linux 内核的 powerpc 处理器上运行的 C 应用程序 在每个任务进行同步以将数据从设备复制到任务的初始阶段之后 任务变得空闲 并且仅在收到警报或需要更改一
  • Maven 依赖项更新报告需要数小时才能完成

    我有任务运行 Jenkins 工作女巫会报告新版本的库 我认为这些可以满足我的需要 org codehaus mojo versions maven plugin 2 5 plugin updates report org codehaus
  • 如何利用磁盘 IO 队列

    我需要从 3 7 GB 文件中读取小数据序列 我需要阅读的职位是不相邻 但我可以命令 IO 以便从头到尾读取文件 该文件存储在 iSCSI SAN 上 该 SAN 应该能够处理 优化排队 IO 问题是 如何一次性请求我需要的所有数据 位置
  • 在未排序的整数列表中最优搜索 k 个最小值

    我刚刚接受采访时提出了一个问题 我很好奇答案应该是什么 问题本质上是 假设您有一个包含 n 个整数的未排序列表 您如何找到此列表中的 k 个最小值 也就是说 如果您有一个 10 11 24 12 13 列表并且正在寻找 2 个最小值 您将得
  • Android 在 ROOM 数据库中插入大量数据

    我有大约 10 个模型 每个模型都有超过 120K 行和 90 列的记录 其中包含双数组值 在 Room 中插入任何模型都需要超过 125 130 秒 任何人都可以建议我需要做什么才能使用一些批量插入技术来保存所有这些 120K 该技术大约
  • 如何提高QNX6下Eclipse IDE的性能

    我们在 VMWare 环境中通过 QNX6 运行 Eclipse 速度非常慢 Eclipse 是这样启动的 usr qnx630 host qnx6 x86 usr qde eclipse eclipse data root workspa
  • 在 JavaScript 中嵌套“switch”案例:有速度优势吗?

    这里有新手问题 我有一个包含大量字符串的 开关 像这样按字母顺序拆分是否有速度优势 switch myString substring 0 1 case a switch myString case a string beginning w
  • glBlitFramebuffer 渲染缓冲区和渲染全屏纹理哪个更快?

    哪个更快更高效 使用 OpenGL 纹理作为 CUDA 表面并在四边形上渲染 新样式 使用渲染缓冲区作为 CUDA 表面并使用 glBlitFramebuffer 进行渲染 None
  • Angularjs 在生产中禁用调试数据

    我正在尝试按照角度文档中的建议禁用生产服务器中的调试数据here https docs angularjs org guide production 补充一点 我并没有真正看到性能和加载时间有任何改进 这是我的代码在 app js 中的样子
  • HTML if 语句在 CDN 失败时加载本地 JS/CSS

    当从 CDN 或任何外部服务器加载 CSS JS 文件时 有可能 即使概率很低 由于外部故障而丢失该文件 在这种情况下 html 页面将因缺乏适当的 CSS 和 JS 而被损坏 有没有一种实用的方法可以在 CDN 故障时加载本地版本 IF

随机推荐

  • ReferenceEquals 在处理字符串时出错

    为什么在这种情况下ReferenceEquals对象的方法有不同的行为吗 string a fg string b fg Console WriteLine object ReferenceEquals a b 所以在这种情况下会得到一个结
  • WordPress - 没有此类文件或目录问题

    当我加载我的 WordPress 网站时收到此错误 警告 include once plugins acf location field master acf location php function include once 未能 打开
  • 库里的“readline”(或“haskeline”)?

    编写程序最实用的方法是什么柯里编程语言 http en wikipedia org wiki Curry programming language会有一个带有不错的行编辑功能的控制台用户界面吗 实际上 我需要传递一个字符串作为用户输入的建议
  • add_dependency 和 add_runtime_dependency 之间的区别?

    使用有什么区别add dependency and add runtime dependency在 Rails 引擎的 gemspec 中 例如 Gem Specification new do s s add dependency jqu
  • 在 Asp Mvc 网站中显示默认路由的完整 url

    我想当有人导航到我网站的根目录时显示完整的 URL 如果他们导航到 www mysite com 默认路由会正确处理它并显示正确的页面 问题是浏览器中的 URL 显示为 www mysite com 而不是 www mysite com u
  • 如何在不影响 MySQL 数据库的情况下跟踪页面浏览量

    我正在尝试使用以下查询跟踪 MySQL DB 中的页面浏览量 UPDATE table SET pageviews pageviews 1 WHERE page id 1 这对于中低流量来说是很好的 然而 在高流量下 对数据库的持续写入将导
  • glTexImage2D完成上传时如何收到通知?

    我想在纹理上传到 OpenGL 后进行渲染 但我无法收到有关完成的通知 我确实想避免使用动画或任何类型的重复渲染 glTexImage2D 到底是异步的吗 据我所知 几乎每个 OpenGL 调用都是异步的 无论如何 如果我也能知道 glDr
  • IoC 容器示例 [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 使用 Pytest 运行文档测试和普通测试

    在我的 Python 项目中 我使用 Pytest 目录结构为 src docs test 我有不同类型的测试 正常测试 在test 源代码中的文档测试 src 文档中的文档测试 docs rst 狮身人面像 我想一次运行所有这些 这样我就
  • 在magento布局xml中使用条件

    想知道是否有人在 magento 的布局 XML 中为自定义模块使用过 or 语句 我意识到我可以检查模块控制器或块本身中的值 但它似乎是逻辑所在的逻辑位置 Mage Core 将它们用于catalog xml测试 JavaScript 谢
  • 是否将可重入锁设为静态并使其成为互斥锁?

    在 Brian Goetz 的 Java Concurrency in Practice 一书中 他的可重入锁示例的编程如下 Lock lock new ReentrantLock 但是 我很好奇是否将上面的代码更改为 private st
  • 在现有pdf中的itext中追加数据

    我正在与文本 pdf图书馆 我想在现有 pdf 的末尾添加内容 举例来说 现有的 pdf 例如 Original pdf 有 4 页 所以我想添加另一页 即第 5 页的内容你好世界我添加了内容并将其保存在同一个 pdf 中 即原件 pdf
  • 切换 WordPress 站点服务器

    我有 WordPress 博客 最近我改变了我的托管 所以我用 filezilla 从旧服务器下载了所有博客文件 在新服务器上 我创建了目录博客 并在该目录下上传了所有文件 我在新服务器上创建了与旧服务器完全相同的新数据库并上传了该数据库
  • Google Play 商店,无法再查看总安装量

    我在 Google Play 商店上有一些应用程序 并使用总安装数作为我的增长计划的一部分 然而 自 2018 年 7 月 16 日起 我无法再在控制台中看到 总安装数 它仅显示 活动安装 谷歌刚刚从游戏商店中删除了最重要的数字之一吗 我还
  • 如何在 Rails 3 的控制器中使用 mixin 或模块?

    我的控制器中有一些行为 我将其提取到模块中 以便更好地测试并在一些地方重复使用它 对此有两个问题 哪里是放置我的模块的好地方 它们需要运行才能可供控制器使用 所以我在想config initializers 目录 不过 这对我来说似乎有点可
  • TextArea 的 JavaFX CSS 样式不起作用

    我正在编写一个简单的 JavaFX 应用程序 但我无法使用某些 CSS 样式 问题是 fx background color财产给我的TextArea 这是相关的CSS text area fx font family Consolas f
  • “numeric_limits”未在此范围内声明,没有匹配的函数可用于调用“max()”

    我在家里用 xcode 在我的 mac 上编译了这段代码 没有出现任何问题 我在学校用 g 在 Linux 上编译它 并收到以下错误 numeric limits 不是 std 的成员 gt 标记之前的预期主要表达式 没有匹配的函数来调用
  • F#代码引用调用、性能和运行时要求

    这里有 4 个与 F 代码引用深度相关的问题 如何调用 F 代码引用 它的调用方式是否会比普通的旧式 F lambda 效率低 到什么程度 它是否需要对高级反射或代码发出功能的运行时支持 我的目标嵌入式平台通常不存在或禁止这种功能 引文只是
  • 需要SDK版本 >=2.16.0 <3.0.0,版本解析失败

    当前的 Dart SDK 版本是 2 14 4 由于trackkit要求SDK版本 gt 2 16 0 我的 pubspec yaml environment sdk gt 2 16 0 lt 3 0 0 运行 flutter master
  • 在 Three.js 中高效渲染数以万计的可变大小/颜色/位置的球体?

    这个问题是从我的上一个问题中提炼出来的 我发现使用积分会导致问题 https stackoverflow com a 60306638 4749956 https stackoverflow com a 60306638 4749956 为