Как эффективно рендерить непосредственно на 3D текстуру в три js / webgl? - PullRequest
0 голосов
/ 15 января 2020

Я сейчас работаю над симуляцией жидкости. Я работаю в 3D, как и входы и выходы. Каждый шейдер берет один или несколько трехмерных образцов и в идеале должен выводить трехмерные данные.

В настоящее время я нарезаю трехмерный куб и запускаю шейдер на каждой плоскости. Этот метод работает, но затем мне нужно скопировать данные из каждой 2D текстуры в CPU, чтобы восстановить 3D текстуру и отправить ее обратно в GPU. Шаг копирования ужасно медленный, и я думаю, что этот метод не оптимален.

const vertexShaderPlane = `#version 300 es

precision highp float;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

in vec3 position;

out vec3 vPosition;

void main() {
    vPosition = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position.xy, 0., 1. );
}
`

const fragmentShaderPlane = `#version 300 es

precision highp float;
precision highp sampler3D;

uniform float uZ;
    
in vec3 vPosition;

out vec4 out_FragColor;
    
void main() {
    out_FragColor = vec4(vPosition.xy, uZ, 1.);
}`

const vertexShaderCube = `#version 300 es

precision highp float;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

in vec3 position;

out vec3 vPosition;

void main() {
    vPosition = position;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`

const fragmentShaderCube = `#version 300 es

precision highp float;
precision highp sampler3D;

uniform sampler3D sBuffer;

in vec3 vPosition;

out vec4 out_FragColor;
    
void main() {
    vec4 data = texture(sBuffer, vec3(vPosition));

    out_FragColor = vec4(data);
}
`

const canvas = document.createElement('canvas')
const context = canvas.getContext('webgl2', { alpha: false, antialias: false })

const scene = new THREE.Scene()
const renderer = new THREE.WebGLRenderer({ canvas, context })

const cameras = {
  perspective: new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 50000),
  texture: new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0, 1)
}

renderer.autoClear = false
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)

// cameras.perspective.position.set(2, 2, 2)

document.body.appendChild(renderer.domElement)

// Uniforms

const planeUniforms = { uZ: { value: 0.0 } }
const cubeUniforms = { sBuffer: { value: null } }

// Plane (2D)

const materialPlane = new THREE.RawShaderMaterial({
  uniforms: planeUniforms,
  vertexShader: vertexShaderPlane,
  fragmentShader: fragmentShaderPlane,
  depthTest: true,
  depthWrite: true
})

const planeGeometry = new THREE.BufferGeometry()
const vertices = new Float32Array([
  0, 0, 0,
  1, 0, 0,
  1, 1, 0,
  1, 1, 0,
  0, 1, 0,
  0, 0, 0
])
planeGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))

const plane = new THREE.Mesh(planeGeometry, materialPlane)
plane.position.set(-0.5, -0.5, -0.5)
scene.add(plane)

// Cube (3D)

const materialCube = new THREE.RawShaderMaterial({
  uniforms: cubeUniforms,
  vertexShader: vertexShaderCube,
  fragmentShader: fragmentShaderCube,
  depthTest: true,
  depthWrite: true,
  visible: false
})

const cube = new THREE.Group()
for (let x = 0; x < 32; x++) {
  const offset = x / 32
  const geometry = new THREE.BufferGeometry()
  const vertices = new Float32Array([
    0, 0, offset,
    1, 0, offset,
    1, 1, offset,
    1, 1, offset,
    0, 1, offset,
    0, 0, offset
  ])
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
  const mesh = new THREE.Mesh(geometry, materialCube)
  cube.add(mesh)
}

cube.position.set(-0.5, 0, -2)
cube.scale.set(0.5, 0.5, 0.5)
cube.rotation.set(1, 1, 1)
scene.add(cube)

// Computing Step

const texture2D = new THREE.WebGLRenderTarget(32, 32, { type: THREE.FloatType })
const planeSize = (32 ** 2 * 4)
const pixelBuffers = Array.from(Array(32), () => new Float32Array(planeSize))

const data = new Float32Array(planeSize * 32)
renderer.setRenderTarget(texture2D)
for (let i = 0; i < 32; i++) {
  materialPlane.uniforms.uZ.value = i / 32

  renderer.render(scene, cameras.texture)

  renderer.readRenderTargetPixels(texture2D, 0, 0, 32, 32, pixelBuffers[i]) // SLOW PART
  data.set(pixelBuffers[i], i * planeSize)
}

const texture3D = new THREE.DataTexture3D(data, 32, 32, 32)
texture3D.format = THREE.RGBAFormat
texture3D.type = THREE.FloatType
texture3D.unpackAlignment = 1

materialPlane.visible = false

// Display Step

materialCube.visible = true
cubeUniforms.sBuffer.value = texture3D
renderer.setRenderTarget(null)
renderer.render(scene, cameras.perspective)
<script src="https://threejs.org/build/three.min.js"></script>

Подчеркну, что рендеринг работает . Это просто очень медленно , потому что мне нужно выполнить дюжину шейдеров.

Потенциальные решения, которые я нашел, следующие:

  • Используйте функцию render.copyFramebufferToTexture для скопируйте данные непосредственно в новую текстуру. К сожалению, я думаю, что это работает только для 2D, а не 3D текстур.
  • Используйте веб-работников, чтобы разделить задачу и отобразить план для каждого работника. Однако, чтобы передать данные работникам, вы должны скопировать их, что возвращает к первоначальной проблеме. Передача данных также не будет эффективной, поскольку, пока один работник будет выполнять свою работу, другие не будут иметь доступа к данным.

РЕДАКТИРОВАТЬ:

Я просто ищу способ ускорить процесс копирования 2D-данных в ЦП, чтобы вернуть 3D-текстуру обратно в графический процессор.

Реальная проблема - renderer.readRenderTargetPixels, которая действительно замедляет мой рендеринг .

1 Ответ

1 голос
/ 16 января 2020

Как упоминалось в @ScieCode, вы не можете писать в 3D-текстуру в WebGL / WebGL2, но вы можете использовать 2D-текстуру в качестве 3D-данных. Представьте, что у нас есть 3D-текстура 4х4х4. Мы можем сохранить это в 2D текстуре. Это 4 ломтика 4х4. Мы можем упорядочить эти срезы

00001111
00001111
00001111
00001111
22223333
22223333
22223333
22223333

Чтобы получить пиксель от этой 2D-текстуры, используемой в качестве 3D-данных

   ivec3 src = ...              // some 3D coord
   int cubeSize = 4;            // could pass in as uniform
   ivec2 slices = size / cubeSize;
   ivec2 size = textureSize(some2DSampler, 0);
   ivec2 src2D = ivec2(
      src.x + (src.z % slices.x) * cubeSize,
      src.y + (src.z / slices.x) * cubeSize);
   vec4 color = texelFetch(some2DSampler, src2D, 0);

Если мы рендерим один квад по всей текстуре, мы знаем, какой пиксель в настоящее время пишется в 3D с

  // assume size is the same as the texture above, otherwise pass it in
  // as a uniform

  int cubeSize = 4;            // could pass in as uniform
  ivec2 slices = size / cubeSize;
  ivec2 dst2D = ivec2(gl_FragCoord.xy);
  ivec3 dst = ivec3(
      dst2D.x % cubeSize,
      dst2D.y % cubeSize,
      dst2D.x / cubeSize + (dst2D.y / cubeSize) * slices.x);

, код выше предполагает, что каждое измерение куба имеет одинаковый размер. Что-то более общее c, скажем, у нас был куб 5x4x6. Мы можем выложить это как срезы 3x2

000001111122222
000001111122222
000001111122222
000001111122222
333334444455555
333334444455555
333334444455555
333334444455555
   ivec3 src = ...                   // some 3D coord
   ivec3 cubeSize = ivec3(5, 4, 6);  // could pass in as uniform
   ivec2 size = textureSize(some2DSampler, 0);
   int slicesAcross = size.x / cubeSize.x; 
   ivec2 src2D = ivec2(
      src.x + (src.z % slicesAcross) * cubeSize,
      src.y + (src.z / slicesAcross) * cubeSize);
   vec4 color = texelFetch(some2DSampler, src2D, 0);

  ivec3 cubeSize = ivec3(5, 4, 6);  // could pass in as uniform
  ivec2 slicesAcross = size.x / cubeSize;
  ivec2 dst2D = ivec2(gl_FragCoord.xy);
  ivec3 dst = ivec3(
      dst2D.x % cubeSize.x,
      dst2D.y % cubeSize.y,
      dst2D.x / cubeSize.x + (dst2D.y / cubeSize.y) * slicesAcross);
...