Line Integral Convolution (LIC) is basically a very fancy name for “bluring along StreamLines”.

Streamlines are the pathlines of particles following e.g. a flow field. In the LIC shader I’m using simple verlet integration to update the particles position/velocity. At each new position a samples is taken and weighted. This effect works best when the source texture contains a lot of random and well distributed noise because particles on the same or nearby Streamlines pick up similar samples and therefore are shaded in a similar way.

Line Integral Convolution (LIC)

Flow Field StreamLines

In general this type of visualization is a rather expensive process because for each pixel a reasonable amount of samples is required  – between 10 and 80, depending on the other parameters of course.

However, on decent devices a GLSL shader can easily do such an amount of texture-lookups per fragment, even though the lack of spatial coherence of the sample positions.

Flow Field Particles

Flow Field

LIC – Shader

Note that the shader is basically the same code as I am already using for the FlowFieldParticles update function.

 

GLSL: github.com/diwi/PixelFlow//flowfield_display_line_integral_convolution.frag

#version 150

#define NUM_SAMPLES 5

#define TRACE_FORWARD 1
#define TRACE_BACKWARD 1
#define APPLY_EXP_SHADING 1

out vec4 out_frag;

uniform vec2 wh_rcp;
uniform vec2 wh_vel_rcp;
uniform float acc_mult = 1.0; // timestep
uniform float vel_mult = 1.0; // damping
uniform vec2  acc_minmax = vec2(0.0, 1.0);
uniform vec2  vel_minmax = vec2(0.0, 1.0);

uniform float intensity_mult = 1.0; 
uniform float intensity_exp = 1.0; 

uniform sampler2D tex_src;
uniform sampler2D tex_acc;

void limitLength(inout vec2 vel, in vec2 lohi){
  float vel_len = length(vel);
  if(vel_len <= lohi.x){
    vel *= 0.0;
  } else {
    vel *= clamp(vel_len - lohi.x, 0.0, lohi.y) / vel_len;
  }
}

void traceStream(inout vec4 samples_sum, inout float weights_sum, in float acc_mult_dir){

  // start position
  vec4 pos = (gl_FragCoord.xy * wh_rcp).xyxy;
  float weight = 1.0;
  for(int i = 0; i < NUM_SAMPLES; i++){
    // acceleration, velocity
    vec2 acc = texture(tex_acc, pos.xy).xy;
    vec2 vel = (pos.xy - pos.zw) / wh_vel_rcp;
    // clamp 
    limitLength(acc, acc_minmax);
    limitLength(vel, vel_minmax); 
    // update position, verlet integration
    pos.zw = pos.xy;
    pos.xy += (vel * vel_mult + acc * acc_mult_dir) * wh_vel_rcp;
    // integrate
#if (APPLY_EXP_SHADING == 1)
    weight = pow(length(acc), intensity_exp);
#endif
    samples_sum += texture(tex_src, pos.xy) * weight;
    weights_sum += 1.0;
  }
}


void main(){

  vec4  samples_sum = vec4(0.0);
  float weights_sum = 0.0;

#if (TRACE_BACKWARD == 1)
  traceStream(samples_sum, weights_sum, -acc_mult);
#endif

#if (TRACE_FORWARD == 1)
  traceStream(samples_sum, weights_sum, +acc_mult);
#endif

  out_frag = intensity_mult * samples_sum / weights_sum;

}


 

Flow Field StreamLines

As mentioned above, the StreamLines can also be created by doing a Flow Field Particle Simulation. In the following video, the Streamlines are created by spawning DwFlowFieldParticles on a regular grid accross the full frame and calling update/render in an alternating fashion.

The number of update steps and the velocity of the flow field determine the length of each StreamLine. Additionally all other features from the Flow Field Particles, like Collisions/Cohesion/Obstacles could be enabled.

 

FLuid Simulation / StreamLines / LIC

In this demo the LIC-Shader is applied on top of a fluid simulation.

The Flow Field, used as input for the LIC-Shader to accelerate particles is the velocity from the fluid simulation.

 

Another fluid simulation using a different background noise texture for the LIC samples.

 

 

Flow Field Pathfinding

Some examples using the LIC shader in my flow-field pathfinding and space syntax test app.

Voronoi, Flow Field Pathfinding, LIC + StreamLines

Maze, Flow Field Pathfinding, LIC + StreamLines