P5.JS 3D点阵图渲染极慢。

3

我试图将一张普通的2D图像(一个简单的JPEG)转换成用户可以移动的3D点阵图。但是在尝试渲染该点阵图时,程序变得非常缓慢。有人能指出我错在哪里吗?

var x = [];
var y = [];
var z = [];
var colors = [];
var a = 0;
var counter = 0;

let img;

function preload() {
  img = loadImage('https://www.paulwheeler.us/files/clooney.jpeg');
}

function setup() {
  createCanvas(720, 400, WEBGL);
  background(0);

  img.resize(width / 3, height / 2);

  for (let col = 0; col < img.width; col += 3) {
    for (let row = 0; row < img.height; row += 3) {
      let c = img.get(col, row);
      let rgb_val = c[0] + c[1] + c[2]
      colors[a] = c
      x[a] = map(col, 0, 255, -125, 125)
      y[a] = map(row, 0, 255, -125, 125)
      z[a] = map(rgb_val, 0, 765, -50, 0)
      stroke(c)
      push();
      a++
    }
  }

}

function draw() {
  translate(0, 0, -50);
  rotateY(frameCount * 0.1);
  background(0);

  for (var i = 0; i < a; i++) {
    stroke(colors[i])
    push();
    translate(x[i], y[i], z[i]);
    sphere(1);
    pop();
  }

  orbitControl();

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>


3
在p5.js中绘制3D基元,在物体数达到数千个时不易扩展。我认为您可以使用自定义几何图形和UV坐标,但这会相当复杂。不幸的是,WebGL不支持几何着色器,否则可以通过更好的性能实现类似功能。您可以考虑切换到Three.js,我认为它在优化持久的3D几何图形方面更加出色:https://threejsfundamentals.org/threejs/lessons/threejs-optimize-lots-of-objects.html - Paul Wheeler
1
此外,请确保您使用的是支持硬件加速WebGL的网络浏览器。我不得不停止使用FireFox,因为它无法解释地无法在我的MacBook Pro上使用p5.js和WebGL的GPU。 - Paul Wheeler
1个回答

3
如我在上面的评论中提到的那样,p5.js中绘制大量单个基元相对较慢。当从大的顶点缓冲区绘制三角形时,3D图形最优化(基本上预计算三维空间中的点,其法向量用于照明,并且三角形列表通过索引参考该缓冲区中的这些点)。因此,真正的赢家是第二个示例,它生成一些p5.Geometry以得到您的点网格,并使用纹理坐标来实现每个点所需的颜色。
尝试调整球体的细节并在球体的轮廓线和填充方式之间切换(对我的系统影响很小):

var x = [];
var y = [];
var z = [];
var colors = [];
var a = 0;

let img;

let fpsDisplay;
let strokeCheckbox;
let detailSlider;
let lastTime = 0;

function preload() {
  img = loadImage('https://www.paulwheeler.us/files/clooney.jpeg');
}

function setup() {
  createCanvas(720, 400, WEBGL);
  background(0);

  img.resize(width / 3, height / 2);

  for (let col = 0; col < img.width; col += 3) {
    for (let row = 0; row < img.height; row += 3) {
      let c = img.get(col, row);
      let rgb_val = c[0] + c[1] + c[2];
      colors[a] = c;
      x[a] = map(col, 0, img.width, -125, 125);
      y[a] = map(row, 0, img.height, -125, 125);
      z[a] = map(rgb_val, 0, 765, -50, 0);
      a++;
    }
  }

  fpsDisplay = createInput('0');
  fpsDisplay.position(10, 10);
  strokeCheckbox = createCheckbox('Stroke', false);
  strokeCheckbox.position(10, 50);
  strokeCheckbox.style('color', 'red');
  detailSlider = createSlider(1, 24, 24);
  detailSlider.position(10, 90);
}

function draw() {
  // translate(0, 0, -50);
  // rotateY(frameCount * 0.1);
  background(0);
  orbitControl(2, 1, 0.1);

  noStroke();
  noFill();
  let useStroke = strokeCheckbox.checked();
  let detail = detailSlider.value();
  for (var i = 0; i < a; i++) {
    if (useStroke) {
      stroke(colors[i]);
    } else {
      fill(colors[i]);
    }
    push();
    translate(x[i], y[i], z[i]);
    sphere(1, detail, detail);
    pop();
  }
  
  let t = millis();
  fpsDisplay.value(`${1000 / (t - lastTime)}`);
  lastTime = t;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

使用 p5.Geometry、UV 坐标和纹理(这在我的系统上可达到每秒 60 帧):

const dotRadius = 1;
const detail = 4;

let img;

let fpsDisplay;
let lastTime = 0;

let geom;

function preload() {
  img = loadImage('https://www.paulwheeler.us/files/clooney.jpeg');
}

function setup() {
  console.log('Initializing');
  createCanvas(720, 400, WEBGL);
  // Because the spheres are so small, the default stroke makes them all black.
  // You could also use strokeWeight(0.1);
  noStroke();

  img.resize(width / 3, height / 2);

  const dotGrid = function() {
    const sliceCount = this.detailX + 1;
    let dotNumber = 0;
    for (let col = 0; col < img.width; col += 3) {
      for (let row = 0; row < img.height; row += 3) {
        let c = img.get(col, row);
        let rgb_val = c[0] + c[1] + c[2];
        let xOff = map(col, 0, img.width, -125, 125);
        let yOff = map(row, 0, img.height, -125, 125);
        let zOff = map(rgb_val, 0, 765, -50, 0);
        for (let i = 0; i <= this.detailY; i++) {
          const v = i / this.detailY;
          const phi = PI * v - PI / 2;
          const cosPhi = cos(phi);
          const sinPhi = sin(phi);

          for (let j = 0; j <= this.detailX; j++) {
            const u = j / this.detailX;
            const theta = 2 * PI * u;
            const cosTheta = cos(theta);
            const sinTheta = sin(theta);
            const p = createVector(
              xOff + dotRadius * cosPhi * sinTheta,
              yOff + dotRadius * sinPhi,
              zOff + dotRadius * cosPhi * cosTheta
            );
            this.vertices.push(p);
            this.vertexNormals.push(p);
            // All vertices in each dot get the same UV coordinates
            this.uvs.push(map(col, 0, img.width, 0, 1), map(row, 0, img.height, 0, 1));
          }
        }

        // Generate faces for the current dot

        // offset = number of vertices for previous dots.
        let offset = dotNumber * (this.detailX + 1) * (this.detailY + 1);
        let v1, v2, v3, v4;
        for (let i = 0; i < this.detailY; i++) {
          for (let j = 0; j < this.detailX; j++) {
            v1 = i * sliceCount + j + offset;
            v2 = i * sliceCount + j + 1 + offset;
            v3 = (i + 1) * sliceCount + j + 1 + offset;
            v4 = (i + 1) * sliceCount + j + offset;
            this.faces.push([v1, v2, v4]);
            this.faces.push([v4, v2, v3]);
          }
        }

        dotNumber++;
      }
    }
    console.log(`Dots: ${dotNumber}`);

    console.log(`Vertices: ${this.vertices.length}`);
    console.log(`Faces: ${this.faces.length}`);
  };

  geom = new p5.Geometry(detail, detail, dotGrid);
  geom.gid = 'dot-grid';

  fpsDisplay = createInput('0');
  fpsDisplay.position(10, 10);
}

function draw() {
  background(0);
  orbitControl(2, 1, 0.1);

  texture(img);
  model(geom);

  let t = millis();
  fpsDisplay.value(`${1000 / (t - lastTime)}`);
  lastTime = t;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>


1
惊人的回答(+1)!我尝试了一下使用beginShape()/endShape(),似乎足够快,但是在p5.js中,在vertex()之前使用stroke()不像在Processing中那样可靠。 - George Profenza
1
@GeorgeProfenza 是的,我的理解是当使用beginShape/endShape时,p5.js不支持每个顶点的描边或填充设置(我也尝试过)。无论如何,p5.Geometry始终是最快的选项,因为顶点和面会被预先转换并缓存为WebGL兼容缓冲区,尽管显然这并不是最容易编写的内容。我认为有机会创建一种Geometry类型,使您可以实际上使用现有基元和beginShape/endShape来绘制Geometry,然后可以重复使用它(就像使用createGraphics创建可重复使用的2d图形一样)。 - Paul Wheeler
哇,谢谢保罗修复了这个问题,我真的放弃了,因为我认为像这样构建东西是不可能的。我相信甚至乔治·克鲁尼也会印象深刻。 - tcjon
@PaulWheeler,你能解释一下dotGrid函数吗?我有点难以理解。 - tcjon
这是我写的一篇关于p5.Geometry的文章,可能会有所帮助:https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/ - Paul Wheeler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接