Как я могу правильно написать эту функцию шейдера в JS?

Что я хочу:

Для тестирования стиля игрового искусства, о котором я думал, я хочу отобразить 3D-мир в пиксельной форме. Так, например, возьмите сцену, подобную этой (но визуализированную с определенным цветом/стилем, чтобы хорошо выглядеть после пикселизации):

Original Image

И заставьте это выглядеть примерно так:

Pixelated Image

Играя разными способами стилизации 3D-источника, я думаю, что пикселированный вывод может выглядеть красиво. Конечно, чтобы получить этот эффект, он просто уменьшает изображение до ~ 80p и увеличивает его до 1080p с повторной выборкой ближайшего соседа. Но более эффективно выводить прямо на холст 80p, чтобы начать с него, и просто сделать масштабирование.

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

Мой код:

Мой буфер для растрового изображения хранится в строке major, как r1, g1, b1, a1, r2, g2, b2, a2... и я использую gpu.js, который по существу преобразует эту функцию JS в шейдер. Моя цель - взять одно растровое изображение и вернуть его в более крупном масштабе с масштабированием ближайшего соседа, поэтому каждый пиксель становится квадратом 2x2 или 3x3 и так далее. Предположим, что inputBuffer представляет собой масштабированную долю размера выхода, определяемую методом setOutput.

var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) {
  var y = Math.floor((this.thread.x / (width[0] * 4)) / scale[0]);
  var x = Math.floor((this.thread.x % (width[0] * 4)) / scale[0]);
  var remainder = this.thread.x % 4;
  return inputBuffer[(x * y) + remainder]; 
}).setOutput([width * height * 4]);

JSFiddle

Имейте в виду, что он перебирает новый буфер вывода полного размера, поэтому мне нужно найти правильные координаты, которые будут существовать в более sourceBuffer основываясь на текущем индексе в outputBuffer (индекс раскрывается lib как this.thread.x).

Что происходит вместо этого:

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

Result

Что я делаю не так?

Примечание. Я задал связанный с этим вопрос: существует ли более простой (и все еще совершенный) способ масштабирования рендеринга холста с повторной выборкой ближайшего соседа?

Ответы

Ответ 1

Итоговый ответ

Ответ Таруна помог мне дойти до моего окончательного решения, поэтому его награда была заслуженной, но я действительно узнал об этой функции (графический вывод в сочетании с совместным использованием контекста для прямого вывода буфера на цель рендеринга) gpu.js, что позволяет примерно на 30 раз быстрее рендеринг, общее время затенения и рендеринг вывода с 30ms+ до ~ 1 мс, и это без дополнительной оптимизации. Теперь я знаю, что можно отправить буфер массива на графический процессор еще быстрее, но у меня просто не было мотивация, чтобы получить время затенения/рендеринга ниже 1 мс.

var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);
var gl = canvas.getContext('webgl');
var gpu = new GPU({
  canvas,
  gl
});
var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, scale, size, purity, div) {
    var subX = Math.floor(this.thread.x / scale[0]);
    var subY = Math.floor(this.thread.y / scale[0]);
    var subIndex = ((subX * 4) + (subY * width[0] * 4));
    var rIndex = subIndex;
    var gIndex = subIndex + 1;
    var bIndex = subIndex + 2;
    var r = ((inputBuffer[rIndex] * purity[0]) + inputBuffer[rIndex - 4] + inputBuffer[rIndex + 4]) / (div[0]);
    var g = ((inputBuffer[gIndex] * purity[0]) + inputBuffer[gIndex - 4] + inputBuffer[gIndex + 4]) / (div[0]);
    var b = ((inputBuffer[bIndex] * purity[0]) + inputBuffer[bIndex - 4] + inputBuffer[bIndex + 4]) / (div[0]);
    this.color(r / 255, g / 255, b / 255);
  }).setOutput([width, height]).setGraphical(true);

inputBuffer - это просто буфер, полученный методом tr.js readRenderTargetPixels.

renderer.render(scene, camera, rt);
renderer.readRenderTargetPixels(rt, 0, 0, smallWidth, smallHeight, frameBuffer);
pixelateMatrix(frameBuffer, [smallWidth], [scale], [size], [purity], [div]);

Примечание

Можем ли мы немного удивляться, сколько энергии WebGL приносит браузеру? Это 8,2944 миллиона многоцелевых задач, выполненных всего за 1 мс. ~ 64 миллиарда полных максимальных математических операций в секунду для моего шейдера по моему счету. Это безумие. Может ли это быть прав? Является ли моя математика неправильной? Я вижу, что nvidia self driving AI выполняет 24 триллиона операций в секунду, поэтому я думаю, что эти цифры на моих 1060 находятся в пределах возможностей. Это просто невероятно.

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

Ответ 2

Обновление 1 - 25 мая 2018 года

Я смог решить большинство вопросов. Было немало

  1. Логика трансформации была неправильной, и данные по какой-то причине перевернулись, поэтому я щелкнул столбцами и строками, чтобы начать снизу справа

    var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) {
    var size = width[0] * height[0] * 4;
    var current_index = Math.floor((size - this.thread.x)/4); 
    var row = Math.floor(current_index / (width[0] * scale[0]) );
    var col = Math.floor((current_index % width[0])/scale[0]);
    var index_old = Math.floor(row * (width[0] / scale[0])) + width[0] - col;
    var remainder = this.thread.x % 4;
    return inputBuffer[index_old * 4 + remainder];
    
    }).setOutput([width * height * 4]);
    
  2. Вы использовали ширину и высоту в поплавках, которые я изменил, чтобы их сначала вычисляли, а затем масштабировали

    var smallWidth = Math.floor(window.innerWidth / scale);
    var smallHeight = Math.floor(window.innerHeight / scale);
    
    var width = smallWidth * scale;
    var height = smallHeight * scale;
    
    
    var rt = new THREE.WebGLRenderTarget(smallWidth, smallHeight);
    var frameBuffer = new Uint8Array(smallHeight * smallHeight * 4);
    var outputBuffer = new Uint8ClampedArray(width * height * 4);
    
  3. Размер холста был установлен как внутренняя ширина и высота, вам нужно установить только ширину и высоту изображения

    context = canvas.getContext('2d');
    canvas.width = width;
    canvas.height = height;
    

Ниже приведен финальный JSFiddle для того же

https://jsfiddle.net/are5Lbw8/6/

Результаты:

Working Upscale

Окончательный код для справки

var container;
var camera, scene, renderer;
var mouseX = 0;
var mouseY = 0;
var scale = 4;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
var smallWidth = Math.floor(window.innerWidth / scale);
var smallHeight = Math.floor(window.innerHeight / scale);

var width = smallWidth * scale;
var height = smallHeight * scale;


var rt = new THREE.WebGLRenderTarget(smallWidth, smallHeight);
var frameBuffer = new Uint8Array(smallHeight * smallHeight * 4);
var outputBuffer = new Uint8ClampedArray(width * height * 4);
var output;
var divisor = 2;
var divisorHalf = divisor / 2;
var negativeDivisorHalf = -1 * divisorHalf;
var canvas;
var context;
var gpu = new GPU();

var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) {
/*   var y = Math.floor((this.thread.x / (width[0] * 4)) / scale[0]);
  var x = Math.floor((this.thread.x % (width[0] * 4)) / scale[0]);
  var remainder = this.thread.x % 4;
  return inputBuffer[(x * y) + remainder];
   */
   var size = width[0] * height[0] * 4;
   var current_index = Math.floor((size - this.thread.x)/4); 
   var row = Math.floor(current_index / (width[0] * scale[0]) );
   var col = Math.floor((current_index % width[0])/scale[0]);
   var index_old = Math.floor(row * (width[0] / scale[0])) + width[0] - col;
   var remainder = this.thread.x % 4;
   return inputBuffer[index_old * 4 + remainder];

}).setOutput([width * height * 4]);
console.log(window.innerWidth);
console.log(window.innerHeight);
init();
animate();

function init() {
  container = document.createElement('div');
  document.body.appendChild(container);
  canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  context = canvas.getContext('2d');
  canvas.width = width;
  canvas.height = height;
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
  camera.position.z = 100;

  // scene
  scene = new THREE.Scene();
  var ambient = new THREE.AmbientLight(0xbbbbbb);
  scene.add(ambient);
  var directionalLight = new THREE.DirectionalLight(0xdddddd);
  directionalLight.position.set(0, 0, 1);
  scene.add(directionalLight);

  // texture
  var manager = new THREE.LoadingManager();
  manager.onProgress = function(item, loaded, total) {
    console.log(item, loaded, total);
  };
  var texture = new THREE.Texture();
  var onProgress = function(xhr) {
    if (xhr.lengthComputable) {
      var percentComplete = xhr.loaded / xhr.total * 100;
      console.log(Math.round(percentComplete, 2) + '% downloaded');
    }
  };
  var onError = function(xhr) {};
  var imgLoader = new THREE.ImageLoader(manager);
  imgLoader.load('https://i.imgur.com/P6158Su.jpg', function(image) {
    texture.image = image;
    texture.needsUpdate = true;
  });

  // model
  var objLoader = new THREE.OBJLoader(manager);
  objLoader.load('https://s3-us-west-2.amazonaws.com/s.cdpn.io/286022/Bulbasaur.obj', function(object) {
    object.traverse(function(child) {
      if (child instanceof THREE.Mesh) {
        child.material.map = texture;
      }
    });
    object.scale.x = 45;
    object.scale.y = 45;
    object.scale.z = 45;
    object.rotation.y = 3;
    object.position.y = -10.5;
    scene.add(object);
  }, onProgress, onError);
  renderer = new THREE.WebGLRenderer({
    alpha: true,
    antialias: false
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(smallWidth, smallHeight);
  container.appendChild(renderer.domElement);
  renderer.context.webkitImageSmoothingEnabled = false;
  renderer.context.mozImageSmoothingEnabled = false;
  renderer.context.imageSmoothingEnabled = false;
  document.addEventListener('mousemove', onDocumentMouseMove, false);
  window.addEventListener('resize', onWindowResize, false);
}

function onWindowResize() {
  windowHalfX = (window.innerWidth / 2) / scale;
  windowHalfY = (window.innerHeight / 2) / scale;
  camera.aspect = (window.innerWidth / window.innerHeight) / scale;
  camera.updateProjectionMatrix();
  renderer.setSize(smallWidth, smallHeight);
}

function onDocumentMouseMove(event) {

  mouseX = (event.clientX - windowHalfX) / scale;
  mouseY = (event.clientY - windowHalfY) / scale;

}

function animate() {
  requestAnimationFrame(animate);
  render();
}

var flag = 0;

function render() {
  camera.position.x += (mouseX - camera.position.x) * .05;
  camera.position.y += (-mouseY - camera.position.y) * .05;
  camera.lookAt(scene.position);
  renderer.render(scene, camera);
  renderer.render(scene, camera, rt);
  renderer.readRenderTargetPixels(rt, 0, 0, smallWidth, smallHeight, frameBuffer);
  //console.time('gpu');
  console.log(frameBuffer, [width], [height], [scale]);
  var outputBufferRaw = pixelateMatrix(frameBuffer, [width], [height], [scale]);
  //console.timeEnd('gpu');
  if (flag < 15) {
    console.log('source', frameBuffer);
    console.log('output', outputBufferRaw);

    var count = 0;
    for (let i = 0; i < frameBuffer.length; i++) {
      if (frameBuffer[i] != 0) {
        count++;
      }
    }
    console.log('source buffer length', frameBuffer.length)
    console.log('source non zero', count);

    var count = 0;
    for (let i = 0; i < outputBufferRaw.length; i++) {
      if (outputBufferRaw[i] != 0) {
        count++;
      }
    }
    console.log('output buffer length', outputBufferRaw.length)
    console.log('output non zero', count);
  }
  outputBuffer = new Uint8ClampedArray(outputBufferRaw);
  output = new ImageData(outputBuffer, width, height);
  context.putImageData(output, 0, 0);
  flag++;
}

Оригинальный ответ

Я закрылся, но осталось два вопроса

  1. Изображение перевернуто
  2. Иногда ваш размер inputBuffer не кратен 4, что приводит к его неправильной работе.

Ниже приведен код, который я использовал

var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) {
   var current_index = Math.floor(this.thread.x/4); 
   var row = Math.floor(current_index / (width[0] * scale[0]) );
   var col = Math.floor((current_index % width[0])/scale[0]);
   var index_old = Math.floor(row * (width[0] / scale[0])) + col;
   var remainder = this.thread.x % 4;
   return inputBuffer[index_old * 4 + remainder];

}).setOutput([width * height * 4]);

Ниже приведен JSFiddle

https://jsfiddle.net/are5Lbw8/

И ниже - текущий выход

Current State

Ответ 3

Я думаю, что функция должна выглядеть так:

var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) {
    var x = Math.floor((this.thread.x / (width[0] * 4)) / scale[0]);
    var y = Math.floor((this.thread.x % (width[0] * 4)) / scale[0]);
    var finalval = y * (Math.floor(width[0]/scale[0]) * 4) + (x * 4);
    var remainder = this.thread.x % 4;
    return inputBuffer[finalval + remainder];
}).setOutput([width * height * 4]);

В основном, получайте x и y аналогично тому, как вы, масштаб x и y, а затем преобразовывая назад (x, y), вы увеличиваете значение y за счет новой масштабированной ширины и добавляете значение x. Не знаете, как вы получили x * y для этой части.