一個(gè)3D模型(譯)

這是500Lines項(xiàng)目中的A 3D modeller文章的翻譯版楣黍,講述如何使用Python,OpenGL却桶,GLUT進(jìn)行3D建模程序的設(shè)計(jì)碱呼。

項(xiàng)目封面

緒論

人類非常具有創(chuàng)造力蒙挑。我們?cè)诓粩嗟卦O(shè)計(jì)和創(chuàng)造新穎有用并且非常有趣的東西。在現(xiàn)代愚臀,我們編寫軟件來輔助這一設(shè)計(jì)和創(chuàng)造的過程惨奕。計(jì)算機(jī)輔助設(shè)計(jì)軟件讓創(chuàng)造者們能夠設(shè)計(jì)建筑蚤氏、橋梁、視頻游戲藝術(shù)、電影特效螃成、3D打印的物體北戏,以及很多構(gòu)建實(shí)物之前的設(shè)計(jì)版本尿赚。

作為他們的核心督惰,CAD工具是能夠?qū)?D的設(shè)計(jì)物體抽象成可以在2D屏幕上展示的方法。為了達(dá)到這種定義茴厉,CAD工具必須提供三類基礎(chǔ)的方法泽台。第一什荣,它們必須要有能夠表現(xiàn)設(shè)計(jì)的3D物體的數(shù)據(jù)結(jié)構(gòu):這是計(jì)算機(jī)理解的用戶正在構(gòu)建的東西。第二怀酷,CAD工具必須提供一些方法把它展現(xiàn)在屏幕上稻爬。雖然人設(shè)計(jì)的東西是3維的,但是屏幕只有2維胰坟。CAD工具必須對(duì)我們?nèi)绾卫斫馕矬w進(jìn)行建模因篇,并且把它們繪制在屏幕上以保證人能夠理解全部的3維結(jié)構(gòu)泞辐。第三笔横,CAD工具還要提供能夠交互設(shè)計(jì)物體的方法。為了能夠讓用戶創(chuàng)造出想要的物體咐吼,必須能能夠添加或者修改這個(gè)設(shè)計(jì)吹缔。額外的,所有的工具都需要一種在磁盤上保存和加載方法以便用戶可以修改锯茄、分享厢塘、和存儲(chǔ)他們的工作。

一個(gè)領(lǐng)域特定的CAD工具可以根據(jù)這個(gè)領(lǐng)域的需求針對(duì)性地提供很多額外的特性肌幽。例如晚碾,一個(gè)建筑CAD工具可以提供很多物理模擬針對(duì)氣候壓力來測(cè)試建筑物,一個(gè)3D打印的工具將會(huì)測(cè)試這個(gè)設(shè)計(jì)是否真的是可以打印的喂急,一個(gè)電氣CAD工具將會(huì)模擬電流流經(jīng)電線的物理現(xiàn)象格嘁,一個(gè)電影特效套件將會(huì)包括精細(xì)地模擬火焰術(shù)的特征。

然而廊移,所有的CAD工具都必須包括至少三個(gè)上面討論過的特性:一個(gè)用于表達(dá)設(shè)計(jì)的數(shù)據(jù)結(jié)構(gòu)糕簿,將其展現(xiàn)在屏幕上的能力,可以交互設(shè)計(jì)的方法狡孔。

記住這些東西懂诗,讓我們來探索如何表達(dá)3D設(shè)計(jì),把這些展現(xiàn)在屏幕上苗膝,并且和它交互殃恒,用500行Python代碼。

指南

很多3D模型背后的設(shè)計(jì)決策的驅(qū)動(dòng)力都是渲染過程辱揭。我們希望能夠在我們的設(shè)計(jì)中存儲(chǔ)和渲染復(fù)雜的對(duì)象芋类,但是我們又希望能夠使得存儲(chǔ)和渲染的代碼復(fù)雜度盡量低。讓我們來考察渲染的過程界阁,并且探索能讓我們用簡(jiǎn)單的渲染邏輯處理任意的復(fù)雜對(duì)象侯繁。

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

在我們開始渲染前,有幾樣?xùn)|西我們要先建立起來泡躯。第一贮竟,我們需要?jiǎng)?chuàng)建一個(gè)展示我們的設(shè)計(jì)的窗口丽焊。第二,我們希望能夠和圖形驅(qū)動(dòng)交流來渲染到屏幕上咕别。我們一般不會(huì)直接和顯示驅(qū)動(dòng)交流技健,所以我們用跨平臺(tái)的抽象層稱為OpenGL,還有一個(gè)叫GLUT(the OpenGL Utility Toolkit)來管理我們的窗口惰拱。

OpenGL 筆記

OpenGL是一個(gè)跨平臺(tái)的圖形程序編程接口開發(fā)工具雌贱。是一個(gè)開發(fā)跨平臺(tái)圖形程序的標(biāo)準(zhǔn)接口。OpenGL有兩個(gè)主要的變體:傳統(tǒng)OpenGL和現(xiàn)代OpenGL偿短。

在OpenGL上進(jìn)行渲染是基于由頂點(diǎn)和法線定義的多邊形欣孤。例如,要渲染方塊的一個(gè)面昔逗,我們需要指定四個(gè)頂點(diǎn)和這面的法線降传。

傳統(tǒng)OpenGL提供了“固定功能流水線”。通過設(shè)置全局變量勾怒,程序員可以啟用和禁用諸如照明婆排,著色,表面剔除等功能的自動(dòng)化實(shí)現(xiàn)笔链。然后OpenGL自動(dòng)使用啟用的功能呈現(xiàn)場(chǎng)景段只。此功能已棄用。

另一方面鉴扫,現(xiàn)代OpenGL具有可編程渲染流水線赞枕,程序員在其中編寫稱為“著色器”的小程序,該程序在專用圖形硬件(GPU)上運(yùn)行幔妨。 Modern OpenGL的可編程流水線已經(jīng)取代了Legacy OpenGL鹦赎。

在這個(gè)項(xiàng)目中,盡管Legacy OpenGL已被棄用误堡,但我們使用它古话。 Legacy OpenGL提供的固定功能對(duì)于保持較小的代碼尺寸非常有用。 它減少了所需的線性代數(shù)知識(shí)的數(shù)量锁施,并簡(jiǎn)化了我們將要編寫的代碼陪踩。

關(guān)于 GLUT

與OpenGL捆綁在一起的GLUT允許我們創(chuàng)建操作系統(tǒng)窗口并注冊(cè)用戶界面回調(diào)。 這個(gè)基本功能對(duì)我們來說已經(jīng)足夠了悉抵。 如果我們想要一個(gè)更全面的窗口管理和用戶交互庫肩狂,我們會(huì)考慮使用像GTK或Qt這樣的完整窗口工具包。

觀察

為了管理GLUT和OpenGL的建立姥饰,并且驅(qū)動(dòng)下面的模型傻谁,我們創(chuàng)建一個(gè)叫Viewer的類。我們一個(gè)一個(gè)Viewer實(shí)例列粪,這個(gè)實(shí)例可以管理窗口的創(chuàng)建和渲染审磁,并且包括很多我們程序的主循環(huán)谈飒。在Viewer的初始化中,我們創(chuàng)建一個(gè)圖形化窗口态蒂,并且初始化OpenGL杭措。

init_interface函數(shù)創(chuàng)建一個(gè)窗口放被渲染的模型,并指定需要渲染設(shè)計(jì)是調(diào)用的函數(shù)钾恢。init_opengl函數(shù)建立起項(xiàng)目中OpenGL需要的狀態(tài)手素。它設(shè)定矩陣,實(shí)現(xiàn)背面剔除瘩蚪,注冊(cè)光線以照亮場(chǎng)景泉懦,并告訴OpenGL我們希望哪些物體被著色。init_scence函數(shù)創(chuàng)建Scene(場(chǎng)景)對(duì)象并且放置一些初始節(jié)點(diǎn)讓用戶開始募舟。我們很快就會(huì)看到Scene數(shù)據(jù)結(jié)構(gòu)祠斧。最后闻察,init_interaction注冊(cè)讓用戶交互的回調(diào)函數(shù)拱礁,我們將在后面討論。

初始化Viewer以后辕漂,我們調(diào)用glutMainLoop來將程序執(zhí)行轉(zhuǎn)移到GLUT呢灶。這個(gè)函數(shù)從不返回。我們?cè)贕LUT事件上注冊(cè)的回調(diào)將在這些事件發(fā)生時(shí)被調(diào)用钉嘹。

import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *


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 setting to render the scene """
        self.inverseModelView = np.identity(4)
        self.modelView = np.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.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)空間

根據(jù)我們的目的跋涣,坐標(biāo)空間是一個(gè)原點(diǎn)和一組3個(gè)基向量缨睡,通常是xyz軸陈辱。

點(diǎn)

三維中的任何點(diǎn)都可以表示為從原點(diǎn)開始的x奖年,yz方向的偏移量。 點(diǎn)的表示與點(diǎn)所在的坐標(biāo)空間有關(guān)沛贪。同一點(diǎn)在不同的坐標(biāo)空間中有不同的表示陋守。 三維中的任何點(diǎn)都可以在任何三維坐標(biāo)空間中表示。

向量

向量是一個(gè)x利赋,yz值水评,分別表示xyz軸中兩個(gè)點(diǎn)之間的差異媚送。

變換矩陣

在計(jì)算機(jī)圖形學(xué)中中燥,為不同類型的點(diǎn)使用多個(gè)不同的坐標(biāo)空間是很方便的。 變換矩陣將點(diǎn)從一個(gè)坐標(biāo)空間轉(zhuǎn)換為另一個(gè)坐標(biāo)空間塘偎。 為了將矢量v從一個(gè)坐標(biāo)空間轉(zhuǎn)換到另一個(gè)坐標(biāo)空間疗涉,我們乘以一個(gè)變換矩陣Mv'= Mv幽纷。 一些常見的變換矩陣是平移,縮放和旋轉(zhuǎn)博敬。

變換流程

為了能夠?qū)⒁粋€(gè)東西繪制在屏幕上友浸,我們需要在幾個(gè)不同的坐標(biāo)空間中進(jìn)行轉(zhuǎn)換。

在上圖的右邊偏窝,包括OpenGL將會(huì)為我們處理的所有從眼見空間到視點(diǎn)空間的變換收恢。

從眼睛空間轉(zhuǎn)換到齊次投影空間由gluPerspective處理,并且轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備空間和視點(diǎn)空間由glViewport處理祭往。 這兩個(gè)矩陣相乘并存儲(chǔ)為GL_PROJECTION矩陣伦意。 我們不需要知道術(shù)語或這些矩陣如何為這個(gè)項(xiàng)目工作的細(xì)節(jié)。

然而硼补,我們確實(shí)需要自己管理圖表的左側(cè)驮肉。 我們定義一個(gè)矩陣,將模型中的點(diǎn)(也稱為網(wǎng)格)從模型空間轉(zhuǎn)換為世界空間已骇,稱為模型矩陣离钝。 我們還定義了從世界空間轉(zhuǎn)換到眼睛空間的視圖矩陣。 在這個(gè)項(xiàng)目中褪储,我們這兩個(gè)矩陣結(jié)合從而得到ModelView矩陣卵渴。

要了解更多關(guān)于整個(gè)圖形渲染流水線和涉及的坐標(biāo)空間的信息,請(qǐng)參閱實(shí)時(shí)渲染的第2章或其他介紹性計(jì)算機(jī)圖形書籍鲤竹。

用Viewer渲染

render函數(shù)首先設(shè)置渲染時(shí)需要完成的全部OpenGL狀態(tài)浪读。 它通過init_view初始化投影矩陣,并使用來自交互成員的數(shù)據(jù)從場(chǎng)景空間轉(zhuǎn)換到世界空間的轉(zhuǎn)換矩陣初始化ModelView矩陣辛藻。 我們將在下面看到更多關(guān)于Interaction類的內(nèi)容碘橘。 它用glClear清除屏幕,再告訴場(chǎng)景渲染自己吱肌,然后呈現(xiàn)單元網(wǎng)格痘拆。

在渲染網(wǎng)格之前,我們禁用OpenGL的照明岩榆。 在禁用照明的情況下错负,OpenGL渲染純色的項(xiàng)目,而不會(huì)去模擬光源勇边。 這樣犹撒,網(wǎng)格就具有與場(chǎng)景的視覺差異。 最后粒褒,glFlush通知圖形驅(qū)動(dòng)程序我們已準(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)

要渲染什么:場(chǎng)景

既然我們已經(jīng)初始化渲染管道來處理世界坐標(biāo)空間中的繪圖,那么我們將渲染什么? 回想一下祥款,我們的目標(biāo)是有一個(gè)由三維模型組成的設(shè)計(jì)清笨。 我們需要一個(gè)數(shù)據(jù)結(jié)構(gòu)來包含設(shè)計(jì),我們需要使用這個(gè)數(shù)據(jù)結(jié)構(gòu)來渲染設(shè)計(jì)刃跛。 注意上面抠艾,我們從查看器的渲染循環(huán)中調(diào)用self.scene.render()。 場(chǎng)景是什么桨昙?

Scene類是我們用來表示設(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)目的列表凹蜂,名為node_list。 它也跟蹤所選項(xiàng)目阁危。 場(chǎng)景中的渲染函數(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()

Nodes

在場(chǎng)景的render函數(shù)中,我們對(duì)場(chǎng)景中node_list的每個(gè)項(xiàng)目調(diào)用render函數(shù)欲芹。但是這些列表中的元素都是什么呢卿啡?我們稱他們?yōu)楣?jié)點(diǎn)吟吝。理論上菱父,一個(gè)節(jié)點(diǎn)就是可以放在場(chǎng)景中任何東西。在面向?qū)ο蟮能浖薪L樱覀儼?code>Node寫成一個(gè)抽象基類浙宜。任何在Scene中表示對(duì)象的東西都是從這個(gè)Node繼承而來的。這個(gè)基類讓我們可以抽象地解釋場(chǎng)景蛹磺。代碼庫地其余部分不需要知道它顯示對(duì)象的細(xì)節(jié)粟瞬;它只需要知道它們是類節(jié)點(diǎn)。

每種Node都定義了渲染它或者和它交互的行為萤捆。這個(gè)Node保持跟蹤關(guān)于它自己的重要數(shù)據(jù):平移矩陣裙品、縮放矩陣、顏色等俗或。將節(jié)點(diǎn)的平移矩陣乘上它的縮放矩陣就得將它從節(jié)點(diǎn)模型坐標(biāo)空間到世界坐標(biāo)空間的轉(zhuǎn)換矩陣市怎。該節(jié)點(diǎn)還存儲(chǔ)一個(gè)軸對(duì)齊的邊界框(AABB)。 當(dāng)我們?cè)谙旅嬗懻撨x擇時(shí)辛慰,我們會(huì)看到更多關(guān)于AABB的信息区匠。

Node最簡(jiǎn)單的具體實(shí)現(xiàn)是一個(gè)原語。 基元是可以添加到場(chǎng)景中的單個(gè)固體形狀帅腌。 在這個(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

基于每個(gè)節(jié)點(diǎn)存儲(chǔ)的轉(zhuǎn)換矩陣對(duì)節(jié)點(diǎn)進(jìn)行渲染。節(jié)點(diǎn)的變換矩陣是其縮放矩陣與其平移矩陣的組合戚篙。 無論節(jié)點(diǎn)是什么類型五鲫,渲染的第一步是將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)所做的任何更改忆某。 我們使用OpenGL中的glPushMatrixglPopMatrix函數(shù)在渲染節(jié)點(diǎn)之前和之后保存和恢復(fù)ModelView矩陣的狀態(tài)。 請(qǐng)注意阔蛉,節(jié)點(diǎn)存儲(chǔ)其顏色弃舒,位置和比例,并在渲染之前將這些應(yīng)用在OpenGL狀態(tài)状原。

如果節(jié)點(diǎn)當(dāng)前被選中聋呢,我們使它發(fā)光。 這樣颠区,用戶就可以看到他們選擇了哪個(gè)節(jié)點(diǎn)削锰。

為了渲染基元,我們使用OpenGL的調(diào)用列表功能毕莱。 OpenGL調(diào)用列表是一系列OpenGL調(diào)用器贩,它們被定義一次并以單一名稱捆綁在一起。 可以使用glCallList(LIST_NAME)分配調(diào)用朋截。 每個(gè)基元(球體和立方體)定義了渲染它所需的調(diào)用列表(未顯示)蛹稍。

例如,立方體的調(diào)用列表繪制了立方體的6個(gè)面部服,其中心位于原點(diǎn)唆姐,而邊緣正好為1個(gè)單位長(zhǎng)。

# Pseudocode Cube definition
# 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)用程序來說是相當(dāng)有限的廓八。 3D模型通常由多個(gè)基元組成(或三角形網(wǎng)格奉芦,這在本項(xiàng)目的范圍之外)。 幸運(yùn)的是剧蹂,我們的Node類的設(shè)計(jì)使多個(gè)基元節(jié)點(diǎn)組成場(chǎng)景變得方便声功。 事實(shí)上,我們可以在不增加復(fù)雜性的情況下支持任意節(jié)點(diǎn)分組国夜。

作為動(dòng)力减噪,讓我們考慮一個(gè)非常基本的數(shù)字:一個(gè)典型的雪人,或由三個(gè)球體組成的雪花圖筹裕。 即使該圖由三個(gè)獨(dú)立的基元組成醋闭,我們希望能夠?qū)⑺暈閱蝹€(gè)對(duì)象。

我們創(chuàng)建一個(gè)名為HierarchicalNode的類朝卒,一個(gè)包含其他節(jié)點(diǎn)的節(jié)點(diǎn)证逻。 它管理一系列“孩子”。HierarchicalNoderender_self函數(shù)只需在每個(gè)子節(jié)點(diǎn)上調(diào)用render_self抗斤。 使用HierarchicalNode類囚企,向場(chǎng)景添加圖像非常簡(jiǎn)單。 現(xiàn)在瑞眼,定義雪圖與指定構(gòu)成它的形狀以及它們的相對(duì)位置和大小一樣簡(jiǎn)單龙宏。

子類的層次結(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ì)觀察到Node對(duì)象形成了一個(gè)樹形數(shù)據(jù)結(jié)構(gòu)。 渲染函數(shù)通過分層節(jié)點(diǎn)在樹中進(jìn)行深度優(yōu)先遍歷伤疙。 在遍歷時(shí)银酗,它會(huì)把用于轉(zhuǎn)換到世界空間的ModelView矩陣壓入棧中。 在每個(gè)步驟中徒像,它將當(dāng)前的ModelView矩陣入棧黍特,當(dāng)它完成所有子節(jié)點(diǎn)的渲染時(shí),它會(huì)將矩陣從堆棧中彈出锯蛀,并將父節(jié)點(diǎn)的ModelView矩陣留在堆棧的頂部灭衷。

通過以這種方式使Node類可擴(kuò)展,我們可以向場(chǎng)景添加新類型的形狀旁涤,而無需更改用于場(chǎng)景操縱和渲染的任何其他代碼翔曲。 使用節(jié)點(diǎn)概念來抽象出一個(gè)場(chǎng)景對(duì)象可能有很多孩子的事實(shí)被稱為復(fù)合設(shè)計(jì)模式。

用戶交互

現(xiàn)在我們的建模器能夠存儲(chǔ)和顯示場(chǎng)景拭抬,我們需要一種與之交互的方式部默。 我們需要促進(jìn)兩種類型的互動(dòng)。 首先造虎,我們需要改變場(chǎng)景觀看角度的能力。 我們希望能夠在場(chǎng)景中移動(dòng)眼睛或相機(jī)纷闺。 其次算凿,我們需要能夠添加新節(jié)點(diǎn)并修改場(chǎng)景中的節(jié)點(diǎn)。

要啟用用戶交互犁功,我們需要知道用戶何時(shí)按下鍵或移動(dòng)鼠標(biāo)氓轰。 幸運(yùn)的是,操作系統(tǒng)已經(jīng)知道這些事件何時(shí)發(fā)生浸卦。 GLUT允許我們注冊(cè)一個(gè)函數(shù)署鸡,在某個(gè)事件發(fā)生時(shí)被調(diào)用。 我們編寫函數(shù)來解釋按鍵和鼠標(biāo)移動(dòng),并告訴GLUT在按下相應(yīng)的鍵時(shí)調(diào)用這些函數(shù)靴庆。 一旦我們知道用戶正在按下哪些按鍵时捌,我們需要解釋輸入并將預(yù)期動(dòng)作應(yīng)用到場(chǎng)景中。

Interaction類中可以找到用于監(jiān)聽操作系統(tǒng)事件并解釋其含義的邏輯炉抒。 我們之前編寫的Viewer類擁有Interaction的單一實(shí)例奢讨。 我們將使用GLUT回調(diào)機(jī)制來注冊(cè)當(dāng)按下鼠標(biāo)按鈕時(shí)(glutMouseFunc),當(dāng)移動(dòng)鼠標(biāo)時(shí)(glutMotionFunc)焰薄,按下鍵盤按鈕(glutKeyboardFunc)拿诸,以及按下方向鍵時(shí)要調(diào)用的函數(shù)(glutSpecialFunc)。 我們將很快看到處理輸入事件的函數(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)

操作系統(tǒng)回調(diào)函數(shù)

為了有意義地解釋用戶輸入亩码,我們需要結(jié)合鼠標(biāo)位置,鼠標(biāo)按鈕和鍵盤的知識(shí)野瘦。 因?yàn)閷⒂脩糨斎虢忉尀橛幸饬x的動(dòng)作需要很多代碼行蟀伸,所以我們將它封裝在一個(gè)獨(dú)立的類中,遠(yuǎn)離主代碼路徑缅刽。 Interaction類隱藏了與代碼庫其余部分無關(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()

內(nèi)部回調(diào)

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

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

當(dāng)用戶界面代碼需要在場(chǎng)景中觸發(fā)事件時(shí)畦戒,Interaction類會(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)用程序中的有意義的請(qǐng)求结序。 Interaction類充當(dāng)操作系統(tǒng)事件和應(yīng)用程序級(jí)事件之間的轉(zhuǎn)換器障斋。 這意味著如果我們決定將建模器移植到除GLUT之外的另一個(gè)工具包中,我們只需要用一個(gè)將新工具箱的輸入轉(zhuǎn)換為同一組有意義的應(yīng)用級(jí)回調(diào)的類來替換Interaction類徐鹤。 我們?cè)谙卤碇惺褂没卣{(diào)和參數(shù)

回調(diào)函數(shù) 參數(shù) 作用
pick x:number, y:number Selects the node at the mouse pointer location.
move x:number, y:number Moves the currently selected node to the mouse pointer location.
place shape:string, x:number, y:number Places a shape of the specified type at the mouse pointer location.
rotate_color forward:boolean Rotates the color of the currently selected node through the list of colors, forwards or backwards.
scale up:boolean Scales the currently selected node up or down, according to parameter.

這個(gè)簡(jiǎn)單的回調(diào)系統(tǒng)提供了我們?cè)谶@個(gè)項(xiàng)目中需要的所有功能垃环。 然而,在構(gòu)建3D建模器中,用戶界面對(duì)象通常是動(dòng)態(tài)創(chuàng)建和銷毀的。 在這種情況下古戴,我們需要一個(gè)更復(fù)雜的事件監(jiān)聽系統(tǒng),其中對(duì)象既可以注冊(cè)也可以取消注冊(cè)事件回調(diào)涛目。

接入場(chǎng)景

通過我們的回調(diào)機(jī)制秸谢,我們可以從Interaction類接收關(guān)于用戶輸入事件的有意義的信息。 我們準(zhǔn)備將這些操作應(yīng)用到場(chǎng)景中霹肝。

移動(dòng)場(chǎng)景

在這個(gè)項(xiàng)目中估蹄,我們通過變換場(chǎng)景來完成相機(jī)運(yùn)動(dòng)。換句話說阿迈,相機(jī)處于固定位置元媚,用戶輸入移動(dòng)場(chǎng)景而不是移動(dòng)相機(jī)。相機(jī)放置在[0, 0, -15]并且對(duì)著世界空間的中心(或者苗沧,我們可以改變透視矩陣來移動(dòng)相機(jī)而不是場(chǎng)景刊棕。 這個(gè)設(shè)計(jì)決定對(duì)其余的項(xiàng)目影響很小。)重新瀏覽Viewer中的render函數(shù)待逞,我們看到Interaction狀態(tài)用于在渲染場(chǎng)景之前轉(zhuǎn)換OpenGL矩陣狀態(tài)甥角。 有兩種與Scene交互的類型:旋轉(zhuǎn)和平移。

用一個(gè)軌跡球旋轉(zhuǎn)場(chǎng)景

我們通過使用軌跡球算法來完成場(chǎng)景的旋轉(zhuǎn)识樱。 軌跡球是用于三維操縱場(chǎng)景的直觀界面嗤无。 從概念上講,軌跡球界面的功能就好像場(chǎng)景在透明球體內(nèi)一樣怜庸。 將一只手放在地球表面并推動(dòng)它旋轉(zhuǎn)地球当犯。 同樣,單擊鼠標(biāo)右鍵并在屏幕上移動(dòng)它可以旋轉(zhuǎn)場(chǎng)景割疾。你可以在OpenGL Wiki中找到更多關(guān)于軌跡球理論的信息嚎卫。 在這個(gè)項(xiàng)目中,我們使用作為Glumpy的一部分提供的軌跡球?qū)嵤?/p>

我們使用drag_to函數(shù)與軌跡球進(jìn)行交互宏榕,將鼠標(biāo)的當(dāng)前位置作為起始位置拓诸,并將鼠標(biāo)位置的變化作為參數(shù)。

self.trackball.drag_to(self.mouse_loc[0], self.mouse_loc[1], dx, dy)

當(dāng)渲染場(chǎng)景時(shí)麻昼,生成的旋轉(zhuǎn)矩陣是Viewer中的trackball.matrix奠支。

補(bǔ)充:四元數(shù)

旋轉(zhuǎn)有兩種傳統(tǒng)的方式表示。 第一個(gè)是圍繞每個(gè)軸的旋轉(zhuǎn)值; 你可以將它存儲(chǔ)為浮點(diǎn)數(shù)的三元組抚芦。 旋轉(zhuǎn)的另一種常見表示是四元數(shù)倍谜,由具有xyz坐標(biāo)的矢量以及w旋轉(zhuǎn)組成的元素燕垃。 使用四元數(shù)對(duì)于每軸旋轉(zhuǎn)有許多好處; 特別是它們?cè)跀?shù)值上更穩(wěn)定枢劝。 使用四元數(shù)可以避免類似萬向節(jié)鎖的問題。 四元數(shù)的缺點(diǎn)是它們不太直觀卜壕,難以理解。 如果你很勇敢并想了解更多關(guān)于四元數(shù)的內(nèi)容烙常,可以參考這個(gè)解釋轴捎。

軌跡球的實(shí)現(xiàn)使通過在內(nèi)部使用四元數(shù)存儲(chǔ)場(chǎng)景的旋轉(zhuǎn)來避免萬向節(jié)鎖定鹤盒。 幸運(yùn)的是,我們不需要直接使用四元數(shù)侦副,因?yàn)檐壽E球上的矩陣成員會(huì)將旋轉(zhuǎn)轉(zhuǎn)換為矩陣侦锯。

場(chǎng)景轉(zhuǎn)換

場(chǎng)景轉(zhuǎn)移(即滑動(dòng)場(chǎng)景)比旋轉(zhuǎn)場(chǎng)景要簡(jiǎn)單得多。 提供隨鼠標(biāo)滾輪和鼠標(biāo)左鍵一起的場(chǎng)景轉(zhuǎn)換秦驯。 鼠標(biāo)左鍵在xy坐標(biāo)中轉(zhuǎn)換場(chǎng)景尺碰。 滾動(dòng)鼠標(biāo)滾輪可以將場(chǎng)景轉(zhuǎn)換為z坐標(biāo)(朝向或遠(yuǎn)離攝像機(jī))。 Interaction類存儲(chǔ)當(dāng)前的場(chǎng)景轉(zhuǎn)換并使用平移功能修改它译隘。 查看器在渲染過程中檢索交互攝像頭位置以用于glTranslated調(diào)用亲桥。

選擇場(chǎng)景對(duì)象

現(xiàn)在,用戶可以移動(dòng)和旋轉(zhuǎn)整個(gè)場(chǎng)景以獲得他們想要的視角固耘,下一步是允許用戶修改和操作構(gòu)成場(chǎng)景的對(duì)象题篷。

為了讓用戶操作場(chǎng)景中的對(duì)象,他們需要能夠選擇場(chǎng)景中的對(duì)象厅目。

要選擇一個(gè)項(xiàng)目番枚,我們使用當(dāng)前投影矩陣生成代表鼠標(biāo)點(diǎn)擊的光線,就好像鼠標(biāo)指針將射線投射到場(chǎng)景中一樣损敷。 所選節(jié)點(diǎn)是射線與射線相交的最近節(jié)點(diǎn)葫笼。 因此,拾取問題簡(jiǎn)化為在光線和場(chǎng)景中的節(jié)點(diǎn)之間找到交點(diǎn)的問題拗馒。 所以問題是:我們?nèi)绾闻袛喙饩€是否碰到節(jié)點(diǎn)路星?

準(zhǔn)確計(jì)算光線是否與節(jié)點(diǎn)相交是一個(gè)在代碼復(fù)雜性和性能方面具有挑戰(zhàn)性的問題。 我們需要為每種類型的基元編寫一個(gè)光線對(duì)象交叉檢查瘟忱。 對(duì)于具有許多面的復(fù)雜網(wǎng)格幾何形狀的場(chǎng)景節(jié)點(diǎn)奥额,計(jì)算精確的光線對(duì)象相交將需要測(cè)試每個(gè)面的光線,并且計(jì)算起來會(huì)有很高的代價(jià)访诱。

為了保持代碼緊湊和性能合理垫挨,我們使用簡(jiǎn)單,快速的近似值進(jìn)行光線對(duì)象相交測(cè)試触菜。 在我們的實(shí)現(xiàn)中九榔,每個(gè)節(jié)點(diǎn)都保存一個(gè)軸對(duì)齊的邊界框(AABB),它是節(jié)點(diǎn)占據(jù)的空間的近似值涡相。 為了測(cè)試光線是否與節(jié)點(diǎn)相交哲泊,我們測(cè)試光線是否與節(jié)點(diǎn)的AABB相交。 這種實(shí)現(xiàn)意味著所有節(jié)點(diǎn)共享相同的代碼進(jìn)行相交測(cè)試催蝗,對(duì)于所有節(jié)點(diǎn)類型而言這意味著性能開銷都是固定的小的切威。

    # 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)被點(diǎn)擊,我們遍歷場(chǎng)景來測(cè)試光線是否碰到任何節(jié)點(diǎn)丙号。 我們?nèi)∠x擇當(dāng)前選擇的節(jié)點(diǎn)先朦,然后選擇最靠近射線源的交點(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類中,pick函數(shù)測(cè)試光線是否與節(jié)點(diǎn)的軸對(duì)齊邊界框相交喳魏。 如果選擇了節(jié)點(diǎn)棉浸,則選擇功能切換節(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)都將自己的變換應(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í)施嗡害。 但是,在某些情況下結(jié)果是錯(cuò)誤的三幻。

AABB 錯(cuò)誤

例如就漾,在Sphere基元的情況下,球體本身只觸及每個(gè)AABB面的中心的AABB念搬。 但是抑堡,如果用戶點(diǎn)擊Sphere的AABB的角落,即使用戶打算點(diǎn)擊Sphere后面的某個(gè)東西朗徊,碰撞也會(huì)被Sphere檢測(cè)到首妖。

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

調(diào)整場(chǎng)景對(duì)象

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

當(dāng)Viewer收到其中一個(gè)事件的回調(diào)時(shí),它會(huì)調(diào)用場(chǎng)景上的相應(yīng)功能魂务,然后將該變換應(yīng)用于當(dāng)前選定的節(jié)點(diǎn)曼验。

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

改變顏色

操作顏色是通過一系列可能的顏色來完成的。 用戶可以通過箭頭鍵在列表中循環(huán)粘姜。 場(chǎng)景將顏色更改命令分派給當(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í)豺裆,顏色將通過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

節(jié)點(diǎn)縮放

與顏色一樣号显,場(chǎng)景會(huì)將所有縮放修改分派給所選節(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)有一個(gè)存儲(chǔ)其比例的當(dāng)前矩陣翼抠。 在這些相應(yīng)方向上通過參數(shù)x咙轩,y和z縮放的矩陣是:

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

給定一個(gè)含x活喊,yz縮放因子的列表丐膝,函數(shù)scaling返回這樣一個(gè)矩陣。

移動(dòng)節(jié)點(diǎn)

為了轉(zhuǎn)化節(jié)點(diǎn)钾菊,我們使用與選取對(duì)象相同的射線計(jì)算方法帅矗。 我們將代表當(dāng)前鼠標(biāo)位置的射線傳遞給場(chǎng)景的move函數(shù)。 節(jié)點(diǎn)的新位置應(yīng)該在射線上煞烫。 為了確定光線放置節(jié)點(diǎn)的位置浑此,我們需要知道節(jié)點(diǎn)距相機(jī)的距離。 由于我們存儲(chǔ)節(jié)點(diǎn)的位置和相機(jī)在選中時(shí)的位置(在pick函數(shù)中)滞详,我們可以在這里使用這些數(shù)據(jù)凛俱。 我們發(fā)現(xiàn)沿著目標(biāo)光線與相機(jī)距離相同的點(diǎn),并計(jì)算新舊位置之間的矢量差料饥。 然后我們通過結(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)空間中定義我們的平移原叮。 因此,我們通過乘以模型視圖矩陣的逆巡蘸,從攝像機(jī)空間平移到世界空間奋隶。

與縮放一樣,每個(gè)節(jié)點(diǎn)存儲(chǔ)代表其平移的矩陣悦荒。平移矩陣如下所示:

\begin{bmatrix} 1 & 0 & 0 & x \\\\ 0 & 1 & 0 & y \\\\ 0 & 0 & 1 & z \\\\ 0 & 0 & 0 & 1 \end{bmatrix}

當(dāng)節(jié)點(diǎn)被平移時(shí)唯欣,我們?yōu)楫?dāng)前平移構(gòu)建一個(gè)新的平移矩陣,并將其乘以節(jié)點(diǎn)的平移矩陣以便在渲染過程中使用逾冬。

    # class Node
    def translate(self, x, y, z):
        self.translation_matrix = numpy.dot(
            self.translation_matrix, 
            translation([x, y, z]))

平移函數(shù)返回給定表示x黍聂,y和z平移距離的列表的平移矩陣。

放置節(jié)點(diǎn)

節(jié)點(diǎn)布局使用拾取和平移技術(shù)身腻。 我們對(duì)當(dāng)前鼠標(biāo)位置使用相同的光線計(jì)算來確定放置節(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)

要放置一個(gè)新節(jié)點(diǎn),我們首先創(chuàng)建相應(yīng)類型節(jié)點(diǎn)的新實(shí)例并將其添加到場(chǎng)景中嘀趟。 我們希望將節(jié)點(diǎn)放置在用戶的光標(biāo)下脐区,因此我們?cè)诰嚯x相機(jī)固定距離的光線上找到一個(gè)點(diǎn)。 因?yàn)樗矗饩€是在相機(jī)空間中表示的牛隅,所以我們通過將其與逆模型視圖矩陣相乘炕柔,將得到的平移向量轉(zhuǎn)換為世界坐標(biāo)空間。 最后媒佣,我們通過計(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])

總結(jié)

恭喜! 我們已經(jīng)成功實(shí)現(xiàn)了一個(gè)小型3D建模器默伍!

簡(jiǎn)單場(chǎng)景

我們看了如何開發(fā)一個(gè)可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)來表示場(chǎng)景中的對(duì)象欢嘿。 我們注意到,使用Composite設(shè)計(jì)模式和基于樹的數(shù)據(jù)結(jié)構(gòu)可以輕松遍歷場(chǎng)景進(jìn)行渲染也糊,并允許我們添加新類型的節(jié)點(diǎn)而不增加復(fù)雜性炼蹦。 我們利用這個(gè)數(shù)據(jù)結(jié)構(gòu)將設(shè)計(jì)渲染到屏幕上,并在場(chǎng)景圖的遍歷中操縱OpenGL矩陣狸剃。 我們?yōu)閼?yīng)用程序級(jí)事件構(gòu)建了一個(gè)非常簡(jiǎn)單的回調(diào)系統(tǒng)掐隐,并使用它來封裝操作系統(tǒng)事件的處理。 我們討論了射線 - 物體碰撞檢測(cè)的可能實(shí)現(xiàn)方式钞馁,以及正確性虑省,復(fù)雜性和性能之間的權(quán)衡。 最后指攒,我們實(shí)現(xiàn)了處理場(chǎng)景內(nèi)容的方法慷妙。

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

我們可以做進(jìn)一步的實(shí)驗(yàn)來為這個(gè)項(xiàng)目添加新的功能。 嘗試其中之一:

  • 添加Node類型以支持任意形狀的三角形網(wǎng)格全闷。
  • 添加撤消堆棧叉寂,以允許撤消/重做模型操作。
  • 使用DXF等3D文件格式保存/加載設(shè)計(jì)总珠。
  • 整合渲染引擎:導(dǎo)出設(shè)計(jì)以用于照片級(jí)渲染器屏鳍。
  • 通過精確的光線對(duì)象交叉來改善碰撞檢測(cè)。

更多拓展

為了深入了解真實(shí)世界的3D建模軟件局服,一些開源項(xiàng)目很有趣钓瞭。

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

OpenSCAD是一款開源3D建模工具鸭丛。 它不是互動(dòng)的; 相反竞穷,它讀取指定如何生成場(chǎng)景的腳本文件。 這可以讓設(shè)計(jì)師“完全控制建模過程”鳞溉。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末穿挨,一起剝皮案震驚了整個(gè)濱河市月弛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌科盛,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菜皂,死亡現(xiàn)場(chǎng)離奇詭異贞绵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恍飘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門榨崩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人章母,你說我怎么就攤上這事母蛛。” “怎么了乳怎?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵彩郊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蚪缀,道長(zhǎng)秫逝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任询枚,我火速辦了婚禮违帆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘金蜀。我一直安慰自己刷后,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布渊抄。 她就那樣靜靜地躺著尝胆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抒线。 梳的紋絲不亂的頭發(fā)上班巩,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼抱慌。 笑死逊桦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抑进。 我是一名探鬼主播强经,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼寺渗!你這毒婦竟也來了匿情?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤信殊,失蹤者是張志新(化名)和其女友劉穎炬称,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涡拘,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玲躯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鳄乏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跷车。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖橱野,靈堂內(nèi)的尸體忽然破棺而出朽缴,到底是詐尸還是另有隱情,我是刑警寧澤水援,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布密强,位于F島的核電站,受9級(jí)特大地震影響裹唆,放射性物質(zhì)發(fā)生泄漏誓斥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一许帐、第九天 我趴在偏房一處隱蔽的房頂上張望劳坑。 院中可真熱鬧,春花似錦成畦、人聲如沸距芬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽框仔。三九已至,卻和暖如春拄养,著一層夾襖步出監(jiān)牢的瞬間离斩,已是汗流浹背银舱。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留跛梗,地道東北人寻馏。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像核偿,于是被迫代替她去往敵國(guó)和親诚欠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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