Перейти к основному содержимому

Трёхмерная графика и Panda3D

Разработчику Архитектору

Panda3D — открытый игровой движок с Python-API, который изначально разрабатывался Disney для онлайн-проектов вроде Toontown Online. Он даёт полноценный 3D-рендеринг (OpenGL/DirectX), загрузку моделей, освещение, камеру и игровой цикл — при этом основная логика пишется на Python.

Эта глава — мост между 2D-играми на Pygame и «настоящим» 3D: как устроена трёхмерная графика в экосистеме Python, из чего состоит Panda3D, где он уместен, и как собрать первые сцены из кода.

Что читать до и после

База Python — первая программа, точка входа main, venv и зависимости. Игровой цикл и события — Pygame. Графики функций и диаграммы — Matplotlib. Общая теория игр — раздел «Разработка игр».


Трёхмерная графика в Python

В Python нет встроенного 3D-рендерера: графика всегда идёт через сторонние библиотеки, которые оборачивают нативный код (C/C++) и OpenGL/Vulkan/DirectX.

ПодходПримерыУровеньТипичное применение
Полноценный движокPanda3D, UrsinaВысокий3D-игры, визуализации, симуляции
Тонкая обёртка над OpenGLPyOpenGL, ModernGLНизкийУчебные шейдеры, свой рендер
2D с «псевдо-3D»Pygame, ArcadeСредний (2D)Спрайты, тайловые карты
Скриптинг в DCCBlender Python APIЗависит от задачиКонтент, анимация, пайплайн
Внешний движок + PythonGodot (GDExtension), Unity (редко)ВысокийПродакшен-игры

Трёхмерная сцена в любом движке опирается на одни и те же идеи:

  • Вершины — точки в пространстве (координаты x, y, z).
  • Примитивы — треугольники, из которых собираются меши.
  • Текстуры и материалы — цвет и картинки на поверхности.
  • Освещение — ambient, directional, point, spot.
  • Камера — точка обзора и проекция (перспектива или ортографика).
  • Цикл кадров — обновление логики и отрисовка ~60 раз в секунду.

Python здесь управляет сценой и логикой; тяжёлая математика и вызовы GPU выполняет нативное ядро движка.

Готовая галерея скриптов с карточками, кубом, пирамидой и составными сценами — Примеры фигур Panda3D на Python. Для 2D-фигур в браузере (JavaScript, без Python) — Примеры фигур на Processing/p5.js. Для 2D-игр на том же Python — Pygame — мини-игры и глава Разработка игр на Python.


Panda3D — что это

Panda3D — это не «один pip-модуль», а движок с несколькими Python-пакетами и C++-ядром (libpanda, libpandaexpress и др.). Он включает:

  • рендер-пайплайн и шейдеры;
  • загрузчик моделей (egg, bam, glTF через плагины, OBJ и др.);
  • систему сценового графа (иерархия узлов);
  • физику (Bullet, ODE — опционально);
  • аудио (OpenAL);
  • ввод, окно, таймеры, задачи (taskMgr).

Движок кроссплатформенный (Windows, Linux, macOS) и распространяется под лицензией BSD. Для учебных 3D-проектов на чистом Python это один из самых зрелых вариантов.


Архитектура

Panda3D строится вокруг сценового графа — дерева узлов (NodePath). Каждый узел может нести геометрию, свет, камеру или пустой «контейнер» для группировки и трансформаций.

ShowBase (приложение)
├── render ← корень 3D-сцены
│ ├── card ← CardMaker / GeomNode
│ ├── cube
│ └── sun_np ← DirectionalLight
├── camera ← камера по умолчанию
├── loader ← текстуры, модели
└── taskMgr ← задачи каждый кадр

ShowBase

Класс ShowBase из direct.showbase.ShowBaseкаркас приложения. При создании он:

  • открывает окно и контекст рендеринга;
  • создаёт render, camera, loader, taskMgr, win, clock;
  • запускает главный цикл через run().

Наследование от ShowBase — типичный паттерн: в __init__ настраиваете сцену, в run() уходит управление движку.

NodePath и трансформации

NodePath — «ручка» к узлу с удобными методами setPos, setHpr (heading-pitch-roll), setScale, lookAt. Дочерние узлы наследуют трансформации родителя — так вращают группы объектов или крепят модель к «руке» персонажа.

Задачи (game loop)

taskMgr.add(callback, "name") регистрирует функцию, вызываемую каждый кадр. Колбэк получает task и возвращает task.cont (продолжить) или task.done. Время между кадрами — globalClock.getDt() — удобно для анимации, не зависящей от FPS.

Рендер и освещение

Объекты вешают на self.render. Свет — отдельные узлы (AmbientLight, DirectionalLight, …), которые подключают через render.setLight(np). Без света объекты с нормалями могут выглядеть чёрными; карточки с текстурой часто переводят в setLightOff().


Состав пакета — основные модули

Модуль / пакетНазначение
panda3d.coreЯдро: геометрия, математика (Vec3, Point3), текстуры, окно, рендер-состояния
panda3d.bulletФизика Bullet (опционально)
panda3d.odeФизика ODE (legacy)
direct.showbase.ShowBaseБазовый класс приложения
direct.taskЗадачи и таймеры
direct.actor.ActorСкелетная анимация персонажей
direct.gui2D-виджеты поверх 3D (DirectButton, OnscreenText)
direct.filterПостобработка (bloom, SSAO — через конфиг)

Установка с PyPI ставит бинарные колёса с уже собранным C++-ядром — отдельно компилировать Panda3D для учебных примеров обычно не нужно.


Ограничения и сложности

ТемаСуть
ПроизводительностьГорячие пути — на C++; Python годится для логики, AI, UI. Тысячи draw call или тяжёлый CPU-код на Python — узкое место.
GILПараллельные потоки Python не ускоряют вычисления на CPU; многопоточность в Panda3D чаще для I/O или C++-стороны.
Кривая обученияНужны базовые 3D-понятия (оси, euler, UV, нормали). API объёмное; документация местами устарела.
ЭкосистемаМеньше ассет-магазинов и туториалов, чем у Unity/Godot. Контент часто готовят в Blender и экспортируют.
РедакторНет единого «Panda3D Editor» уровня Unity; сцены собирают кодом или через panda3d-gltf / egg-файлы.
Размер дистрибутиваКолесо ~100+ MB — для встраиваемых скриптов это много.
Когда выбирать Panda3D

Уместен для учебных 3D-проектов на Python, визуализаций, прототипов и инструментов, где важен код, а не визуальный редактор. Для коммерческого AAA или мобильного хита чаще смотрят на Unity, Unreal или Godot; для 2D — Pygame проще. Для Unity + C# с редакторомкурс в редакторе и готовые скрипты в Lab.


Установка и первый запуск

Рекомендуется виртуальное окружение — см. Зависимости Python.

python -m venv panda_env
# Windows
panda_env\Scripts\activate
# Linux / macOS
source panda_env/bin/activate

pip install panda3d

Проверка:

import panda3d
print(panda3d.__version__)

Минимальное приложение — окно с цветным фоном и вращающейся карточкой:

#!/usr/bin/env python3
"""Minimal Panda3D application."""

from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, CardMaker, DirectionalLight


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

self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -8, 2)
self.camera.lookAt(0, 0, 0)

ambient = AmbientLight("ambient")
ambient.setColor((0.35, 0.35, 0.4, 1))
self.render.setLight(self.render.attachNewNode(ambient))

sun = DirectionalLight("sun")
sun.setColor((0.9, 0.9, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(45, -45, 0)
self.render.setLight(sun_np)

cm = CardMaker("card")
cm.setFrame(-2, 2, -2, 2)
self.card = self.render.attachNewNode(cm.generate())
self.card.setColor(0.25, 0.55, 0.95, 1)
self.card.setP(-20)

self.taskMgr.add(self.spin, "spin")

def spin(self, task):
self.card.setH(self.card.getH() + 60 * globalClock.getDt())
return task.cont


if __name__ == "__main__":
app = App()
app.run()

Разбор фрагмента:

  • ShowBase.__init__ поднимает окно и подсистемы.
  • disableMouse() отключает стандартное управление камерой мышью — в учебном примере камера фиксирована.
  • CardMaker создаёт плоский прямоугольник в плоскости XY; setP(-20) наклоняет карточку.
  • spin крутит объект по оси H (heading) с угловой скоростью 60°/с, умноженной на getDt().

Текстура на карточке

Загрузка PNG через loader.loadTexture. Путь лучше давать относительно проекта — Panda3D ожидает Unix-пути внутри models/ или корня:

#!/usr/bin/env python3
"""Spinning image on a card — like main.py, but with a texture."""

import sys
from pathlib import Path

from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, CardMaker, DirectionalLight, Filename, TransparencyAttrib


def default_image_path():
return Path(__file__).resolve().parent / "images" / "sample.png"


def panda_texture_path(path):
resolved = path.resolve()
project_dir = Path(__file__).resolve().parent

try:
return resolved.relative_to(project_dir).as_posix()
except ValueError:
return Filename.fromOsSpecific(str(resolved)).asUnix()


class App(ShowBase):
def __init__(self, image_path):
ShowBase.__init__(self)

self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -8, 2)
self.camera.lookAt(0, 0, 0)

ambient = AmbientLight("ambient")
ambient.setColor((0.35, 0.35, 0.4, 1))
self.render.setLight(self.render.attachNewNode(ambient))

sun = DirectionalLight("sun")
sun.setColor((0.9, 0.9, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(45, -45, 0)
self.render.setLight(sun_np)

texture = self.loader.loadTexture(panda_texture_path(image_path))
if texture is None:
sys.exit(f"Cannot load image: {image_path}")

width = texture.getXSize()
height = texture.getYSize()
aspect = width / height if height else 1.0
half_h = 2.0
half_w = half_h * aspect

cm = CardMaker("card")
cm.setFrame(-half_w, half_w, -half_h, half_h)
self.card = self.render.attachNewNode(cm.generate())
self.card.setTexture(texture)
self.card.setTransparency(TransparencyAttrib.MAlpha)
self.card.setLightOff()
self.card.setP(-20)

self.taskMgr.add(self.spin, "spin")

def spin(self, task):
self.card.setH(self.card.getH() + 60 * globalClock.getDt())
return task.cont


if __name__ == "__main__":
image = Path(sys.argv[1]) if len(sys.argv) > 1 else default_image_path()
if not image.is_file():
sys.exit(f"Image not found: {image}")

app = App(image)
app.run()

Структура каталога:

my_panda_app/
├── main_texture.py
└── images/
└── sample.png

setTransparency(TransparencyAttrib.MAlpha) нужен для PNG с альфа-каналом. setLightOff() — текстура рисуется «как есть», без затенения по нормалям.


Куб из вершин и нормалей

Готового «куба одной строкой» в ядре нет — меш собирают через Geom API: вершины, нормали, цвет, индексы треугольников.

#!/usr/bin/env python3
"""Simple cube example — lit 3D cube with optional spin."""

from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)


def make_cube(size=2.0):
half = size * 0.5
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("cube", fmt, Geom.UHStatic)

vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")

rgba = (0.35, 0.55, 0.95, 1)

def quad(n, corners):
for corner in corners:
vertex.addData3(*corner)
normal.addData3(n)
color.addData4(*rgba)

quad(Vec3(0, 0, 1), [(-half, -half, half), (half, -half, half), (half, half, half), (-half, half, half)])
quad(Vec3(0, 0, -1), [(half, -half, -half), (-half, -half, -half), (-half, half, -half), (half, half, -half)])
quad(Vec3(0, 1, 0), [(-half, half, half), (half, half, half), (half, half, -half), (-half, half, -half)])
quad(Vec3(0, -1, 0), [(-half, -half, -half), (half, -half, -half), (half, -half, half), (-half, -half, half)])
quad(Vec3(1, 0, 0), [(half, -half, half), (half, -half, -half), (half, half, -half), (half, half, half)])
quad(Vec3(-1, 0, 0), [(-half, -half, -half), (-half, -half, half), (-half, half, half), (-half, half, -half)])

tris = GeomTriangles(Geom.UHStatic)
for face in range(6):
base = face * 4
tris.addVertices(base, base + 1, base + 2)
tris.addVertices(base, base + 2, base + 3)
tris.closePrimitive()

geom = Geom(vdata)
geom.addPrimitive(tris)

node = GeomNode("cube")
node.addGeom(geom)
return node


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

self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -8, 3)
self.camera.lookAt(0, 0, 0)

ambient = AmbientLight("ambient")
ambient.setColor((0.35, 0.35, 0.4, 1))
self.render.setLight(self.render.attachNewNode(ambient))

sun = DirectionalLight("sun")
sun.setColor((0.9, 0.9, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(45, -45, 0)
self.render.setLight(sun_np)

self.cube = self.render.attachNewNode(make_cube())
self.cube.setP(15)

self.taskMgr.add(self.spin, "spin")

def spin(self, task):
self.cube.setH(self.cube.getH() + 45 * globalClock.getDt())
return task.cont


if __name__ == "__main__":
app = App()
app.run()

Формат getV3n3c4() — vertex + normal + color на каждую вершину; directional light использует нормали для расчёта яркости граней. В продакшене кубы обычно загружают из .gltf / .bam, а не пишут руками.


Загрузка моделей и ввод

Типичный следующий шаг после примитивов:

model = self.loader.loadModel("models/box.bam")
model.reparentTo(self.render)
model.setScale(0.5)
model.setPos(0, 5, 0)

Обработка клавиш через accept:

self.accept("escape", sys.exit)
self.accept("w", self.move_forward)

Для персонажей с анимацией — direct.actor.Actor и файлы .egg/.bam с joint-анимацией.


Сравнение с Pygame и другими 3D-вариантами

PygamePanda3DUrsina
Размерность2D3D3D (над Panda3D)
Порог входаНизкийСреднийНизкий
КонтрольПолный (SDL)Полный движокМеньше деталей
ДокументацияХорошаяОбъёмная, местами стараяКомпактная

Pygame остаётся лучшим стартом для игрового цикла и 2D; готовые скрипты с разбором — мини-игры в Lab. Переход на Panda3D логичен, когда нужны перспектива, освещение и модели в 3D.


Что попробовать дальше

  1. Добавить управление камерой (enableMouse() или WASD).
  2. Загрузить модель из Blender (glTF → через конвертер или panda3d-gltf).
  3. Подключить физику panda3d.bullet для падения объектов.
  4. Наложить 2D-HUD через direct.gui.OnscreenText.
  5. Сравнить с мини-играми Pygame и Практикумом игр — те же идеи цикла и состояния, другой рендер.
Частые ошибки
  • Чёрный экран — нет света или камера смотрит мимо объекта.
  • Текстура не грузится — путь не относительный или файл вне рабочей директории.
  • Анимация «дёргается» — забыли умножить скорость на globalClock.getDt().
  • Окно «зависло» — долгий код в задаче без возврата task.cont.

Полезные ссылки


В подборках

Статья входит в тематические маршруты из меню Подборки и блока "С чего начать?" на главной. Соседние шаги того же маршрута:

Разработка видеоигрРазработка игр на Python, Pygame — мини-игры на Python, Minecraft — команды и datapack, Unity C# — скрипты для новичков, Практикум разработки игр — о разделе, Разработка игр — о разделе, Языки программирования игр, Разработка игр с использованием C++.


См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").