Deform a procedurally generated mesh with a slider

Hey Community,

I would like to know it it is possible to set up pivot liaisons inside a generated mesh (that is not an Actor node but a GeomNode).
I know the geometry of the mesh so I could rewrite the vertices of the mesh but it seems heavy computation. So is there a way to articulate the mesh as in Blender where you use Bones?

Cheers all!

Look into using controlJoints… Basically, you’ll assign vertices to your bones in blender as desired, name the bones in blender, then, in panda3d, create control joints out of the bones, by the names you’ve assigned them in blender, and the manipulate the joints to deform your mesh. As @rdb and @Thaumaturge noted to me recently, the controlJoint only affects vertices, not your nodepath or node. So any higherlevel things like collision rays will not be aware of the deformations applied to the controlJoint and its vertices. For that you apparently will need exposeJoint, but I haven’t figured out the needed relationships there between nodes, and the controlled vs exposed joint…

Hi nate,

Thx for the answers ^^, but I procedurally generate the mesh with a Geom Node. I don’t load a model with bones from Blender. So my question is : how can you setup bones for a Geom Node?

Cheers all!

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.

Hey Epihaius,

Thx for your answer, it is exactly what I wanted ^^
Cheers all!

For posterity…

To set up morph targets, you add a SliderTable to your GeomVertexData, and then set up extra vertex columns named something like vertex.morph.slider1 and vertex.morph.slider2, etc., containing the offset to add to the vertex position for each respective slider. You can use UserVertexSlider to directly set the values of the sliders.

To set up bones, you have to create a TransformBlendTable which is an array of TransformBlend objects, each of which is a weighted set of multiple VertexTransform objects (usually JointVertexTransform to bind it to a PartBundle tree, but this can also be a UserVertexTransform to control it directly). Then, you add a vertex column called transform_blend containing the index into this transform blend table.

So a vertex with transform_blend index 1 will have its position transformed by the weighted sed of transformations added to the TransformBlend object in the TransformBlendTable object with index 1.

1 Like

Hi rdb,

Could you explain a bit more your method (I’m lost ^^)?
Also, which method would be the fastest ?

Cheers all!

I don’t think performance plays a huge role here, but rather applicability to your specific use case. If you can explain your use case better and explain which part you are struggling with I may be able to advise further (I’m afraid I don’t have the time at the moment to be able to create a detailed write-up of the merits of each approach).

Thx for the reply rdb ^^

I want to deform a generated mesh using a DirectSlider.
So I think speed is a must in this case ^^

Cheers all!

Hi Epihaius,

How do I setup a SparseArray with a list?

Cheers all!

EDIT : I found a way ^^

    from panda3d.core import SparseArray
    myList = [0, 19, 15, 20]
    myarray = SparseArray()
    for nb in myList : 
        myarray.setBit(nb)