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

Примеры фигур 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()

Связанные материалы


См. также

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