Avatar Optimization

From VR Wiki

Optimizing your avatar or world or 3D asset is a massively important part of the avatar creation process. In general, increasing the render efficiency of your avatar means that it has less impact on your, and other people's framerates.

VRChat has performance ratings, but these are only rough guides to actual optimisation. They're not that bad when you actually understand optimization, but this article will focus on one thing: Reducing the time it takes for your avatar to render. It is definitely possible to make an 'Excellent' avatar that is godawful, or a 'Very Poor' avatar that's extremely performant; at the extremes, but even at middling values it only records the totality of the avatar and does not represent what the avatar is like during render time. With Avatar 3.0 and lots of toggles it has become increasingly unrepresentative.

The Process

Actually working on optimizing your avatar will be a balancing act between how many shits you give, what kind of environment you expect to use the avatar in, and how many features you want.

It's recommended to focus your efforts on making appropriate avatars, rather than min-maxing optimisation every time. For example, an avatar you're going to use in a crowded instance you will definitely want to optimise as much as you can, else you'll be performance blocked and no-one will see your cute avatar. For instances where it's just a few people... or just two people.... then you can basically not optimise at all and be fine. Plus, some optimisaiton is harder than others. For most avatars doing the quick wins can get you a lot of extra frames for little effort.

Optimisation covers both blender and unity since the final asset is built in both. Some things are kind of blended between editors.

Focus areas:

  • Number of distinct mesh objects
  • Vertex Count (number of polygons)
  • Texture Size
  • Material Quantity
  • Shader settings
  • Use of Blendshapes on high poly-count meshes
  • Number of bones
  • Physbones (previously dynamic bones) count
  • Number of animator controller layers
  • Other crap added (particles, Lights, etc)

The main render/resources to be focused on are:

  • VRAM / RAM usage
  • GPU Frametime / Framerate (How long the avatar actually takes your GPU to render)
  • CPU Frametime (How long the CPU takes to go through all the avatar's logic and drawcalls and shit)
  • Download size

Unity Rendering tidbits

Understanding how unity renders things is important. Details may be in the focus areas but in general:

  • Inactive game objects contribute to file size and VRAM/RAM usage, but if they are deactivated they are not rendering and do not effect frame times.
  • Unity treats the VRAM and RAM as a sort of hybrid cache; the takeaway is that assets from inactive game objects get moved to RAM after a while, but pulling shit from RAM to VRAM is still slower than pulling from VRAM to present something onto the screen. Larger the inactive game objects, the larger the lag spike when the game object is toggled on.
  • Textures are uncompressed in VRAM. Texture size is the single largest overriding factor in VRAM usage.

Focus Areas

These should be roughly sorted by ease of optimisation. Removal tends to be the most efficient optimisation for any part of an avatar.

Other Crap added (Particles, Lights, etc)

Reduce these as much as possible, basically. Easy to optimise since you have to go out of your way to add these things.

For the Dynamic Penetration System, this uses lights to coordinate mesh bending.

If they're turned off they won't do anything though. Optimise by leaving them OFF by default, and try not to leave them on accidentally.

  • Remove from optimised avatars
    • Using Pumkin's tools - open the pumkins tools tab, load your avatar into the tool, and look for the "delete all" options. Or look through your avatar for particles and other items and remove them.
    • If using a prefab, they'll typically be on a toggle so you can check the toggles to indicate what you should remove.
  • Leave off when not in use


Shader settings

Generally: don't use effects you don't need, and try and use the same shader as much as possible. Shaders are basically code that runs on your GPU and the fancier the effect the longer it takes. GPUs are fast at running them so you honestly don't need to worry about performant shaders much at all.

Except in a few cases like reflection probes, grab passes, and computational heavy stuff that usually has to do with light bendingy and lensy type effects.

Don't be scared from using shader effects and material maps and caps and such, they're pretty and not that bad at all. Normal maps and mat caps were developed as faster ways to render detail than polygons or fancy lighting effects, after all.

Access the shader settings through the material settings. Conceptually, a material is the shader, as the shader is just code that runs that decides how the mesh is rendered. Multiple ways to do this:

  • For External materials, the settings can be found by locating the material in the project folders, and clicking on it to make it appear in the inspector. Remember you can lock a tab to the current thing; since clicking around happens quite a lot.
  • The settings can also be found in the mesh object. If you select a mesh object in your scene, there will be a drop-down menu for each material that is applied to that mesh.

Process

  • Avoid using heavy shaders like ones with transparent grab passes.
    • A transparent grab pass has to pause all rendering to render what is behind the transparent object. While this is useful for things like lenses and other light refraction type effects, it is an effect that is expensive to render time.
  • Performance: Opaque > cutout > transparent
  • Leave (back) culling on if you can. Nobody actually needs to see your nipples from the inside.
  • Using maps, cutouts, speculars, etc etc all have a minor performance draw
  • funny poiyomi effects also have a small impact

Number of bones

More bones = more shit that the game has to calculate when you move around. To optimise, reduce the amount of superfluous bones.

Performance impact from this is low for just having bones that exist; but pretty high for bones that will have physbones attached to them. Long chains of lots of little bones are bad for performance.

Minimal effort: Delete bones you aren't using. For example you deleted a skirt mesh, get rid of the bones too.

Low effort: Use CATS to merge the bones. Merging them with CATS will take the weights from one bone and merge them into the other, and delete one of the bones.

Find Model Options > Merge Weights.

Select the bone you want to merge and remove; then use ctrl-click to select another bone. Then hit Merge Weights - To Active. Or select one bone and hit 'To Parents' to merge it to the parent. This is most useful for reducing the amount of bones in a chain; some hair has a million bones and even with physbones it's overkill.

  • Reduce Bone count by deletion or merging

Number of Animator Controller Layers

The number of layers in your animator does actually matter when it comes to performance. Generally don't worry at all about it under 20 layers; but if you start to make loads of layers you can save some performance by redu cing them down. additionally, weight 0 layers take up CPU time and that's a unity bug.

  • Try not to do more than 20 layers
  • Zero weight layers still count

Use of Blendshapes on high-poly meshes

Shapekeys work by recording the difference on every single vertex between one state and the other and interpolating between them for the partial shapekey activation effects. This actually takes additional processing power and gets more difficult the more vertexes there are.

Facial gestures in VRC are usually done with shapekeys on the face. If the entire avatar is just one mesh, each time the shapekey is used the GPU has to calculate positions for every single vertex in the entire avatar, clothing and all - not just the face. This is why you make or keep the face seperate - the facial gestures are activated often and cause a performance drain if there are lots of polygons.

Keep in mind that If the avatar is under 20k polygons in total it's not a big deal, the render cost goes up a little non-linearly.

Number of Distinct Mesh Objects

Every seperate mesh requires a drawcall to activate. This is a set of instructions that lags the GPU until all the resources required to render the mesh are loaded and calculated. This happens every frame. Uncombined meshes basically waste GPU time on absolutely nothing and there is usually no reason to have them seperate.

Join meshes in Blender. Use ctrl-j to join your meshes. One except: High poly areas with shapekeys, like the face. Do not join the face to everything else.

Before joining everything like a madman, there are several reasons you'd actually want seperate meshes:

  • To have different outfits/props.
  • to save performance on shapekeys.
  • To reduce the amount of polygons rendering at once.

Actual optimisation is going to be a balancing act between enough meshes to get the functionality you want, without being too laggy. However; when making a maximal optimised avatar at above ~30k polygons you should have only two meshes; the face, and everything else. For avatars that are specifically low polygon, the reduction in drawcalls helps more than the shapekey issue. Balancing act.

Note that only active meshes actually take a drawcall. You could have 10 meshes, but if only face, clothing, body are the three meshes all at once, this will actually perform the same as a 3 mesh avatar. The VRC performance stats can't tell the difference between 10 all at once and just 3.

  • Minimise meshes that are rendering at the same time
  • Join meshes
  • For unjoined meshes, do not have many active at once.

Vertex Count / Poly Count

Fairly intuitive, more polygons the more rendering that needs to be done.

Polygons are also often referred to as verts, so a vertex count can be thought of as an analogous measure, as polygons themselves can be tris, quads, or beyond.

The general rule is to reduce the amount of polygons to a sensible limit. Like all other optimization efforts, there is a balancing act between effort, performance and fidelity. A model with more polygons will usually have more detail, although detail can be added with effects such as normal maps, mat caps and other shader rendering effects. Also keep in mind that shapekeys are per separate mesh object, and the amount of vertexes affects how much computation is needed to process a shapekey when active.

Reducing vertex count can be done in a number of ways, some more time intensive or destructive than others.

Mesh Deletion

The simplest and most effective means of vertex reduction is simply to delete mesh you don't need. This can be excess ribbons, adornments or other features, or deleting mesh that isnt even visible, such as skin under clothing, or layered outfits.

To delete mesh that is part of a mesh object, [>>todo: explain the difference between a mesh object and linked vertexes] select the certexes you want to remove in edit mode and press delete. To help you select mesh the following can be used:

  • Circle select - c
  • Box select - b (middle mouse button deselects from current selection)
  • shift + clicking individual verts
  • select linked - l and ctrl-l
  • Different views: wireframe, xray mode
  • h and alt-h to hide current selection to preview your deletion
  • knife tools- to cut new vertexes into the mesh for finer deletion
  • separate into new mesh object - p

You can also delete entire mesh objects, removing them from the model.

When deleting your mesh it is worth also checking the armature to see if any bones were for mesh you just deleted and removing them too as they are now useless.

Also important is that deletion is a destructive process and you should save iterative copies (ctrl-shift-s) so any mistakes can be easily fixed down the line. Trust me on this one.

Decimation

Decimation is another destructive process that forcefully reduces vertex count by automatically merging polygons together. Most useful on suspiciously high polygon clothes, attachments and hair.

To decimate, first separate out the mesh you want to have the polygon count reduced on by selecting it and pressing p.

Then in object mode with the mesh selected, go to the modifiers tab (blue spanner) and find the decimate modifier. Use the modifier to reduce the polygon count. if you get a non zero shapekey error, open the dropdown on the mesh object and delete shapekey data. This will delete all the shapekeys on that mesh.

Decimation is a pretty brutal process but it can be very effective, but keep in mind the following guidance:

  • Don't decimate the face. it has so many goddamn shapekeys and is what most people will be looking at the most
  • Hands, fingers and other joints get super fugged by decimation so avoid those areas.
  • It is often a good policy to not decimate the base skin mesh at all. the topology on that is often efficient already, and you'll likely cause yourself some problems.

Separating Mesh Objects

This is for when you have multiple meshes, such as outfits, but you don't need them on at the same time, such as two different hats, even two entire different outfits.

Why this is optimisation and not just a weird opposite of the earlier message to join meshes, is that your priority with a unity avatar is to reduce the amount of meshes rendering at the same time. Any deactivated game object is not taking up draw calls or other resources, so to have multiple outfits, the optimal play is to have only one rendering at once, one mesh, one outfit; for a total of 3 meshes rendering: face, body, outfit. And with many outfits on the same avatar that can be switched to but only using 3 draw calls at once.

Of course the most optimised is to have just one outfit, merge the body and outfit, delete unwanted mesh [>>todo: example use cases] and not change outfit. But to change outfit you would need to change avatar, and that would make everyone else in the instance reload the entire avatar just to have you put a hat on. The balance between fun and performance should always be a factor in how hard you optimise.

Anyway, the blender part of separating meshes is pretty simple:

  • Select the mesh you want separate
  • Press p to make it into a separate mesh object
  • Give it a name
  • Make a toggle for it in unity sometime later

Some people will make outfit toggles that are shapekeys which hide the alternate outfit inside their body or something. These people are indeed wrong, but they are saving a draw call or two. However, they are also adding two more performance hogs: the hidden mesh is still rendering and will take frames even though it is hidden inside the avatars. The shapekeys are still requiring calculation on the higher poly mesh of both outfits at once. This usually outweighs the benefits of saving a drawcall. I think- if anyone wants to test how many polygons a drawcall is worth that would be good to quantify.

Retopologising

This is an advanced technique that is for when you are making a from scratch model and want to clean the topology. it will murder your texturing, though. best done before texturing.

VRAM usage

Everything your avatar uses to render is stored, relatively uncompressed, inside the VRAM of people's GPUs as they render your avatar. VRAM is a limited resource, and while unity does some dynamic memory pool stuff between RAM and VRAM, when it swaps data between these two locations it is relatively slow. Once VRAM is overloaded rendering slows down significantly and frame rate drops. Therefore for an optimized avatar you will want to reduce the amount of VRAM your avatar uses.

when a texture or mask or map is loaded into unity, you configure what size the image is resized to when bundled into the game asset. The size or type of the image you drag into unity doesn't matter, it's resized as one of the things that unity takes so long on when uploading an avatar.

VRChat now checks for VRAM usage, thankfully, so everyone is going to actually pay attention now. Like you should have been in the first place.

Crunch compression compresses the textures OUTSIDE of VRAM. It affects download size and not how much a texture uses in VRAM. What affects VRAM usage is primarily texture resolution. Compression quality (not crunch!) affects VRAM usage a little, but it is going to be way less effective than lowering the resolution.

Textures and Maps

Materials use image files for texturing, for masks and for maps. They can be set at resolutions in the import settings, and this changes what the image is resized to for the uploaded version of the avatar. Higher resolutions will use more VRAM to render more pixels of the texture. If the texture goes in low quality, resolution won't help.

To access your texture import settings, find the texture file used in the material and view it in the inspector .

  • Search for it in your project folder
  • Find the material, and through the material click on the texture to highlight it in the project folder
  • Manually look through your texture folder.

Things to check

  • Use https://github.com/Thryrallo/VRCAvatarTools (It shows you which texture is the problem)
  • Lower the resolution of your textures in unity.
  • Remove mat caps/normal maps/ etc if you don't need them.
  • Use the tool above to find normal maps, masks, etc that are all causing VRAM usage.
  • The example in Booth_Optimization_example shows a basic process; all I did is set all 4k textures to 2k.
  • If you don't need the alpha channel (transparency, IE you have an opaque material), set the alpha source to none on the texture in Unit. With normal quality (not crunch) compression, RGB textures are half as big as RGBA

Examples

That's all well and good but how to actually do it? Here are some (one) case study of examples.

References

https://docs.unity3d.com/2022.1/Documentation/Manual/OptimizingGraphicsPerformance.html

https://docs.unity3d.com/2022.1/Documentation/Manual/ModelingOptimizedCharacters.html

https://docs.vrchat.com/docs/avatar-optimizing-tips