function ShepardZoomDemo
% ShepardZoomDemo
%
% This demo implements kind of a visual version of the Shepard tone
% illusion. This is an infinite zoom, by use of an OpenGL technique called
% "Spectral Texturing", a clever way of blending multiple textures
% with each other by use of alpha-blending...
%
% You can find more information about the technique and stimulus at:
% http://www.kyb.tuebingen.mpg.de/~berger/spectraltexturing/
%
% The "Spectral Texturing" method was published as a sketch at Siggraph 2003:
% http://doi.acm.org/10.1145/965400.965509
% It was also demonstrated at VSS demo night 2004.
%
% Feel free to use the stimulus for experiments, but please kindly tell
% Daniel Berger how you use it: daniel.berger@tuebingen.mpg.de
%
% The original code for this demo has been written by Daniel Berger in 2003.
% The code has been adapted for use with the PsychToolbox by Mario Kleiner
% in 2006.

% Make sure we're running on OpenGL Psychtoolbox, abort if not:
AssertOpenGL;

% Setup keycodes for our control keys:
KbName('UnifyKeyNames');
rotccw=KbName('LeftArrow');
rotcw=KbName('RightArrow');
decrease=KbName('DownArrow');
increase=KbName('UpArrow');
escape=KbName('ESCAPE');
toggle(1)=KbName('1!');
toggle(2)=KbName('2@');
toggle(3)=KbName('3#');
toggle(4)=KbName('4$');
toggle(5)=KbName('5%');
helptoggle=KbName('h');

% Initial direction and speed of zoom:
stepper = 1;

% Initial rotation speed:
rotspeed=-0.1;

% Shorten synctests for this demo:
oldlevel = Screen('Preference', 'SkipSyncTests', 1);

% Initialize OpenGL low-level support of Psychtoolbox:
InitializeMatlabOpenGL;

% Open onscreen window on secondary display, with a black
% background color:
screenid = max(Screen('Screens'));
[win winrect] = Screen('OpenWindow', screenid, 0);

% Query size of window. We need it to compute correct scaling for windows
% aspect ratio later on:
[width height] = Screen('WindowSize', win);

ifi=Screen('GetFlipInterval', win);
vbl=Screen('Flip', win);

% Some specs for the zoom...
showlayer =[1,1,1,1,1];
hold=0;
flattransparency=0;

% We read PNG images with color- and alpha channels:
teximage = imread([ PsychtoolboxRoot 'PsychDemos/OpenGL4MatlabDemos/snowy_rgb.png' ]);
alpha    = imread([ PsychtoolboxRoot 'PsychDemos/OpenGL4MatlabDemos/snowy_a.png' ]);

% Make sure our teximage is represented as a 3-layer RGB image, even if
% delivered by imread() as a one layer luminance image: This is a lame way
% of simplifying code later down the road...
if size(teximage,3) == 1
    teximage(:,:,2) = teximage(:,:,1);
    teximage(:,:,3) = teximage(:,:,1);
end

% Add alpha channel as 4th layer to image:
teximage(:,:,4)=alpha(:,:);

if (flattransparency)
    % For this mode, we set alpha to a constant 50%:
    teximage(:,:,4)=128;
end

% For quick texture mipmap creation, the hardware needs to support
% framebuffer object extensions...
if ~isempty(strfind(glGetString(GL.EXTENSIONS), '_framebuffer_object'))
    % Automatic, fast, hardware based creation of mipmap texture resolution
    % pyramids supported on this system. Use it to quickly create the
    % texture mipmap object:

    % Build "power of two" (pot) texture (in this case, a texture of size
    % 2^8*2^8, that is 256 pixels wide and 256 pixels high) from input image:
    tex = Screen('MakeTexture', win, teximage, [], 1);

    % Retrieve OpenGL texture handle to the power of two sized texture:
    texture=Screen('GetOpenGLTexture', win, tex);

    % Activate our own OpenGL rendering code:
    Screen('BeginOpenGL', win);
    
    % Bind the texture for use:
    glBindTexture(GL.TEXTURE_2D, texture);   %  2d texture (x and y size)

    % Ask the hardware to generate all depth levels automatically:
    glGenerateMipmapEXT(GL.TEXTURE_2D);
else
    % OpenGL extension for automatic mipmap texture creation is not
    % supported by this graphics hardware. We need to do it the manual,
    % compute intense way...
    
    % Convert Matlab matrix manually into OpenGL binary image data:
    for i=1:4
        glpixels(i,:,:) = teximage(:,:,i);
    end;
    glpixels = uint8(glpixels);
    
    % Activate our own OpenGL rendering code:
    Screen('BeginOpenGL', win);

    % Generate a texture object:
    texture = glGenTextures(1);
    
    % Bind the texture object for use as power-of-two texture:
    glBindTexture(GL.TEXTURE_2D, texture);

    % Use GLU to compute the image resolution mipmap pyramid and create
    % OpenGL textures ouf of it: This is slooow...
    r = gluBuild2DMipmaps(GL.TEXTURE_2D, 4, size(teximage,1), size(teximage,2), GL.RGBA, GL.UNSIGNED_BYTE, glpixels);
    if r>0
        error('gluBuild2DMipmaps failed for some reason.');
    end
end

% We have our texture, setup its rendering parameters:

% Setup magnification and minification filters:
glTexParameteri(GL.TEXTURE_2D,GL.TEXTURE_MAG_FILTER,GL.LINEAR); %  scale linearly when rendered image bigger than texture
if ~IsGLES
    glTexParameteri(GL.TEXTURE_2D,GL.TEXTURE_MIN_FILTER,GL.LINEAR_MIPMAP_LINEAR); %  scale linearly + mipmap when rendered image smalled than texture
else
    glTexParameteri(GL.TEXTURE_2D,GL.TEXTURE_MIN_FILTER,GL.LINEAR);
end

% Set wrapping behaviour to repeat infinite in any direction:
glTexParameteri(GL.TEXTURE_2D,GL.TEXTURE_WRAP_S,GL.REPEAT);
glTexParameteri(GL.TEXTURE_2D,GL.TEXTURE_WRAP_T,GL.REPEAT);

% Texture colors shall modulate global color value:
glTexEnvf(GL.TEXTURE_ENV, GL.TEXTURE_ENV_MODE, GL.MODULATE);

% Setup a blending equation: Playing with these can be instructive!
blendfg = 6;
blendbg = 7;

switch(blendfg)
  case 0 %  GL.ZERO
    blend_sfactor=GL.ZERO;
  case 1 %  GL.ONE
    blend_sfactor=GL.ONE;
  case 2 %  GL.DST_COLOR
    blend_sfactor=GL.DST_COLOR;
  case 3 % GL.SRC_COLOR
    blend_sfactor=GL.SRC_COLOR;
  case 4 % GL.ONE_MINUS_DST_COLOR
    blend_sfactor=GL.ONE_MINUS_DST_COLOR;
  case 5 % GL.ONE_MINUS_SRC_COLOR
    blend_sfactor=GL.ONE_MINUS_SRC_COLOR;
  case 6 % GL.SRC_ALPHA
    blend_sfactor=GL.SRC_ALPHA;
  case 7 % GL.ONE_MINUS_SRC_ALPHA
    blend_sfactor=GL.ONE_MINUS_SRC_ALPHA;
  case 8 % GL.DST_ALPHA
    blend_sfactor=GL.DST_ALPHA;
  case 9 % GL.ONE_MINUS_DST_ALPHA
    blend_sfactor=GL.ONE_MINUS_DST_ALPHA;
  case 10 % GL.SRC_ALPHA_SATURATE
    blend_sfactor=GL.SRC_ALPHA_SATURATE;
end

switch(blendbg)
  case 0 %  GL.ZERO
    blend_dfactor=GL.ZERO;
  case 1 %  GL.ONE
    blend_dfactor=GL.ONE;
  case 2 %  GL.DST_COLOR
    blend_dfactor=GL.DST_COLOR;
  case 3 % GL.SRC_COLOR
    blend_dfactor=GL.SRC_COLOR;
  case 4 % GL.ONE_MINUS_DST_COLOR
    blend_dfactor=GL.ONE_MINUS_DST_COLOR;
  case 5 % GL.ONE_MINUS_SRC_COLOR
    blend_dfactor=GL.ONE_MINUS_SRC_COLOR;
  case 6 % GL.SRC_ALPHA
    blend_dfactor=GL.SRC_ALPHA;
  case 7 % GL.ONE_MINUS_SRC_ALPHA
    blend_dfactor=GL.ONE_MINUS_SRC_ALPHA;
  case 8 % GL.DST_ALPHA
    blend_dfactor=GL.DST_ALPHA;
  case 9 % GL.ONE_MINUS_DST_ALPHA
    blend_dfactor=GL.ONE_MINUS_DST_ALPHA;
  case 10 % GL.SRC_ALPHA_SATURATE
    blend_dfactor=GL.SRC_ALPHA_SATURATE;
end

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

% Enable alpha blending:
glEnable(GL.BLEND);

% Setup perspective projection for our virtual camera, take displays
% aspect ratio into account:
glMatrixMode(GL.PROJECTION);
glLoadIdentity;
gluPerspective(63.0, width/height,0.5,2000.0);

% Setup camera position and orientation:
glMatrixMode(GL.MODELVIEW);
glLoadIdentity;
yaw=0.0;
pitch=90.0;
roll=0.0;
glRotatef(yaw, 0, 1, 0);
glRotatef(pitch, 1, 0, 0);
glRotatef(roll, 0, 0, 1);
glTranslatef(0,-50,0);

% Setup clear color and clear buffers:
clearcolor = [0.9 0.9 1 1];
glClearColor(clearcolor(1),clearcolor(2),clearcolor(3),clearcolor(4));
glClear; % Clear depth and color buffer.

% Setup initial settings for layer positions and order...
layercnt=[0,20000,40000,60000,80000];
firstlayer=0;
xo=[0.2, 0.3, 0.4, 0.5, 0.6];
yo=[0.6, 0.5, 0.4, 0.3, 0.2];

% Setup base colors of textures for different layers:
texcolor = ones(8,4);
texcolor(1,:)=[128 100 80 255];
texcolor(2,:)=[255 255 255 200];
texcolor(3,:)=[255 255 255 200];
texcolor(4,:)=[255 255 255 200];
texcolor(5,:)=[255 255 255 200];
texcolor(6,:)=[255 255 255 200];
texcolor(7,:)=[255 255 255 200];
texcolor(8,:)=[255 255 255 200];
texcolor=texcolor/255;

% Setup help text settings:
drawhelp = 1;
helptext = ['Daniel Bergers "Shepard Zoom"\n\n' ...
            'Keys and their meaning:\n\n(h) - Toggle display of this help text\n(1) to (5) - Toggle texture layer 1 to 5\n' ...
            'Left/Right Cursor keys - Rotate ccw/cw\nUp/Down Cursor keys - Zoom in/out\nESCape key - Quit demo\n'];

Screen('TextSize', win, 24);
Screen('TextStyle', win, 1);
Screen('TextColor', win, [255 0 0]);

% Setup initial rotation speed:
rotpos=0.0;
zpos=0;

% Endless animation loop. Will run until ESCape key pressed.
while 1
    % Check keyboard:
    [isdown secs keycode]=KbCheck;
    [mox, moy, buttons]=GetMouse;
    if isdown || any(buttons)
        if keycode(escape) || any(buttons)
            % Exit loop, and thereby the demo.
            break;
        end
        
        if keycode(increase)
            % Increase speed of zoom in:
            stepper=stepper+0.01;
        end
        
        if keycode(decrease)
            % Decrease speed of zoom in:
            stepper=stepper-0.01;
        end

        if keycode(rotcw)
            % Increase clockwise rotation speed:
            rotspeed = rotspeed - 0.001;
        end

        if keycode(rotccw)
            % Decrease clockwise rotation speed:
            rotspeed = rotspeed + 0.001;
        end

        if keycode(helptoggle)
            % Toggle help text display:
            drawhelp = 1 - drawhelp;

            % Debounce key:
            while KbCheck; end;
        end

        for i=1:5
            % Toggle rendering of i'th layer:
            if keycode(toggle(i))
                showlayer(i)=1-showlayer(i);
                % Debounce key:
                while KbCheck; end;
            end
        end
    end

    % Make backup of current modelview matrix, so we can restore it to
    % current state at end of frame drawing:
    glPushMatrix;

    % Render an infinite zoom into or out of the texture by using blending and
    % moving textured layers.

    % Draw lowest layer of groundplane in uniform color:
    glColor4f(texcolor(1,1), texcolor(1,2), texcolor(1,3), texcolor(1,4));
    glDisable(GL.TEXTURE_2D);
    glBlendFunc(GL.ONE,GL.ZERO);
    glBegin(GL.QUADS);
        glVertex3f(-100.0, 0.0, -100.0);     % top left
        glVertex3f(100.0, 0.0, -100.0);  % top right
        glVertex3f(100.0, 0.0, 100.0);  % bottom right
        glVertex3f(-100.0, 0.0, 100.0);  % bottom left
    glEnd;

    % All other layers are drawn with the texture applied:
    glEnable(GL.TEXTURE_2D);
    glBindTexture(GL.TEXTURE_2D, texture);
    
    % Setup proper alpha-blending:
    glBlendFunc(blend_sfactor,blend_dfactor);

    % Update our virtual position over the plane - our zoom factor:
    oz=zpos;
    zpos=zpos+stepper;
    ad=(zpos*100.0)-(oz*100.0);

    % 'stepper' defines direction and speed of zoom:
    if stepper>0
        while (zpos>7500.0) zpos=zpos-5000.0; end
    else
        while (zpos<2500.0) zpos=zpos+5000.0; end
    end

    % Rotate the plane while zooming in:
    rotpos=rotpos+rotspeed;
    if (rotpos>360.0) rotpos=rotpos-360.0; end
    if (rotpos<-360.0) rotpos=rotpos+360.0; end
    glRotatef(rotpos,0.0,1.0,0.0);

    % Update layer scalings and layer order:
    for i=0:4
        % Update layers virtual depth level:
        if (hold==0) layercnt(i+1)=layercnt(i+1) + 2; end
        layercnt(i+1)=layercnt(i+1)+ad;

        % Update depends on zoom direction 'stepper' ...
        if stepper>0
            % Has layer i reached the outer edge?
            if (layercnt(i+1)>=100000)
                % Yes. Reset its depths and make it the new first layer:
                layercnt(i+1)=0;
                firstlayer=i;
                % Randomize its texture coordinates:
                xo(i+1)=rand;
                yo(i+1)=rand;
            end
        else
            % Has layer i reached the inner edge?
            if (layercnt(i+1)<=0)
                % Yes. Reset its depths and make it the new first layer:
                layercnt(i+1)=100000;
                firstlayer=mod(i+1, 5);
                % Randomize its texture coordinates:
                xo(i+1)=rand;
                yo(i+1)=rand;
            end
        end
    end

    % Draw all five layers, starting with 'firstlayer' at the bottom:
    for j=firstlayer:firstlayer+4
        i=mod(j,5)+1;
        if (showlayer(i))
            % Draw layer with updated spatial scaling factor 'fstep':
            fstep=exp(layercnt(i)/15000.0);

            % fk is the alpha-factor premultiplied onto this layer. Its
            % used to smoothly fade in a layer after appearing and smoothly
            % fade it out before disappearing.
            
            % Default to 1 == fully visible.
            fk=1.0;

            % Use a linear ramp to fade it in or out during the first 20000
            % units or last 20000 units:
            %if (layercnt(i)<10000) fk=layercnt(i)/10000.0; end
            if (layercnt(i)<20000) fk=layercnt(i)/20000.0; end
            if (layercnt(i)>80000) fk=(99999-layercnt(i))/20000.0; end

            % Apply computed alpha value fk to layer:
            glColor4f(texcolor(i+3,1),texcolor(i+3, 2),texcolor(i+3, 3),texcolor(i+3, 4)*fk);

            % Draw it:
            glBegin(GL.QUADS);
            glTexCoord2f(0.0+xo(i), 20.0+yo(i)); glVertex3f(-40.0*fstep, 0.0, -40.0*fstep);     % top left
            glTexCoord2f(20.0+xo(i),20.0+yo(i)); glVertex3f(40.0*fstep, 0.0, -40.0*fstep);  % top right
            glTexCoord2f(20.0+xo(i), 0.0+yo(i)); glVertex3f(40.0*fstep, 0.0, 40.0*fstep);  % bottom right
            glTexCoord2f(0.0+xo(i), 0.0+yo(i)); glVertex3f(-40.0*fstep, 0.0, 40.0*fstep);  % bottom left
            glEnd;
        end
    end

    % Restore modelview matrix to pre-render state:
    glPopMatrix;
    
    % Finish our OpenGl rendering:
    Screen('EndOpenGL', win);

    % Draw help text if enabled:
    if drawhelp
        DrawFormattedText(win, helptext, 'center', 'center');
    end
    
    % Perform flip in sync with retrace.
    vbl = Screen('Flip', win, vbl + 1.5 * ifi);
    
    % Activate our own OpenGL rendering code again for drawing the next frame:
    Screen('BeginOpenGL', win);

end % Next iteration of animation loop...

% Finish our OpenGl rendering:
Screen('EndOpenGL', win);

% We're done! Close window and release all texture ressources:
sca;

% Restore synctest setting to pre-demo state:
Screen('Preference', 'SkipSyncTests', oldlevel);

% Exit.
return;
