1
\$\begingroup\$

I'm writing a renderer for an old DOS game in which 3D polygons were rendered in software. I'd like to simulate, using a shader, the following material/effect, used for filling a triangle:

enter image description here

The fist pixel of each scanline starts with a given color (here purple) and progressively fades to another color (amber). It's like each scanline is a gradient

In a shader, is it possible to have a value between 0 and 1 which tell the position of the pixel regarding the scanline bounds? (start and end, which are S0 and S1 in the example)

\$\endgroup\$

2 Answers 2

4
+50
\$\begingroup\$

First you need to generate a set of Barycentric coordinates for your triangle.

You can set the vertex color of each triangles to the barycentric coordinates: (1,0,0), (0,1,0) and (0,0,1) (for each triangle each vertex must be set to a different barycentric coordinate, the order doesn't matter).

You can also store them in the UVs and generate z using:

o.barycentric.xy = i.uv.xy; 
o.barycentric.z = 1 - i.uv.x - i.uv.y;

in the vertex shader, or store them inside the normals if you're not going to use any shading.

Then it's a question of calculating the shortest intersection on the x axis slope using the ddx Cg function, in both directions. Then use the total distance (add both directions) to calculate the interpolation value and find where we are on the X axis according to the closest triangle edges on either side.

float ci = negative_distance / max(1.0, negative_distance + positive_distance);

col.xyz = lerp(_Color1, _Color2, ci); 

Result: Screenshot

The Unity shader:

I'm generating fake barycentric coordinates using the UVs from Unity's default cube shape for testing purpose.

Replace the part that fills in o.barycentric with the actual barycentric coordinates according to the way you choose to store those values into your mesh (uv, 1 color, or the normal). The Unity default cube happens to have UVs that can be converted.

This will depend on what other values you need to pass to your shader.

Shader "Unlit/GradientTest2"
{
    Properties
    {
        _Color1 ("Color1", Color) = (1.0, 0.5, 0, 1)
        _Color2 ("Color2", Color) = (0.5, 0, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float3 barycentric : TEXCOORD1;
            };

            fixed4 _Color1;
            fixed4 _Color2;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                // generate fake barycentric coordinates from Unity's default Cube mesh UV
                // In a real application you should set and use the vertex color as the barycentric coordinates instead
                o.barycentric = float3(1-v.uv.x, v.uv.y, abs(1.0 - (1-v.uv.x + v.uv.y)));
                float3 tmp = (o.barycentric - float3(1, 1, 1));
                if( dot(tmp, tmp) == 0){
                    o.barycentric = float3(0, 0, 1);
                }

                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                col.a = 1.0;

                float3 b = i.barycentric;
                float3 x = ddx(b);

                float negative_distance = 1000000.0f;
                if(x.x < 0){
                    negative_distance = min(negative_distance, b.x / -x.x); // pixel distance;
                } 
                if(x.y < 0){
                    negative_distance = min(negative_distance, b.y / -x.y); // pixel distance;
                } 
                if(x.z < 0){
                    negative_distance = min(negative_distance, b.z / -x.z); // pixel distance;
                } 

                float positive_distance = 1000000.0f;
                if(x.x > 0){
                    positive_distance = min(positive_distance, b.x / x.x); // pixel distance;
                } 
                if(x.y > 0){
                    positive_distance = min(positive_distance, b.y / x.y); // pixel distance;
                } 
                if(x.z > 0){
                    positive_distance = min(positive_distance, b.z / x.z); // pixel distance;
                }

                float ci = negative_distance / max(1.0, negative_distance + positive_distance);

                col.xyz = lerp(_Color1, _Color2, ci); 

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

Previous solution

EDIT: kept as this matched the example image in the Question and may be useful to someone.

For the gradient effect we only need two of the coordinates (barycentric.y, barycentric.z) so we can encode those into one of the UV coordinate sets or the vertex color as (uv.x, uv.y) instead.

And one of the coordinates (1 - barycentric.z) need to be reversed in our calculation so we'll do that right inside the UVs and reverse uv.y (the top and right corners in the image).

Set the UV for your triangle to be (0,0) for the top "pinch" corner, (0,0) for the "left" side and (1, 1) for the "right" side then use this formula to calculate the color:

fixed4 col;
col.a = 1.0;
col.xyz = lerp(_Color1, _Color2, i.uv.x / max(0.001, i.uv.y));

The max operation is to avoid a division by zero.

Result gradient (UVs are marked as [x,y])

Full unity shader:

Shader "Unlit/GradientTest"
{
    Properties
    {
        _Color1 ("Color1", Color) = (1.0, 0.5, 0, 1)
        _Color2 ("Color2", Color) = (0.5, 0, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            fixed4 _Color1;
            fixed4 _Color2;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                col.a = 1.0;
                col.xyz = lerp(_Color1, _Color2, i.uv.x / max(0.001, i.uv.y)); 

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

Bonus animated Shadertoy (it's GLSL, not Cg) used to generate this image.

The code is different from Unity shaders due to the way Shadertoy works, I had to draw a triangle inside the quad. The last few lines are the important ones:

    float divider = max(0.001, (1.0 - barycentric.b));
    vec3 col = mix(left_color, right_color, barycentric.g / divider);

Just paste this right in Shadertoy to see the shader in action:

vec3 GetBarycentric(vec3 p, vec3 a, vec3 b, vec3 c)
{
    vec3 v0 = b - a;
    vec3 v1 = c - a;
    vec3 v2 = p - a;

    vec3 v0b = c - b;
    vec3 v2b = p - b;

    float d00 = dot(v0, v0);
    float d01 = dot(v0, v1);
    float d11 = dot(v1, v1);
    float d20 = dot(v2, v0);
    float d21 = dot(v2, v1);
    float i = 1.0 / (d00 * d11 - d01 * d01);

    float d00b = dot(v0b, v0b);
    float d01b = dot(v0b, -v0);
    float d20b = dot(v2b, v0b);
    float d21b = dot(v2b, -v0);
    float ib = 1.0 / (d00b * d11 - d01b * d01b);


    vec3 r;
    r.x = (d11 * d20 - d01 * d21) * i;
    r.y = (d00 * d21 - d01 * d20) * i;
    r.z = (d00b * d21b - d01b * d20b) * ib;
    //r.z = 1.0f - r.x - r.z; // we need r.z to go negative for a quick and dirty triangle

    return r;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from -1 to 1)
    vec2 uv = (fragCoord/iResolution.xy) * 2.0 - 1.0;

    //triangle verts
    vec3 a=vec3(0.0,0.5,0.0);
    vec3 b=vec3(-0.5,-0.5,0.0);
    vec3 c=vec3(0.5,-0.5,0.0);

    {
        mat3 m;
        float si = sin(iTime);
        float co = cos(iTime);
        m[0] = vec3(co, si, 0);
        m[1] = vec3(-si, co, 0);
        m[2] = vec3(0, 0, 1);

        a *= m;
        b *= m;
        c *= m;
    }

    // Time varying pixel color
    vec3 barycentric = GetBarycentric(vec3(uv, 0), a, b, c);

    float alpha = 1.0;

    if((barycentric.x < 0.0) || (barycentric.y < 0.0) || (barycentric.z < 0.0)){
        alpha = 0.0;
    }

    vec3 left_color = vec3(0.5, 0.0, 1.0);
    vec3 right_color = vec3(1.0, 0.5, 0.0);

    float divider = max(0.001, (1.0 - barycentric.b));

    vec3 col = mix(left_color, right_color, barycentric.g / divider);

    // Output to screen
    fragColor = vec4(col * alpha, alpha);
}
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your detailed answer. It's close to the solution, but it's not exactly what I want. The gradient should be always horizontal, aligned with the scanline and not "pinched" in a corner (check OP). Here is an example. Maybe there is a way by sorting the barycentric coordinates ? \$\endgroup\$
    – tigrou
    May 20, 2018 at 20:27
  • \$\begingroup\$ @tigrou Update: Wrote a 2nd shader that generates the gradient always according to horizontal scanlines using the ddx function with new screenshot. \$\endgroup\$ May 20, 2018 at 21:02
1
\$\begingroup\$

You can take a look at this project which implements a software render.

This revision shows how to draw triangles (but filled with the same color). I modified triangle method so that it accepts 2 colors and draws gradient.

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor c1, TGAColor c2) {
    if (t0.y==t1.y && t0.y==t2.y) return; // i dont care about degenerate triangles
    if (t0.y>t1.y) std::swap(t0, t1);
    if (t0.y>t2.y) std::swap(t0, t2);
    if (t1.y>t2.y) std::swap(t1, t2);
    int total_height = t2.y-t0.y;
    for (int i=0; i<total_height; i++) {
        bool second_half = i>t1.y-t0.y || t1.y==t0.y;
        int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y;
        float alpha = (float)i/total_height;
        float beta  = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here
        Vec2i A =               t0 + (t2-t0)*alpha;
        Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta;
        if (A.x>B.x) std::swap(A, B);
        float width = B.x - A.x;
        for (int j=A.x; j<=B.x; j++) {
            float k1 = (j - A.x) / width;
            float k2 = 1 - k1;
            TGAColor color = TGAColor(c1.r * k1 + c2.r * k2, c1.g * k1 + c2.g * k2, c1.b * k1 + c2.b * k2, 255 );
            image.set(j, t0.y + i, color); // attention, due to int casts t0.y+i != A.y
        }
    }
}

Value k1 is the position of current pixel regarding the scanline bounds.

\$\endgroup\$
2
  • \$\begingroup\$ Seems like I misunderstood your question. My answer is not related to OpenGL shaders or Unity. Please disregard it. \$\endgroup\$
    – dmitry747
    Feb 11, 2016 at 7:32
  • \$\begingroup\$ Yes, my OP is related to Cg shaders (unity). However, understanding how polygons are filled in software might help writing a shader to do the same effect. \$\endgroup\$
    – tigrou
    Feb 11, 2016 at 12:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .