如何保持粒子之间的协调以及哪个纹理像素包含每个粒子的信息?

How to keep coordination between particles and which texture pixel contains each one’s information?

以 4x4x4 网格为例,我有 64 个顶点(我称之为粒子),它们以相对于彼此的特定位置开始。这 64 个粒子将在 x、y 和 z 方向上移动,失去它们相对于彼此的初始位置。然而每个循环,新的粒子位置和速度都需要根据粒子与其原始邻居之间的原始起始关系来计算。

我了解到我需要使用纹理,因此需要为此使用帧缓冲区,现在我可以编写两个 3DTextures 触发器来提供写入和读取功能来执行此操作。然而,在下一个循环中,当 gl_FragCoord 被传递给片段着色器时,带有一个粒子的新位置(例如可以与另一个粒子切换),我没有看到任何机制可以使纹理的原始坐标保存粒子信息的文件将写入粒子的当前信息。是否有某种我不理解的机制允许移动粒子将它们的数据存储在静态网格(3D 纹理)中,每个粒子的数据总是填充相同的坐标,所以我可以使用 texelFetch 来获取粒子的数据,以及原始邻居的数据?我可以更改 gl_FragCoord,并在我想要的位置输出像素,还是一个不可更改的输入变量?

一旦我解决了这个问题,我希望随后实现一个变换反馈来执行顶点的实际移动,而不会将纹理转储到 CPU 并提取位置数据并将其重新上传到下一个周期的 GPU。

对于如何使用帧缓冲区中编写的纹理跟踪每个粒子的原始位置、原始邻居以及相对于那些原始邻居的当前位置,是否有任何建议?

我对你的困惑感到困惑

这是一个简单的 JavaScript 只有粒子系统。每个粒子从一个随机位置开始并沿随机方向移动

'use strict';

const ctx = document.querySelector('canvas').getContext('2d')
const {width, height} = ctx.canvas;

const numParticles = 128;
const particleParameters = [];  // info that does not change
let currentParticleState = [];  // info that does change
let nextParticleState = [];     // computed from currentState

for (let i = 0; i < numParticles; ++i) {
  particleParameters.push({
    velocity: [rand(-100, 100), rand(-100, 100)],
  });
  currentParticleState.push({
    position: [rand(0, width), rand(0, height)],
  });
  nextParticleState.push({
    position: [0, 0],
  });
}


function rand(min, max) {
  return Math.random() * (max - min) + min;
}

function euclideanModulo(n, m) {
  return (( n % m) + m) % m;
}

let then = 0;
function render(now) {
  now *= 0.001;  // convert to seconds
  const deltaTime = now - then;
  then = now;

  for (let i = 0; i < numParticles; ++i) {
    const curPos = currentParticleState[i].position;
    const nxtPos = nextParticleState[i].position;
    const data = particleParameters[i];
    
    nxtPos[0] = euclideanModulo(curPos[0] + data.velocity[0] * deltaTime, width);
    nxtPos[1] = euclideanModulo(curPos[1] + data.velocity[1] * deltaTime, height);    
  }
  
  const t = nextParticleState;
  nextParticleState = currentParticleState;
  currentParticleState = t;

  ctx.clearRect(0, 0, width, height);
  for (let i = 0; i < numParticles; ++i) {
    const [x, y] = currentParticleState[i].position;
    ctx.fillRect(x - 1, y - 1, 3, 3);
  }
  

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<canvas></canvas>

这里的相同粒子系统仍在 JavaScript 中,但 运行 更像是 WebGL 运行。我不知道这是否会或多或少令人困惑。重要的一点是更新名为 fragmentShader 的粒子位置的代码无法选择要更新的内容。它只是更新 gl.outColor。它也没有输入,除了 gl.fragCoordgl.currentProgram.uniforms。 currentParticleState 是一个包含 4 个值数组的数组,而之前它是一个包含位置 属性 的对象数组。 particleParameters 也只是 4 个值数组的数组,而不是具有速度值的对象数组。这是为了模拟这样一个事实,即这些在真实的 WebGL 中是纹理,因此 positionvelocity 之类的任何含义都会丢失。

实际绘制粒子的代码是无关紧要的。

'use strict';

const ctx = document.querySelector('canvas').getContext('2d')
const {width, height} = ctx.canvas;

const numParticles = 128;
const particleParameters = [];  // info that does not change
let currentParticleState = [];  // info that does change
let nextParticleState = [];     // computed from currentState

for (let i = 0; i < numParticles; ++i) {
  particleParameters.push(
    [rand(-100, 100), rand(-100, 100)],
  );
  currentParticleState.push(
    [rand(0, width), rand(0, height)],
  );
  nextParticleState.push(
    [0, 0],
  );
}


function rand(min, max) {
  return Math.random() * (max - min) + min;
}

function euclideanModulo(n, m) {
  return (( n % m) + m) % m;
}


const gl = {
  fragCoord: [0, 0, 0, 0],
  outColor: [0, 0, 0, 0],
  currentProgram: null,
  currentFramebuffer: null,
  
  bindFramebuffer(fb) {
    this.currentFramebuffer = fb;
  },
  
  createProgram(vs, fs) {
    return {
      vertexShader: vs,  // not using
      fragmentShader: fs,
      uniforms: {
      },
    }
  },
  
  useProgram(p) {
    this.currentProgram = p;
  },
  
  uniform(name, value) {
    this.currentProgram.uniforms[name] = value;
  },
  
  draw(count) {
    for (let i = 0; i < count; ++i) {
      this.fragCoord[0] = i + .5;
      this.currentProgram.fragmentShader();
      this.currentFramebuffer[i][0] = this.outColor[0];
      this.currentFramebuffer[i][1] = this.outColor[1];
      this.currentFramebuffer[i][2] = this.outColor[2];
      this.currentFramebuffer[i][3] = this.outColor[3];
    }
  },
};


// just to make it look more like GLSL
function texelFetch(sampler, index) {
  return sampler[index];
}

// notice this function has no inputs except
// `gl.fragCoord` and `gl.currentProgram.uniforms`
// and it just writes to `gl.outColor`. It doesn't
// get to choose where to write. That is handled
// by `gl.draw`
function fragmentShader() {
  // to make the code below more readable
  const {
    resolution, 
    deltaTime,
    currentState,
    particleParams,
  } = gl.currentProgram.uniforms;
  
  const i = Math.floor(gl.fragCoord[0]);
  const curPos = texelFetch(currentState, i);
  const data = texelFetch(particleParameters, i);
    
  gl.outColor[0] = euclideanModulo(curPos[0] + data[0] * deltaTime, resolution[0]);
  gl.outColor[1] = euclideanModulo(curPos[1] + data[1] * deltaTime, resolution[1]);
}


const prg = gl.createProgram(null, fragmentShader);

let then = 0;
function render(now) {
  now *= 0.001;  // convert to seconds
  const deltaTime = now - then;
  then = now;

  gl.bindFramebuffer(nextParticleState);
  gl.useProgram(prg);
  gl.uniform('deltaTime', deltaTime);
  gl.uniform('currentState', currentParticleState);
  gl.uniform('particleParameters', particleParameters);
  gl.uniform('resolution', [width, height]);
  gl.draw(numParticles);
  
  const t = nextParticleState;
  nextParticleState = currentParticleState;
  currentParticleState = t;

  // not relavant!!!
  ctx.clearRect(0, 0, width, height);
  for (let i = 0; i < numParticles; ++i) {
    const [x, y] = currentParticleState[i];
    ctx.fillRect(x - 1, y - 1, 3, 3);
  }
  
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; }
<canvas></canvas>

这是实际 WebGL 中的相同代码

'use strict';

function main() {
  const gl = document.querySelector('canvas').getContext('webgl2')
  if (!gl) {
    return alert('sorry, need webgl2');
  }
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    return alert('sorry, need EXT_color_buffer_float');
  }
  
  const {width, height} = gl.canvas;

  const numParticles = 128;
  const particleParameters = [];  // info that does not change
  let currentParticleState = [];  // info that does change
  let nextParticleState = [];     // computed from currentState

  for (let i = 0; i < numParticles; ++i) {
    particleParameters.push(rand(-100, 100), rand(-100, 100), 0, 0);
    currentParticleState.push(rand(0, width), rand(0, height), 0, 0);
  }


  function rand(min, max) {
    return Math.random() * (max - min) + min;
  }

  const particleParamsTex = twgl.createTexture(gl, {
    src: new Float32Array(particleParameters),
    internalFormat: gl.RGBA32F,
    width: numParticles,
    height: 1,
    minMax: gl.NEAREST,
  });
  const currentStateTex = twgl.createTexture(gl, {
    src: new Float32Array(currentParticleState),
    internalFormat: gl.RGBA32F,
    width: numParticles,
    height: 1,
    minMax: gl.NEAREST,
  });
  const nextStateTex = twgl.createTexture(gl, {
    internalFormat: gl.RGBA32F,
    width: numParticles,
    height: 1,
    minMax: gl.NEAREST,
  });

  // create a framebuffer with 1 attachment (currentStateTex)
  // and record that it's numParticles wide and 1 pixel tall
  let currentStateFBI = twgl.createFramebufferInfo(gl, [
    { attachment: currentStateTex, },
  ], numParticles, 1);

  // create a framebuffer with 1 attachment (nextStateTex)
  // and record that it's numParticles wide and 1 pixel tall
  let nextStateFBI = twgl.createFramebufferInfo(gl, [
    { attachment: nextStateTex, },
  ], numParticles, 1);

  const particleVS = `
  #version 300 es
  in vec4 position;
  void main() {
    gl_Position = position;
  }
  `;

  const particleFS = `
  #version 300 es
  precision highp float;

  uniform vec2 resolution;
  uniform float deltaTime;
  uniform sampler2D particleParamsTex;
  uniform sampler2D currentStateTex;

  out vec4 outColor;

  vec4 euclideanModulo(vec4 n, vec4 m) {
    return mod(mod(n, m) + m, m);
  }

  void main() {
    int i = int(gl_FragCoord.x);
    vec4 curPos = texelFetch(currentStateTex, ivec2(i, 0), 0);
    vec4 velocity = texelFetch(particleParamsTex, ivec2(i, 0), 0);

    outColor = euclideanModulo(curPos + velocity * deltaTime, vec4(resolution, 1, 1));
  }

  `;

  const drawVS = `
  #version 300 es
  uniform sampler2D currentStateTex;
  uniform vec2 resolution;
  void main() {
    gl_PointSize = 3.0;
    // we calculated pos in pixel coords 
    vec4 pos = texelFetch(currentStateTex, ivec2(gl_VertexID, 0), 0);
    gl_Position = vec4(
       pos.xy / resolution * 2. - 1.,  // convert to clip space
       0,
       1);
  }
  `;

  const drawFS = `
  #version 300 es
  precision mediump float;
  out vec4 outColor;
  void main() {
    outColor = vec4(0, 0, 0, 1);
  }
  `;

  // compile shaders, link program, look up locations.
  const particleProgramInfo = twgl.createProgramInfo(gl, [particleVS, particleFS]);
  const drawProgramInfo = twgl.createProgramInfo(gl, [drawVS, drawFS]);

  // create a -1 to +1 quad vertices and put in a buffer.
  const quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl, 2);

  let then = 0;
  function render(now) {
    now *= 0.001;  // convert to seconds
    const deltaTime = now - then;
    then = now;

    // bind the framebuffer and set the viewport to match
    twgl.bindFramebufferInfo(gl, nextStateFBI);
    gl.useProgram(particleProgramInfo.program);
    twgl.setBuffersAndAttributes(gl, particleProgramInfo, quadBufferInfo);
    twgl.setUniformsAndBindTextures(particleProgramInfo, {
      resolution: [width, height],
      deltaTime: deltaTime,
      currentStateTex: currentStateFBI.attachments[0],
      particleParamsTex,
    });
    // call drawArrays or drawBuffers
    twgl.drawBufferInfo(gl, quadBufferInfo);

    const t = nextStateFBI;
    nextStateFBI = currentStateFBI;
    currentStateFBI = t;  

    // bind the canvas and set the viewport to match
    twgl.bindFramebufferInfo(gl, null);
    gl.useProgram(drawProgramInfo.program);
    twgl.setUniforms(drawProgramInfo, {
      resolution: [width, height],
      currentStateTex: currentStateFBI.attachments[0],
    });
    gl.drawArrays(gl.POINTS, 0, numParticles);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}

main();
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>