昨晚帶兒子去看了成都芙蓉園的燈會。絢爛的燈光和精美的燈體造型,簡直令人歎為觀止,稱之為藝術一點也不誇張。無圖無真相,先分享幾張照片。
看著這些漂亮的燈體模型,就禁不住在想,用Python來渲染3D模型是不是也挺有趣呢?於是,我確定了今天的分享主題:一款有趣的3D引擎-Panda3D。
Panda3D(https://www.panda3d.org),是一個免費的3D渲染引擎,支持Python和C++語言。用戶可使用Python完整的構建和渲染3D場景及其中的模型對象,甚至用它製作一款3D遊戲。
要使用Panda3D,需要先安裝SDK(最新版本是1.10.6)。有兩種方式:
(1)直接下載安裝包安裝(以Windows為例)
(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就是設置縮放比例和初始座標位置。
該段代碼運行之後的效果是:
第二步:加入對相機的控制
<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")
這兩行代碼是關鍵,一是讓熊貓模型和行走的動畫數據進行加載,二是讓行走動畫一直循環執行。
運行之後,就可以看到一隻憨態可掬的大熊貓行走的畫面了。
怎麼樣,使用Panda3D渲染一個模型是不是非常簡單呀?我們再看一個比較複雜的示例:漫遊拉爾夫(Roaming-Ralph)。也是Panda3D自帶的示例。
代碼在安裝目錄的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>
最終運行的效果比較有趣,可以用鍵盤對畫面中的人物進行操控,使其在環境中四處漫遊。
結束語
本次分享就到這裡,感謝閱讀。今天的內容只是把Panda3D這樣一款優秀的3D引擎介紹給大家,把示例的運行進行了展示,目的是能起一個“點火”的作用,讓感興趣的朋友可以再深入去學習和了解。關於3D編程的數學原理和技術細節,我將在未來的分享中進行詳細闡述,請大家持續關注。