Procedural mesh generation

Hi,

I’m new to USD so let me know if I’m posting under the wrong category.

I’m currently trying to port a procedural mesh generator to use USD/Hydra (targeting usd-23.08). Essentially, we have a procedural engine capable of generating geometry in the form of meshes (points + topology + point attributes). The engine takes in a set of parameters and outputs a mesh. The meshes can be fairly large, so we currently use an Arnold procedural plug-in to create the meshes at render-time, saving the need to transfer huge files to the render farm. However, this approach is obviously DCC-specific (to Arnold in this case), which is why we are looking to implement something similar for USD/Hydra.

Two different approaches have been tried so far:

(1) Inspired by Paolo Emilio Selva’s Siggraph 2023 presentation (starts at 0:52:00) we implemented a custom UsdGeomGprim since we want to provide extent as well as represent our procedural parameters. This follows the examples, https://github.com/wetadigital/USDPluginExamples, provided by Weta. Next, we implemented a custom UsdImagingGprimAdapter with the idea that our adapter can read the parameters from our custom prim and generate a suitable mesh. While this works in theory, there are some practical issues. For instance, we need to implement both GetPoints and GetTopology on our adapter. In our case this means that our engine generates the procedural mesh twice, once to provide points and a second time to provide topology. The question here is: Is there a way to generate our procedural mesh only once and have it cached somewhere so that we can answer point/topology queries using the cached data? My understanding of the UsdImagingGprimAdapter interface is rudimentary at this time so I’m hoping there is a solution to this problem, since generating points together with topology is a common strategy.

(2) Our second approach has been to use the more modern HdGpGenerativeProcedural method, based on this article (translated to English). Here we don’t have the issues of having to generate points/topology separately, since we are free to build our HdSceneIndexPrim however we like in the GetChildPrim method. What worries us here is that HDGP_INCLUDE_DEFAULT_RESOLVER must be defined in order for this to work, meaning that existing DCC’s might not be able to handle HdGpGenerativeProcedural just yet.

Any input on best practices or suggested strategies are highly welcomed at this point. (sorry for inlining one of the links above, as a new user I am only allowed to use two links)

Best regards,

Tommy

Hi Tommy,

One very important question I would start with is: are you interested in having access to your generated geometry on the USD side or are you happy to just render it in a delegate, and have if fully driven by procedural parameters ?

Basically, if you want to have your procedural to generare “usd-prims”, you can implement it as SdfFileFormat plugin.
Probably not the most performant solution, but it lets you expose the result of your procedural in usd-prims, which you could, for example, then modify in Houdini, just because then they are just usd-prims.
Pretty useful if you also want to bake the result of your procedural and pass it around to others without providing any custom plugins.

Be aware that any of the following alternatives will not make it straight-forward to share your files with others that don’t access to your plugins.

If you are interested in just expanding the procedural for rendering purposes, you are only interested in generating “hydra-prims”, and you want to implement a Hydra PrimAdapter or a Hydra Generative Procedural, as you pointed out.
This is like a “black box” on the usd-side, you only see one usd-prim (custom, you have to implement it), and the primadapter will create generic hydra-prims procedurally, for any render delegate.

The other level of “blackbox-ness”, is to actually even by pass hydra entirely, and make a small plugin that passes directly your procedural to a very specific render delegate, and you have to implement a custom hydra-prim, as well as a custom usd-prims.
(say, you still want to render your procedural for Arnold only, as arnold-procedural, and mostly reuse your entire current code).

There is no one solution for all procedurals, it really depends on how you want to interact with the procedural (or how you want users/artists to interact with it).

I don’t want to be too drastic but it is important to understand here that based on which approach you take, you actually are going to generate geometry in usd-format or hydra-format, which are effectively two different systems, and Pixar made it easy for us to provide “translators” from usd-to-hydra for all of the generic/common primtypes in the OpenUSD repo.

As FYI, we have implemented all of the above because we have a wide range of procedurals, that can be used in various ways.

Sorry for the long message, I hope this helps taking the right approach for your procedural.
Happy to continue the conversation based on which one is your preferred/required approach.

cheers,
Paolo

Hey Tommy,

in case you need a starting point, I’ve briefly put together a very simple SdfFileFormat plugin that generates cubes-procedurally.
This is the first option I was referring to, which only requires you to create a fileformat plugin, but you can then generate usd-prims that could be easily exported for portability, and they can easily render in any usd-compatible host.

It compiles using standard OpenUSD (which you need to clone/compile) or, if you have it installed, HUSD from SideFX Houdini/Solaris, so you can have a nicer UI to help change parameters of your procedural.

Compatible with usd-23.11 and Houdini-20.0

Cheers,
Paolo

I’ll add to Paolo’s feedback that it’s useful to understand how much/little control you have of the end-to-end rendering pipeline here. You mentioned, for example, “What worries us here is that HDGP_INCLUDE_DEFAULT_RESOLVER must be defined in order for this to work, meaning that existing DCC’s might not be able to handle HdGpGenerativeProcedural just yet.”

There are (at least) three potential ways you can get the HdGP resolver to be active:
1 - use that environment variable
2 - have the rendering application (e.g., Houdini, Maya, Katana) insert a resolver into the chain of Scene Indexes
3 - have the rendering delegate (e.g., hdPrman) insert a resolver into the chain of Scene Indexes

Following Rob’s feedback: the environment variable is not a must-have for enabling the HdGP. You may also just copy the code inside the if statement: OpenUSD/pxr/imaging/hdGp/sceneIndexPlugin.cpp at 0b18ad3f840c24eb25e16b795a5b0821cf05126e · PixarAnimationStudios/OpenUSD · GitHub
and paste it somewhere before your application initializes the Hydra renderer.

Hi,

Thanks for all the suggestions! This is a lot of food for thought and I’m sure we will have additional questions when have had time to explore the options mentioned above.

Thanks,

T

Hi again,

Hope it’s alright to re-open this thread for some further questions.

We have gone down the path of implementing our procedural as a HdGpGenerativeProceduralPlugin. As a first attempt we follow the simple pattern suggested in suggested in TestUsdImagingGLHdGpProcedurals.cpp:

// myProc.cpp

#define MYPROC_TOKENS                 \
    ((center,  "myProc:center"))      \
    ((sizeX,   "myProc:sizeX"))       \
    ((sizeZ,   "myProc:sizeZ"))       \
    ((rows,    "myProc:rows"))        \
    ((columns, "myProc:columns"))    

TF_DEFINE_PRIVATE_TOKENS(MyProcTokens,
    MYPROC_TOKENS
);

class MyProc : public HdGpGenerativeProcedural {
public:
  MyProc(...) : HdGpGenerativeProcedural(...) {}

  DependecyMap UpdateDependencies(...) override { ... }
  ChildPrimTypeMap Update(...) override { ... }
  HdSceneIndexPrim GetChildPrim(...) override { ... }
};


private:
  struct _Args
  {
    GfVec3f center = GfVec3f{ 0.F, 0.F, 0.F };
    float sizeX = 10.F;
    float sizeZ = 10.F;
    uint32_t rows = 10U;
    uint32_t columns = 10U;
  };

  // Called to retrieve values for arguments used to construct a (grid) mesh
  _Args _GetArgs(...) 
  {
    _Args result{};
    HdSceneIndexPrim myPrim = inputScene->GetPrim(_GetProceduralPrimPath());
    HdPrimvarsSchema primvars =
      HdPrimvarsSchema::GetFromParent(myPrim.dataSource);
    if (!primvars.IsDefined()) {
      return result;
    }
      
    if (auto primvar = primvars.GetPrimvar(MyProcTokens->center); primvar.IsDefined()) {
      if (HdSampledDataSourceHandle ds = primvar.GetPrimvarValue();
          ds) {
        if (VtValue v = ds->GetValue(/*shutterOffset=*/0.F);
            v.IsHolding<GfVec3f>()) {
          args.center = v.UncheckedGet<GfVec3f>();
        }
      }
    }
    // And similar for other members of _Args ...
    return result;
  }

class MyProcPlugin : public HdGpGenerativeProceduralPlugin
{
public:
  MyProcPlugin() = default;

  HdGpGenerativeProcedural* Construct(...) override
  {
    return new MyProc(...);
  }
};

TF_REGISTRY_FUNCTION(TfType)
{
  HdGpGenerativeProceduralPluginRegistry::
      Define<MyProcPlugin, HdGpGenerativeProceduralPlugin>();
}

While this can be made to work in a simple scene like the following:

#usda 1.0
(
   startFrame = 0
   endFrame = 100
)

def Scope "world"
{
   
    def GenerativeProcedural "my_proc" (
        prepend apiSchemas = ["HydraGenerativeProceduralAPI"]
    )
    {
        token primvars:hdGp:proceduralType = "MyProc"

        float3 primvars:myProc:center = (0.0, 0.0, 0.0)
        float primvars:myProc:sizeX = 10.0
        float primvars:myProc:sizeZ = 10.0
        uint primvars:myProc:rows = 20
        uint primvars:myProc:columns = 20
    }
}

It seems noteworthy to me that the author of this scene must be aware of the names and types that the procedural expects to exist. In a DCC, like Houdini/Solaris, it is of course possible to construct prims that correspond to the my_proc prim in the example above, but it requires knowledge of how the procedural operates internally, i.e. what primvars it expects to read its arguments from.

In his presentation on generative procedurals (around 12:50) Steve LaVietes suggests that procedural-specific API schemas could be used to solve the issues mentioned previously. However, I’m not sure how to implement such an API schema.

Some sort of attempt at an API schema, based on this example:

#usda 1.0
(
    subLayers = [
        @usd/schema.usda@
    ]
) 

class "MyProcAPI" (
    # We must inherit from APISchemaBase!?
    inherits = </APISchemaBase>

    # These listed applied API schemas will be built-in to this schema type and
    # will always be applied when this schema is applied.
    prepend apiSchemas = ["HydraGenerativeProceduralAPI"]

    customData = {
        token apiSchemaType = "singleApply"
        token[] apiSchemaCanOnlyApplyTo = ["GenerativeProcedural"]
    }
)
{
   token primvars:hdGp:proceduralType = "MyProc"

    float3 primvars:myProc:center = (0.0, 0.0, 0.0) (
        customData = {
            string apiName = "center"
        }
        doc = "DOCS!"
    )

    # And similar for other args...
}

Some questions here:

  • My understanding is that API schemas must inherit directly from APISchemaBase. Now, since I want to “extend” HydraGenerativeProceduralAPI, how do I go about doing that? When I try to “prepend” HydraGenerativeProceduralAPI usdGenSchema gives an error:
File "/home/tohi-local/packages/houdini/20.0.506/bin/usdGenSchema", line 808, in _GetAPISchemaOverridePropertyNames
    if not propStack[0].customData.get('apiSchemaOverride', False):
IndexError: list index out of range
  • Do I need to add something to subLayers to be able to prepend HydraGenerativeProceduralAPI?
  • Is it “correct” to try to provide a fallback value for primvars:hdGp:proceduralType in this sitation, or should that be handled elsewhere? (and if it should be done by the API schema, how is it done?)
  • WIll using an API schema require changes to how arguments are read inside the procedural? I can imagine having to go through the API somehow, but I’m not sure how it would work.

As a side note, it seems tricky to use things like apiSchemaAutoApplyTo since the prim type in our case will always be GenerativeProcedural, and the “actual” type is govered by primvars:hdGp:proceduralType. This is not a big issue, just something to note I guess.

As always, any input is helpful!

Cheers,

Tommy