Flipbook keyframe animation with individual mesh per frame

Hello all,

I have a LiDAR point cloud animation which I want to generate a USD version of. I have different particles count possible in each frame, so I want to generate per-frame points mesh that I flip through on each new frame.

I managed to generate this with an AI agent and it does seem like what I am after:

#usda 1.0
(
    defaultPrim = "World"
    endTimeCode = 2
    framesPerSecond = 1
    startTimeCode = 0
)

def Xform "World"
{
    def Points "Frame_0"
    {
        point3f[] points = [
            (-3.0, 0.0, 0.0), (-2.5, 0.2, 0.1), (-2.0, 0.0, 0.0), (-1.5, -0.1, 0.2), (-1.0, 0.0, 0.0),
            (-0.5, 0.3, -0.1), (0.0, 0.0, 0.0), (0.5, -0.2, 0.3), (1.0, 0.0, 0.0), (1.5, 0.1, -0.2),
            (2.0, 0.0, 0.0), (-2.2, 0.5, 0.4), (-0.8, -0.3, 0.1), (0.3, 0.4, -0.3), (1.8, -0.1, 0.2)
        ]
        color3f[] primvars:displayColor = [
            (1.0, 0.0, 0.0), (0.9, 0.1, 0.0), (0.8, 0.2, 0.0), (0.7, 0.3, 0.0), (0.6, 0.4, 0.0),
            (0.5, 0.5, 0.0), (1.0, 0.0, 0.2), (0.9, 0.0, 0.3), (0.8, 0.0, 0.4), (0.7, 0.0, 0.5),
            (0.6, 0.0, 0.6), (1.0, 0.2, 0.0), (0.8, 0.4, 0.0), (0.6, 0.6, 0.0), (0.9, 0.1, 0.1)
        ] (
            interpolation = "vertex"
        )
        float[] widths = [
            0.8, 1.0, 0.9, 1.2, 0.7, 1.1, 0.6, 1.3, 0.8, 0.9, 1.0, 0.7, 1.2, 0.8, 1.1
        ]
        
        token visibility.timeSamples = {
            0: "inherited",
            1: "invisible", 
            2: "invisible"
        }

        uniform token subdivisionScheme = "none"
        double3 xformOp:rotateXYZ = (0, 0, 0)
        double3 xformOp:scale = (1, 1, 1)
        double3 xformOp:translate = (0, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
    }

    def Points "Frame_1" 
    {
        point3f[] points = [
            (-2.8, 1.2, 0.5), (-2.0, 1.8, 0.3), (-1.3, 0.9, -0.2), (-0.7, 1.5, 0.4), (0.2, 0.7, 0.1),
            (0.8, 1.3, -0.3), (1.5, 0.4, 0.6), (2.2, 1.1, -0.1), (2.7, 0.8, 0.2), (-2.5, -0.6, 0.7),
            (-1.1, 1.7, -0.4), (0.4, -0.8, 0.5), (1.7, 1.6, 0.0), (2.3, -0.3, 0.8), (0.0, 2.1, -0.2)
        ]
        color3f[] primvars:displayColor = [
            (0.0, 1.0, 0.0), (0.1, 0.9, 0.2), (0.2, 0.8, 0.4), (0.0, 0.7, 0.6), (0.3, 0.6, 0.1),
            (0.1, 0.9, 0.3), (0.0, 0.8, 0.5), (0.2, 0.7, 0.2), (0.0, 1.0, 0.1), (0.4, 0.6, 0.0),
            (0.1, 0.8, 0.4), (0.0, 0.9, 0.2), (0.3, 0.7, 0.1), (0.0, 0.6, 0.7), (0.2, 1.0, 0.0)
        ] (
            interpolation = "vertex"
        )
        float[] widths = [
            1.2, 0.8, 1.4, 0.6, 1.6, 0.9, 1.1, 1.3, 0.7, 1.5, 0.8, 1.0, 1.2, 0.9, 1.4
        ]
        
        token visibility.timeSamples = {
            0: "invisible",
            1: "inherited",
            2: "invisible"
        }

        uniform token subdivisionScheme = "none"
        double3 xformOp:rotateXYZ = (0, 0, 0)
        double3 xformOp:scale = (1, 1, 1)
        double3 xformOp:translate = (0, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
    }

    def Points "Frame_2"
    {
        point3f[] points = [
            (-1.5, -1.8, 1.0), (-0.9, -2.3, 0.8), (-0.2, -1.4, 1.2), (0.6, -2.0, 0.6), (1.3, -1.1, 0.9),
            (1.9, -1.7, 1.1), (2.5, -0.8, 0.7), (-2.1, 0.9, -0.5), (-1.4, -0.4, 1.3), (-0.6, 0.6, -0.8),
            (0.1, -1.9, 1.4), (0.9, 0.3, -0.6), (1.6, -1.2, 1.0), (2.2, 0.7, -0.4), (-0.3, 1.8, 0.9)
        ]
        color3f[] primvars:displayColor = [
            (0.0, 0.0, 1.0), (0.2, 0.0, 0.9), (0.4, 0.0, 0.8), (0.6, 0.0, 0.7), (0.1, 0.2, 0.9),
            (0.3, 0.1, 0.8), (0.0, 0.3, 1.0), (0.2, 0.2, 0.8), (0.4, 0.0, 0.9), (0.0, 0.4, 0.7),
            (0.5, 0.1, 0.8), (0.1, 0.0, 1.0), (0.3, 0.2, 0.6), (0.0, 0.1, 0.9), (0.4, 0.3, 0.7)
        ] (
            interpolation = "vertex"
        )
        float[] widths = [
            0.9, 1.3, 0.7, 1.5, 0.8, 1.1, 1.4, 0.6, 1.2, 1.0, 0.9, 1.3, 0.8, 1.4, 1.1
        ]
        
        token visibility.timeSamples = {
            0: "invisible",
            1: "invisible",
            2: "inherited"
        }

        uniform token subdivisionScheme = "none"
        double3 xformOp:rotateXYZ = (0, 0, 0)
        double3 xformOp:scale = (1, 1, 1)
        double3 xformOp:translate = (0, 0, 0)
        uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"]
    }
}

So far I can play this file correctly in macOS preview. It also aligns with this sample I found online.

When I try to convert it to USDZ to send it as an iMessage for example, the file no longer plays back. I am using usdzip to create my .usdz file. My questions are:

  1. The `token visibility.timeSamples = { “visible”, “hidden” }` playback approach seems correct to me? Core glTF does not have anything like visibility.timeSamples so I am forced to move inactive frames to position (1000, 0, 0) instead in my exporter.
  2. Why would the .usdz file produced by usdzip fail opening and playing back? Can such a point cloud animation be achieved? Ideally I want to be able to play the point clouds in iMessage / macOS as I am aiming towards a more casual crowd.

Thanks in advance!

Hi!

so the issue you’re hitting is that RealityKit doesn’t support vertex animation.

see this link for the differences in renderers Validating feature support for USD files | Apple Developer Documentation

Thanks for the answer @dhruvgovil! I read through the linked docs and all makes sense.

Another approach I had in mind was to instance a shape many times to achieve my point clouds. I can see however that PointInstancer is not supported either.

I suppose I can’t do anything to achieve animated point clouds? I guess for now I will export each point cloud frame as a single merged mesh.

Correct, unfortunately you’d currently have to do it via meshes.

You could do some tricks in a shader or code. In code you could hide and unhide stuff. For shaders, you could drive properties of the mesh by time.

A common technique to get around the lack of vertex animation in many realtime engines is a vertex animation texture. Basically baking the position deltas down per frame and then using a shader to apply the deltas.

But very much depends on the specific projects as there’s no silver bullet

Interesting, as I was reading on the docs I did not see mention of programmability. I want to support QuickLook / macOS preview primarily. Could runtime logic be inserted somehow?

Apologies, I didn’t mean that you could program it directly within the USD. But you could if you were making an app

1 Like

Also just wanted to note, even though it doesn’t help with the iOS/USDZ target, that the example you found isn’t really using the schemas in their intended, most efficient way. Firstly, the Points schemas has no subdivisionScheme attribute (that belongs only to Mesh), but more importantly, all of the attributes in the schema are directly animatable with timeSamples, so rather than needing to make a separate Points prim for each frame and coordinating each’s visibility, you can just animate the points, primvars:displayColor, and widths attributes over time. That should work equally well in MacOS preview (I believe since it’s using Storm), and most DCC integrations.

1 Like

Thanks for the input all. I have different amount of points per frame so morphing would not work.

my questions have been answered - thanks!

Last time, I tried a similar approach to work in AR Quick Look, I used scale = (0,0,0) instead of visibility or moving erratically. Problem with this: It can cause motion blur.

To counter/reduce that, try to either increase the fps from 60 to 600, or use timeSamples frames like 0.95, 1.0, 1.05