【问题标题】:How can I properly write this shader function in JS?如何在 JS 中正确编写此着色器函数?
【发布时间】:2018-05-20 00:11:05
【问题描述】:

我想要发生的事情:

为了测试我想到的游戏艺术风格,我想以像素艺术的形式渲染一个 3D 世界。因此,例如,采用这样的场景(但使用特定的颜色/样式进行渲染,以便像素化后看起来不错):

让它看起来像这样:

通过使用不同的方式设置 3D 源的样式,我认为像素化输出看起来不错。当然,要获得这种效果,只需将图像缩小到 ~80p,然后通过最近邻重采样将其放大到 1080p。但是,直接渲染到 80p 画布并进行升级会更有效。

这通常不是使用着色器以最近邻格式调整位图大小的方式,但它的性能比我发现的任何其他实时进行此类转换的方式都要好。

我的代码:

我的位图缓冲区存储在主行中,如r1, g1, b1, a1, r2, g2, b2, a2...,我正在使用gpu.js,它实质上将此JS func 转换为着色器。我的目标是获取一个位图并以最近邻缩放的更大比例返回一个,因此每个像素变成 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(索引由库公开为this.thread.x)。

发生了什么事:

这不是让最近的邻居高档,而是在做一个漂亮的小彩虹(上面是小的正常渲染,下面是着色器的结果,在右边你可以看到一些带有关于输入的统计信息的调试日志和输出缓冲区):

我做错了什么?

注意:我在这里问了一个相关的问题,Is there a simpler (and still performant) way to upscale a canvas render with nearest neighbor resampling?

【问题讨论】:

  • 我认为您的想法是正确的,可能最简单的方法是使用您想要的分辨率绘制到屏幕外的画布上。然后使用 webGL 图像过滤将其放大到您的显示分辨率。
  • @MrSmith 是的,这就是我正在做的,但我的算法是错误的,这就是问题所在。我已经对其进行了审查,但它很难调试,因为您不能在内核函数中使用控制台日志,而且我似乎无法弄清楚如何修复它。
  • 这可能不是最好的方法,我已经在你的其他问题上留下了答案。
  • @MrSmith 会尽快审核,谢谢
  • Np,请询问是否有任何混淆。可能尚不清楚如何将其与现有渲染器一起使用。

标签: javascript algorithm


【解决方案1】:

2018 年 5 月 1 日至 25 日更新

我能够解决大部分问题。有不少

  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/

结果:

最终代码供参考

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/

下面是当前的输出

【讨论】:

  • 是的,@david 也是如此,这是在我修改了大约 2 个小时之后:jsfiddle.net/31tw9nh3/10
  • 我注意到像这样添加一个特定值(取决于屏幕宽度)可以修复倾斜,但我无法弄清楚如何在所有屏幕尺寸上一致地修复它,或者为什么它甚至可以工作:var inWidth = (width[0] / scale[0]) - 0.8;
  • 另外,我无法重现您的说法,即 inputBuffer 大小并不总是 4 的倍数。我一直在寻找 inputBuffer 的任何奇怪问题,但一切看起来都不错。输入缓冲区只是从three.js 的renderer.readRenderTargetPixels 方法中获取的。
  • @johndoe,请加入这个chat房间
【解决方案2】:

我认为函数应该是这样的:

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 的。

【讨论】:

  • 是的,它渲染了bulbasaur,jsfiddle.net/31tw9nh3/2,但在它的一边。所以有些事情还没有结束
  • 那么只要翻转 x 和 y 就可以了,如果它在一边的话。
  • 如此接近。翻转 x 和 y 会奇怪地在横向和上下颠倒之间切换。我会继续使用它。顺便说一句,这里有一个小提琴
  • 除了倒置或侧向(取决于翻转 x 和 y)之外,它还在倒置时渲染模型的 4 个副本,并且在两种配置中,模型的垂直拉伸为 100% .我在角落渲染了一个正方形,只是为了验证它是被拉伸的输出,而不是画布。
  • @david 尝试在聊天室中修复它,他也有一些有趣的问题,我一直在修补它试图让它工作但还没有运气:jsfiddle.net/31tw9nh3/1
【解决方案3】:

最终答案

Tarun 的回答帮助我找到了最终解决方案,所以他的赏金是当之无愧的,但我实际上了解了 gpu.js 的一个功能(图形输出与上下文共享配对,用于直接将缓冲区输出到渲染目标),它允许渲染速度大约提高了 30 倍,使着色 渲染输出的总时间从 30 毫秒以上缩短到了约 1 毫秒,而且这没有额外的优化,我现在知道甚至可以将数组缓冲区发送到 GPU更快,但我只是没有任何动力将着色/渲染时间降低到 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 只是通过three.js 的readRenderTargetPixels 方法检索到的缓冲区。

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

旁注

我们能否惊叹一下 WebGL 为浏览器带来的强大功能?即 829.44 万个多操作任务在大约 1 毫秒内完成。根据我的统计,我的着色器每秒的最大数学运算总数约为 640 亿。那是精神错乱。这可能是对的吗?我的数学错了吗? I see nvidia's self driving AI is performing 24 trillion ops/s,所以我想我的 1060 上的这些数字在可能的范围内。真是不可思议。

GPU.js 在优化矩阵运算以在 GPU 上运行而无需学习着色器代码方面做得非常出色,而且创建者在项目中非常活跃,通常在几个小时内就可以响应问题。强烈建议你们试试这个库。机器学习吞吐量特别棒。

【讨论】:

  • 感谢分享更新 :-),如果您也可以添加更新的 JSFiddle 那就太好了
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-16
  • 2017-11-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多