3D §7.1 3D 对象 字数 1925 个 代码 174 行 图片 10 张 阅读时间 9 分钟 访问量
一、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。
2024-11-10 2024-08-09 GitHub