响应式 canvas 使用 webgl 和 meatballs.js

Responsive canvas using webgl and meatballs.js

我正在尝试将 this codepen 作为我个人网站的背景。我对 WebGL 没有真正的了解,所以请耐心等待。我临时添加了一个事件侦听器以在调整页面大小时更新 canvas 的宽度和高度。我可以说这是有效的,因为当气泡开始越界时,它们会继续前进并且不会从页面边缘反弹,所以我知道它有点像我想要的那样工作。定义片段着色器源时,它还定义了宽度和高度,我不确定之后如何更改这些变量。我尝试使用新的宽度和高度重新定义、重新编译和重新附加片段着色器源代码。这显然是行不通的,因为在创建 canvas 时,气泡不会呈现超过页面大小。我不确定我是否以正确的方式进行此操作,如果是这样,我做错了什么? All/any感谢帮助,谢谢。

我修改的代码:

var canvas = document.createElement("canvas");
var width = canvas.width = window.innerWidth * 0.75;
var height = canvas.height = window.innerHeight * 0.75;
document.body.appendChild(canvas);
var gl = canvas.getContext('webgl');

var mouse = {x: 0, y: 0};

var numMetaballs = 30;
var metaballs = [];

var first = true

window.addEventListener('resize', function(){
    width = canvas.width = window.innerWidth * 0.75;
    height = canvas.height = window.innerHeight * 0.75;
    shaderStuff()
})

function shaderStuff(){
    if(!first) {
        gl.detachShader(program, gl.getAttachedShaders(program)[1])
    }
    first = false
    
    var fragmentShaderSrc = `
    precision highp float;

    const float WIDTH = ` + (width >> 0) + `.0;
    const float HEIGHT = ` + (height >> 0) + `.0;

    uniform vec3 metaballs[` + numMetaballs + `];

    void main(){
    float x = gl_FragCoord.x;
    float y = gl_FragCoord.y;

    float sum = 0.0;
    for (int i = 0; i < ` + numMetaballs + `; i++) {
    vec3 metaball = metaballs[i];
    float dx = metaball.x - x;
    float dy = metaball.y - y;
    float radius = metaball.z;

    sum += (radius * radius) / (dx * dx + dy * dy);
    }

    if (sum >= 0.99) {
    gl_FragColor = vec4(mix(vec3(x / WIDTH, y / HEIGHT, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0);
    return;
    }

    gl_FragColor = vec4(0, 0, 0, 0);
    }

    `;
    
    var fragmentShader = compileShader(fragmentShaderSrc, gl.FRAGMENT_SHADER);
    gl.attachShader(program, fragmentShader);
}

for (var i = 0; i < numMetaballs; i++) {
  var radius = Math.random() * 60 + 10;
  metaballs.push({
    x: Math.random() * (width - 2 * radius) + radius,
    y: Math.random() * (height - 2 * radius) + radius,
    vx: (Math.random() - 0.5) * 3,
    vy: (Math.random() - 0.5) * 3,
    r: radius * 0.75
  });
}

var vertexShaderSrc = `
attribute vec2 position;

void main() {
// position specifies only x and y.
// We set z to be 0.0, and w to be 1.0
gl_Position = vec4(position, 0.0, 1.0);
}
`;



var vertexShader = compileShader(vertexShaderSrc, gl.VERTEX_SHADER);


var program = gl.createProgram();
gl.attachShader(program, vertexShader);
shaderStuff()
gl.linkProgram(program);
gl.useProgram(program);

整个项目https://meatballsjs.000webhostapp.com/

https://codepen.io/TC5550/pen/WNNWoaO

最简单的方法是将所有后台创建代码放在一个函数中,并在每次调整页面大小时调用它。

您还需要添加一些代码来停止之前的后台循环,并且您应该添加一些节流以防止一次创建太多后台。

这有点低效,但大多数用户不希望应用程序在调整大小时具有极高的响应能力,而且调整大小是一种不常见的操作。

我添加了一个代码片段,它似乎可以工作,但是我无法让我的更改在 codepen 中工作。我相信这是因为 codepen 以某种破坏代码的方式检测和修改代码(jsbin 具有类似的行为来防止无限循环,并将其沙盒化)。然而,我只在一个 .html 文件中测试了我的更改,它们似乎在那里工作,所以它们应该在您的网站上工作。

附带一提,WebGL 的使用非常酷!

var nextBackgroundId = 1;
var currentBackgroundId = 0;

setupBackground(currentBackgroundId);
window.addEventListener("resize", () => {
    var ourBackgroundId = nextBackgroundId++;
    currentBackgroundId = ourBackgroundId;
    setTimeout(() => {
        setupBackground(ourBackgroundId);
    }, 100);
});

function setupBackground(ourBackgroundId) {
    if (currentBackgroundId !== ourBackgroundId) {
        return;
    }

    var prevCanvas = document.getElementById("blob-canvas");
    if (prevCanvas) {
        prevCanvas.remove();
    }

    var canvas = document.createElement("canvas");
    canvas.id = "blob-canvas";
    var mouse = { x: 0, y: 0 };

    canvas.onmousemove = function (e) {
        mouse.x = e.clientX;
        mouse.y = e.clientY;
    }

    var width = canvas.width = window.innerWidth;
    var height = canvas.height = window.innerHeight;
    document.body.appendChild(canvas);
    var gl = canvas.getContext('webgl');

    var numMetaballs = 30;
    var metaballs = [];

    for (var i = 0; i < numMetaballs; i++) {
        var radius = Math.random() * 60 + 10;
        metaballs.push({
            x: Math.random() * (width - 2 * radius) + radius,
            y: Math.random() * (height - 2 * radius) + radius,
            vx: (Math.random() - 0.5) * 3,
            vy: (Math.random() - 0.5) * 3,
            r: radius * 0.75
        });
    }

    var vertexShaderSrc = `
attribute vec2 position;

void main() {
// position specifies only x and y.
// We set z to be 0.0, and w to be 1.0
gl_Position = vec4(position, 0.0, 1.0);
}
`;

    var fragmentShaderSrc = `
precision highp float;

const float WIDTH = ` + (width >> 0) + `.0;
const float HEIGHT = ` + (height >> 0) + `.0;

uniform vec3 metaballs[` + numMetaballs + `];

void main(){
float x = gl_FragCoord.x;
float y = gl_FragCoord.y;

float sum = 0.0;
for (int i = 0; i < ` + numMetaballs + `; i++) {
vec3 metaball = metaballs[i];
float dx = metaball.x - x;
float dy = metaball.y - y;
float radius = metaball.z;

sum += (radius * radius) / (dx * dx + dy * dy);
}

if (sum >= 0.99) {
gl_FragColor = vec4(mix(vec3(x / WIDTH, y / HEIGHT, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0);
return;
}

gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}

`;

    var vertexShader = compileShader(vertexShaderSrc, gl.VERTEX_SHADER);
    var fragmentShader = compileShader(fragmentShaderSrc, gl.FRAGMENT_SHADER);

    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    var vertexData = new Float32Array([
        -1.0, 1.0, // top left
        -1.0, -1.0, // bottom left
        1.0, 1.0, // top right
        1.0, -1.0, // bottom right
    ]);
    var vertexDataBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexDataBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);

    var positionHandle = getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(positionHandle);
    gl.vertexAttribPointer(positionHandle,
        2, // position is a vec2
        gl.FLOAT, // each component is a float
        gl.FALSE, // don't normalize values
        2 * 4, // two 4 byte float components per vertex
        0 // offset into each span of vertex data
    );

    var metaballsHandle = getUniformLocation(program, 'metaballs');

    loop();
    function loop() {
        if (currentBackgroundId !== ourBackgroundId) {
            return;
        }
        for (var i = 0; i < numMetaballs; i++) {
            var metaball = metaballs[i];
            metaball.x += metaball.vx;
            metaball.y += metaball.vy;

            if (metaball.x < metaball.r || metaball.x > width - metaball.r) metaball.vx *= -1;
            if (metaball.y < metaball.r || metaball.y > height - metaball.r) metaball.vy *= -1;
        }

        var dataToSendToGPU = new Float32Array(3 * numMetaballs);
        for (var i = 0; i < numMetaballs; i++) {
            var baseIndex = 3 * i;
            var mb = metaballs[i];
            dataToSendToGPU[baseIndex + 0] = mb.x;
            dataToSendToGPU[baseIndex + 1] = mb.y;
            dataToSendToGPU[baseIndex + 2] = mb.r;
        }
        gl.uniform3fv(metaballsHandle, dataToSendToGPU);

        //Draw
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        requestAnimationFrame(loop);
    }

    function compileShader(shaderSource, shaderType) {
        var shader = gl.createShader(shaderType);
        gl.shaderSource(shader, shaderSource);
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            throw "Shader compile failed with: " + gl.getShaderInfoLog(shader);
        }

        return shader;
    }

    function getUniformLocation(program, name) {
        var uniformLocation = gl.getUniformLocation(program, name);
        if (uniformLocation === -1) {
            throw 'Can not find uniform ' + name + '.';
        }
        return uniformLocation;
    }

    function getAttribLocation(program, name) {
        var attributeLocation = gl.getAttribLocation(program, name);
        if (attributeLocation === -1) {
            throw 'Can not find attribute ' + name + '.';
        }
        return attributeLocation;
    }
}
body {
    font-family: 'Alatsi', sans-serif;
    margin: 0;
    overflow: hidden;
    background: black;
}

.container {
    display: flex;
    justify-content: center;
    align-items: center;

    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
}

.title {
    font-size: 10vw;
    color: white;
}

canvas {
    width: 100%;
}
<div class="container">
    <span class="title">MEATBALLS</span>
</div>

那个 codepen 有很多问题。

  1. 它正在修改 canvas 大小,而不是让 CSS 调整 canvas 大小。

    在代码中 canvas 大小设置为

    var width = canvas.width = window.innerWidth * 0.75;
    var height = canvas.height = window.innerHeight * 0.75;
    

    最好让浏览器调整 canvas

    html, body {
      height: 100%;
      overflow: hidden;
    }     
    canvas {
      width: 100%;
      height: 100%;
    }
    

    然后询问浏览器 canvas 的大小并设置 canvas 的分辨率以匹配

    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
    
  2. 它要求的东西比 window 大,所以它会得到一个滚动条,然后通过隐藏滚动条来隐藏这个事实。这没有道理。如果您不需要滚动条,请不要要求需要滚动条的内容。

    html, body {
      height: 100%;
      /* removed overflow: hidden */
    }     
    canvas {
      width: 100%;
      height: 100%;
      display: block;
    }
    
  3. 它使用的是模板字符串,但实际上并没有将它们用作模板

    var fragmentShaderSrc = `
    precision highp float;
    
    const float WIDTH = ` + (width >> 0) + `.0;
    const float HEIGHT = ` + (height >> 0) + `.0;
    
    uniform vec3 metaballs[` + numMetaballs + `];
    ...
    `;
    

    可以说是

    var fragmentShaderSrc = `
    precision highp float;
    
    const float WIDTH = ${width >> 0}.0;
    const float HEIGHT = ${height >> 0}.0;
    
    uniform vec3 metaballs[${numMetaballs}];
    ...
    `;
    

    对字符串使用反引号的要点是这样你就可以 使用模板功能 ${code}

  4. 硬编码宽度和高度

    const float WIDTH = ${width >> 0}.0;
    const float HEIGHT = ${height >> 0}.0;
    

    可以说是

    uniform float WIDTH;
    uniform float HEIGHT;
    

    所以可以设置它们

  5. Metaballs 被错误拼写为 Meatballs(可能是故意的)

这是一个新版本。注意:只要调整 window 的大小,元球就会获得随机位置。如果您在调整 canvas 大小后注释掉对 updateMetaballs 的调用,那么它们将不会获得新的随机位置。哪个更好取决于你。它们弹跳的逻辑是这样的,即在您调整大小后离开屏幕的任何球都将留在屏幕之外。您可以修复它,使它们朝向屏幕,并且只会从内部反弹回来。当前的代码是这样的,在外部它们只会在它们所在的位置摆动。

var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var gl = canvas.getContext('webgl');

var mouse = {x: 0, y: 0};

var numMetaballs = 30;
var metaballs = [];

function updateMetaballs() {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  for (var i = 0; i < numMetaballs; i++) {
    var radius = Math.random() * 60 + 10;
    metaballs[i] = {
      x: Math.random() * (width - 2 * radius) + radius,
      y: Math.random() * (height - 2 * radius) + radius,
      vx: (Math.random() - 0.5) * 3,
      vy: (Math.random() - 0.5) * 3,
      r: radius * 0.75
    };
  }
}
updateMetaballs();

var vertexShaderSrc = `
attribute vec2 position;

void main() {
  // position specifies only x and y.
  // We set z to be 0.0, and w to be 1.0
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

var fragmentShaderSrc = `
precision highp float;

uniform float WIDTH;
uniform float HEIGHT;

#define NUM_METABALLS ${numMetaballs}
uniform vec3 metaballs[NUM_METABALLS];

void main(){
  float x = gl_FragCoord.x;
  float y = gl_FragCoord.y;

  float sum = 0.0;
  for (int i = 0; i < NUM_METABALLS; i++) {
    vec3 metaball = metaballs[i];
    float dx = metaball.x - x;
    float dy = metaball.y - y;
    float radius = metaball.z;

    sum += (radius * radius) / (dx * dx + dy * dy);
  }

  if (sum >= 0.99) {
    gl_FragColor = vec4(mix(vec3(x / WIDTH, y / HEIGHT, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0);
    return;
  }

  gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}

`;

var vertexShader = compileShader(vertexShaderSrc, gl.VERTEX_SHADER);
var fragmentShader = compileShader(fragmentShaderSrc, gl.FRAGMENT_SHADER);

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

var vertexData = new Float32Array([
  -1.0,  1.0, // top left
  -1.0, -1.0, // bottom left
  1.0,  1.0, // top right
  1.0, -1.0, // bottom right
]);
var vertexDataBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexDataBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);

var positionHandle = getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionHandle);
gl.vertexAttribPointer(positionHandle,
                       2, // position is a vec2
                       gl.FLOAT, // each component is a float
                       gl.FALSE, // don't normalize values
                       2 * 4, // two 4 byte float components per vertex
                       0 // offset into each span of vertex data
                      );

var metaballsHandle = getUniformLocation(program, 'metaballs');
var widthHandle = getUniformLocation(program, 'WIDTH');
var heightHandle = getUniformLocation(program, 'HEIGHT');

function resizeCanvasToDisplaySize(canvas) {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    canvas.width = width;
    canvas.height = height;
  }
  return needResize;
}

loop();
function loop() {
  if (resizeCanvasToDisplaySize(canvas)) {
    updateMetaballs();
  }
  const {width, height} = canvas;
  gl.viewport(0, 0, canvas.width, canvas.height);
  
  for (var i = 0; i < numMetaballs; i++) {
    var metaball = metaballs[i];
    metaball.x += metaball.vx;
    metaball.y += metaball.vy;

    if (metaball.x < metaball.r || metaball.x > width - metaball.r) metaball.vx *= -1;
    if (metaball.y < metaball.r || metaball.y > height - metaball.r) metaball.vy *= -1;
  }

  var dataToSendToGPU = new Float32Array(3 * numMetaballs);
  for (var i = 0; i < numMetaballs; i++) {
    var baseIndex = 3 * i;
    var mb = metaballs[i];
    dataToSendToGPU[baseIndex + 0] = mb.x;
    dataToSendToGPU[baseIndex + 1] = mb.y;
    dataToSendToGPU[baseIndex + 2] = mb.r;
  }
  gl.uniform3fv(metaballsHandle, dataToSendToGPU);
  gl.uniform1f(widthHandle, canvas.clientWidth);
  gl.uniform1f(heightHandle, canvas.clientHeight);
  
  //Draw
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  requestAnimationFrame(loop);
}

function compileShader(shaderSource, shaderType) {
  var shader = gl.createShader(shaderType);
  gl.shaderSource(shader, shaderSource);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    throw "Shader compile failed with: " + gl.getShaderInfoLog(shader);
  }

  return shader;
}

function getUniformLocation(program, name) {
  var uniformLocation = gl.getUniformLocation(program, name);
  if (uniformLocation === -1) {
    throw 'Can not find uniform ' + name + '.';
  }
  return uniformLocation;
}

function getAttribLocation(program, name) {
  var attributeLocation = gl.getAttribLocation(program, name);
  if (attributeLocation === -1) {
    throw 'Can not find attribute ' + name + '.';
  }
  return attributeLocation;
}

canvas.onmousemove = function(e) {
  mouse.x = e.clientX;
  mouse.y = e.clientY;
}
html, body {
  font-family: 'Alatsi', sans-serif;
  margin: 0;
  background: black;
  height: 100%;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.title {
  font-size: 10vw;
  color: white;
}

canvas {
  width: 100%;
  height: 100%;
  display: block;
}
<div class="container">
    <span class="title">METABALLS</span>
</div>

如果您想学习 WebGL,请考虑 these tutorials