© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
M. Cywiak, D. CywiakMulti-Platform Graphics Programming with Kivyhttps://doi.org/10.1007/978-1-4842-7113-1_6

6. Stereoscopic 3D Programming

Moisés Cywiak1   and David Cywiak2
(1)
Leon, Guanajuato, Mexico
(2)
Queretaro, Mexico
 

In this chapter, we describe the basic programming tools needed to provide stereoscopic 3D views of the polygons.

6.1 Basics of a Stereoscopic Scene

To produce stereoscopic 3D views of polygons, we create two images of the three-dimensional objects. The first image is the projection of the three-dimensional polygons onto the screen, using the equations given in Chapter 4 and a point of projection with coordinates (VX1, VY1, VZ1). The second image uses a second point of projection with coordinates (VX2, VY2, VZ2), slightly shifted horizontally from the first. Note that VX2=VX1+⊗X, VY2=VY1, and VZ2=VZ1. Here, ⊗X represents a small shift.

The two images obtained will not be displayed on the screen. Instead, we store them in a memory region to be binary ORed. The resulting image will appear on the computer's screen. Therefore, this process requires us to allocate three binary images in a memory region.

To appreciate the stereoscopic effect, you need to observe the scene through colored filters, red for the left eye and cyan for the right eye. For this purpose, you can construct glasses made with red and cyan cellophane films, which are commercially available, or you can acquire commercially available glasses specifically made for this purpose (often called 3D glasses). However, in both cases, we must ensure that each filter transmits only its corresponding color. In our experience, we had to add an extra film to the red filter. Additionally, we must take care of the properties of the cellophane films as we have found different transparency qualities among them.

The program will perform the required calculations and will be in charge of drawing the corresponding red and cyan images on the screen. Figure 6-1 shows a scene of the program running on an Android cell phone. As mentioned, you can see the stereoscopic effect only if you observe the scene through appropriate red-cyan filters.
../images/510726_1_En_6_Chapter/510726_1_En_6_Fig1_HTML.jpg
Figure 6-1

Program screenshot obtained from an Android cell phone. Best viewed with 3D glasses

6.2 Programming and ORing the Images

To calculate the binary OR between the left and right images, we have to import Pillow into our program, which is a specialized library for image processing. To verify if you have it installed on your computer, use the pip3 list directive . It should appear on the list. Otherwise, you can install it by typing the pip3 install Pillow command in your Terminal window.

We need three modules from Pillow, which will be available in the program by using the from PIL import Image, ImageDraw, ImageFont directive.

From the three libraries, Image will allow us to create the three images that this process requires. The second module, ImageDraw, provides tools for drawing lines. We will use straight lines to draw the edges of our polygons and to fill their faces. In our working example, we will fill only the back faces of our polygons. Finally, the last module, ImageFont, will allow us to display text.

Because the resulting image is a binary one, to be able to display it on the screen, we need to convert it into an appropriate image file. For our working example, we will use the png format. We will perform this conversion in a memory region. Therefore, we need to allocate memory and make it behave as a file. We will refer to a block of memory with this characteristic as an in-memory file. We will achieve this task using the library io, which we will incorporate into our program with the import io directive . Finally, the image stored in the in-memory file will be converted into a Kivy image to be displayed on the screen. We describe this process in detail in the following sections.

6.3 Projections

The program begins by creating the three required images by utilizing the Initialize(B, *args) function , which receives the handle B to access the widgets. Initialize(B, *args) uses the Image.new directive from PIL to create the three images, which we will name PilImage1, PilImage2, and PilImage3. The first image, PilImage1, will store the red image, while PilImage2 will store the cyan image. PilImage3 will store the result of the bitwise OR operation.

Additionally, to draw lines, we need to create two drawing instances, which we will name Draw1 and Draw2. These instances also allow us to display text.

We want to display a rounded frame like the one depicted in Figure 6-2, so the dimensions of our PIL images will be reduced by ten pixels in width and height compared to the Kivy image declared in the file.

When the program initiates, or when Button2 is pressed, the 3D Images text is displayed, as shown in Figure 6-2. To display text on the screen, we have to select one of the TrueType fonts available in Ubuntu, in the computer/usr/share/fonts/truetype folder. In this folder, we have several subfolders with names of the TrueType fonts. Copy the appropriate ttf file into your working folder. For our programs, we have selected Gargi.ttf.
../images/510726_1_En_6_Chapter/510726_1_En_6_Fig2_HTML.jpg
Figure 6-2

Program screenshot showing text

The code that corresponds to the Initialize(B, *args) function is shown in Listing 6-1.
def Initialize(B, *args):
        global W,H, XC,YC;
        global PilImage1,PilImage2, Draw1,Draw2;
        W,H=B.ids.Screen1.size;
        XC=int (W/2)
        YC=int(H/2)
        PilImage1= Image.new('RGB', (W-10, H-10),
                                     (60, 70, 30));
        Draw1 = ImageDraw.Draw(PilImage1);
        PilImage2= Image.new('RGB', (W-10, H-10),
                                     (60, 70, 30));
        Draw2 = ImageDraw.Draw(PilImage2);
        Font = ImageFont.truetype('Gargi.ttf', 70)
        Draw1.text( (30,200), "3D Images",
                    fill =(255,0,0,1), font=Font);
        Draw1.text( (50,200), "3D Images",
                    fill =(0,255,255,1), font=Font);
        ShowScene(B);
Listing 6-1

The Initialize(B, *args) Function

After creating the left and right images with their respective texts, Initialize(B, *args) calls the ShowScene(B) function . This function receives handle B to access the widgets declared in File.kv and performs the following tasks.
  1. 1.

    ShowScene(B) creates two NumPy arrays, called Array1 and Array2, resulting from the conversion of the data stored in PilImage1 and PilImage2.

     
  2. 2.

    An additional array, named Array3, is created as a result of bitwise ORing the elements of Array1 with Array2.

     
  3. 3.

    PilImage3 is created after converting Array3 into image data.

     
  4. 4.

    At this point, PilImage3 is converted into a Kivy image, following the procedure given at

    https://stackoverflow.com/questions/10762454/load-image-from-memory-in-kivy

    The procedure is as follows.
    1. a.

      A region of memory, behaving as a file, is allocated using the Memory=io.BytesIO() directive. This directive provides a memory region named Memory that can be used as a file. Now we can store our image in this memory block by means of the PilImage3.save(Memory, ext="png") directive.

       
    2. b.

      As Memory behaves like a file, it has an inherent file pointer. Each time we access it for writing or reading, this file pointer is updated. Therefore, before copying the image to the Kivy widget, it is necessary to reload the file pointer to the beginning of the file, where our data will be stored. This is accomplished by using the Memory.seek(0) directive.

       
     
  5. 5.

    We now proceed to convert the image into the png format by using the ImagePNG = CoreImage(Memory, ext="png") directive. Now, ImagePNG holds our result.

     
  6. 6.

    The final step consists of copying ImagePNG to the Kivy image widget that is declared in the File.kv file. As ImagePNG has the appropriate format, it will be displayed on the screen simply by using the B.ids.Screen1.texture = ImagePNG.texture directive.

     
Before ShowScene(B) finishes, it must free the regions of memory that were dynamically allocated to accomplish this process. This is done by means of the directives shown in Listing 6-2.
ImagePNG.remove_from_cache();
Memory.close();
PilImage3.close();
Array1=None;
Array2=None;
Array3=None.
Listing 6-2

Freeing Previously Allocated Resources

The code for ShowScene(B) is shown in Listing 6-3.
def ShowScene(B):
    Array1=np.array(PilImage1);
    Array2=np.array(PilImage2);
    Array3=Array1 | Array2;
    PilImage3=Image.fromarray(Array3);
    Memory=io.BytesIO();
    PilImage3.save(Memory, format="png");
    Memory.seek(0);
    ImagePNG=CoreImage(Memory, ext="png");
    B.ids.Screen1.texture=ImagePNG.texture;
    ImagePNG.remove_from_cache()
    Memory.close();
    PilImage3.close();
    Array1=None;
    Array2=None;
    Array3=None;
Listing 6-3

Code for the ShowScene(B) Function

As our program allows the polygons to be rotated in real-time, we have to clean the previous scene before exhibiting the new one. The ClearObjects() function performs this task. This function draws in Draw1 and Draw2 rectangles with the same sizes as PilImage1 and PilImage2 using the background colors that we previously selected. As a result, the scene will appear to have been cleaned. The code for ClearObjects() is given in Listing 6-4.
def ClearObjects():
    Draw1.rectangle( (0, 0, H-10, W-10),
                     fill=(60, 70, 30, 1) );
    Draw2.rectangle( (0, 0, H-10, W-10),
                     fill=(60, 70, 30, 1) );
Listing 6-4

Function to Clear the Screen

6.4 Polygon Structure

To place the polygon in a stereoscopic scene, you can use the same polygon structure described in Chapter 5.

A stereoscopic scene provides depth perception, which means that small changes in the Z-coordinate can cause changes in the polygon position that can drive it off-screen. Therefore, it is convenient to devise a method to help position the polygons.

To visualize this situation, let’s consider a point of projection VX1 situated on a plane of projection at a distance D from the screen, as depicted in Figure 6-3. The point X on the screen is the projection of the red point. We obtain point X by drawing a straight line that goes from VX1 through the red point up to the screen.
../images/510726_1_En_6_Chapter/510726_1_En_6_Fig3_HTML.jpg
Figure 6-3

Point X projected by the point of projection VX1. The red point, placed in the Z-axis, is in the middle of the two planes

In Figure 6-3, the Z-axis passes through the center of the screen and the projected point, X, is shifted from this center. This method consists in calculating this shift. This quantity, calculated using properties of similar triangles, corresponds to VX1. An analog situation occurs for the y-direction. We will shift the whole scene using this value.

Before shifting the scene, we have to recall the equations for projecting a three-dimensional point given by Equations (5.​1) and (5.​2). Additionally, we have to rewrite these equations appropriately because the origin (0,0) of a PIL image is in the upper-left corner, in contrast to a Kivy image in which (0,0) corresponds to the bottom-left corner. Therefore, we have to maintain Equation (5.​1) unaltered but, in Equation (5.​2), the values added to YC should now be subtracted instead. The equations that correspond to Equations (5.​1) and (5.​2) for a PIL image read as follows:
$$ {X}_p={X}_C+frac{D-{V}_Z}{D-{Z}_P-{V}_Z}left(x-{V}_X
ight)+{V}_X $$
(6.1)
$$ {Y}_p={Y}_C-frac{D-{V}_Z}{D-{Z}_P-{V}_Z}left(y-{V}_Y
ight)-{V}_Y $$
(6.2)
Examining Equations (6.1) and (6.2) suggests the following parameter:
$$ Factor=frac{D-{V}_Z}{D-{Z}_P-{V}_Z} $$
(6.3)
Now, as the red point depicted in Figure 6-3 is at $$ {Z}_P=frac{D}{2} $$, substituting this value in Equation (6.3) allows us to define the following constant factor:
$$ Factor0=frac{D-{V}_Z}{frac{D}{2}-{V}_Z} $$
(6.4)

In Equation (6.4), Factor0 âˆ— VX1 will shift the projection of the red point, or equivalently, the point (0,0,0) precisely to the center of the screen.

Now, the Z distance is measured from the screen up to the plane of projection, as depicted in Figure 6-4. Therefore, Z=0 corresponds to points at the screen plane, while Z=D corresponds to points at the plane of projection. To proceed further, let’s define a percentage value between 0 and 1, denoted as P. Then, we can position points at any distance between the screen and the plane of projection by using the formula Z=P*D. Let’s focus on one of these points, depicted in blue in Figure 6-4.
../images/510726_1_En_6_Chapter/510726_1_En_6_Fig4_HTML.jpg
Figure 6-4

Positioning an arbitrary point (blue point) to attain the same projection as the red point

As indicated in Figure 6-4, the blue point is located at a distance Z= P*D, measured from the screen. Using triangle properties:
$$ frac{Vx}{frac{D}{2}}=frac{h}{left(Past D-frac{D}{2}
ight)} $$
(6.5)
From Equation (6.5), the required shift h is given as follows:
$$ h=left(2P-1
ight) VX $$
(6.6)

Equation (6.6) specifies that, when the blue point is positioned at a vertical height h and at a distance Z=P*D from the screen, it will give the same projection X as the red point. The complete scene will be shifted by an amount equal to Factor0 âˆ— VX1, so the red point and all the points satisfying Equation (6.6) will be projected at the center of the screen.

From this description, we can now describe our method for positioning the polygons at distances Z=P*D from the screen. The method consists of positioning the polygons first at the center of the screen using Equation (6.6), and then shifting them to a desired position.

Equation (6.6) does not depend on the parameter D, which means we can experiment with different stereoscopic perspectives using different D values, without the drawback of losing the polygons from the scene.

We can now proceed to program the polygons based on this description. We will first set the coordinates of the points of projection and the value of distance D. For the working example, we chose the following values.
D=8000;
VX1=1000; VY1=1000; VZ1=0;
VX2=970; VY2=1000; VZ2=0;
Factor0 =(D-VZ1) / (D/2-VZ1);
P=0.65;
Next, we provide the polygon data. For this example, the cube polygon has edges with a length equal to 15 pixels, placed at a distance of 0.65*D pixels from the screen, and centered at (30, 40). The corresponding code is shown in Listing 6-5.
P=0.65;
LX=15; LY=15; LZ=15; # Cube dimensions
#Offsets from the origin.
#OffZ is measured starting from the screen
OffX=(2*P-1)*VX1+30; OffY=(2*P-1)*VY1+40; OffZ=P*D;
Cube=[
    5,   #Number of vertexes +1
    #Front face
    [ LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 1
    [ LX+OffX, -LY+OffY,  LZ+OffZ],    #Vertex 2
    [-LX+OffX, -LY+OffY,  LZ+OffZ],    #Vertex 3
    [-LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 4
    [ LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 1
    #Back face
    [ LX+OffX,  LY+OffY,  -LZ+OffZ],    #Vertex 1
    [ LX+OffX, -LY+OffY,  -LZ+OffZ],    #Vertex 2
    [-LX+OffX, -LY+OffY,   -LZ+OffZ],   #Vertex 3
    [-LX+OffX,  LY+OffY,   -LZ+OffZ],   #Vertex 4
    [ LX+OffX,  LY+OffY,   -LZ+OffZ],   #Vertex 1
    ##Red, green, blue color. Entries must be
    #between 0 and 1. Entry = 2*Num+1
    [0,0,1],
    #Rotation Center RC; Entry = 2*Num+2
    [OffX,OffY,OffZ]
    ];
Listing 6-5

Filling the Cube Polygon

Although they are not used here, we maintain the polygon colors in the 2*Num+1 place of the structure, as in Chapter 5, for compatibility purposes.

6.5 DrawAxes Function

The DrawAxes (P, VX, VY ,VZ, Which) function is in charge of drawing the three-dimensional axes on the screen. This function receives a parameter P, corresponding to a polygon structure, and the three coordinates of a point of projection, (VX, VY, VZ). The parameter Which is 0 for the red projection and 1 for the cyan projection. In our program, VZ is always set to 0. This variable may be used in future applications. As described, the shifting values Factor0*VX and Factor0*VY are used to position the three-dimensional origin (0,0,0) at the center of the screen.

The code for DrawAxes(VX,VY,VZ,Which) is shown in Listing 6-6.
def DrawAxes(VX,VY,VZ,Which):
    global  Draw1,Draw2,Factor0;
    Length=60;
    if (Which==0):
        r,g,b = 255, 0, 0; #red Image
        Draw=Draw1;
    else:
        r,g,b = 0, 255, 255; #blue image
        Draw=Draw2;
    Z0=D/2; #Origin of the three-dimensional axis
    Factor=(D-VZ)/(D-Z0-VZ);
    x1=XC+Factor*(0-VX)+Factor0*VX;
    y1=YC-Factor*(0-VY)-Factor0*VY;
    x2=XC+Factor*(Length-VX)+Factor0*VX;
    y2=YC-Factor*(0-VY)-Factor0*VY;
    #Drawing axis x
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
    x2=XC+Factor*(0-VX)+Factor0*VX;
    y2=YC-Factor*(Length-VY)-Factor0*VY;
    #Drawing axis y
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
    Z0=Z0+Length;
    Factor=(D-VZ)/(D-Z0-VZ);
    x2=XC+Factor*(0-VX)+Factor0*VX;
    y2=YC-Factor*(0-VY)-Factor0*VY;
    #Drawing axis z
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
Listing 6-6

The DrawAxes(VX,VY,VZ,Which) Function

The DrawEdges(P, VX, VY, VZ, Which) and FillPolygon(P, VX, VY, VZ, Which) functions are similar to their respective functions described in Chapter 5, with the addition of the Which parameter.

6.6 Points of Projection

The points of projection for the red and cyan images have horizontal coordinates VX1 and VX2 and should differ slightly. In principle, increasing the difference between VX1 and VX2 produces a better stereoscopic appearance. However, there is a maximum difference allowed. It is advisable to experiment with different values.

Listing 6-7 shows the complete code for main.py and Listing 6-8 shows the complete code for File.kv.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Line, Color
from kivy.clock import Clock
from kivy.core.image import Image as CoreImage
from PIL import Image, ImageDraw, ImageFont
import io
import os
import numpy as np
from kivy.lang import Builder
Builder.load_file(
    os.path.join(os.path.dirname(os.path.abspath(
        __file__)), 'File.kv')
                 );
#Avoid Form1 of being resizable
from kivy.config import Config
Config.set("graphics","resizable", False)
Config.set('graphics', 'width',  '480');
Config.set('graphics', 'height', '680');
#The width and height (W,H) and the
#center (XC,YC) of canvas are set by the
#function Initialize() after
#Clock.schedule_once has been executed.
Flag=False;
NUMBER=0;  #Polygon number
#============== Polygon structures =================
#The first entry corresponds to the
#number of vertexes+1.
#Following pair of entries correspond to the list
#of vertices ordered in clockwise direction.
#The first vertex is repeated as the last one.
#The next pair of entries corresponds to
#the center of rotation in screen coordinates
#The triplet before the end of the structure
#corresponds to the polygon color, red, green, blue.
D=8000;
VX1=1000; VY1=1000; VZ1=0;
VX2=970; VY2=1000; VZ2=0;
Factor0 =(D-VZ1) / (D/2-VZ1);
P=0.65;
LX=15; LY=15; LZ=15; # Cube dimensions
#Offsets from the origin.
#OffZ is measured starting from the screen
OffX=(2*P-1)*VX1+30; OffY=(2*P-1)*VY1+40; OffZ=P*D;
Cube=[
    5,   #Number of vertexes +1
    #Front face
    [ LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 1
    [ LX+OffX, -LY+OffY,  LZ+OffZ],    #Vertex 2
    [-LX+OffX, -LY+OffY,  LZ+OffZ],    #Vertex 3
    [-LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 4
    [ LX+OffX,  LY+OffY,  LZ+OffZ],    #Vertex 1
    #Back face
    [ LX+OffX,  LY+OffY,  -LZ+OffZ],    #Vertex 1
    [ LX+OffX, -LY+OffY,  -LZ+OffZ],    #Vertex 2
    [-LX+OffX, -LY+OffY,   -LZ+OffZ],   #Vertex 3
    [-LX+OffX,  LY+OffY,   -LZ+OffZ],   #Vertex 4
    [ LX+OffX,  LY+OffY,   -LZ+OffZ],   #Vertex 1
    ##Red, green, blue color. Entries must be
    #between 0 and 1. Entry = 2*Num+1
    [0,0,1],
    #Rotation Center RC; Entry = 2*Num+2
    [OffX,OffY,OffZ]
    ];
P=0.0;
L=80; LZ=20;  # Pentagon dimensions
#Offsets from the origin. OffZ measured starting at the screen
OffX=(2*P-1)*VX1-90; OffY=(2*P-1)*VY1-110; OffZ=P*D;
Pentagon=[
        6,  #Number of vertexes +1
        #Front face
        #Vertex 1
        [ 1*L+OffX,       0*L+OffY,     LZ+OffZ],
        #Vertex 2
        [ 0.309*L+OffX,  0.95*L+OffY,    LZ+OffZ],
        #Vertex 3
        [ -0.809*L+OffX,  0.58*L+OffY,  LZ+OffZ],
        #Vertex 4
        [ -0.809*L+OffX,  -0.58*L+OffY, LZ+OffZ],
        #Vertex 5
        [ 0.309*L+OffX,  -0.95*L+OffY,  LZ+OffZ],
        #Vertex 1, repeated
        [ 1*L+OffX,       0*L+OffY,     LZ+OffZ],
        #Back face
        #Vertex 1
        [ 1*L+OffX,       0*L+OffY,     -LZ+OffZ],
        #Vertex 2
        [ 0.309*L+OffX,  0.95*L+OffY,    -LZ+OffZ],
        #Vertex 3
        [ -0.809*L+OffX,  0.58*L+OffY,  -LZ+OffZ],
        #Vertex 4
        [ -0.809*L+OffX,  -0.58*L+OffY, -LZ+OffZ],
        #Vertex 5
        [ 0.309*L+OffX,  -0.95*L+OffY,  -LZ+OffZ],
        #Vertex 1, repeated
        [ 1*L+OffX,       0*L+OffY,     -LZ+OffZ],
        #Red, green, blue color. Entries must
        #be between 0 and 1. Entry = 2*Num+1
        [1,0,0],
        #Rotation Center RC, Entry = 2*Num+2
        [OffX,OffY,OffZ]
        ];
#---------------------------------------------------
def DrawAxes(VX,VY,VZ,Which):
    global  Draw1,Draw2,Factor0;
    Length=60;
    if (Which==0):
        r,g,b = 255, 0, 0; #red Image
        Draw=Draw1;
    else:
        r,g,b = 0, 255, 255; #blue image
        Draw=Draw2;
    Z0=D/2; #Origin of the three-dimensional axis
    Factor=(D-VZ)/(D-Z0-VZ);
    x1=XC+Factor*(0-VX)+Factor0*VX;
    y1=YC-Factor*(0-VY)-Factor0*VY;
    x2=XC+Factor*(Length-VX)+Factor0*VX;
    y2=YC-Factor*(0-VY)-Factor0*VY;
    #Drawing axis x
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
    x2=XC+Factor*(0-VX)+Factor0*VX;
    y2=YC-Factor*(Length-VY)-Factor0*VY;
    #Drawing axis y
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
    Z0=Z0+Length;
    Factor=(D-VZ)/(D-Z0-VZ);
    x2=XC+Factor*(0-VX)+Factor0*VX;
    y2=YC-Factor*(0-VY)-Factor0*VY;
    #Drawing axis z
    Draw.line( (x1,y1,x2,y2),fill=(r,g,b),width=3 );
def DrawEdges(P, VX, VY, VZ, Which):
    Num=P[0]; #Reading number of face edges +1
    if (Which==0):
        r,g,b =255, 0, 0; #red Image
        Draw=Draw1
    else:
        r,g,b = 0, 255 ,255;  #cyan image
        Draw=Draw2
    for n in range(1,Num): #Drawing front edges
        Factor=(D-VZ)/(D-P[n][2]-VZ);
        x1=XC+Factor*(P[n][0]-VX)+Factor0*VX;
        y1=YC-Factor*(P[n][1]-VY)-Factor0*VY;
        Factor=(D-VZ)/(D-P[n+1][2]-VZ);
        x2=XC+Factor*(P[n+1][0]-VX)+Factor0*VX;
        y2=YC-Factor*(P[n+1][1]-VY)-Factor0*VY;
        Draw.line( (x1,y1,x2,y2),fill=(r,g,b),
                                   width=5 );
    for n in range(Num+1,2*Num): #Drawing back edges
        Factor=(D-VZ)/(D-P[n][2]-VZ);
        x1=XC+Factor*(P[n][0]-VX)+Factor0*VX;
        y1=YC-Factor*(P[n][1]-VY)-Factor0*VY;
        Factor=(D-VZ)/(D-P[n+1][2]-VZ);
        x2=XC+Factor*(P[n+1][0]-VX)+Factor0*VX;
        y2=YC-Factor*(P[n+1][1]-VY)-Factor0*VY;
        Draw.line( (x1,y1,x2,y2),fill=(r,g,b),
                                   width=5 );
    #Drawing edges between back and front faces
    for n in range(1,Num):
        Factor=(D-VZ)/(D-P[n][2]-VZ);
        x1=XC+Factor*(P[n][0]-VX)+Factor0*VX;
        y1=YC-Factor*(P[n][1]-VY)-Factor0*VY;
        Factor=(D-VZ)/(D-P[Num+n][2]-VZ);
        x2=XC+Factor*(P[Num+n][0]-VX)+Factor0*VX;
        y2=YC-Factor*(P[Num+n][1]-VY)-Factor0*VY;
        Draw.line( (x1,y1,x2,y2),  fill=(r,g,b),
                                   width=5 );
#---------------------------------------------------
def Rotate(P, Sense):
    global p,q;
    if Sense==-1:
        Teta=np.pi/180*(-5);
    else:
        Teta=np.pi/180*(5);
    Cos_Teta=np.cos(Teta)
    Sin_Teta=np.sin(Teta);
    Num=P[0];
    # Center of rotation
    RCp=P[2*Num+2][p]; RCq=P[2*Num+2][q];
    #Rotating front face
    for n in range(1,Num+1):
        Xp=(P[n][p]-RCp)*Cos_Teta
            +(P[n][q]-RCq)*Sin_Teta +RCp;
        Xq=-(P[n][p]-RCp)*Sin_Teta
            +(P[n][q]-RCq)*Cos_Teta +RCq;
        P[n][p]=Xp;
        P[n][q]=Xq;
    #Rotating Back face
    for n in range(Num+1,2*Num+1):
        Xp=(P[n][p]-RCp)*Cos_Teta
            +  (P[n][q]-RCq)*Sin_Teta +RCp;
        Xq=-(P[n][p]-RCp)*Sin_Teta
            +  (P[n][q]-RCq)*Cos_Teta +RCq;
        P[n][p]=Xp;
        P[n][q]=Xq;
#---------------------------------------------------
#Function for filling the back polygon
#face, line by line
#Array to store intersection points
Intersect=np.zeros(30);
def FillPolygon(P, VX,VY,VZ,Which):
    Num=P[0]; #Reading number of face edges +1
    if (Which==0):
        r,g,b = 200, 0, 0; #red Image
        Draw=Draw1
    else:
        r,g,b= 0, 200, 200; #cyan image
        Draw=Draw2
    #Calculating polygon YMin, Ymax for
    #limiting number of line scans
    Factor=(D-VZ)/(D-P[Num+1][2]-VZ);
    YMin=YC-Factor*(P[Num+1][1]-VY)-VY;
    YMax=YMin;
    for n in range(Num+1,2*Num):
        Factor=(D-VZ)/(D-P[n+1][2]-VZ);
        if YMin>YC-Factor*(P[n+1][1]-VY)-Factor0*VY:
            YMin=YC-Factor*(P[n+1][1]-VY)-Factor0*VY;
        if YMax<YC-Factor*(P[n+1][1]-VY)-Factor0*VY:
            YMax=YC-Factor*(P[n+1][1]-VY)-Factor0*VY;
    #We have now (YMin, YMax)
    #We now proceed filling lines
    #between Ymin and Ymax
    for y in np.arange (YMin, YMax, 2):
        Counter=0;
        #We will search line cuts segment by segment
        for n in range(Num+1, 2*Num):
            # We first order the two vertices of
            #each segment such that Y1<Y2
            Factor1=(D-VZ)/(D-P[n][2]-VZ);
            YA=YC-Factor1*(P[n][1]-VY)-Factor0*VY;
            Factor2=(D-VZ)/(D-P[n+1][2]-VZ);
            YB=YC-Factor2*(P[n+1][1]-VY)-Factor0*VY;
            if ( YA<YB ):
                Y1=YA;
                X1=XC+Factor1*(P[n][0]-VX)
                        +Factor0*VX;
                Y2=YB;
                X2=XC+Factor2*(P[n+1][0]-VX)
                        +Factor0*VX;
            else:
                Y1=YB;
                X1=XC+Factor2*(P[n+1][0]-VX)
                        +Factor0*VX;
                Y2=YA;
                X2=XC+Factor1*(P[n][0]-VX)
                        +Factor0*VX ;
            if (Y1<=y and y<Y2):
                if (Y2-Y1)!=0:
                    M=(X2-X1)/(Y2-Y1);
                else:
                    # if Y1=Y2, the slope is infinite.
                    #We assign to M a large value
                    M=1.0e8;
                #We store the x value
                Intersect[Counter]=(y-Y1)*M+X1;
                # And we increment Counter as a new
                #value has being stored
                Counter=Counter+1;
        # Geometrical closed figure.
        #Only even number of cuts allowed
        if(Counter>1 and Counter %2 ==0):
            #Intersect1 contains ordered
            #pairs of x values
            Intersect1=np.sort(Intersect[0:Counter]);
            #We now trace lines between
            #pairs of intersections
            for n in range(0,Counter,2):
                XIntersect1=int(Intersect1[n]);
                XIntersect2=int(Intersect1[n+1]);
                Y=int(y)
                #Drawing an horizontal line
                #between x pairs
                Draw.line( (XIntersect1,
                            Y,XIntersect2,Y),
                           fill=(r,g,b), width=1 );
#---------------------------------------------------
def ShowScene(B):
    Array1=np.array(PilImage1);
    Array2=np.array(PilImage2);
    Array3=Array1 | Array2;
    PilImage3=Image.fromarray(Array3);
    Memory=io.BytesIO();
    PilImage3.save(Memory, format="png");
    Memory.seek(0);
    ImagePNG=CoreImage(Memory, ext="png");
    B.ids.Screen1.texture=ImagePNG.texture;
    ImagePNG.remove_from_cache()
    Memory.close();
    PilImage3.close();
    Array1=None;
    Array2=None;
    Array3=None;
#---------------------------------------------------
def ClearObjects():
    Draw1.rectangle( (0, 0, H-10, W-10),
                     fill=(60, 70, 30, 1) );
    Draw2.rectangle( (0, 0, H-10, W-10),
                     fill=(60, 70, 30, 1) );
class Form1(FloatLayout):
    def __init__(Handle, **kwargs):
        super(Form1, Handle).__init__(**kwargs);
        Event1=Clock.schedule_once(
                        Handle.Initialize);
        Event2=Clock.schedule_interval(
                            Handle.Temporal,0.1);
    def Initialize(B, *args):
        global W,H, XC,YC;
        global PilImage1,PilImage2, Draw1,Draw2;
        W,H=B.ids.Screen1.size;
        XC=int (W/2)
        YC=int(H/2)
        PilImage1= Image.new('RGB', (W-10, H-10),
                                     (60, 70, 30));
        Draw1 = ImageDraw.Draw(PilImage1);
        PilImage2= Image.new('RGB', (W-10, H-10),
                                     (60, 70, 30));
        Draw2 = ImageDraw.Draw(PilImage2);
        Font = ImageFont.truetype('Gargi.ttf', 70)
        Draw1.text( (30,200), "3D Images",
                    fill =(255,0,0,1), font=Font);
        Draw1.text( (50,200), "3D Images",
                    fill =(0,255,255,1), font=Font);
        ShowScene(B);
    def Temporal(B, *args):
        global Flag, NUMBER, p, q;
        if (Flag==True):
            if (B.ids.Button3.state=="down"):
                Sense=-1; p=0; q=1;
            if(B.ids.Button5.state=="down"):
                Sense=-1; p=0;q=2;
            if(B.ids.Button7.state=="down"):
                Sense=-1; p=1;q=2;
            if(B.ids.Button4.state=="down"):
                Sense=1; p=0;q=1;
            if(B.ids.Button6.state=="down"):
                Sense=1;p=0;q=2;
            if(B.ids.Button8.state=="down"):
                Sense=1;p=1;q=2;
            if(NUMBER==0):
                Rotate(Cube, Sense);
            if (NUMBER==1):
                Rotate(Pentagon, Sense);
            #  Clearing Draw1 and Draw2
            ClearObjects();
            DrawAxes(VX1,VY1,VZ1,0);
            DrawAxes(VX2,VY2,VZ2,1);
            FillPolygon(Cube,VX1,VY1,VZ1,0);
            DrawEdges(Cube,VX1,VY1,VZ1,0); #red cube
            FillPolygon(Cube,VX2,VY2,VZ2,1);
            DrawEdges(Cube,VX2,VY2,VZ2,1); #cyan cube
            #red pentagon
            FillPolygon(Pentagon,VX1,VY1,VZ1,0);
            DrawEdges(Pentagon,VX1,VY1,VZ1,0);
            #cyan pentagon
            FillPolygon(Pentagon,VX2,VY2,VZ2,1);
            DrawEdges(Pentagon,VX2,VY2,VZ2,1);
            ShowScene(B);
    def Button1_Click(B):
        global Draw1, Draw2, PilImage1, PilImage2;
        #Clearing Draw1 and Draw2
        ClearObjects();
        DrawAxes(VX1,VY1,VZ1,0);
        DrawAxes(VX2,VY2,VZ2,1);
        FillPolygon(Cube,VX1,VY1,VZ1,0);
        DrawEdges(Cube,VX1,VY1,VZ1,0); #red cube
        FillPolygon(Cube,VX2,VY2,VZ2,1);
        DrawEdges(Cube,VX2,VY2,VZ2,1); #cyan cube
        #red pentagon
        FillPolygon(Pentagon,VX1,VY1,VZ1,0);
        DrawEdges(Pentagon,VX1,VY1,VZ1,0);
        #cyan pentagon
        FillPolygon(Pentagon,VX2,VY2,VZ2,1);
        DrawEdges(Pentagon,VX2,VY2,VZ2,1);
        ShowScene(B);
    def Button2_Click(B):
        #  Clearing Draw1 and Draw2
        ClearObjects();
        Font = ImageFont.truetype('Gargi.ttf', 70)
        Draw1.text( (30,200), "3D Images",
                    fill =(255,0,0,1), font=Font);
        Draw1.text( (50,200), "3D Images",
                    fill =(0,255,255,1), font=Font);
        ShowScene(B);
    def Button3_Click(B):
        global Flag;
        Flag=True;
    def Button3_Release(B):
        global Flag;
        Flag=False;
    def Button4_Click(B):
        global Flag;
        Flag=True;
    def Button4_Release(B):
        global Flag;
        Flag=False;
    def Button5_Click(B):
        global Flag;
        Flag=True;
    def Button5_Release(B):
        global Flag;
        Flag=False;
    def Button6_Click(B):
        global Flag;
        Flag=True;
    def Button6_Release(B):
        global Flag;
        Flag=False;
    def Button7_Click(B):
        global Flag ;
        Flag=True;
    def Button7_Release(B):
        global Flag;
        Flag=False;
    def Button8_Click(B):
        global Flag;
        Flag=True;
    def Button8_Release(B):
        global Flag;
        Flag=False;
    def Button9_Click(B):
        global NUMBER;
        NUMBER=(NUMBER+1)%2;
        B.ids.Label1.text=str(NUMBER);
# This is the Start Up code.
class StartUp (App):
    def build (BU):
        BU.title="Form1"
        return Form1();
if __name__ =="__main__":
    StartUp().run();
Listing 6-7

Code Listing for main.py

#:set W 440
#:set H 440
<Form1>:
    id : Form1
    Image:
        id: Screen1
        size_hint: None,None
        pos_hint: {"x":0.04, "y":0.34}
        size: W,H
        canvas.before:
            Color:
                rgba: 0.8 ,0.8, 0.0 ,1
            RoundedRectangle:
                pos:  self.pos
                size: self.size
    Button:
        id: Button1
        on_press: Form1.Button1_Click()
        text: "Button1"
        size_hint: None,None
        pos_hint: {"x": 0.2, "y":0.03}
        size: 100,30
    Button:
        id: Button2
        on_press: Form1.Button2_Click()
        text: "Button2"
        size_hint: None,None
        pos_hint: {"x": 0.63, "y":0.03}
        size: 100,30
    Button:
        id: Button3
        on_press: Form1.Button3_Click()
        on_release: Form1.Button3_Release()
        text: "Button3"
        size_hint: None,None
        pos_hint: {"x": 0.05, "y":0.12}
        size: 100,30
        always_release: True
    Button:
        id: Button4
        on_press: Form1.Button4_Click()
        on_release:  Form1.Button4_Release()
        text: "Button4"
        size_hint: None,None
        pos_hint: {"x": 0.73, "y":0.12}
        size: 100,30
    Button:
        id: Button5
        on_press: Form1.Button5_Click()
        on_release:  Form1.Button5_Release()
        text: "Button5"
        size_hint: None,None
        pos_hint: {"x": 0.05, "y":0.20}
        size: 100,30
    Button:
        id: Button6
        on_press: Form1.Button6_Click()
        on_release:  Form1.Button6_Release()
        text: "Button6"
        size_hint: None,None
        pos_hint: {"x": 0.73, "y":0.20}
        size: 100,30
    Button:
        id: Button7
        on_press: Form1.Button7_Click()
        on_release:  Form1.Button7_Release()
        text: "Button7"
        size_hint: None,None
        pos_hint: {"x": 0.05, "y":0.28}
        size: 100,30
    Button:
        id: Button8
        on_press: Form1.Button8_Click()
        on_release:  Form1.Button8_Release()
        text: "Button8"
        size_hint: None,None
        pos_hint: {"x": 0.73, "y":0.28}
        size: 100,30
    Button:
        id: Button9
        on_press: Form1.Button9_Click()
        text: "Button9"
        size_hint: None,None
        pos_hint: {"x": 0.38, "y":0.12}
        size: 100,30
    Label:
        id: Label1
        text: "0"
        font_size: 30
        color: 1,1,0,1
        size_hint: None,None
        pos_hint: {"x": 0.38, "y":0.20}
        size: 100,30
Listing 6-8

Code Listing for File.kv

6.7 Summary

In this chapter, we programmed the polygons in 3D stereoscopic scenes. We placed and rotated them using the three-dimensional projection equations derived in previous chapters. We created two images using Pillow and performed the logical OR between them using NumPy. Finally, the resulting ORed image was converted to a Kivy image and displayed on the screen.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.239.46