mkrebser / GPUInstance

Instancing & Animation library for Unity3D
Other
216 stars 41 forks source link
compute-shader gpu-animation gpu-instancing instance-mesh instancing skinned-animation skinnedmeshrenderer unity

GPUInstance

Instancing & Animation library for Unity3D.

Alt text

This library can be used to quickly and efficiently render thousands to hundreds of thousands of complex models in Unity3D. At a high level, this library uses compute shaders to implement an entity hierarchy system akin to the GameObject-Transform hierarchy Unity3D uses.

Features

A scene with many cube instances flying out in all directions. Alt text

Paths

Alt text

Billboards

Performant Instancing

Instance Transform Readbacks

Can Slow down, Speed up, & pause instance times.

Guide

This guide will be very basic- you will be expected to look at the demo scenes & demo models to learn how things work. You are expected to already know how to rig your models, create LODS (if you are using them), setup animations, etc... Additionally, this library requires that you know how to code (in c#).

Preparing a Skinned Mesh

Instancing Stuff

// Initialize GPU Instancer this.m = new MeshInstancer(); // The MeshInstancer is the main object you will be using to create/modify/destroy instances this.m.Initialize(max_parent_depth: hierarchy_depth + 2, num_skeleton_bones: skeleton_bone_count, pathCount: 2); this.p = new PathArrayHelper(this.m); // PathArrayHelper can be used to manage & spawn paths for instances to follow

// Add all animations to GPU buffer this.m.SetAllAnimations(controllers);

// Add all character mesh types to GPU Instancer- this must be done for each different skinned mesh prefab you have foreach (var character in this.characters) this.m.AddGPUSkinnedMeshType(character);

// Everything is initialized and ready to go.. So go ahead and create instances for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) { var mesh = characters[Random.Range(0, characters.Count)]; // pick a random character from our character list var anim = mesh.anim.namedAnimations["walk"]; // pick the walk animation from the character instances[i, j] = new SkinnedMesh(mesh, this.m); // The SkinnedMesh struct is used to specify GPU Skinned mesh instances- It will create and manage instances for every skinend mesh & skeleton bone. instances[i, j].mesh.position = new Vector3(i, 0, j); // set whatever position you want for the instance instances[i, j].SetRadius(1.75f); // The radius is used for culling & LOD. This library uses radius aware LOD & culling. Objects with larger radius will change LOD and be culled at greater distances from the camera. instances[i, j].Initialize(); // Each instance must be initialized before it can be rendered. Really this just allocates some IDs for the instance.

    instances[i, j].SetAnimation(anim, speed: 1.4f, start_time: Random.Range(0.0f, 1.0f)); // set the walk animation from before

    var path = GetNewPath(); // create new path
    instances[i, j].mesh.SetPath(path, this.m); // You have to make the instance aware of any paths it should be following.
    paths[i, j] = path;

    instances[i, j].UpdateAll(); // Finnally, invoke Update(). This function will append the instance you created above to a buffer which will be sent to the GPU.
}

// Get New Path Function. This will create a simple 2-point path. private Path GetNewPath() { // Get 2 random points which will make a path var p1 = RandomPointOnFloor(); var p2 = RandomPointOnFloor(); while ((p1 - p2).magnitude < 10) // ensure the path is atleast 10 meters long p2 = RandomPointOnFloor();

// The Path Struct will specify various parameters about how you want an instance to behave whilst following a path. See Pathing.cs for more details.
Path p = new Path(path_length: 2, this.m, loop: true, path_time: (p2 - p1).magnitude, yaw_only: true, avg_path: false, smoothing: false);

// Initialize path- this allocates path arrays & reserves a path gpu id
this.p.InitializePath(ref p);

// Copy path into buffers
var start_index = this.p.StartIndexOfPath(p); // what happening here is we're just copying the 2 random points into an array
this.p.path[start_index] = p1;
this.p.path[start_index + 1] = p2;
this.p.AutoCalcPathUpAndT(p); // Auto calculate the 'up' and 'T' values for the path.

// Each path you create requires that you specify an 'up direction' for each point on the path
// This is necessary for knowing how to orient the instance whilst it follows the path

// Additionally you need to specify a 'T' value. This 'T' value can be thought of as an interpolation parameter along the path.
// Setting path[4]=0.5 will mean that the instance will be half way done traversing the path at the 4th point on the path
// The 'T' value is used to specify how fast/slow the instance will traverse each segment in the path.

// send path to GPU
this.p.UpdatePath(ref p); // Finally, this function will append the path you created to a buffer which will send it to the GPU!

return p;

}



## Some performance considerations
* Try and reduce the number of multi-skinned meshes you have. Each additional mesh causes an additional DrawInstancedIndirect call- which results in more draw calls.
* You can change the animation blend quality of your models at each LOD by just changing it on the SkinnedMeshRenderer of your model prefab. I recommend 1-2 bones for lower LODS- it is pointless to have more.
* You can use different materials at different LODS. (And should). Eg, using fancy shader for LOD0 and basic diffuse for LOD4. Again, just specify this on your Skinned Mesh.
* You can toggle shadows on off for different LODS- again on your skinned mesh.
* Don't spawn more than ~50000 of the same (mesh,material) type. Instead break it up into batches by instantiating a new identical material. 
  * You will have much higher FPS instancing 20 objects with instantiated materials than all one million as the same type. This has to due with contention on the GPU.
* If you aren't using LODS for you skinned mesh then use them. 
  * On a GTX 10606GB- All of the demos (using 10-15000 skinned mesh) will run at above 150FPS.
  * Without LOD- Maybe 20-30FPS. There is simply too many animated vertices.
* This library has very little CPU overhead. You will only really get CPU overhead from populating the buffers which send data to the GPU.
* Changing the depth of entities with many children can be expensive. If you need to reparent entities with many children, try keeping them at the same hierarchy depth before and after reparenting.
* You can create/modify/and destroy instances on different threads than the Unity Main update thread.
* That being said, thread safety for this library is implented via simple mutual exclusion locks.

## Other Notes
* Some of the animations look Jank ASF because I am not an artist- I used Mixamo rigger with all my LODS at once which results in Jank
* Root animations not supported.
* Very simple animation state- No animation blending is implemented.
* If you want though, you can enable/disable animation for select bones and manually pose them yourself. Eg- Have a ragdoll control it or something.
* Tile textures are supported. You can specify the tiling & offset for the instance to use.
* There is also an optional per-instance color that you can set.
* If you want to modify the compute shader and add your own stuff- you can overwrite some of the fields in the property struct safely.
  * You can overwrite the offset/tiling if you dont need them. You can overwrite the color if you dont need it. You can overwrite the pathInstanceTicks if not using a path. You can overwrite the instanceTicks if not using an animation. The pad2 field is completely unused- you can use it for whatever without any worries.
* What version of Unity is supported? Unity 2023.2 is what this project was most recently built with- but it should work for most versions. See *branches* for versions with explicit support.