Python 实现三维建模工具
一、 内容介绍
人类是那么得有创造力,我们创造、发明、设计、生产了一切大自然没有直接给予我们的东西使我们的生活变得更轻松更美好。在过去,我们只能在图纸上进行产品的创造与设计,而现在,有了计算机的帮助,有了 CAD(计算机辅助设计)软件,大大节省了我们的精力与时间成本,使我们的工作更高效,能够拥有更多时间去思考设计本身。
那么 CAD 软件是如何写出来的呢?CAD 软件种类繁多,但它们有一个共同的特点,就是对三维世界的建模,对三维世界中物体的控制,对三维设计的展示。
课程知识点
本课程项目完成过程中,我们将学习:
OpenGL 坐标系的转换
实现简单的用户输入事件回调机制
设计模式中组合模式的使用
基于包围盒的碰撞检测
二、 实现原理及步骤
1.用户接口
新建 interaction.py 文件,用户接口在 Interaction 类中实现。
导入需要的库:
from collections import defaultdict
from OpenGL.GLUT import glutGet, glutKeyboardFunc, glutMotionFunc, glutMouseFunc, glutPassiveMotionFunc, \
glutPostRedisplay, glutSpecialFunc
from OpenGL.GLUT import GLUT_LEFT_BUTTON, GLUT_RIGHT_BUTTON, GLUT_MIDDLE_BUTTON, \
GLUT_WINDOW_HEIGHT, GLUT_WINDOW_WIDTH, \
GLUT_DOWN, GLUT_KEY_UP, GLUT_KEY_DOWN, GLUT_KEY_LEFT, GLUT_KEY_RIGHT
import trackball
初始化 Interaction 类,注册 glut 的事件回调函数。
class Interaction(object):
def __init__(self):
""" 处理用户接口 """
#被按下的键
self.pressed = None
#轨迹球,会在之后进行说明
self.trackball = trackball.Trackball(theta = -25, distance=15)
#当前鼠标位置
self.mouse_loc = None
#回调函数词典
self.callbacks = defaultdict(list)
self.register()
def register(self):
""" 注册glut的事件回调函数 """
glutMouseFunc(self.handle_mouse_button)
glutMotionFunc(self.handle_mouse_move)
glutKeyboardFunc(self.handle_keystroke)
glutSpecialFunc(self.handle_keystroke)
回调函数的实现:
def handle_mouse_button(self, button, mode, x, y):
""" 当鼠标按键被点击或者释放的时候调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - y # OpenGL原点在窗口左下角,窗口原点在左上角,所以需要这种转换。
self.mouse_loc = (x, y)
if mode == GLUT_DOWN:
#鼠标按键按下的时候
self.pressed = button
if button == GLUT_RIGHT_BUTTON:
pass
elif button == GLUT_LEFT_BUTTON:
self.trigger('pick', x, y)
else: # 鼠标按键被释放的时候
self.pressed = None
#标记当前窗口需要重新绘制
glutPostRedisplay()
def handle_mouse_move(self, x, screen_y):
""" 鼠标移动时调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - screen_y
if self.pressed is not None:
dx = x - self.mouse_loc[0]
dy = y - self.mouse_loc[1]
if self.pressed == GLUT_RIGHT_BUTTON and self.trackball is not None:
# 变化场景的角度
self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)
elif self.pressed == GLUT_LEFT_BUTTON:
self.trigger('move', x, y)
elif self.pressed == GLUT_MIDDLE_BUTTON:
self.translate(dx/60.0, dy/60.0, 0)
else:
pass
glutPostRedisplay()
self.mouse_loc = (x, y)
def handle_keystroke(self, key, x, screen_y):
""" 键盘输入时调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - screen_y
if key == 's':
self.trigger('place', 'sphere', x, y)
elif key == 'c':
self.trigger('place', 'cube', x, y)
elif key == GLUT_KEY_UP:
self.trigger('scale', up=True)
elif key == GLUT_KEY_DOWN:
self.trigger('scale', up=False)
elif key == GLUT_KEY_LEFT:
self.trigger('rotate_color', forward=True)
elif key == GLUT_KEY_RIGHT:
self.trigger('rotate_color', forward=False)
glutPostRedisplay()
2.回调函数
初始化 Interaction 类,注册 glut 的事件回调函数。
class Interaction(object):
def __init__(self):
""" 处理用户接口 """
#被按下的键
self.pressed = None
#轨迹球,会在之后进行说明
self.trackball = trackball.Trackball(theta = -25, distance=15)
#当前鼠标位置
self.mouse_loc = None
#回调函数词典
self.callbacks = defaultdict(list)
self.register()
def register(self):
""" 注册glut的事件回调函数 """
glutMouseFunc(self.handle_mouse_button)
glutMotionFunc(self.handle_mouse_move)
glutKeyboardFunc(self.handle_keystroke)
glutSpecialFunc(self.handle_keystroke)
回调函数的实现:
def handle_mouse_button(self, button, mode, x, y):
""" 当鼠标按键被点击或者释放的时候调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - y # OpenGL原点在窗口左下角,窗口原点在左上角,所以需要这种转换。
self.mouse_loc = (x, y)
if mode == GLUT_DOWN:
#鼠标按键按下的时候
self.pressed = button
if button == GLUT_RIGHT_BUTTON:
pass
elif button == GLUT_LEFT_BUTTON:
self.trigger('pick', x, y)
else: # 鼠标按键被释放的时候
self.pressed = None
#标记当前窗口需要重新绘制
glutPostRedisplay()
def handle_mouse_move(self, x, screen_y):
""" 鼠标移动时调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - screen_y
if self.pressed is not None:
dx = x - self.mouse_loc[0]
dy = y - self.mouse_loc[1]
if self.pressed == GLUT_RIGHT_BUTTON and self.trackball is not None:
# 变化场景的角度
self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)
elif self.pressed == GLUT_LEFT_BUTTON:
self.trigger('move', x, y)
elif self.pressed == GLUT_MIDDLE_BUTTON:
self.translate(dx/60.0, dy/60.0, 0)
else:
pass
glutPostRedisplay()
self.mouse_loc = (x, y)
def handle_keystroke(self, key, x, screen_y):
""" 键盘输入时调用 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - screen_y
if key == 's':
self.trigger('place', 'sphere', x, y)
elif key == 'c':
self.trigger('place', 'cube', x, y)
elif key == GLUT_KEY_UP:
self.trigger('scale', up=True)
elif key == GLUT_KEY_DOWN:
self.trigger('scale', up=False)
elif key == GLUT_KEY_LEFT:
self.trigger('rotate_color', forward=True)
elif key == GLUT_KEY_RIGHT:
self.trigger('rotate_color', forward=False)
glutPostRedisplay()
针对用户行为会调用 self.trigger 方法,它的第一个参数指明行为期望的效果,后续参数为该效果的参数,trigger 的实现如下:
def trigger(self, name, *args, **kwargs):
for func in self.callbacks[name]:
func(*args, **kwargs)
将方法添加进 callbacks ,需要实现一个注册回调函数的方法:
def register_callback(self, name, func):
self.callbacks[name].append(func)
Viewer 中未实现的 self.init_interaction() ,在这里注册回调函数的,补完 init_interaction
from interaction import Interaction
...
class Viewer(object):
...
def init_interaction(self):
self.interaction = Interaction()
self.interaction.register_callback('pick', self.pick)
self.interaction.register_callback('move', self.move)
self.interaction.register_callback('place', self.place)
self.interaction.register_callback('rotate_color', self.rotate_color)
self.interaction.register_callback('scale', self.scale)
def pick(self, x, y):
""" 鼠标选中一个节点 """
pass
def move(self, x, y):
""" 移动当前选中的节点 """
pass
def place(self, shape, x, y):
""" 在鼠标的位置上新放置一个节点 """
pass
def rotate_color(self, forward):
""" 更改选中节点的颜色 """
pass
def scale(self, up):
""" 改变选中节点的大小 """
pass
3.与场景交互
旋转场景 在这个项目中摄像机是固定的,我们主要靠移动场景来观察不同角度下的 3D 模型。摄像机固定在距离原点 15 个单位的位置,面对世界坐标系的原点。
使用轨迹球 使用轨迹球算法来完成场景的旋转,旋转的方法理解起来很简单,想象一个可以向任意角度围绕球心旋转的地球仪,你的视线是不变的,但是通过你的手在拨这个球,你可以想看哪里拨哪里。在我们的项目中,这个拨球的手就是鼠标右键,点着右键拖动就能实现这个旋转场景的效果。
在这个项目中,我们使用 Glumpy 中轨迹球的实现。
下载 trackball.py 文件,并将其置于工作目录下:
wget http://labfile.oss.aliyuncs.com/courses/561/trackball.py
drag_to 方法实现与轨迹球的交互,它会比对之前的鼠标位置和移动后的鼠标位置来更新旋转矩阵。
self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)
得到的旋转矩阵保存在 viewer 的 trackball.matrix 中。
更新 viewer.py 下的 ModelView 矩阵:
class Viewer(object):
...
def render(self):
self.init_view()
glEnable(GL_LIGHTING)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# 将ModelView矩阵设为轨迹球的旋转矩阵
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
glMultMatrixf(self.interaction.trackball.matrix)
# 存储ModelView矩阵与其逆矩阵之后做坐标系转换用
currentModelView = numpy.array(glGetFloatv(GL_MODELVIEW_MATRIX))
self.modelView = numpy.transpose(currentModelView)
self.inverseModelView = inv(numpy.transpose(currentModelView))
self.scene.render()
glDisable(GL_LIGHTING)
glCallList(G_OBJ_PLANE)
glPopMatrix()
glFlush()
4.选择场景中的对象
新建 aabb.py,编写包围盒类:
from OpenGL.GL import glCallList, glMatrixMode, glPolygonMode, glPopMatrix, glPushMatrix, glTranslated, \
GL_FILL, GL_FRONT_AND_BACK, GL_LINE, GL_MODELVIEW
from primitive import G_OBJ_CUBE
import numpy
import math
#判断误差
EPSILON = 0.000001
class AABB(object):
def __init__(self, center, size):
self.center = numpy.array(center)
self.size = numpy.array(size)
def scale(self, scale):
self.size *= scale
def ray_hit(self, origin, direction, modelmatrix):
""" 返回真则表示激光射中了包盒
参数说明: origin, distance -> 激光源点与方向
modelmatrix -> 世界坐标到局部对象坐标的转换矩阵 """
aabb_min = self.center - self.size
aabb_max = self.center + self.size
tmin = 0.0
tmax = 100000.0
obb_pos_worldspace = numpy.array([modelmatrix[0, 3], modelmatrix[1, 3], modelmatrix[2, 3]])
delta = (obb_pos_worldspace - origin)
# test intersection with 2 planes perpendicular to OBB's x-axis
xaxis = numpy.array((modelmatrix[0, 0], modelmatrix[0, 1], modelmatrix[0, 2]))
e = numpy.dot(xaxis, delta)
f = numpy.dot(direction, xaxis)
if math.fabs(f) > 0.0 + EPSILON:
t1 = (e + aabb_min[0])/f
t2 = (e + aabb_max[0])/f
if t1 > t2:
t1, t2 = t2, t1
if t2 < tmax:
tmax = t2
if t1 > tmin:
tmin = t1
if tmax < tmin:
return (False, 0)
else:
if (-e + aabb_min[0] > 0.0 + EPSILON) or (-e+aabb_max[0] < 0.0 - EPSILON):
return False, 0
yaxis = numpy.array((modelmatrix[1, 0], modelmatrix[1, 1], modelmatrix[1, 2]))
e = numpy.dot(yaxis, delta)
f = numpy.dot(direction, yaxis)
# intersection in y
if math.fabs(f) > 0.0 + EPSILON:
t1 = (e + aabb_min[1])/f
t2 = (e + aabb_max[1])/f
if t1 > t2:
t1, t2 = t2, t1
if t2 < tmax:
tmax = t2
if t1 > tmin:
tmin = t1
if tmax < tmin:
return (False, 0)
else:
if (-e + aabb_min[1] > 0.0 + EPSILON) or (-e+aabb_max[1] < 0.0 - EPSILON):
return False, 0
# intersection in z
zaxis = numpy.array((modelmatrix[2, 0], modelmatrix[2, 1], modelmatrix[2, 2]))
e = numpy.dot(zaxis, delta)
f = numpy.dot(direction, zaxis)
if math.fabs(f) > 0.0 + EPSILON:
t1 = (e + aabb_min[2])/f
t2 = (e + aabb_max[2])/f
if t1 > t2:
t1, t2 = t2, t1
if t2 < tmax:
tmax = t2
if t1 > tmin:
tmin = t1
if tmax < tmin:
return (False, 0)
else:
if (-e + aabb_min[2] > 0.0 + EPSILON) or (-e+aabb_max[2] < 0.0 - EPSILON):
return False, 0
return True, tmin
def render(self):
""" 渲染显示包围盒,可在调试的时候使用 """
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glTranslated(self.center[0], self.center[1], self.center[2])
glCallList(G_OBJ_CUBE)
glPopMatrix()
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
更新 Node 类与 Scene 类,加入与选中节点有关的内容。
更新 Node 类:
from aabb import AABB
...
class Node(object):
def __init__(self):
self.color_index = random.randint(color.MIN_COLOR, color.MAX_COLOR)
self.aabb = AABB([0.0, 0.0, 0.0], [0.5, 0.5, 0.5])
self.translation_matrix = numpy.identity(4)
self.scaling_matrix = numpy.identity(4)
self.selected = False
...
def render(self):
glPushMatrix()
glMultMatrixf(numpy.transpose(self.translation_matrix))
glMultMatrixf(self.scaling_matrix)
cur_color = color.COLORS[self.color_index]
glColor3f(cur_color[0], cur_color[1], cur_color[2])
if self.selected: # 选中的对象会发光
glMaterialfv(GL_FRONT, GL_EMISSION, [0.3, 0.3, 0.3])
self.render_self()
if self.selected:
glMaterialfv(GL_FRONT, GL_EMISSION, [0.0, 0.0, 0.0])
glPopMatrix()
def select(self, select=None):
if select is not None:
self.selected = select
else:
self.selected = not self.selected
更新scene类:
class Scene(object):
def __init__(self):
self.node_list = list()
self.selected_node = None
在 Viewer 类中实现通过鼠标位置获取激光的函数以及 pick 函数:
# class Viewer
def get_ray(self, x, y):
"""
返回光源和激光方向
"""
self.init_view()
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# 得到激光的起始点
start = numpy.array(gluUnProject(x, y, 0.001))
end = numpy.array(gluUnProject(x, y, 0.999))
# 得到激光的方向
direction = end - start
direction = direction / norm(direction)
return (start, direction)
def pick(self, x, y):
""" 是否被选中以及哪一个被选中交由Scene下的pick处理 """
start, direction = self.get_ray(x, y)
self.scene.pick(start, direction, self.modelView)
为了确定是哪个对象被选中,我们会遍历场景下的所有对象,检查激光是否与该对象相交,取离摄像机最近的对象为选中对象。
# Scene 下实现
def pick(self, start, direction, mat):
"""
参数中的mat为当前ModelView的逆矩阵,作用是计算激光在局部(对象)坐标系中的坐标
"""
import sys
if self.selected_node is not None:
self.selected_node.select(False)
self.selected_node = None
# 找出激光击中的最近的节点。
mindist = sys.maxsize
closest_node = None
for node in self.node_list:
hit, distance = node.pick(start, direction, mat)
if hit and distance < mindist:
mindist, closest_node = distance, node
# 如果找到了,选中它
if closest_node is not None:
closest_node.select()
closest_node.depth = mindist
closest_node.selected_loc = start + direction * mindist
self.selected_node = closest_node
# Node下的实现
def pick(self, start, direction, mat):
# 将modelview矩阵乘上节点的变换矩阵
newmat = numpy.dot(
numpy.dot(mat, self.translation_matrix),
numpy.linalg.inv(self.scaling_matrix)
)
results = self.aabb.ray_hit(start, direction, newmat)
return results
5.操作场景中的对象
对对象的操作主要包括在场景中加入新对象,移动对象、改变对象的颜色与改变对象的大小。因为这部分的实现较为简单,所以仅实现加入新对象与移动对象的操作。
加入新对象的代码如下:
# Viewer下的实现
def place(self, shape, x, y):
start, direction = self.get_ray(x, y)
self.scene.place(shape, start, direction, self.inverseModelView)
# Scene下的实现
import numpy
from node import Sphere, Cube, SnowFigure
...
def place(self, shape, start, direction, inv_modelview):
new_node = None
if shape == 'sphere': new_node = Sphere()
elif shape == 'cube': new_node = Cube()
elif shape == 'figure': new_node = SnowFigure()
self.add_node(new_node)
# 得到在摄像机坐标系中的坐标
translation = (start + direction * self.PLACE_DEPTH)
# 转换到世界坐标系
pre_tran = numpy.array([translation[0], translation[1], translation[2], 1])
translation = inv_modelview.dot(pre_tran)
new_node.translate(translation[0], translation[1], translation[2])
移动目标对象的代码如下:
# Viewer下的实现
def move(self, x, y):
start, direction = self.get_ray(x, y)
self.scene.move_selected(start, direction, self.inverseModelView)
# Scene下的实现
def move_selected(self, start, direction, inv_modelview):
if self.selected_node is None: return
# 找到选中节点的坐标与深度(距离)
node = self.selected_node
depth = node.depth
oldloc = node.selected_loc
# 新坐标的深度保持不变
newloc = (start + direction * depth)
# 得到世界坐标系中的移动坐标差
translation = newloc - oldloc
pre_tran = numpy.array([translation[0], translation[1], translation[2], 0])
translation = inv_modelview.dot(pre_tran)
# 节点做平移变换
node.translate(translation[0], translation[1], translation[2])
node.selected_loc = newloc
三、 源代码
viewer.py 代码:
from OpenGL.GL import glCallList, glClear, glClearColor, glColorMaterial, glCullFace, glDepthFunc, glDisable, glEnable,\
glFlush, glGetFloatv, glLightfv, glLoadIdentity, glMatrixMode, glMultMatrixf, glPopMatrix, \
glPushMatrix, glTranslated, glViewport, \
GL_AMBIENT_AND_DIFFUSE, GL_BACK, GL_CULL_FACE, GL_COLOR_BUFFER_BIT, GL_COLOR_MATERIAL, \
GL_DEPTH_BUFFER_BIT, GL_DEPTH_TEST, GL_FRONT_AND_BACK, GL_LESS, GL_LIGHT0, GL_LIGHTING, \
GL_MODELVIEW, GL_MODELVIEW_MATRIX, GL_POSITION, GL_PROJECTION, GL_SPOT_DIRECTION
from OpenGL.constants import GLfloat_3, GLfloat_4
from OpenGL.GLU import gluPerspective, gluUnProject
from OpenGL.GLUT import glutCreateWindow, glutDisplayFunc, glutGet, glutInit, glutInitDisplayMode, \
glutInitWindowSize, glutMainLoop, \
GLUT_SINGLE, GLUT_RGB, GLUT_WINDOW_HEIGHT, GLUT_WINDOW_WIDTH, glutCloseFunc
import numpy
from numpy.linalg import norm, inv
import random
from OpenGL.GL import glBegin, glColor3f, glEnd, glEndList, glLineWidth, glNewList, glNormal3f, glVertex3f, \
GL_COMPILE, GL_LINES, GL_QUADS
from OpenGL.GLU import gluDeleteQuadric, gluNewQuadric, gluSphere
import color
from scene import Scene
from primitive import init_primitives, G_OBJ_PLANE
from node import Sphere, Cube, SnowFigure
class Viewer(object):
def __init__(self):
""" Initialize the viewer. """
#初始化接口,创建窗口并注册渲染函数
self.init_interface()
#初始化opengl的配置
self.init_opengl()
#初始化3d场景
self.init_scene()
#初始化交互操作相关的代码
self.init_interaction()
init_primitives()
def init_interface(self):
""" 初始化窗口并注册渲染函数 """
glutInit()
glutInitWindowSize(640, 480)
glutCreateWindow("3D Modeller")
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB)
#注册窗口渲染函数
glutDisplayFunc(self.render)
def init_opengl(self):
""" 初始化opengl的配置 """
#模型视图矩阵
self.inverseModelView = numpy.identity(4)
#模型视图矩阵的逆矩阵
self.modelView = numpy.identity(4)
#开启剔除操作效果
glEnable(GL_CULL_FACE)
#取消对多边形背面进行渲染的计算(看不到的部分不渲染)
glCullFace(GL_BACK)
#开启深度测试
glEnable(GL_DEPTH_TEST)
#测试是否被遮挡,被遮挡的物体不予渲染
glDepthFunc(GL_LESS)
#启用0号光源
glEnable(GL_LIGHT0)
#设置光源的位置
glLightfv(GL_LIGHT0, GL_POSITION, GLfloat_4(0, 0, 1, 0))
#设置光源的照射方向
glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, GLfloat_3(0, 0, -1))
#设置材质颜色
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glEnable(GL_COLOR_MATERIAL)
#设置清屏的颜色
glClearColor(0.4, 0.4, 0.4, 0.0)
def init_scene(self):
#创建一个场景实例
self.scene = Scene()
#初始化场景内的对象
self.create_sample_scene()
def create_sample_scene(self):
cube_node = Cube()
cube_node.translate(2, 0, 2)
cube_node.color_index = 1
self.scene.add_node(cube_node)
sphere_node = Sphere()
sphere_node.translate(-2, 0, 2)
sphere_node.color_index = 3
self.scene.add_node(sphere_node)
hierarchical_node = SnowFigure()
hierarchical_node.translate(-2, 0, -2)
self.scene.add_node(hierarchical_node)
def init_interaction(self):
#初始化交互操作相关的代码,之后实现
pass
def main_loop(self):
#程序主循环开始
glutMainLoop()
def render(self):
#初始化投影矩阵
self.init_view()
#启动光照
glEnable(GL_LIGHTING)
#清空颜色缓存与深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
#设置模型视图矩阵,这节课先用单位矩阵就行了。
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
#渲染场景
self.scene.render()
#每次渲染后复位光照状态
glDisable(GL_LIGHTING)
glCallList(G_OBJ_PLANE)
glPopMatrix()
#把数据刷新到显存上
glFlush()
def init_view(self):
""" 初始化投影矩阵 """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
#得到屏幕宽高比
aspect_ratio = float(xSize) / float(ySize)
#设置投影矩阵
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
#设置视口,应与窗口重合
glViewport(0, 0, xSize, ySize)
#设置透视,摄像机上下视野幅度70度
#视野范围到距离摄像机1000个单位为止。
gluPerspective(70, aspect_ratio, 0.1, 1000.0)
#摄像机镜头从原点后退15个单位
glTranslated(0, 0, -15)
if __name__ == "__main__":
viewer = Viewer()
viewer.main_loop()
四、源代码
viewer.py完整代码:
# Viewer下的实现
def move(self, x, y):
start, direction = self.get_ray(x, y)
self.scene.move_selected(start, direction, self.inverseModelView)
# Scene下的实现
def move_selected(self, start, direction, inv_modelview):
if self.selected_node is None: return
# 找到选中节点的坐标与深度(距离)
node = self.selected_node
depth = node.depth
oldloc = node.selected_loc
# 新坐标的深度保持不变
newloc = (start + direction * depth)
# 得到世界坐标系中的移动坐标差
translation = newloc - oldloc
pre_tran = numpy.array([translation[0], translation[1], translation[2], 0])
translation = inv_modelview.dot(pre_tran)
# 节点做平移变换
node.translate(translation[0], translation[1], translation[2])
node.selected_loc = newloc
五、 结果及分析
到这里我们就已经实现了一个简单的 3D 建模工具了,想一下这个程序还能在什么地方进行改进,或是增加一些新的功能?比如说:
-编写新的节点类,支持三角形网格能够组合成任意形状。
-增加一个撤销栈,支持撤销命令功能。
-能够保存/加载 3D 设计,比如保存为 DXF 3D 文件格式
-改进程序,选中目标更精准。