Custom particle rendering

Obi provides a flexible particle/fluid rendering pipeline. While the default renderers might be enough for most users, some of you will want to customize the look of your game or fine tune rendering performance in a specific device. Particles can either be rendered on their own, or be used as a basis to render fluid. Because of this, we will divide this section in two parts: particle rendering, and fluid rendering.

Particle rendering

By adding a ObiParticleRenderer component to any ObiActor, its particles will be rendered each frame. To be able to efficiently render thousands of particles, ObiParticleRenderer uses impostors: camera-facing squares, made of only two triangles each, shaded so that they look like 3D spheres.

Impostor rendering works like this: every frame, for each particle in the actor, ObiParticleRenderer generates a mesh made up of 4 vertices per particle. All four vertices are collapsed in the center of the particle. The vertex shader used for rendering this mesh is responsible for expanding these 4 vertices into a camera-facing quad. The fragment shader then discards all pixels that lie outside of the sphere silhouette, and calculates lightning and depth correction for the remaining ones.

Obi comes with two built-in shaders for impostor rendering:

ParticleShader.shader / ParticleShaderURP.shader

Renders perspetive-correct ellipsoids, with depth correction, lit using probe ambient lighting and one directional light. Each particle will look like an ellipsoid (squashed sphere) and will intersect correctly with the scene and other particles. This is the default shader used by ObiParticleRenderer.

Stretched particles due to perspective distorion.
Correct intersections.

SimpleParticleShader.shader / SimpleParticleShaderURP.shader

Renders camera-facing spheres, without depth correction, lit using constant color ambient lighting and one directional light. Each particle will look like a shaded circle regardless of the amount of perspective distortion of the camera or particle anisotropy. Intersections with the scene or other particles will be calculated using the planar quad geometry. Prefer this shader over the full particle shader when targeting mobile platforms.

Particles have circular silhouettes, regardless of perspective.
Planar intersections.

In order to be able to generate a quad for each group of 4 vertices and lit each particle correctly, additional vertex data is passed to the shader:

Corner
A float3 offset for each corner of the quad. The four possible values are: (1,1,0) (-1,1,0) (-1,-1,0) and (1,-1,0). This offset is generally scaled by the particle radius and added to the initial position of the vertices, to expand them into a quad.
Color
Per-vertex particle diffuse color.
t0
First anisotropy basis vector. The first three components contain a normalized principal axis (local X direction), the last component contains its length.
t1
Second anisotropy basis vector. The first three components contain a normalized principal axis (local Y direction), the last component contains its length.
t2
Third anisotropy basis vector. The first three components contain a normalized principal axis (local Z direction), the last component contains its length.

When writing custom shaders for particle rendering, Obi offers you two libraries for ellipsoid and sphere impostor rendering: ObiEllipsoids.cginc and ObiParticles.cginc, respectively. They provide methods to calculate vertex positions and shading for both standard and simple particle rendering. Please refer to ParticleShader.shader and SimpleParticleShader.shader to see how these libraries should be used.

Fluid rendering

Fluid surface rendering in Obi is done using impostor splatting: particles are drawn to full-screen texture buffers using the mesh generated by ObiParticleRenderer. The resulting textures are processed using special shaders to smooth out, threshold, and lit the fluid surface. This process makes use of Unity's CommandBuffers.

When writing your own fluid renderer, you should create a new class derived from ObiBaseFluidRenderer. This abstract class provides you a basic framework for rendering using your own shaders. The protected "renderFluid" variable is the command buffer that you should fill, and "particleRenderers" the list of renderers you should draw into the fluid buffers. There are three methods that you should implement in your renderer:

Setup
Lazy initialization of the required materials/shaders/data needed by the renderer.
Cleanup
Should clean up any initialization performed in Setup().
UpdateFluidRenderingCommandBuffer()
Should clear the command buffer and set it up to render the fluid.

Here's a basic skeleton showing how a fluid renderer should roughly look like. Please refer to Unity's documentation for detailed info on how to write shaders, use render targets and command buffers. Also look at ObiFluidRenderer.cs and ObiSimpleFluidRenderer.cs for reference.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Rendering;

namespace Obi
{
	public class ObiTestFluidRenderer : ObiBaseFluidRenderer
	{

		private Material customMaterial;

		protected override void Setup(){

			if (customMaterial == null)
			{
				customMaterial = CreateMaterial(Shader.Find("MyCustomShader"));
			}

			bool shadersSupported = customMaterial;

			if (!shadersSupported || !SystemInfo.supportsImageEffects)
			{
				enabled = false;
				Debug.LogWarning("Obi Test Fluid Renderer not supported in this platform.");
				return;
			}

		}

		protected override void Cleanup()
		{
			if (customMaterial != null)
				Object.DestroyImmediate (customMaterial);
		}

		public override void UpdateFluidRenderingCommandBuffer()
		{

			renderFluid.Clear();

			if (particleRenderers == null)
				return;

			// declare buffers:
			int buffer = Shader.PropertyToID("_Buffer");

			// get RTs:
			renderFluid.GetTemporaryRT(buffer,-2,-2,0,FilterMode.Bilinear);

			// prepare RTs:
			renderFluid.SetRenderTarget(buffer);
			renderFluid.ClearRenderTarget(true,true,Color.clear);

			// render particles to RT:
			foreach(ObiParticleRenderer renderer in particleRenderers){
				if (renderer != null){

				renderFluid.SetGlobalColor("_ParticleColor",renderer.particleColor);
				renderFluid.SetGlobalFloat("_RadiusScale",renderer.radiusScale);

					foreach(Mesh mesh in renderer.ParticleMeshes){
						renderFluid.DrawMesh(mesh,Matrix4x4.identity,customMaterial,0,0);
					}
				}
			}

			// Composite fluid buffer with screen contents:
			renderFluid.Blit(buffer,BuiltinRenderTextureType.CameraTarget);

		}

	}
}