A 3D Modeller

1. 介紹

人類天生具有創(chuàng)造力。我們不斷設(shè)計和構(gòu)建新穎,實用和有趣的東西。在現(xiàn)代,我們編寫軟件來協(xié)助設(shè)計和創(chuàng)作過程。計算機輔助設(shè)計(CAD)軟件允許創(chuàng)建者在構(gòu)建設(shè)計的物理版本之前設(shè)計建筑物,橋梁,視頻游戲藝術(shù),電影怪物,3D可打印對象以及許多其他東西。

CAD工具的核心是將三維設(shè)計抽象為可在二維屏幕上查看和編輯的內(nèi)容的方法。為了實現(xiàn)該定義,CAD工具必須提供三個基本功能。

  • 首先,他們必須有一個數(shù)據(jù)結(jié)構(gòu)來表示正在設(shè)計的對象:這是計算機對用戶正在構(gòu)建的三維世界的理解。
  • 其次,CAD工具必須提供一些在用戶屏幕上顯示設(shè)計的方法。用戶正在設(shè)計具有3個維度的物理對象,但計算機屏幕只有2個維度。CAD工具必須模擬我們?nèi)绾胃兄獙ο?,并以用戶可以理解對象的所?個維度的方式將它們繪制到屏幕上。
  • 第三,CAD工具必須提供與所設(shè)計對象交互的方式。用戶必須能夠添加和修改設(shè)計才能產(chǎn)生所需的結(jié)果。此外,所有工具都需要一種從磁盤保存和加載設(shè)計的方法,以便用戶可以協(xié)作,共享和保存他們的工作。

特定領(lǐng)域的CAD工具為相應(yīng)領(lǐng)域的特定要求提供了許多其他功能。例如,建筑CAD工具將提供物理模擬來測試建筑物上的氣候壓力,3D打印工具將具有檢查物體是否實際上有效打印的功能,電子CAD工具將模擬通過銅的電流物理和電影特效套件將包括準(zhǔn)確模擬熱動力學(xué)的功能。

但是,所有CAD工具必須至少包括上面討論的三個特征:表示設(shè)計的數(shù)據(jù)結(jié)構(gòu),將其顯示到屏幕的能力以及與設(shè)計交互的方法。

考慮到這一點,讓我們探索如何在500行Python中表示3D設(shè)計,將其顯示在屏幕上并與之交互。

2. 渲染指南

3D建模器中許多設(shè)計決策背后的驅(qū)動力是渲染過程。我們希望能夠在設(shè)計中存儲和渲染復(fù)雜對象,但我們同時希望保持渲染代碼的復(fù)雜性較低。讓我們檢查渲染過程,并探索設(shè)計的數(shù)據(jù)結(jié)構(gòu),允許我們使用簡單的渲染邏輯來存儲和繪制任意復(fù)雜的對象。

2.1 管理接口和主循環(huán)

在我們開始渲染之前,我們需要設(shè)置一些東西。

  • 首先,我們需要創(chuàng)建一個窗口來顯示我們的設(shè)計。
  • 其次,我們希望與圖形驅(qū)動程序通信以呈現(xiàn)到屏幕。我們不直接與圖形驅(qū)動程序通信,因此我們使用一個名為OpenGL的跨平臺抽象層,以及一個名為GLUT(OpenGL Utility Toolkit)的庫來管理我們的窗口。

2.1.1 關(guān)于OpenGL的注意事項

OpenGL是一個用于跨平臺開發(fā)的圖形化應(yīng)用程序編程接口。它是跨平臺開發(fā)圖形應(yīng)用程序的標(biāo)準(zhǔn)API。OpenGL有兩個主要變體:Legacy OpenGLModern OpenGL

OpenGL中的渲染基于由頂點和法線定義的多邊形。例如,要渲染立方體的一側(cè),我們指定4個頂點和邊的法線。

Legacy OpenGL提供了“固定功能管道”。通過設(shè)置全局變量,程序員可以啟用和禁用照明,著色,面部剔除等功能的自動實現(xiàn)。然后,OpenGL會自動使用啟用的功能呈現(xiàn)場景。不推薦使用此功能。

另一方面,Modern OpenGL具有可編程渲染管道,程序員可在其中編寫在專用圖形硬件(GPU)上運行的稱為“著色器”的小程序。Modern OpenGL的可編程管道已經(jīng)取代了Legacy OpenGL。

在這個項目中,盡管它已被棄用,我們?nèi)匀皇褂肔egacy OpenGL。Legacy OpenGL提供的固定功能對于保持較小的代碼大小非常有用。它減少了所需的線性代數(shù)知識量,并簡化了我們編寫的代碼。

2.1.2 關(guān)于GLUT

與OpenGL捆綁在一起的GLUT允許我們創(chuàng)建操作系統(tǒng)窗口并注冊用戶界面回調(diào)。這個基本功能足以滿足我們的目的。如果我們想要一個用于窗口管理和用戶交互的功能更全面的庫,我們會考慮使用像GTK或Qt這樣的完整窗口工具包。

2.1.3 創(chuàng)建Viewer類

為了管理GLUT和OpenGL的設(shè)置,并驅(qū)動模型的其余部分,我們創(chuàng)建了一個名為Viewer的類。我們使用單個Viewer實例來管理窗口創(chuàng)建和渲染,并包含我們程序的主循環(huán)。在Viewer初始化過程中,我們創(chuàng)建GUI窗口并初始化OpenGL。

  • 函數(shù)init_interface創(chuàng)建將渲染建模器的窗口,并指定在需要渲染設(shè)計時要調(diào)用的函數(shù)。
  • 函數(shù) init_opengl設(shè)置項目所需的OpenGL狀態(tài)。它設(shè)置矩陣,啟用背面剔除,注冊燈光以照亮場景,并告訴OpenGL我們希望對象被著色。
  • 函數(shù)init_scene創(chuàng)建Scene對象并放置一些初始節(jié)點以使用戶啟動。稍后我們將很快看到有關(guān)Scene數(shù)據(jù)結(jié)構(gòu)的更多信息。
  • 最后,函數(shù)init_interaction注冊用戶交互的回調(diào),我們稍后會討論。

初始化Viewer后,我們調(diào)用glutMainLoop將程序執(zhí)行轉(zhuǎn)移到GLUT。此函數(shù)永遠(yuǎn)沒有返回值。我們在GLUT事件上注冊的回調(diào)將在這些事件發(fā)生時被調(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ù)知識。

  • 坐標(biāo)空間
    出于我們的目的,坐標(biāo)空間是一個原點和一組3個基矢量,通常是x,yz


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

  • 向量
    向量是x,yz值,分別表示x,yz軸中兩個點之間的距離。

  • 轉(zhuǎn)換矩陣
    在計算機圖形學(xué)中,為不同類型的點使用多個不同的坐標(biāo)空間是方便的。轉(zhuǎn)換矩陣將點從一個坐標(biāo)空間轉(zhuǎn)換為另一個坐標(biāo)空間。 為了將矢量v從一個坐標(biāo)空間轉(zhuǎn)換為另一個坐標(biāo)空間,我們乘以變換矩陣M:v'= Mv。 一些常見的變換矩陣是平移,縮放和旋轉(zhuǎn)。

  • Model, World, View, and Projection Coordinate Spaces

    1. Transformation Pipeline

為了將項目繪制到屏幕,我們需要在幾個不同的坐標(biāo)空間之間進行轉(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處理。這兩個矩陣相乘并存儲為GL_PROJECTION矩陣。

我們不需要知道這些矩陣如何為這個項目工作的術(shù)語或細(xì)節(jié)。但是,我們需要自己管理圖表的左側(cè)。

  • 我們定義了一個矩陣,它將模型中的點(也稱為網(wǎng)格)從model spaces轉(zhuǎn)換為world space,稱為模型矩陣(model matrix)。
  • 我們還定義了視圖矩陣(view matrix),它從world space轉(zhuǎn)換為eye space。

在這個項目中,我們將這兩個矩陣組合起來以獲得ModelView矩陣。

要了解有關(guān)完整圖形渲染管道以及所涉及的坐標(biāo)空間的更多信息,請參閱實時渲染的第2章或其他介紹性計算機圖形手冊。

2.2 使用Viewer進行渲染(Rendering with the Viewer)

render函數(shù)首先設(shè)置需要在渲染時完成的任意OpenGL狀態(tài)。

  • 它通過init_view并初始化投影矩陣,
  • 使用來自交互( interaction)成員的數(shù)據(jù),
  • 使用從 scene space轉(zhuǎn)換為world space的變換矩陣初始化ModelView矩陣。

我們將在下面看到有關(guān)Interaction類的更多信息。

  • 它使用glClear清除屏幕,它告訴場景(scene)渲染自己,然后渲染單位網(wǎng)格。

我們在渲染網(wǎng)格之前禁用OpenGL的光照。禁用照明后,OpenGL會渲染純色項目,而不是模擬光源。這樣,網(wǎng)格與場景具有視覺差異。最后,glFlush向圖形驅(qū)動程序發(fā)出信號,告知我們已準(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 渲染什么:場景(What to Render: The Scene)

現(xiàn)在我們已經(jīng)初始化了渲染管道來處理世界坐標(biāo)空間中的繪圖,我們要渲染什么?回想一下,我們的目標(biāo)是設(shè)計一個由3D模型組成的設(shè)計。我們需要一個包含設(shè)計的數(shù)據(jù)結(jié)構(gòu),我們需要使用這個數(shù)據(jù)結(jié)構(gòu)來呈現(xiàn)設(shè)計。請注意,我們self.scene.render()從查看器的渲染循環(huán)中調(diào)用。 scene是什么?

Scene類是接口,我們用它來表示設(shè)計的數(shù)據(jù)結(jié)構(gòu)。它抽象出數(shù)據(jù)結(jié)構(gòu)的細(xì)節(jié),并提供與設(shè)計交互所需的必要接口功能,包括渲染,添加項目和操作項目的功能。viewer擁有一個Scene對象。 Scene實例保留場景中所有項目的列表,稱為node_list。 它還跟蹤所選項目。Scene上的渲染函數(shù)只是在node_list的每個成員上調(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é)點(Nodes)

在Scene的render函數(shù)中,我們在Scene的node_list中的每個項目上調(diào)用render。但該清單的要素是什么?我們稱它們?yōu)?strong>節(jié)點。從概念上講,節(jié)點是可以放置在場景中的任何東西。在面向?qū)ο蟮能浖?,我們?code>Node編寫為抽象基類。表示要放置在Scene中的對象的任何類都將從Node繼承。這個基類允許我們抽象地推斷場景。代碼庫的其余部分不需要知道它顯示的對象的細(xì)節(jié);它只需要知道它們屬于Node類。

每種類型的Node都定義了自己的行為,用于呈現(xiàn)自身和任何其他交互。節(jié)點跟蹤關(guān)于其自身的重要數(shù)據(jù):平移矩陣,比例矩陣,顏色等。將節(jié)點的平移矩陣乘以其縮放矩陣,得到從節(jié)點的模型坐標(biāo)空間到世界坐標(biāo)空間的變換矩陣。該節(jié)點還存儲軸對齊的邊界框(AABB)。當(dāng)我們在下面討論選擇時,我們會看到更多有關(guān)AABB的信息。

Node最簡單的具體實現(xiàn)是基元的?;强梢蕴砑拥綀鼍爸械膯蝹€實體形狀。在這個項目中,基元是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é)點基于每個節(jié)點存儲的變換矩陣。節(jié)點的變換矩陣是其縮放矩陣與其平移矩陣的組合。無論節(jié)點類型如何,

  • 渲染的第一步是將OpenGL ModelView矩陣設(shè)置為變換矩陣,以便從模型坐標(biāo)空間轉(zhuǎn)換為視圖坐標(biāo)空間。
  • 一旦OpenGL矩陣是最新的,我們調(diào)用render_self告訴節(jié)點進行必要的OpenGL調(diào)用以繪制自己。
  • 最后,我們撤消對此特定節(jié)點對OpenGL狀態(tài)所做的任何更改。我們在OpenGL中使用glPushMatrixglPopMatrix函數(shù)來保存和恢復(fù)ModelView矩陣在渲染節(jié)點之前和之后的狀態(tài)。請注意,節(jié)點存儲其顏色,位置和比例,并在渲染之前將這些應(yīng)用于OpenGL狀態(tài)。

如果當(dāng)前選擇了節(jié)點,我們將其照亮。這樣,用戶可以看到他們選擇了哪個節(jié)點。

要渲染基元,我們使用OpenGL的調(diào)用列表功能。 OpenGL調(diào)用列表是一系列OpenGL調(diào)用,它們被定義一次并在單個名稱下捆綁在一起??梢允褂?code>glCallList(LIST_NAME)調(diào)度調(diào)用。每個基元(SphereCube)定義渲染它所需的調(diào)用列表(未顯示)。

例如,立方體的調(diào)用列表繪制立方體的6個面,中心位于原點,邊緣恰好為1個單位長。

# 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))

僅使用基元對于建模應(yīng)用程序?qū)⑹欠浅S邢薜摹?3D模型通常由多個基元(或三角形網(wǎng)格,在本項目范圍之外)組成。幸運的是,我們設(shè)計的Node類有助于由多個基元組成的Scene節(jié)點。實際上,我們可以支持任意節(jié)點分組,而不會增加復(fù)雜性。

作為動機,讓我們考慮一個非?;镜娜宋铮阂粋€典型的雪人,由三個球體組成。盡管該圖由三個獨立的基元組成,但我們希望能夠?qū)⑵湟暈閱蝹€對象。

我們創(chuàng)建了一個名為HierarchicalNode的類,一個包含其他節(jié)點的Node。它管理一個“子”列表。分層節(jié)點的render_self函數(shù)只是在每個子節(jié)點上調(diào)用render_self。使用HierarchicalNode類,可以非常輕松地將圖形添加到場景中?,F(xiàn)在,定義雪人就像指定構(gòu)成它的形狀及其相對位置和大小一樣簡單。

2. Node子類的層次結(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])

你可能會發(fā)現(xiàn)Node對象形成了數(shù)據(jù)結(jié)構(gòu)。 render函數(shù)通過分層節(jié)點執(zhí)行深度優(yōu)先遍歷樹。 當(dāng)它遍歷時,它保留了一堆ModelView矩陣,用于轉(zhuǎn)換到世界空間。 在每一步中,它將當(dāng)前的ModelView矩陣推送到堆棧上,當(dāng)它完成所有子節(jié)點的渲染時,它會將矩陣從堆棧中彈出,將父節(jié)點的ModelView矩陣保留在堆棧的頂部。

通過以這種方式使Node可擴展,我們可以向場景添加新類型的形狀,而無需更改任何其他用于場景操作和渲染的代碼。 使用節(jié)點概念來抽象出一個Scene對象可能有許多子節(jié)點的事實被稱為復(fù)合設(shè)計模式。

2.5 用戶交互(User Interaction)

現(xiàn)在我們的建模器能夠存儲和顯示場景,我們需要一種與它交互的方法。我們需要促進兩種類型的交互。首先,我們需要改變場景的觀看視角的能力。我們希望能夠在場景周圍移動眼睛或相機。其次,我們需要能夠添加新節(jié)點并修改場景中的節(jié)點。

為了實現(xiàn)用戶交互,我們需要知道用戶何時按下按鍵或移動鼠標(biāo)。幸運的是,操作系統(tǒng)已經(jīng)知道這些事件何時發(fā)生。 GLUT允許我們在發(fā)生特定事件時注冊要調(diào)用的函數(shù)。我們編寫函數(shù)來解釋按鍵和鼠標(biāo)移動,并告訴GLUT在按下相應(yīng)鍵時調(diào)用這些函數(shù)。一旦我們知道用戶正在按哪些鍵,我們就需要解釋輸入并將預(yù)期的動作應(yīng)用到場景中。

可以在Interaction類中找到用于偵聽操作系統(tǒng)事件和解釋其含義的邏輯。我們之前寫的Viewer類擁有Interaction的單個實例。我們將使用GLUT回調(diào)機制來記錄

  • 按下鼠標(biāo)按鈕時(glutMouseFunc),
  • 鼠標(biāo)移動時(glutMotionFunc),
  • 按下鍵盤按鈕(glutKeyboardFunc
  • 按下箭頭鍵時( 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)按鈕和鍵盤的知識。 因為將用戶輸入解釋為有意義的動作需要多行代碼,所以我們將其封裝在一個單獨的類中,遠(yuǎn)離主代碼路徑Interaction類隱藏了與代碼庫其余部分無關(guān)的復(fù)雜性,并將操作系統(tǒng)事件轉(zhuǎn)換為應(yīng)用程序級事件。

 # 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)

在上面的代碼片段中,你會注意到當(dāng)Interaction實例解釋用戶操作時,它會使用描述操作類型的字符串調(diào)用self.trigger。
Interaction類上的觸發(fā)器函數(shù)是我們將用于處理應(yīng)用程序級事件的簡單回調(diào)系統(tǒng)的一部分。 回想一下,Viewer類上的init_interaction函數(shù)通過調(diào)用register_callback來注冊Interaction實例上的回調(diào)。

# class Interaction
    def register_callback(self, name, func):
        self.callbacks[name].append(func)

當(dāng)用戶界面代碼需要在場景上觸發(fā)事件時,Interaction類會調(diào)用它為該特定事件保存的所有已保存的回調(diào):

# class Interaction
    def trigger(self, name, *args, **kwargs):
        for func in self.callbacks[name]:
            func(*args, **kwargs)

這個應(yīng)用程序級回調(diào)系統(tǒng)抽象出系統(tǒng)其余部分需要了解操作系統(tǒng)輸入。 每個應(yīng)用程序級回調(diào)代表應(yīng)用程序中的一個有意義的請求。 Interaction類充當(dāng)操作系統(tǒng)事件和應(yīng)用程序級事件之間的轉(zhuǎn)換器。 這意味著如果我們決定除了GLUT之外還將建模器移植到另一個工具包,我們只需要將一個類替換,該類將來自新工具包的輸入轉(zhuǎn)換為同一組有意義的應(yīng)用程序級回調(diào)。 我們在表13.1中使用了回調(diào)和參數(shù)。

Interaction callbacks and arguments

2.6 與場景交互

使用我們的回調(diào)機制,我們可以從Interaction類接收有關(guān)用戶輸入事件的有用信息。我們準(zhǔn)備將這些操作應(yīng)用到場景中。

2.6.1 移動場景

在這個項目中,我們通過改變場景來完成相機運動。換句話說,相機處于固定位置,用戶輸入移動場景而不是移動相機。相機放置在[0,0,-15]并面向世界空間原點。 (或者,我們可以更改透視矩陣來移動相機而不是場景。這個設(shè)計決定對項目的其余部分影響很小。)重新審視Viewer中的渲染功能,我們看到交互狀態(tài)用于在渲染場景之前轉(zhuǎn)換OpenGL矩陣狀態(tài)。與場景有兩種類型的交互:旋轉(zhuǎn)和平移。

2.6.2 使用軌跡球旋轉(zhuǎn)場景

我們使用軌跡球算法完成場景的旋轉(zhuǎn)。軌跡球是一個直觀的界面,用于以三維方式操縱場景。從概念上講,軌跡球界面的功能就像場景位于透明地球儀內(nèi)部一樣。將手放在地球表面并推動它會使地球旋轉(zhuǎn)。同樣,單擊鼠標(biāo)右鍵并在屏幕上移動它會旋轉(zhuǎn)場景。你可以在OpenGL Wiki上找到有關(guān)軌跡球理論的更多信息。在這個項目中,我們使用作為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)矩陣是渲染場景時viewer中的trackball.matrix

2.6.3 旁白:四元數(shù)

旋轉(zhuǎn)是以兩種方式之一表示。第一個是圍繞每個軸的旋轉(zhuǎn)值;你可以將它存儲為3元組的浮點數(shù)。旋轉(zhuǎn)的另一個常見表示是四元數(shù),由具有x,yz坐標(biāo)的向量組成的元素,以及w旋轉(zhuǎn)。使用四元數(shù)比每軸旋轉(zhuǎn)有許多好處;特別是,它們在數(shù)值上更穩(wěn)定。使用四元數(shù)避免了萬向節(jié)鎖定等問題。四元數(shù)的缺點是它們不太直觀,難以理解。如果你希望了解有關(guān)四元數(shù)的更多信息,請參閱此說明。

軌跡球?qū)崿F(xiàn)通過在內(nèi)部使用四元數(shù)來存儲場景的旋轉(zhuǎn)來避免萬向節(jié)鎖定。幸運的是,我們不需要直接使用四元數(shù),因為軌跡球上的矩陣成員將旋轉(zhuǎn)轉(zhuǎn)換為矩陣。

2.6.4 翻轉(zhuǎn)場景

翻譯場景(即滑動場景)比旋轉(zhuǎn)場景簡單得多。使用鼠標(biāo)滾輪和鼠標(biāo)左鍵提供場景轉(zhuǎn)換。鼠標(biāo)左鍵可以在xy坐標(biāo)中平移場景。滾動鼠標(biāo)滾輪可以在z坐標(biāo)(朝向或遠(yuǎn)離攝像機)中平移場景。 Interaction類存儲當(dāng)前場景轉(zhuǎn)換并使用translate函數(shù)對其進行修改。查看器在渲染期間檢索交互相機位置以在glTranslated調(diào)用中使用。

2.6.5 選擇場景對象

既然用戶可以移動和旋轉(zhuǎn)整個場景以獲得他們想要的視角,下一步就是允許用戶修改和操縱構(gòu)成場景的對象。

為了讓用戶操縱場景中的對象,他們需要能夠選擇項目。

要選擇項目,我們使用當(dāng)前投影矩陣生成表示鼠標(biāo)單擊的光線,就像鼠標(biāo)指針將光線射入場景一樣。所選節(jié)點是與光線相交的攝像機最近的節(jié)點。因此,拾取問題減少了在場景中找到光線和節(jié)點之間的交叉點的問題。所以問題是:我們?nèi)绾闻袛喙饩€是否擊中節(jié)點?

準(zhǔn)確地計算射線是否與節(jié)點相交在代碼復(fù)雜性和性能方面都是一個具有挑戰(zhàn)性的問題。我們需要為每種類型的基元編寫一個光線對象交叉檢查。對于具有多個面的復(fù)雜網(wǎng)格幾何的場景節(jié)點,計算精確的光線 - 對象交叉將需要針對每個面測試光線并且計算上是昂貴的。

為了保持代碼緊湊和性能合理,我們使用簡單,快速的近似來進行光線 - 物體相交測試。在我們的實現(xiàn)中,每個節(jié)點都存儲一個軸對齊的邊界框(AABB),它是它占據(jù)的空間的近似值。為了測試光線是否與節(jié)點相交,我們測試光線是否與節(jié)點的AABB相交。此實現(xiàn)意味著所有節(jié)點共享相同的交叉測試代碼,這意味著所有節(jié)點類型的性能成本都是恒定的。

# 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)

為了確定單擊了哪個節(jié)點,我們遍歷場景以測試光線是否到達(dá)任何節(jié)點。 我們?nèi)∠x擇當(dāng)前選定的節(jié)點,然后選擇最接近光線原點的交點的節(jié)點。

 # 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類中,pick函數(shù)測試光線是否與Node的軸對齊邊界框相交。 如果選擇了節(jié)點,則select函數(shù)切換節(jié)點的選定狀態(tài)。 請注意,AABB的ray_hit函數(shù)接受框的坐標(biāo)空間和光線的坐標(biāo)空間之間的變換矩陣作為第三個參數(shù)。 在進行ray_hit函數(shù)調(diào)用之前,每個節(jié)點都將自己的轉(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選擇方法非常易于理解和實現(xiàn)。 但是,在某些情況下結(jié)果是錯誤的。

3. AABB錯誤

例如,在Sphere基元的情況下,球體本身僅接觸每個AABB面部中心的AABB。 但是,如果用戶點`Sphere AABB的角落,即使用戶打算通過Sphere點擊其后面的某些東西,也會檢測到Sphere的碰撞(圖13.3)。

復(fù)雜性,性能和準(zhǔn)確性之間的這種折衷在計算機圖形學(xué)和軟件工程的許多領(lǐng)域中是常見的。

2.6.6 修改場景對象

接下來,我們希望允許用戶操作所選節(jié)點。 他們可能想要移動,調(diào)整大小或更改所選節(jié)點的顏色。 當(dāng)用戶輸入操作節(jié)點的命令時,Interaction類將輸入轉(zhuǎn)換為用戶想要的操作,并調(diào)用相應(yīng)的回調(diào)。

當(dāng)Viewer收到其中一個事件的回調(diào)時,它會調(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)。 場景將顏色更改命令調(diào)度到當(dāng)前選定的節(jié)點。

 # 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)

每個節(jié)點存儲其當(dāng)前顏色。rotate_color函數(shù)只是修改節(jié)點的當(dāng)前顏色。 渲染節(jié)點時,顏色將通過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ào)度對所選節(jié)點的任何縮放修改(如果有)

  # class Scene
    def scale_selected(self, up):
        """ Scale the current selection """
        if self.selected_node is None: return
        self.selected_node.scale(up)
    

每個節(jié)點存儲一個存儲其比例的當(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é)點的比例時,將得到的縮放矩陣乘以該節(jié)點的當(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 移動節(jié)點

為了翻轉(zhuǎn)節(jié)點,我們使用我們用于拾取的相同射線計算。 我們將表示當(dāng)前鼠標(biāo)位置的光線傳遞給場景的move函數(shù)。 節(jié)點的新位置應(yīng)該在光線上。 為了確定放置節(jié)點的光線的位置,我們需要知道節(jié)點與相機的距離。 由于我們在選擇節(jié)點時存儲了節(jié)點的位置和距離(在pick函數(shù)中),我們可以在此處使用該數(shù)據(jù)。 我們找到與目標(biāo)射線上相機距離相同的點,并計算新舊位置之間的矢量差異。 然后,我們通過結(jié)果向量轉(zhuǎn)換節(jié)點。

# 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

請注意,新舊位置是在攝像機坐標(biāo)空間中定義的。 我們需要在世界坐標(biāo)空間中定義我們的翻轉(zhuǎn)。 因此,我們通過乘以模型視圖矩陣的逆將camera space轉(zhuǎn)換轉(zhuǎn)換為world space轉(zhuǎn)換。

與比例一樣,每個節(jié)點存儲表示其轉(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é)點時,我們?yōu)楫?dāng)前翻轉(zhuǎn)構(gòu)建一個新的翻轉(zhuǎn)矩陣,并將其乘以節(jié)點的翻轉(zhuǎn)矩陣,以便在渲染過程中使用。

 # 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é)點

節(jié)點放置使用拾取和轉(zhuǎn)換的技術(shù)。 我們對當(dāng)前鼠標(biāo)位置使用相同的光線計算來確定節(jié)點的放置位置。

  # 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é)點,我們首先創(chuàng)建相應(yīng)類型節(jié)點的新實例并將其添加到場景中。 我們希望將節(jié)點放在用戶光標(biāo)下面,這樣我們就可以在與攝像機相距固定距離的光線上找到一個點。 同樣,光線在相機空間中表示,因此我們將得到的平移向量轉(zhuǎn)換為世界坐標(biāo)空間,方法是將其乘以逆模型視圖矩陣。 最后,我們通過計算的向量轉(zhuǎn)換新節(jié)點。

 # 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. 示例場景

在這個項目中,
我們了解了如何開發(fā)可擴展的數(shù)據(jù)結(jié)構(gòu)來表示場景中的對象。我們注意到使用Composite設(shè)計模式和基于樹的數(shù)據(jù)結(jié)構(gòu)可以輕松遍歷場景進行渲染,并允許我們添加新類型的節(jié)點而不會增加復(fù)雜性。

我們利用這種數(shù)據(jù)結(jié)構(gòu)將設(shè)計渲染到屏幕上,并在場景圖的遍歷中操縱OpenGL矩陣。我們?yōu)閼?yīng)用程序級事件構(gòu)建了一個非常簡單的回調(diào)系統(tǒng),并使用它來封裝操作系統(tǒng)事件的處理。

我們討論了光線對象碰撞檢測的可能實現(xiàn),以及正確性,復(fù)雜性和性能之間的權(quán)衡。

最后,我們實現(xiàn)了操作場景內(nèi)容的方法。

你可以在生產(chǎn)3D軟件中找到這些相同的基本構(gòu)建塊。場景圖結(jié)構(gòu)和相對坐標(biāo)空間可以在許多類型的3D圖形應(yīng)用程序中找到,從CAD工具到游戲引擎。該項目的一個主要簡化是在用戶界面中。生產(chǎn)3D建模器應(yīng)該具有完整的用戶界面,這將需要更復(fù)雜的事件系統(tǒng)而不是我們簡單的回調(diào)系統(tǒng)。

我們可以做進一步的實驗來為這個項目添加新功能。嘗試以下方法之一:

  • 添加節(jié)點類型以支持任意形狀的三角形網(wǎng)格。
  • 添加撤消堆棧,以允許撤消/重做建模器操作。
  • 使用DXF等3D文件格式保存/加載設(shè)計。
  • 集成渲染引擎:導(dǎo)出設(shè)計以在逼真的渲染器中使用。
  • 通過準(zhǔn)確的光線 - 物體交叉改善碰撞檢測。

4. 進一步探索

為了進一步了解真實的3D建模軟件,一些開源項目很有意思。

  • Blender是一個開源的全功能3D動畫套件。 它提供了一個完整的3D管道,用于在視頻或游戲創(chuàng)建中構(gòu)建特殊效果。 建模器只是該項目的一小部分,它是將建模器集成到大型軟件套件中的一個很好的例子。

  • OpenSCAD是一個開源3D建模工具。 它不是互動的; 相反,它讀取一個腳本文件,指定如何生成場景。 這使設(shè)計人員“完全控制建模過程”。

  • 有關(guān)計算機圖形學(xué)中的算法和技術(shù)的更多信息,Graphics Gems是一個很好的資源。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容