javascript - sonido - ¿Cómo puedo escribir correctamente esta función de sombreado en JS?
efecto tipeo css (3)
Lo que quiero que suceda:
Para probar un estilo de arte de juego en el que pensé, quiero renderizar un mundo 3D en forma de pixel art. Por ejemplo, tome una escena como esta (pero renderizada con cierto colorido / estilo para que se vea bien una vez pixelada):
Y haz que se vea algo como esto:
Al jugar con diferentes formas de estilo de la fuente 3D, creo que la salida pixelada podría verse bien. Por supuesto, para obtener este efecto, solo se reduce el tamaño de la imagen a ~ 80p y se eleva a 1080p con el remuestreo del vecino más cercano. Pero, para comenzar, es más eficiente renderizar directamente a un lienzo de 80p y simplemente hacer el escalado.
Por lo general, no es así como se usaría un sombreado para cambiar el tamaño de un mapa de bits en el formato vecino más cercano, pero el rendimiento en él es mejor que cualquier otra forma que he encontrado para realizar dicha conversión en tiempo real.
Mi código:
Mi búfer para el mapa de bits se almacena en la fila principal, como r1, g1, b1, a1, r2, g2, b2, a2...
y estoy usando gpu.js que esencialmente convierte esta función JS en un sombreado. Mi objetivo es tomar un mapa de bits y devolver uno a una escala mayor con la escala del vecino más cercano, para que cada píxel se convierta en un cuadrado de 2x2 o 3x3 y así sucesivamente. Suponga que inputBuffer
es una fracción escalada del tamaño de la salida determinada por el método 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]);
Tenga en cuenta que está iterando sobre un nuevo búfer de la salida de tamaño completo, así que tengo que encontrar las coordenadas correctas que existirán en el
sourceBuffer
mássourceBuffer
basado en el índice actual en eloutputBuffer
(el índice está expuesto por la lib comothis.thread.x
).
Lo que está sucediendo en su lugar:
Esto, en lugar de hacer una mejora exclusiva del vecino más cercano, es hacer un pequeño arco iris agradable (arriba está el renderizado normal pequeño, a continuación se muestra el resultado del sombreado y, a la derecha, puede ver algunos registros de depuración con estadísticas sobre los buffers de entrada y salida ):
¿Qué estoy haciendo mal?
Nota: Hice una pregunta relacionada aquí: ¿Existe una forma más sencilla (y aún más eficaz) de ampliar el renderizado de un lienzo con el remuestreo del vecino más cercano?
Respuesta final
La respuesta de Tarun me ayudó a llegar a mi solución final, por lo que su recompensa fue bien merecida, pero en realidad aprendí sobre una característica (salida gráfica emparejada con intercambio de contexto para salida de búfer directa al objetivo de procesamiento) de gpu.js que permite aproximadamente 30 veces más rápido renderización, lo que da el tiempo total para sombrear y representar la salida de 30ms + a ~ 1ms, y esto es sin una optimización adicional. Ahora sé que es posible enviar el buffer de matriz a la GPU aún más rápido, pero no tenía ninguna motivación para hacerlo. bajar el tiempo de sombreado / renderizado a menos de 1 ms.
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
es simplemente el búfer recuperado a través del método readRenderTargetPixels de readRenderTargetPixels
.
renderer.render(scene, camera, rt);
renderer.readRenderTargetPixels(rt, 0, 0, smallWidth, smallHeight, frameBuffer);
pixelateMatrix(frameBuffer, [smallWidth], [scale], [size], [purity], [div]);
Nota al margen
¿Podemos simplemente maravillarnos por un momento acerca de cuánta potencia WebGL aporta al navegador? Eso es 8.2944 millones de tareas de operación múltiple realizadas en solo ~ 1 ms. ~ 64 billones de operaciones matemáticas máximas por segundo para mi shader por mi cuenta. Eso es una locura. ¿Puede eso ser correcto? ¿Es mi matemática incorrecta en eso? Veo que la inteligencia automática de nvidia AI está realizando 24 billones de operaciones , por lo que supongo que estos números en mi 1060 están dentro del ámbito de lo posible. Aunque es simplemente increíble.
GPU.js hace un trabajo fantástico al optimizar las operaciones de matriz para ejecutarse en la GPU sin la necesidad de aprender el código de sombreado, y el creador es extremadamente activo en el proyecto, respondiendo a los problemas generalmente en cuestión de horas. Recomiendo encarecidamente que prueben el lib. Especialmente impresionante para el rendimiento de aprendizaje automático.
Creo que la función debería verse como:
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]);
Básicamente, obtenga x e y de la misma manera que usted, escale x e y, luego vuelva a convertir desde (x, y) multiplicando el valor y por el nuevo ancho escalado y agregue el valor x. No estoy seguro de cómo obtuviste x * y para esa parte.
Actualización 1 - 25 de mayo de 2018
Pude resolver la mayoría de los problemas. Habían unos cuantos
La lógica de la transformación estaba equivocada, también los datos venían volcados por alguna razón, así que di la vuelta de columnas y filas para comenzar desde la parte inferior derecha.
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]);
Estabas usando el ancho y la altura en flotadores, los cuales he cambiado para ser calculados primero y luego escalados
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);
El tamaño del lienzo se estableció en ancho y alto internos, debe configurarlo solo en el ancho y alto de la imagen
context = canvas.getContext(''2d''); canvas.width = width; canvas.height = height;
A continuación se muestra el JSFiddle final para el mismo
https://jsfiddle.net/are5Lbw8/6/
Resultados:
Código final para referencia
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++;
}
Respuesta original
Me he acercado pero quedan dos cuestiones
- La imagen se está invirtiendo.
- A veces su tamaño de
inputBuffer
no es un múltiplo de 4, lo que hace que seinputBuffer
mal.
A continuación se muestra el código que usé
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]);
A continuación se muestra el JSFiddle
https://jsfiddle.net/are5Lbw8/
Y abajo está la salida de corriente.