A 3D Modeller

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 OpenGLModern 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è)基矢量,通常是x,yz

  • 點(diǎn)
    3維中的任何點(diǎn)都可以表示為距離原點(diǎn)x,yz方向的偏移。 點(diǎn)的表示相對(duì)于該點(diǎn)所在的坐標(biāo)空間。同一點(diǎn)在不同的坐標(biāo)空間中具有不同的表示。3維中的任何點(diǎn)都可以在任何3維坐標(biāo)空間中表示。

  • 向量
    向量是x,yz值,分別表示x,yz軸中兩個(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)空間。 為了將矢量v從一個(gè)坐標(biāo)空間轉(zhuǎn)換為另一個(gè)坐標(biāo)空間,我們乘以變換矩陣M:v'= Mv。 一些常見(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)目中,基元是CubeSphere

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中使用glPushMatrixglPopMatrix函數(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è)基元(SphereCube)定義渲染它所需的調(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)單。

2. Node子類(lèi)的層次結(jié)構(gòu)

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í)(glutMotionFunc),
  • 按下鍵盤(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ù)。

Interaction callbacks and arguments

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ù),由具有x,yz坐標(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)左鍵可以在xy坐標(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ò)誤的。

3. AABB錯(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ù)x,yz縮放的矩陣是:
\begin{bmatrix} x & 0 & 0 & 0 \\ 0 & y & 0 & 0 \\ 0 & 0 & z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}
當(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)

在給定x,yz縮放因子的列表的情況下,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)矩陣如下所示:

\begin{bmatrix} 1 & 0 & 0 & x \\ 0 & 1 & 0 & y \\ 0 & 0 & 1 & z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}
翻轉(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ù)返回給定表示x,yz平移距離的列表的轉(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é)

4. 示例場(chǎng)景

在這個(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è)很好的資源。

參考:http://aosabook.org/en/500L/a-3d-modeller.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一 寫(xiě)在前面 未經(jīng)允許,不得轉(zhuǎn)載,謝謝~~ 這篇文章是DeepMind團(tuán)隊(duì)發(fā)在CVPR2017年的文章,它把視頻分...
    與陽(yáng)光共進(jìn)早餐閱讀 4,930評(píng)論 7 20
  • 初見(jiàn)唐詩(shī),已覺(jué)盛美。 乍逢宋詞,頓時(shí)驚艷。 后遇歌賦,眼花繚亂,目眩神迷。 這樣美好的字字句句,...
    畫(huà)堂韶光久閱讀 676評(píng)論 12 25
  • (其一) 落盡斜陽(yáng)天色暗,單衣入夜微寒。 南園煮酒試春盤(pán)。明星三四點(diǎn),新鉤小玉鐮。 醉了身傾眠芳草,東君偷換流年。...
    山中曉柯閱讀 1,467評(píng)論 28 45
  • 中午吃飯的時(shí)候,韓文聯(lián)主任、宋瑜主任和張瑤芳老師坐到一塊,便對(duì)教學(xué)組的問(wèn)題進(jìn)行研討,討論激烈處,竟忘記吃飯...
    力_美_閱讀 531評(píng)論 0 2
  • 祖國(guó)啊,我是你撒落在青藏高原的一顆明珠,千百年來(lái),我迷藏于茶馬古道和漢藏走廊,承襲著神秘的風(fēng)俗和古老的傳統(tǒng)。我曾被...
    西環(huán)房客閱讀 133評(píng)論 0 1