(第一期)Python三维绘图、绘制立体图形、PythonTurtle画图教程、PythonTurtle绘制立体图形、Python维度转换程序(保姆级教程)

声明

作者Python只学了几个月,

本文仅供参考。

文中代码类、函数、变量,

命名随意,

可能存在不恰当、拼写错误等问题。

前言

Python内置了一个库——Turtle。

这恐怕是一个新手必学的库。

因为它实在是太基本了。

你可能用它画过很多东西。

但你肯定没有想过用它来绘制三维空间中的立体图形。

因为电脑屏幕是一块平面,

只能显示二维图形。

“.goto()”也只需要输入两个坐标——X轴和Y轴。

但是我们去看一个物体,也只能从某个角度去看。

所以看到的画面也是二维的。

那我们能否通过某种方式存储三维图形的信息,

并将其某个角度的样子用Turtle画出来呢?

这就是这篇文章所要讲的内容。

核心思路

生活中的现象

在坐火车的时候,

你会发现:

好像近处的景物移动的很快;

远处的景物移动的很慢。

但事实上,

远近景物移动的速度是一样快的。

所以这是一种错觉。

你可以这样想:

假设火车不动,

景物在动(物体的移动是相对的)。

你在2点0分0秒时朝窗外看了一眼;

又在2点0分3秒时朝窗外看了一眼;

然后你在这两个时间点都看到了近处的景物A和远处的景物B。

此时景物A在0秒时的位置为一个顶点;

景物A在3秒时的位置为一个顶点;

你观察的位置为一个顶点。

就得到了一个三角形。

再对景物B进行相同的操作,

也得到一个三角形。

将它们放在一起,

你会发现:

景物A的三角形夹角比景物B大。

其实观察物体,

所看到的大小或移动距离都是这个夹角的角度。

“月亮在晚上为什么会跟着看他的人走”和“玩手影时手影大小与远近有关”也差不多是这个道理,

只不是过换了个形式,

换汤不换料。

深入

那假设:

有一个点,

离观察点5(单位长度)远。

比观察点高1(单位长度)。

还比观察点更靠右1(单位长度)。

如果视野是60度,

那观察到的应该是这样的:

具体的理论

实际上这幅图是这样画出来的:

假设观察点的坐标是(0, 0, 0)。

X轴向右增加;

Y轴向前增加;

Z轴向上增加。

那么点的坐标就应该是(1, 1, 5)。

再:

从(0, 0, 0)画线到(1, 0, 5);

从(0, 0, 0)画线到(0, 1, 5);

从(0, 0, 0)画线到(0, 0, 5)。

第一二条线与第三条线的夹角都约为14度。

所以品红色的点大约在(14, 14)。

上图生成代码:

import pylab
import math
import matplotlib.font_manager

pylab.scatter([math.degrees(math.atan(1 / 5))],[math.degrees(math.atan(1 / 5))],s=[20],c='m',marker='o',label='点')
pylab.scatter([0],[0],s=[100],c='b',marker='+',label='中心')
pylab.xticks([-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30],[-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30],fontproperties = 'STSong')
pylab.yticks([-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30],[-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30],fontproperties = 'STSong')
pylab.legend(prop=matplotlib.font_manager.FontProperties("STSong"))
pylab.show()

估计你看到了里面的“atan”函数,

没错,

这就是最为关键的函数——余切。

假设有一个长方形:

把a除以b的商赋值给余切函数,

就能得到“∠1”的角度。

所以,

要想将三维坐标转换成二维坐标,

只需要两个余切函数。

代码实现

坐标存储

以长方体为例。

它不能被一笔画完,

而是要分很多笔(12条棱)。

作者使用的数据类型是三维列表:

一维:存储每一条直线(曲线);

二维:存储一条直线(曲线)的所有坐标;

三维:存储一个点的X、Y、Z坐标。

并使用一个类来生成这堆坐标:

class dim_perspective_dim3_figure:
    def __init__(self):
        pass
    def cuboid(self, cuboid_size, cuboid_coordinate):
        self.cuboid_L = (cuboid_size[0])
        self.cuboid_W = (cuboid_size[1])
        self.cuboid_H = (cuboid_size[2])
        self.cuboid_X = (cuboid_coordinate[0])
        self.cuboid_Y = (cuboid_coordinate[1])
        self.cuboid_Z = (cuboid_coordinate[2])
        return [[[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))]]
            , [[(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))], [(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z - (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y - (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]
            , [[(self.cuboid_X - (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))], [(self.cuboid_X + (self.cuboid_L / 2)), (self.cuboid_Y + (self.cuboid_W / 2)), (self.cuboid_Z + (self.cuboid_H / 2))]]]

共两个参数(位置(坐标),大小(长、宽、高))。

数学

作者使用一个类来存储一些数学计算的函数:

class dim_math:
    #类的初始化函数
    def __init__(self):
        pass
    #正切函数
    def tan(self, d):
        return ((0 - (math.tan((math.radians(d))))) if ((math.fabs(d)) > 90) else (math.tan((math.radians(d)))))
    #余切函数
    def atan(self, a, b):
        return (math.degrees(math.atan2(a, b)))
    #正弦函数
    def sin(self, d):
        return (math.sin((math.radians(d))))
    #余弦函数
    def cos(self, d):
        return (math.cos((math.radians(d))))
    #映射函数
    def map(self, v, al, ah, bl, bh):
        return (bl + ((bh - bl) * ((v - al) / (ah - al))))
    #绕点旋转函数
    def rotates(self, coord, degrees):
        return [(((math.sin((math.radians(degrees)))) * ((coord[1]))) + ((math.sin((math.radians((degrees + 90))))) * ((coord[0]))))
            , (((math.cos((math.radians(degrees)))) * ((coord[1]))) + ((math.cos((math.radians((degrees + 90))))) * ((coord[0]))))]
    #勾股定理函数
    def Pythagorean(self, ta, tb):
        return ((((math.fabs(ta)) ** 2) + ((math.fabs(tb)) ** 2)) ** 0.5)
    #限制坐标范围函数
    def constraint(self, coord, screen_size):
        cX = (coord[0])
        cY = (coord[1])
        if ((((screen_size[0])[0]) <= cX <= ((screen_size[0])[1])) and (((screen_size[1])[0]) <= cY <= ((screen_size[1])[1]))):
            X = 1
            return [(cX * X), (cY * X)]
        elif((((screen_size[0])[0]) > cX) and (((screen_size[1])[0]) > cY)):
            X = min((((screen_size[0])[0]) / cX), (((screen_size[1])[0]) / cY))
            return [(cX * X), (cY * X)]
        elif((((screen_size[0])[1]) < cX) and (((screen_size[1])[0]) > cY)):
            X = min((((screen_size[0])[1]) / cX), (((screen_size[1])[0]) / cY))
            return [(cX * X), (cY * X)]
        elif((((screen_size[0])[0]) > cX) and (((screen_size[1])[1]) < cY)):
            X = min((((screen_size[0])[0]) / cX), (((screen_size[1])[1]) / cY))
            return [(cX * X), (cY * X)]
        elif((((screen_size[0])[1]) < cX) and (((screen_size[1])[1]) < cY)):
            X = min((((screen_size[0])[1]) / cX), (((screen_size[1])[1]) / cY))
            return [(cX * X), (cY * X)]
        elif(((screen_size[0])[0]) > cX):
            X = (((screen_size[0])[0]) / cX)
            return [(cX * X), (cY * X)]
        elif(((screen_size[0])[1]) < cX):
            X = (((screen_size[0])[1]) / cX)
            return [(cX * X), (cY * X)]
        elif(((screen_size[1])[0]) > cY):
            X = (((screen_size[1])[0]) / cY)
            return [(cX * X), (cY * X)]
        elif(((screen_size[1])[1] < cY)):
            X = (((screen_size[1])[1]) / cY)
            return [(cX * X), (cY * X)]
        else:
            os.error("Error")
    #差函数
    def differ(self, va, vb):
        return (math.fabs((va - vb)))
    #投影函数
    def dim3to2(self, coordinate):
        return [(math.degrees((math.atan2((coordinate[0]), (coordinate[1])))))
            , (math.degrees((math.atan2((coordinate[2]), (coordinate[1])))))]
    #反投影函数
    def dim2to3(self, coordinate, coordinate_Y):
        def tangent(degrees):
            return ((0 - (math.tan((math.radians(degrees))))) if ((math.fabs(degrees)) > 90) else (math.tan((math.radians(degrees)))))
        return [(tangent((coordinate[0])) * coordinate_Y)
            , (tangent((coordinate[1])) * coordinate_Y)
            , coordinate_Y]

你会发现这里有甚至勾股定理,

这主要是为了方便拓展。

1.正切函数

使用math库“tan”函数,

为了使角度输入范围达到(-180~180),

用了个条件推导式。

“radians”类似于一种预处理的作用。

2.余切

使用math库的”atan2”函数,

也是因为范围能达到(-180~180)。

“degrees”就是“radians”的逆运算,

因为”atan”是“tan”的逆运算。

3.正弦、余弦函数

和”atan”差不多。

4.映射

范围转换的函数,

用习惯了米思齐(Mixly)的映射,

打代码时,

把它的源代码复制了。

5.绕点旋转

正余弦函数画圆的改版。

6.勾股定理

小学时候学的,

很简单。

7.限制坐标

限制一条线从中心点到指定长方形内部的某点。

但第二点超出时,

角度不变。

8.差函数

求差,

单只返回自然数。

9.投影

详见前文(具体的理论)。

10.反投影

投影的逆运算。

(三维)绕某点旋转

为了是画出来的图形可以旋转,

作者创建了一个类:

class dim_perspective_dim3_rotates:
    # 类的初始化函数
    def __init__(self, NP, axis):
        self.NPX = (NP[0])
        self.NPY = (NP[1])
        self.NPZ = (NP[2])
        self.aX = (axis[0])
        self.aY = (axis[1])
        self.aZ = (axis[2])
        #需要使用数学计算类
        self.math = dim_math()
    #三维坐标旋转函数
    def rotates(self, coord):
        #坐标预处理
        self.__X = ((coord[0]) - (self.NPX))
        self.__Y = ((coord[1]) - (self.NPY))
        self.__Z = ((coord[2]) - (self.NPZ))
        #第一次旋转——X轴
        self.__r1 = self.math.rotates(((self.__Y), (self.__Z)), (self.aX))
        self.__X1 = (self.__X)
        self.__Y1 = ((self.__r1)[0])
        self.__Z1 = ((self.__r1)[1])
        # 第二次旋转——Y轴
        self.__r2 = self.math.rotates(((self.__X1), (self.__Z1)), (self.aY))
        self.__X2 = ((self.__r2)[0])
        self.__Y2 = (self.__Y1)
        self.__Z2 = ((self.__r2)[1])
        # 第三次旋转——Z轴
        self.__r3 = self.math.rotates(((self.__X2), (self.__Y2)), (self.aZ))
        self.__X3 = ((self.__r3)[0])
        self.__Y3 = ((self.__r3)[1])
        self.__Z3 = (self.__Z2)
        return [((self.__X3) + (self.NPX)), ((self.__Y3) + (self.NPY)), ((self.__Z3) + (self.NPZ))]

通过三次平面的旋转,

实现立体旋转。

绘制三维图形

作者在此也使用一个类来包装相关的程序:

#(透视)二三维坐标转换类
class dim_perspective_convert_dim:
    # 类的初始化函数
    def __init__(self, screen_parameter, eye_parameter):
        self.screenX = ((screen_parameter[0])[0])
        self.screenY = ((screen_parameter[0])[1])
        self.screenlenth = (screen_parameter[1])
        self.eye_visual_angle = ((eye_parameter[0]) / 2)
        self.eye_dX = ((eye_parameter[1])[0])
        self.eye_dY = ((eye_parameter[1])[1])
        self.eye_dZ = ((eye_parameter[1])[2])
        self.eye_daX = ((eye_parameter[2])[0])
        self.eye_daY = ((eye_parameter[2])[1])
        self.eye_daZ = ((eye_parameter[2])[2])
        self.math = dim_math()
    #将三维转换成二维并绘制
    def drawdim3_todim2(self, coord, drawfunction):
        self.__rotates = dim_perspective_dim3_rotates(((0 - (self.eye_dX)), (0 - (self.eye_dY)), (0 - (self.eye_dZ))), ((self.eye_daX), (self.eye_daY), (self.eye_daZ)))
        out = 0
        for d1 in range(len(coord)):
            for d2 in range(len((coord[(d1)]))):
                self.X = (((coord[d1])[d2])[0])
                self.Y = (((coord[d1])[d2])[1])
                self.Z = (((coord[d1])[d2])[2])
                self.r = (self.__rotates.rotates((((self.eye_dX) - (self.X)), ((self.eye_dY) - (self.Y)), ((self.eye_dZ) - (self.Z)))))#((coord[d1])[d2])
                self.a = (self.math.dim3to2((self.r)))
                if (((0 - (self.eye_visual_angle)) <= ((self.a)[0]) <= (0 + (self.eye_visual_angle))) and ((0 - (self.eye_visual_angle)) <= ((self.a)[1]) <= (0 + (self.eye_visual_angle)))):
                    self.a2 = (self.math.constraint((self.a), (((0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle))), ((0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle))))))
                    drawfunction([(self.math.map(((self.a)[0]), (0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle)), (0 - (self.screenlenth)), (0 + (self.screenlenth))))
                        , (self.math.map(((self.a)[1]), (0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle)), (0 - (self.screenlenth)), (0 + (self.screenlenth))))]
                    , d1, (d2 - out))
                else:
                    out += 1

这可谓是这个项目的灵魂,

它需要输入很多参数才能正常运行。

dim_perspective_convert_dim((((投影)幕布的X轴, (投影)幕布的Y轴), (投影)幕布的边长的一半), ((观察点)的视角, (观察点)的X轴(向左增加), (观察点)的Y轴(向后增加), (观察点)的Z轴(向下增加)), ((观察点)的俯仰角, (观察点)的滚转角, (观察点)的自转角度)))

#(透视)二三维坐标转换类
class dim_perspective_convert_dim:
    # 类的初始化函数
    def __init__(self, screen_parameter, eye_parameter):
        self.screenX = ((screen_parameter[0])[0])
        self.screenY = ((screen_parameter[0])[1])
        self.screenlenth = (screen_parameter[1])
        self.eye_visual_angle = ((eye_parameter[0]) / 2)
        self.eye_dX = ((eye_parameter[1])[0])
        self.eye_dY = ((eye_parameter[1])[1])
        self.eye_dZ = ((eye_parameter[1])[2])
        self.eye_daX = ((eye_parameter[2])[0])
        self.eye_daY = ((eye_parameter[2])[1])
        self.eye_daZ = ((eye_parameter[2])[2])
        self.math = dim_math()
    #将三维转换成二维并绘制
    def drawdim3_todim2(self, coord, drawfunction):
        self.__rotates = dim_perspective_dim3_rotates(((0 - (self.eye_dX)), (0 - (self.eye_dY)), (0 - (self.eye_dZ))), ((self.eye_daX), (self.eye_daY), (self.eye_daZ)))
        out = 0
        for d1 in range(len(coord)):
            for d2 in range(len((coord[(d1)]))):
                self.X = (((coord[d1])[d2])[0])
                self.Y = (((coord[d1])[d2])[1])
                self.Z = (((coord[d1])[d2])[2])
                self.r = (self.__rotates.rotates((((self.eye_dX) - (self.X)), ((self.eye_dY) - (self.Y)), ((self.eye_dZ) - (self.Z)))))#((coord[d1])[d2])
                self.a = (self.math.dim3to2((self.r)))
                if (((0 - (self.eye_visual_angle)) <= ((self.a)[0]) <= (0 + (self.eye_visual_angle))) and ((0 - (self.eye_visual_angle)) <= ((self.a)[1]) <= (0 + (self.eye_visual_angle)))):
                    self.a2 = (self.math.constraint((self.a), (((0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle))), ((0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle))))))
                    drawfunction([(self.math.map(((self.a)[0]), (0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle)), (0 - (self.screenlenth)), (0 + (self.screenlenth))))
                        , (self.math.map(((self.a)[1]), (0 - (self.eye_visual_angle)), (0 + (self.eye_visual_angle)), (0 - (self.screenlenth)), (0 + (self.screenlenth))))]
                    , d1, (d2 - out))
                else:
                    out += 1

细节

X轴:向右增加

Y轴:向前增加

Z轴:向上增加

示例

import turtle
import time
tina = turtle.Turtle()
tina.speed(10)
tina.screen.delay(0)
tina.hideturtle()
f1 = []
f1 += (dim_perspective_dim3_figure().cuboid((100, 100, 100), (0, 0, 0)))
converter = dim_perspective_convert_dim(((0, 0), 300), (360, (0, 0, 0), (0, 0, 0)))
def draw(pos, i, j):
    if (j == 0):
        tina.penup()
    else:
        tina.pendown()
    tina.goto(pos)
for i in range(-400, 201, 4):
    tina.clear()
    converter.eye_dY = i
    converter.drawdim3_todim2(f1, draw)
    time.sleep(0.1)

结尾

希望大家多多点赞;

留言讨论!

如发现有错别字、BUG、错误之处等,

欢迎在评论区指正!

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值