Debugging Shaders - sahilshahpatel/fluid-sim GitHub Wiki
Debugging is one of the hardest parts of graphics applications. Because your code is being run on the GPU and because it's so parallelized, there are a lot fewer tools available to help you debug. The easiest forms of debugging on the CPU -- print statements and breakpoints -- are not available!
There are a few tools available for debugging the data sent to the GPU (e.g. vertex attributes and shader uniforms), but since this project focuses less on the geometry and more on the shaders we won't address them here. Instead we'll talk about how you can use the visual output of a shader to infer what is happening on the GPU.
Our basic render shader (if you haven't yet checked out Appendix A) outputs the RGB triple (dye, vel.x, vel.y) as-is. This is very surface-level debugging. You may have noticed that in all our demonstration GIFs we tend to click and drag upwards or to the right. This is because while our textures can contain any float values, those values are clamped into the range [0, 1] before being sent to the screen as color values.
While float textures can store any value, displayed pixels are clamped to [0, 1]
This is important because it means negative values are displayed the same as zero, with the color black. It is also important to keep in mind that values above 1 will all be displayed the same.
To combat this, you can shift your values by + vec3(0.5)
so that 0 is displayed as gray. Another option is to only debug a single float value at a time rather than three. In that case you can use red for positive and blue for negative. This can be acheived by multiplying the value by vec3(1., -1., 0.)
.
No matter your color representation scheme, it is often useful view the initial output, modify the shader, hard refresh, and compare. This will help you identify what your changes are doing.
Within our template you could write your own debug shader and wrapper function or you can modify your render shader temporarily.
One other important point to note is that shader programs don't have a great way to deal with runtime errors. Dividing by 0 in a shader, for example, has undefined behavior. In our experience, it is common for division by 0 to set all fragments to 0 and to automatically disable the program so it can't be run again. Since this often happens in our calculation shaders but not in our render shader, runtime errors in this project are often represented by a blank render screen (that's black with the basic render shader or your background color of choice from Appendix A).
The above methods are good for debugging separate, unrelated float values. To debug a vector field (like our velocity texture, for example) different methods may be better.
The first method is to create shaders which compute the divergence and curl of the vector field. These are calculus concepts which describe how much the field is coming together/moving apart and how much it's spinning respectively. We actually need to compute these things anyway as part of projection and vorticity confinement! This method is similar to section one in that you should output these values to separate color channels and develop an intuition for how to interpret the visual output.
The second method is to sample the data at a few points and draw arrows representing that data to screen. This is the method we use to produce our visuals for projection because it is a lot more intuitive.
If you want to implement this yourself we recommend being familiar with fragment shader drawing techniques -- SDFs (signed distance fields) in particular. For most readers we recommend copying the code below into your render shader.
(Spoiler!) Vector field debug code
/**
* Returns a boolean indicating if this
* fragment is in the arrow or not.
*/
bool inArrow(void){
const float arrowDensity = 16.;
// Velocity should be measured at center for entire arrow
vec2 cellSelector = normalize(dataRes) * arrowDensity;
vec2 v = texture(vel, (floor(fragUV * cellSelector) + 0.5) / cellSelector).xy;
if(length(v) <= 0.) return false;
vec2 pos = fragUV * cellSelector;
vec2 p = (fract(pos) - 0.5) * 2.; // This is [-1, 1] position w/ origin at center of cell
// Resize and rotate p to orient arrow
float size = clamp(length(v / dataRes * vec2(32., 20.)), 0.1, 1.);
p = rotate(p / size, atan(v.y, v.x));
// We will use an SDF to draw over arrows
float d = sdTriangle(p, vec2(0.1, 0.5), vec2(0.1, -0.5), vec2(0.75, 0));
d = min(d, sdBox(p - vec2(-0.2, 0), vec2(0.3, 0.1)));
return d <= 0.;
}
vec2 rotate(vec2 p, float a) {
float s = sin(a);
float c = cos(a);
mat2 m = mat2(c, -s, s, c);
return m * p;
}
// From https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
float s = sign( e0.x*e2.y - e0.y*e2.x );
vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
return -sqrt(d.x)*sign(d.y);
}
float sdBox( in vec2 p, in vec2 b )
{
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}
Recall that in GLSL we must add function headers at the top to avoid compilation errors.
bool inArrow(void);
vec2 rotate(vec2 p, float a);
float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 );
float sdBox( in vec2 p, in vec2 b );
In your main
function, then, you'll want to add a check at the beginning to draw your arrow
if(inArrow()){
fragColor = vec4(1., 0., 0., 1.);
return;
}
// The normal render shader code should follow here...
This will create an output like this: