当前位置:主页 > 资料 >

GPU-Accelerated Particles with WebGL 2
栏目分类:资料   发布日期:2017-06-25   浏览次数:

导读:本文为去找网小编(www.7zhao.net)为您推荐的GPU-Accelerated Particles with WebGL 2,希望对您有所帮助,谢谢! The WebGL 2 specification is based on GL ES 3, and has many new features compared to its predecessor. One of

本文为去找网小编(www.7zhao.net)为您推荐的GPU-Accelerated Particles with WebGL 2,希望对您有所帮助,谢谢!

www.7zhao.net



去找(www.7zhao.net欢迎您

The WebGL 2 specification is based on GL ES 3, and has many new features compared to its predecessor. One of those features is transform feedback. In this post, I'll be exploring how to use it to simulate a lot of particles entirely on the GPU.

去找(www.7zhao.net欢迎您

NOTE: the demos here use WebGL 2. While it is supported by recent desktop versions of most major browsers, it's still fairly new at the time of this writing. The demos might not work in some mobile browsers. If your browser doesn't support WebGL 2, you should see a placeholder message. In that case, I recommend either updating your current browser, or switching to a recent version of , or .

www.7zhao.net

Particle Systems

Particle systems are great for various visual effects like fireworks, rain, snow, flame, etc. Basically, anything with lots of small bits that fly around and look pretty. 内容来自www.7zhao.net

Let's start by defining what we mean when we use the term "particle system" hereafter.

内容来自www.7zhao.net

A particle system is a set of independent entities (particles), each of which is fully described by a small set of parameters (same for each particle). The particles appear, disappear and change the values of their parameters depending on time and other factors, according to a well-defined set of simple rules.

www.7zhao.net

The particular (no pun intended) set of parameters describing a particle is chosen by the implementation. You'd want to have obvious mundane things like a particle's position and velocity, but you can also be creative and add other stuff. For example, weight or size.

去找(www.7zhao.net欢迎您

While we're here, let's decide on a few details about the particle system we're going to build. We're going to have the following parameters for our particles:

内容来自www.7zhao.net

  • Position - to describe where the particle is
  • Velocity - to describe where the particle is headed and how fast
  • Age - the number of seconds the particle has been alive
  • Life - the number of seconds the particle should stay alive

The following rules will apply: 欢迎访问www.7zhao.net

  • Particles obey simple laws of motion (location of the particle after delta seconds equals its current location plus velocity multiplied by delta, velocity is affected by forces acting on the particle at that instance).
  • There's one special force affecting all particles at all times. We'll refer to it as "gravity", though this is mere convenience: our "gravity" is tunable by the user, can be set to pull in any direction with any strength, and can be turned off completely.
  • Particles cease to exist as soon as they reach a certain age.
  • As soon as a particle dies, a new one is born. Total population never exceeds a certain fixed number (chosen by the user).
  • Particles do NOT affect each other or the environment (collision detection on the GPU would be interesting to explore, but is outside the scope of this post :-))

Why GPU Accelerated Particles?

Before we dive into the implementation details though, let's stop for a second and reflect on why what we're about to do makes sense. Let's ask ourselves, what's the most straightforward way to implement a particle system?

copyright www.7zhao.net

The most obvious thing to do, of course, is to go through all the particles every frame, update their parameters according to the rules, and then send the updated positions (and maybe some other parameters) of the particles up to the GPU to be rendered. This works and is simple. 本文来自去找www.7zhao.net

Problems with that solution manifest when you dramatically increase the number of particles in the system (or the number of particle systems themeselves). Suddenly, not only you have to do more work per frame to compute new values of parameters, you also have to deliver a lot more data to the GPU every time you want to draw your particles, which isn't cheap either! 内容来自www.7zhao.net

But what if we could run the simulation directly on the GPU? First, we'd be able to harness the parallelism by updating a bunch of particles at a time, instead of one at a time. Second, we wouldn't need to upload the updated data every frame, since it's already right there! Luckily, with transform feedback, running the simulation on GPU is easy to pull off! 去找(www.7zhao.net欢迎您

Transform Feedback

As you know, the first programmable stage of the GPU pipeline is the vertex shader. It gets invoked for each vertex, taking as input some per-vertex data, processing it, and spitting out another set of per-vertex data. Usually, the main thing you expect to come out of the vertex shader are the positions of vertices in normalized device coordinates, but you can produce an arbitrary number of additional outputs (often referred to as varyings ). The varyings get pushed through the next stages of the pipeline. Transform feedback is a feature that lets you capture some of these varyings into a memory buffer on the GPU, and reuse that data later. 欢迎访问www.7zhao.net

Transform feedback works on both vertex and geometry shader outputs, but since WebGL 2 doesn't support geometry shaders, we'll won't be touching those.

copyright www.7zhao.net

Writing the Code

High-Level Overview

  • We're going to have two GL programs: the "particle update" program, and the "particle render" program.
    • The particle update program will consist of just a vertex shader. It will take as input the current state of particles in the system, and as output, it will produce a buffer containing the updated state of particles after a given elapsed time. Side note: this program will technically have a fragment shader as well (because WebGL 2 won't allow vertex-only programs), but it'll be a no-op that discards all fragments.
    • The particle render program will have a vertex shader and a fragment shader. It will take as input the updated state of particles produce a rendering of them.
  • In the beginning, we will create two buffers of the same size, big enough to hold the data about the max number of particles possible in our system. One of those we'll designate as the "read" buffer, and the other we'll designate as the "write" buffer.
  • We will populate the "read" buffer with the initial state of the particle system.
  • On each frame:
    • We bind the "read" buffer as input and the "write buffer" as transform feedback buffer. Then we invoke the "particle update" program to write the updated state of particles into the transform feedback buffer.
    • Now we bind the "write" buffer as input and invoke the "particle render" program to show the updated state of particles on-screen.
    • "Read" and "write" buffers get swapped

Particle Update - Vertex Shader

Let's start off by writing the vertex shader that implements the particle update step. We'll begin with our uniform inputs. Those are the same for all particles in the system. The meaning of each is explained in a comment.

去找(www.7zhao.net欢迎您

#version 300 es
precision mediump float;

/* Number of seconds (possibly fractional) that has passed since the last
   update step. */
uniform float u_TimeDelta;

/* A texture with just 2 channels (red and green), filled with random values.
   This is needed to assign a random direction to newly born particles. */
uniform sampler2D u_RgNoise;

/* This is the gravity vector. It's a force that affects all particles all the
   time.*/
uniform vec2 u_Gravity;

/* This is the point from which all newborn particles start their movement. */
uniform vec2 u_Origin;

/* Theta is the angle between the vector (1, 0) and a newborn particle's
   velocity vector. By setting u_MinTheta and u_MaxTheta, we can restrict it
   to be in a certain range to achieve a directed "cone" of particles.
   To emit particles in all directions, set these to -PI and PI. */
uniform float u_MinTheta;
uniform float u_MaxTheta;

/* The min and max values of the (scalar!) speed assigned to a newborn
   particle.*/
uniform float u_MinSpeed;
uniform float u_MaxSpeed; 
欢迎访问www.7zhao.net

Side note: as you can see, I am using a noise texture here, as a source of randomness. It is possible to do without a texture at all. There are ways to .

去找(www.7zhao.net欢迎您

Now, let's move on to our inputs and outputs: 内容来自www.7zhao.net

/* Inputs. These reflect the state of a single particle before the update. */

/* Where the particle is. */
in vec2 i_Position;

/* Age of the particle in seconds. */
in float i_Age;

/* How long this particle is supposed to live. */
in float i_Life;

/* Which direction it is moving, and how fast. */ 
in vec2 i_Velocity;


/* Outputs. These mirror the inputs. These values will be captured
   into our transform feedback buffer! */
out vec2 v_Position;
out float v_Age;
out float v_Life;
out vec2 v_Velocity; 
copyright www.7zhao.net

The above should be fairly obvious. We have a one-to-one mapping between vertices and particles: every invocation of the vertex shader has to update just one corresponding particle. Let's see how it's actually done: 去找(www.7zhao.net欢迎您

void main() {
  if (i_Age >= i_Life) {
    /* Particle has exceeded its lifetime! Time to spawn a new one
       in place of the old one, in accordance with our rules.*/
    
    /* First, choose where to sample the random texture. I do it here
       based on particle ID. It means that basically, you're going to
       get the same initial random values for a given particle. The result
       still looks good. I suppose you could get fancier, and sample
       based on particle ID *and* time, or even have a texture where values
       are not-so-random, to control the pattern of generation. */
    ivec2 noise_coord = ivec2(gl_VertexID % 512, gl_VertexID / 512);
    
    /* Get the pair of random values. */
    vec2 rand = texelFetch(u_RgNoise, noise_coord, 0).rg;

    /* Decide the direction of the particle based on the first random value.
       The direction is determined by the angle theta that its vector makes
       with the vector (1, 0).*/
    float theta = u_MinTheta + rand.r*(u_MaxTheta - u_MinTheta);

    /* Derive the x and y components of the direction unit vector.
       This is just basic trig. */
    float x = cos(theta);
    float y = sin(theta);

    /* Return the particle to origin. */
    v_Position = u_Origin;

    /* It's new, so age must be set accordingly.*/
    v_Age = 0.0;
    v_Life = i_Life;

    /* Generate final velocity vector. We use the second random value here
       to randomize speed. */
    v_Velocity =
      vec2(x, y) * (u_MinSpeed + rand.g * (u_MaxSpeed - u_MinSpeed));

  } else {
    /* Update parameters according to our simple rules.*/
    v_Position = i_Position + i_Velocity * u_TimeDelta;
    v_Age = i_Age + u_TimeDelta;
    v_Life = i_Life;
    v_Velocity = i_Velocity + u_Gravity * u_TimeDelta;
  }
} 去找(www.7zhao.net欢迎您 

That's it for the particle update shader. We'll make some small modifications to it later, but the main logic is there. 欢迎访问www.7zhao.net

Rendering the Particles

Now, let's write a couple of simple shaders to actually visualize the output of the previous step. This is going to be very basic for the time being, but we will add to it later. 欢迎访问www.7zhao.net

Here we go, vertex shader for visualizing particles, version 1: 内容来自www.7zhao.net

#version 300 es
precision mediump float;

in vec2 i_Position;
in float i_Age;
in float i_Life;
in vec2 i_Velocity;

void main() {
  gl_PointSize = 1.0;
  gl_Position = vec4(i_Position, 0.0, 1.0);
} 去找(www.7zhao.net欢迎您 

As you can see, it is intended to draw points primitives. For now, it's only using the position attribute, but this is going to change later.

本文来自去找www.7zhao.net

And here's the corresponding fragment shader, also super simple for now:

去找(www.7zhao.net欢迎您

#version 300 es
precision mediump float;

out vec4 o_FragColor;

void main() {
  o_FragColor = vec4(1.0);
} 去找(www.7zhao.net欢迎您 

Basically, this pair of shaders makes particles appear on screen as white dots. Like I said, we'll make it a bit fancier later :-)

本文来自去找www.7zhao.net

The JavaScript Code

Before we can finally see our shaders in action, we have to write a bit of javascript code. copyright www.7zhao.net

First step is loading and compiling shaders. In this post, I will follow the convention where shaders are inlined into html code, like this:

去找(www.7zhao.net欢迎您

<script type = "text/x-fragment-shader" id = "particle-render-frag">
 // ... shader code goes here...
</script> 
去找(www.7zhao.net欢迎您

And here is a pair of functions that load shaders from "script" tags, compile them and link them into a program. I would normally skip this, but it contains a little bit of detail that is relevant to our interests.

欢迎访问www.7zhao.net

function createShader(gl, shader_info) {
  var shader = gl.createShader(shader_info.type);
  var i = 0;
  var shader_source = document.getElementById(shader_info.name).text;
  /* skip whitespace to avoid glsl compiler complaining about
    #version not being on the first line*/
  while (/\s/.test(shader_source[i])) i++; 
  shader_source = shader_source.slice(i);
  gl.shaderSource(shader, shader_source);
  gl.compileShader(shader);
  var compile_status = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (!compile_status) {
    var error_message = gl.getShaderInfoLog(shader);
    throw "Could not compile shader \"" +
          shader_info.name +
          "\" \n" +
          error_message;
  }
  return shader;
}

/* Creates an OpenGL program object.
   `gl' shall be a WebGL 2 context.
   `shader_list' shall be a list of objects, each of which have a `name'
      and `type' properties. `name' will be used to locate the script tag
      from which to load the shader. `type' shall indicate shader type (i. e.
      gl.FRAGMENT_SHADER, gl.VERTEX_SHADER, etc.)
  `transform_feedback_varyings' shall be a list of varying that need to be
    captured into a transform feedback buffer.*/
function createGLProgram(gl, shader_list, transform_feedback_varyings) {
  var program = gl.createProgram();
  for (var i = 0; i < shader_list.length; i++) {
    var shader_info = shader_list[i];
    var shader = createShader(gl, shader_info);
    gl.attachShader(program, shader);
  }

  /* Specify varyings that we want to be captured in the transform
     feedback buffer. */
  if (transform_feedback_varyings != null) {
    gl.transformFeedbackVaryings(
      program,
      transform_feedback_varyings,
      gl.INTERLEAVED_ATTRIBS)
  }

  gl.linkProgram(program);
  var link_status = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!link_status) {
    var error_message = gl.getProgramInfoLog(program);
    throw "Could not link program.\n" + error_message;
  }
  return program;
} 
内容来自www.7zhao.net

I want you to pay attention to the third parameter of createGLProgram . As I mentioned earlier, transform feedback lets us capture the values of the varyings that a vertex shader outputs. However, we must specify the varyings that we want to capture before we link the program . This is done through a call to transformFeedbackVaryings . When calling that function, you have to specify the program you're dealing with, the list of captured varyings, and the preferred way of capture (capture them interleaved into a single buffer, or capture each one into a separate buffer). In this case we're using interleaved capture mode. copyright www.7zhao.net

Next, I am going to introduce a couple of helper functions that we're going to use during the initialization process of our particle system. 本文来自去找www.7zhao.net

function randomRGData(size_x, size_y) {
  var d = [];
  for (var i = 0; i < size_x * size_y; ++i) {
    d.push(Math.random() * 255.0);
    d.push(Math.random() * 255.0);
  }
  return new Uint8Array(d);
}

function initialParticleData(num_parts, min_age, max_age) {
  var data = [];
  for (var i = 0; i < num_parts; ++i) {
    // position
    data.push(0.0);
    data.push(0.0);

    var life = min_age + Math.random() * (max_age - min_age);
    // set age to max. life + 1 to ensure the particle gets initialized
    // on first invocation of particle update shader
    data.push(life + 1);
    data.push(life);

    // velocity
    data.push(0.0);
    data.push(0.0);
  }
  return data;
} 
www.7zhao.net

The first one, randomRGData , simply generates data for a random 2-channel texture that is going to be used in the particle update shader.

去找(www.7zhao.net欢迎您

The second one, initialParticleData , generates the data representing the initial state of the particle system. This will get sent to the GPU once at the beginning, and from there it will be updated by the update shader. 内容来自www.7zhao.net

The next function is the one that sets up the particle system. It's a bit long, but it's mostly trivial to follow:

www.7zhao.net

/*
 * Returns an object representing a particle system with the given parameters.
 * `gl' shall be a valid WebGL 2 context.
 * `particle_birth_rate' defines the number of particles born per millisecond.
 * `num_particles' shall be the total number of particles in the system.
 * `min_age' and `max_age' define the allowed age range for particles, in
 *     seconds. No particle will survive beyond max_age, and every particle
 *     is guaranteed to remain alive for at least min_age seconds.
 * `min_theta' and `max_theta' define the range of directions in which new
 *     particles are allowed to be emitted.
 * `min_speed' and `max_speed' define the valid range of speeds for new
 *     particles.
 * `gravity' is a 2-vector representing a force affecting all particles at all
 *     times.
 */
function init(
    gl,
    num_particles,
    particle_birth_rate,
    min_age,
    max_age, 
    min_theta,
    max_theta,
    min_speed,
    max_speed,
    gravity) {
  /* Do some parameter validation */
  if (max_age < min_age) {
    throw "Invalid min-max age range.";
  }
  if (max_theta < min_theta ||
      min_theta < -Math.PI ||
      max_theta > Math.PI) {
    throw "Invalid theta range.";
  }
  if (min_speed > max_speed) {
    throw "Invalid min-max speed range.";
  }

  /* Create OpenGL programs objects for updating and displaying particles. */
  var update_program = createGLProgram(
    gl,
    [
      {name: "particle-update-vert", type: gl.VERTEX_SHADER},
      {name: "passthru-frag-shader", type: gl.FRAGMENT_SHADER},
    ],
    [
      "v_Position",
      "v_Age",
      "v_Life",
      "v_Velocity",
    ]);
  var render_program = createGLProgram(
    gl,
    [
      {name: "particle-render-vert", type: gl.VERTEX_SHADER},
      {name: "particle-render-frag", type: gl.FRAGMENT_SHADER},
    ],
    null);

  /* We shall use a kind of double-buffering system: the "read" buffer
     will contain the state of the particle system at time t, and will
     be used as input for the "update" program.
     The "write" buffer will be bound as transform feedback buffer for the
     "update" program. After the update program is finished, it will contain
     the state of the particle system at time t + delta.
     After that, "write" buffer becomes the "read" buffer and the old "read"
     buffer becomes the new "write" buffer for the next frame.
     Here, we create both buffers, their corresponding VAOs, and populate the
     buffers with some initial data.
     */
  var buffers = [
    gl.createBuffer(),
    gl.createBuffer()
  ];
  var vaos = [
    gl.createVertexArray(),
    gl.createVertexArray()
  ];
  var initial_data =
    new Float32Array(initialParticleData(num_particles, min_age, max_age));
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers[0]);
  gl.bufferData(gl.ARRAY_BUFFER, initial_data, gl.STREAM_DRAW);
  setupParticleBufferVAO(gl, buffers[0], vaos[0]);
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers[1]);
  gl.bufferData(gl.ARRAY_BUFFER, initial_data, gl.STREAM_DRAW);
  setupParticleBufferVAO(gl, buffers[1], vaos[1]);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  
  /* Create a texture object for our random RG noise. */
  var rg_noise_texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, rg_noise_texture);
  gl.texImage2D(gl.TEXTURE_2D,
                0, 
                gl.RG8,
                512, 512,
                0,
                gl.RG,
                gl.UNSIGNED_BYTE,
                randomRGData(512, 512));
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  /* We will need this later ;) */
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

  return {
    particle_sys_buffers: buffers,
    particle_sys_vaos: vaos,
    read: 0,
    write: 1,
    particle_update_program: update_program,
    particle_render_program: render_program,
    num_particles: initial_data.length / 6,
    old_timestamp: 0.0,
    rg_noise: rg_noise_texture,
    total_time: 0.0,
    born_particles: 0,
    birth_rate: particle_birth_rate,
    gravity: gravity,
    origin: [0.0, 0.0],
    min_theta: min_theta,
    max_theta: max_theta,
    min_speed: min_speed,
    max_speed: max_speed
  };
}

/* This one just sets up a VAO for our buffers, nothing fancy here. */
function setupParticleBufferVAO(gl, buffer, vao, attrib_locations) {
  gl.bindVertexArray(vao);
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

  // Position attrib pointer.
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 6 * 4, 0);

  // Age attrib pointer.
  gl.enableVertexAttribArray(1);
  gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 6 * 4, 2 * 4);

  // Max life attrib pointer.
  gl.enableVertexAttribArray(2);
  gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 6 * 4, 3 * 4);

  // Velocity attrib pointer.
  gl.enableVertexAttribArray(3);
  gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 6 * 4, 4 * 4);

  gl.bindVertexArray(null);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

} 

去找(www.7zhao.net欢迎您

I just need to make an additional comment on the particle_birth_rate parameter. The thing is, when rendering a particle system with, say, 100000 particles, we don't necessarily want to spawn all of them into existence on the very first frame (or maybe we do, it depends on if you want to get an explosion-like effect :-)). The birth rate parameter lets us control that: instead of spawning all particles at once, it lets us gradually introduce new ones into the system, until maximum capacity is reached. You'll see how it works very soon. www.7zhao.net

It's now time to introduce the main render loop, the part where the particle system actually gets updated and displayed!

copyright www.7zhao.net

/* Gets called every frame.
   `gl' shall be a valid WebGL 2 context
   `state' is shall be the state of the particle system
   `timestamp_millis' is the current timestamp in milliseconds
   */
function render(gl, state, timestamp_millis) {
  var num_part = state.born_particles;

  /* Calculate time delta. */
  var time_delta = 0.0;
  if (state.old_timestamp != 0) {
    time_delta = timestamp_millis - state.old_timestamp;
    if (time_delta > 500.0) {
      /* If delta is too high, pretend nothing happened.
         Probably tab was in background or something. */
      time_delta = 0.0;
    }
  }

  /* Here's where birth rate parameter comes into play.
     We add to the number of active particles in the system
     based on birth rate and elapsed time. */
  if (state.born_particles < state.num_particles) {
    state.born_particles =
        Math.min(state.num_particles,
                  Math.floor(state.born_particles +
                             state.birth_rate * time_delta));
  }

  /* Set the previous update timestamp for calculating time delta in the
     next frame. */
  state.old_timestamp = timestamp_millis;


  /* Most of the following is trivial setting of uniforms */
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.useProgram(state.particle_update_program);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_TimeDelta"),
    time_delta / 1000.0);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_TotalTime"),
    state.total_time);
  gl.uniform2f(
    gl.getUniformLocation(state.particle_update_program, "u_Gravity"),
    state.gravity[0], state.gravity[1]);
  gl.uniform2f(
    gl.getUniformLocation(state.particle_update_program, "u_Origin"),
    state.origin[0],
    state.origin[1]);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_MinTheta"),
    state.min_theta);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_MaxTheta"),
    state.max_theta);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_MinSpeed"),
    state.min_speed);
  gl.uniform1f(
    gl.getUniformLocation(state.particle_update_program, "u_MaxSpeed"),
    state.max_speed);
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, state.rg_noise);
  gl.uniform1i(
    gl.getUniformLocation(state.particle_update_program, "u_RgNoise"),
    0);

  /* Bind the "read" buffer - it contains the state of the particle system
    "as of now".*/
  gl.bindVertexArray(state.particle_sys_vaos[state.read]);

  /* Bind the "write" buffer as transform feedback - the varyings of the
     update shader will be written here. */
  gl.bindBufferBase(
    gl.TRANSFORM_FEEDBACK_BUFFER, 0, state.particle_sys_buffers[state.write]);

  /* Since we're not actually rendering anything when updating the particle
     state, disable rasterization.*/
  gl.enable(gl.RASTERIZER_DISCARD);

  /* Begin transform feedback! */
  gl.beginTransformFeedback(gl.POINTS);
  gl.drawArrays(gl.POINTS, 0, num_part); /* Run update! */
  gl.endTransformFeedback(); /* End transform feedback!

  gl.disable(gl.RASTERIZER_DISCARD);

  /* Don't forget to unbind the transform feedback buffer! */
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

  /* Now, we draw the particle system. Note that we're actually
     drawing the data from the "read" buffer, not the "write" buffer
     that we've written the updated data to. */
  gl.useProgram(state.particle_render_program);
  gl.drawArrays(gl.POINTS, 0, num_part);

  /* Finally, we swap read and write buffers. The updated state will be
     rendered on the next frame. */
  var tmp = state.read;
  state.read = state.write;
  state.write = tmp;

  /* This just loops this function. */
  window.requestAnimationFrame(function(ts) { render(gl, state, ts); });
} 去找(www.7zhao.net欢迎您 

Finally, we just need a little bit of code to create a WebGL 2 context for us, and kick off the entire thing:

copyright www.7zhao.net

function main() {
  var canvas_element = document.createElement("canvas");
  canvas_element.width = 800;
  canvas_element.height = 600;
  var webgl_context = canvas_element.getContext("webgl2");
  if (webgl_context != null) {
    document.body.appendChild(canvas_element);
    var state =
      init(
        webgl_context,
        10000, /* number of particles */
        0.5, /* birth rate */
        1.01, 1.15, /* life range */
        Math.PI/2.0 - 0.5, Math.PI/2.0 + 0.5, /* direction range */
        0.5, 1.0, /* speed range */
        [0.0, -0.8]); /* gravity */

    /* Makes the particle system follow the mouse pointer */
    canvas_element.onmousemove = function(e) {
      var x = 2.0 * (e.pageX - this.offsetLeft)/this.width - 1.0; 
      var y = -(2.0 * (e.pageY - this.offsetTop)/this.height - 1.0);
      state.origin = [x, y];
    };
    window.requestAnimationFrame(
      function(ts) { render(webgl_context, state, ts); });
  } else {
    document.write("WebGL2 is not supported by your browser");
  }
} 

内容来自www.7zhao.net

Don't forget to put <body onload="main()"> into your HTML file!

去找(www.7zhao.net欢迎您

Aaand, let's see it in action!

本文来自去找www.7zhao.net

Click the image below to open an interactive demo in a new tab.

内容来自www.7zhao.net

Enhancements

So, now we have ourselves a little fountain of white specks. What can we do to improve its appearance a bit? 欢迎访问www.7zhao.net

Varying Particle Appearance With Age

The most obvious thing that comes to mind is to change up some stuff in the particle rendering shader based on how old a given particle is. I will vary three parameters: transparency, size, and color. www.7zhao.net

For this, we only need to update the code in particle rendering shaders. Here is the updated vertex shader for rendering a particle: 欢迎访问www.7zhao.net

#version 300 es
precision mediump float;

in vec2 i_Position;
in float i_Age;
in float i_Life;
in vec2 i_Velocity;

out float v_Age;
out float v_Life;

void main() {
  /* Set varyings so that frag shader can use these values too.*/
  v_Age = i_Age;
  v_Life = i_Life;

  /* Vary point size based on age. Make old particles shrink. */
  gl_PointSize = 1.0 + 6.0 * (1.0 - i_Age/i_Life);

  gl_Position = vec4(i_Position, 0.0, 1.0);
} www.7zhao.net 

And here's the updated fragment shader:

内容来自www.7zhao.net

#version 300 es
precision mediump float;

in float v_Age;
in float v_Life;

out vec4 o_FragColor;

/* From http://iquilezles.org/www/articles/palettes/palettes.htm */
vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d )
{  return a + b*cos( 6.28318*(c*t+d) ); }

void main() {
  float t =  v_Age/v_Life;
  o_FragColor = vec4(
    palette(t,
            vec3(0.5,0.5,0.5),
            vec3(0.5,0.5,0.5),
            vec3(1.0,0.7,0.4),
            vec3(0.0,0.15,0.20)), 1.0 - t);
} 本文来自去找www.7zhao.net 

Here, I am once again borrowing this . Also, I am making older particles more transparent. www.7zhao.net

Here is what it looks like in action:

www.7zhao.net

Click the image below to open an interactive demo in a new tab. 内容来自www.7zhao.net

Turning On the Force Field

Currently, there is one force affecting all of our particles at all times. We could add some interesting variation to the movement if we made different forces act on a particle depending on its position in space. 内容来自www.7zhao.net

To that end, we're going to use an additional texture with red and green values. I'll refer to it as a "force field". Here's the one I'm going to use: 欢迎访问www.7zhao.net

This image simply has perlin noise in its red and green channels. I chose smooth continious noise because it results in more interesting movement.

欢迎访问www.7zhao.net

The code will require a couple of modifications. The vertex update shader will need to sample the force field like so: www.7zhao.net

//...
  vec2 force = 4.0 * (2.0 * texture(u_ForceField, i_Position).rg - vec2(1.0));
//... 欢迎访问www.7zhao.net 

And apply the force like this: 本文来自去找www.7zhao.net

//...
    v_Velocity = i_Velocity + u_Gravity * u_TimeDelta + force * u_TimeDelta;
//... 本文来自去找www.7zhao.net 

The changes in the javascript code itself are trivial and pretty much just boil down to creating a new texture, uploading the noise image to it, and binding it before running the update step. Here's the result:

www.7zhao.net

Click the image below to open an interactive demo in a new tab.

本文来自去找www.7zhao.net

That's nice, but can be taken further. If, instead of using a static texture, you actually generate Perlin noise in the fragment shader (as describedhere), and animate it with time, you can get something like this:

去找(www.7zhao.net欢迎您

Click the image below to open an interactive demo in a new tab. 去找(www.7zhao.net欢迎您

Billboards

So far, we've been using point primitives to render our particles. Depending on the aesthetic you're going for, this might actually be perfectly suitable. However, in most cases, you probably want to draw something other than a point in place of each particle. For example, a textured quad.

copyright www.7zhao.net

This is where we run into some problems. Our particle data is per-vertex. So far, this hasn't been a problem - one point on screen corresponded to one vertex, and therefore, to one set of particle parameters. However, if we want to draw a quad, we need four vertices. So, we need a way of sharing a particle's parameters between the vertices of its corresponding quad.

www.7zhao.net

One way of achieving this is using a . The input to the geometry shader would be points, and it would generate corresponding quads on the other end. However, WebGL 2 does not support geometry shaders, so that option is right out.

copyright www.7zhao.net

Another way of doing it is instancing. In short, instancing allows you to render multiple instances of a mesh with a single draw call. When setting up your vertex arrays, you may choose some attributes to stay the same value within an instance (or even across a fixed number of consecutive instances). For example, you might have a "position" attribute, which only changes once per instance. For each instance, it would contain the world-space position at which to draw it.

去找(www.7zhao.net欢迎您

We're going to do exactly that - our instances are going to be quads, while all the particle data (age, position, etc.) will be per-instance. 去找(www.7zhao.net欢迎您

The particle update shader doesn't change at all. 本文来自去找www.7zhao.net

There are some changes in the particle render shader though. Let's have a look at them. To make things easier, I've highlighted changed parts. copyright www.7zhao.net

#version 300 es
precision mediump float;

// These attributes stay the same for all vertices in an instance.
in vec2 i_Position;
in float i_Age;
in float i_Life;
in vec2 i_Velocity;

// These attributes change for each of the instance's vertices.
in vec2 i_Coord;
in vec2 i_TexCoord;

out float v_Age;
out float v_Life;
out vec2 v_TexCoord;

void main() {
  vec2 vert_coord = i_Position +
					(0.75*(1.0-i_Age / i_Life) + 0.25) * 0.1 * i_Coord +
					i_Velocity * 0.0;
  v_Age = i_Age;
  v_Life = i_Life;
  v_TexCoord = i_TexCoord;
  gl_Position = vec4(vert_coord, 0.0, 1.0);
} www.7zhao.net 

As you can see, we've added new attributes for a quad's vertex positions and texture coordinates. The vertex positions will always be (-1,-1), (-1, 1), (1, 1), (1, -1). We'll translate and scale them based on particle attributes.

www.7zhao.net

Let's have a quick look at the corresponding fragment shader as well.

内容来自www.7zhao.net

#version 300 es
precision mediump float;

// Particle sprite texture.
uniform sampler2D u_Sprite;

in float v_Age;
in float v_Life;
in vec2 v_TexCoord;

out vec4 o_FragColor;

void main() {
  float t =  v_Age/v_Life;
  vec4 color = vec4(vec3(1.0, 0.8, 0.3), 1.0-(v_Age/v_Life));
  o_FragColor = color * texture(u_Sprite, v_TexCoord);
} 去找(www.7zhao.net欢迎您 

As you can see, we've added a sprite texture, and all this shader does is apply the texture to our quad. 内容来自www.7zhao.net

The changes to the javascript code are a bit more substantial. First, let's have a look at the changes in the init function. I'll only show the relevant parts:

去找(www.7zhao.net欢迎您

function init(
    gl,
    num_particles,
    particle_birth_rate,
    min_age,
    max_age, 
    min_theta,
    max_theta,
    min_speed,
    max_speed,
    gravity,
    part_img) { // Note the new parameter.
    // ... skip unimportant stuff...
    
  // Now we're using 4 VAOs - 2 for the particle update,
  // and 2 for the instanced rendering.
  var vaos = [
    // VAO for update operation.
    gl.createVertexArray(),
    gl.createVertexArray(),
    
    // VAO for render operation.
    gl.createVertexArray(),
    gl.createVertexArray()
  ];

  // This is the vertex data of a quad.
  // Positions and texture coordinates.
  // Positions will be scaled and translated in the vertex shader.
  var vert_data =
    new Float32Array([
      1, 1,
      1, 1,

      -1, 1,
      0, 1,
      
      -1, -1,
      0, 0,
      
      1, 1,
      1, 1,
      
      -1, -1,
      0, 0,
      
      1, -1,
      1, 0]);
  var buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, vert_data, gl.STATIC_DRAW);

  var initial_data =
    new Float32Array(initialParticleData(num_particles, min_age, max_age));
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers[0]);
  gl.bufferData(gl.ARRAY_BUFFER, initial_data, gl.STREAM_DRAW);
  setupParticleBufferVAO(gl, buffers[0], null, vaos[0]); // Set up the VAO for update
  setupParticleBufferVAO(gl, buffers[0], buf, vaos[2]);  // Set up the VAO for rendering
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers[1]);
  gl.bufferData(gl.ARRAY_BUFFER, initial_data, gl.STREAM_DRAW);
  setupParticleBufferVAO(gl, buffers[1], null, vaos[1]);
  setupParticleBufferVAO(gl, buffers[1], buf, vaos[3]);
  // ...skip more unimportant stuff...

  gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // additive blending!

  // Create a texture object for our sprite:
  var particle_tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, particle_tex);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 32, 32, 0, gl.RGBA, gl.UNSIGNED_BYTE, part_img);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  
  return {
    //...snip...
    particle_tex: particle_tex
  };
} 欢迎访问www.7zhao.net 

The most important part here is that now we're using different VAOs for updating and rendering particles. The "update" VAO pretty much stays the same as before, but we do some additional stuff to the "render" VAO for instanced rendering. Let's have a look at setupParticleBufferVAO to see how it's different:

欢迎访问www.7zhao.net

function setupParticleBufferVAO(gl,
                                instance_buffer,
                                buffer,
                                vao,
                                attrib_locations) {
  gl.bindVertexArray(vao);
  gl.bindBuffer(gl.ARRAY_BUFFER, instance_buffer);

  // Position attrib pointer.
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 6 * 4, 0);
  if (buffer != null) gl.vertexAttribDivisor(0, 1);

  // Age attrib pointer.
  gl.enableVertexAttribArray(1);
  gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 6 * 4, 2 * 4);
  if (buffer != null) gl.vertexAttribDivisor(1, 1);

  // Max life attrib pointer.
  gl.enableVertexAttribArray(2);
  gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 6 * 4, 3 * 4);
  if (buffer != null) gl.vertexAttribDivisor(2, 1);
  
  // Velocity attrib pointer.
  gl.enableVertexAttribArray(3);
  gl.vertexAttribPointer(3, 2, gl.FLOAT, false, 6 * 4, 4 * 4);
  if (buffer != null) gl.vertexAttribDivisor(3, 1);

  if (buffer != null) {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(4);
    gl.vertexAttribPointer(4, 2, gl.FLOAT, false, 4 * 4, 0);
    gl.vertexAttribDivisor(4, 0);
    gl.enableVertexAttribArray(5);
    gl.vertexAttribPointer(5, 2, gl.FLOAT, false, 4 * 4, 2 * 4);
    gl.vertexAttribDivisor(5, 0);
  }

  gl.bindVertexArray(null);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
} www.7zhao.net 

The calls to vertexAttribDivisor specify the rate at which the attribute changes during instanced rendering. vertexAttribDivisor(3, 1) , for example, means to advance the value of attribute at index 3 once per instance. Passing "0" for the second argument essentially means that the attribute changes for each vertex of the instance.

去找(www.7zhao.net欢迎您

Finally, let's look at how the render function has changed. The changes there are minimal: 去找(www.7zhao.net欢迎您

function render(gl, state, timestamp_millis) {
  //...skip unchanged parts...

  // particle_sys_vaos[0] and [1] are used for update.
  // [1] and [2] are used for rendering.
  gl.bindVertexArray(state.particle_sys_vaos[state.read + 2]);
  gl.useProgram(state.particle_render_program);
  // Bind the sprite texture.
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, state.particle_tex);
  gl.uniform1i(
    gl.getUniformLocation(state.particle_render_program, "u_Sprite"),
    0);
  // Make an instanced draw call. Each particle is an instance.
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, num_part);
  // ...snip...
} 

copyright www.7zhao.net

And here it is in action, a blazing fireball:

www.7zhao.net

Click the image below to open an interactive demo in a new tab.

内容来自www.7zhao.net

And that'll be it for this post. This was fairly long, and code-heavy, so let me know if I can do a better job explaining some parts.

去找(www.7zhao.net欢迎您

Like this post? this blog on Twitter for more!

www.7zhao.net

copyright www.7zhao.net


本文原文地址:http://gpfault.net/posts/webgl2-particles.txt.html

以上为GPU-Accelerated Particles with WebGL 2文章的全部内容,若您也有好的文章,欢迎与我们分享!

欢迎访问www.7zhao.net

Copyright ©2008-2017去找网版权所有   皖ICP备12002049号-2 皖公网安备 34088102000435号   关于我们|联系我们| 免责声明|友情链接|网站地图|手机版