If you want to procedurally implement skinning, rigging and animation using Panda3D’s own classes for these purposes (`Actor`

, `PartBundle`

, `AnimBundle`

, etc.), then I’m afraid I can’t really help with that (although I’d like to try out these things for my own project at some point), but perhaps you just want to transform a specific set of vertices explicitly?

To do this, you’ll need to create a matrix that describes the transformation, as well as a `SparseArray`

to define a set of vertex indices.

In the following example, the top half of a procedurally generated cylinder is rotated 30 degrees about an arbitrary axis, with the center of the cylinder as pivot point:

```
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from math import pi, sin, cos
import array
def create_cylinder(parent, radius, height, segments):
segs_c = segments["circular"]
segs_h = segments["height"]
delta_angle = 2 * pi / segs_c
data = array.array("f", [])
tri_data = array.array("I", [])
for i in range(segs_h + 1):
z = height * i / segs_h
for j in range(segs_c):
angle = delta_angle * j
x = radius * cos(angle)
y = radius * sin(angle)
data.extend((x, y, z))
data.extend(Vec3(x, y, 0.).normalized())
for i in range(segs_h):
for j in range(segs_c - 1):
vi1 = (i + 1) * segs_c + j
vi2 = vi1 - segs_c
vi3 = vi2 + 1
vi4 = vi1 + 1
tri_data.extend((vi1, vi2, vi3))
tri_data.extend((vi1, vi3, vi4))
vi1 = i * segs_c
vi2 = vi1 + segs_c
vi3 = vi2 + segs_c - 1
vi4 = vi1 + segs_c - 1
tri_data.extend((vi1, vi2, vi3))
tri_data.extend((vi1, vi3, vi4))
vert_count = segs_c * (segs_h + 1)
vertex_format = GeomVertexFormat.get_v3n3()
vertex_data = GeomVertexData("vertex_data", vertex_format, Geom.UH_static)
vertex_data.unclean_set_num_rows(vert_count)
data_array = vertex_data.modify_array(0)
memview = memoryview(data_array).cast("B").cast("f")
memview[:] = data
tris_prim = GeomTriangles(Geom.UH_static)
tris_prim.set_index_type(Geom.NT_uint32)
tris_prim.reserve_num_vertices(len(tri_data))
tris_array = tris_prim.modify_vertices()
tris_array.unclean_set_num_rows(len(tri_data))
memview = memoryview(tris_array).cast("B").cast("I")
memview[:] = tri_data
geom = Geom(vertex_data)
geom.add_primitive(tris_prim)
node = GeomNode("cylinder_node")
node.add_geom(geom)
cylinder = parent.attach_new_node(node)
return cylinder
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# set up a light source
p_light = PointLight("point_light")
p_light.set_color((1., 1., 1., 1.))
self.light = self.camera.attach_new_node(p_light)
self.light.set_pos(5., -10., 7.)
self.render.set_light(self.light)
radius = 2.
height = 5.
segments = {"circular": 5, "height": 3}
# create a cylinder parented to the scene root
self.cylinder = create_cylinder(self.render, radius, height, segments)
# create a matrix describing the inverse of the pivot position
# (the "object-origin-to-pivot" transformation; in this example,
# the pivot is located at the center of the cylinder)...
mat = Mat4.translate_mat(0., 0., -height * .5)
# ...combine it with a matrix describing a rotation about that
# pivot point...
mat *= Mat4.rotate_mat(30., Vec3(1., -1., 0.))
# ...and finally combine this with another matrix, this time
# describing the pivot position itself
# (the "pivot-to-object-origin" transformation)
mat *= Mat4.translate_mat(0., 0., height * .5)
vert_count = segments["circular"] * (segments["height"] + 1)
vert_count_half = vert_count // 2
rows = SparseArray()
# set the bits corresponding to the row indices of the vertices to be
# transformed to "on" (in this example the vertices in the top half
# of the cylinder)
rows.set_range(vert_count - vert_count_half, vert_count_half)
vertex_data = self.cylinder.node().modify_geom(0).modify_vertex_data()
vertex_data.transform_vertices(mat, rows)
app = MyApp()
app.run()
```

To make sure the transformation is applied relative to the pivot point, the matrix describing it has to be the combination of the “object-origin-to-pivot” matrix, the matrix describing the actual transformation and the “pivot-to-object-origin” matrix (the inverse of the first one). Note that these are “object space” matrices, i.e. they are relative to the `GeomNode`

containing the cylinder geometry.

In the above example, the vertices were added from bottom to top, so the last half of the vertex indices is set as “on-bits” in the `SparseArray`

passed into the call to `GeomVertexData.transform_vertices`

.