
#pragma strict

@script ExecuteInEditMode
@script RequireComponent (Camera)
@script AddComponentMenu ("Image Effects/Depth of Field (3.4)") 

enum Dof34QualitySetting {
	OnlyBackground = 1,
	BackgroundAndForeground = 2,
}

enum DofResolution {
	High = 2,
	Medium = 3,
	Low = 4,	
}

enum DofBlurriness {
	Low = 1,	
	High = 2,
	VeryHigh = 4,
}

enum BokehDestination {
	Background = 0x1,	
	Foreground = 0x2,
	BackgroundAndForeground = 0x3,
}

class DepthOfField34 extends PostEffectsBase {

	static private var SMOOTH_DOWNSAMPLE_PASS : int = 6;
	static private var BOKEH_EXTRA_BLUR : float = 2.0f;
	
	public var quality : Dof34QualitySetting = Dof34QualitySetting.OnlyBackground;
	public var resolution : DofResolution  = DofResolution.Low;
	public var simpleTweakMode : boolean = true;
	
	public var focalPoint : float = 1.0f;
	public var smoothness : float = 0.5f;
	
	public var focalZDistance : float = 0.0f;
	public var focalZStartCurve : float = 1.0f;
	public var focalZEndCurve : float = 1.0f;
	
	private var focalStartCurve : float = 2.0f;
	private var focalEndCurve : float = 2.0f;
	private var focalDistance01 : float = 0.1f;
		
	public var objectFocus : Transform = null;
	public var focalSize : float = 0.0f;
	
	public var bluriness : DofBlurriness = DofBlurriness.High;
	public var maxBlurSpread : float = 1.75f;
	
	public var foregroundBlurExtrude : float = 1.15f;
			
	public var dofBlurShader : Shader;
	private var dofBlurMaterial : Material = null;	
	
	public var dofShader : Shader;
	private var dofMaterial : Material = null;
    
    public var visualize : boolean = false;
    public var bokehDestination : BokehDestination = BokehDestination.Background;
    
    private var widthOverHeight : float = 1.25f;
    private var oneOverBaseSize : float = 1.0f / 512.0f;	
        
    public var bokeh : boolean = false;
    public var bokehSupport : boolean = true;
    public var bokehShader : Shader;
    public var bokehTexture : Texture2D;
    public var bokehScale : float = 2.4f;
    public var bokehIntensity : float = 0.15f;
    public var bokehThreshholdContrast : float = 0.1f;
    public var bokehThreshholdLuminance : float = 0.55f;
    public var bokehDownsample : int = 1;
    private var bokehMaterial : Material;
	
	function CreateMaterials () {		
		dofBlurMaterial = CheckShaderAndCreateMaterial (dofBlurShader, dofBlurMaterial);
		dofMaterial = CheckShaderAndCreateMaterial (dofShader,dofMaterial);  
		bokehSupport = bokehShader.isSupported;     

		if(bokeh && bokehSupport && bokehShader) 
			bokehMaterial = CheckShaderAndCreateMaterial (bokehShader, bokehMaterial);
	}
	
	function CheckResources () : boolean {		
		CheckSupport (true);
	
		dofBlurMaterial = CheckShaderAndCreateMaterial (dofBlurShader, dofBlurMaterial);
		dofMaterial = CheckShaderAndCreateMaterial (dofShader,dofMaterial);  
		bokehSupport = bokehShader.isSupported;  
				
		if(bokeh && bokehSupport && bokehShader) 
			bokehMaterial = CheckShaderAndCreateMaterial (bokehShader, bokehMaterial);
					
		if(!isSupported)
			ReportAutoDisable ();
		return isSupported;		  
	}

	function OnDisable () {
		Quads.Cleanup ();	
		
		if (dofBlurMaterial)
		    DestroyImmediate(dofBlurMaterial);
		if (dofMaterial)
		    DestroyImmediate(dofMaterial);
		if (bokehMaterial)
		    DestroyImmediate(bokehMaterial);		
	}

	function OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;		
	}
	
	function FocalDistance01 (worldDist : float) : float {
		return camera.WorldToViewportPoint((worldDist-camera.nearClipPlane) * camera.transform.forward + camera.transform.position).z / (camera.farClipPlane-camera.nearClipPlane);	
	}
	
	function GetDividerBasedOnQuality () {
		var divider : int = 1;
		if (resolution == DofResolution.Medium)
			divider = 2;
		else if (resolution == DofResolution.Low)
			divider = 2;	
		return divider;	
	}
	
	function GetLowResolutionDividerBasedOnQuality (baseDivider : int) {
        var lowTexDivider : int = baseDivider;
        if (resolution == DofResolution.High)	
        	lowTexDivider *= 2;   
        if (resolution == DofResolution.Low)	
        	lowTexDivider *= 2; 	
        return lowTexDivider;	
	}
	
	private var foregroundTexture : RenderTexture = null; 
	private var mediumRezWorkTexture : RenderTexture = null;
	private var finalDefocus : RenderTexture = null;
	private var lowRezWorkTexture : RenderTexture = null;
	private var bokehSource : RenderTexture = null;
	private var bokehSource2 : RenderTexture = null;
	
	function OnRenderImage (source : RenderTexture, destination : RenderTexture) {	
		if(CheckResources()==false) {
			Graphics.Blit (source, destination);
			return;
		}	
		
		if (smoothness < 0.1f)
			smoothness = 0.1f;
		
		// update needed focal & rt size parameter
		
		bokeh = bokeh && bokehSupport;
		var bokehBlurAmplifier : float = bokeh ? BOKEH_EXTRA_BLUR : 1.0f;

		var blurForeground : boolean = quality > Dof34QualitySetting.OnlyBackground;	
		var focal01Size : float = focalSize / (camera.farClipPlane - camera.nearClipPlane);;

		if (simpleTweakMode) {		
			focalDistance01 = objectFocus ? (camera.WorldToViewportPoint (objectFocus.position)).z / (camera.farClipPlane) : FocalDistance01 (focalPoint);
			focalStartCurve = focalDistance01 * smoothness;
			focalEndCurve = focalStartCurve;
			blurForeground = blurForeground && (focalPoint > (camera.nearClipPlane + Mathf.Epsilon));
		} 
		else {
			if(objectFocus) {
				var vpPoint = camera.WorldToViewportPoint (objectFocus.position);
				vpPoint.z = (vpPoint.z) / (camera.farClipPlane);
				focalDistance01 = vpPoint.z;			
			} 
			else 
				focalDistance01 = FocalDistance01 (focalZDistance);	
			
			focalStartCurve = focalZStartCurve;
			focalEndCurve = focalZEndCurve;
			blurForeground = blurForeground && (focalPoint > (camera.nearClipPlane + Mathf.Epsilon));				
		}
		
		widthOverHeight = (1.0f * source.width) / (1.0f * source.height);
		oneOverBaseSize = 1.0f / 512.0f;		
        		
		dofMaterial.SetFloat ("_ForegroundBlurExtrude", foregroundBlurExtrude);
		dofMaterial.SetVector ("_CurveParams", Vector4 (simpleTweakMode ? 1.0f / focalStartCurve : focalStartCurve, simpleTweakMode ? 1.0f / focalEndCurve : focalEndCurve, focal01Size * 0.5, focalDistance01));
		dofMaterial.SetVector ("_InvRenderTargetSize", Vector4 (1.0 / (1.0 * source.width), 1.0 / (1.0 * source.height),0.0,0.0));
				
		var divider : int =  GetDividerBasedOnQuality ();
        var lowTexDivider : int = GetLowResolutionDividerBasedOnQuality (divider);
		
        AllocateTextures (blurForeground, source, divider, lowTexDivider);

		// WRITE COC to alpha channel		
		// source is only being bound to detect y texcoord flip
		Graphics.Blit (source, source, dofMaterial, 3); 
				
	    // better DOWNSAMPLE (could actually be weighted for higher quality)
		Downsample (source, mediumRezWorkTexture);	
		
       	// BLUR A LITTLE first, which has two purposes
       	// 1.) reduce jitter, noise, aliasing
       	// 2.) produce the little-blur buffer used in composition later     	     
		Blur (mediumRezWorkTexture, mediumRezWorkTexture, DofBlurriness.Low, 4, maxBlurSpread);			

		if (bokeh && (bokehDestination & BokehDestination.Background)) { 		
           	dofMaterial.SetVector ("_Threshhold", Vector4(bokehThreshholdContrast, bokehThreshholdLuminance, 0.95f, 0.0f));
				
			// add and mark the parts that should end up as bokeh shapes
			Graphics.Blit (mediumRezWorkTexture, bokehSource2, dofMaterial, 11);	
						
			// remove those parts (maybe even a little tittle bittle more) from the regurlarly blurred buffer		
			//Graphics.Blit (mediumRezWorkTexture, lowRezWorkTexture, dofMaterial, 10);
			Graphics.Blit (mediumRezWorkTexture, lowRezWorkTexture);//, dofMaterial, 10);
			
			// maybe you want to reblur the small blur ... but not really needed.
			//Blur (mediumRezWorkTexture, mediumRezWorkTexture, DofBlurriness.Low, 4, maxBlurSpread);						
			
			// bigger BLUR
			Blur (lowRezWorkTexture, lowRezWorkTexture, bluriness, 0, maxBlurSpread * bokehBlurAmplifier);				
		} 
		else  {
			// bigger BLUR
			Downsample (mediumRezWorkTexture, lowRezWorkTexture);			
			Blur (lowRezWorkTexture, lowRezWorkTexture, bluriness, 0, maxBlurSpread);	
		}	
       		
		dofBlurMaterial.SetTexture ("_TapLow", lowRezWorkTexture);
		dofBlurMaterial.SetTexture ("_TapMedium", mediumRezWorkTexture);							
		Graphics.Blit (null, finalDefocus, dofBlurMaterial, 3);	
		
		// we are only adding bokeh now if the background is the only part we have to deal with
		if (bokeh && (bokehDestination & BokehDestination.Background))
			AddBokeh (bokehSource2, bokehSource, finalDefocus);
		
		dofMaterial.SetTexture ("_TapLowBackground", finalDefocus); 
		dofMaterial.SetTexture ("_TapMedium", mediumRezWorkTexture); // needed for debugging/visualization
						
		// FINAL DEFOCUS (background)
		Graphics.Blit (source, blurForeground ? foregroundTexture : destination, dofMaterial, visualize ? 2 : 0); 
		
		// FINAL DEFOCUS (foreground)
		if (blurForeground) {			
			// WRITE COC to alpha channel			
			Graphics.Blit (foregroundTexture, source, dofMaterial, 5); 
			
	    	// DOWNSAMPLE (unweighted)
			Downsample (source, mediumRezWorkTexture);	
			
       		// BLUR A LITTLE first, which has two purposes
       		// 1.) reduce jitter, noise, aliasing
       		// 2.) produce the little-blur buffer used in composition later   
       		BlurFg (mediumRezWorkTexture, mediumRezWorkTexture, DofBlurriness.Low, 2, maxBlurSpread);	
       		
	 		if (bokeh && (bokehDestination & BokehDestination.Foreground)) { 
	           	dofMaterial.SetVector ("_Threshhold", Vector4(bokehThreshholdContrast * 0.5f, bokehThreshholdLuminance, 0.0f, 0.0f));
	 			
				// add and mark the parts that should end up as bokeh shapes
				Graphics.Blit (mediumRezWorkTexture, bokehSource2, dofMaterial, 11);	
				
				// remove the parts (maybe even a little tittle bittle more) that will end up in bokeh space			
				//Graphics.Blit (mediumRezWorkTexture, lowRezWorkTexture, dofMaterial, 10);
				Graphics.Blit (mediumRezWorkTexture, lowRezWorkTexture);//, dofMaterial, 10);
				
				// big BLUR		
				BlurFg (lowRezWorkTexture, lowRezWorkTexture, bluriness, 1, maxBlurSpread * bokehBlurAmplifier);				
			} 
			else  {      		
       			// big BLUR		
				BlurFg (mediumRezWorkTexture, lowRezWorkTexture, bluriness, 1, maxBlurSpread);	
			}
					
			// simple upsample once						
			Graphics.Blit (lowRezWorkTexture, finalDefocus);	

			dofMaterial.SetTexture ("_TapLowForeground", finalDefocus);	
			Graphics.Blit (source, destination, dofMaterial, visualize ? 1 : 4);

			if (bokeh && (bokehDestination & BokehDestination.Foreground))
				AddBokeh (bokehSource2, bokehSource, destination);							
		}					
		
		ReleaseTextures ();
	}
	
	function Blur (from : RenderTexture, to : RenderTexture, iterations : DofBlurriness, blurPass: int, spread : float) {
		var tmp : RenderTexture = RenderTexture.GetTemporary (to.width, to.height);	
		if (iterations > 1) {
			BlurHex (from, to, blurPass, spread, tmp);	
			if (iterations > 2) {
				dofBlurMaterial.SetVector ("offsets", Vector4 (0.0, spread * oneOverBaseSize, 0.0, 0.0));
				Graphics.Blit (to, tmp, dofBlurMaterial, blurPass);
				dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  0.0, 0.0, 0.0));		
				Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 	
			}		
		}
		else {
			dofBlurMaterial.SetVector ("offsets", Vector4 (0.0, spread * oneOverBaseSize, 0.0, 0.0));
			Graphics.Blit (from, tmp, dofBlurMaterial, blurPass);
			dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  0.0, 0.0, 0.0));		
			Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 
		}	
		RenderTexture.ReleaseTemporary (tmp);
	}

	function BlurFg (from : RenderTexture, to : RenderTexture, iterations : DofBlurriness, blurPass: int, spread : float) {
		// we want a nice, big coc, hence we need to tap once from this (higher resolution) texture
		dofBlurMaterial.SetTexture ("_TapHigh", from);
		
		var tmp : RenderTexture = RenderTexture.GetTemporary (to.width, to.height);	
		if (iterations > 1) {
			BlurHex (from, to, blurPass, spread, tmp);	
			if (iterations > 2) {
				dofBlurMaterial.SetVector ("offsets", Vector4 (0.0, spread * oneOverBaseSize, 0.0, 0.0));
				Graphics.Blit (to, tmp, dofBlurMaterial, blurPass);
				dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  0.0, 0.0, 0.0));		
				Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 	
			}		
		}
		else {
			dofBlurMaterial.SetVector ("offsets", Vector4 (0.0, spread * oneOverBaseSize, 0.0, 0.0));
			Graphics.Blit (from, tmp, dofBlurMaterial, blurPass);
			dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  0.0, 0.0, 0.0));		
			Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 
		}	
		RenderTexture.ReleaseTemporary (tmp);
	}

	function BlurHex (from : RenderTexture, to : RenderTexture, blurPass: int, spread : float, tmp : RenderTexture) {		
		dofBlurMaterial.SetVector ("offsets", Vector4 (0.0, spread * oneOverBaseSize, 0.0, 0.0));
		Graphics.Blit (from, tmp, dofBlurMaterial, blurPass);
		dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  0.0, 0.0, 0.0));		
		Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 
		dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  spread * oneOverBaseSize, 0.0, 0.0));		
		Graphics.Blit (to, tmp, dofBlurMaterial, blurPass);	 
		dofBlurMaterial.SetVector ("offsets", Vector4 (spread / widthOverHeight * oneOverBaseSize,  -spread * oneOverBaseSize, 0.0, 0.0));		
		Graphics.Blit (tmp, to, dofBlurMaterial, blurPass);	 
	}
	
	function Downsample (from : RenderTexture, to : RenderTexture) {
		dofMaterial.SetVector ("_InvRenderTargetSize", Vector4 (1.0f / (1.0f * to.width), 1.0f / (1.0f * to.height), 0.0f, 0.0f));
		Graphics.Blit (from, to, dofMaterial, SMOOTH_DOWNSAMPLE_PASS);				
	}

	function AddBokeh (bokehInfo : RenderTexture, tempTex : RenderTexture, finalTarget : RenderTexture) {
		if (bokehMaterial) {
			var meshes : Mesh[] = Quads.GetMeshes (tempTex.width, tempTex.height);	// quads: exchanging more triangles with less overdraw			
			    
			RenderTexture.active = tempTex;
        	GL.Clear (false, true, Color (0.0f, 0.0f, 0.0f, 0.0f));	    
			    
			GL.PushMatrix ();
			GL.LoadIdentity ();			
			
			// point filter mode is important, otherwise we get bokeh shape & size artefacts
			bokehInfo.filterMode = FilterMode.Point;

			var arW : float = (bokehInfo.width * 1.0f) / (bokehInfo.height * 1.0f);			
			var sc : float = 2.0f / (1.0f * bokehInfo.width);
			sc += bokehScale * maxBlurSpread * BOKEH_EXTRA_BLUR * oneOverBaseSize;
			
			bokehMaterial.SetTexture ("_Source", bokehInfo);
			bokehMaterial.SetTexture ("_MainTex", bokehTexture);
			bokehMaterial.SetVector ("_ArScale", Vector4 (sc, sc * arW, 0.5f, 0.5f * arW));
			bokehMaterial.SetFloat ("_Intensity", bokehIntensity);
			bokehMaterial.SetPass (0);	
			
			for (var m : Mesh in meshes)
				if (m) Graphics.DrawMeshNow (m, Matrix4x4.identity);	
	
			GL.PopMatrix ();
				
			Graphics.Blit (tempTex, finalTarget, dofMaterial, 8);    		
			
			// important to set back as we sample from this later on
			bokehInfo.filterMode = FilterMode.Bilinear;
		}	
	}
	
	
	function ReleaseTextures () {
		if (foregroundTexture) RenderTexture.ReleaseTemporary (foregroundTexture);
		if (finalDefocus) RenderTexture.ReleaseTemporary (finalDefocus);
		if (mediumRezWorkTexture) RenderTexture.ReleaseTemporary (mediumRezWorkTexture);
		if (lowRezWorkTexture) RenderTexture.ReleaseTemporary (lowRezWorkTexture);
		if (bokehSource) RenderTexture.ReleaseTemporary (bokehSource);
		if (bokehSource2) RenderTexture.ReleaseTemporary (bokehSource2);
	}
	
	function AllocateTextures (blurForeground : boolean, source : RenderTexture, divider : int, lowTexDivider : int) {
        foregroundTexture = null;
        if (blurForeground)
        	foregroundTexture = RenderTexture.GetTemporary (source.width, source.height, 0); 
		mediumRezWorkTexture = RenderTexture.GetTemporary (source.width / divider, source.height / divider, 0);         
        finalDefocus = RenderTexture.GetTemporary (source.width / divider, source.height / divider, 0);    
        lowRezWorkTexture  = RenderTexture.GetTemporary (source.width / lowTexDivider, source.height / lowTexDivider, 0);     
		bokehSource = null;
		bokehSource2 = null;
 		if (bokeh) {
        	bokehSource  = RenderTexture.GetTemporary (source.width / (lowTexDivider * bokehDownsample), source.height / (lowTexDivider * bokehDownsample), 0, RenderTextureFormat.ARGBHalf); 
        	bokehSource2  = RenderTexture.GetTemporary (source.width / (lowTexDivider * bokehDownsample), source.height / (lowTexDivider * bokehDownsample), 0,  RenderTextureFormat.ARGBHalf);
        	bokehSource.filterMode = FilterMode.Bilinear;
        	bokehSource2.filterMode = FilterMode.Bilinear;
        	RenderTexture.active = bokehSource2;
        	GL.Clear (false, true, Color(0.0f, 0.0f, 0.0f, 0.0f));   	        	        	
 		}    
        
        // to make sure: always use bilinear filter setting
        
        source.filterMode = FilterMode.Bilinear;
        finalDefocus.filterMode = FilterMode.Bilinear;
        mediumRezWorkTexture.filterMode = FilterMode.Bilinear;    
        lowRezWorkTexture.filterMode = FilterMode.Bilinear;     
        if (foregroundTexture)
        	foregroundTexture.filterMode = FilterMode.Bilinear;   	
	}	
}
