Thursday, June 7, 2018

Opaque Active Camouflage Part 1

In this post I will show how to implement an active camouflage technique using the previous frame buffer similar to the effect used in Ghost Recon: Future Soldier.  This example will be done in Unity but the principles are the same for implementing it in any engine.

What is great about this technique is that you can apply it to any object in the scene and it will just sort of blend in with whats around it.  You don't actually see through the object so it also obscures whatever is behind it making it great for "invisibility cloaks" and what not.

The full repo with example assets can be found HERE

There are 4 main steps to this effect:
1: A command buffer for grabbing the frame buffer at the right time
2: A command buffer for drawing the active camo version of the objects over top of the original objects.
3: A script that tells objects they should be drawn with active camo.
4: The active camo shader itself.

1. Grabbing the frame Buffer


To grab the frame buffer we will use a command buffer on the main camera.  I'm making a new script called FrameGrabCommandBuffer
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class FrameGrabCommandBuffer : MonoBehaviour {

 private CommandBuffer rbFrame;
 [SerializeField]
 private CameraEvent rbFrameQueue = CameraEvent.AfterForwardAlpha;
Start by declaring a new Command Buffer and a new CameraEvent, I've serialized it so you can see the results of different events.  AfterForwardAlpha will cause this command buffer to execute after all the transparent stuff has drawn but before an canvas UI is drawn. Special in-world UI may need to go into it's own command buffer.
 public RenderTexture lastFrame;
 public RenderTexture lastFrameTemp;
 private RenderTargetIdentifier lastFrameRTI;
We need a render texture for the last frame, and a temporary texture in case the screen is resized and we need to make a new lastFrame texture.  Command buffers use RenderTargetIdentifier instead of the RenderTexture so we will need one for the last frame texture.
 private int screenX = 0;
 private int screenY = 0;
 private Camera thisCamera;
screenX and screenY store the current size of the camera and thisCamera is the camera the script is attached to.
 void OnEnable() {

  thisCamera = GetComponent<Camera> ();

  rbFrame = new CommandBuffer();
  rbFrame.name = "FrameCapture";
  thisCamera.AddCommandBuffer(rbFrameQueue, rbFrame);

  RebuildCBFrame ();

  Shader.SetGlobalFloat( "_GlobalActiveCamo", 1.0f );
 }
When this script is enabled we want to set thisCamera, initialize the command buffer and apply is to the camera.
RebuildCBFrame () is the function that will actually build the command buffer but an empty buffer can be applied to the camera and updated later.
Set a global shader value to let all the shaders know that the active camo is ready to go!
 void OnDisable() {

  if (rbFrame != null) {
   thisCamera.RemoveCommandBuffer(rbFrameQueue, rbFrame);
   rbFrame = null;
  }

  if (lastFrame != null) {
   lastFrame.Release();
   lastFrame = null;
  }

  Shader.SetGlobalFloat( "_GlobalActiveCamo", 0.0f );
 }
When the script is disabled we want to remove the command buffer from the camera and clean up the last frame texture to avoid memory leaks.  Also inform the shaders that there is no more active camo.  Next build the actual command buffer.
 RebuildCBFrame() {

  rbFrame.Clear ();
First clear it in case there are any instructions in it since this function may be called from time to time.
  if (lastFrame != null) {
   lastFrameTemp = RenderTexture.GetTemporary(lastFrame.width, lastFrame.height, 0, RenderTextureFormat.DefaultHDR);
   Graphics.Blit (lastFrame, lastFrameTemp);
   lastFrame.Release();
   lastFrame = null;
  }
If the last frame texture already exists that means the screen size has changed so we need to store the existing last frame in a temp texture to copy later.  Then release the last frame texture to free up its memory.
 screenX = thisCamera.pixelWidth;
  screenY = thisCamera.pixelHeight;
Store the current width and height of the camera, this is used to check it the camera has been resized later.
  lastFrame = new RenderTexture(screenX/2, screenY/2, 0, RenderTextureFormat.DefaultHDR);
  lastFrame.wrapMode = TextureWrapMode.Clamp;
  lastFrame.Create ();
  lastFrameRTI = new RenderTargetIdentifier(lastFrame);
Make a new render texture for the last frame.  Half of the screen resolution is enough for this effect. The wrap mode should be clamp so that when the texture is being distorted by the shader it won't pull in things from the other side of the screen.  Lastly create the render texture and get the render target identifier for it.
  if (lastFrameTemp != null) {
   Graphics.Blit (lastFrameTemp, lastFrame);
   RenderTexture.ReleaseTemporary (lastFrameTemp);
   lastFrameTemp = null;
  }
If the temp last frame texture exists that means we need to copy it to the new last frame texture we just made.  A standard Graphics.Blit will do.  Then release the temp texture and null it.
  Shader.SetGlobalTexture ("_LastFrame", lastFrame);
Inform all the shaders what texture they will be using for their active camo.
  RenderTargetIdentifier cameraTargetID = new RenderTargetIdentifier(BuiltinRenderTextureType.CameraTarget);
  rbFrame.Blit(cameraTargetID, lastFrameRTI);
 }
This is the actual command buffer instructions.  Get the render target identifier of the camera and blit it to the last frame render target identifier.  Pretty simple.
 void OnPreRender(){

  if (screenX != thisCamera.pixelWidth || screenY != thisCamera.pixelHeight) {
   RebuildCBFrame ();
  }
 }
}
Last but not least, before the camera renders, check to see if the screen size has changed.  If it has, rebuild the command buffer.

2. Drawing the Active Camo Objects


Now we want to set up the command buffer that will render the active camo objects. Make a new script called ActiveCamoCommandBuffer.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class ActiveCamoObject {
 public Renderer renderer;
 public Material material;
}
The first thing we need is a class that will hold the renderer that we want to draw and the material we want to use to draw it.
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class ActiveCamoCommandBuffer : MonoBehaviour {

 public static ActiveCamoCommandBuffer instance;

 private CommandBuffer rbDrawAC;
 [SerializeField]
 private CameraEvent rbDrawACQueue = CameraEvent.AfterForwardOpaque;

 private HashSet<ActiveCamoObject> acObjects = new HashSet<ActiveCamoObject>();
 private Camera thisCamera;
 private bool updateActiveCamoCB = false;
There needs to be a static instance of this script so that later on we can have each active camo object tell this script that it needs to be drawn. Also make a new command buffer and a new camera event. AfterForwardOpaque will happen after all of the opaque things have been drawn and before the transparent things get drawn. The hash set of active camo objects will be iterated over to draw each object. thisCamera is just hte camera the script is attached to. updateActiveCamoCB is the variable we will use to see if the command buffer needs to be rebuilt.
 void Awake(){
  ActiveCamoCommandBuffer.instance = this;
 }
The first thing that needs to happen is the instance needs to be set. Awake() is the first thing that gets called so it is an ideal place for setting instances.
 void OnEnable() {
  thisCamera = GetComponent<Camera> ();

  rbDrawAC = new CommandBuffer();
  rbDrawAC.name = "DrawActiveCamo";
  thisCamera.AddCommandBuffer(rbDrawACQueue, rbDrawAC);
  updateActiveCamoCB = true;
 }
When the script is enabled it should set the camera variable, create the command buffer, add it to the camera, and set the variable letting us know that the command buffer should be updated. The reason we don't rebuild the command buffer immediately is because something else may change that also requires a command buffer rebuild.
 void OnDisable() {
  if (rbDrawAC != null) {
   thisCamera.RemoveCommandBuffer(rbDrawACQueue, rbDrawAC);
   rbDrawAC = null;
  }
 }
When the script is disabled it should remove the command buffer from the camera.
 public void AddRenderer( ActiveCamoObject newObject ) {
  acObjects.Add (newObject);
  updateActiveCamoCB = true;
 }

 public void RemoveRenderer( ActiveCamoObject newObject ) {
  acObjects.Remove (newObject);
  updateActiveCamoCB = true;
 }
These two public functions will add/remove the ActiveCamoObject passed to them to/from the hash set of active camo objects. Whenever there is a change to the hash set the command buffer needs to be rebuilt.
 void RebuildCBActiveCamo(){
  rbDrawAC.Clear ();
  foreach( ActiveCamoObject acObject in acObjects ){
   rbDrawAC.DrawRenderer(acObject.renderer, acObject.material);
  }
  updateActiveCamoCB = false;
 }
This function actually rebuilds the command buffer. first clear the buffer and then draw each renderer with its material from the acObjects hash set. Final set updateActiveCamoCB to false.
 void OnPreRender(){
  if (updateActiveCamoCB) {
   RebuildCBActiveCamo ();
  }
 }
}
The last step is to check if the command buffer needs to be rebuilt and if so rebuild it.

3. Per Object Active Camo Script


Make a new script called ActiveCamoRenderer that will be applied to any game object with a Renderer component that should get active camo
using UnityEngine;

public class ActiveCamoRenderer : MonoBehaviour {

 private Renderer thisRenderer;
 [SerializeField]
 private Material ActiveCamoMaterial;
 private MaterialPropertyBlock MPB;
 private ActiveCamoObject acObject;
 [HideInInspector]
 public float ActiveCamoRamp = 0.0f;
We need a variable for the renderer, an exposed variable for the active camo material, a material property block that will control the active camo material, the ActiveCamoObject that will get sent to the command buffer script an a public float variable that will be used to control the active camo material from another controller script.
 void Start(){
  MPB = new MaterialPropertyBlock ();
  thisRenderer = GetComponent ();
  acObject = new ActiveCamoObject();
  acObject.renderer = thisRenderer;
  acObject.material = ActiveCamoMaterial;
 }
When the script starts it needs to initialize the property block, get the renderer, create the ActiveCamoObject and assign the renderer and material to it.
 void OnBecameVisible(){
  ActiveCamoCommandBuffer.instance.AddRenderer (acObject);
 }

 void OnBecameInvisible() {
  ActiveCamoCommandBuffer.instance.RemoveRenderer (acObject);
 }
OnBecomeVisible and OnBecomeInvisible are functions that get called by unity when that object is first visible on screen and when it is first no longer visible on screen respectively. When the object becomes visible we want to add the ActiveCamoObject to the ActiveCamoCommandBuffer, and remove it when it becomes invisible.
 void Update () {
  MPB.SetFloat ("_ActiveCamoRamp", ActiveCamoRamp);
  thisRenderer.SetPropertyBlock (MPB);
 }
}
Each frame set the _ActiveCamoRamp shader variable on the material property block and then apply the block to the renderer. The MaterialPropertyBlock lets us use the same material for multiple objects but still control the material on a per renderer basis.
The last thing we need is a script that we can drop onto a character and control all the active camo objects on that character at once. So make a new script called ActiveCamoController.
using UnityEngine;

public class ActiveCamoController : MonoBehaviour {

 [SerializeField]
 private ActiveCamoRenderer[] activeCamoRenderers;

 [SerializeField]
 [Range (0f,1f)]
 private float ActiveCamoRamp = 0.0f;
 
 // Update is called once per frame
 void Update () {
  for (int i = 0; i < activeCamoRenderers.Length; i++) {
   activeCamoRenderers [i].ActiveCamoRamp = ActiveCamoRamp;
  }
 }
}
This script just loops over all ActiveCamoRenderers and changes their ActiveCamoRamp variable.

4. The Active Camo Shader


This shader will use a texture to add some random flow to the the active camo. This distortion texture is just 256x256 Photoshop clouds. Import settings should have compression set to none (because it is a small effects texture it is important that it be free of compression artifacts) and sRGB sampling unchecked (because it will be used for distorting texture coordinates).

Start by creating a new shader and calling it ActiveCamoUnlitSimple. This shader will be a lot simpler than the one on display but I will make another post about the full shader for part 2.
Shader "Unlit/ActiveCamoUnlitSimple"
{
 Properties
 {
  _DistortTex ("Distortion", 2D) = "grey" {}
  _DistortTexTiling ("Distortion Tiling", Vector) = (1,1,0,0)
  _DistortAmount ("Distortion Amount", Range(0,1)) = 0.1
  _VertDistortAmount ("Vert Distortion Amount", Range(0,1)) = 0.1
 }
These are the the properties that will be used. we'll need a distortion texture, Vector parameter for tiling x and y and scrolling x and y, a float parameter to control the amount of distortion, and a float parameter to control the amount of pull the shader does one the surrounding environment.
 SubShader
 {
  Tags { "RenderType"="Transparent" "Queue"="Transparent" }
  LOD 100
The RenderType should be Transparent. The Queue is not so important since a command buffer will be drawing this at a specific point in the rendering pipeline. The LOD is unchanged from its initialization.
  Pass
  {
   Offset -1,-1
   Blend One OneMinusSrcAlpha
Offset -1,-1 will make sure that there will be no z-fighting with the objects it is supposed to be drawing on top of. Blend One OneMinusSrcAlpha is pre-multiplied alpha blending and will provide similar results between HDR and non HDR rendering.
   CGPROGRAM
   #pragma vertex vert
   #pragma fragment frag

   #include "UnityCG.cginc"
Just telling the shader what the vertex program and the fragment program will be and including some base Unity shader functions
   sampler2D _DistortTex;
   float4 _DistortTexTiling;
   float _DistortAmount;
   float _VertDistortAmount;

   // per instance variables
   float _ActiveCamoRamp;

   // global variables
   sampler2D _LastFrame;
   float _GlobalActiveCamo;
Here are all the variables the shader will use. The first 4 are controlled by the properties in the material. _ActiveCamoRamp is passed in with the MaterialPropertyBlock from the ActiveCamoRenderer script. _LastFrame and _GlobalActiveCamo are defined globally by the FrameGrabCommandBuffer script.
   struct v2f
   {
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD0;
    float4 screenPos: TEXCOORD1;
    float2 screenNormal : TEXCOORD2;
   };
v2f is the data structure that will get passed from the vertex shader to the pixel (fragment) shader. We need the position, the uv coords, the screen position, and the x and y values of the screen normal.
   v2f vert (appdata_full v)
   {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.texcoord;
    o.screenPos = ComputeScreenPos(o.vertex);
    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    o.screenNormal = mul( (float3x3)UNITY_MATRIX_V, worldNormal ).xy;

    return o;
   }
This is the vertex shader. o.vertex, o.uv, and o.screenPos and worldNormal are pretty common in many vertex shaders so I won't go into them. To get the screen normal we have to multiply the world normal by the camera view matrix.
   fixed4 frag (v2f IN) : SV_Target
   {

    // get the distortion for the prevous frame coords
    half2 distortion = tex2D (_DistortTex, IN.uv.xy * _DistortTexTiling.xy + _Time.yy * _DistortTexTiling.zw ).xy;
    distortion -= tex2D (_DistortTex, IN.uv.xy * _DistortTexTiling.xy + _Time.yy * _DistortTexTiling.wz ).yz;
The start of the pixel shader. Get the red and green channel of distortion texture. Then subtract the green and blue channel of another distortion texture with swizzled scrolling values so it scrolls a different direction. This produces a distortion value that ranges from -1 to 1.
    // get the last frame to use as camo
    float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
    screenUV += distortion * _DistortAmount * 0.1;
    screenUV += IN.screenNormal * _VertDistortAmount * 0.1;
    half3 lastFrame = tex2D (_LastFrame, screenUV).xyz;
Get the screen uv for the last frame texture and add the distortion texture multiplied by the distortion amount at 1/10th. A little goes a long way when distorting texture coordinates. Then add the screen normal multiplied by the vert distort amount at 1/10th. This is what will pull the surroundings onto the camo shader. Finaly sample the last frame texture with the screen uv.
    // the final amound of active camo to apply
    half activeCamo = _ActiveCamoRamp * _GlobalActiveCamo;

    // premultiplied alpha camo
    half4 final = half4( lastFrame * activeCamo, activeCamo);
    final.w = saturate( final.w);

    return final;
   }
   ENDCG
  }
 }
}
Multiply the per instance _ActiveCamoRamp variable and the _GlobalActiveCamo variable together to form the alpha value for the shader. To pre-multiply the alpha just multiply the final color by the alpha before returning it.

Setting it all up


Make a material for each object you want to apply camo to. You don't need unique material but this allows you to have different tiling and distortion values.
Apply the ActiveCamoRenderer script to each of the objects you want to have active camo on and apply to active camo material you made to them.
Now add the ActiveCamoController script to the main object and drag all the active camo objects into the Active camo renderers array.
The last thing to do is add the ActiveCamoCommandBuffer script and the FrameGrabCommandBuffer script to the main camera.
Now press play and drag the slider on the control script to make things vanish!