Производительный рендеринг десятков тысяч сфер переменного размера / цвета / положения в три. js? - PullRequest
0 голосов
/ 20 февраля 2020

Этот вопрос взят из моего последнего вопроса, где я обнаружил, что использование Баллов приводит к проблемам: { ссылка }

Чтобы решить эту проблему, вам нужно нарисовать точки с использованием квадратов вместо точек. Есть много способов сделать это. Нарисуйте каждый четырехугольник как отдельный me sh или спрайт, или объедините все четырехугольники в другой me sh, или используйте InstancedMe sh, где вам понадобится матрица для каждой точки, или напишите собственные шейдеры для выполнения точек (см. последний пример этой статьи)

Я пытался выяснить этот ответ. Мои вопросы

Что такое «инстансинг»? В чем разница между слиянием геометрии и экземпляром? И, если бы я сделал один из них, какую геометрию я бы использовал и как бы менял цвет? Я смотрел на этот пример:

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

И я вижу, что для каждой сферы у вас будет геометрия, которая будет применять положение и размер (шкала?). Будет ли лежащая в основе геометрия SphereBufferGeometry единичного радиуса, тогда? Но как применить цвет?

Кроме того, я читал о методе нестандартного шейдера, и он имеет некоторый смутный смысл. Но это кажется более сложным. Будет ли производительность лучше, чем выше?

Ответы [ 2 ]

0 голосов
/ 20 февраля 2020

Основываясь на вашем предыдущем вопросе ...

Во-первых, Instancing - это способ сказать три. js рисовать одну и ту же геометрию несколько раз, но изменять еще одну вещь для каждого "экземпляра". IIR C единственное, что поддерживает три. js «из коробки» устанавливает различные матрицы (положение, ориентация, масштаб) для каждого экземпляра. В прошлом, как например, с разными цветами, вы должны писать собственные шейдеры.

Instancing позволяет попросить систему нарисовать много вещей с одним «запросом» вместо «спросить» за вещь. Это означает, что все заканчивается гораздо быстрее. Вы можете думать об этом как о чем угодно. Если вам нужны 3 торговца, вы можете попросить кого-нибудь сделать вас 1. Когда они закончат, вы можете попросить их сделать другого. Когда они закончат, вы можете попросить их сделать третий. Это было бы намного медленнее, чем просто попросить их сделать 3 хамбергера на старте. Это не идеальная аналогия, но она указывает на то, что запрос нескольких вещей по одному менее эффективен, чем запрос нескольких вещей одновременно.

Объединение мешей - это еще одно решение после плохо аналогия выше, объединение ячеек похоже на создание одного большого 1-фунтового гамбургера вместо трех 1/3-фунтовых гамбургеров. Перевернуть один большой бургер и положить на один большой булку начинки и булочки немного быстрее, чем сделать то же самое с 3 маленькими бургерами.

Что является лучшим решением для вас, которое зависит. В своем исходном коде вы просто рисовали текстурированные квадраты, используя Точки. Точки всегда рисуют свой квад в пространстве экрана. С другой стороны, сетки вращаются в мировом пространстве по умолчанию, поэтому, если вы создали экземпляры четырехугольников или объединенный набор четырехугольников и попытались повернуть их, они поворачивались бы и не смотрели на камеру, как точки. Если бы вы использовали сферную геометрию, то у вас возникли бы проблемы, заключающиеся в том, что вместо вычисления 6 вершин на каждый квад с нарисованной окружностью вы бы вычисляли 100 или 1000 вершин на сферу, что было бы медленнее, чем 6 вершин на квад.

Итак, опять же, для сохранения точек, обращенных к камере, требуется собственный шейдер.

Чтобы сделать это с помощью создания короткой версии, вы решаете, какие данные вершин повторяются в каждом экземпляре. Например, для текстурированного четырехугольника нам нужно 6 позиций вершин и 6 uvs. Для этого вы делаете нормальный BufferAttribute

Затем вы решаете, какие данные вершин являются уникальными для каждого экземпляра. В вашем случае размер, цвет и центр точки. Для каждого из них мы создаем InstancedBufferAttribute

Мы добавляем все эти атрибуты к InstancedBufferGeometry и в качестве последнего аргумента мы сообщаем ему, сколько экземпляров.

Во время отрисовки вы можете Думайте об этом так

  • для каждого экземпляра
    • установить размер на следующее значение в атрибуте размера
    • установить цвет на следующее значение в атрибуте цвета
    • установить центр на следующее значение в атрибуте center
    • 6 раз вызвать вершинный шейдер, а в их атрибутах position и uv установить n-ное значение.

Таким образом, вы получаете одну и ту же геометрию (позиции и uvs), используемые несколько раз, но каждый раз меняются несколько значений (размер, цвет, центр).

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>
0 голосов
/ 20 февраля 2020

Это довольно широкий топи c. Короче говоря, слияние и создание экземпляров сводятся к уменьшению количества вызовов отрисовки при рендеринге чего-либо.

Если вы привязали геометрию сферы один раз, но продолжали ее повторную визуализацию, вам стоило бы больше сказать компьютеру, чтобы он рисовал ее много раз, чем компьютеру, чтобы вычислить, что нужно для ее рисования. В итоге вы получаете GPU, мощное устройство параллельной обработки, которое бездействует.

Очевидно, что если вы создаете уникальную сферу в каждой точке пространства и объединяете их все, вы платите цену за сообщение GPU: сделать один раз, и он будет занят рендерингом тысяч ваших сфер.

Тем не менее, это объединение увеличит объем используемой памяти и будет иметь некоторые издержки при создании уникальных данных. Instancing - это встроенный умный способ достижения того же эффекта при меньших затратах памяти.

У меня есть статья, написанная на эту тему c.

...