\documentclass{article}
\title{Creating a Skybox in Python}
\author{Justin Schirle}
\date{}
\begin{document}
\maketitle{}
\begin{abstract}
Implementing a skybox into a program can be useful for adding an aesthetic appeal by creating a 3D enviroment which gives the illusion of infinite space. It can be done fairly simply in OpenGL by creating a cube which is unaffected by depth and translations, and by applying textures to each face of the cube.
\end{abstract}
\section{Loading Textures}
Before rendering the skybox, one must load the textures which will be later used. For this, and throughout, I will assume that you have the PIL Imaging library in addition to the OpenGL libraries. The first step is importing these libraries:\\
\\*
\texttt{from OpenGL.GL import *}\\
\texttt{from OpenGL.GLU import *}\\
\texttt{from OpenGL.GLUT import *}\\
\texttt{import sys}\\
\texttt{from PIL import Image}\\*

We now begin loading the images for our textures, and then binding them. This should all be done during the start of the program, so that they are only loaded once. Otherwise the program will load the images for each frame which would make the program run extremely slowly. For a typical PySkel SZG program, this is done in onWindowStartGL. The portion run in the start will be a function \texttt{loadskybox()}.\\
\\*
\texttt{def loadskybox():}\\

 All the commands will be within this function until otherwise noted. First we load our images.\\
\\*
\texttt{	im1 = Image.open("file.jpg").rotate(180).transpose(Image.FLIP\_LEFT\_RIGHT).resize((512,512))}\\
\texttt{	texture1 = im1.tostring()}\\*

We use the command \texttt{Image.open} from the PIL library to open and read the file. Be sure to specify the correct path to your file. The rotate and transpose are used here to correct the orientation of the image. If we were to simply load the image and then redisplay it, it would appear upside-down. We send the data to a string - a usable format - using \texttt{im.tostring()}. This process should be repeated for each of the 6 images that will be used for the cube faces. For the back and left faces, you should not use \texttt{transpose(Image.FLIP\_LEFT\_RIGHT)}, because our view of them from inside the cube flips them naturally. This will make more sense when you create your box. Finally, we resize the image to 512x512 pixels. These dimensions can be changed, however later it will be necessary for them to be a power of 2 (256x256, 512x512, 1024x1024,...) If a high resolution is used, it may not run on some graphics cards. This can be easily fixed by lowering the resolution.
\section{Creating Textures}
Now that the images are loaded, we must bind them to textures so that they can be used later. We begin by generating a texture:\\
\\*
\texttt{glGenTextures(1)}\\*

The 1 means that it will generate one texture. The first time \texttt{glGenTextures(1)} is used, the output will be 1. Each time after that, it will output an integer which is one greater than the previous time. So the second time it will output a value of 2. It should be noted that if \texttt{glGenTextures} is used in a Python shell, it will output a large integer. This is not how it behaves if it is used in code. Next we bind the textures:\\
\\*
\texttt{glBindTexture(GL\_TEXTURE\_2D, 1)}\\*

This binds a 2D texture to 1, the texture we generated above. For the second texture, you will use \texttt{glBindTexture(GL\_TEXTURE\_2D, 2)} and so on. Next we define the parameters of the texture:\\
\\*
\texttt{glPixelStorei(GL\_UNPACK\_ALIGNMENT, 1)}\\
\texttt{glTexParameterf(GL\_TEXTURE\_2D, GL\_TEXTURE\_WRAP\_S, GL\_CLAMP)}\\
\texttt{glTexParameterf(GL\_TEXTURE\_2D, GL\_TEXTURE\_WRAP\_T, GL\_CLAMP)}\\
\texttt{glTexParameterf(GL\_TEXTURE\_2D, GL\_TEXTURE\_MAG\_FILTER, GL\_NEAREST)}\\
\texttt{glTexParameterf(GL\_TEXTURE\_2D, GL\_TEXTURE\_MIN\_FILTER, GL\_NEAREST)}\\
\texttt{glTexImage2D(GL\_TEXTURE\_2D, 0, GL\_RGB, 512, 512, 0, GL\_RGB, GL\_UNSIGNED\_BYTE, texture1)}\\*

The \texttt{glPixelStorei} sets how the pixels will be stored for the 2D texture. The \texttt{GL\_CLAMP} means that the textures will be clamped at the edges on both the sides and top/bottom. (\texttt{GL\_TEXTURE\_WRAP\_S} and \texttt{GL\_TEXTURE\_WRAP\_T}), as opposed to having the texture repeated to fill the space, which would be accomplished with \texttt{GL\_REPEAT}. I do not really understand entirely what the final two parameters do. One could also use \texttt{GL\_LINEAR} instead of \texttt{GL\_NEAREST}, but I am not sure of the difference. Finally, the nine arguments of the glTexImage2D. The first is the target texture and should be left as \texttt{GL\_TEXTURE\_2D}. The second is the detail level, and should be left at 0. The third is the color type of the texture, in this case just \texttt{GL\_RGB}. The fourth and fifth are the width and height. These need to be a power of 2, and should be the same size as your resized images from before. The sixth argument is the border type, which must be either 0 or 1. The seventh is the pixel format of the images loaded as textures, usually \texttt{GL\_RGB}. The eighth is the data type of the image, which should be \texttt{GL\_UNSIGNED\_BYTE}. The final part of the glTexImage2D is a pointer to your image data. This is the string of image data from before, which in this case is texture1. Once you have done this for your first texture, repeat it for the other 5 textures of the skybox. The only thing that needs to be changed each time is the number in glBindTexture, and the last entry of glTexImage2D.
\section{Rendering}
Once the textures have been loaded, we are able to draw our skybox. We begin by defining a new function:\\
\\*
\texttt{def drawskybox():}\\*

Everything following is within this function. This should be called to draw the skybox, and it is important that this is done before all other objects are drawn. This is so that the skybox is in the background and does not cover other things up. For a typical PySkel SZG program, this should be called in onDraw. First, certain parameters must be changed to use the textures:\\*
\\*
\texttt{glEnable(GL\_TEXTURE\_2D)}\\
\texttt{glDisable(GL\_DEPTH\_TEST)}\\

This enables 2D textures, and turns off the depth test. This means the skybox will be unaffected by translation (but rotation still affects it). To render the skybox, we will draw 6 quadrilaterals.\\*
\\*
\texttt{glColor3f(1,1,1)}
\texttt{\# front face}\\
\texttt{glBindTexture(GL\_TEXTURE\_2D, 1)}\\
\texttt{glBegin(GL\_QUADS)}\\
\texttt{glTexCoord2f(0, 0)}\\
\texttt{glVertex3f(-10.0, -10.0, -10.0)}\\
\texttt{glTexCoord2f(1, 0)}\\
\texttt{glVertex3f(10.0, -10.0, -10.0)}\\
\texttt{glTexCoord2f(1, 1)}\\
\texttt{glVertex3f(10.0, 10.0, -10.0)}\\
\texttt{glTexCoord2f(0, 1)}\\
\texttt{glVertex3f(-10.0, 10.0, -10.0)}\\
\texttt{glEnd()}\\

We first define the color to be white. This the textures are drawn using all colors. Before drawing the quadrilaterals, we must bind the texture we want to use. In this case, we are using texture 1. \texttt{glBindTexture()} cannot be used between \texttt{glBegin()} and \texttt{glEnd()}. We then establish the coordinates of the texture using \texttt{glTexCoord2f} and the vertices. The vertices might need to be changed to fit your needs. For subsequent faces, simply bind a different texture and change the vertices. The same texture coordinates are used each time. After each of the faces has been drawn, do the following:\\
\\*
\texttt{glBindTexture(GL\_TEXTURE\_2D, 0)}\\
\texttt{glEnable(GL\_DEPTH\_TEST)}\\

The 0 texture is the null texture. If we do not bind it, all subsequent objects will be rendering with colors of the last texture used, and they will not look correct. We then enable the depth test and continue with the program.
\end{document}