Как я могу быстро вывести точки из очень разреженной текстуры? - PullRequest
0 голосов
/ 03 июля 2019

По сути, у меня есть объект WebGLTexture 512x512x512, который равен 0. Везде, кроме примерно 3 вокселей, где он равен 1. Мне нужно как можно быстрее распечатать координаты XYZ этих 3 вокселей для приложения для научных вычислений.связан с моими исследованиями, но лучшее, что я могу сделать, - это использовать [параллельный] цикл for после прохождения объекта через неуклюжую цепочку методов WebGL2.Кто-нибудь знает более быстрый способ получить эти координаты?Есть ли способ отправить примитивы vec3 в массив из фрагмента-шейдера?

Я безуспешно искал полезные расширения.

Я помещаю tbl.compressedTable в массив каждый временной шаг с помощью:

                var tbl = new Abubu.RgbaCompressedDataFromTexture({ 
                    target    : env.stipt,
                    threshold : env.fthrsh,
                    compressionThresholdChannel : 'r',
                });
                this.timeSeries.push(time) ;
                this.lastRecordedTime = time ;
                this.samples.push([tbl.compressedTable]) ;

Где последняя строка - убийца.Я использую прототип класса:

class RgbaCompressedDataFromTexture extends RgbaCompressedData{
    constructor( options={} ){
        if ( options.target == undefined && 
             options.texture == undefined ) return null ;

        var texture ;
        texture = readOption(options.target, null ) ;
        texture = readOption(options.texture, options.target ) ;

        var ttbond = new Float32TextureTableBond({ target : texture } ) ;
        ttbond.tex2tab() ;
        var table       = ttbond.table ;
        var width       = ttbond.width ;
        var height      = ttbond.height ;
        var op          = options ;
        op.width        = width ;
        op.height       = height ;

        super( table, op ) ;
        this.ttbond     = ttbond ;
        this.texture    = texture ;
    }
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *  CONSTRUCTOR ENDS
 *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 */

Расширение класса:

class RgbaCompressedData{
    constructor( data, options={}){

        if (data == undefined){
            log( 'You need to provide data source for compression!') ;
            return null ;
        }

        this.data       = new Float32Array(data) ;
        this.width      = readOption( options.width,    data.length/4   ) ;
        this.height     = readOption( options.height,   1               ) ;
        if ( (this.width == (data.length/4)) && height != 1 ){
            this.width = (data.length/this.height)/4 ;
        }

        this.threshold  = readOption(   options.threshold, 0            ) ;
        this.threshold  = readOption(   options.compressionThreshold,
                                        this.threshold                  ) ;

        this.compressionThresholdChannel
                        = readOption(   options.channel,    'r'         ) ;

        switch (this.compressionThresholdChannel){
            case 'r' :
                this.channel = 0 ;
                break ;
            case 'g' :
                this.channel = 1 ;
                break ;
            case 'b' :
                this.channel = 2 ;
                break ;
            case 'a' :
                this.channel = 3 ;
                break ;
            default :
                this.channel = 0 ;
                break ;
        }

        this.compThresholdData = new Float32Array(this.width*this.height) ;

/*------------------------------------------------------------------------
 * count number of pixels above the compression threshold
 *------------------------------------------------------------------------
 */
        this.noAboveThreshold = 0 ;
        for(var j=0 ; j<this.height ; j++){
            for (var i=0 ; i <this.width; i++){
                var indx    = i + j*this.width ;
                this.compThresholdData[indx]
                        = this.data[indx*4 + this.channel] ;
                if (this.compThresholdData[indx]>this.threshold){
                        this.noAboveThreshold++ ;
                }
            }
        }

/*------------------------------------------------------------------------
 * allocating memory to data
 *------------------------------------------------------------------------
 */
        this.compressedSize    =
            Math.ceil( Math.sqrt( this.noAboveThreshold )) ;

        this.compressedTable =
            new Float32Array(this.compressedSize*this.compressedSize*4  ) ;
        this.decompressionMapTable =
            new Float32Array(this.compressedSize*this.compressedSize*4  ) ;
        this.compressionMapTable =
            new Float32Array(this.width*this.height * 4 ) ;

/*------------------------------------------------------------------------
 * compress data
 *------------------------------------------------------------------------
 */
        var num = 0 ;
        for(var j=0 ; j<this.height ; j++){
            for (var i=0 ; i <this.width; i++){
                var indx    = i + j*this.width ;
                if (this.compThresholdData[indx]>this.threshold){
                    var jj  = Math.floor( num/this.compressedSize) ;
                    var ii  = num - jj*this.compressedSize ;

                    var x   = ii/this.compressedSize
                            + 0.5/this.compressedSize ;
                    var y   = jj/this.compressedSize
                            + 0.5/this.compressedSize ;

                    var nindx = ii + jj*this.compressedSize ;

                    this.compressionMapTable[indx*4     ]   = x ;
                    this.compressionMapTable[indx*4 + 1 ]   = y ;
                    this.decompressionMapTable[nindx*4  ]   =
                        i/this.width + 0.5/this.width ;
                    this.decompressionMapTable[nindx*4+1]   =
                        j/this.height+ 0.5/this.height ;

                    for (var k = 0 ; k<4 ; k++){
                        this.compressedTable[nindx*4+k]
                            = this.data[indx*4+k] ;
                    }
                    num++ ;
                }else{
                    this.compressionMapTable[indx*4     ]
                        = 1.-0.5/this.compressedSize ;
                    this.compressionMapTable[indx*4 + 1 ]
                        = 1.-0.5/this.compressedSize ;
                }

            }
        }
        var ii = this.compressedSize -1 ;
        var jj = this.compressedSize -1 ;
        var nindx = ii + jj*this.compressedSize ;
        for (var k = 0 ; k<4 ; k++){
            this.compressedTable[nindx*4+k] = 0. ;
        }

/*------------------------------------------------------------------------
 * setting compressedData, compressionMap, decompressionMap textures
 *------------------------------------------------------------------------
 */
        this.full   = new TableTexture(
            this.data,
            this.width,
            this.height,
            {
                minFilter : 'nearest' ,
                magFilter : 'nearest'
            }
        ) ;

        this.sparse = new TableTexture(
            this.compressedTable,
            this.compressedSize ,
            this.compressedSize ,
            {
                minFilter : 'nearest' ,
                magFilter : 'nearest'
            }
        ) ;

        this.compressionMap     = new TableTexture(
            this.compressionMapTable,
            this.width,
            this.height ,
            {
                minFilter : 'nearest' ,
                magFilter : 'nearest'
            }
        ) ;

        this.decompressionMap   = new TableTexture(
            this.decompressionMapTable ,
            this.compressedSize ,
            this.compressedSize ,
            {
                minFilter : 'nearest' ,
                magFilter : 'nearest'
            }
        ) ;
    }   
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *  CONSTRUCTOR ENDS
 *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 */

И использую следующий класс:

class Float32TextureTableBond{

/*------------------------------------------------------------------------
 * constructor
 *------------------------------------------------------------------------
 */
    constructor( options={}){
        if ( options.target == undefined && options.texture == undefined ){
            return null ;
        } ;

        this.texture = readOptions( options.target , null ) ;
        this.texture = readOptions( options.texture, this.target ) ;

        this.framebuffer = gl.createFramebuffer() ;
        gl.bindFramebuffer( gl.READ_FRAMEBUFFER, this.framebuffer) ;
        gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
                                gl.TEXTURE_2D,
                                this.target.texture, 0 ) ;
        gl.readBuffer( gl.COLOR_ATTACHMENT0 ) ;
        this.canRead    = (
            gl.checkFramebufferStatus(gl.READ_FRAMEBUFFER)
            == gl.FRAMEBUFFER_COMPLETE
        ) ;
        gl.bindFramebuffer( gl.READ_FRAMEBUFFER, null) ;

        this.width  = this.target.width ;
        this.height = this.target.height ;
        this.table   = readOption(options.table, 
                new Float32Array(this.width*this.height*4 ) ) ;
    }
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *  CONSTRUCTOR ENDS
 *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 */

Нет сообщений об ошибках, правильный вывод.Когда я начинаю записывать данные, моё моделирование замедляется до скорости летаргической черепахи.

Ответы [ 2 ]

0 голосов
/ 03 июля 2019

Я на самом деле не продумал это, но вот код, который может дать вам идеи.

Проблема в том, что в WebGL2 AFAIK нет способа условно выводить данные.Вы можете отбросить в фрагментном шейдере, но здесь это не кажется полезным.

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

Итак, вот одна из идей, разделить 3D-текстуру на ячейки NxNxN большие, ищите в каждой ячейке по вокселям.Если ячейка имеет размер 32x32x32, то для входа 512x512x512 есть 4096 вещей для распараллеливания.Для каждой ячейки обойдите ячейку и суммируйте позиции совпадений

sum = vec4(0)
for each voxel in cell
   if voxel === 1
     sum += vec4(positionOfVoxel, 1);

outColor = sum;

В результате, если в этой ячейке всего 1 совпадение, то sum.xyz будет содержать позицию, а sum.w будет 1Если существует более одного совпадения, sum.w будет> 1

. Приведенный ниже код создает текстуру 4096x1 и отдает ей четверку.Он использует gl_FragCoord.x, чтобы вычислить, какой ячейке соответствует каждый отображаемый пиксель, и суммирует результаты для соответствующей ячейки.

Затем он использует readPixels для чтения результата, проходит и распечатывает их.В идеале мне бы хотелось, чтобы сама графическая карта вычисляла результаты, но, учитывая, что вы не можете условно отбросить, у меня не было никаких идей.

Для ячейки с одним результатом выводится результат.Для ячейки с множественным результатом другой шейдер, который сканирует ячейку.Мы знаем, сколько результатов в конкретной ячейке, поэтому мы можем визуализировать numResults на 1 пиксель.Затем шейдер перебирает ячейку и смотрит только на N-й результат, который он находит

 int idOfResultWeWant = int(gl_FragCoord.x)
 int resultId = 0
 for (z...) {
   for (y...) {
     for (x...) {
       if (voxel) {
         if (resultId === idOfResultWeWant) {
           outColor = position
         }
         ++resultId
       }
    }
 }

Приведенный ниже код ленив и использует 1D текстуры результатов, что означает, что большинство ячеек, которые он может обработать, составляет gl.getParameter(gl.MAX_TEXTURE_SIZE),Для больших размеров пришлось бы немного измениться.

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

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

function main() {
  const gl = document.createElement('canvas').getContext('webgl2');
  if (!gl) {
    return alert('need webgl2');
  }
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    return alert('need EXT_color_buffer_float');
  }
  const size = 512;
  const cellSize = 32;
  const cellsPer = size / cellSize;
  const numCells = (size * size * size) / (cellSize * cellSize * cellSize);
  
  const dataTexture = twgl.createTexture(gl, {
    target: gl.TEXTURE_3D,
    width: size,
    height: size,
    depth: size,
    minMag: gl.NEAREST,
    internalFormat: gl.R8,
    auto: false,
  });
  
  function setData(x, y, z) {
    log('set voxel:', x, y, z);
    gl.texSubImage3D(
       gl.TEXTURE_3D, 0, x, y, z, 1, 1, 1, 
       gl.RED, gl.UNSIGNED_BYTE, new Uint8Array([255]));
  }
  
  for (let i = 0; i < 3; ++i) {
    const x = randInt(size);
    const y = randInt(size);
    const z = randInt(size);
    setData(x, y, z);
  }
  setData(128, 267, 234);
  setData(128 + 4, 267, 234);
  setData(128 + 9, 267, 234);
  
  const cellVS = `#version 300 es
  in vec4 position;
  void main() {
    gl_Position = position;
  }
  `;
  
  const cellFS = `#version 300 es
  precision highp float;
  uniform highp sampler3D data;
  uniform int cellSize;
  out vec4 outColor;
  void main() {
    // really should use 2D but I'm lazy
    int ndx = int(gl_FragCoord.x);
    // assumes equal sides
    int size = textureSize(data, 0).x;
    int cellsPer = size / cellSize;
    int cellZ = ndx / cellsPer / cellsPer;
    int cellY = ndx / cellsPer % cellsPer;
    int cellX = ndx % cellsPer;
    
    ivec3 cell = ivec3(cellX, cellY, cellZ) * cellSize;
    vec4 sum = vec4(0);
    for (int z = 0; z < cellSize; ++z) {
      for (int y = 0; y < cellSize; ++y) {
        for (int x = 0; x < cellSize; ++x) {
          ivec3 pos = cell + ivec3(x, y, z);
          // assumes data is 0 or 1
          float occupied = texelFetch(
              data, 
              pos,
              0).r;
          sum += vec4(pos, 1) * occupied;
        }
      }
    }
    outColor = sum; 
  }
  `;
  
  const cellScanFS = `#version 300 es
  precision highp float;
  uniform highp sampler3D data;
  uniform int cellSize;
  uniform ivec3 cell;  // offset into cell
  out vec4 outColor;
  void main() {
    // really should use 2D but I'm lazy
    int idWeWant = int(gl_FragCoord.x);
    // assumes equal sides
    int size = textureSize(data, 0).x;
    int cellsPer = size / cellSize;
    
    vec4 result = vec4(0);
    int id = 0;
    for (int z = 0; z < cellSize; ++z) {
      for (int y = 0; y < cellSize; ++y) {
        for (int x = 0; x < cellSize; ++x) {
          ivec3 pos = cell + ivec3(x, y, z);
          float occupied = texelFetch(
              data, 
              pos,
              0).r;
          if (occupied > 0.0) {
            if (id == idWeWant) {
              result = vec4(pos, 1);
            }
            ++id;
          }
        }
      }
    }
    outColor = result; 
  }
  `;
  
  const cellProgramInfo = twgl.createProgramInfo(gl, [cellVS, cellFS]);
  const cellScanProgramInfo = twgl.createProgramInfo(gl, [cellVS, cellScanFS]);
  
  const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl, 2);
  
  // as long as numCells is less than the max
  // texture dimensions we can use a 
  // numCells by 1 result texture.
  // If numCells is > max texture dimension
  // we'd need to adjust the code to use
  // a 2d result texture.

  const cellResultWidth = numCells;
  const cellResultHeight = 1;
  const cellResultFBI = twgl.createFramebufferInfo(gl, [
    { internalFormat: gl.RGBA32F, minMag: gl.NEAREST }
  ], cellResultWidth, cellResultHeight);
  
  twgl.bindFramebufferInfo(gl, cellResultFBI);

  twgl.setBuffersAndAttributes(gl, cellProgramInfo, quadBufferInfo);
  
  gl.useProgram(cellProgramInfo.program);
  twgl.setUniforms(cellProgramInfo, {
    cellSize,
    data: dataTexture,
  });
  
  // draw the quad
  twgl.drawBufferInfo(gl, quadBufferInfo);
  
  const data = new Float32Array(numCells * 4);
  gl.readPixels(0, 0, numCells, 1, gl.RGBA, gl.FLOAT, data);
  
  gl.useProgram(cellScanProgramInfo.program);
  
  {
    for (let i = 0; i < numCells; ++i) {
      const off = i * 4;
      const numResultsInCell = data[off + 3];
      if (numResultsInCell) {
        if (numResultsInCell === 1) {
          log('result at: ', ...data.slice(off, off + 3));
        } else {
          getResultsForCell(i, numResultsInCell);
        }
      }
    }
  }
  
  function getResultsForCell(i, numResultsInCell) {
    const cellZ = (i / cellsPer | 0) / cellsPer | 0;
    const cellY = (i / cellsPer | 0) % cellsPer;
    const cellX = i % cellsPer;

    twgl.setUniforms(cellScanProgramInfo, {
      cellSize,
      data: dataTexture,
      cell: [cellX * cellSize, cellY * cellSize, cellZ * cellSize],
    });
    twgl.drawBufferInfo(gl, quadBufferInfo);

    // note: cellResultsFBI is still bound. It's 4096x1
    // so we can only get up to 4096 results without switching to
    // a 2D texture
    gl.viewport(0, 0, numResultsInCell, 1);

    const result = new Float32Array(numResultsInCell * 4);
    gl.readPixels(0, 0, numResultsInCell, 1, gl.RGBA, gl.FLOAT, result);
    for (let j = 0; j < numResultsInCell; ++j) {
      const off = j * 4;
      log('result at:', ...result.slice(off, off + 3));
    }
  }  
  
  function randInt(min, max) {
    return Math.floor(rand(min, max));
  }
  
  function rand(min, max) {
    if (max === undefined) {
      max = min;
      min = 0;
    }
    return Math.random() * (max - min) + min;
  }

  function log(...args) {
    const elem = document.createElement('pre');
    elem.textContent = [...args].join(' ');
    document.body.appendChild(elem);
  }

}
main();
pre { margin: 0; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
0 голосов
/ 03 июля 2019

Есть ли способ передать примитивы vec3 в массив из фрагмента шейдера?

Да, использовать буфер хранения шейдера.Что-то вроде:

layout(std430, binding = 0) buffer Output
{
  uvec3 out_vals[];
};

Это должно быть связано с буфером, достаточно большим, чтобы хранить возвращенные аргументы (Сверху я думаю, что std430 допускает типы вывода vec3,но у меня также есть это странное ощущение, что тип out может быть и должен быть uint, поэтому вам может понадобиться написать 3 значения за раз - не могу вспомнить, к сожалению)

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

layout(binding = 0, offset = 0) uniform atomic_uint out_count;

Затем в вашем шейдере сгенерируйте свой индекс из gl_GlobalInvocatonID (если вы используете вычислительный шейдер) или gl_SamplePosition для фрагментных шейдерови вы должны быть в состоянии записать данные:

uint index = atomicCounterIncrement(out_count);
out_vals[index] = gl_GlobalInvocatonID;

Можно напрямую использовать атомарные операции с буферами хранения шейдеров, но большинство советов, которые я видел, рекомендует вместо этого использовать ACB.

...