为了好玩而编写 Metaballs,想尝试更多的球、更高的分辨率、不同的颜色托盘等,但目前的性能很糟糕。我在哪里可以改进?也许着色器什么的?我不知道我正在做的事情是否可行,我认为 WebGL 可能有用,但我从来没有搞砸过。
const balls = [];
function setup() {
createCanvas(300, 200);
for (let i = 0; i < 3; i++) balls.push(new Ball(random(width), random(height)))
}
function draw() {
loadPixels();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let sum = 0;
for (const ball of balls) {
let dx = ball.pos.x - x;
let dy = ball.pos.y - y;
let d = Math.hypot(dx, dy);
sum += ball.r / d;
}
set(x, y, color((11 > sum) * 255));
}
}
updatePixels();
for (const ball of balls) ball.update(balls);
}
const BASE_SPEED = 1.5;
class Ball {
constructor(x, y) {
this.pos = {
x: x,
y: y
}
this.angle = random(0, 2 * Math.PI);
this.vel = {
x: BASE_SPEED * cos(this.angle),
y: BASE_SPEED * sin(this.angle)
}
this.r = 150;
}
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
for (const ball of balls) {
if (ball == this) return;
const dx = ball.pos.x - this.pos.x;
const dy = ball.pos.y - this.pos.y;
const d = Math.hypot(dx, dy);
this.vel.x += dx / (d * 10);
this.vel.y += dy / (d * 10);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
这里是将 JavaScript 代码的渲染部分简单翻译成 GLSL。所有球状态和更新代码仍在 JavaScript 中。
着色器通过对每个像素执行一次函数来确定该像素应该是什么颜色来工作。这与您用于呈现的 JavaScript 代码非常相似。但是,当您将其作为着色器实现时,速度会更快,因为其中许多操作是并行发生的,而不是顺序发生的。
我把球的数量增加到13个来展示性能。
let metaballShader;
let balls = [];
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
metaballShader =
createShader(
// vertex program:
`attribute vec3 aPosition;
// Uniforms allow you to pass information from JavaScript to your shader
uniform float width;
uniform float height;
// Varying values pass data from the vertex shader to the fragment shader
// their values will be smoothly interpolated from one vertex to the next
varying highp vec2 vPos;
void main() {
// convert position attribute into screen position (-1, -1) to (1, 1)
gl_Position = vec4(aPosition, 1.0);
// convert position in screen space to position in pixel space
vPos = vec2(
(gl_Position.x + 1.) / 2. * width,
(gl_Position.y + 1.) / 2. * height);
}`,
// fragment shader:
`precision highp float;
#define BALLS 13
uniform float xs[BALLS];
uniform float ys[BALLS];
uniform float rs[BALLS];
varying highp vec2 vPos;
// called once per pixel (equivalent to the body of your for loops over x and y)
void main() {
float sum = 0.;
// calculate the sum value for the current pixel (vPos.x, vPos.y)
for (int i = 0; i < BALLS; i++) {
float dx = xs[i] - vPos.x;
float dy = ys[i] - vPos.y;
float d = length(vec2(dx, dy));
sum += rs[i] / d;
}
// Set the pixel color based on the sum of distances to the balls
if (sum > 11.) {
gl_FragColor = vec4(vec3(0.), 1.);
} else {
gl_FragColor = vec4(vec3(1.), 1.);
}
}`
);
shader(metaballShader);
for (let i = 0; i < 13; i++) {
balls.push(new Ball(random(width), random(height)));
}
metaballShader.setUniform('width', width);
metaballShader.setUniform('height', height);
metaballShader.setUniform('rs', balls.map(b => b.r));
}
function draw() {
metaballShader.setUniform('xs', balls.map(b => b.pos.x));
metaballShader.setUniform('ys', balls.map(b => b.pos.y));
quad(-1, -1, 1, -1, 1, 1, -1, 1);
for (const ball of balls) {
ball.update(balls);
}
}
const BASE_SPEED = 0.8;
class Ball {
constructor(x, y) {
this.pos = {
x: x,
y: y
}
this.angle = random(0, 2 * Math.PI);
this.vel = {
x: BASE_SPEED * cos(this.angle),
y: BASE_SPEED * sin(this.angle)
}
this.r = 100;
}
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
for (const ball of balls) {
if (ball == this) return;
const dx = ball.pos.x - this.pos.x;
const dy = ball.pos.y - this.pos.y;
const d = Math.hypot(dx, dy);
this.vel.x += dx / (d * 20);
this.vel.y += dy / (d * 20);
}
}
}
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
这是用于性能比较的 13 个球的原始代码:
const balls = [];
function setup() {
createCanvas(windowWidth, windowHeight);
for (let i = 0; i < 13; i++) balls.push(new Ball(random(width), random(height)))
}
function draw() {
loadPixels();
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let sum = 0;
for (const ball of balls) {
let dx = ball.pos.x - x;
let dy = ball.pos.y - y;
let d = Math.hypot(dx, dy);
sum += ball.r / d;
}
set(x, y, color((11 > sum) * 255));
}
}
updatePixels();
for (const ball of balls) ball.update(balls);
}
const BASE_SPEED = 1.5;
class Ball {
constructor(x, y) {
this.pos = {
x: x,
y: y
}
this.angle = random(0, 2 * Math.PI);
this.vel = {
x: BASE_SPEED * cos(this.angle),
y: BASE_SPEED * sin(this.angle)
}
this.r = 100;
}
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
for (const ball of balls) {
if (ball == this) return;
const dx = ball.pos.x - this.pos.x;
const dy = ball.pos.y - this.pos.y;
const d = Math.hypot(dx, dy);
this.vel.x += dx / (d * 10);
this.vel.y += dy / (d * 10);
}
}
}
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>