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.
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:
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.
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.
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:
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 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:
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); } } }