Large Numbers of GameObjects/Items

I’ve been tasked with developing a front end for a situational awareness system that tracks objects globally and I’ve two solutions, both with serious issues, and despite a few weeks of research failing to find a solution.

The scenario: Currently I’m tracking around 45,000 objects, this is due to increase to around 150,000. Each object has a position, direction and speed that vary over time and need to be displayed on screen. Hardware isn’t so much an issue as my workstation has a Quadro K5000 card and the system that will be running this in production has three Quadro K6000, 24 cores and 256GB RAM.

The approaches I’ve taken are as follows;

  1. A particle system based on this tutorial. This approach is brilliant for the global view and can easily handle a million data points on screen at a time. This includes rendering on a rotating globe. The big issue here is that due to a limitation, of the particle system or of my knowledge, zooming in causes the whole system to be occluded. As I understand it, this occurs when the centre of the particle system is off camera for performance purposes.
  2. An array of GameObject with a simple mesh, instantiated in Start() and moved into position as needed. This allows me to render 45,000 or so objects and zoom in at will as they each have a rendered of their own, the massive drawback to this approach is that transformations need to be done on the main thread and as such performance drops to around 13fps making the whole thing jerky.

Personally I love the particle system approach as performance is brilliant and can handle up to a million particles without breaking a sweat. If I could solve the occlusion issue it would be ideal. From the past few weeks of forum trawling it seems to have been an issue since the system was introduced and not an easy one to fix. It certainly doesn’t help that I’m not using the particle system for it’s intended use I guess.

My question is, is there a best practice for handling large numbers of objects such as my requirement? I’m not sure if my approaches are off or my expectations but if someone could point me in the right direction I’d greatly appreciate it.

I think your current particle system issue can be figured out. But I was giving some though to how this would be done in a mesh (vs individual game object), and decided to write some test code. What I came up with is a class called ParticleDisplay. It has most of the standard particle properties:

  • color;
  • lifetime;
  • position;
  • rotation;
  • size;
  • velocity;
  • angularVelocity

There’s no emitter…just access to the array so the properties can be modified dynamically. Performance is around 10x faster than using game objects and roughly 1/2 the performance of a particle system with all particles displayed. I’m sure there are general performance improvements to be had in this code, but given your hardware, the best thing would be to rework it to use threads.

using UnityEngine;
using System.Collections;

public class ParticleDisplay : MonoBehaviour {
	public Camera camera;                     // Camera to use for rotation
	public int maxParticles = 1000;
	public bool dynamicColor = false;         // Use color defined in particle
	public Material material;                 // Material for particles
	public float defaultParticleSize = 0.1f;
	public Color defaultParticleColor = Color.white;

	public struct Particle {
		public float angularVelocity;
		public Color color;
		public float lifetime;
		public Vector3 position;
		public float rotation;
		public float size;
		public Vector3 velocity;
	}

	public Particle[] particles;

	private const int quadsPerMesh = 16380;

	private GameObject[] goMeshes;
	private Mesh[] meshes;
	private bool isRunning = false;

	private Transform camTrans;

	void Awake() {
		if (camera == null) 
			camera = Camera.main;

		if (camera != null)
			camTrans = camera.transform;

		CreateMeshes();
	}

	void CreateMeshes() {
		if (maxParticles <= 0) return;
		
		particles = new Particle[maxParticles];
		for (int i = 0; i < particles.Length; i++) {
			particles*.size = defaultParticleSize;*

_ particles*.color = defaultParticleColor;_
_
}*_

* int meshCount = maxParticles / quadsPerMesh + 1;*
* int quadsLastMesh = maxParticles % quadsPerMesh;*

* goMeshes = new GameObject[meshCount];*
* meshes = new Mesh[meshCount];*

* for (int i = 0; i < meshCount; i++) {*
* GameObject go = new GameObject();*
* go.transform.parent = transform;*
* MeshFilter mf = go.AddComponent();*
* Mesh mesh = new Mesh();*
* mesh.MarkDynamic ();*
* mf.mesh = mesh;*
* Renderer rend = go.AddComponent();*
* rend.material = material;*

* Vector3[] vertices;*
* if (i != meshCount - 1) {*
_ vertices = new Vector3[4 * quadsPerMesh];
* }*
* else {*
vertices = new Vector3[4 * quadsLastMesh];_

* }*

* mesh.vertices = vertices;*

_ int triangles = new int[mesh.vertices.Length / 2 * 3];_

* for (int j = 0; j < vertices.Length / 4; j++) {*

triangles[j * 6 + 0] = j * 4 + 0; // 0_ 3 0 ___ 3
_ triangles[j * 6 + 1] = j * 4 + 3; // | / | /|_
triangles[j * 6 + 2] = j * 4 + 1; // 1|/ 1|/__|2

_ triangles[j * 6 + 3] = j * 4 + 3; // 3_
_ triangles[j * 6 + 4] = j * 4 + 2; // /|
triangles[j * 6 + 5] = j * 4 + 1; // 1/|2

* }*

* mesh.triangles = triangles;*
* Color[] colors = new Color[mesh.vertices.Length];*

* for (int j = 0; j < colors.Length; j++) {*
* colors[j] = defaultParticleColor;*
* }*

* mesh.colors = colors;*

* Vector2[] uvs = new Vector2[mesh.vertices.Length];*
* for (int j = 0; j < vertices.Length / 4; j++) {*
_ uvs[j * 4 + 0] = new Vector2(0,1);
uvs[j * 4 + 1] = new Vector2(0,0);
uvs[j * 4 + 2] = new Vector2(1,0);
uvs[j * 4 + 3] = new Vector2(1,1);_

* }*

* mesh.uv = uvs;*

_ goMeshes = go;
meshes = mesh;
* }
}*_

* public void Clear () {*
* for (int i = 0; i < particles.Length; i++)*
_ particles*.lifetime = -1.0f;
}*_

* public void Play() {*
* isRunning = true;*
* foreach (GameObject go in goMeshes) go.renderer.enabled = true;*
* }*

* public void Stop () {*
* isRunning = false;*
* foreach (GameObject go in goMeshes) go.renderer.enabled = false;*
* }*

* void LateUpdate() {*
* if (!isRunning) return;*

* Vector3 fwd = camTrans.forward;*
* Quaternion qCam = camTrans.rotation;*

* for (int i = 0; i < meshes.Length; i++) {*
_ Mesh mesh = meshes*;
Vector3 vertices = mesh.vertices;
Color colors = null;
if (dynamicColor) {
colors = mesh.colors;
}
for (int j = 0; j < vertices.Length / 4; j++) {*

Particle particle = particles[i * quadsPerMesh + j];_

* if (particle.lifetime < 0) {*
_ vertices[j * 4] = Vector3.zero;
vertices[j * 4 + 1] = Vector3.zero;
vertices[j * 4 + 2] = Vector3.zero;
vertices[j * 4 + 3] = Vector3.zero;
* }
else {
float l = particle.size / 2.0f;
Vector3 v0 = new Vector3(-l, l, 0);
Vector3 v1 = new Vector3(-l,-l, 0);
Vector3 v2 = new Vector3( l,-l, 0);
Vector3 v3 = new Vector3( l, l, 0);*_

_ Quaternion q = Quaternion.AngleAxis(particle.rotation, fwd) * qCam;_

_ vertices[j * 4] = particle.position + q * v0;
vertices[j * 4 + 1] = particle.position + q * v1;
vertices[j * 4 + 2] = particle.position + q * v2;
vertices[j * 4 + 3] = particle.position + q * v3;_

* if (dynamicColor) {*
_ colors[j * 4] = particle.color;
colors[j * 4 + 1] = particle.color;
colors[j * 4 + 2] = particle.color;
colors[j * 4 + 3] = particle.color;
* }*_

* particle.lifetime -= Time.deltaTime;*
_ particle.rotation += particle.angularVelocity * Time.deltaTime;
particle.position += particle.velocity * Time.deltaTime;_

_ particles[i * quadsPerMesh + j] = particle;
* }
}
mesh.vertices = vertices;*_

* if (dynamicColor) {*
* mesh.colors = colors;*
* }*
* }*
* }*
}
And here is simple class as an example of how to drive the ParticleDisplay class:
using UnityEngine;
using System.Collections;

public class ParticleDisplayTest : MonoBehaviour {

* private ParticleDisplay pd;*
* private ParticleDisplay.Particle[] pdp;*

* void Start () {*

* pd = GetComponent();*
* pdp = pd.particles;*

* for (int i = 0; i < pdp.Length; i++) {*
* Particle p;*
_ pdp*.velocity = Random.insideUnitSphere;
pdp.angularVelocity = Random.Range (0.0f, 180.0f);
pdp.lifetime = Mathf.Infinity;
pdp.color = new Color(Random.Range (0.0f, 1.0f), Random.Range (0.0f, 1.0f), Random.Range (0.0f, 1.0f));
}*_

* pd.Play();*
* }*

* void Update () {*
* for (int i = 0; i < pdp.Length; i++) {*
_ if (pdp*.position.magnitude > 8.0f) {
pdp.velocity = -pdp.velocity;
pdp.position = pdp.position.normalized * 7.999f;
pdp.color.r = (pdp.color.r + Time.deltaTime * 0.5f) % 1.0f;
}
}
}
}*

There’s no default material, so you’ll need to create one. I used Particles/Additive as the shader, and a snowflake with transparency for the test image._