maximizing performance of 3 d user generated assets in unity
TRANSCRIPT
Maximizing performance of 3D user-generated
assets in UnityMax Pixel
Freeform Labs
DBA since May 2015Incorporated March 2016
Your Speaker
2003Games & Websites as a hobby
2008Freelance
web development
2011 - 2014 - 2015USC Film Production
→Interactive Entertainment
Freeform Labs
Inception Feb 2014Funded March 2016
Q: Why is it freezing?
A: GARBAGECOLLECTION
The AgendaOptimization principles
Real examples
???
PROFIT
Hands-on Demonstration!
Starting Point:http://wiki.unity3d.com/index.php/OptimizedTrailRenderer
https://github.com/M-Pixel/unity-procedural-mesh-optimization-presentation
Encapsulation grumble grumblepublic float maxAngle → [SerializeField] private float _maxAngle
Hold down alt for magic Buy ReSharper for more magic
Full disclosure: I have no affiliation with ReSharper. They don’t give me money. I just really like it.
Startprivate void Start(){ _trailObj = new GameObject("Trail"); // Why have a game object that just controls another one? _trailObj.transform.parent = null; _trailObj.transform.position = Vector3.zero; _trailObj.transform.rotation = Quaternion.identity; _trailObj.transform.localScale = Vector3.one; var meshFilter = (MeshFilter)_trailObj.AddComponent(typeof(MeshFilter)); _mesh = meshFilter.mesh; _trailObj.AddComponent(typeof(MeshRenderer)); _instanceMaterial = new Material(_material); _fadeOutRatio = 1f / _instanceMaterial.GetColor("_TintColor").a; _trailObj.GetComponent<Renderer>().material = _instanceMaterial;}
Prefer Observation[SerializeField] private Transform _source;
RequireComponent[RequireComponent(typeof(MeshFilter))][RequireComponent(typeof(MeshRenderer))]public class Trail : MonoBehaviour {
Destroy(_trailObj);Destroy(this); // ???
New Startprivate void Start(){ _mesh = GetComponent<MeshFilter>().mesh; _instanceMaterial = new Material(_material); _fadeOutRatio = 1f / _instanceMaterial.GetColor("_TintColor").a; _renderer = GetComponent<MeshRenderer>(); _renderer.material = _instanceMaterial;}
(because the mesh origin needs to be stationary, that’s why)
Avoid Reflectionif (_pointCnt < 2){
_trailObj.GetComponent<Renderer>().enabled = false;return;}_trailObj.GetComponent<Renderer>().enabled = true;
Revisedif (_pointCnt < 2){ _renderer.enabled = false; return;}_renderer.enabled = true;
Add to Classprivate MeshRenderer _renderer;
In Start_renderer = GetComponent<MeshRenderer>();
Optimize by actually optimizing, not reducing quality
// Optimizationif (_pointCnt > _optimizeCount){ _maxAngle += _optimizeAngleInterval; _maxVertexDistance += _optimizeDistanceInterval; _optimizeCount += 1;}
Heap vs StackLong-term, Garbage Collected● Class instances● Arrays
○ Even tiny ones ( ノ ಥ益ಥ) ノ
Very short-term● POD● Structs
○ … sometimes
Points are being deleted and reallocated at runtimefor (var i = _pointCnt - 1; i >= 0; i--) {
var point = _points[i];if (point == null || point.TimeAlive > _segmentLifetime) {
_points[i] = null;_pointCnt--;
} else break;}if (_emit) {
if (_pointCnt == 0) {_points[_pointCnt++] = new Point(_source.transform);_points[_pointCnt++] = new Point(_source.transform);}...
indices > _pointCnt are ignored anywaysfor (var i = _pointCnt - 1; i >= 0; i--) {
var point = _points[i];if (point == null || point.TimeAlive > _segmentLifetime) {
_points[i] = null;_pointCnt--;
} else break;}if (_emit) {
// Make sure there are always at least 2 points when emittingif (_pointCnt < 2) {if (_pointCnt < 1) InsertPoint();InsertPoint();}...
RAM & Speed
Putting things away and taking them out again takes a lot of time
Shifting is unnecessarily expensiveRemoving old points
for (var i = _pointCnt - 1; i >= 0; i--){var point = _points[i];if (point.TimeAlive >
_segmentLifetime) {_pointCnt--;
} else break;}
Adding new points
for(int i = pointCnt; i > 0; i--)points[i] = points[i-1];
points[0] = new Point(transform);pointCnt++;
What about adding new points on top of the array?
Just reverses problem
What about a Queue?
Yes!
using (var e = _ints.GetEnumerator()) {while (e.MoveNext()) {
_current += e.Current;}}
for (var i = 0; i < _ints.Count; i++) {var item = _ints.Dequeue();_current += item;_ints.Enqueue(item);}
Unity’s Enumerator Problem
Temporary Arrays
// Rebuild itvar vertices = new Vector3[pointCount * 2];var uvs = new Vector2[pointCount * 2];var triangles = new int[(pointCount - 1) * 6];var meshColors = new Color[pointCount * 2];
5.3 MB/s
Comparing Distance
var sqrDistance = (_points[1].Position -_source.transform.position
).sqrMagnitude;
if (sqrDistance > _minVertexDistance * _minVertexDistance){
…}
if (Vector3.Dot(a, b) > _preSquaredBasis) …
Comparing Distance
a2 + b2 = c2
a2 + b2 + c2 = d2
d = sqrt ( a2 + b2 + c2 )
Square root is a relatively expensive operation
Dot == a2 + b2 + c2
Vector3.Distance == Sqrt ( Dot ( A , B ) )
Comparing dots is faster than comparing distances
Double trouble: Unity’s Vector3.Dot uses doublesSo if you wanna go real fast, make your own Dot
Other Optimizations
Use arrays for vertex, color, uv buffers (triangle buffer must be a List)
Make Point a struct (Pre-allocate)
Eliminate (inline) pool
Eliminate Points altogether, work directly with vertices?
What about extra vertices?
It turns out, any number of extra verts/colors/etc. have negligible impact
Only triangles matter!
mesh.SetTriangles takes a List
The final result...
34 to 60KB garbage + .41msvs
0B garbage + .24ms(per frame)
Takeaways
Use the Unity Profiler’s gcalloc column to find out where heap allocation occurs
First use utilities like Queue, List to eliminate GC, then once your pattern is confirmed to work and the design is locked down, you can refactor to naked arrays
Reuse Arrays, Delegates, G.O.s whenever possible
Start/end indexes
Pool unused instances
Don’t use IEnumerators, find a workaround
When comparing distances, use Dot instead of Distance
Mesh data: Use List for triangles, Array for everything else
Multithreading is easy! Use queue + lock, and good encapsulation
Thank [email protected]