In order to safely write custom code to emit/kill/control/modify advected particles, you must either:
Data for the advected particles is stored in 4 arrays in the ObiSolver component:
These arrays are always ObiSolver.maxFoamParticles in size. However, not all particles in them are alive and require processing. There's an extra foamCount array that holds the amount of alive foam particles in its fourth entry (foamCount[3]).
You can get a NativeArray that wraps around the data in these arrays by calling AsNativeArray<T>() on them. And when using the Compute backend, you can get their data on a GraphicsBuffer by accesing their computeBuffer public variable.
We will now go over 3 different approaches to modifying advected particle data: in the CPU using raw C#, in the CPU using Burst/Jobs, and in the GPU using compute shaders. To clearly showcase the differences between all methods, our goal will be the same in all 3 examples: kill spray particles by making particles age faster (and die sooner) when they have few fluid neighbors.
This is by far the slowest method (only recommended if you're dealing with a modest amount of advected particles), but also the easiest if you're unfamiliar with Burst/Jobs multithreading or compute shaders.
using UnityEngine; using Obi; [RequireComponent(typeof(ObiSolver))] public class KillBallisticFoamParticles : MonoBehaviour { void OnEnable() { GetComponent<ObiSolver>().OnAdvection += KillBallisticFoamParticles_OnAdvection; } void OnDisable() { GetComponent<ObiSolver>().OnAdvection -= KillBallisticFoamParticles_OnAdvection; } private void KillBallisticFoamParticles_OnAdvection(ObiSolver solver) { // iterate over all advected particles for (int i = 0; i < solver.foamCount[3]; ++i) { // if the particle has fewer than 8 fluid neighbors: if (solver.foamPositions[i].w < 8) { Vector4 attrib = solver.foamAttributes[i]; // age at a rate of ten times its lifetime in 1 second. attrib.y = 10; solver.foamAttributes[i] = attrib; } } } }
By calling AsNativeArray<T>() on the advected particle data lists, we can pass them to a job to take advantage of multithreading.
using UnityEngine; using Obi; using Unity.Burst; using Unity.Jobs; using Unity.Collections; using Unity.Mathematics; [RequireComponent(typeof(ObiSolver))] public class KillBallisticFoamParticles : MonoBehaviour { void OnEnable() { GetComponent<ObiSolver>().OnAdvection += KillBallisticFoamParticles_OnAdvection; } void OnDisable() { GetComponent<ObiSolver>().OnAdvection -= KillBallisticFoamParticles_OnAdvection; } private void KillBallisticFoamParticles_OnAdvection(ObiSolver solver) { var job = new KillBallisticJob { foamPositions = solver.foamPositions.AsNativeArray<float4>(), foamAttributes = solver.foamAttributes.AsNativeArray<float4>() }; job.Schedule(solver.foamCount[3], 64).Complete(); } [BurstCompile] struct KillBallisticJob : IJobParallelFor { [ReadOnly] public NativeArray<float4> foamPositions; public NativeArray<float4> foamAttributes; public void Execute(int i) { // if the particle has less than 8 fluid neighbors: if (foamPositions[i].w < 8) { float4 attrib = foamAttributes[i]; // age at a rate of ten times its lifetime in 1 second. attrib.y = 10; foamAttributes[i] = attrib; } } } }
When using the Compute backend, this is the fastest way to modify advected particles as it doesn't require reading data back to the CPU. Note how the code subscribes to OnSimulationEnd, instead of OnAdvection (which would trigger a readback).
using UnityEngine; using Obi; [RequireComponent(typeof(ObiSolver))] public class KillBallisticFoamParticles : MonoBehaviour { ComputeShader shader; void Awake() { shader = Instantiate(Resources.Load<ComputeShader>("KillBallistic")); } void OnEnable() { GetComponent<ObiSolver>().OnSimulationEnd += KillBallisticFoamParticles_OnSimulationEnd; } void OnDisable() { GetComponent<ObiSolver>().OnSimulationEnd -= KillBallisticFoamParticles_OnSimulationEnd; } private void KillBallisticFoamParticles_OnSimulationEnd(ObiSolver solver, float simulatedTime, float substepTime) { if (solver.backendType == ObiSolver.BackendType.Compute) { shader.SetBuffer(0, "positions", solver.foamPositions.computeBuffer); shader.SetBuffer(0, "attributes", solver.foamAttributes.computeBuffer); shader.SetBuffer(0, "foamCount", solver.foamCount.computeBuffer); shader.DispatchIndirect(0, solver.foamCount.computeBuffer,0); } } }
And the corresponding compute shader, that must be placed in a /Resources folder:
#pragma kernel CSMain StructuredBuffer<float4> positions; RWStructuredBuffer<float4> attributes; StructuredBuffer<int> foamCount; [numthreads(128,1,1)] void CSMain (uint3 id : SV_DispatchThreadID) { if((int)id.x >= foamCount[3]) return; // if the particle has less than 8 fluid neighbors: if (positions[id.x].w < 8) { float4 attrib = attributes[id.x]; // die at a rate of ten times its lifetime in 1 second. attrib.y = 10; attributes[id.x] = attrib; } }