Как я могу правильно написать эту функцию шейдера в JS?
Что я хочу:
Для тестирования стиля игрового искусства, о котором я думал, я хочу отобразить 3D-мир в пиксельной форме. Так, например, возьмите сцену, подобную этой (но визуализированную с определенным цветом/стилем, чтобы хорошо выглядеть после пикселизации):
И заставьте это выглядеть примерно так:
Играя разными способами стилизации 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
).
Что происходит вместо этого:
Это, вместо того, чтобы сделать ближайший соседский высококлассный, делает приятную маленькую радугу (выше - маленький нормальный рендер, ниже - результат шейдера), а справа вы можете увидеть отладочный журнал с статистикой о входных и выходных буферах ):
Что я делаю не так?
Примечание. Я задал связанный с этим вопрос: существует ли более простой (и все еще совершенный) способ масштабирования рендеринга холста с повторной выборкой ближайшего соседа?
Ответы
Ответ 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 года
Я смог решить большинство вопросов. Было немало
-
Логика трансформации была неправильной, и данные по какой-то причине перевернулись, поэтому я щелкнул столбцами и строками, чтобы начать снизу справа
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]);
-
Вы использовали ширину и высоту в поплавках, которые я изменил, чтобы их сначала вычисляли, а затем масштабировали
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);
-
Размер холста был установлен как внутренняя ширина и высота, вам нужно установить только ширину и высоту изображения
context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
Ниже приведен финальный JSFiddle для того же
https://jsfiddle.net/are5Lbw8/6/
Результаты:
Окончательный код для справки
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++;
}
Оригинальный ответ
Я закрылся, но осталось два вопроса
- Изображение перевернуто
- Иногда ваш размер
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/
И ниже - текущий выход
Ответ 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 для этой части.