Wiki

Clone wiki

Core / EllipseShader

Back to Built-in Shader Packs


Ellipse Shader

Introduction

'Ellipse' is an example shader provided in Codea's 'Filters' Shaders Pack.

Vertex shader

The vertex shader simply passes the 'attribute' variable texCoord on to the accompanying fragment shader as 'varying' variable vTexCoord. However, the value of texCoord is first converted from ranging between (0.0, 0.0) to (1.0, 1.0) to ranging between (-1.0, -1.0) and (1.0, 1.0):

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

As texCoord is of type vec2, and the same operations are applied to each of its components, this transformation could be rewritten more succinctly as:

vTexCoord = texCoord * 2.0 - 1.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

In Cartesian co-ordinates, the region of an ellipse centred on the origin and aligned with the axes can be expressed by the relationship:

c <= 1.0

where:

c = x * x / (r.x * r.x) + y * y / (r.y * r.y)

and where r.x and r.y are where the circumference of the ellipse intersects the x-axis and y-axis, respectively. A variable of type vec2 can be used to store these two values.

The fragment shader depends on four such vec2 'radii' of reducing amount: radius, RadiusAA, innerRadius and innerRadiusAA:

uniform highp vec2 radius;
main()
{
    highp vec2 RadiusAA = vec2(radius.x - 4.0, radius.y - 4.0);
    ...
    highp vec2 innerRadius = vec2(radius.x - strokeWidth * 2.0,
        radius.y - strokeWidth * 2.0);
    highp vec2 innerRadiusAA = vec2(radius.x - strokeWidth * 2.0 - 4.0,
        radius.y - strokeWidth * 2.0 - 4.0);
    ...
}

That is:

vec2 radius;     //= Half the dimensions of the rectangular mesh 
vec2 RadiusAA      = radius - 4.0;
vec2 innerRadius   = radius - strokeWidth * 2.0;
vec2 innerRadiusAA = radius - strokeWidth * 2.0 - 4.0;

Four values are calculated based on these vec2 'radii': c, cAA, cInner and cInnerAA:

vec2 scaledPointSq = vec2( 
    (vTexCoord.x * radius.x) * (vTexCoord.x * radius.x),
    (vTexCoord.y * radius.y) * (vTexCoord.y * radius.y));
float c = (scaledPointSq.x / (radius.x * radius.x)) +
    (scaledPointSq.y / (radius.y * radius.y));
float cAA = (scaledPointSq.x / (RadiusAA.x * RadiusAA.x)) +
    (scaledPointSq.y / (RadiusAA.y * RadiusAA.y));
float cInner = (scaledPointSq.x / (innerRadius.x * innerRadius.x)) +
    (scaledPointSq.y / (innerRadius.y * innerRadius.y));
float cInnerAA = (scaledPointSq.x / (innerRadiusAA.x * innerRadiusAA.x)) +
    (scaledPointSq.y / (innerRadiusAA.y * innerRadiusAA.y));

The colour of the fragment (gl_FragColor) is calculated based on these ratios and using the built-in function smoothstep() (see Section 8.3 'Common Functions' GLSL ES specification). The logic is clarified below by introducing variables mixInner and maxOuter:

float mixInner = smoothstep(cInner / cInnerAA, 1.0, cInner);
lowp vec4 fragCol = mix(fillColor, strokeColor, mixInner);
float mixOuter = smoothstep(c / cAA, 1.0, c); 
gl_FragColor = mix(fragCol, vec4(0, 0, 0, 0), mixOuter);

For example, if cInner is less than or equal to cInner / cInnerAA, then fragCol is set to fillColor. That condition will be true if cInnerAA is less than or equal to 1.0. If cInner is greater than or equal to 1.0, then fragCol is set to strokeColor.

Similarly, if c is less than or equal to c / cAA, then gl_FragColor is set to fragCol. That condition will be true if cAA is less than or equal to 1.0. If c is greater than or equal to 1.0, then gl_FragColor is set to vec4(0, 0, 0, 0) (transparent black).

Example of use

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

supportedOrientations(LANDSCAPE_ANY)
function setup()
    parameter.integer("w", 1, 200, 100)
    parameter.integer("h", 1, 200, 100)
    parameter.integer("widthOfStroke", 1, 30, 10)
    parameter.color("strokeColor", 255)
    parameter.color("fillColor", 255, 0, 0)
end

function draw()
    background(0)
    strokeWidth(widthOfStroke)
    stroke(strokeColor)
    fill(fillColor)
    ellipse(WIDTH / 4, HEIGHT / 2, w, h)
    myEllipse(WIDTH * 3/4, HEIGHT / 2, w, h)    
end

function myEllipse(x, y, w, h)
    local myMesh = mesh()
    myMesh:addRect(x, y, w, h)
    myMesh.shader = shader("Patterns:Ellipse")
    myMesh.shader.radius = vec2(w / 2, h / 2)
    myMesh.shader.strokeWidth = strokeWidth() / 2
    myMesh.shader.strokeColor = color(stroke())
    myMesh.shader.fillColor = color(fill())
    myMesh:draw()
end

Updated