#!/usr/bin/perl

=head1 NAME

tessellation.pl - Show workings of perl based tessellation

=head1 SOURCE

general ideas taken from:
http://glprogramming.com/red/chapter11.html

=head1 AUTHOR

Paul Seamons

=cut

use OpenGL qw(:all);
use strict;
use warnings;

print "Starting $0\n";

my $color_toggle = 1;
my $edge_toggle  = 1;
my $solid_toggle = 1;
my $antialias_toggle = 1;
my $defaults_toggle  = 0;
my $opaque_toggle    = 'off';
my $opaque_cycle     = 0;
my ($w, $h) = (800, 600);

main();
exit;

sub main {
    glutInit();
    glutInitWindowSize($w, $h);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
    glutCreateWindow("Tessellation");
    glClearColor (0.0, 0.0, 0.0, 0.0);

    init();

    glutDisplayFunc(\&render_scene);
    glutKeyboardFunc(sub {
        if ($_[0] == 27 || $_[0] == ord('q')) {
            exit;
        } elsif ($_[0] == ord('e')) {
            $edge_toggle = ($edge_toggle) ? 0 : 1;
        } elsif ($_[0] == ord('a')) {
            $antialias_toggle = ($antialias_toggle) ? 0 : 1;
        } elsif ($_[0] == ord('s')) {
            $solid_toggle = ($solid_toggle) ? 0 : 1;
        } elsif ($_[0] == ord('d')) {
            $defaults_toggle = ($defaults_toggle) ? 0 : 1;
        } elsif ($_[0] == ord('o')) {
            $opaque_toggle = ($opaque_toggle eq 'off') ? 'polygon_data' : ($opaque_toggle eq 'polygon_data') ? 'vertex_data' : 'off';
        } elsif ($_[0] == ord('y')) {
            $opaque_cycle++;
        } else {
            $color_toggle = ($color_toggle) ? 0 : 1;
        }
        render_scene();
    });

    print "'q' - Quit
'e' - Toggle edge flags (show triangles)
's' - Toggle solid (polygon vs lines)
'a' - Toggle anti-alias lines
'd' - Toggle perl callbacks vs default c implemented callbacks
'c' - Toggle color (perl callbacks only)
'o' - Toggle opaque data passing (off, polygon_data, vertex_data) (perl callbacks only)
'y' - Cycle the type of opaque data passed (perl callbacks only)
";
    glutMainLoop();
}

sub init {
    glViewport(0,0, $w,$h);

    glMatrixMode(GL_PROJECTION());
    glLoadIdentity();

    if ( @_ ) {
        gluPerspective(45.0,4/3,0.1,100.0);
    } else {
        glFrustum(-0.1,0.1,-0.075,0.075,0.175,100.0);
    }

    glMatrixMode(GL_MODELVIEW());
    glLoadIdentity();
}

sub render_scene {
    glClear (GL_COLOR_BUFFER_BIT);

    glLoadIdentity();
    glTranslatef(0, 0, -6);

    print "Callbacks: ".($defaults_toggle ? "c based" : '   perl')
        .", Solid: ".($solid_toggle ? ' on' : 'off')
        .", EdgeFlags: " .($edge_toggle ? ' on' : 'off')
        .", Color: " .($color_toggle ? ' on' : 'off')
        .", Anti-alias: " .($antialias_toggle ? ' on' : 'off')
        .", Opaque: $opaque_toggle"
        ."\n";

    my $tess = gluNewTess('do_color');
    my %opaque_printed;

    # ideally - these would be loaded into a call list - but this is just a sampling
    if ($defaults_toggle) {
        gluTessCallback($tess, GLU_TESS_BEGIN(),     'DEFAULT');
        gluTessCallback($tess, GLU_TESS_ERROR(),     'DEFAULT');
        gluTessCallback($tess, GLU_TESS_END(),       'DEFAULT');
        gluTessCallback($tess, GLU_TESS_VERTEX(),    'DEFAULT');
        gluTessCallback($tess, GLU_TESS_EDGE_FLAG(), 'DEFAULT') if $edge_toggle;
        gluTessCallback($tess, GLU_TESS_COMBINE(),   'DEFAULT');
    } else {
        gluTessCallback($tess, GLU_TESS_BEGIN(),     sub { glBegin(shift) });
        gluTessCallback($tess, GLU_TESS_ERROR(),     sub { my $errno = shift; my $err = gluErrorString($errno); print "got an error ($errno - $err)\n" });
        gluTessCallback($tess, GLU_TESS_END(),       sub { glEnd(); });
        gluTessCallback($tess, GLU_TESS_EDGE_FLAG(), sub { glEdgeFlag(shift) }) if $edge_toggle;

        my $type = ($opaque_toggle eq 'vertex_data') ? GLU_TESS_VERTEX() : GLU_TESS_VERTEX_DATA();
        gluTessCallback($tess, $type, sub {
            my ($x, $y, $z, $r, $g, $b, $a, $opaque) = @_;
            glColor4f($r, $g, $b, $a) if $color_toggle;
            glVertex3f($x, $y, $z);

            # the following is only a test of passing opaque polygon data or vertex data
            if ($opaque) {
                my $ref = ref($opaque) || 'SCALAR';
                my $pv = ($ref eq 'CODE')     ? $opaque->()
                       : ($ref eq 'ARRAY')    ? $opaque->[0]
                       : ($ref eq 'HASH')     ? $opaque->{'key'}
                       : ($opaque =~ /^\d+$/) ? do { $ref = 'SCALAR NUM'; chr($opaque) }
                       : $opaque;
                my $str = "Vertices were passed ".($pv eq 'p' ? 'polygon' : $pv eq 'v' ? 'vertex' : "other ($pv)")." data of type $ref\n";
                print $str if ! $opaque_printed{$str}++;
                print "We received a non-vertex data type ($pv $ref)\n" if $opaque_toggle eq 'vertex_data' && $pv ne 'v';
            }
        });

        gluTessCallback($tess, GLU_TESS_COMBINE(), sub {
            my ($x, $y, $z,
                $v0, $v1, $v2, $v3,
                $w0, $w1, $w2, $w3,
                $polygon_data) = @_; # polygon data is passed to COMBINE in addition to COMBINE_DATA

            # GLU_TESS_COMBINE and GLU_TESS_COMBINE_DATA call the same code so polygon data is always passed
            # When GLU_TESS_VERTEX is used, the two-four opaque elements passed to gluTessVertex are passed as the final element of each vector data
            #     In the GLU_TESS_VERTEX case an 8th return parameter can then be returned which can be any perl variable,
            #     which is then eventually passed as the data to the GLU_TESS_VERTEX callback.
            return (
                $x, $y, $z,
                $w0*$v0->[3] + $w1*$v1->[3] + $w2*$v2->[3] + $w3*$v3->[3],
                $w0*$v0->[4] + $w1*$v1->[4] + $w2*$v2->[4] + $w3*$v3->[4],
                $w0*$v0->[5] + $w1*$v1->[5] + $w2*$v2->[5] + $w3*$v3->[5],
                $w0*$v0->[6] + $w1*$v1->[6] + $w2*$v2->[6] + $w3*$v3->[6],
                ((@$v0 == 8 || @$v0 == 11) ? ($v0->[7] || $v1->[7] || $v2->[7] || $v3->[7]) : ()), # if we received vertex data - return some for the new vertex
                );
        });
    }

    glPolygonMode(GL_FRONT_AND_BACK, $solid_toggle ? GL_FILL : GL_LINE);

    glEnable (GL_BLEND);
    if ($antialias_toggle) {
        glEnable (GL_LINE_SMOOTH);
        glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glHint (GL_LINE_SMOOTH_HINT, GL_DONT_CARE);
        glHint (GL_POLYGON_SMOOTH_HINT, GL_DONT_CARE);
    } else {
        glDisable(GL_LINE_SMOOTH);
    }

    glColor3f(1,1,1);

    # triangle
    glPushMatrix();
    glTranslatef(-2.2, 1.2, 0);
    glScalef(.9, .9, 0);
    my $tri1 = [[0,1,0, 1,0,0,1], [-1,-1,0, 0,1,0,1], [1,-1,0, 0,0,1,1]];
    gluTessBeginPolygon($tess);
    gluTessBeginContour($tess);
    for my $q (@$tri1) {
        gluTessVertex_p($tess, @$q);
    }
    gluTessEndContour($tess);
    gluTessEndPolygon($tess);
    glPopMatrix();

    # square
    glPushMatrix();
    glTranslatef(0, 1.2, 0);
    glScalef(.9, .9, 0);
    my $quad0 = [[-1,1,0, 1,0,0,1], [1,1,0, 0,1,0,1], [1,-1,0, 0,0,1,1], [-1,-1,0, 1,1,0,1]];
    $quad0 = [reverse @$quad0];
    gluTessBeginPolygon($tess);
    gluTessBeginContour($tess);
    for my $q (@$quad0) {
        gluTessVertex_p($tess, @$q);
    }
    gluTessEndContour($tess);
    glColor3f(1,1,1);
    gluTessEndPolygon($tess);
    glPopMatrix();

    # pontiac
    glPushMatrix();
    glTranslatef(2.2, .1, 0);
    glScalef(.7, .7, 0);
    my $quad1 = [[-1,3,0, 1,0,0,1], [0,0,0, 1,1,0,1], [1,3,0, 0,0,1,1], [0,2,0, 0,1,0,1]];
    gluTessBeginPolygon($tess);
    gluTessBeginContour($tess);
    for my $q (@$quad1) {
        gluTessVertex_p($tess, @$q);
    }
    gluTessEndContour($tess);
    glColor3f(1,1,1);
    gluTessEndPolygon($tess);
    glPopMatrix();

    # window
    glPushMatrix();
    glTranslatef(-2.2, -2.1, 0);
    glScalef(.45, .45, 0);
    my $quad2 = [
        [[-2,3,0, 1,0,0,1], [-2,0,0, 1,1,0,1], [2,0,0, 0,0,1,1], [2,3,0, 0,1,0,1]],
        [[-1,2,0, 1,0,0,1], [-1,1,0, 1,1,0,1], [1,1,0, 0,0,1,1], [1,2,0, 0,1,0,1]],
        ];
    gluTessBeginPolygon($tess);
    for my $c (@$quad2) {
        gluTessBeginContour($tess);
        for my $q (@$c) {
            gluTessVertex_p($tess, @$q);
        }
        gluTessEndContour($tess);
    }
    glColor3f(1,1,1);
    gluTessEndPolygon($tess);
    glPopMatrix();

    # star
    glPushMatrix();
    glTranslatef(0, -2.1, 0);
    glScalef(.6, .6, 0);
    my $coord3 = [
        [ 0.0, 3.0, 0,  1,0,0,1],
        [-1.0, 0.0, 0,  0,1,0,1],
        [ 1.6, 1.9, 0,  1,0,1,1],
        [-1.6, 1.9, 0,  1,1,0,1],
        [ 1.0, 0.0, 0,  0,0,1,1],
        ];
    gluTessProperty($tess, GLU_TESS_WINDING_RULE(), GLU_TESS_WINDING_NONZERO());
    my @p_cycle = (sub { "p" }, ["p"], {key => "p"}, "p", ord('p'));
    my @v_cycle = (sub { "v" }, ["v"], {key => "v"}, "v", ord('v'));
    if ($opaque_toggle eq 'off') {
        gluTessBeginPolygon($tess);
    } else {
        gluTessBeginPolygon($tess, $p_cycle[$opaque_cycle % @p_cycle]);
    }
    gluTessBeginContour($tess);
    for my $q (@$coord3) {
        if ($opaque_toggle eq 'off') {
            gluTessVertex_p($tess, @$q);
        } else {
            gluTessVertex_p($tess, @$q, $v_cycle[$opaque_cycle % @v_cycle]);
        }
    }
    gluTessEndContour($tess);
    glColor3f(1,1,1);
    gluTessEndPolygon($tess);
    glPopMatrix();

    # octagon
    glPushMatrix();
    glTranslatef(2, -1.3, 0);
    glScalef(.35, .35, 0);
    my $coord4 = [
        [   -1,  2.4, 0,   1, 0, 0,1],
        [    1,  2.4, 0,   1, 1, 0,1],
        [  2.4,    1, 0,   0, 1, 0,1],
        [  2.4,   -1, 0,   0, 1, 1,1],
        [    1, -2.4, 0,   0, 0, 1,1],
        [   -1, -2.4, 0,   1, 0, 1,1],
        [ -2.4,   -1, 0,   1, 1, 1,1],
        [ -2.4,    1, 0,  .5,.5,.5,1],
        ];
    $coord4 = [reverse @$coord4];
    gluTessProperty($tess, GLU_TESS_WINDING_RULE(), GLU_TESS_WINDING_ODD());
    gluTessBeginPolygon($tess);
    gluTessBeginContour($tess);
    for my $q (@$coord4) {
        gluTessVertex_p($tess, @$q);
    }
    gluTessEndContour($tess);
    glColor3f(1,1,1);
    gluTessEndPolygon($tess);
    glPopMatrix();


    gluDeleteTess($tess);

    glutSwapBuffers();
}
