3D 移動

Jason990420發表於2020-05-02

建立日期: 2020/05/02

修訂日期: 2020/05/08 (Revised for Tool.py PEP8)

相關軟體資訊:

Win 10 Python 3.7.6 PySimpleGUI 4.18.2 PIL/Pillow 7.1.1

Tool

說明: 本文請隨意引用或更改,只須標示出處及作者,作者不保證內容絕對正確無誤,如造成任何後果,請自行負責.

前言

一個3D 移動的程式, 169 行.

內容說明

  1. 固定的遠景
  2. 馬路
  3. 分路線
  4. 路邊
  5. 路樹
  6. 其他綠地
  7. 向前移動
  8. 3D

輸出畫面

3D 移動

程式碼及說明

  1. 庫的匯入
import PySimpleGUI as sg
import math
from pathlib import Path
from Tool import read_URL, mapping
from PIL import Image
from io import BytesIO
  1. 其本引數設定: 物件長寛, 顏色及數量, 攝像頭位置, 螢幕位置,
class GUI():

    def __init__(self):
        self.boxes = 51
        self.box_height = 50
        self.x0, self.y0, self.z0 = 0, 150, -50
        self.z1 = 0
        self.gap_width = 860
        self.road_width = 800
        self.line_width = 20
        self.offset = 0.5
        self.road_color = 'grey'
        self.gap_color = 'black'
        self.line_color = 'white'
        self.figures = []
        self.count = 0
        self.Create_Window()
  1. 建立畫布物件

    def Create_Canvas(self):
        self.width = 1080
        self.height = 400
        return sg.Graph((self.width, self.height), (-self.width//2, 0),
            (self.width//2, self.height), key='Graph')
  1. 建立 GUI 視窗, 從網路上下載景及樹木的圖片, 並畫上背景
    def Create_Window(self):
        self.window = sg.Window('3D Road', layout=[[self.Create_Canvas()]],
            return_keyboard_events=True, finalize=True)
        self.draw = self.window.FindElement('Graph')
        self.Load_Background()
        self.Load_Tree()
        self.Draw_Background()
  1. 下載圖片, 並以要求的尺寸存檔
    def Download_Picture(self, url, file, size):
        if not Path(file).is_file():
            sg.popup(f'First time to load {file} from web ...', no_titlebar=True,
                auto_close=True, auto_close_duration=2)
            response, data = read_URL(url, byte=True)
            if data:
                with open(file, 'wb') as f:
                    f.write(data)
                im = read_file(file)
                im = im.resize(size)
                im.save(file)
                sg.popup(f'{file} loaded ...', no_titlebar=True,
                    auto_close=True, auto_close_duration=1)
            else:
                sg.popup(f'{file} load failed...', no_titlebar=True,
                    auto_close=True, auto_close_duration=2)
                self.window.close()
                quit()
  1. 轉換圖片檔案成適合顯示的資料
    def Load_Data(self, file):
        im = Image.open(file)
        with BytesIO() as output:
            im.save(output, format='PNG')
            data = output.getvalue()
        return (data, im.width, im.height)
  1. 載入背景圖片
    def Load_Background(self):
        url = 'https://ae01.alicdn.com/kf/HTB18XRYXBv0gK0jSZKb762K2FXaF.png'
        file = 'background.png'
        size = (self.width, 300)
        self.Download_Picture(url, file, size)
        self.background, self.background_width, self.background_height = (
            self.Load_Data(file))
  1. 載入樹木圖片
    def Load_Tree(self):
        url = 'https://www.vippng.com/png/full/458-4585828_fall-tree-png.png'
        file = 'tree.png'
        size = (250, 250)
        self.Download_Picture(url, file, size)
        self.tree = Image.open(file)
        self.tree_width, self.tree_height = self.tree.size
  1. 轉換 3D 座標成 2D 座標
    def Point_To_2D(self, point):
        x, y, z = point
        s1 = (z-self.z1)/(z-self.z0)
        X = x + (self.x0 - x)*s1 - self.x0
        Y = y + (self.y0 - y)*s1
        return [X, Y]
  1. 根據左下角的標及其變化, 生成框的四個角座標
    def Point_To_Box(self, x, y, z, dx, dy, dz, dx2):
        return [[x, y, z], [x+dx, y, z],
                [x+dx+dx2, y+dy, z+dz], [x+dx2, y+dy, z+dz]]
  1. 顯示背景圖片
    def Draw_Background(self):
        self.background_figure = self.draw.DrawImage(data=self.background,
            location=(-self.background_width//2, self.height))
  1. 顯示一個填色的框
    def Draw_Box(self, x, y, z, dx, dy, dz, dx2, color):
        points = self.Point_To_Box(x, y, z, dx, dy, dz, dx2)
        points = mapping(self.Point_To_2D, points)
        self.figures.append(self.draw.DrawPolygon(points, fill_color=color))
  1. 顯示綠地
    def Draw_Grass(self, x, y, z, dx, dy, dz, dx2, color):
        points = self.Point_To_Box(x, y, z, dx, dy, dz, dx2)
        points = mapping(self.Point_To_2D, points)
        points[0][0] = points[3][0] = -self.width//2
        points[1][0] = points[2][0] = self.width//2
        self.figures.append(self.draw.DrawPolygon(points, fill_color=color))
  1. 顯示左右兩邊的樹木, 並按比率縮小
    def Draw_Tree(self, x, y, z, dx, dy, dz, scale=1):
        w = max(1, int(self.tree_width*scale))
        h = max(1, int(self.tree_height*scale))
        im = self.tree.resize((w, h))
        with BytesIO() as output:
            im.save(output, format='PNG')
            data = output.getvalue()
        X, Y = self.Point_To_2D([x, y, z])
        figure = self.draw.DrawImage(data=data,
            location = (X-self.tree_width*scale/2, Y+self.tree_height*scale))
        self.figures.append(figure)
        X, Y = self.Point_To_2D([x+dx, y+dy, z+dy])
        figure = self.draw.DrawImage(data=data,
            location = (X-self.tree_width*scale/2, Y+self.tree_height*scale))
        self.figures.append(figure)
  1. 刪除記錄中所有已顯示的物件
    def Delete_Figures(self):
        for figure in self.figures:
            self.draw.DeleteFigure(figure)
        self.figures = []
  1. 螢幕更新
  • 先刪除已畫的物件
  • 畫綠地
  • 由遠到近把路邊, 馬路, 中線以及樹畫出
  • 馬路的中心位置由sin函式來調整
  • 馬路高低也由sin函式來調整
  • 樹的放大比率也隨著距離變化
    def Update_All(self):
        self.Delete_Figures()
        self.Draw_Grass(-self.width//2, 0, self.offset,
            self.width, 0, self.boxes*self.box_height, 0, 'green')
        for box in range(self.boxes-1, -1, -1):
            center = -500*math.sin(2*(box+self.count)/self.boxes*math.pi)
            dx = -500*math.sin(2*(box+self.count+1)/self.boxes*math.pi)-center
            dy = center/2-250
            z = box*self.box_height
            if (box+self.count)%3 == 0:
                self.Draw_Box(center-self.gap_width//2, 0, z, self.gap_width,
                    0, self.box_height, dx, self.gap_color)
            self.Draw_Box(center-self.road_width//2, 0, z, self.road_width,
                    0, self.box_height, dx, self.road_color)
            if (box+self.count)%3 == 0:
                self.Draw_Box(center-self.line_width//2, 0, z, self.line_width,
                    0, self.box_height, dx, self.line_color)
            if (box+self.count)%10 == 0:
                scale = 1- (z-self.z1)/(z-self.z0)
                self.Draw_Tree(center-self.gap_width//2-50, 0, z,
                    self.gap_width+100, 0, 0, scale)
        self.count += 1
        if self.count == self.boxes:
            self.count = 0
        self.x0 = center
  1. 簡單的GUI事件處理迴路
G = GUI()
while True:

    G.Update_All()
    event, values = G.window.Read(timeout=100)

    if event == None:
        break

    elif event in ['Left:37', 'Right:39', 'Up:38', 'Down:40']:
        pass

G.window.close()
本作品採用《CC 協議》,轉載必須註明作者和本文連結
Jason Yang

相關文章