Wiki

Clone wiki

Core / ElectricityShader

Back to Built-in Shader Packs


Electricity Shader

Introduction

'Electricity' is an example shader provided in Codea's 'Effects' Shaders Pack. The source of the fragment shader is one in the GLSL Sandbox gallery at glsl.heroku.com posted by Trisomie21.

The shader outputs a horizontal neon purple arcing spark against the background of a faint dark grey square grid.

Vertex shader

The vertex shader manipulates the 'attribute' variable texCoord before passing it on to the accompanying fragment shader as 'varying' variable vTexCoord:

void main()
{
    vTexCoord = vec2(texCoord.x * 2.0 - 1.0,
        texCoord.y * 2.0 - 1.0);
    ...
    gl_Position = modelViewProjection * position;
}

The effect of the manipulation is to map values of texCoord in the square region defined by the opposite corners (0, 0) and (1, 1) onto values in the square region defined by (-1, -1) and (1, 1) (with the centre at (0, 0)).

gl_Position is a variable that is intended for outputting the vertex position in homogenous co-ordinates (that is, as a vec4 value). All vertex shaders must write a value into that variable. (See Section 7.1 'Vertex Shader Special Variables' of the GLSL ES specification.) Here, the modelViewProjection 4x4 matrix is applied to the vertex's position. modelViewProjection is a 'uniform' mat4 variable supplied automatically by Codea when the shader is used with a mesh. It is the current model matrix * view matrix * projection matrix. position is a vec4 'attribute' variable, also supplied automatically by Codea from the mesh.

Fragment shader

The fragment shader makes use of two 'uniform' variables: resolution and time. The latter allows for the animation of the result of the shader.

The fragment shader sets gl_FragColor to the output of the Wave() function (with an opacity of 1.0). In input to the Wave() function is the vTexCoord scaled by the ratio of the first and second components of the 'uniform' variable resolution:

void main(void)
{
    gl_FragColor = vec4(Wave(vTexCoord * (resolution.y / resolution.x)), 1.0);
}

The Wave() function is complex. It takes as its input a vec2 value representing a position, and scales that value by s = 30.0. The function can be re-written more explicitly as:

vec3 Wave(vec2 p)
{
    float s = 30.0;
    p = p * s;
    
    // Logic for the arcing spark
    float x = floor(p.x);
    vec2 p0 = vec2(x - 0.5, Amp(x - 0.5, s));
    vec2 p1 = vec2(x + 0.5, Amp(x + 0.5, s));
    vec2 p3 = vec2(x + 1.5, Amp(x + 1.5, s));

    float d1 = segment(p, p0, p1);
    float d2 = segment(p, p1, p2);   
    float d = min(d1, d2) - 0.2;
    
    float a1 = clamp(max(d, 0.0) * s, 0.0, 1.0);
    float a0 = clamp(max(abs(d) - 0.05, 0.0) * s, 0.0, 4.0);

    // Logic for the background square grid
    vec3 pFrac = mod(p, vec2(1.0, 1.0))
    vec3 pGrid = abs(pFrac - vec2(0.5, 0.5)) - 0.01;
    float b = clamp(min(pGrid.x, pGrid.y) * resolution.x / s, 0.0, 1.0);

    vec3 colBackgroundGrid = vec3((1.0 - b) * 0.2 + 0.2) * 0.2;
    vec3 colSpark = vec3(a0 * 0.6, a0 * 0.3, a0);
    
    // Mix the spark and the background
    return vec3(mix(colBackgroundGrid, colSpark,(1.0 - a1)));
}

The Wave() function makes use of two other helper functions: Amp(x, s) and segment(P, P0, P1).

Amp(x, s)

Amp() scales down the value of its first parameter, x, by its second parameter s, clamps the result in the range -0.5 to +0.5 (using the built-in GLSL ES function clamp() - see Section 8.3 'Common Functions' of the GLSL ES specification), and translates the result to the range 0.0 to 1.0. The result is stored in variable f.

The result in f is later transformed to a quadratic factor that is scaled up by s: f * (1.0 - f) * s. This factor is 0.0 when f is 0.0 or 1.0 and reaches a maximum of 0.25 * s when f is 0.5.

The scaling factor is applied to:

fract(sin(x + time * 0.00005) * 1e5)) - 0.5

This expression yields a value between -0.5 and 0.5 based on the digits after the fifth decimal place in the result of the sin() function. The expression is used as a pseudo-random number generator.

Amp() can be re-written more explicitly as:

float Amp(float x, float s)
{
    float f1 = clamp(x / s, -0.5, 0.5) + 0.5;
    float f2 = f1 * (1.0 - f1) * s;
    float rand = fract(sin(x + time * 0.00005) * 1e5)) - 0.5;
    return rand * f2;
}

segment(P, P0, P1)

segment() calculates the intersection between the line passing through points P0 and P1 and the perpendicular line passing through point P. The intersection is clamped to the line segment from point P0 to point P1. The function then returns the distance between point P and that point.

float segment(vec2 P, vec2 P0, vec2 P1)
{
    vec2 v = P1 - P0;
    vec2 w = P - P0;
    float b = dot(w,v) / dot(v,v);
    v *= clamp(b, 0.0, 1.0);
    return length(w - v);
}

Example of use

The code below is a simple example of the use of the shader:

function setup()
    local dim = math.min(WIDTH, HEIGHT)
    parameter.integer("resX", 10, dim, 322)
    parameter.integer("resY", 10, dim, 322)
    myMesh = mesh()
    myMesh:addRect(WIDTH / 2, HEIGHT / 2, dim, dim)
    myMesh.shader = shader("Effects:Electricity")
end

function draw()
    background(0)
    myMesh.shader.time = ElapasedTime
    myMesh.shader.resolution = vec2(resX, resY)
    myMesh:draw()
end

Updated