© 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_7

7. 3D Plots Programming

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

In this chapter, we describe basic elements to create and plot functions of the form f (x, y).

As an example, Figure 7-1 shows a circular Gaussian function of the program running on an Android cell phone.
../images/510726_1_En_7_Chapter/510726_1_En_7_Fig1_HTML.jpg
Figure 7-1

Screenshot showing a 3D plot of a circular Gaussian function

7.1 Programming the Basic Operations

The buttons in Figure 7-1 are used as follows.

Button1 shows the plot and Button2 clears the screen. Button3, Button5, and Button7 rotate and show the plot in a counterclockwise direction in steps of 5 degrees in the x-y, x-z, and y-z directions, respectively. Accordingly, Button4, Button6, and Button8 rotate the plot in clockwise directions.

In general, cell phones have limited speed capacities compared with computers. As a consequence, it is advisable to carefully press one of the buttons once and wait until the corresponding step completes.

7.2 Function Overview

The program can plot basically any function f (x, y). For our example, we chose a circular Gaussian function, written as follows:
$$ fleft(x,y
ight)= Aexpleft(-frac{{left(x-{x}_0
ight)}^2+{left(y-{y}_0
ight)}^2}{{r_0}^2}
ight) $$
(7.1)

In Equation (7.1), A is the amplitude (or maximum vertical height) of the Gaussian function. The x0 and y0 parameters are used to shift the function from the origin. The r0 parameter gives the semi-width of the function.

To display the plot in the screen, we need to take into account that the z-axis is normal to the screen, whereas the x- and y-axes are parallel to the screen. This is depicted in Figure 4-1, Chapter 4. Therefore, the vertical height distribution, given by f (x, y) in Equation (7.1), must be rewritten as y(x, z). This lets the y-coordinate take the roll of the vertical distribution function and the x- and z-coordinates take the roll of the horizontal and depth axes, respectively.

7.3 Generating the Axes, the Mesh, and the Function

To represent the function, it is convenient to start selecting an appropriate number of pixels, N. When setting this value, we must keep in mind that increasing N increases the precision of the plot at the cost of increasing the processing time. Conversely, decreasing N reduces the processing time, but lowers the accuracy of the results. Since we are dealing with three-dimensional data, a slight increment of N will only slightly improve the quality of the plots, but will drastically increase the processing time. According to our experience with the circular Gaussian function case, suitable graphs and processing times can be obtained when N ranges between 40 and 70.

The x- and z-axes are generated using the following code:
D=4000; L=140;
for n in range (0,N+1):
    x[n]=(n-N/2)/N*L;
    z[n]=(n-N/2)/N*L;

In this code, L represents the width of the plot and D is the distance between the plane of projection and the screen. Note that in the for loop, the (n-N/2)/N*L equation will generate the x-axis ranging from -L/2 to L/2. In the for loop, when n equals 0, x[0] takes the value -L/2. At the other end, when n is N, x[N] takes the value L/2. Similarly, the z-axis ranges from -L/2 +D/2 to L/2 +D/2. This way, the z-axis represents an interval of length L, centered at D/2. These values will allow us to calculate the Gaussian function in the center of the screen and at a depth equal to D/2. This depth corresponds to the center point located between the plane of projection and the screen, as described in Chapter 6.

Now we proceed to calculate the two-dimensional array that corresponds to the y[n][m] function with the following code:
for n in range(0,N+1):
    for m in range(0,N+1):
        y[n][m]=np.exp(- (x[n]**2 +z[m]**2)/r0**2 );

Note from this code that the amplitude (vertical height) of the Gaussian function is 1. The function is centered at x and z equal to 0.

Finally, to plot and rotate the function, we need to create the mesh. The code shown in Listing 7-1 creates and fills the mesh.
x1=np.zeros( (N+1,N+1) );
y1=np.zeros( (N+1,N+1) );
z1=np.zeros( (N+1,N+1) ); #Mesh
Scale_x=140; Scale_y=140; Scale_z=140;
for n in range (0,N+1):
    for m in range(0,N+1)
        x1[n][m]=Scale_x * x[n];
        z1[n][m]=Scale_z * z[m]+ZG0;
        y1[n][m]=Scale_y * y[n][m];
Listing 7-1

Creating and Filling the Mesh and the Vertical Coordinate

Arrays x1[n][m], y1[n][m], and z1[n][m] compose the mesh. All the arrays have sizes equal to that of y[n][m], which means a unique function value can be assigned to each point in the x-z plane. This condition is necessary to allow us to rotate the three-dimensional set of points represented by the function at the corresponding (x, z) values.

7.4 Plotting the Function on the Screen

The three-dimensional plot consists of drawing horizontal and vertical lines around a vicinity of the center of the function in the StencilView widget declared in the Kivy file called File.kv . To reduce the processing time as much as possible, instead of drawing individual lines between pairs of points in the screen, Kivy allows us to provide a list of points to draw lines in a sequential manner. This begins with the first point listed, to the last point in the list. We will take advantage of this property by defining an array that will store N+1 (x, y) points. The code to create this array is as follows:
#Array to store list of points
PointList=np.zeros( (N+1,2) );

To plot the function, we need values for D, VX, VY, VZ, and N as well as x1, y, and z1.

We use the following:
D=4000; VX=600; VY=1200; VZ=0;
Factor0 =(D-VZ) / (D/2-VZ);
N=40;

The Factor0 parameter is used to position the function in the center of the screen.

Before placing the plot, we need to clear the screen. For this task, we use the following code:
B.ids.Screen1.canvas.clear(); #Clears the screen.
To plot the function, we use Equations (6.​1), (6.​2), and (6.​3) to calculate each point in the screen, first in the horizontal direction and then in the vertical direction. Each calculation is stored in the array. The code for recording the horizontal lines in the PointList array is shown in Listing 7-2.
for n in range (0,N+1):#Recording horizontal lines
        for m in range (0,N+1):
            Factor=(D-VZ)/(D-z1[n][m]-VZ);
            xA=XC+Factor*(x1[n][m]-VX)+Factor0*VX;
            yA=YC+Factor*(y[n][m]-VY)+Factor0*VY;
            PointList[m]=xA,yA;
Listing 7-2

Code for Recording the Horizontal Lines in the PointList Array

Once a complete horizontal list of points is stored in the PointList array, the corresponding line is plotted by means of the following directive:
B.ids.Screen1.canvas.add(Line(points=
                         PointList.tolist(), width=1.3));

This process is repeated for the vertical lines.

GraphFunction(B) is in charge of drawing the plot. The parameter B is a handle to give us access to the widgets defined in File.kv.

The code that corresponds to GraphFunction(B)) is shown in Listing 7-3.
def GraphFunction(B):
    global x1,y, z1, N, Step;
    B.ids.Screen1.canvas.clear(); #Clear the screen
    B.ids.Screen1.canvas.add( Color(1,0,0) ); #color
    for n in range (0,N+1):#Drawing horizontal lines
        for m in range (0,N+1):
            Factor=(D-VZ)/(D-z1[n][m]-VZ);
            xA=XC+Factor*(x1[n][m]-VX)+Factor0*VX;
            yA=YC+Factor*(y[n][m]-VY)+Factor0*VY;
            PointList[m]=xA,yA;
        B.ids.Screen1.canvas.add( Line(points=
                    PointList.tolist(), width=1.3));
    for n in range (0,N+1): #Drawing vertical lines
        for m in range (0,N+1, 1):
            Factor=(D-VZ)/(D-z1[m][n]-VZ);
            xA=XC+Factor*(x1[m][n]-VX)+Factor0*VX;
            yA=YC+Factor*(y[m][n]-VY)+Factor0*VY;
            PointList[m]=xA,yA;
        B.ids.Screen1.canvas.add( Line(points=
                    PointList.tolist(), width=1.3));
Listing 7-3

Code for GraphFunction(B)

7.5 Rotating the Plot

Rotating the plot is performed by using Equations (4.​26) and (4.​27) for each direction of rotation. RotateFunction(B,Sense) receives the handle B to access the widgets and the parameter Sense, which takes the values 0 or 1 to indicate the sense of rotation. At each rotation, the code verifies which button is pressed and the requested sense of rotation. The code for RotateFunction(B,Sense) is shown in Listing 7-4.
def RotateFunction(B, Sense):
    global p,q, x1, y, z1, D, N;
    if Sense==-1:
        Teta=np.pi/180*(-4.0);
    else:
        Teta=np.pi/180*(4.0);
    Cos_Teta=np.cos(Teta)
    Sin_Teta=np.sin(Teta);
    X0=0;  Y0=0;  Z0=D/2 # Center of rotation
    for n in range(0,N+1):
        for m in range(0,N+1):
            if (B.ids.Button3.state=="down" or
                      B.ids.Button4.state=="down"):
                yP=(y[n][m]-Y0)*Cos_Teta
                    + (x1[n][m]-X0)*Sin_Teta + Y0;
                xP=-(y[n][m]-Y0)*Sin_Teta
                    +(x1[n][m]-X0)*Cos_Teta + X0;
                y[n][m]=yP;
                x1[n][m]=xP;
            if (B.ids.Button5.state=="down" or
                      B.ids.Button6.state=="down"):
                yP=(y[n][m]-Y0)*Cos_Teta
                    + (z1[n][m]-Z0)*Sin_Teta + Y0;
                zP=-(y[n][m]-Y0)*Sin_Teta
                    +(z1[n][m]-Z0)*Cos_Teta + Z0;
                y[n][m]=yP;
                z1[n][m]=zP;
            if (B.ids.Button7.state=="down" or
                    B.ids.Button8.state=="down"):
                xP=(x1[n][m]-X0)*Cos_Teta
                    + (z1[n][m]-Z0)*Sin_Teta + X0;
                zP=-(x1[n][m]-X0)*Sin_Teta
                    +(z1[n][m]-Z0)*Cos_Teta + Z0;
                x1[n][m]=xP;
                z1[n][m]=zP;
Listing 7-4

Code for RotateFunction(B, Sense)

The complete code listings for the main.py and File.kv files are shown in Listings 7-5 and 7-6, respectively.
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Line, Ellipse, Color
from kivy.clock import Clock
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');
#These values are adjusted by the function
#Initialize() after Clock.schedule_once has
#been executed.(XC,YC) = Canvas center,
#(W, H)= width, height of canvas
D=4000;
VX=600; VY=1200; VZ=0;
Factor0 =(D-VZ) / (D/2-VZ);
N=40;
#Coordinates arrays
x=np.zeros(N+1);
z=np.zeros(N+1); y=np.zeros( (N+1,N+1) );
#  Mesh
x1=np.zeros( (N+1,N+1) ); z1=np.zeros( (N+1,N+1) );
L=140;  r0=L/5;
for n in range (0,N+1):
    x[n]=(n-N/2)/N*L;
    z[n]=(n-N/2)/N*L+D/2;
for n in range(0,N+1):
    for m in range(0,N+1): #Create Gaussian function
        y[n][m]=120*np.exp(- ((x[n]-0)**2
                            +(z[m]-D/2)**2)/r0**2 );
for n in range (0,N+1):
    for m in range(0,N+1):#Filling Mesh
        x1[n][m]=x[n];
        z1[n][m]=z[m]
PointList=np.zeros( (N+1,2) ); #Array to store list of points
def GraphFunction(B):
    global x1,y, z1, N, Step;
    B.ids.Screen1.canvas.clear(); #Clear the screen
    B.ids.Screen1.canvas.add( Color(1,0,0) ); #color
    for n in range (0,N+1):#Drawing horizontal lines
        for m in range (0,N+1):
            Factor=(D-VZ)/(D-z1[n][m]-VZ);
            xA=XC+Factor*(x1[n][m]-VX)+Factor0*VX;
            yA=YC+Factor*(y[n][m]-VY)+Factor0*VY;
            PointList[m]=xA,yA;
        B.ids.Screen1.canvas.add( Line(points=
                    PointList.tolist(), width=1.3));
    for n in range (0,N+1): #Drawing vertical lines
        for m in range (0,N+1, 1):
            Factor=(D-VZ)/(D-z1[m][n]-VZ);
            xA=XC+Factor*(x1[m][n]-VX)+Factor0*VX;
            yA=YC+Factor*(y[m][n]-VY)+Factor0*VY;
            PointList[m]=xA,yA;
        B.ids.Screen1.canvas.add( Line(points=
                    PointList.tolist(), width=1.3));
def RotateFunction(B, Sense):
    global p,q, x1, y, z1, D, N;
    if Sense==-1:
        Teta=np.pi/180*(-4.0);
    else:
        Teta=np.pi/180*(4.0);
    Cos_Teta=np.cos(Teta)
    Sin_Teta=np.sin(Teta);
    X0=0;  Y0=0;  Z0=D/2 # Center of rotation
    for n in range(0,N+1):
        for m in range(0,N+1):
            if (B.ids.Button3.state=="down" or
                      B.ids.Button4.state=="down"):
                yP=(y[n][m]-Y0)*Cos_Teta
                    + (x1[n][m]-X0)*Sin_Teta + Y0;
                xP=-(y[n][m]-Y0)*Sin_Teta
                    +(x1[n][m]-X0)*Cos_Teta + X0;
                y[n][m]=yP;
                x1[n][m]=xP;
            if (B.ids.Button5.state=="down" or
                      B.ids.Button6.state=="down"):
                yP=(y[n][m]-Y0)*Cos_Teta
                    + (z1[n][m]-Z0)*Sin_Teta + Y0;
                zP=-(y[n][m]-Y0)*Sin_Teta
                    +(z1[n][m]-Z0)*Cos_Teta + Z0;
                y[n][m]=yP;
                z1[n][m]=zP;
            if (B.ids.Button7.state=="down" or
                    B.ids.Button8.state=="down"):
                xP=(x1[n][m]-X0)*Cos_Teta
                    + (z1[n][m]-Z0)*Sin_Teta + X0;
                zP=-(x1[n][m]-X0)*Sin_Teta
                    +(z1[n][m]-Z0)*Cos_Teta + Z0;
                x1[n][m]=xP;
                z1[n][m]=zP;
class Form1(FloatLayout):
    def __init__(Handle, **kwargs):
        super(Form1, Handle).__init__(**kwargs);
        Event1=Clock.schedule_once(
                            Handle.Initialize);
    def Initialize(B, *args):
        global W,H, XC,YC;
        W,H=B.ids.Screen1.size;
        XI,YI=B.ids.Screen1.pos;
        XC=XI+int (W/2);
        YC=YI+int(H/2)-50;
    def Button1_Click(B):
        GraphFunction(B);
    def Button2_Click(B):
        B.ids.Screen1.canvas.clear();
    def Button3_Click(B):
        RotateFunction(B,1),
        GraphFunction(B);
    def Button4_Click(B):
        RotateFunction(B,-1),
        GraphFunction(B);
    def Button5_Click(B):
        RotateFunction(B,-1),
        GraphFunction(B);
    def Button6_Click(B):
        RotateFunction(B,1 ),
        GraphFunction(B);
    def Button7_Click(B):
        RotateFunction(B,-1),
        GraphFunction(B);
    def Button8_Click(B):
        RotateFunction(B,1),
        GraphFunction(B);
# This is the Start Up code.
class StartUp (App):
    def build (BU):
        BU.title="Form1"
        return Form1();
if __name__ =="__main__":
    StartUp().run();
Listing 7-5

Code for the main.py File

#:set W 440
#:set H 440
<Form1>:
    id : Form1
    StencilView:
        id: Screen1
        size_hint: None,None
        pos_hint: {"x":0.04, "y":0.34}
        size: W,H
        canvas.before:
            Color:
                rgba: 0.9, 0.9, 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()
        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()
        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()
        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()
        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()
        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()
        text: "Button8"
        size_hint: None,None
        pos_hint: {"x": 0.73, "y":0.28}
        size: 100,30
Listing 7-6

Code for the File.kv File

As mentioned, this program can be used to plot distinct formulas. For example, instead of the Gaussian function, which is an even function, multiplying by x gives the following:
$$ fleft(x,y
ight)=A x mathit{exp}left(-frac{x^2+{y}^2}{{r_0}^2}
ight) $$
(7.2)
Equation (7.2) is an odd function. The y[n][m] array to plot this function is filled, as shown in Listing 7-7.
Amplitude =8;
for n in range(0,N+1):
    for m in range(0,N+1):
        y[n][m]=Amplitude*x[n]*np.exp(  - (x[n]**2 +z[m]**2)   )/(r0**2 );
Listing 7-7

Code for Filling y[n][m]

The corresponding plot obtained with this program is shown in Figure 7-2.
../images/510726_1_En_7_Chapter/510726_1_En_7_Fig2_HTML.jpg
Figure 7-2

Plot of a Gaussian function multiplied by x, obtained using this program

7.6 Summary

In this chapter, we described basic elements to create and plot functions of the form f (x, y). You learned how to generate the axes, the mesh, and the functions. You also learned how to rotate them and display them 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