function SuperShapeDemo
% Draw SuperShapes with OpenGL
%
% This demo computes and displays a 3D supershape using the formula
% found by Gielis (A generic geometric transformation that unifies
% a wide range of natural and abstract shapes. American Journal of
% Botany 90(3):333�338, 2003)
%
% See also http://en.wikipedia.org/wiki/Superformula
%
% "The parameter m relates to the number of symmetries of a shape,
% parameter n1 determines flatness and sharpness of corners and convexity
% of sides. Both parameters n2 and n3 denote whether the shape is inscribed
% or circumscribed in the unit circle." (from
% http://know-center.tugraz.at/download_extern/papers/ISGI_SuperShapes_Lex_Kienreich.pdf)
%
% You can rotate the shape with the cursor keys.
%
% You can change shape parameters by pressing the keys shown in the demo and
% entering new numeric parameters.
%
% Pressing ESCape exits the demo.
%
% Please be patient when running this demo. Computation of the supershape and
% its normal vectors and mesh is compute intense and therefore time consuming.
% The only way to speed this up a lot would be to perform most of the computations
% inside of GLSL vertex- and geometry-shaders on the GPU. This advanced exercise
% for now left to the reader.
%
% This demo was written and contributed by Joana Leitao.
%

% History:
% 16-Sep-2013   jsl Written.
% 30-Sep-2015   mk  Pimped with VR HMD support.
% 01-Jun-2017   mk  Pimped VR HMD support a bit more - Performance optimization.

if IsARM
    % Does not work on OpenGL-ES due to unsupported OpenGL display lists:
    error('Sorry, this demos does not work yet on embedded ARM devices.');
end

%% Initialization
stereoscopic = 0;

% Setup unified keynames and normalized 0-1 color space:
PsychDefaultSetup(2);

% Suppress character output to console:
ListenChar(2);

% Setup Psychtoolbox for OpenGL 3D rendering support and initialize the
% mogl OpenGL wrapper:
InitializeMatlabOpenGL;

% Find the screen to use for display:
screenid = max(Screen('Screens'));

% Define background color:
backgroundColor = GrayIndex(screenid);

% Open a double-buffered full-screen window:
PsychImaging('PrepareConfiguration');
hmd = PsychVRHMD('AutoSetupHMD', 'Tracked3DVR', 'LowPersistence TimeWarp FastResponse DebugDisplay', 0);
[win , winRect] = PsychImaging('OpenWindow', screenid, backgroundColor);

DrawFormattedText(win, 'Hang on, I am working on my first supershape :)', 'center', 'center');
Screen('Flip', win);

% Setup the OpenGL rendering context of the onscreen window for use by
% OpenGL wrapper. After this command, all following OpenGL commands will
% draw into the onscreen window 'win':
Screen('BeginOpenGL', win);

% Get the aspect ratio of the screen:
ar = RectHeight(winRect) / RectWidth(winRect);

%% Set camera position etc

% Set viewport properly:
glViewport(0, 0, RectWidth(winRect), RectHeight(winRect));

% Set projection matrix: This defines a perspective projection,
% corresponding to the model of a pin-hole camera - which is a good
% approximation of the human eye and of standard real world cameras --
% well, the best aproximation one can do with 3 lines of code ;-)
glMatrixMode(GL.PROJECTION);
glLoadIdentity;

if ~isempty(hmd)
    % Use VR HMD for 3D stereoscopic display:
    % Retrieve and set camera projection matrix for optimal rendering on the HMD:
    [projMatrix{1}, projMatrix{2}] = PsychVRHMD('GetStaticRenderParameters', hmd);

    % Enable stereo rendering:
    stereoscopic = 1;
else
    % Field of view is 25 degrees from line of sight. Objects closer than
    % 0.1 distance units or farther away than 100 distance units get clipped
    % away, aspect ratio is adapted to the monitors aspect ratio:
    gluPerspective(25, 1/ar, 0.1, 100);
end

% Setup modelview matrix: This defines the position, orientation and
% looking direction of the virtual camera:
glMatrixMode(GL.MODELVIEW);
glLoadIdentity;

% Cam is located at 3D position (0,10,15), points upright (0,1,0) and fixates
% at the origin (0,0,0) of the worlds coordinate system:
gluLookAt(0, 10, 15,0,0,0,0,1,0);

%% Setup position and emission properties of the light source:

% Turn on OpenGL local lighting model: The lighting model supported by
% OpenGL is a local Phong model with Gouraud shading.
glEnable(GL.LIGHTING);

% Enable the first local light source GL.LIGHT_0. Each OpenGL
% implementation is guaranteed to support at least 8 light sources.
glEnable(GL.LIGHT0);

% Our point lightsource is at position (x,y,z) == (1,2,3)...
glLightfv(GL.LIGHT0,GL.POSITION,[ 1 2 3 0 ]);

% % Emits white (1,1,1,1) diffuse light:
glLightfv(GL.LIGHT0,GL.DIFFUSE, [ 1 1 1 1 ]);

% There's also some white, but weak (R,G,B) = (0.1, 0.1, 0.1)
% ambient light present:
glLightfv(GL.LIGHT0,GL.AMBIENT, [0.1 0.1 0.1 1]);

%% Define Material Properties

% Define the cubes light reflection properties by setting up reflection
% coefficients for ambient, diffuse and specular reflection:
glMaterialfv(GL.FRONT,GL.AMBIENT, [ 0.0 0.0 1.0 1 ]);
glMaterialfv(GL.FRONT,GL.DIFFUSE, [ 0.1 0.1 1 1 ]);
glMaterialfv(GL.FRONT,GL.SHININESS,27.8);

%%

% Enable proper occlusion handling via depth tests:
glEnable(GL.DEPTH_TEST);

% Enable smooth gouraud shading:
glShadeModel(GL.SMOOTH);

% Set background clear color to 'gray' (R,G,B,A)=(0.5,0.5,0.5,1):
glClearColor(0.5,0.5,0.5,1);

% Clear out the backbuffer: This also cleans the depth-buffer for
% proper occlusion handling: You need to glClear the depth buffer whenever
% you redraw your scene, e.g., in an animation loop. Otherwise occlusion
% handling will screw up in funny ways...
glClear;

%% generate initial vertices
m = 8;
a = 1;
b = 1;
n1 = 0.5;
n2 = 0.5;
n3 = 0.5;

super_vertices = superformula(a,b,m,n1,n2,n3);

% predraw strip
super_list = glGenLists(1);
glNewList(super_list, GL.COMPILE);
supershape(super_vertices);
glEndList;

Screen('EndOpenGL', win);
overlaytex = updateTextOverlay(win, a,b,m,n1,n2,n3);
showhelp = 1;
abort = 0;

% Camera position when using head tracking + HMD:
camPos = [0, 0, 1];

%% Animation loop: Run until escape key is pressed
while ~abort
    if ~isempty(hmd)
        state = PsychVRHMD('PrepareRender', hmd, PsychGetPositionYawMatrix(camPos, 0));
    end

    for view = 0:stereoscopic
        Screen('SelectStereoDrawBuffer', win, view);

        Screen('BeginOpenGL', win);

        % HMD in use?
        if ~isempty(hmd)
            % Use per-eye projection matrices:
            glMatrixMode(GL.PROJECTION);
            glLoadMatrixd(projMatrix{view + 1});

            % Use per-eye modelView matrices, driven by head tracking:
            modelView = state.modelView{view + 1};
            glMatrixMode(GL.MODELVIEW);
            glLoadMatrixd(modelView);
        end

        [pressed, ~, keyCode] = KbCheck;

        if pressed

            if length(find(keyCode)) == 1

                switch find(keyCode)

                    case {KbName('LeftArrow')}
                        glRotatef(-5,0.0,1.0,0.0);

                    case {KbName('RightArrow')}
                        glRotatef(5,0.0,1.0,0.0);

                    case {KbName('UpArrow')}
                        glRotatef(-5,1.0,0.0,0.0);

                    case {KbName('DownArrow')}
                        glRotatef(5,1.0,0.0,0.0);

                    case {KbName('m'),KbName('a'),KbName('b'),KbName('j'),KbName('k'),KbName('l')}
                        % Finish OpenGL rendering into PTB window and check for OpenGL errors.
                        Screen('EndOpenGL', win);

                        if find(keyCode) == KbName('m')
                            m = GetEchoNumber(win, 'Please input a new number for parameter m followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        if find(keyCode) == KbName('a')
                            a = GetEchoNumber(win, 'Please input a new number for parameter a followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        if find(keyCode) == KbName('b')
                            b = GetEchoNumber(win, 'Please input a new number for parameter b followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        if find(keyCode) == KbName('j')
                            n1 = GetEchoNumber(win, 'Please input a new number for parameter n1 followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        if find(keyCode) == KbName('k')
                            n2 = GetEchoNumber(win, 'Please input a new number for parameter n2 followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        if find(keyCode) == KbName('l')
                            n3 = GetEchoNumber(win, 'Please input a new number for parameter n3 followed by enter', 0.05*winRect(3),0.05*winRect(4), [], backgroundColor);
                        end

                        Screen('Flip', win);
                        DrawFormattedText(win, 'Hang on, I am thinking hard about your numbers...', 'center', 'center');
                        Screen('Flip', win);

                        overlaytex = updateTextOverlay(overlaytex, a,b,m,n1,n2,n3);

                        % Switch to OpenGL rendering again for drawing of next frame:
                        Screen('BeginOpenGL', win);

                        % generate vertices
                        super_vertices = superformula(a,b,m,n1,n2,n3);
                        % predraw strip
                        super_list = glGenLists(1);
                        glNewList(super_list, GL.COMPILE);
                        supershape(super_vertices);
                        glEndList;

                    case {KbName('ESCAPE')}
                        abort = 1;

                    case {KbName('space')}
                        KbReleaseWait;
                        showhelp = ~showhelp;
                end
            end
        end

        % Clear framebuffer and redraw supershape:
        glClear;
        glCallList(super_list);

        % Finish OpenGL rendering into PTB window and check for OpenGL errors.
        Screen('EndOpenGL', win);

        Screen('FrameRect', win, 0.5, [0 0 5 5]);
    end

    if showhelp
        Screen('DrawTexture', win, overlaytex, [], []);
    end

    % Show rendered image at next vertical retrace:
    Screen('Flip', win);
end

% Delete super_list and exit OpenGL rendering:
Screen('BeginOpenGL', win);
glDeleteLists(super_list,1);
Screen('EndOpenGL', win);

% Close onscreen window and release all other ressources:
Screen('Close', overlaytex);
sca;

% Enable character output to Matlab:
ListenChar(0);

% Well done!
return

function overlaytex = updateTextOverlay(overlaytex, a,b,m,n1,n2,n3)
    if Screen('WindowKind', overlaytex) == -1
        Screen('FillRect', overlaytex, 0.5);
    else
        winRect = Screen('Rect', overlaytex);
        overlaytex = Screen('OpenOffscreenWindow', overlaytex, 0.5, [0, 0, RectWidth(winRect), 0.2 * RectHeight(winRect)]);
    end

    text1 = 'SuperShape with parameters a, b, m, j (aka n1), k (aka n2) and l (aka n3).';
    text2 = ['Current values: m = ' num2str(m) ', a = ' num2str(a) ', b = ' num2str(b) ', j = ' num2str(n1) ', k = ' num2str(n2) ', l = ' num2str(n3)];
    text3 = 'To change a parameter press the corresponding key.\nTo rotate the object use the arrows.\nTo quit the program press escape';
    text4 = 'To toggle help press space';
    fulltext = [text1 '\n' text2 '\n' text3 '\n' text4];
    DrawFormattedText(overlaytex, fulltext, 'center', 'center', 0, 80)
    %DrawFormattedText(overlaytex, text1, 0.05*winRect(3), 0.03*winRect(4));
    %DrawFormattedText(overlaytex, text2, 0.05*winRect(3), 0.06*winRect(4));
    %DrawFormattedText(overlaytex, text3, 0.05*winRect(3), 0.10*winRect(4));
return

%% this function creates quad_strips with the given input vertices
function supershape(vertices)
global GL

for row_ind = 1:size(vertices(:,:,1),1)-1

    % Begin drawing of a new quad strip:
    glBegin(GL.QUAD_STRIP);

    for column_ind = 1:size(vertices(:,:,1),2)

        for k = 1:-1:0

            aux_var = row_ind + k;

            x = vertices(aux_var,column_ind,1);
            y = vertices(aux_var,column_ind,2);
            z = vertices(aux_var,column_ind,3);

            norm_vector = calculateNormVec(vertices, aux_var, column_ind);
            glNormal3f(norm_vector(1),norm_vector(2),norm_vector(3));
            glVertex3f(x,y,z)
        end
    end

    glEnd;
end

return


%% this function creates the vertices of a 3D supershape using the formula
% found by Gielis (A generic geometric transformation that unifies
% a wide range of natural and abstract shapes. American Journal of
% Botany 90(3):333�338, 2003). See also http://en.wikipedia.org/wiki/Superformula

% "The parameter m relates to the number of symmetries of a shape,
% parameter n1 determines flatness and sharpness of corners and convexity
% of sides. Both parameters n2 and n3 denote whether the shape is inscribed
% or circumscribed in the unit circle." (from
% http://know-center.tugraz.at/download_extern/papers/ISGI_SuperShapes_Lex_Kienreich.pdf)
function [super_vertices] = superformula(a,b,m,n1,n2,n3)

increments = 100;
theta = -pi:2*pi/(increments-1):pi;
phi = -pi/2:2*pi/(increments-1):pi/2;

x = zeros(length(theta),length(phi));
y = zeros(length(theta),length(phi));
z = zeros(length(theta),length(phi));
super_vertices = zeros(length(theta),length(phi),3);

for theta_ind = 1:length(theta)

    for phi_ind = 1:length(phi)

        r_theta = (abs(1/a*cos(theta(theta_ind)* m/4)).^n2  +  abs(1/b*sin(theta(theta_ind)*m/4)).^n3)^(1/n1);
        r_phi = (abs(1/a*cos(phi(phi_ind)* m/4)).^n2  +  abs(1/b*sin(phi(phi_ind)*m/4)).^n3)^(1/n1);

        x(theta_ind,phi_ind) = r_theta*cos(theta(theta_ind))*r_phi*cos(phi(phi_ind));
        y(theta_ind,phi_ind) = r_theta*sin(theta(theta_ind))*r_phi*cos(phi(phi_ind));
        z(theta_ind,phi_ind) = r_phi*sin(phi(phi_ind));
    end;

end

super_vertices(:,:,1) = x./max(max(abs(x)));
super_vertices(:,:,2) = y./max(max(abs(y)));
super_vertices(:,:,3) = z./max(max(abs(z)));

return


%% this function creates the normal vector at a given vertex.
function [norm_vector] = calculateNormVec(vertices, row_ind, column_ind)

size_aux = vertices(:,:,1);

if column_ind == 1

    % if column_ind equals one, this mean phi equals -pi/2 and hence, that we
    % are at the buttom point of the supershape, which is oriented along the
    % z-axis. Hence the normal vector at any of the vertices with column = 1
    % equals [0 0 -1];
    norm_vector = [0 0 -1];

elseif column_ind == size(size_aux,2)

    % if column_ind equals size(size_aux,2), this mean phi equals pi/2 and
    % hence, that we are at the upper point of the supershape, which is
    % oriented along the z-axis. Hence the normal vector at any of the
    % vertices with column = size(size_aux,2) equals [0 0 1];
    norm_vector = [0 0 1];

elseif row_ind == 1 || row_ind == size(size_aux,1)

    % in these cases we are at the edge where the supershape closes
    % (basically theta equals 0 and 2*pi. To calculate the normal at these
    % the same as below is done, only that one has to consider values at
    % the end/beginning of the rows instead.
    row_aux = 1;

    norm_vector1 = cross(vertices(row_aux+1,column_ind,:)-vertices(row_aux,column_ind,:),vertices(row_aux,column_ind+1,:)-vertices(row_aux,column_ind,:));
    norm_vector1_aux = [norm_vector1(1,1,1) norm_vector1(1,1,2) norm_vector1(1,1,3)]./norm([norm_vector1(1,1,1) norm_vector1(1,1,2) norm_vector1(1,1,3)]);

    norm_vector2 = cross(vertices(row_aux,column_ind+1,:)-vertices(row_aux,column_ind,:),vertices(end-1,column_ind,:)-vertices(row_aux,column_ind,:));
    norm_vector2_aux = [norm_vector2(1,1,1) norm_vector2(1,1,2) norm_vector2(1,1,3)]./norm([norm_vector2(1,1,1) norm_vector2(1,1,2) norm_vector2(1,1,3)]);

    norm_vector3 = cross(vertices(end-1,column_ind,:)-vertices(row_aux,column_ind,:),vertices(row_aux,column_ind-1,:)-vertices(row_aux,column_ind,:));
    norm_vector3_aux = [norm_vector3(1,1,1) norm_vector3(1,1,2) norm_vector3(1,1,3)]./norm([norm_vector3(1,1,1) norm_vector3(1,1,2) norm_vector3(1,1,3)]);

    norm_vector4 = cross(vertices(row_aux,column_ind-1,:)-vertices(row_aux,column_ind,:),vertices(row_aux+1,column_ind,:)-vertices(row_aux,column_ind,:));
    norm_vector4_aux = [norm_vector4(1,1,1) norm_vector4(1,1,2) norm_vector4(1,1,3)]./norm([norm_vector4(1,1,1) norm_vector4(1,1,2) norm_vector4(1,1,3)]);

    norm_vector = sum([norm_vector1_aux; norm_vector2_aux; norm_vector3_aux; norm_vector4_aux]);

else
    % To calculate the normal at all the other vertices the normals at all
    % the four surrounding quad faces are calculated and then added up to
    % give the normal at the vertex
    norm_vector1 = cross(vertices(row_ind+1,column_ind,:)-vertices(row_ind,column_ind,:),vertices(row_ind,column_ind+1,:)-vertices(row_ind,column_ind,:));
    norm_vector1_aux = [norm_vector1(1,1,1) norm_vector1(1,1,2) norm_vector1(1,1,3)]./norm([norm_vector1(1,1,1) norm_vector1(1,1,2) norm_vector1(1,1,3)]);

    norm_vector2 = cross(vertices(row_ind,column_ind+1,:)-vertices(row_ind,column_ind,:),vertices(row_ind-1,column_ind,:)-vertices(row_ind,column_ind,:));
    norm_vector2_aux = [norm_vector2(1,1,1) norm_vector2(1,1,2) norm_vector2(1,1,3)]./norm([norm_vector2(1,1,1) norm_vector2(1,1,2) norm_vector2(1,1,3)]);

    norm_vector3 = cross(vertices(row_ind-1,column_ind,:)-vertices(row_ind,column_ind,:),vertices(row_ind,column_ind-1,:)-vertices(row_ind,column_ind,:));
    norm_vector3_aux = [norm_vector3(1,1,1) norm_vector3(1,1,2) norm_vector3(1,1,3)]./norm([norm_vector3(1,1,1) norm_vector3(1,1,2) norm_vector3(1,1,3)]);

    norm_vector4 = cross(vertices(row_ind,column_ind-1,:)-vertices(row_ind,column_ind,:),vertices(row_ind+1,column_ind,:)-vertices(row_ind,column_ind,:));
    norm_vector4_aux = [norm_vector4(1,1,1) norm_vector4(1,1,2) norm_vector4(1,1,3)]./norm([norm_vector4(1,1,1) norm_vector4(1,1,2) norm_vector4(1,1,3)]);

    norm_vector = sum([norm_vector1_aux; norm_vector2_aux; norm_vector3_aux; norm_vector4_aux]);

end

return
