Примеры фигур Panda3D на Python
Основы 3D на Python
Перед запуском примеров изучите главу Трёхмерная графика и Panda3D — там архитектура ShowBase, освещение, taskMgr и разбор первых сцен.
Примеры ниже — самостоятельные скрипты: сохраните код в файл .py и запустите python имя_файла.py. Нужен установленный пакет panda3d (pip install panda3d). Интерактивного симулятора в браузере нет — окно открывается локально на вашем компьютере.
Для 2D в браузере (без Python) — Примеры фигур на Processing/p5.js. Для 2D на Python с черепашкой — Turtle. Для интерактивных 2D-игр — Pygame — мини-игры на Python.
Обязательный каркас
Любое приложение Panda3D строится на ShowBase и завершается вызовом run():
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
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)
if __name__ == "__main__":
app = App()
app.run()
Общая функция освещения, которую переиспользуют многие примеры ниже:
from panda3d.core import AmbientLight, DirectionalLight
def add_default_lights(render):
ambient = AmbientLight("ambient")
ambient.setColor((0.35, 0.35, 0.4, 1))
render.setLight(render.attachNewNode(ambient))
sun = DirectionalLight("sun")
sun.setColor((0.9, 0.9, 0.85, 1))
sun_np = render.attachNewNode(sun)
sun_np.setHpr(45, -45, 0)
render.setLight(sun_np)
return sun_np
Стартовые фигуры
Цветная карточка
Плоский прямоугольник через CardMaker, медленное вращение:
#!/usr/bin/env python3
"""Minimal Panda3D application — colored card."""
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()
Карточка с текстурой
PNG рядом со скриптом в images/sample.png или путь аргументом:
#!/usr/bin/env python3
"""Spinning image on a card."""
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()
Освещённый куб
Меш из вершин, нормалей и треугольников:
#!/usr/bin/env python3
"""Lit 3D cube from Geom API."""
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()
Примеры фигур
1. Каркас и линии
1.1. Каркас куба (LineSegs)
Объёмная фигура без заливки — только рёбра:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs
def wireframe_cube(segs, size=2.0):
half = size * 0.5
corners = [
(-half, -half, -half), (half, -half, -half), (half, half, -half), (-half, half, -half),
(-half, -half, half), (half, -half, half), (half, half, half), (-half, half, half),
]
edges = [
(0, 1), (1, 2), (2, 3), (3, 0),
(4, 5), (5, 6), (6, 7), (7, 4),
(0, 4), (1, 5), (2, 6), (3, 7),
]
segs.setColor(0.4, 0.85, 1.0, 1)
segs.setThickness(2)
for a, b in edges:
segs.moveTo(*corners[a])
segs.drawTo(*corners[b])
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.05, 0.06, 0.1, 1)
self.disableMouse()
self.camera.setPos(0, -10, 4)
self.camera.lookAt(0, 0, 0)
ls = LineSegs("wire_cube")
wireframe_cube(ls, 2.5)
self.wire = self.render.attachNewNode(ls.create())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.wire.setH(self.wire.getH() + 30 * globalClock.getDt())
self.wire.setP(self.wire.getP() + 15 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
1.2. Правильный многоугольник в плоскости XY
Контур из n вершин по окружности:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs
def regular_polygon_lines(segs, n, radius, z=0.0):
segs.setThickness(3)
for i in range(n + 1):
angle = 2 * math.pi * i / n
x = radius * math.cos(angle)
y = radius * math.sin(angle)
if i == 0:
segs.moveTo(x, y, z)
else:
segs.drawTo(x, y, z)
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -12, 0)
self.camera.lookAt(0, 0, 0)
for sides, radius, color in [(3, 2, (1, 0.3, 0.3, 1)), (6, 3, (0.3, 1, 0.5, 1)), (8, 4, (0.4, 0.6, 1, 1))]:
ls = LineSegs(f"poly_{sides}")
ls.setColor(*color)
regular_polygon_lines(ls, sides, radius, z=0)
np = self.render.attachNewNode(ls.create())
np.setY(radius - 4)
if __name__ == "__main__":
App().run()
2. Объёмные примитивы
2.1. Пирамида с квадратным основанием
Пять вершин, восемь треугольных граней:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_pyramid(base=2.0, height=2.5):
half = base * 0.5
apex = (0, 0, height)
base_corners = [(-half, -half, 0), (half, -half, 0), (half, half, 0), (-half, half, 0)]
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("pyramid", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
def add_tri(p1, p2, p3, rgba):
edge1 = Vec3(p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
edge2 = Vec3(p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2])
n = edge1.cross(edge2)
n.normalize()
for p in (p1, p2, p3):
vertex.addData3(*p)
normal.addData3(n)
color.addData4(*rgba)
side_color = (0.9, 0.55, 0.2, 1)
base_color = (0.25, 0.45, 0.85, 1)
for i in range(4):
p1 = base_corners[i]
p2 = base_corners[(i + 1) % 4]
add_tri(p1, p2, apex, side_color)
add_tri(base_corners[0], base_corners[2], base_corners[1], base_color)
add_tri(base_corners[0], base_corners[3], base_corners[2], base_color)
tris = GeomTriangles(Geom.UHStatic)
for i in range(6):
base = i * 3
tris.addVertices(base, base + 1, base + 2)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("pyramid")
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, -9, 3)
self.camera.lookAt(0, 1, 1)
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.95, 0.9, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(50, -50, 0)
self.render.setLight(sun_np)
self.pyramid = self.render.attachNewNode(make_pyramid())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.pyramid.setH(self.pyramid.getH() + 40 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
2.2. Сфера из стандартных моделей Panda3D
В комплекте с движком идут встроенные .egg-модели:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, 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.4, 0.4, 0.45, 1))
self.render.setLight(self.render.attachNewNode(ambient))
sun = DirectionalLight("sun")
sun.setColor((1, 0.95, 0.9, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(45, -45, 0)
self.render.setLight(sun_np)
self.sphere = self.loader.loadModel("models/misc/sphere")
self.sphere.reparentTo(self.render)
self.sphere.setScale(2)
self.sphere.setColor(0.3, 0.75, 0.55, 1)
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.sphere.setH(self.sphere.getH() + 35 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
Если модель не найдена, проверьте print(self.loader.getModelPath()) и установку panda3d из PyPI.
2.3. Призма — многоугольное основание
Обобщение куба: основание с n сторонами и высота по оси Z:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_prism(n, radius, height, rgba=(0.5, 0.35, 0.9, 1)):
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("prism", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
bottom = []
top = []
for i in range(n):
angle = 2 * math.pi * i / n
x = radius * math.cos(angle)
y = radius * math.sin(angle)
bottom.append((x, y, 0))
top.append((x, y, height))
def add_quad(p0, p1, p2, p3):
edge1 = Vec3(p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2])
edge2 = Vec3(p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2])
n = edge1.cross(edge2)
n.normalize()
for p in (p0, p1, p2, p3):
vertex.addData3(*p)
normal.addData3(n)
color.addData4(*rgba)
for i in range(n):
j = (i + 1) % n
add_quad(bottom[i], bottom[j], top[j], top[i])
tris = GeomTriangles(Geom.UHStatic)
tri_count = n * 2
for i in range(tri_count):
base = i * 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("prism")
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, -10, 3)
self.camera.lookAt(0, 0, 1)
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.prism = self.render.attachNewNode(make_prism(6, 1.8, 2.5))
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.prism.setH(self.prism.getH() + 50 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
3. Композиции и узоры
3.1. Сетка цветных карточек
Плоское «мозаичное» поле из CardMaker:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import CardMaker
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.06, 0.07, 0.1, 1)
self.disableMouse()
self.camera.setPos(0, -14, 10)
self.camera.lookAt(0, 0, 0)
size = 1.4
gap = 0.15
step = size + gap
colors = [
(0.9, 0.3, 0.3, 1), (0.3, 0.85, 0.45, 1), (0.35, 0.55, 0.95, 1),
(0.95, 0.75, 0.2, 1), (0.75, 0.4, 0.9, 1), (0.3, 0.85, 0.85, 1),
]
idx = 0
for row in range(3):
for col in range(4):
cm = CardMaker(f"tile_{row}_{col}")
cm.setFrame(-size / 2, size / 2, -size / 2, size / 2)
tile = self.render.attachNewNode(cm.generate())
tile.setPos((col - 1.5) * step, (row - 1) * step, 0)
tile.setColor(*colors[idx % len(colors)])
tile.setLightOff()
idx += 1
self.render.setP(-55)
if __name__ == "__main__":
App().run()
3.2. Кольцо из карточек
Карточки размещены по окружности и повёрнуты к центру:
#!/usr/bin/env python3
import math
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.05, 0.07, 0.12, 1)
self.disableMouse()
self.camera.setPos(0, -12, 3)
self.camera.lookAt(0, 0, 0)
ambient = AmbientLight("ambient")
ambient.setColor((0.45, 0.45, 0.5, 1))
self.render.setLight(self.render.attachNewNode(ambient))
count = 12
radius = 4.0
self.cards = []
for i in range(count):
angle = 360.0 * i / count
rad = math.radians(angle)
cm = CardMaker(f"petal_{i}")
cm.setFrame(-0.6, 0.6, -1.2, 1.2)
card = self.render.attachNewNode(cm.generate())
card.setPos(radius * math.sin(rad), radius * math.cos(rad), 0)
card.setH(angle + 180)
card.setColor(0.2 + i / count, 0.5, 0.9 - i / count, 1)
self.cards.append(card)
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
for i, card in enumerate(self.cards):
card.setR(10 * math.sin(globalClock.getFrameTime() + i))
return task.cont
if __name__ == "__main__":
App().run()
3.3. Сцена из нескольких объектов
Куб, сфера и наклонённая карточка в одном кадре:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, CardMaker, DirectionalLight, Vec3
from panda3d.core import Geom, GeomNode, GeomTriangles, GeomVertexData, GeomVertexFormat, GeomVertexWriter
def make_cube(size=1.5, rgba=(0.35, 0.55, 0.95, 1)):
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")
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, -12, 4)
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(1.8))
self.cube.setPos(-2.5, 0, 0.9)
self.sphere = self.loader.loadModel("models/misc/sphere")
self.sphere.reparentTo(self.render)
self.sphere.setScale(1.6)
self.sphere.setPos(2.5, 0, 0.9)
self.sphere.setColor(0.3, 0.8, 0.5, 1)
cm = CardMaker("floor")
cm.setFrame(-5, 5, -5, 5)
floor = self.render.attachNewNode(cm.generate())
floor.setP(-90)
floor.setColor(0.15, 0.17, 0.22, 1)
floor.setLightOff()
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
dt = globalClock.getDt()
self.cube.setH(self.cube.getH() + 40 * dt)
self.sphere.setH(self.sphere.getH() - 30 * dt)
return task.cont
if __name__ == "__main__":
App().run()
4. Переиспользуемые функции
4.1. Плоский n-угольник с заливкой
Параметрическая «розетка» из CardMaker не подходит — нужен Geom:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Geom, GeomNode, GeomTriangles, GeomVertexData, GeomVertexFormat, GeomVertexWriter, Vec3
def make_filled_polygon(n, radius, rgba=(0.9, 0.4, 0.2, 1)):
if n < 3:
raise ValueError("n >= 3")
fmt = GeomVertexFormat.getV3c4()
vdata = GeomVertexData("poly", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
color = GeomVertexWriter(vdata, "color")
vertex.addData3(0, 0, 0)
color.addData4(*rgba)
for i in range(n):
angle = 2 * math.pi * i / n
vertex.addData3(radius * math.cos(angle), radius * math.sin(angle), 0)
color.addData4(*rgba)
tris = GeomTriangles(Geom.UHStatic)
for i in range(n):
tris.addVertices(0, i + 1, (i % n) + 2 if i < n - 1 else 1)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("filled_poly")
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, -10, 0)
self.camera.lookAt(0, 0, 0)
for i, sides in enumerate((5, 7, 9)):
poly = self.render.attachNewNode(make_filled_polygon(sides, 2.0 - i * 0.3))
poly.setPos(0, i * 3 - 3, 0)
poly.setLightOff()
poly.setColor(0.3 + i * 0.2, 0.5, 0.9 - i * 0.15, 1)
if __name__ == "__main__":
App().run()
4.2. Шаблон эксперимента
Базовый класс с освещением и вращением — копируйте и дополняйте:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, DirectionalLight
class ExperimentApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -10, 3)
self.camera.lookAt(0, 0, 0)
self._setup_lights()
def _setup_lights(self):
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)
def add_spinner(self, nodepath, speed=45.0):
def spin(task, np=nodepath, rate=speed):
np.setH(np.getH() + rate * globalClock.getDt())
return task.cont
self.taskMgr.add(spin, f"spin_{nodepath.getName()}")
# class MyScene(ExperimentApp):
# def __init__(self):
# super().__init__()
# ...
#
# if __name__ == "__main__":
# MyScene().run()
5. Дополнительные примеры
5.1. Цилиндр
Боковая поверхность и две крышки из треугольников:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_cylinder(radius=1.0, height=2.5, segments=24, rgba=(0.45, 0.7, 0.35, 1)):
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("cylinder", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
def add_vertex(x, y, z, nx, ny, nz):
vertex.addData3(x, y, z)
normal.addData3(nx, ny, nz)
color.addData4(*rgba)
half = height * 0.5
ring_bottom = []
ring_top = []
for i in range(segments):
angle = 2 * math.pi * i / segments
cx = math.cos(angle)
sy = math.sin(angle)
ring_bottom.append((radius * cx, radius * sy, -half))
ring_top.append((radius * cx, radius * sy, half))
tris = GeomTriangles(Geom.UHStatic)
idx = 0
for i in range(segments):
j = (i + 1) % segments
b0, b1 = ring_bottom[i], ring_bottom[j]
t0, t1 = ring_top[i], ring_top[j]
for p1, p2, p3 in ((b0, b1, t1), (b0, t1, t0)):
edge1 = Vec3(p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
edge2 = Vec3(p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2])
n = edge1.cross(edge2)
n.normalize()
for p in (p1, p2, p3):
add_vertex(*p, n.x, n.y, n.z)
tris.addVertices(idx, idx + 1, idx + 2)
idx += 3
for z, ring, nz in ((-half, ring_bottom, -1), (half, ring_top, 1)):
center_idx = idx
add_vertex(0, 0, z, 0, 0, nz)
idx += 1
start = idx
for x, y, _ in ring:
add_vertex(x, y, z, 0, 0, nz)
idx += 1
for i in range(segments):
j = (i + 1) % segments
if nz < 0:
tris.addVertices(center_idx, start + j, start + i)
else:
tris.addVertices(center_idx, start + i, start + j)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("cylinder")
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, -9, 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)
self.cyl = self.render.attachNewNode(make_cylinder())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.cyl.setH(self.cyl.getH() + 50 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.2. Процедурная UV-сфера
Без загрузки models/misc/sphere — меш из широт и долгот:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_uv_sphere(radius=1.5, stacks=16, slices=24, rgba=(0.85, 0.4, 0.35, 1)):
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("uv_sphere", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
def add_point(theta, phi):
x = radius * math.sin(phi) * math.cos(theta)
y = radius * math.sin(phi) * math.sin(theta)
z = radius * math.cos(phi)
n = Vec3(x, y, z)
n.normalize()
vertex.addData3(x, y, z)
normal.addData3(n)
color.addData4(*rgba)
tris = GeomTriangles(Geom.UHStatic)
for i in range(stacks):
phi0 = math.pi * i / stacks
phi1 = math.pi * (i + 1) / stacks
for j in range(slices):
th0 = 2 * math.pi * j / slices
th1 = 2 * math.pi * (j + 1) / slices
base = vertex.getWriteRow()
for theta, phi in ((th0, phi0), (th1, phi0), (th1, phi1), (th0, phi1)):
add_point(theta, phi)
tris.addVertices(base, base + 1, base + 2)
tris.addVertices(base, base + 2, base + 3)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("uv_sphere")
node.addGeom(geom)
return node
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.06, 0.08, 0.12, 1)
self.disableMouse()
self.camera.setPos(0, -7, 1)
self.camera.lookAt(0, 0, 0)
ambient = AmbientLight("ambient")
ambient.setColor((0.3, 0.3, 0.35, 1))
self.render.setLight(self.render.attachNewNode(ambient))
sun = DirectionalLight("sun")
sun.setColor((1, 0.92, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(60, -40, 0)
self.render.setLight(sun_np)
self.ball = self.render.attachNewNode(make_uv_sphere())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.ball.setH(self.ball.getH() + 25 * globalClock.getDt())
self.ball.setP(self.ball.getP() + 10 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.3. Правильный тетраэдр
Четыре равносторонние грани:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_tetrahedron(size=2.5, rgba=(0.9, 0.75, 0.2, 1)):
s = size
verts = [
(s, s, s), (s, -s, -s), (-s, s, -s), (-s, -s, s),
]
faces = [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("tetra", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
tris = GeomTriangles(Geom.UHStatic)
idx = 0
for face in faces:
p1, p2, p3 = (verts[i] for i in face)
e1 = Vec3(p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
e2 = Vec3(p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2])
n = e1.cross(e2)
n.normalize()
for p in (p1, p2, p3):
vertex.addData3(*p)
normal.addData3(n)
color.addData4(*rgba)
tris.addVertices(idx, idx + 1, idx + 2)
idx += 3
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("tetrahedron")
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, -10, 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.95, 0.9, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(40, -50, 0)
self.render.setLight(sun_np)
self.tetra = self.render.attachNewNode(make_tetrahedron())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
t = globalClock.getFrameTime()
self.tetra.setH(40 * t)
self.tetra.setP(25 * t)
return task.cont
if __name__ == "__main__":
App().run()
5.4. Спираль из карточек
Винтовая лента — карточки поднимаются по оси Z:
#!/usr/bin/env python3
import math
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.05, 0.07, 0.11, 1)
self.disableMouse()
self.camera.setPos(0, -14, 4)
self.camera.lookAt(0, 0, 2)
ambient = AmbientLight("ambient")
ambient.setColor((0.45, 0.45, 0.5, 1))
self.render.setLight(self.render.attachNewNode(ambient))
count = 24
self.pieces = []
for i in range(count):
angle = math.radians(i * 15)
cm = CardMaker(f"helix_{i}")
cm.setFrame(-0.5, 0.5, -0.8, 0.8)
card = self.render.attachNewNode(cm.generate())
r = 3.0
card.setPos(r * math.cos(angle), r * math.sin(angle), i * 0.25)
card.setH(math.degrees(angle) + 90)
card.setColor(0.2 + i / count, 0.5, 1.0 - i / count, 1)
card.setLightOff()
self.pieces.append(card)
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
dt = globalClock.getDt()
for card in self.pieces:
card.setH(card.getH() + 30 * dt)
return task.cont
if __name__ == "__main__":
App().run()
5.5. Сетка координат на полу
Плоская разметка через LineSegs:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, CardMaker, DirectionalLight, LineSegs
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -15, 8)
self.camera.lookAt(0, 0, 0)
ls = LineSegs("grid")
ls.setColor(0.35, 0.45, 0.6, 0.8)
ls.setThickness(1)
step = 1.0
extent = 8
for i in range(-extent, extent + 1):
x = i * step
ls.moveTo(x, -extent * step, 0)
ls.drawTo(x, extent * step, 0)
ls.moveTo(-extent * step, x, 0)
ls.drawTo(extent * step, x, 0)
self.render.attachNewNode(ls.create())
cm = CardMaker("floor")
cm.setFrame(-extent, extent, -extent, extent)
floor = self.render.attachNewNode(cm.generate())
floor.setP(-90)
floor.setColor(0.12, 0.14, 0.18, 1)
floor.setLightOff()
ambient = AmbientLight("ambient")
ambient.setColor((0.4, 0.4, 0.45, 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, -60, 0)
self.render.setLight(sun_np)
self.cube = self.loader.loadModel("models/misc/sphere")
self.cube.reparentTo(self.render)
self.cube.setScale(1.2)
self.cube.setPos(0, 0, 1.2)
self.cube.setColor(0.4, 0.75, 0.95, 1)
if __name__ == "__main__":
App().run()
5.6. Точечный свет
PointLight освещает сцену от конкретной точки — видны блики на гранях:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, PointLight
from panda3d.core import 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.6, 0.6, 0.65, 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.02, 0.02, 0.05, 1)
self.disableMouse()
self.camera.setPos(0, -9, 2)
self.camera.lookAt(0, 0, 0)
ambient = AmbientLight("ambient")
ambient.setColor((0.08, 0.08, 0.1, 1))
self.render.setLight(self.render.attachNewNode(ambient))
pl = PointLight("point")
pl.setColor((1, 0.85, 0.6, 1))
self.light_np = self.render.attachNewNode(pl)
self.light_np.setPos(2, -1, 3)
self.render.setLight(self.light_np)
self.cube = self.render.attachNewNode(make_cube())
self.taskMgr.add(self.orbit_light, "orbit_light")
def orbit_light(self, task):
t = globalClock.getFrameTime()
self.light_np.setPos(3 * math.cos(t), 3 * math.sin(t), 2.5)
return task.cont
if __name__ == "__main__":
App().run()
5.7. Орбитальная камера
Камера облетает объект — без enableMouse():
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, DirectionalLight
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
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.model = self.loader.loadModel("models/misc/sphere")
self.model.reparentTo(self.render)
self.model.setScale(2)
self.model.setColor(0.35, 0.6, 0.95, 1)
self.orbit_radius = 10.0
self.orbit_height = 3.0
self.orbit_speed = 25.0
self.taskMgr.add(self.orbit_camera, "orbit_camera")
def orbit_camera(self, task):
angle = math.radians(self.orbit_speed * globalClock.getFrameTime())
self.camera.setPos(
self.orbit_radius * math.sin(angle),
-self.orbit_radius * math.cos(angle),
self.orbit_height,
)
self.camera.lookAt(0, 0, 0)
return task.cont
if __name__ == "__main__":
App().run()
5.8. Подпись в сцене (TextNode)
Трёхмерный текст, повёрнутый к камере:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, DirectionalLight, TextNode
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.08, 0.1, 0.14, 1)
self.disableMouse()
self.camera.setPos(0, -10, 2)
self.camera.lookAt(0, 0, 0)
ambient = AmbientLight("ambient")
ambient.setColor((0.4, 0.4, 0.45, 1))
self.render.setLight(self.render.attachNewNode(ambient))
self.sphere = self.loader.loadModel("models/misc/sphere")
self.sphere.reparentTo(self.render)
self.sphere.setScale(1.8)
self.sphere.setColor(0.3, 0.7, 0.5, 1)
tn = TextNode("label")
tn.setText("Panda3D")
tn.setAlign(TextNode.ACenter)
tn.setTextColor(1, 0.95, 0.8, 1)
label = self.render.attachNewNode(tn.generate())
label.setScale(0.8)
label.setPos(0, 0, 2.5)
label.setBillboardPointEye()
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.sphere.setH(self.sphere.getH() + 30 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.9. Башня из кубов
Классический «стек» — масштаб и цвет меняются по высоте:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import AmbientLight, DirectionalLight
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.07, 0.09, 0.13, 1)
self.disableMouse()
self.camera.setPos(0, -12, 5)
self.camera.lookAt(0, 0, 2)
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(50, -45, 0)
self.render.setLight(sun_np)
template = self.loader.loadModel("models/box")
levels = 8
for i in range(levels):
block = template.copyTo(self.render)
s = 1.4 - i * 0.08
block.setScale(s, s, 0.6)
block.setPos(0, 0, i * 0.65 + 0.3)
t = i / max(levels - 1, 1)
block.setColor(0.3 + t * 0.5, 0.5 - t * 0.2, 0.9 - t * 0.4, 1)
self.taskMgr.add(self.sway, "sway")
def sway(self, task):
self.render.setH(5 * math.sin(globalClock.getFrameTime() * 0.5))
return task.cont
if __name__ == "__main__":
App().run()
5.10. Каркасная сфера
Параллели и меридианы из линий:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs
def wireframe_sphere(segs, radius=2.0, meridians=12, parallels=8):
segs.setColor(0.5, 0.85, 1.0, 1)
segs.setThickness(1)
for m in range(meridians):
theta = 2 * math.pi * m / meridians
for p in range(parallels + 1):
phi = math.pi * p / parallels
x = radius * math.sin(phi) * math.cos(theta)
y = radius * math.sin(phi) * math.sin(theta)
z = radius * math.cos(phi)
if p == 0:
segs.moveTo(x, y, z)
else:
segs.drawTo(x, y, z)
for p in range(1, parallels):
phi = math.pi * p / parallels
r = radius * math.sin(phi)
z = radius * math.cos(phi)
for m in range(meridians + 1):
theta = 2 * math.pi * m / meridians
x = r * math.cos(theta)
y = r * math.sin(theta)
if m == 0:
segs.moveTo(x, y, z)
else:
segs.drawTo(x, y, z)
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.04, 0.05, 0.09, 1)
self.disableMouse()
self.camera.setPos(0, -9, 2)
self.camera.lookAt(0, 0, 0)
ls = LineSegs("wire_sphere")
wireframe_sphere(ls)
self.wire = self.render.attachNewNode(ls.create())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.wire.setH(self.wire.getH() + 20 * globalClock.getDt())
self.wire.setP(self.wire.getP() + 8 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.11. Тор (бублик)
Кольцо из сегментов — упрощённая геометрия:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def make_torus(major_r=2.0, minor_r=0.5, major_seg=32, minor_seg=16, rgba=(0.55, 0.35, 0.85, 1)):
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("torus", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
def add_vertex(x, y, z, nx, ny, nz):
vertex.addData3(x, y, z)
normal.addData3(nx, ny, nz)
color.addData4(*rgba)
tris = GeomTriangles(Geom.UHStatic)
for i in range(major_seg):
theta0 = 2 * math.pi * i / major_seg
theta1 = 2 * math.pi * (i + 1) / major_seg
for j in range(minor_seg):
phi0 = 2 * math.pi * j / minor_seg
phi1 = 2 * math.pi * (j + 1) / minor_seg
def sample(theta, phi):
cx, sx = math.cos(theta), math.sin(theta)
cy, sy = math.cos(phi), math.sin(phi)
x = (major_r + minor_r * cy) * cx
y = (major_r + minor_r * cy) * sx
z = minor_r * sy
nx = cy * cx
ny = cy * sx
nz = sy
return (x, y, z), (nx, ny, nz)
corners = [sample(theta0, phi0), sample(theta1, phi0), sample(theta1, phi1), sample(theta0, phi1)]
base = vertex.getWriteRow()
for (x, y, z), (nx, ny, nz) in corners:
add_vertex(x, y, z, nx, ny, nz)
tris.addVertices(base, base + 1, base + 2)
tris.addVertices(base, base + 2, base + 3)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("torus")
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, -10, 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.torus = self.render.attachNewNode(make_torus())
self.torus.setP(70)
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.torus.setH(self.torus.getH() + 35 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.12. Звезда — extruded 2D-контур
Плоская звезда с толщиной, собранная из треугольников:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
def star_points(outer_r, inner_r, spikes=5):
pts = []
for i in range(spikes * 2):
angle = math.pi / 2 + math.pi * i / spikes
r = outer_r if i % 2 == 0 else inner_r
pts.append((r * math.cos(angle), r * math.sin(angle)))
return pts
def make_star_mesh(outer_r=2.0, inner_r=0.9, thickness=0.3, spikes=5, rgba=(0.95, 0.75, 0.15, 1)):
outline = star_points(outer_r, inner_r, spikes)
half = thickness * 0.5
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("star", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
def add_tri(p1, p2, p3, n):
for p in (p1, p2, p3):
vertex.addData3(*p)
normal.addData3(n)
color.addData4(*rgba)
tris = GeomTriangles(Geom.UHStatic)
idx = 0
for z, nz in ((-half, -1), (half, 1)):
for i in range(len(outline)):
j = (i + 1) % len(outline)
x0, y0 = outline[i]
x1, y1 = outline[j]
add_tri((0, 0, z), (x0, y0, z), (x1, y1, z), (0, 0, nz))
tris.addVertices(idx, idx + 1, idx + 2)
idx += 3
for i in range(len(outline)):
j = (i + 1) % len(outline)
x0, y0 = outline[i]
x1, y1 = outline[j]
p1 = (x0, y0, -half)
p2 = (x1, y1, -half)
p3 = (x1, y1, half)
p4 = (x0, y0, half)
edge = Vec3(x1 - x0, y1 - y0, 0)
n = edge.cross(Vec3(0, 0, 1))
n.normalize()
for a, b, c in ((p1, p2, p3), (p1, p3, p4)):
add_tri(a, b, c, (n.x, n.y, n.z))
tris.addVertices(idx, idx + 1, idx + 2)
idx += 3
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("star")
node.addGeom(geom)
return node
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.06, 0.08, 0.12, 1)
self.disableMouse()
self.camera.setPos(0, -9, 1)
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((1, 0.95, 0.85, 1))
sun_np = self.render.attachNewNode(sun)
sun_np.setHpr(50, -40, 0)
self.render.setLight(sun_np)
self.star = self.render.attachNewNode(make_star_mesh())
self.star.setP(-20)
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.star.setH(self.star.getH() + 45 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.13. Двойная спираль (DNA-стиль)
Две линии, закрученные вокруг оси Y:
#!/usr/bin/env python3
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0.05, 0.06, 0.1, 1)
self.disableMouse()
self.camera.setPos(0, -12, 2)
self.camera.lookAt(0, 0, 2)
turns = 4
steps = 120
radius = 1.5
height = 5.0
for phase, color in ((0, (0.4, 0.85, 1, 1)), (math.pi, (1, 0.5, 0.4, 1))):
ls = LineSegs(f"helix_{phase}")
ls.setColor(*color)
ls.setThickness(3)
for i in range(steps + 1):
t = i / steps
angle = turns * 2 * math.pi * t + phase
x = radius * math.cos(angle)
y = radius * math.sin(angle)
z = height * t
if i == 0:
ls.moveTo(x, y, z)
else:
ls.drawTo(x, y, z)
self.render.attachNewNode(ls.create())
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.render.setH(self.render.getH() + 15 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
5.14. Куб с разным цветом граней
Шесть оттенков — по одному на грань:
#!/usr/bin/env python3
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
AmbientLight,
DirectionalLight,
Geom,
GeomNode,
GeomTriangles,
GeomVertexData,
GeomVertexFormat,
GeomVertexWriter,
Vec3,
)
FACE_COLORS = [
(0.9, 0.2, 0.2, 1),
(0.2, 0.85, 0.3, 1),
(0.25, 0.45, 0.95, 1),
(0.95, 0.85, 0.2, 1),
(0.85, 0.35, 0.9, 1),
(0.3, 0.85, 0.85, 1),
]
def make_rubik_cube(size=2.0):
half = size * 0.5
faces = [
(Vec3(0, 0, 1), [(-half, -half, half), (half, -half, half), (half, half, half), (-half, half, half)]),
(Vec3(0, 0, -1), [(half, -half, -half), (-half, -half, -half), (-half, half, -half), (half, half, -half)]),
(Vec3(0, 1, 0), [(-half, half, half), (half, half, half), (half, half, -half), (-half, half, -half)]),
(Vec3(0, -1, 0), [(-half, -half, -half), (half, -half, -half), (half, -half, half), (-half, -half, half)]),
(Vec3(1, 0, 0), [(half, -half, half), (half, -half, -half), (half, half, -half), (half, half, half)]),
(Vec3(-1, 0, 0), [(-half, -half, -half), (-half, -half, half), (-half, half, half), (-half, half, -half)]),
]
fmt = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData("rubik", fmt, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, "vertex")
normal = GeomVertexWriter(vdata, "normal")
color = GeomVertexWriter(vdata, "color")
tris = GeomTriangles(Geom.UHStatic)
for fi, (n, corners) in enumerate(faces):
rgba = FACE_COLORS[fi]
base = vertex.getWriteRow()
for corner in corners:
vertex.addData3(*corner)
normal.addData3(n)
color.addData4(*rgba)
tris.addVertices(base, base + 1, base + 2)
tris.addVertices(base, base + 2, base + 3)
tris.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(tris)
node = GeomNode("rubik_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, 2)
self.camera.lookAt(0, 0, 0)
ambient = AmbientLight("ambient")
ambient.setColor((0.45, 0.45, 0.5, 1))
self.render.setLight(self.render.attachNewNode(ambient))
self.cube = self.render.attachNewNode(make_rubik_cube())
self.cube.setLightOff()
self.taskMgr.add(self.spin, "spin")
def spin(self, task):
self.cube.setH(self.cube.getH() + 40 * globalClock.getDt())
self.cube.setP(self.cube.getP() + 20 * globalClock.getDt())
return task.cont
if __name__ == "__main__":
App().run()
Связанные материалы
- Трёхмерная графика и Panda3D — теория, архитектура, ограничения
- Разработка игр на Python — Pygame и 2D
- Примеры фигур Turtle — 2D-аналог в turtle
- Примеры фигур на Processing/p5.js — 2D в браузере
- Tkinter — окна и виджеты — формы и кнопки без 3D-сцены
- if name == "main" — точка входа в скриптах
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Практическая карта типовых IT-задач: термины, пошаговое внедрение, проверка качества и типичные ошибки. Простой консольный чат на C# — учебное приложение с сокетами: TCP между клиентом и сервером, многопоточность и обмен сообщениями в консоли. Примеры вёрстки на HTML и CSS с разбором: центрирование, Flexbox, Grid, формы, шапка, подвал и адаптив для учебы и портфолио. Перед началом работы обязательно изучите главу Turtle . Готовые docker-compose.yml с разбором каждой строки — nginx, PostgreSQL, Redis, WordPress, MongoDB. Примеры для школьников и студентов: postgres example, поднять базу локально, app + db. Примеры nginx.conf для статики, reverse proxy, React/Vue SPA, PHP, SSL и балансировки — построчный разбор директив, проверка curl и типичные ошибки для лабораторных и VPS. dockerfile example — 10 готовых Dockerfile с построчным разбором: node, python, golang, react nginx, spring boot, php, dotnet. Для студентов, лабораторных и docker build с нуля. PromQL example — готовые запросы Prometheus и Grafana с построчным разбором: up, rate, node_exporter cpu, memory, disk, http_requests_total, histogram_quantile p99, алерты. Для студентов, лабораторных и devops docker compose. Готовые манифесты Kubernetes с разбором каждой строки — Pod, Deployment, Service, ConfigMap, Secret, Ingress. Примеры для Minikube, kind и kubectl apply. Примеры графиков Matplotlib на Python для школьников и студентов — sin, cos, парабола, столбцы, scatter, гистограмма, подграфики; код с подробным разбором. Примеры pandas на Python для школьников и студентов — DataFrame, фильтрация, groupby, очистка, merge, сводные таблицы и экспорт; код с подробным разбором каждой строки. p5.js и Processing — готовый код фигур с подробным разбором каждой строки: квадрат, треугольник, цветок, снежинка, фракталы, анимация; для школьников и студентов.Готовые решения
Простой консольный чат на CSharp
HTML + CSS — готовые макеты
Примеры фигур Turtle на Python
Docker Compose — готовые стеки
Nginx — конфиги под задачу
Dockerfile — 10 типовых образов
Prometheus + Grafana — запросы
Kubernetes YAML — минимальные манифесты
Matplotlib — графики
Pandas — типовые операции
Примеры фигур на Processing/p5.js