用Python完成一件小事:3D模型渲染

昨晚帶兒子去看了成都芙蓉園的燈會。絢爛的燈光和精美的燈體造型,簡直令人歎為觀止,稱之為藝術一點也不誇張。無圖無真相,先分享幾張照片。


用Python完成一件小事:3D模型渲染

八方來財


用Python完成一件小事:3D模型渲染

燈籠高掛


用Python完成一件小事:3D模型渲染

夢幻神鹿

看著這些漂亮的燈體模型,就禁不住在想,用Python來渲染3D模型是不是也挺有趣呢?於是,我確定了今天的分享主題:一款有趣的3D引擎-Panda3D。

Panda3D(https://www.panda3d.org),是一個免費的3D渲染引擎,支持Python和C++語言。用戶可使用Python完整的構建和渲染3D場景及其中的模型對象,甚至用它製作一款3D遊戲。

要使用Panda3D,需要先安裝SDK(最新版本是1.10.6)。有兩種方式:

(1)直接下載安裝包安裝(以Windows為例)


用Python完成一件小事:3D模型渲染

下載windows版本SDK

(2)如果已經安裝了Python,則可以使用pip進行安裝

<code>pip install panda3d==1.10.6/<code>

等待安裝完成即可。

Panda3D的基礎框架比較簡單,可以使用很少量代碼就能看到效果,所以比較容易上手,值得大家瞭解和學習。

那麼,接下來必然是Panda3D版本的Hello World程序。

第一步:加載場景

<code>from direct.showbase.ShowBase import ShowBase


class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        # 加載環境場景,models/environment是SDK默認的資源可以直接使用.
        self.scene = self.loader.loadModel("models/environment")
        # Reparent the model to render.
        self.scene.reparentTo(self.render)
        # Apply scale and position transforms on the model.
        self.scene.setScale(0.25, 0.25, 0.25)
        self.scene.setPos(-8, 42, 0)

app = MyApp()
app.run()/<code>

代碼中的scene是Panda3D中的場景對象,是每個3D應用都必不可少的。"models/environment"是SDK默認的資源,可以直接使用。

而“self.scene.reparentTo(self.render)”一行代碼中reparentTo字面上就是把scene的所屬指向render。這裡提到的render的比較關鍵,所有場景的最根部就是render,瞭解過3D渲染的朋友一定知道,為了方便計算機對場景中各對象的查詢搜索,所有對象在計算機中都是用樹狀數據結構來進行管理的,這顆樹的根,就是render。

setScale和setPos就是設置縮放比例和初始座標位置。

該段代碼運行之後的效果是:


用Python完成一件小事:3D模型渲染

第二步:加入對相機的控制

<code>class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        # Load the environment model.
        self.scene = self.loader.loadModel("models/environment")
        # Reparent the model to render.
        self.scene.reparentTo(self.render)
        # Apply scale and position transforms on the model.
        self.scene.setScale(0.25, 0.25, 0.25)
        self.scene.setPos(-8, 42, 0)

        # Add the spinCameraTask procedure to the task manager.
        self.taskMgr.add(self.spinCameraTask, "SpinCameraTask")

    # Define a procedure to move the camera.
    def spinCameraTask(self, task):
        angleDegrees = task.time * 6.0
        angleRadians = angleDegrees * (pi / 180.0)
        self.camera.setPos(20 * sin(angleRadians), -20 * cos(angleRadians), 3)
        self.camera.setHpr(angleDegrees, 0, 0)
        return Task.cont/<code>

該段代碼在原基礎上,加入了相機的旋轉。運行後可以看到畫面在動態旋轉,此時,對環境的觀察可以更加全面了。

接下來,是最重要的模型渲染。

第三步:模型加載及動畫渲染

<code>class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        # Load the environment model.
        self.scene = self.loader.loadModel("models/environment")
        # Reparent the model to render.
        self.scene.reparentTo(self.render)
        # Apply scale and position transforms on the model.
        self.scene.setScale(0.25, 0.25, 0.25)
        self.scene.setPos(-8, 42, 0)

        # Add the spinCameraTask procedure to the task manager.
        self.taskMgr.add(self.spinCameraTask, "SpinCameraTask")

        # Load and transform the panda actor.
        self.pandaActor = Actor("models/panda-model",
                                {"walk": "models/panda-walk4"})
        self.pandaActor.setScale(0.005, 0.005, 0.005)
        self.pandaActor.reparentTo(self.render)
        # Loop its animation.
        self.pandaActor.loop("walk")/<code>

self.pandaActor = Actor("models/panda-model",{"walk": "models/panda-walk4"})

self.pandaActor.loop("walk")

這兩行代碼是關鍵,一是讓熊貓模型和行走的動畫數據進行加載,二是讓行走動畫一直循環執行。

運行之後,就可以看到一隻憨態可掬的大熊貓行走的畫面了。


用Python完成一件小事:3D模型渲染

熊貓行走

怎麼樣,使用Panda3D渲染一個模型是不是非常簡單呀?我們再看一個比較複雜的示例:漫遊拉爾夫(Roaming-Ralph)。也是Panda3D自帶的示例。


用Python完成一件小事:3D模型渲染

代碼在安裝目錄的samples/roaming-ralph中,models中是模型和環境相關文件,main.py是Python主程序。

該示例在常規的環境、模型渲染之外,增加了對人物和攝像機的控制,已經是一個簡單的遊戲場景了。

關鍵代碼如下:

(1)增加鍵盤控制人物和相機旋轉功能。

<code># Accept the control keys for movement and rotation
        self.accept("escape", sys.exit)
        self.accept("arrow_left", self.setKey, ["left", True])
        self.accept("arrow_right", self.setKey, ["right", True])
        self.accept("arrow_up", self.setKey, ["forward", True])
        self.accept("a", self.setKey, ["cam-left", True])
        self.accept("s", self.setKey, ["cam-right", True])
        self.accept("arrow_left-up", self.setKey, ["left", False])
        self.accept("arrow_right-up", self.setKey, ["right", False])
        self.accept("arrow_up-up", self.setKey, ["forward", False])
        self.accept("a-up", self.setKey, ["cam-left", False])
        self.accept("s-up", self.setKey, ["cam-right", False])
        taskMgr.add(self.move, "moveTask")/<code>

(2)增加了碰撞檢測和限制功能。

<code># We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain.  One ray will
        # start above ralph's head, and the other will start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.
        self.cTrav = CollisionTraverser()

        self.ralphGroundRay = CollisionRay()
        self.ralphGroundRay.setOrigin(0, 0, 9)
        self.ralphGroundRay.setDirection(0, 0, -1)
        self.ralphGroundCol = CollisionNode('ralphRay')
        self.ralphGroundCol.addSolid(self.ralphGroundRay)
        self.ralphGroundCol.setFromCollideMask(CollideMask.bit(0))
        self.ralphGroundCol.setIntoCollideMask(CollideMask.allOff())
        self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol)
        self.ralphGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler)

        self.camGroundRay = CollisionRay()
        self.camGroundRay.setOrigin(0, 0, 9)
        self.camGroundRay.setDirection(0, 0, -1)
        self.camGroundCol = CollisionNode('camRay')
        self.camGroundCol.addSolid(self.camGroundRay)
        self.camGroundCol.setFromCollideMask(CollideMask.bit(0))
        self.camGroundCol.setIntoCollideMask(CollideMask.allOff())
        self.camGroundColNp = self.camera.attachNewNode(self.camGroundCol)
        self.camGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)/<code>

(3)增加人物運動時的場景追蹤變換功能。

<code># Accepts arrow keys to move either the player or the menu cursor,
    # Also deals with grid checking and collision detection
    def move(self, task):
        # Get the time that elapsed since last frame.  We multiply this with
        # the desired speed in order to find out with which distance to move
        # in order to achieve that desired speed.
        dt = globalClock.getDt()

        # If the camera-left key is pressed, move camera left.
        # If the camera-right key is pressed, move camera right.
        if self.keyMap["cam-left"]:
            self.camera.setX(self.camera, -20 * dt)
        if self.keyMap["cam-right"]:
            self.camera.setX(self.camera, +20 * dt)

        # save ralph's initial position so that we can restore it,
        # in case he falls off the map or runs into something.

        startpos = self.ralph.getPos()
        # If a move-key is pressed, move ralph in the specified direction.

        if self.keyMap["left"]:
            self.ralph.setH(self.ralph.getH() + 300 * dt)
        if self.keyMap["right"]:
            self.ralph.setH(self.ralph.getH() - 300 * dt)
        if self.keyMap["forward"]:
            self.ralph.setY(self.ralph, -25 * dt)

        # If ralph is moving, loop the run animation.
        # If he is standing still, stop the animation.

        if self.keyMap["forward"] or self.keyMap["left"] or self.keyMap["right"]:
            if self.isMoving is False:
                self.ralph.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.ralph.stop()
                self.ralph.pose("walk", 5)
                self.isMoving = False

        # If the camera is too far from ralph, move it closer.
        # If the camera is too close to ralph, move it farther.
        camvec = self.ralph.getPos() - self.camera.getPos()
        camvec.setZ(0)
        camdist = camvec.length()
        camvec.normalize()
        if camdist > 10.0:
            self.camera.setPos(self.camera.getPos() + camvec * (camdist - 10))
            camdist = 10.0
        if camdist < 5.0:
            self.camera.setPos(self.camera.getPos() - camvec * (5 - camdist))
            camdist = 5.0
        # Normally, we would have to call traverse() to check for collisions.
        # However, the class ShowBase that we inherit from has a task to do
        # this for us, if we assign a CollisionTraverser to self.cTrav.
        #self.cTrav.traverse(render)

        # Adjust ralph's Z coordinate.  If ralph's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.
        entries = list(self.ralphGroundHandler.getEntries())
        entries.sort(key=lambda x: x.getSurfacePoint(render).getZ())

        if len(entries) > 0 and entries[0].getIntoNode().getName() == "terrain":
            self.ralph.setZ(entries[0].getSurfacePoint(render).getZ())
        else:
            self.ralph.setPos(startpos)

        # Keep the camera at one foot above the terrain,
        # or two feet above ralph, whichever is greater.

        entries = list(self.camGroundHandler.getEntries())
        entries.sort(key=lambda x: x.getSurfacePoint(render).getZ())

        if len(entries) > 0 and entries[0].getIntoNode().getName() == "terrain":
            self.camera.setZ(entries[0].getSurfacePoint(render).getZ() + 1.0)
        if self.camera.getZ() < self.ralph.getZ() + 2.0:
            self.camera.setZ(self.ralph.getZ() + 2.0)

        # The camera should look in ralph's direction,
        # but it should also try to stay horizontal, so look at
        # a floater which hovers above ralph's head.
        self.camera.lookAt(self.floater)
        return task.cont/<code>

最終運行的效果比較有趣,可以用鍵盤對畫面中的人物進行操控,使其在環境中四處漫遊。


用Python完成一件小事:3D模型渲染


用Python完成一件小事:3D模型渲染

結束語

本次分享就到這裡,感謝閱讀。今天的內容只是把Panda3D這樣一款優秀的3D引擎介紹給大家,把示例的運行進行了展示,目的是能起一個“點火”的作用,讓感興趣的朋友可以再深入去學習和了解。關於3D編程的數學原理和技術細節,我將在未來的分享中進行詳細闡述,請大家持續關注。


分享到:


相關文章: