From b2be3dcb8cd8e2467b9d1d7baa3ee82bbd883498 Mon Sep 17 00:00:00 2001
From: Emmanuel Benoit <tseeker@nocternity.net>
Date: Sun, 1 Oct 2017 18:51:02 +0200
Subject: [PATCH] Depth of field!!!

---
 Makefile                           |  2 +
 TODO                               |  2 +
 bloom.cc                           | 24 +--------
 bloom.hh                           |  5 +-
 combine.cc                         | 59 ++++++++++++++++++++++
 bloom-combine.glsl => combine.glsl |  0
 combine.hh                         | 34 +++++++++++++
 dof-common.glsl                    | 61 ++++++++++++++++++++++
 dof-pass1.glsl                     |  8 +++
 dof-pass2.glsl                     | 15 ++++++
 dof.cc                             | 81 ++++++++++++++++++++++++++++++
 dof.hh                             | 41 +++++++++++++++
 fsquad.glsl                        | 12 +++++
 main.cc                            | 14 +++++-
 map.glsl                           |  2 +-
 raymarch-header.glsl               |  3 +-
 raymarcher.cc                      | 37 +++++++++-----
 raymarcher.glsl                    |  3 +-
 raymarcher.hh                      |  4 +-
 rendertarget.cc                    |  8 ++-
 texture.cc                         |  7 ++-
 utilities.cc                       | 50 +++++++++++++++++-
 utilities.hh                       | 17 +++++++
 23 files changed, 441 insertions(+), 48 deletions(-)
 create mode 100644 combine.cc
 rename bloom-combine.glsl => combine.glsl (100%)
 create mode 100644 combine.hh
 create mode 100644 dof-common.glsl
 create mode 100644 dof-pass1.glsl
 create mode 100644 dof-pass2.glsl
 create mode 100644 dof.cc
 create mode 100644 dof.hh
 create mode 100644 fsquad.glsl

diff --git a/Makefile b/Makefile
index 4bdc3b5..34b6db7 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,8 @@ DEMO = \
 	 rendertarget.o \
 	 raymarcher.o \
 	 bloom.o \
+	 dof.o \
+	 combine.o \
 	 profiling.o
 
 DEMO_DEPS = $(DEMO:%.o=.%.d)
diff --git a/TODO b/TODO
index d5e77a7..79b216f 100644
--- a/TODO
+++ b/TODO
@@ -11,6 +11,8 @@ Post-processing:
 * Lens dirt
 
 Technical:
+* Use program pipelines
+* Use uniform buffers
 * GLSL with includes
 * GLSL constants generator
 * Common handling for parameters
diff --git a/bloom.cc b/bloom.cc
index 7c86933..9e69415 100644
--- a/bloom.cc
+++ b/bloom.cc
@@ -20,16 +20,13 @@ T_BloomPass::T_BloomPass(
 		spHighpass_( GL_FRAGMENT_SHADER , watcher ) ,
 		spDownsample_( GL_FRAGMENT_SHADER , watcher ) ,
 		spBlur_( GL_FRAGMENT_SHADER , watcher ) ,
-		spCombine_( GL_FRAGMENT_SHADER , watcher ) ,
 		//
 		txBlur0_( input.width( ) , input.height( ) , E_TexType::RGB16F , BloomLevels ) ,
 		txBlur1_( input.width( ) , input.height( ) , E_TexType::RGB16F , BloomLevels ) ,
 		//
 		filterParams_{ 1.6 , 1.2 , .95 } ,
 		blurWeights_{ 0.324 , 0.232 , 0.0855 , 0.0205 } ,
-		blurSize_{ 1.3 } ,
-		combineStrength_( 0.45 ) ,
-		combineAttenuation_( 0.3 )
+		blurSize_{ 1.3 }
 {
 	txBlur0_.wrap( E_TexWrap::CLAMP_EDGE ).samplingMode( E_TexSampling::LINEAR );
 	txBlur1_.wrap( E_TexWrap::CLAMP_EDGE ).samplingMode( E_TexSampling::LINEAR );
@@ -42,12 +39,10 @@ T_BloomPass::T_BloomPass(
 	spHighpass_.addFile( "bloom-highpass.glsl" );
 	spDownsample_.addFile( "downsample.glsl" );
 	spBlur_.addFile( "blur-pass.glsl" );
-	spCombine_.addFile( "bloom-combine.glsl" );
 
 	spHighpass_.load( );
 	spDownsample_.load( );
 	spBlur_.load( );
-	spCombine_.load( );
 }
 
 void T_BloomPass::render( )
@@ -113,21 +108,6 @@ void T_BloomPass::render( )
 		glRectf( -1 , -1 , 1 , 1 );
 	}
 
-	T_Rendertarget::MainOutput( );
-	glClearColor( 1 , 0 , 1 , 1 );
-	glClear( GL_COLOR_BUFFER_BIT );
-	if ( spCombine_.activate( ) ) {
-		T_TextureBinding main( 0 ) , blur( 1 );
-		main.set( 0 , input_ );
-		main.bind( );
-		blur.set( 1 , txBlur0_ );
-		blur.bind( );
-		glUniform2f( 2 , input_.width( ) , input_.height( ) );
-		glUniform2f( 3 , combineStrength_ , combineAttenuation_ );
-		glRectf( -1, -1 , 1 , 1 );
-		glBindTexture( GL_TEXTURE_2D , 0 );
-	}
-
 	PEND( );
 }
 
@@ -140,6 +120,4 @@ void T_BloomPass::makeUI( )
 	ImGui::DragFloat3( "Filter" , filterParams_ , .01f , 0.1 , 10 );
 	ImGui::DragFloat4( "Weights" , blurWeights_ , .001f , 0. , 10 );
 	ImGui::DragFloat( "Blur size" , &blurSize_ , .001f , 0. , 10 );
-	ImGui::DragFloat( "Initial strength" , &combineStrength_ , .001f , 0. , 10 );
-	ImGui::DragFloat( "Attenuation" , &combineAttenuation_ , .001f , 0. , 1 );
 }
diff --git a/bloom.hh b/bloom.hh
index 4f1cef1..ea31403 100644
--- a/bloom.hh
+++ b/bloom.hh
@@ -23,13 +23,14 @@ struct T_BloomPass
 	void render( );
 	void makeUI( );
 
+	T_Texture& output( ) { return txBlur0_; }
+
     private:
 	T_Texture& input_;
 
 	T_ShaderProgram spHighpass_;
 	T_ShaderProgram spDownsample_;
 	T_ShaderProgram spBlur_;
-	T_ShaderProgram spCombine_;
 
 	T_Texture txBlur0_ , txBlur1_;
 	std::vector< T_Rendertarget > rtBlur0_ , rtBlur1_;
@@ -37,6 +38,4 @@ struct T_BloomPass
 	float filterParams_[ 3 ];
 	float blurWeights_[ 4 ];
 	float blurSize_;
-	float combineStrength_;
-	float combineAttenuation_;
 };
diff --git a/combine.cc b/combine.cc
new file mode 100644
index 0000000..95aafc3
--- /dev/null
+++ b/combine.cc
@@ -0,0 +1,59 @@
+#include "externals.hh"
+#include "utilities.hh"
+#include "texture.hh"
+#include "rendertarget.hh"
+#include "combine.hh"
+#include "bloom.hh"
+#include "profiling.hh"
+
+namespace {
+	static const std::string Name_( "Combine" );
+}
+
+#define PSTART() T_Profiler::Profiler.start( Name_ )
+#define PEND() do { glFinish( ); T_Profiler::Profiler.end( Name_ ); } while ( 0 )
+
+
+T_CombinePass::T_CombinePass(
+		__rw__ T_FilesWatcher& watcher ,
+		__rw__ T_Texture& image ,
+		__rw__ T_Texture& bloom )
+	: txImage_( image ) , txBloom_( bloom ) ,
+		program_( GL_FRAGMENT_SHADER , watcher ) ,
+		bloomStrength_( 0.45 ) ,
+		bloomAttenuation_( 0.3 )
+{
+	program_.addFile( "combine.glsl" );
+	program_.load( );
+}
+
+void T_CombinePass::render( )
+{
+	PSTART( );
+	T_Rendertarget::MainOutput( );
+	glClearColor( 1 , 0 , 1 , 1 );
+	glClear( GL_COLOR_BUFFER_BIT );
+	if ( program_.activate( ) ) {
+		T_TextureBinding main( 0 ) , blur( 1 );
+		main.set( 0 , txImage_ );
+		main.bind( );
+		blur.set( 1 , txBloom_ );
+		blur.bind( );
+		glUniform2f( 2 , txImage_.width( ) , txImage_.height( ) );
+		glUniform2f( 3 , bloomStrength_ , bloomAttenuation_ );
+		glRectf( -1, -1 , 1 , 1 );
+		glBindTexture( GL_TEXTURE_2D , 0 );
+	}
+	PEND( );
+}
+
+void T_CombinePass::makeUI( )
+{
+	if ( !ImGui::CollapsingHeader( "Combine" ) ) {
+		return;
+	}
+
+	ImGui::DragFloat( "Bloom strength" , &bloomStrength_ , .001f , 0. , 10 );
+	ImGui::DragFloat( "Bloom attenuation" , &bloomAttenuation_ , .001f , 0. , 1 );
+}
+
diff --git a/bloom-combine.glsl b/combine.glsl
similarity index 100%
rename from bloom-combine.glsl
rename to combine.glsl
diff --git a/combine.hh b/combine.hh
new file mode 100644
index 0000000..0e431cf
--- /dev/null
+++ b/combine.hh
@@ -0,0 +1,34 @@
+#pragma once
+#ifndef REAL_BUILD
+# define REAL_BUILD
+# include "externals.hh"
+# include "texture.hh"
+# include "rendertarget.hh"
+# include "utilities.hh"
+# undef REAL_BUILD
+#endif
+
+
+struct T_CombinePass
+{
+	T_CombinePass( ) = delete;
+	T_CombinePass( T_CombinePass const& ) = delete;
+	T_CombinePass( T_CombinePass&& ) = delete;
+
+	T_CombinePass( __rw__ T_FilesWatcher& watcher ,
+			__rw__ T_Texture& image ,
+			__rw__ T_Texture& bloom );
+
+	void render( );
+	void makeUI( );
+
+    private:
+	T_Texture& txImage_;
+	T_Texture& txBloom_;
+
+	T_ShaderProgram program_;
+
+	float bloomStrength_;
+	float bloomAttenuation_;
+};
+
diff --git a/dof-common.glsl b/dof-common.glsl
new file mode 100644
index 0000000..aba3547
--- /dev/null
+++ b/dof-common.glsl
@@ -0,0 +1,61 @@
+#version 450 core
+
+//#define USE_RANDOM
+
+layout( location = 0 ) uniform sampler2D u_Input;
+layout( location = 1 ) uniform sampler2D u_Depth;
+layout( location = 2 ) uniform vec4 u_Parameters;
+layout( location = 3 ) uniform float u_Samples;
+layout( location = 4 ) uniform vec3 u_ResolutionTime;
+
+#define uSharpDist (u_Parameters.x)
+#define uSharpRange (u_Parameters.y)
+#define uBlurFalloff (u_Parameters.z)
+#define uMaxBlur (u_Parameters.w)
+
+#define uResolution (u_ResolutionTime.xy)
+#define uTime (u_ResolutionTime.z)
+
+float CoC( float z )
+{
+	return uMaxBlur * min( 1 ,
+		max( 0 , abs( z - uSharpDist ) - uSharpRange ) / uBlurFalloff );
+}
+
+layout( location = 0 ) out vec3 o_Color;
+
+float hash1( vec2 p )
+{
+	p = fract(p * vec2(5.3987, 5.4421));
+	p += dot(p.yx, p.xy + vec2(21.5351, 14.3137));
+	return fract(p.x * p.y * 95.4307);
+}
+
+// z: z at UV
+// coc: blur radius at UV
+// uv: initial coordinate
+// blurvec: smudge direction
+vec3 depthDirectionalBlur( float z , float coc , vec2 uv , vec2 blurvec )
+{
+	vec3 sumcol = vec3( 0. );
+	for ( int i = 0 ; i < u_Samples ; i++ ) {
+		float r = i;
+#ifdef USE_RANDOM
+		r += hash1( uv + float( i + uTime ) ) - .5;
+#endif
+		r = r / float( u_Samples - 1 ) - .5;
+		vec2 p = uv + r * coc * blurvec;
+		vec3 smpl = texture( u_Input , p ).xyz;
+		float sz = texture( u_Depth , p ).x;
+		if ( sz < z ) {
+			// if sample is closer consider it's CoC
+			p = uv + r * min( coc , CoC( sz ) ) * blurvec;
+			p = uv + r * CoC( sz ) * blurvec;
+			smpl = texture( u_Input , p ).xyz;
+		}
+		sumcol += smpl;
+	}
+	sumcol /= float( u_Samples );
+	sumcol = max( sumcol , 0. );
+	return sumcol;
+}
diff --git a/dof-pass1.glsl b/dof-pass1.glsl
new file mode 100644
index 0000000..d55f187
--- /dev/null
+++ b/dof-pass1.glsl
@@ -0,0 +1,8 @@
+void main()
+{
+	vec2 uv = gl_FragCoord.xy / uResolution;
+	vec2 blurvec = vec2( 0 , 1 ) / uResolution;
+
+	float z = texture( u_Depth , uv ).x;
+	o_Color = depthDirectionalBlur( z , CoC( z ) , uv , blurvec );
+}
diff --git a/dof-pass2.glsl b/dof-pass2.glsl
new file mode 100644
index 0000000..77f2298
--- /dev/null
+++ b/dof-pass2.glsl
@@ -0,0 +1,15 @@
+void main()
+{
+	vec2 uv = gl_FragCoord.xy / uResolution;
+	float z = texture( u_Depth , uv ).x;
+
+	vec2 blurdir = vec2( 1.0 , 0.577350269189626 );
+	vec2 blurvec = normalize( blurdir ) / uResolution;
+	vec3 color0 = depthDirectionalBlur( z , CoC( z ) , uv , blurvec );
+
+	blurdir.x = -1;
+	blurvec = normalize( blurdir ) / uResolution;
+	vec3 color1 = depthDirectionalBlur( z , CoC( z ) , uv , blurvec );
+
+	o_Color = min( color0 , color1 );
+}
diff --git a/dof.cc b/dof.cc
new file mode 100644
index 0000000..a10df7f
--- /dev/null
+++ b/dof.cc
@@ -0,0 +1,81 @@
+#include "externals.hh"
+#include "utilities.hh"
+#include "texture.hh"
+#include "rendertarget.hh"
+#include "dof.hh"
+#include "profiling.hh"
+
+namespace {
+	static const std::string Name_( "DoF" );
+}
+
+#define PSTART() T_Profiler::Profiler.start( Name_ )
+#define PEND() do { glFinish( ); T_Profiler::Profiler.end( Name_ ); } while ( 0 )
+
+
+T_DoFPass::T_DoFPass(
+		__rw__ T_FilesWatcher& watcher ,
+		__rw__ T_Texture& imageInput ,
+		__rw__ T_Texture& depthInput )
+	: imageInput_( imageInput ) , depthInput_( depthInput ) ,
+		spPass1_( GL_FRAGMENT_SHADER , watcher ) ,
+		spPass2_( GL_FRAGMENT_SHADER , watcher ) ,
+		txPass1_( imageInput.width( ) , imageInput.height( ) ,
+				E_TexType::RGB16F ) ,
+		txOutput_( imageInput.width( ) , imageInput.height( ) ,
+				E_TexType::RGB16F ) ,
+		rtPass1_( T_RendertargetSetup( ).add( txPass1_ ).create( ) ) ,
+		rtPass2_( T_RendertargetSetup( ).add( txOutput_ ).create( ) ) ,
+		filterParams_{ 10 , 2 , 5 , 16 } ,
+		nSamples_( 16 )
+{
+	spPass1_.addFile( "dof-common.glsl" );
+	spPass1_.addFile( "dof-pass1.glsl" );
+	spPass1_.load( );
+
+	spPass2_.addFile( "dof-common.glsl" );
+	spPass2_.addFile( "dof-pass2.glsl" );
+	spPass2_.load( );
+}
+
+void T_DoFPass::render( )
+{
+	PSTART( );
+	enum {
+		U_INPUT		= 0 ,
+		U_DEPTH		= 1 ,
+		U_PARAMS	= 2 ,
+		U_SAMPLES	= 3 ,
+		U_RES_TIME	= 4
+	};
+	T_TextureBinding tb0( 0 ) , tb1( 1 );
+	if ( spPass1_.activate( ) && rtPass1_.activate( ) ) {
+		tb0.set( U_INPUT , imageInput_ );
+		tb1.set( U_DEPTH , depthInput_ );
+		tb0.bind( );
+		tb1.bind( );
+		glUniform4fv( U_PARAMS , 1 , filterParams_ );
+		glUniform1f( U_SAMPLES , nSamples_ );
+		glUniform3f( U_RES_TIME , imageInput_.width( ) ,
+				imageInput_.height( ) ,
+				0 );
+		glRectf( -1 , -1 , 1 , 1 );
+	}
+	if ( spPass2_.activate( ) && rtPass2_.activate( ) ) {
+		tb0.set( U_INPUT , txPass1_ );
+		tb1.set( U_DEPTH , depthInput_ );
+		tb0.bind( );
+		tb1.bind( );
+		glUniform4fv( U_PARAMS , 1 , filterParams_ );
+		glUniform1f( U_SAMPLES , nSamples_ );
+		glUniform3f( U_RES_TIME , imageInput_.width( ) ,
+				imageInput_.height( ) ,
+				0 );
+		glRectf( -1 , -1 , 1 , 1 );
+	}
+	PEND( );
+}
+
+void T_DoFPass::makeUI( )
+{
+}
diff --git a/dof.hh b/dof.hh
new file mode 100644
index 0000000..dcf0630
--- /dev/null
+++ b/dof.hh
@@ -0,0 +1,41 @@
+#pragma once
+#ifndef REAL_BUILD
+# define REAL_BUILD
+# include "externals.hh"
+# include "texture.hh"
+# include "rendertarget.hh"
+# include "utilities.hh"
+# undef REAL_BUILD
+#endif
+
+
+struct T_DoFPass
+{
+	T_DoFPass( ) = delete;
+	T_DoFPass( T_DoFPass const& ) = delete;
+	T_DoFPass( T_DoFPass&& ) = delete;
+
+	T_DoFPass( __rw__ T_FilesWatcher& watcher ,
+			__rw__ T_Texture& imageInput ,
+			__rw__ T_Texture& depthInput );
+
+	void render( );
+	void makeUI( );
+
+	T_Texture& output( ) { return txOutput_; }
+
+    private:
+	T_Texture& imageInput_;
+	T_Texture& depthInput_;
+
+	T_ShaderProgram spPass1_;
+	T_ShaderProgram spPass2_;
+
+	T_Texture txPass1_ , txOutput_;
+	T_Rendertarget rtPass1_ , rtPass2_;
+
+	// SharpDist/SharpRange/Falloff/MaxBlur
+	float filterParams_[ 4 ];
+	int nSamples_;
+};
+
diff --git a/fsquad.glsl b/fsquad.glsl
new file mode 100644
index 0000000..b64e5b0
--- /dev/null
+++ b/fsquad.glsl
@@ -0,0 +1,12 @@
+#version 450 core
+
+out gl_PerVertex {
+	vec4 gl_Position;
+};
+
+void main( void )
+{
+	gl_Position = vec4(
+		vec2( gl_VertexID >> 1 , gl_VertexID & 1 ) * 2 - 1 ,
+		0 , 1 );
+}
diff --git a/main.cc b/main.cc
index 6926527..6ec1b77 100644
--- a/main.cc
+++ b/main.cc
@@ -4,8 +4,10 @@
 #include "utilities.hh"
 #include "texture.hh"
 #include "rendertarget.hh"
-#include "bloom.hh"
 #include "raymarcher.hh"
+#include "dof.hh"
+#include "bloom.hh"
+#include "combine.hh"
 #include "profiling.hh"
 
 
@@ -32,7 +34,9 @@ struct T_Main
 	std::unique_ptr< T_ShaderProgram > spCopy;
 
 	std::unique_ptr< T_Raymarcher > raymarcher;
+	std::unique_ptr< T_DoFPass > dofPass;
 	std::unique_ptr< T_BloomPass > bloomPass;
+	std::unique_ptr< T_CombinePass > combinePass;
 
 	void startIteration( );
 	void handleCapture( );
@@ -66,8 +70,12 @@ T_Main::T_Main( )
 
 	initProgram( );
 	raymarcher = std::make_unique< T_Raymarcher >( watcher , 1280 , 720 );
+	dofPass = std::make_unique< T_DoFPass >( watcher ,
+			raymarcher->output( ) , raymarcher->depth( ) );
 	bloomPass = std::make_unique< T_BloomPass >( watcher ,
 			raymarcher->output( ) );
+	combinePass = std::make_unique< T_CombinePass >( watcher ,
+			dofPass->output( ) , bloomPass->output( ) );
 }
 
 void T_Main::mainLoop( )
@@ -166,7 +174,9 @@ void T_Main::makeUI( )
 
 	ImGui::Checkbox( "Profiler" , &T_Profiler::Profiler.uiEnabled( ) );
 	raymarcher->makeUI( );
+	dofPass->makeUI( );
 	bloomPass->makeUI( );
+	combinePass->makeUI( );
 
 	ImGui::End( );
 
@@ -177,7 +187,9 @@ void T_Main::render( )
 {
 	T_Profiler::Profiler.start( "Render" );
 	raymarcher->render( );
+	dofPass->render( );
 	bloomPass->render( );
+	combinePass->render( );
 	glFinish( ); T_Profiler::Profiler.end( "Render" );
 
 	glUseProgram( 0 );
diff --git a/map.glsl b/map.glsl
index 3681bbe..38058e2 100644
--- a/map.glsl
+++ b/map.glsl
@@ -1,6 +1,6 @@
 vec2 map( vec3 pos )
 {
 	vec3 q = pos;
-	q.xy = mod( q.xy + 2. , 4. ) - 2.;
+	q.xy = mod( q.xy + 4. , 8. ) - 4.;
 	return vec2( length( q ) - 1.8 , step( 0. , 1.9 - length( pos.xy ) ) );
 }
diff --git a/raymarch-header.glsl b/raymarch-header.glsl
index 2821952..0ccd569 100644
--- a/raymarch-header.glsl
+++ b/raymarch-header.glsl
@@ -12,7 +12,8 @@ layout( location = 7 ) uniform vec4 u_Render;
 vec3 camPos , lookAt , camUp;
 float nearPlane;
 
-layout( location = 0 ) out vec4 color;
+layout( location = 0 ) out vec3 o_Color;
+layout( location = 1 ) out float o_Z;
 
 void setCamFromUniforms( ) {
 	camPos = u_CamPos;
diff --git a/raymarcher.cc b/raymarcher.cc
index 0ab310d..1c84af9 100644
--- a/raymarcher.cc
+++ b/raymarcher.cc
@@ -18,24 +18,36 @@ T_Raymarcher::T_Raymarcher(
 		__rd__ const uint32_t height )
 	: camera_( ) , program_( GL_FRAGMENT_SHADER , watcher ) ,
 		txOutput_( width , height , E_TexType::RGB16F ) ,
-		rtOutput_( T_RendertargetSetup( ).add( txOutput_ ).create( ) )
+		txDepth_( width , height , E_TexType::R16F ) ,
+		rtOutput_( T_RendertargetSetup( )
+				.add( txOutput_ )
+				.add( txDepth_ )
+				.create( ) )
 {
+	glGenProgramPipelines( 1 , &pipeline_ );
+
 	program_.addFile( "raymarch-header.glsl" );
 	program_.addFile( "map.glsl" );
 	program_.addFile( "raymarcher.glsl" );
 	program_.load( );
+
+	auto& fsq( T_ShaderProgram::FullscreenQuad( watcher ) );
+	glUseProgramStages( pipeline_ , GL_VERTEX_SHADER_BIT , fsq.id( ) );
+	glUseProgramStages( pipeline_ , GL_FRAGMENT_SHADER_BIT , program_.id( ) );
+	assert( glGetError( ) == GL_NO_ERROR );
 }
 
 
 void T_Raymarcher::render( )
 {
 	PSTART( );
-	if ( !( program_.activate( ) && rtOutput_.activate( ) ) ) {
+	if ( ! rtOutput_.activate( ) ) {
 		PEND( );
 		return;
 	}
 
-	glClearColor( 0 , 1 , 1 , 1 );
+	glBindProgramPipeline( pipeline_ );
+	glClearColor( 0 , 0 , 0 , 1 );
 	glClear( GL_COLOR_BUFFER_BIT );
 	enum {
 		U_TIME			= 0 ,
@@ -48,20 +60,21 @@ void T_Raymarcher::render( )
 		U_RAYMARCHER		= 7 ,
 	};
 
-	glUniform1f( U_TIME , 0 );
-	glUniform2f( U_RESOLUTION , rtOutput_.width( ) , rtOutput_.height( ) );
+	glProgramUniform1f( program_.id( ) , U_TIME , 0 );
+	glProgramUniform2f( program_.id( ) , U_RESOLUTION , rtOutput_.width( ) , rtOutput_.height( ) );
 
-	glUniform3fv( U_CAM_POS , 1 , &camera_.pos.x );
-	glUniform3fv( U_LOOK_AT , 1 , &camera_.lookAt.x );
-	glUniform3fv( U_CAM_UP , 1 , &camera_.up.x );
-	glUniform1f( U_NEAR_PLANE , camera_.np );
+	glProgramUniform3fv( program_.id( ) , U_CAM_POS , 1 , &camera_.pos.x );
+	glProgramUniform3fv( program_.id( ) , U_LOOK_AT , 1 , &camera_.lookAt.x );
+	glProgramUniform3fv( program_.id( ) , U_CAM_UP , 1 , &camera_.up.x );
+	glProgramUniform1f( program_.id( ) , U_NEAR_PLANE , camera_.np );
 
-	glUniform3f( U_LIGHT_DIR , 0 , 1 , 1 );
+	glProgramUniform3f( program_.id( ) , U_LIGHT_DIR , 0 , 1 , 1 );
 
-	glUniform4f( U_RAYMARCHER , rmIterations , rmStep ,
+	glProgramUniform4f( program_.id( ) , U_RAYMARCHER , rmIterations , rmStep ,
 			rmEpsilon , rmMaxDist );
 
-	glRectf( -1, -1 , 1 , 1 );
+	glDrawArrays( GL_TRIANGLE_STRIP , 0 , 4 );
+	glBindProgramPipeline( 0 );
 
 	PEND( );
 }
diff --git a/raymarcher.glsl b/raymarcher.glsl
index afc7e7a..0ae8d56 100644
--- a/raymarcher.glsl
+++ b/raymarcher.glsl
@@ -77,7 +77,8 @@ void main( )
 		bc = vec3( 0. );
 	}
 
-	color = vec4( bc , 1 );
+	o_Color = bc;
+	o_Z = r.x;
 
 	/*
 	if ( r.y == -2 ) {
diff --git a/raymarcher.hh b/raymarcher.hh
index 0418501..34fa908 100644
--- a/raymarcher.hh
+++ b/raymarcher.hh
@@ -27,13 +27,15 @@ struct T_Raymarcher
 	T_Camera& camera( ) noexcept { return camera_; }
 
 	T_Texture& output( ) noexcept { return txOutput_; }
+	T_Texture& depth( ) noexcept { return txDepth_; }
 
     private:
 	T_Camera camera_;
 
+	GLuint pipeline_;
 	T_ShaderProgram program_;
 
-	T_Texture txOutput_;
+	T_Texture txOutput_ , txDepth_;
 	T_Rendertarget rtOutput_;
 
 	int rmIterations = 128;
diff --git a/rendertarget.cc b/rendertarget.cc
index cfd8084..4cf62f8 100644
--- a/rendertarget.cc
+++ b/rendertarget.cc
@@ -56,8 +56,8 @@ T_Rendertarget T_RendertargetSetup::create( )
 	glBindFramebuffer( GL_FRAMEBUFFER , id );
 	for ( auto i = 0u ; i < nca ; i ++ ) {
 #ifdef INTRUSIVE_TRACES
-		printf( "init fb %p att %d tx %p level %d\n" ,
-				this , i ,
+		printf( "init %p fb %d att %d tx %p level %d\n" ,
+				this , id , i ,
 				colorAttachments_[ i ].texture ,
 				colorAttachments_[ i ].level );
 #endif
@@ -132,6 +132,10 @@ bool T_Rendertarget::activate( )
 			for ( auto i = 0u ; i < nCats_ ; i ++ ) {
 				buffers_.push_back( GL_COLOR_ATTACHMENT0 + i );
 			}
+#ifdef INTRUSIVE_TRACES
+			printf( "fb %d: created buffers (%d items)\n" , id_ ,
+					int( buffers_.size( ) ) );
+#endif
 		}
 		glBindFramebuffer( GL_FRAMEBUFFER , id_ );
 		glViewport( 0 , 0 , width_ , height_ );
diff --git a/texture.cc b/texture.cc
index 7ac3023..e27efc8 100644
--- a/texture.cc
+++ b/texture.cc
@@ -1,4 +1,5 @@
 #include "externals.hh"
+#include "utilities.hh"
 #include "texture.hh"
 
 
@@ -62,13 +63,17 @@ T_Texture::T_Texture(
 
 	uint32_t w = width , h = height;
 	for ( auto i = 0u ; i < levels ; i ++ ) {
+#ifdef INTRUSIVE_TRACES
+		printf( "init %p txid %d lv %d sz %dx%d\n" , this , id_ ,
+				i , w , h );
+#endif
 		glTexImage2D( GL_TEXTURE_2D , i , ifmt , w , h , 0 , fmt , dt , nullptr );
 		w >>= 1;
 		h >>= 1;
 		assert( w && h );
 	}
 
-	assert( glGetError( ) == GL_NO_ERROR );
+	GL_CHECK( );
 }
 
 T_Texture::~T_Texture( )
diff --git a/utilities.cc b/utilities.cc
index ffd3c23..8d8c8fa 100644
--- a/utilities.cc
+++ b/utilities.cc
@@ -339,16 +339,46 @@ void T_ShaderProgram::load( )
 	}
 
 	T_ShaderCode sc( n );
+	printf( "LOAD PROGRAM" );
 	for ( auto i = 0u ; i < n ; i ++ ) {
 		if ( chunksOrFiles_[ i ] ) {
+			printf( " %d:(chunk)" , i );
 			sc.setPart( i , parts_[ i ].c_str( ) );
-		} else if ( sc.loadPart( i , parts_[ i ].c_str( ) , errors_ ) ) {
-			files_.watch( parts_[ i ] );
+		} else {
+			printf( " %d:%s" , i , parts_[ i ].c_str( ) );
+			if ( sc.loadPart( i , parts_[ i ].c_str( ) , errors_ ) ) {
+				files_.watch( parts_[ i ] );
+			}
 		}
 	}
+	printf( "\n" );
 
 	if ( errors_.size( ) == 0 ) {
 		program_ = sc.createProgram( programType_ , errors_ );
+		if ( program_ != 0 ) {
+			GLint x;
+			glGetProgramInterfaceiv( program_ , GL_PROGRAM_OUTPUT ,
+					GL_ACTIVE_RESOURCES , &x );
+			printf( "-> LOADED (%d output(s)" , x );
+			for ( int i = 0 ; i < x ; i ++ ) {
+				static const GLenum query[] = {
+					GL_NAME_LENGTH , GL_LOCATION
+				};
+				int output[ 2 ];
+				glGetProgramResourceiv( program_ , GL_PROGRAM_OUTPUT ,
+						i , 2 , query , 2 , nullptr ,
+						output );
+
+				char rName[ output[ 0 ] + 1 ];
+				glGetProgramResourceName( program_ ,
+						GL_PROGRAM_OUTPUT , i ,
+						sizeof( rName ) , nullptr ,
+						rName );
+				printf( "%s %s@%d" , i == 0 ? ":" : "" , rName ,
+						output[ 1 ] );
+			}
+			printf( ")\n" );
+		}
 	}
 
 	if ( errors_.size( ) ) {
@@ -367,6 +397,22 @@ bool T_ShaderProgram::activate( ) const
 	return program_ != 0;
 }
 
+namespace {
+	std::unique_ptr< T_ShaderProgram > FsQuad_;
+}
+
+T_ShaderProgram const& T_ShaderProgram::FullscreenQuad(
+		__rw__ T_FilesWatcher& watcher )
+{
+	if ( !FsQuad_ ) {
+		FsQuad_ = std::make_unique< T_ShaderProgram >(
+				GL_VERTEX_SHADER , watcher );
+		FsQuad_->addFile( "fsquad.glsl" );
+		FsQuad_->load( );
+	}
+	return *FsQuad_;
+}
+
 
 /*= T_Camera =================================================================*/
 
diff --git a/utilities.hh b/utilities.hh
index 89523a6..f79fe67 100644
--- a/utilities.hh
+++ b/utilities.hh
@@ -15,6 +15,18 @@ inline void reenableButtons( )
 
 /*----------------------------------------------------------------------------*/
 
+#define GL_CHECK( ) \
+	do { \
+		auto err_( glGetError( ) ); \
+		if ( err_ != GL_NO_ERROR ) { \
+			fprintf( stderr , "GL error %x in %s:%d\n" , \
+					err_ , __FILE__ , __LINE__ ); \
+			abort( ); \
+		} \
+	} while ( 0 )
+
+/*----------------------------------------------------------------------------*/
+
 // Add some value to an angle, keeping it in [-180;180]
 void updateAngle(
 		__rw__	float& initial ,
@@ -143,6 +155,11 @@ struct T_ShaderProgram
 	void load( );
 	bool activate( ) const;
 
+	GLuint id( ) const { return program_; }
+
+	static T_ShaderProgram const& FullscreenQuad(
+			__rw__ T_FilesWatcher& watcher );
+
     private:
 	T_WatchedFiles files_;