1. 介紹
人類(lèi)天生具有創(chuàng)造力。我們不斷設(shè)計(jì)和構(gòu)建新穎,實(shí)用和有趣的東西。在現(xiàn)代,我們編寫(xiě)軟件來(lái)協(xié)助設(shè)計(jì)和創(chuàng)作過(guò)程。計(jì)算機(jī)輔助設(shè)計(jì)(CAD)軟件允許創(chuàng)建者在構(gòu)建設(shè)計(jì)的物理版本之前設(shè)計(jì)建筑物,橋梁,視頻游戲藝術(shù),電影怪物,3D可打印對(duì)象以及許多其他東西。
CAD工具的核心是將三維設(shè)計(jì)抽象為可在二維屏幕上查看和編輯的內(nèi)容的方法。為了實(shí)現(xiàn)該定義,CAD工具必須提供三個(gè)基本功能。
- 首先,他們必須有一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)表示正在設(shè)計(jì)的對(duì)象:這是計(jì)算機(jī)對(duì)用戶正在構(gòu)建的三維世界的理解。
- 其次,CAD工具必須提供一些在用戶屏幕上顯示設(shè)計(jì)的方法。用戶正在設(shè)計(jì)具有3個(gè)維度的物理對(duì)象,但計(jì)算機(jī)屏幕只有2個(gè)維度。CAD工具必須模擬我們?nèi)绾胃兄獙?duì)象,并以用戶可以理解對(duì)象的所有3個(gè)維度的方式將它們繪制到屏幕上。
- 第三,CAD工具必須提供與所設(shè)計(jì)對(duì)象交互的方式。用戶必須能夠添加和修改設(shè)計(jì)才能產(chǎn)生所需的結(jié)果。此外,所有工具都需要一種從磁盤(pán)保存和加載設(shè)計(jì)的方法,以便用戶可以協(xié)作,共享和保存他們的工作。
特定領(lǐng)域的CAD工具為相應(yīng)領(lǐng)域的特定要求提供了許多其他功能。例如,建筑CAD工具將提供物理模擬來(lái)測(cè)試建筑物上的氣候壓力,3D打印工具將具有檢查物體是否實(shí)際上有效打印的功能,電子CAD工具將模擬通過(guò)銅的電流物理和電影特效套件將包括準(zhǔn)確模擬熱動(dòng)力學(xué)的功能。
但是,所有CAD工具必須至少包括上面討論的三個(gè)特征:表示設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu),將其顯示到屏幕的能力以及與設(shè)計(jì)交互的方法。
考慮到這一點(diǎn),讓我們探索如何在500行Python中表示3D設(shè)計(jì),將其顯示在屏幕上并與之交互。
2. 渲染指南
3D建模器中許多設(shè)計(jì)決策背后的驅(qū)動(dòng)力是渲染過(guò)程。我們希望能夠在設(shè)計(jì)中存儲(chǔ)和渲染復(fù)雜對(duì)象,但我們同時(shí)希望保持渲染代碼的復(fù)雜性較低。讓我們檢查渲染過(guò)程,并探索設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu),允許我們使用簡(jiǎn)單的渲染邏輯來(lái)存儲(chǔ)和繪制任意復(fù)雜的對(duì)象。
2.1 管理接口和主循環(huán)
在我們開(kāi)始渲染之前,我們需要設(shè)置一些東西。
- 首先,我們需要?jiǎng)?chuàng)建一個(gè)窗口來(lái)顯示我們的設(shè)計(jì)。
- 其次,我們希望與圖形驅(qū)動(dòng)程序通信以呈現(xiàn)到屏幕。我們不直接與圖形驅(qū)動(dòng)程序通信,因此我們使用一個(gè)名為OpenGL的跨平臺(tái)抽象層,以及一個(gè)名為GLUT(OpenGL Utility Toolkit)的庫(kù)來(lái)管理我們的窗口。
2.1.1 關(guān)于OpenGL的注意事項(xiàng)
OpenGL是一個(gè)用于跨平臺(tái)開(kāi)發(fā)的圖形化應(yīng)用程序編程接口。它是跨平臺(tái)開(kāi)發(fā)圖形應(yīng)用程序的標(biāo)準(zhǔn)API。OpenGL有兩個(gè)主要變體:Legacy OpenGL和Modern OpenGL。
OpenGL中的渲染基于由頂點(diǎn)和法線定義的多邊形。例如,要渲染立方體的一側(cè),我們指定4個(gè)頂點(diǎn)和邊的法線。
Legacy OpenGL提供了“固定功能管道”。通過(guò)設(shè)置全局變量,程序員可以啟用和禁用照明,著色,面部剔除等功能的自動(dòng)實(shí)現(xiàn)。然后,OpenGL會(huì)自動(dòng)使用啟用的功能呈現(xiàn)場(chǎng)景。不推薦使用此功能。
另一方面,Modern OpenGL具有可編程渲染管道,程序員可在其中編寫(xiě)在專(zhuān)用圖形硬件(GPU)上運(yùn)行的稱(chēng)為“著色器”的小程序。Modern OpenGL的可編程管道已經(jīng)取代了Legacy OpenGL。
在這個(gè)項(xiàng)目中,盡管它已被棄用,我們?nèi)匀皇褂肔egacy OpenGL。Legacy OpenGL提供的固定功能對(duì)于保持較小的代碼大小非常有用。它減少了所需的線性代數(shù)知識(shí)量,并簡(jiǎn)化了我們編寫(xiě)的代碼。
2.1.2 關(guān)于GLUT
與OpenGL捆綁在一起的GLUT允許我們創(chuàng)建操作系統(tǒng)窗口并注冊(cè)用戶界面回調(diào)。這個(gè)基本功能足以滿足我們的目的。如果我們想要一個(gè)用于窗口管理和用戶交互的功能更全面的庫(kù),我們會(huì)考慮使用像GTK或Qt這樣的完整窗口工具包。
2.1.3 創(chuàng)建Viewer類(lèi)
為了管理GLUT和OpenGL的設(shè)置,并驅(qū)動(dòng)模型的其余部分,我們創(chuàng)建了一個(gè)名為Viewer
的類(lèi)。我們使用單個(gè)Viewer實(shí)例
來(lái)管理窗口創(chuàng)建和渲染,并包含我們程序的主循環(huán)。在Viewer
初始化過(guò)程中,我們創(chuàng)建GUI窗口并初始化OpenGL。
- 函數(shù)
init_interface
創(chuàng)建將渲染建模器的窗口,并指定在需要渲染設(shè)計(jì)時(shí)要調(diào)用的函數(shù)。 - 函數(shù)
init_opengl
設(shè)置項(xiàng)目所需的OpenGL狀態(tài)。它設(shè)置矩陣,啟用背面剔除,注冊(cè)燈光以照亮場(chǎng)景,并告訴OpenGL我們希望對(duì)象被著色。 - 函數(shù)
init_scene
創(chuàng)建Scene對(duì)象并放置一些初始節(jié)點(diǎn)以使用戶啟動(dòng)。稍后我們將很快看到有關(guān)Scene
數(shù)據(jù)結(jié)構(gòu)的更多信息。 - 最后,函數(shù)
init_interaction
注冊(cè)用戶交互的回調(diào),我們稍后會(huì)討論。
初始化Viewer
后,我們調(diào)用glutMainLoop
將程序執(zhí)行轉(zhuǎn)移到GLUT。此函數(shù)永遠(yuǎn)沒(méi)有返回值。我們?cè)贕LUT事件上注冊(cè)的回調(diào)將在這些事件發(fā)生時(shí)被調(diào)用。
class Viewer(object):
def __init__(self):
""" Initialize the viewer. """
self.init_interface()
self.init_opengl()
self.init_scene()
self.init_interaction()
init_primitives()
def init_interface(self):
""" initialize the window and register the render function """
glutInit()
glutInitWindowSize(640, 480)
glutCreateWindow("3D Modeller")
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB)
glutDisplayFunc(self.render)
def init_opengl(self):
""" initialize the opengl settings to render the scene """
self.inverseModelView = numpy.identity(4)
self.modelView = numpy.identity(4)
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LESS)
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):
""" initialize the scene object and initial scene """
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 = 2
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):
""" init user interaction and callbacks """
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 main_loop(self):
glutMainLoop()
if __name__ == "__main__":
viewer = Viewer()
viewer.main_loop()
在我們深入研究render函數(shù)之前,我們先回顧一些線性代數(shù)知識(shí)。
坐標(biāo)空間
出于我們的目的,坐標(biāo)空間是一個(gè)原點(diǎn)和一組3個(gè)基矢量,通常是和
軸
點(diǎn)
3維中的任何點(diǎn)都可以表示為距離原點(diǎn)和
方向的偏移。 點(diǎn)的表示相對(duì)于該點(diǎn)所在的坐標(biāo)空間。同一點(diǎn)在不同的坐標(biāo)空間中具有不同的表示。3維中的任何點(diǎn)都可以在任何3維坐標(biāo)空間中表示。
向量
向量是和
值,分別表示
和
軸中兩個(gè)點(diǎn)之間的距離。
轉(zhuǎn)換矩陣
在計(jì)算機(jī)圖形學(xué)中,為不同類(lèi)型的點(diǎn)使用多個(gè)不同的坐標(biāo)空間是方便的。轉(zhuǎn)換矩陣將點(diǎn)從一個(gè)坐標(biāo)空間轉(zhuǎn)換為另一個(gè)坐標(biāo)空間。 為了將矢量從一個(gè)坐標(biāo)空間轉(zhuǎn)換為另一個(gè)坐標(biāo)空間,我們乘以變換矩陣
。 一些常見(jiàn)的變換矩陣是平移,縮放和旋轉(zhuǎn)。
-
Model, World, View, and Projection Coordinate Spaces
1. Transformation Pipeline
為了將項(xiàng)目繪制到屏幕,我們需要在幾個(gè)不同的坐標(biāo)空間之間進(jìn)行轉(zhuǎn)換。
圖13.1的右側(cè),包括從Eye Space到Viewport Space的所有轉(zhuǎn)換,都將由OpenGL為我們處理。
- 從Eye Space到homogeneous clip space的轉(zhuǎn)換由
gluPerspective
處理, - 轉(zhuǎn)換到normalized device space和viewport space 由
glViewport
處理。這兩個(gè)矩陣相乘并存儲(chǔ)為GL_PROJECTION矩陣。
我們不需要知道這些矩陣如何為這個(gè)項(xiàng)目工作的術(shù)語(yǔ)或細(xì)節(jié)。但是,我們需要自己管理圖表的左側(cè)。
- 我們定義了一個(gè)矩陣,它將模型中的點(diǎn)(也稱(chēng)為網(wǎng)格)從model spaces轉(zhuǎn)換為world space,稱(chēng)為模型矩陣(model matrix)。
- 我們還定義了視圖矩陣(view matrix),它從world space轉(zhuǎn)換為eye space。
在這個(gè)項(xiàng)目中,我們將這兩個(gè)矩陣組合起來(lái)以獲得ModelView矩陣。
要了解有關(guān)完整圖形渲染管道以及所涉及的坐標(biāo)空間的更多信息,請(qǐng)參閱實(shí)時(shí)渲染的第2章或其他介紹性計(jì)算機(jī)圖形手冊(cè)。
2.2 使用Viewer進(jìn)行渲染(Rendering with the Viewer)
該render
函數(shù)首先設(shè)置需要在渲染時(shí)完成的任意OpenGL狀態(tài)。
- 它通過(guò)
init_view
并初始化投影矩陣, - 使用來(lái)自交互( interaction)成員的數(shù)據(jù),
- 使用從 scene space轉(zhuǎn)換為world space的變換矩陣初始化ModelView矩陣。
我們將在下面看到有關(guān)Interaction類(lèi)
的更多信息。
- 它使用
glClear
清除屏幕,它告訴場(chǎng)景(scene)渲染自己,然后渲染單位網(wǎng)格。
我們?cè)阡秩揪W(wǎng)格之前禁用OpenGL的光照。禁用照明后,OpenGL會(huì)渲染純色項(xiàng)目,而不是模擬光源。這樣,網(wǎng)格與場(chǎng)景具有視覺(jué)差異。最后,glFlush
向圖形驅(qū)動(dòng)程序發(fā)出信號(hào),告知我們已準(zhǔn)備好將緩沖區(qū)刷新并顯示到屏幕上。
# class Viewer
def render(self):
""" The render pass for the scene """
self.init_view()
glEnable(GL_LIGHTING)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# Load the modelview matrix from the current state of the trackball
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
loc = self.interaction.translation
glTranslated(loc[0], loc[1], loc[2])
glMultMatrixf(self.interaction.trackball.matrix)
# store the inverse of the current modelview.
currentModelView = numpy.array(glGetFloatv(GL_MODELVIEW_MATRIX))
self.modelView = numpy.transpose(currentModelView)
self.inverseModelView = inv(numpy.transpose(currentModelView))
# render the scene. This will call the render function for each object
# in the scene
self.scene.render()
# draw the grid
glDisable(GL_LIGHTING)
glCallList(G_OBJ_PLANE)
glPopMatrix()
# flush the buffers so that the scene can be drawn
glFlush()
def init_view(self):
""" initialize the projection matrix """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
aspect_ratio = float(xSize) / float(ySize)
# load the projection matrix. Always the same
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glViewport(0, 0, xSize, ySize)
gluPerspective(70, aspect_ratio, 0.1, 1000.0)
glTranslated(0, 0, -15)
2.3 渲染什么:場(chǎng)景(What to Render: The Scene)
現(xiàn)在我們已經(jīng)初始化了渲染管道來(lái)處理世界坐標(biāo)空間中的繪圖,我們要渲染什么?回想一下,我們的目標(biāo)是設(shè)計(jì)一個(gè)由3D模型組成的設(shè)計(jì)。我們需要一個(gè)包含設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu),我們需要使用這個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)呈現(xiàn)設(shè)計(jì)。請(qǐng)注意,我們self.scene.render()
從查看器的渲染循環(huán)中調(diào)用。 scene是什么?
Scene類(lèi)
是接口,我們用它來(lái)表示設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)。它抽象出數(shù)據(jù)結(jié)構(gòu)的細(xì)節(jié),并提供與設(shè)計(jì)交互所需的必要接口功能,包括渲染,添加項(xiàng)目和操作項(xiàng)目的功能。viewer擁有一個(gè)Scene
對(duì)象。 Scene
實(shí)例保留場(chǎng)景中所有項(xiàng)目的列表,稱(chēng)為node_list
。 它還跟蹤所選項(xiàng)目。Scene
上的渲染函數(shù)只是在node_list
的每個(gè)成員上調(diào)用渲染。
class Scene(object):
# the default depth from the camera to place an object at
PLACE_DEPTH = 15.0
def __init__(self):
# The scene keeps a list of nodes that are displayed
self.node_list = list()
# Keep track of the currently selected node.
# Actions may depend on whether or not something is selected
self.selected_node = None
def add_node(self, node):
""" Add a new node to the scene """
self.node_list.append(node)
def render(self):
""" Render the scene. """
for node in self.node_list:
node.render()
2.4 節(jié)點(diǎn)(Nodes)
在Scene的render
函數(shù)中,我們?cè)赟cene的node_list
中的每個(gè)項(xiàng)目上調(diào)用render
。但該清單的要素是什么?我們稱(chēng)它們?yōu)?strong>節(jié)點(diǎn)。從概念上講,節(jié)點(diǎn)是可以放置在場(chǎng)景中的任何東西。在面向?qū)ο蟮能浖校覀儗?code>Node編寫(xiě)為抽象基類(lèi)。表示要放置在Scene
中的對(duì)象的任何類(lèi)都將從Node
繼承。這個(gè)基類(lèi)允許我們抽象地推斷場(chǎng)景。代碼庫(kù)的其余部分不需要知道它顯示的對(duì)象的細(xì)節(jié);它只需要知道它們屬于Node
類(lèi)。
每種類(lèi)型的Node
都定義了自己的行為,用于呈現(xiàn)自身和任何其他交互。節(jié)點(diǎn)跟蹤關(guān)于其自身的重要數(shù)據(jù):平移矩陣,比例矩陣,顏色等。將節(jié)點(diǎn)的平移矩陣乘以其縮放矩陣,得到從節(jié)點(diǎn)的模型坐標(biāo)空間到世界坐標(biāo)空間的變換矩陣。該節(jié)點(diǎn)還存儲(chǔ)軸對(duì)齊的邊界框(AABB)。當(dāng)我們?cè)谙旅嬗懻撨x擇時(shí),我們會(huì)看到更多有關(guān)AABB的信息。
Node
最簡(jiǎn)單的具體實(shí)現(xiàn)是基元的。基元是可以添加到場(chǎng)景中的單個(gè)實(shí)體形狀。在這個(gè)項(xiàng)目中,基元是Cube
和Sphere
。
class Node(object):
""" Base class for scene elements """
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):
""" renders the item to the screen """
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: # emit light if the node is 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 render_self(self):
raise NotImplementedError(
"The Abstract Node Class doesn't define 'render_self'")
class Primitive(Node):
def __init__(self):
super(Primitive, self).__init__()
self.call_list = None
def render_self(self):
glCallList(self.call_list)
class Sphere(Primitive):
""" Sphere primitive """
def __init__(self):
super(Sphere, self).__init__()
self.call_list = G_OBJ_SPHERE
class Cube(Primitive):
""" Cube primitive """
def __init__(self):
super(Cube, self).__init__()
self.call_list = G_OBJ_CUBE
渲染節(jié)點(diǎn)基于每個(gè)節(jié)點(diǎn)存儲(chǔ)的變換矩陣。節(jié)點(diǎn)的變換矩陣是其縮放矩陣與其平移矩陣的組合。無(wú)論節(jié)點(diǎn)類(lèi)型如何,
- 渲染的第一步是將OpenGL ModelView矩陣設(shè)置為變換矩陣,以便從模型坐標(biāo)空間轉(zhuǎn)換為視圖坐標(biāo)空間。
- 一旦OpenGL矩陣是最新的,我們調(diào)用
render_self
告訴節(jié)點(diǎn)進(jìn)行必要的OpenGL調(diào)用以繪制自己。 - 最后,我們撤消對(duì)此特定節(jié)點(diǎn)對(duì)OpenGL狀態(tài)所做的任何更改。我們?cè)贠penGL中使用
glPushMatrix
和glPopMatrix
函數(shù)來(lái)保存和恢復(fù)ModelView矩陣在渲染節(jié)點(diǎn)之前和之后的狀態(tài)。請(qǐng)注意,節(jié)點(diǎn)存儲(chǔ)其顏色,位置和比例,并在渲染之前將這些應(yīng)用于OpenGL狀態(tài)。
如果當(dāng)前選擇了節(jié)點(diǎn),我們將其照亮。這樣,用戶可以看到他們選擇了哪個(gè)節(jié)點(diǎn)。
要渲染基元,我們使用OpenGL的調(diào)用列表功能。 OpenGL調(diào)用列表是一系列OpenGL調(diào)用,它們被定義一次并在單個(gè)名稱(chēng)下捆綁在一起。可以使用glCallList(LIST_NAME)
調(diào)度調(diào)用。每個(gè)基元(Sphere
和Cube
)定義渲染它所需的調(diào)用列表(未顯示)。
例如,立方體的調(diào)用列表繪制立方體的6個(gè)面,中心位于原點(diǎn),邊緣恰好為1個(gè)單位長(zhǎng)。
# Left face
((-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (-0.5, 0.5, -0.5)),
# Back face
((-0.5, -0.5, -0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (0.5, -0.5, -0.5)),
# Right face
((0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (0.5, 0.5, 0.5), (0.5, -0.5, 0.5)),
# Front face
((-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)),
# Bottom face
((-0.5, -0.5, 0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, -0.5, 0.5)),
# Top face
((-0.5, 0.5, -0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (0.5, 0.5, -0.5))
僅使用基元對(duì)于建模應(yīng)用程序?qū)⑹欠浅S邢薜摹?3D模型通常由多個(gè)基元(或三角形網(wǎng)格,在本項(xiàng)目范圍之外)組成。幸運(yùn)的是,我們?cè)O(shè)計(jì)的Node
類(lèi)有助于由多個(gè)基元組成的Scene
節(jié)點(diǎn)。實(shí)際上,我們可以支持任意節(jié)點(diǎn)分組,而不會(huì)增加復(fù)雜性。
作為動(dòng)機(jī),讓我們考慮一個(gè)非常基本的人物:一個(gè)典型的雪人,由三個(gè)球體組成。盡管該圖由三個(gè)獨(dú)立的基元組成,但我們希望能夠?qū)⑵湟暈閱蝹€(gè)對(duì)象。
我們創(chuàng)建了一個(gè)名為HierarchicalNode
的類(lèi),一個(gè)包含其他節(jié)點(diǎn)的Node
。它管理一個(gè)“子”列表。分層節(jié)點(diǎn)的render_self
函數(shù)只是在每個(gè)子節(jié)點(diǎn)上調(diào)用render_self
。使用HierarchicalNode
類(lèi),可以非常輕松地將圖形添加到場(chǎng)景中。現(xiàn)在,定義雪人就像指定構(gòu)成它的形狀及其相對(duì)位置和大小一樣簡(jiǎn)單。
class HierarchicalNode(Node):
def __init__(self):
super(HierarchicalNode, self).__init__()
self.child_nodes = []
def render_self(self):
for child in self.child_nodes:
child.render()
class SnowFigure(HierarchicalNode):
def __init__(self):
super(SnowFigure, self).__init__()
self.child_nodes = [Sphere(), Sphere(), Sphere()]
self.child_nodes[0].translate(0, -0.6, 0) # scale 1.0
self.child_nodes[1].translate(0, 0.1, 0)
self.child_nodes[1].scaling_matrix = numpy.dot(
self.scaling_matrix, scaling([0.8, 0.8, 0.8]))
self.child_nodes[2].translate(0, 0.75, 0)
self.child_nodes[2].scaling_matrix = numpy.dot(
self.scaling_matrix, scaling([0.7, 0.7, 0.7]))
for child_node in self.child_nodes:
child_node.color_index = color.MIN_COLOR
self.aabb = AABB([0.0, 0.0, 0.0], [0.5, 1.1, 0.5])
你可能會(huì)發(fā)現(xiàn)Node
對(duì)象形成了樹(shù)數(shù)據(jù)結(jié)構(gòu)。 render
函數(shù)通過(guò)分層節(jié)點(diǎn)執(zhí)行深度優(yōu)先遍歷樹(shù)。 當(dāng)它遍歷時(shí),它保留了一堆ModelView
矩陣,用于轉(zhuǎn)換到世界空間。 在每一步中,它將當(dāng)前的ModelView
矩陣推送到堆棧上,當(dāng)它完成所有子節(jié)點(diǎn)的渲染時(shí),它會(huì)將矩陣從堆棧中彈出,將父節(jié)點(diǎn)的ModelView
矩陣保留在堆棧的頂部。
通過(guò)以這種方式使Node
類(lèi)可擴(kuò)展,我們可以向場(chǎng)景添加新類(lèi)型的形狀,而無(wú)需更改任何其他用于場(chǎng)景操作和渲染的代碼。 使用節(jié)點(diǎn)概念來(lái)抽象出一個(gè)Scene
對(duì)象可能有許多子節(jié)點(diǎn)的事實(shí)被稱(chēng)為復(fù)合設(shè)計(jì)模式。
2.5 用戶交互(User Interaction)
現(xiàn)在我們的建模器能夠存儲(chǔ)和顯示場(chǎng)景,我們需要一種與它交互的方法。我們需要促進(jìn)兩種類(lèi)型的交互。首先,我們需要改變場(chǎng)景的觀看視角的能力。我們希望能夠在場(chǎng)景周?chē)苿?dòng)眼睛或相機(jī)。其次,我們需要能夠添加新節(jié)點(diǎn)并修改場(chǎng)景中的節(jié)點(diǎn)。
為了實(shí)現(xiàn)用戶交互,我們需要知道用戶何時(shí)按下按鍵或移動(dòng)鼠標(biāo)。幸運(yùn)的是,操作系統(tǒng)已經(jīng)知道這些事件何時(shí)發(fā)生。 GLUT允許我們?cè)诎l(fā)生特定事件時(shí)注冊(cè)要調(diào)用的函數(shù)。我們編寫(xiě)函數(shù)來(lái)解釋按鍵和鼠標(biāo)移動(dòng),并告訴GLUT在按下相應(yīng)鍵時(shí)調(diào)用這些函數(shù)。一旦我們知道用戶正在按哪些鍵,我們就需要解釋輸入并將預(yù)期的動(dòng)作應(yīng)用到場(chǎng)景中。
可以在Interaction
類(lèi)中找到用于偵聽(tīng)操作系統(tǒng)事件和解釋其含義的邏輯。我們之前寫(xiě)的Viewer
類(lèi)擁有Interaction
的單個(gè)實(shí)例。我們將使用GLUT回調(diào)機(jī)制來(lái)記錄
- 按下鼠標(biāo)按鈕時(shí)(
glutMouseFunc
), - 鼠標(biāo)移動(dòng)時(shí)(
glutMotionFun
c), - 按下鍵盤(pán)按鈕(
glutKeyboardFunc
) - 按下箭頭鍵時(shí)(
glutSpecialFunc
)
要調(diào)用的函數(shù)。我們將很快看到處理輸入事件的函數(shù)。
class Interaction(object):
def __init__(self):
""" Handles user interaction """
# currently pressed mouse button
self.pressed = None
# the current location of the camera
self.translation = [0, 0, 0, 0]
# the trackball to calculate rotation
self.trackball = trackball.Trackball(theta = -25, distance=15)
# the current mouse location
self.mouse_loc = None
# Unsophisticated callback mechanism
self.callbacks = defaultdict(list)
self.register()
def register(self):
""" register callbacks with glut """
glutMouseFunc(self.handle_mouse_button)
glutMotionFunc(self.handle_mouse_move)
glutKeyboardFunc(self.handle_keystroke)
glutSpecialFunc(self.handle_keystroke)
2.5.1 操作系統(tǒng)回調(diào)
為了解釋用戶輸入的意義,我們需要結(jié)合鼠標(biāo)位置,鼠標(biāo)按鈕和鍵盤(pán)的知識(shí)。 因?yàn)閷⒂脩糨斎虢忉尀橛幸饬x的動(dòng)作需要多行代碼,所以我們將其封裝在一個(gè)單獨(dú)的類(lèi)中,遠(yuǎn)離主代碼路徑。 Interaction
類(lèi)隱藏了與代碼庫(kù)其余部分無(wú)關(guān)的復(fù)雜性,并將操作系統(tǒng)事件轉(zhuǎn)換為應(yīng)用程序級(jí)事件。
# class Interaction
def translate(self, x, y, z):
""" translate the camera """
self.translation[0] += x
self.translation[1] += y
self.translation[2] += z
def handle_mouse_button(self, button, mode, x, y):
""" Called when the mouse button is pressed or released """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - y # invert the y coordinate because OpenGL is inverted
self.mouse_loc = (x, y)
if mode == GLUT_DOWN:
self.pressed = button
if button == GLUT_RIGHT_BUTTON:
pass
elif button == GLUT_LEFT_BUTTON: # pick
self.trigger('pick', x, y)
elif button == 3: # scroll up
self.translate(0, 0, 1.0)
elif button == 4: # scroll up
self.translate(0, 0, -1.0)
else: # mouse button release
self.pressed = None
glutPostRedisplay()
def handle_mouse_move(self, x, screen_y):
""" Called when the mouse is moved """
xSize, ySize = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)
y = ySize - screen_y # invert the y coordinate because OpenGL is inverted
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:
# ignore the updated camera loc because we want to always
# rotate around the origin
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):
""" Called on keyboard input from the user """
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.5.2 內(nèi)部回調(diào)
在上面的代碼片段中,你會(huì)注意到當(dāng)Interaction
實(shí)例解釋用戶操作時(shí),它會(huì)使用描述操作類(lèi)型的字符串調(diào)用self.trigger
。
Interaction
類(lèi)上的觸發(fā)器函數(shù)是我們將用于處理應(yīng)用程序級(jí)事件的簡(jiǎn)單回調(diào)系統(tǒng)的一部分。 回想一下,Viewer
類(lèi)上的init_interaction
函數(shù)通過(guò)調(diào)用register_callback
來(lái)注冊(cè)Interaction
實(shí)例上的回調(diào)。
# class Interaction
def register_callback(self, name, func):
self.callbacks[name].append(func)
當(dāng)用戶界面代碼需要在場(chǎng)景上觸發(fā)事件時(shí),Interaction
類(lèi)會(huì)調(diào)用它為該特定事件保存的所有已保存的回調(diào):
# class Interaction
def trigger(self, name, *args, **kwargs):
for func in self.callbacks[name]:
func(*args, **kwargs)
這個(gè)應(yīng)用程序級(jí)回調(diào)系統(tǒng)抽象出系統(tǒng)其余部分需要了解操作系統(tǒng)輸入。 每個(gè)應(yīng)用程序級(jí)回調(diào)代表應(yīng)用程序中的一個(gè)有意義的請(qǐng)求。 Interaction
類(lèi)充當(dāng)操作系統(tǒng)事件和應(yīng)用程序級(jí)事件之間的轉(zhuǎn)換器。 這意味著如果我們決定除了GLUT之外還將建模器移植到另一個(gè)工具包,我們只需要將一個(gè)類(lèi)替換,該類(lèi)將來(lái)自新工具包的輸入轉(zhuǎn)換為同一組有意義的應(yīng)用程序級(jí)回調(diào)。 我們?cè)诒?3.1中使用了回調(diào)和參數(shù)。
2.6 與場(chǎng)景交互
使用我們的回調(diào)機(jī)制,我們可以從Interaction
類(lèi)接收有關(guān)用戶輸入事件的有用信息。我們準(zhǔn)備將這些操作應(yīng)用到場(chǎng)景中。
2.6.1 移動(dòng)場(chǎng)景
在這個(gè)項(xiàng)目中,我們通過(guò)改變場(chǎng)景來(lái)完成相機(jī)運(yùn)動(dòng)。換句話說(shuō),相機(jī)處于固定位置,用戶輸入移動(dòng)場(chǎng)景而不是移動(dòng)相機(jī)。相機(jī)放置在[0,0,-15]并面向世界空間原點(diǎn)。 (或者,我們可以更改透視矩陣來(lái)移動(dòng)相機(jī)而不是場(chǎng)景。這個(gè)設(shè)計(jì)決定對(duì)項(xiàng)目的其余部分影響很小。)重新審視Viewer
中的渲染功能,我們看到交互狀態(tài)用于在渲染場(chǎng)景之前轉(zhuǎn)換OpenGL矩陣狀態(tài)。與場(chǎng)景有兩種類(lèi)型的交互:旋轉(zhuǎn)和平移。
2.6.2 使用軌跡球旋轉(zhuǎn)場(chǎng)景
我們使用軌跡球算法完成場(chǎng)景的旋轉(zhuǎn)。軌跡球是一個(gè)直觀的界面,用于以三維方式操縱場(chǎng)景。從概念上講,軌跡球界面的功能就像場(chǎng)景位于透明地球儀內(nèi)部一樣。將手放在地球表面并推動(dòng)它會(huì)使地球旋轉(zhuǎn)。同樣,單擊鼠標(biāo)右鍵并在屏幕上移動(dòng)它會(huì)旋轉(zhuǎn)場(chǎng)景。你可以在OpenGL Wiki上找到有關(guān)軌跡球理論的更多信息。在這個(gè)項(xiàng)目中,我們使用作為Glumpy的一部分提供的軌跡球?qū)崿F(xiàn)。
我們使用drag_to
函數(shù)與軌跡球交互,鼠標(biāo)的當(dāng)前位置作為起始位置,鼠標(biāo)位置的變化作為參數(shù)。
self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)
生成的旋轉(zhuǎn)矩陣是渲染場(chǎng)景時(shí)viewer中的trackball.matrix
。
2.6.3 旁白:四元數(shù)
旋轉(zhuǎn)是以兩種方式之一表示。第一個(gè)是圍繞每個(gè)軸的旋轉(zhuǎn)值;你可以將它存儲(chǔ)為3元組的浮點(diǎn)數(shù)。旋轉(zhuǎn)的另一個(gè)常見(jiàn)表示是四元數(shù),由具有和
坐標(biāo)的向量組成的元素,以及w旋轉(zhuǎn)。使用四元數(shù)比每軸旋轉(zhuǎn)有許多好處;特別是,它們?cè)跀?shù)值上更穩(wěn)定。使用四元數(shù)避免了萬(wàn)向節(jié)鎖定等問(wèn)題。四元數(shù)的缺點(diǎn)是它們不太直觀,難以理解。如果你希望了解有關(guān)四元數(shù)的更多信息,請(qǐng)參閱此說(shuō)明。
軌跡球?qū)崿F(xiàn)通過(guò)在內(nèi)部使用四元數(shù)來(lái)存儲(chǔ)場(chǎng)景的旋轉(zhuǎn)來(lái)避免萬(wàn)向節(jié)鎖定。幸運(yùn)的是,我們不需要直接使用四元數(shù),因?yàn)檐壽E球上的矩陣成員將旋轉(zhuǎn)轉(zhuǎn)換為矩陣。
2.6.4 翻轉(zhuǎn)場(chǎng)景
翻譯場(chǎng)景(即滑動(dòng)場(chǎng)景)比旋轉(zhuǎn)場(chǎng)景簡(jiǎn)單得多。使用鼠標(biāo)滾輪和鼠標(biāo)左鍵提供場(chǎng)景轉(zhuǎn)換。鼠標(biāo)左鍵可以在和
坐標(biāo)中平移場(chǎng)景。滾動(dòng)鼠標(biāo)滾輪可以在z坐標(biāo)(朝向或遠(yuǎn)離攝像機(jī))中平移場(chǎng)景。
Interaction
類(lèi)存儲(chǔ)當(dāng)前場(chǎng)景轉(zhuǎn)換并使用translate
函數(shù)對(duì)其進(jìn)行修改。查看器在渲染期間檢索交互相機(jī)位置以在glTranslated
調(diào)用中使用。
2.6.5 選擇場(chǎng)景對(duì)象
既然用戶可以移動(dòng)和旋轉(zhuǎn)整個(gè)場(chǎng)景以獲得他們想要的視角,下一步就是允許用戶修改和操縱構(gòu)成場(chǎng)景的對(duì)象。
為了讓用戶操縱場(chǎng)景中的對(duì)象,他們需要能夠選擇項(xiàng)目。
要選擇項(xiàng)目,我們使用當(dāng)前投影矩陣生成表示鼠標(biāo)單擊的光線,就像鼠標(biāo)指針將光線射入場(chǎng)景一樣。所選節(jié)點(diǎn)是與光線相交的攝像機(jī)最近的節(jié)點(diǎn)。因此,拾取問(wèn)題減少了在場(chǎng)景中找到光線和節(jié)點(diǎn)之間的交叉點(diǎn)的問(wèn)題。所以問(wèn)題是:我們?nèi)绾闻袛喙饩€是否擊中節(jié)點(diǎn)?
準(zhǔn)確地計(jì)算射線是否與節(jié)點(diǎn)相交在代碼復(fù)雜性和性能方面都是一個(gè)具有挑戰(zhàn)性的問(wèn)題。我們需要為每種類(lèi)型的基元編寫(xiě)一個(gè)光線對(duì)象交叉檢查。對(duì)于具有多個(gè)面的復(fù)雜網(wǎng)格幾何的場(chǎng)景節(jié)點(diǎn),計(jì)算精確的光線 - 對(duì)象交叉將需要針對(duì)每個(gè)面測(cè)試光線并且計(jì)算上是昂貴的。
為了保持代碼緊湊和性能合理,我們使用簡(jiǎn)單,快速的近似來(lái)進(jìn)行光線 - 物體相交測(cè)試。在我們的實(shí)現(xiàn)中,每個(gè)節(jié)點(diǎn)都存儲(chǔ)一個(gè)軸對(duì)齊的邊界框(AABB),它是它占據(jù)的空間的近似值。為了測(cè)試光線是否與節(jié)點(diǎn)相交,我們測(cè)試光線是否與節(jié)點(diǎn)的AABB相交。此實(shí)現(xiàn)意味著所有節(jié)點(diǎn)共享相同的交叉測(cè)試代碼,這意味著所有節(jié)點(diǎn)類(lèi)型的性能成本都是恒定的。
# class Viewer
def get_ray(self, x, y):
"""
Generate a ray beginning at the near plane, in the direction that
the x, y coordinates are facing
Consumes: x, y coordinates of mouse on screen
Return: start, direction of the ray
"""
self.init_view()
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# get two points on the line.
start = numpy.array(gluUnProject(x, y, 0.001))
end = numpy.array(gluUnProject(x, y, 0.999))
# convert those points into a ray
direction = end - start
direction = direction / norm(direction)
return (start, direction)
def pick(self, x, y):
""" Execute pick of an object. Selects an object in the scene. """
start, direction = self.get_ray(x, y)
self.scene.pick(start, direction, self.modelView)
為了確定單擊了哪個(gè)節(jié)點(diǎn),我們遍歷場(chǎng)景以測(cè)試光線是否到達(dá)任何節(jié)點(diǎn)。 我們?nèi)∠x擇當(dāng)前選定的節(jié)點(diǎn),然后選擇最接近光線原點(diǎn)的交點(diǎn)的節(jié)點(diǎn)。
# class Scene
def pick(self, start, direction, mat):
"""
Execute selection.
start, direction describe a Ray.
mat is the inverse of the current modelview matrix for the scene.
"""
if self.selected_node is not None:
self.selected_node.select(False)
self.selected_node = None
# Keep track of the closest hit.
mindist = sys.maxint
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 we hit something, keep track of it.
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
類(lèi)中,pick
函數(shù)測(cè)試光線是否與Node
的軸對(duì)齊邊界框相交。 如果選擇了節(jié)點(diǎn),則select
函數(shù)切換節(jié)點(diǎn)的選定狀態(tài)。 請(qǐng)注意,AABB的ray_hit
函數(shù)接受框的坐標(biāo)空間和光線的坐標(biāo)空間之間的變換矩陣作為第三個(gè)參數(shù)。 在進(jìn)行ray_hit
函數(shù)調(diào)用之前,每個(gè)節(jié)點(diǎn)都將自己的轉(zhuǎn)換應(yīng)用于矩陣。
# class Node
def pick(self, start, direction, mat):
"""
Return whether or not the ray hits the object
Consume:
start, direction form the ray to check
mat is the modelview matrix to transform the ray by
"""
# transform the modelview matrix by the current translation
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
def select(self, select=None):
""" Toggles or sets selected state """
if select is not None:
self.selected = select
else:
self.selected = not self.selected
ray-AABB選擇方法非常易于理解和實(shí)現(xiàn)。 但是,在某些情況下結(jié)果是錯(cuò)誤的。
例如,在
Sphere
基元的情況下,球體本身僅接觸每個(gè)AABB面部中心的AABB。 但是,如果用戶點(diǎn)`Sphere AABB的角落,即使用戶打算通過(guò)Sphere點(diǎn)擊其后面的某些東西,也會(huì)檢測(cè)到Sphere的碰撞(圖13.3)。
復(fù)雜性,性能和準(zhǔn)確性之間的這種折衷在計(jì)算機(jī)圖形學(xué)和軟件工程的許多領(lǐng)域中是常見(jiàn)的。
2.6.6 修改場(chǎng)景對(duì)象
接下來(lái),我們希望允許用戶操作所選節(jié)點(diǎn)。 他們可能想要移動(dòng),調(diào)整大小或更改所選節(jié)點(diǎn)的顏色。 當(dāng)用戶輸入操作節(jié)點(diǎn)的命令時(shí),Interaction
類(lèi)將輸入轉(zhuǎn)換為用戶想要的操作,并調(diào)用相應(yīng)的回調(diào)。
當(dāng)Viewer
收到其中一個(gè)事件的回調(diào)時(shí),它會(huì)調(diào)用Scene
上的相應(yīng)函數(shù),然后將該轉(zhuǎn)換應(yīng)用于當(dāng)前選定的Node
。
# class Viewer
def move(self, x, y):
""" Execute a move command on the scene. """
start, direction = self.get_ray(x, y)
self.scene.move_selected(start, direction, self.inverseModelView)
def rotate_color(self, forward):
"""
Rotate the color of the selected Node.
Boolean 'forward' indicates direction of rotation.
"""
self.scene.rotate_selected_color(forward)
def scale(self, up):
""" Scale the selected Node. Boolean up indicates scaling larger."""
self.scene.scale_selected(up)
2.6.7 改變顏色
使用可能的顏色列表完成顏色操作。 用戶可以使用箭頭鍵在列表中循環(huán)。 場(chǎng)景將顏色更改命令調(diào)度到當(dāng)前選定的節(jié)點(diǎn)。
# class Scene
def rotate_selected_color(self, forwards):
""" Rotate the color of the currently selected node """
if self.selected_node is None: return
self.selected_node.rotate_color(forwards)
每個(gè)節(jié)點(diǎn)存儲(chǔ)其當(dāng)前顏色。rotate_color
函數(shù)只是修改節(jié)點(diǎn)的當(dāng)前顏色。 渲染節(jié)點(diǎn)時(shí),顏色將通過(guò)glColor
傳遞給OpenGL。
# class Node
def rotate_color(self, forwards):
self.color_index += 1 if forwards else -1
if self.color_index > color.MAX_COLOR:
self.color_index = color.MIN_COLOR
if self.color_index < color.MIN_COLOR:
self.color_index = color.MAX_COLOR
2.6.8 縮放節(jié)點(diǎn)
與顏色一樣,場(chǎng)景會(huì)調(diào)度對(duì)所選節(jié)點(diǎn)的任何縮放修改(如果有)
# class Scene
def scale_selected(self, up):
""" Scale the current selection """
if self.selected_node is None: return
self.selected_node.scale(up)
每個(gè)節(jié)點(diǎn)存儲(chǔ)一個(gè)存儲(chǔ)其比例的當(dāng)前矩陣。 在這些相應(yīng)方向上按參數(shù)和
縮放的矩陣是:
當(dāng)用戶修改節(jié)點(diǎn)的比例時(shí),將得到的縮放矩陣乘以該節(jié)點(diǎn)的當(dāng)前縮放矩陣。
# class Node
def scale(self, up):
s = 1.1 if up else 0.9
self.scaling_matrix = numpy.dot(self.scaling_matrix, scaling([s, s, s]))
self.aabb.scale(s)
在給定和
縮放因子的列表的情況下,
scaling
函數(shù)返回這樣的矩陣。
def scaling(scale):
s = numpy.identity(4)
s[0, 0] = scale[0]
s[1, 1] = scale[1]
s[2, 2] = scale[2]
s[3, 3] = 1
return s
2.6.9 移動(dòng)節(jié)點(diǎn)
為了翻轉(zhuǎn)節(jié)點(diǎn),我們使用我們用于拾取的相同射線計(jì)算。 我們將表示當(dāng)前鼠標(biāo)位置的光線傳遞給場(chǎng)景的move
函數(shù)。 節(jié)點(diǎn)的新位置應(yīng)該在光線上。 為了確定放置節(jié)點(diǎn)的光線的位置,我們需要知道節(jié)點(diǎn)與相機(jī)的距離。 由于我們?cè)谶x擇節(jié)點(diǎn)時(shí)存儲(chǔ)了節(jié)點(diǎn)的位置和距離(在pick
函數(shù)中),我們可以在此處使用該數(shù)據(jù)。 我們找到與目標(biāo)射線上相機(jī)距離相同的點(diǎn),并計(jì)算新舊位置之間的矢量差異。 然后,我們通過(guò)結(jié)果向量轉(zhuǎn)換節(jié)點(diǎn)。
# class Scene
def move_selected(self, start, direction, inv_modelview):
"""
Move the selected node, if there is one.
Consume:
start, direction describes the Ray to move to
mat is the modelview matrix for the scene
"""
if self.selected_node is None: return
# Find the current depth and location of the selected node
node = self.selected_node
depth = node.depth
oldloc = node.selected_loc
# The new location of the node is the same depth along the new ray
newloc = (start + direction * depth)
# transform the translation with the modelview matrix
translation = newloc - oldloc
pre_tran = numpy.array([translation[0], translation[1], translation[2], 0])
translation = inv_modelview.dot(pre_tran)
# translate the node and track its location
node.translate(translation[0], translation[1], translation[2])
node.selected_loc = newloc
請(qǐng)注意,新舊位置是在攝像機(jī)坐標(biāo)空間中定義的。 我們需要在世界坐標(biāo)空間中定義我們的翻轉(zhuǎn)。 因此,我們通過(guò)乘以模型視圖矩陣的逆將camera space轉(zhuǎn)換轉(zhuǎn)換為world space轉(zhuǎn)換。
與比例一樣,每個(gè)節(jié)點(diǎn)存儲(chǔ)表示其轉(zhuǎn)換的矩陣。 翻轉(zhuǎn)矩陣如下所示:
翻轉(zhuǎn)節(jié)點(diǎn)時(shí),我們?yōu)楫?dāng)前翻轉(zhuǎn)構(gòu)建一個(gè)新的翻轉(zhuǎn)矩陣,并將其乘以節(jié)點(diǎn)的翻轉(zhuǎn)矩陣,以便在渲染過(guò)程中使用。
# class Node
def translate(self, x, y, z):
self.translation_matrix = numpy.dot(
self.translation_matrix,
translation([x, y, z]))
translation
函數(shù)返回給定表示和
平移距離的列表的轉(zhuǎn)換矩陣。
def translation(displacement):
t = numpy.identity(4)
t[0, 3] = displacement[0]
t[1, 3] = displacement[1]
t[2, 3] = displacement[2]
return t
2.6.10 放置節(jié)點(diǎn)
節(jié)點(diǎn)放置使用拾取和轉(zhuǎn)換的技術(shù)。 我們對(duì)當(dāng)前鼠標(biāo)位置使用相同的光線計(jì)算來(lái)確定節(jié)點(diǎn)的放置位置。
# class Viewer
def place(self, shape, x, y):
""" Execute a placement of a new primitive into the scene. """
start, direction = self.get_ray(x, y)
self.scene.place(shape, start, direction, self.inverseModelView)
要放置新節(jié)點(diǎn),我們首先創(chuàng)建相應(yīng)類(lèi)型節(jié)點(diǎn)的新實(shí)例并將其添加到場(chǎng)景中。 我們希望將節(jié)點(diǎn)放在用戶光標(biāo)下面,這樣我們就可以在與攝像機(jī)相距固定距離的光線上找到一個(gè)點(diǎn)。 同樣,光線在相機(jī)空間中表示,因此我們將得到的平移向量轉(zhuǎn)換為世界坐標(biāo)空間,方法是將其乘以逆模型視圖矩陣。 最后,我們通過(guò)計(jì)算的向量轉(zhuǎn)換新節(jié)點(diǎn)。
# class Scene
def place(self, shape, start, direction, inv_modelview):
"""
Place a new node.
Consume:
shape the shape to add
start, direction describes the Ray to move to
inv_modelview is the inverse modelview matrix for the scene
"""
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)
# place the node at the cursor in camera-space
translation = (start + direction * self.PLACE_DEPTH)
# convert the translation to world-space
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])
3. 總結(jié)
在這個(gè)項(xiàng)目中,
我們了解了如何開(kāi)發(fā)可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)來(lái)表示場(chǎng)景中的對(duì)象。我們注意到使用Composite設(shè)計(jì)模式和基于樹(shù)的數(shù)據(jù)結(jié)構(gòu)可以輕松遍歷場(chǎng)景進(jìn)行渲染,并允許我們添加新類(lèi)型的節(jié)點(diǎn)而不會(huì)增加復(fù)雜性。
我們利用這種數(shù)據(jù)結(jié)構(gòu)將設(shè)計(jì)渲染到屏幕上,并在場(chǎng)景圖的遍歷中操縱OpenGL矩陣。我們?yōu)閼?yīng)用程序級(jí)事件構(gòu)建了一個(gè)非常簡(jiǎn)單的回調(diào)系統(tǒng),并使用它來(lái)封裝操作系統(tǒng)事件的處理。
我們討論了光線對(duì)象碰撞檢測(cè)的可能實(shí)現(xiàn),以及正確性,復(fù)雜性和性能之間的權(quán)衡。
最后,我們實(shí)現(xiàn)了操作場(chǎng)景內(nèi)容的方法。
你可以在生產(chǎn)3D軟件中找到這些相同的基本構(gòu)建塊。場(chǎng)景圖結(jié)構(gòu)和相對(duì)坐標(biāo)空間可以在許多類(lèi)型的3D圖形應(yīng)用程序中找到,從CAD工具到游戲引擎。該項(xiàng)目的一個(gè)主要簡(jiǎn)化是在用戶界面中。生產(chǎn)3D建模器應(yīng)該具有完整的用戶界面,這將需要更復(fù)雜的事件系統(tǒng)而不是我們簡(jiǎn)單的回調(diào)系統(tǒng)。
我們可以做進(jìn)一步的實(shí)驗(yàn)來(lái)為這個(gè)項(xiàng)目添加新功能。嘗試以下方法之一:
- 添加節(jié)點(diǎn)類(lèi)型以支持任意形狀的三角形網(wǎng)格。
- 添加撤消堆棧,以允許撤消/重做建模器操作。
- 使用DXF等3D文件格式保存/加載設(shè)計(jì)。
- 集成渲染引擎:導(dǎo)出設(shè)計(jì)以在逼真的渲染器中使用。
- 通過(guò)準(zhǔn)確的光線 - 物體交叉改善碰撞檢測(cè)。
4. 進(jìn)一步探索
為了進(jìn)一步了解真實(shí)的3D建模軟件,一些開(kāi)源項(xiàng)目很有意思。
Blender是一個(gè)開(kāi)源的全功能3D動(dòng)畫(huà)套件。 它提供了一個(gè)完整的3D管道,用于在視頻或游戲創(chuàng)建中構(gòu)建特殊效果。 建模器只是該項(xiàng)目的一小部分,它是將建模器集成到大型軟件套件中的一個(gè)很好的例子。
OpenSCAD是一個(gè)開(kāi)源3D建模工具。 它不是互動(dòng)的; 相反,它讀取一個(gè)腳本文件,指定如何生成場(chǎng)景。 這使設(shè)計(jì)人員“完全控制建模過(guò)程”。
有關(guān)計(jì)算機(jī)圖形學(xué)中的算法和技術(shù)的更多信息,Graphics Gems是一個(gè)很好的資源。