声明
作者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、错误之处等,
欢迎在评论区指正!