Being a student volunteer at the SIGGRAPH convention this past summer, I was lucky enough to go to Pixar's Art and Science Fair. During the fair, I came across a booth they had set up talking about USD and Hydra. I heard a lot about USD while at SIGGRAPH (it was hard not to), but I still didn't really know what it was. Walking up to the booth, there were two team members from the project who gave me a great description and left something ringing in my ears long after we had finished talking - “I could easily integrate USD into any of my personal renderers and 3D applications.” Shortly after SIGGRAPH, classes began again at Purdue and, as part of one of my goals for my second year of university, I formed an animation production studio with my classmates and the Computer Graphics Technology department. As part of the studio, I began work on a production path tracer that we could use for learning purposes running on— you guessed it— USD and Hydra. (Don’t worry, I plan on making later posts where I talk about the path tracer more) I started off by looking online for resources about the USD/Hydra C++ API. While there were lots of Python examples, there weren't many for C++, so I thought I would make this guide talking about how I implemented Tiny, a tiny USD/Hydra program that Pixar presented at SIGGRAPH 2019 during their Hydra presentation. Tiny is a mock application, so it won't render any real images and instead just outputs to the console. For it to work, we are going to need to make a Scene Delegate, a Render Delegate, a Scene Graph, some Renderer Classes and a couple of Hydra Tasks. To start off, Pixar has this great visual in the presentation which shows how applications interact with Hydra: Our tiny application mainly communicates with the engine and the RenderIndex. We'll talk more about the engine later, but let's first get to the RenderIndex. The RenderIndex is the main communication class between the scene description and the renderer. It keeps track of all the objects in the scene and performs the main Hydra Loop: Sync, Commit Resources, and Execute. During the Sync phase, objects are pulled from the scene delegate and into the render delegate to be rendered. Commit Resources is when the renderer commits the results of the Sync phase. Finally, during the Execute phase is when the actual Hydra tasks are performed, i.e. render, colorcorrect etc. The engine tells the render Index when to start the execution loop. For the RenderIndex to be able to perform the loop, it communicates with both a scene delegate and render delegate, which in turn communicate with either their respective scene description or the renderer. If that sounds confusing, it’s ok, it'll make more sense once we walk through the program and you'll be able to see the flexibility Hydra can bring to your pipeline. So, the first classes we need to create are the TinySceneDelegate and TinyRenderDelegate classes to act as our delegates that connect with our scene description and renderer: TinySceneDelegate is used to communicate with your scene description and it extends HdSceneDelegate. For a scene description to be able to connect with the RenderIndex, it needs its own personalized delegate that passes data between the two objects (USD’s sceneDelegate is called UsdImagingDelegate). TinySceneDelegate takes in a pointer to the application’s RenderIndex and its SdfPath Id, which is normally just “pxr::SdfPath::AbsoluteRootPath()”. Tiny will only support mesh primitives, so our scene delegate will also only have mesh support. When Tiny calls populate(), it will construct our scene based off of the provided SceneGraph (we are just feeding it empty paths). AddMesh() is then used to add the objects id to the RenderIndex while its representation information is added to a “_mesh” struct which stored in the “_meshes” map. GetTransform() is called by the RenderIndex during Sync to get an object’s transform information and to pass it along to the renderDelegate. Finally, setTime() would be used to change the data in a primitive, but for our case, it just marks the prim with the sdfPath of "/Cube1" as Dirty (I'll talk more about what that means a bit later). The TinyRenderDelgate class is used to communicate with the renderer similar to the TinySceneDelegate, but it extends HdRenderdelegate. Hydra separates primitives into three separate categories: Rprim’s for renderable primitives like meshes, Sprim’s for state primitives like cameras and Bprim’s for buffer primitives like textures. Our “tiny renderer” only supports meshes, so we specify that in the SUPPORTED_RPRIM_TYPES TftTokenVector and leave the other primitive type vectors empty. I won't go through all of the functions in the class because many of them are self-explanatory and most return nullptr, as they are not used in Tiny explicitly but are needed, as they are pure virtual functions of HdRenderdelegate; however, I will touch on the important ones. CreateRprim() is called by the RenderIndex to create an rprim object in the context of our renderer, and, again, since our renderer only supports meshes, we can just return a mesh object for all CreateRPrim() calls. If we had other Rprims, we would need to identify and return the right type of object. CreateRenderPass() is used to create a renderpass based off of the inputed collection of Rprim objects. Our “renderer” has two helper classes: TinyRenderDelegate_Mesh and TinyRenderDelegate_RenderPass. A TinyRenderDelegate_Mesh is created for each Rprim object in the scene index. It extends HdMesh which itself extends HdRprim. To understand the functions in the class, we must first talk about the change tracker. The RenderIndex keeps track of the data that needs to be pulled from the scene delegate with the change tracker. Instead of pulling all of a primitives data at once every time Sync is called, it only pulls the data bits that are marked as dirty in the change tracker. In the case of Tiny, the only bits we have for our meshes is transform. All this pulling of data is done during the Sync phase. GetInitialDirtyBitsMask() gets the data we want to be pulled the first time Sync is called on an object. Because we want all the data to be pulled, we return all bits as being dirty. _InitRepr() is used to set the rendering representation of the rprim. In our case, we will set the representation as hull (there is a much better explanation of this in the USD documentation). The Sync() function is called by the RenderIndex during the sync phase and checks each of a mesh’s bits to see if it is dirty or not. If it finds a dirty bit, it pulls that data from the scene delegate to be used in the renderer. TinyRenderDelegate_RenderPass is used by our “renderer” to create a renderPass. It extends HdRenderPass. The only function it contains is _Execute(), which is called by the renderTask during the execute phase to actually do the renderpass. Hydra knows what what actions to perform based off of the tasks the program provides. For example, there could be a task for rendering the image and then a separate task for color correcting that image. TinyRender_Task extends HdTask. The task simply calls the Sync(), Prepare(), and Execute() functions at the appropriate time in the TinyRenderDelegate_RenderPass from its own Sync(), Prepare(), and Execute() respectively. TinyColorCorrection_Task extends HdTask similar to TinyRender_Task but simply prints to the console that color correction is occurring Finally, we have the main function of Tiny that brings the whole pipeline together. The only new class here is the collection, and it is required to get the rprims to sync, prepare and render. A collection is just a set of Rprims that you specify for the RenderIndex to send to the renderer. If all goes well, you should get something like this image (if some of the "Pulling new Transform->" outputs look out of place, that's ok - Rprims are synced using multithreading, so just run the program a few more times and it should fix itself): Hydra’s greatest strength is allowing applications to easily switch between any number of scene descriptions and connect it to any renderer (given of course they both have Hydra delegates). I have just scratched the surface of the whole API and I am by no means an expert, but I hope this will help you get started using the C++ API. My next step will be to apply these delegate concepts to Luna (my path tracer), so that we can use USD files in our productions. USD definitely feels like it's here to stay (and for good reason) and I’m excited to both learn all about it and help teach others at Purdue!
Notes on compiling: I compiled this program using windows msvc17x64 and there were some things I needed to do to get it working:
I’ll be putting the code up on my Github soon, which can be found here: https://github.com/Danielsirota Resources used: Hydra Presentation: http://graphics.pixar.com/usd/files/Siggraph2019_Hydra.pdf USD Resources: http://graphics.pixar.com/usd/downloads.html USD API Docs: http://graphics.pixar.com/usd/docs/api/index.html Building USD on windows: https://www.manicmachinegames.com/blog/2019/1/10/tutorial-setting-up-and-building-pixars-usd-on-windows
0 Comments
|