§7.1 3D 对象#
约 1925 个字 181 行代码 预计阅读时间 25 分钟
一、3D 对象的容器控件#
tkintertools
的 3D 画布不是只有一个,它实际的继承关系是下面这样的:
flowchart RL
id1(tkinter.Canvas)
id2(tkintertools.Canvas)
id3(tools_3d.Canvas3D)
id4(tools_3d.Space)
id4 --> id3 --> id2 --> id1
其中,后面两个是属于 tkintertools
的子模块 tools_3d
里面的。
1.1 Space 类#
内置的 Space
类已经帮我们绑定好了相关的操作方法,如鼠标右键拖动就是平移(translate
)、鼠标左键拖动就是旋转(rotate
)以及鼠标滚轮滚动就是缩放(scale
),一般用于查看 3D 对象的外观。
1.2 Canvas3D 类#
Canvas3D
属于是什么 3D 事件都没有绑定的一个容器控件了。它存在的目的就是让使用者可以自定义绑定的事件及绑定的函数,而不是强制使用某些按键来触发某些事件(如鼠标左键拖动旋转 3D 对象)。这里要说明一点的就是,由于 tkinter
模块制定绑定事件时,会把之前已经绑定的同类事件给覆盖掉,因此我们在自定义绑定事件的时候,不能把之前的给丢掉了,要在新的绑定事件中加上它们,此外,多个绑定事件之间的执行顺序非常重要,有时候顺序错误会导致不可估计的后果。
Canvas3D
类有一个名为 space_sort
方法非常关键,在每次画面改动之后都必须调用它来更新,否则画面不会有任何的变化。比如每一次平移、每一次旋转和缩放等,都需要调用一下它来更新一下画面。“space sort” 意为“空间位置排序”,也就是说,每次调用它后,它会根据每个 3D 对象的数据来得出它们的前后位置关系(尽管不精确,后面会再次提到),计算出前后关系再根据前后位置关系来更新画面。
不过 Space
类中已经帮我们内置好了这个方法的调用,因此 Space
相比于 Canvas3D
来说非常方便。不过有时候我们并不想让用户能够拖动画面,我们可能只是想展示某一 3D 动画或者画面,这时 Canvas3D
就派上用场了!因为它根本就没有对应事件的绑定!
注意
尽管 Space
如此方便了,但是无论是 Space
还是 Canvas3D
,在新增加一个 3D 对象的时候,都应该调用一下 space_sort
方法来更新画面,因为考虑到性能和某些特殊要求的情况下,这个时候 space_sort
方法并不会被自动调用。
1.3 3D 容器控件的坐标规划#
3D 容器控件采用的都是右手系,原点位于画布中央,下图清晰地展示了 3D 容器中的空间坐标系:
上图中,X、Y 和 Z 分别表示直角空间坐标系的三个轴,O 表示原点。画布的默认视角都是正对着 X 轴正方向的,当然,这里我为了展示清楚,将整体平移和旋转了一下,所以原点并不在画布中央,视角也不是正对着 X 轴正方向的。
1.4 3D 容器控件上的 UI 控件#
尽管 3D 对象的基准在画布中央,但是这并不会影响 UI 控件的布局。和其他画布一样,UI 控件仍是以左上角为基准,反平面坐标系(y 轴反向)。
提示
无论怎样布局,UI 控件总是在 3D 对象的上方,也就是说,3D 对象不会遮挡 UI 控件。
二、创建基本的 3D 对象#
基本的 3D 对象包括点(Point
)、线(Line
)和面(Side
),后面还会讲到几种稍微复杂的 3D 对象,如长方体等。
为了方便演示效果,下面的代码都将采用 Space
作为容器控件来完成。
2.1 点#
我们通过类 Point
来显示一个点,具体的参数可以去文档中查看。下面的代码我们绘制了多个点。
注意
点本身是没有大小的,但是这里我们为了显示出它,给了它一个虚拟的大小,但是这个大小并不会随着这个点在视觉上的远近而缩放,因为一旦进行缩放,那就是不是点,而是球了。
点的大小只是方便我们观察它而设定的,你可以根据你的喜好或者需要,设定它显示出来的大小。
下面只是随机地在一些位置绘制了一个点,总体上表现为一个球:
源代码
import math
import random
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Point', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
for _ in range(10000):
x = random.randint(-1000, 1000)
y = random.randint(-1000, 1000)
z = random.randint(-1000, 1000)
c = random.randint(0, 256 ** 3 - 1)
if math.hypot(x, y, z) <= 400: # 只要半径 400 以内的
t3d.Point(space, (x, y, z), size=3, fill=f'#{c:06X}', outline='grey')
space.space_sort()
root.mainloop()
或者我们可以让它们变得有规律些(应该看得出来是个正方体的样子):
源代码
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Point', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
for x, r in zip([-300, -100, 100, 300], ['00', '55', 'AA', 'FF']):
for y, g in zip([-300, -100, 100, 300], ['00', '55', 'AA', 'FF']):
for z, b in zip([-300, -100, 100, 300], ['00', '55', 'AA', 'FF']):
t3d.Point(space, [x, y, z], fill=(fill := f'#{r}{g}{b}'), size=5, outline='grey')
space.space_sort()
root.mainloop()
2.2 线#
我们通过类 Line
来显示一条有限长直线,具体参数参考文档。
下面是一个十分简单的示例:
源代码
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Line', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
for x, r in zip([-100, 0, 100], ['00', '77', 'FF']):
for y, g in zip([-100, 0, 100], ['00', '77', 'FF']):
for z, b in zip([-100, 0, 100], ['00', '77', 'FF']):
t3d.Line(space, [0, 0, 0], [x, y, z], fill=f'#{r}{g}{b}', width=3)
space.space_sort()
root.mainloop()
或许我们可以再玩点更高级的?
怎么样?是不是非常酷炫呢?
源代码
import random
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Line', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
def flower(x, y, z, k): # type: (int, int, int, int) -> None
"""绘制线条花"""
for dx, r in zip([-k, 0, k], ['00', '77', 'FF']):
for dy, g in zip([-k, 0, k], ['00', '77', 'FF']):
for dz, b in zip([-k, 0, k], ['00', '77', 'FF']):
t3d.Line(space, (x, y, z), (x + dx, y + dy, z + dz), fill=f'#{r}{g}{b}', width=3)
for _ in range(25):
x = random.randint(-500, 500)
y = random.randint(-500, 500)
z = random.randint(-500, 500)
k = random.randint(50, 100) # 线条最大长度
flower(x, y, z, k)
space.space_sort()
root.mainloop()
2.3 面#
我们通过类 Side
来显示一个有限面积直边平面,具体参数参考文档。
下面简单用面来绘制一个正二十面体:
源代码
import itertools
import math
import statistics
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Side', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
m = 200 * math.sqrt(50 - 10 * math.sqrt(5)) / 10
n = 200 * math.sqrt(50 + 10 * math.sqrt(5)) / 10
points = []
dis_side = 200 * (3 * math.sqrt(3) + math.sqrt(15)) / 12 / ((math.sqrt(10 + 2 * math.sqrt(5))) / 4) # 面到中心的距离
count, color_lst = 0, ['00', '77', 'FF']
color = [f'#{r}{g}{b}' for r in color_lst for g in color_lst for b in color_lst]
for i in m, -m:
for j in n, -n:
points.append([0, j, i])
points.append([i, 0, j])
points.append([j, i, 0])
for p in itertools.combinations(points, 3): # 所有的顶点组合
dis = math.hypot(*[statistics.mean(c[i] for c in p) for i in range(3)])
if math.isclose(dis, dis_side):
t3d.Side(space, *p, fill=color[count], outline='white')
count += 1
space.space_sort()
root.mainloop()
面其实也可以玩出很多花样,比如,让它内部透明,只让其边框线条有颜色,这样或许我们可以得到这样的效果:
是不是看着挺有科技感的?在学了后面的章节“3D 动画”之后,让它动起来就更有内味儿了!
源代码
import itertools
import math
import statistics
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Side', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
m = 200 * math.sqrt(50 - 10 * math.sqrt(5)) / 10
n = 200 * math.sqrt(50 + 10 * math.sqrt(5)) / 10
points = []
dis_side = 200 * (3 * math.sqrt(3) + math.sqrt(15)) / 12 / ((math.sqrt(10 + 2 * math.sqrt(5))) / 4) # 面到中心的距离
for i in m, -m:
for j in n, -n:
points.append([0, j, i])
points.append([i, 0, j])
points.append([j, i, 0])
for p in itertools.combinations(points, 3): # 所有的顶点组合
dis = math.hypot(*[statistics.mean(c[i] for c in p) for i in range(3)])
if math.isclose(dis, dis_side):
t3d.Side(space, *p, fill='', outline='cyan')
space.space_sort()
root.mainloop()
警告
目前由于技术原因,实际过程中不建议给面的内部上色,当两个面的距离比较近时,这两个面可能出现前后位置关系显示颠倒的问题!尚无比较好的解决办法!对于后面的复杂 3D 对象(即几何体)也是如此。
目前前后位置的确定是通过欧几里得距离计算得到的,在两个面之间有一定距离时才能正确显示其前后位置关系。
三、创建复杂的 3D 对象#
复杂的 3D 对象是指非基本的对象,如长方体(Cuboid
)、四面体(Tetrahedron
)等复杂的几何体,当然,你也可以直接用基本的 3D 对象将它们模仿出来,但这并不是封装好的,它们并非一个整体,直接使用 Geometry
及其子类是更加推荐的选择。
3.1 长方体#
我们可以直接使用 Cuboid
来创建一个长方体,具体参数见文档。
下面利用长方体简单地绘制一个魔方,这里放一张白色背景的图(防止你认为只有黑色背景)。
源代码
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Cuboid', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, keep=False)
for a in -100, 0, 100:
for b in -100, 0, 100:
for c in -100, 0, 100:
t3d.Cuboid(space, a - 50, b - 50, c - 50, 100, 100, 100,
color_fill_up='white', color_fill_down='yellow', color_fill_left='red',
color_fill_right='orange', color_fill_front='blue', color_fill_back='green')
space.space_sort()
root.mainloop()
当我们将长方体也设为透明,再加上前一章所学的渐变色,就可以得到像下面这样的效果:
源代码
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Cuboid', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, bg='black', keep=False)
for l, c in zip(range(10, 300 + 1, 10), tkt.color(['white', 'black'], seqlength=30)):
s = l << 1
t3d.Cuboid(space, -l, -l, -l, s, s, s,
color_outline_back=c, color_outline_down=c, color_outline_front=c,
color_outline_left=c, color_outline_right=c, color_outline_up=c)
space.space_sort()
root.mainloop()
3.2 四面体#
同上,我们也可以用 Tetrahedron
直接创建四面体,具体参数见文档。
这里就不炫了,搞个简单点的完事儿!
源代码
import math
import tkintertools as tkt
from tkintertools import tools_3d as t3d
root = tkt.Tk('Tetrahedron', 1280, 720)
space = t3d.Space(root, 1280, 720, 0, 0, keep=False)
t3d.Tetrahedron(space, [-100, 0, 0 + 10], [50, 50 * math.sqrt(3), 0 + 10], [50, -50 * math.sqrt(
3), 0 + 10], [0, 0, 100 * math.sqrt(2) + 10], color_fill=['red', 'yellow', 'blue', 'green'])
t3d.Tetrahedron(space, [-100, 0, 0 - 10], [50, 50 * math.sqrt(3), 0 - 10], [50, -50 * math.sqrt(
3), 0 - 10], [0, 0, -100 * math.sqrt(2) - 10], color_fill=['red', 'yellow', 'blue', 'green'])
space.space_sort()
root.mainloop()
3.3 任意凸面几何体#
除了上面提到的两种,实际我们可以通过 Geometry
创建任意的(凸面)多面体。但这里要注意的是,凹面几何体也是可以创建的,不过目前不保证凹面几何体完全没问题,因此需慎用凹面几何体。
本质上,Geometry
的背后都是 Side
,它只不过是将其进行了一个组合和封装,因此我们就是通过多个 Side
类创建它的,此外,我们在创建了它之后,还可以用它的 append
方法向其继续添加 Side
。
欢迎在评论区留下你的想法
下面没有更多评论啦😆