+typedef struct framebuffer framebuffer;
+
+
+/*
+ * All standard buffers used in rendering
+ */
+VG_STATIC struct pipeline
+{
+ glmesh fsquad;
+
+ framebuffer *fb_main,
+ *fb_heightmap,
+ *fb_water_reflection,
+ *fb_water_beneath;
+
+ /* STD140 */
+ struct ub_world_lighting
+ {
+ /* v3f (padded) */
+ v4f g_light_colours[3],
+ g_light_directions[3],
+ g_ambient_colour;
+
+ v4f g_water_plane,
+ g_depth_bounds;
+
+ float g_water_fog;
+ int g_light_count;
+ int g_light_preview;
+ int g_shadow_samples;
+ }
+ ub_world_lighting;
+
+ struct light_widget
+ {
+ int enabled;
+ v2f dir;
+ v3f colour;
+ }
+ widgets[3];
+
+ float shadow_spread, shadow_length;
+ GLuint ubo_world_lighting,
+ ubo_world;
+
+ int ready;
+}
+gpipeline =
+{
+ .widgets =
+ {
+ {
+ .enabled = 1,
+ .colour = { 1.36f, 1.35f, 1.01f },
+ .dir = { 0.63f, -0.08f }
+ },
+ {
+ .enabled = 1,
+ .colour = { 0.33f, 0.56f, 0.64f },
+ .dir = { -2.60f, -0.13f }
+ },
+ {
+ .enabled = 1,
+ .colour = { 0.05f, 0.05f, 0.23f },
+ .dir = { 2.60f, -0.84f }
+ }
+ },
+ .shadow_spread = 0.65f,
+ .shadow_length = 9.50f,
+
+ .ub_world_lighting =
+ {
+ .g_ambient_colour = { 0.09f, 0.03f, 0.07f }
+ }
+};
+
+struct framebuffer
+{
+ const char *display_name;
+ int resolution_div,
+ fixed_w,
+ fixed_h;
+
+ struct framebuffer_attachment
+ {
+ const char *display_name;
+
+ enum framebuffer_attachment_type
+ {
+ k_framebuffer_attachment_type_none,
+ k_framebuffer_attachment_type_colour,
+ k_framebuffer_attachment_type_renderbuffer
+ }
+ purpose;
+
+ enum framebuffer_quality_profile
+ {
+ k_framebuffer_quality_all,
+ k_framebuffer_quality_high_only
+ }
+ quality;
+
+ GLenum internalformat,
+ format,
+ type,
+ attachment;
+
+ GLuint id;
+
+ /* Runtime */
+ int debug_view;
+ }
+ attachments[5];
+ GLuint fb;
+ framebuffer **link;
+}
+framebuffers[] =
+{
+ {
+ /*
+ * The primary draw target
+ */
+ "main",
+ .link = &gpipeline.fb_main,
+ .resolution_div = 1,
+ .attachments =
+ {
+ {
+ "colour", k_framebuffer_attachment_type_colour,
+
+ .internalformat = GL_RGB,
+ .format = GL_RGB,
+ .type = GL_UNSIGNED_BYTE,
+ .attachment = GL_COLOR_ATTACHMENT0
+ },
+ {
+ "motion", k_framebuffer_attachment_type_colour,
+
+ .quality = k_framebuffer_quality_high_only,
+ .internalformat = GL_RG16F,
+ .format = GL_RG,
+ .type = GL_FLOAT,
+ .attachment = GL_COLOR_ATTACHMENT1
+ },
+ {
+ "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+
+ .internalformat = GL_DEPTH24_STENCIL8,
+ .attachment = GL_DEPTH_STENCIL_ATTACHMENT
+ }
+ }
+ },
+ {
+ /*
+ * A ortho projection of the world, used for shadows and ocean colouring.
+ * Note: it does not have a render buffer attachement because it's
+ * intended to be drawn to in a MAX blending mode
+ */
+ "heightmap",
+ .link = &gpipeline.fb_heightmap,
+ .fixed_w = 1024,
+ .fixed_h = 1024,
+
+ .attachments =
+ {
+ {
+ "depth", k_framebuffer_attachment_type_colour,
+
+ .internalformat = GL_R32F,
+ .format = GL_RED,
+ .type = GL_FLOAT,
+ .attachment = GL_COLOR_ATTACHMENT0
+ }
+ }
+ },
+ {
+ /*
+ * Second rendered view from the perspective of the water reflection
+ */
+ "water_reflection",
+ .link = &gpipeline.fb_water_reflection,
+ .resolution_div = 3,
+ .attachments =
+ {
+ {
+ "colour", k_framebuffer_attachment_type_colour,
+ .internalformat = GL_RGB,
+ .format = GL_RGB,
+ .type = GL_UNSIGNED_BYTE,
+ .attachment = GL_COLOR_ATTACHMENT0
+ },
+ {
+ "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+
+ .internalformat = GL_DEPTH24_STENCIL8,
+ .attachment = GL_DEPTH_STENCIL_ATTACHMENT
+ }
+ }
+ },
+ {
+ /*
+ * Thid rendered view from the perspective of the camera, but just
+ * captures stuff thats under the water
+ */
+ "water_beneath",
+ .link = &gpipeline.fb_water_beneath,
+ .resolution_div = 4,
+ .attachments =
+ {
+ {
+ "colour", k_framebuffer_attachment_type_colour,
+ .internalformat = GL_RGBA,
+ .format = GL_RGBA,
+ .type = GL_UNSIGNED_BYTE,
+ .attachment = GL_COLOR_ATTACHMENT0
+ },
+ {
+ "depth_stencil", k_framebuffer_attachment_type_renderbuffer,
+
+ .internalformat = GL_DEPTH24_STENCIL8,
+ .attachment = GL_DEPTH_STENCIL_ATTACHMENT
+ }
+ }
+ }
+};
+
+/*
+ * Get the current (automatically scaled or fixed) resolution of framebuffer
+ */
+VG_STATIC void render_fb_get_current_res( struct framebuffer *fb,
+ int *x, int *y )
+{
+ if( fb->resolution_div )
+ {
+ *x = vg.window_x / fb->resolution_div;
+ *y = vg.window_y / fb->resolution_div;
+ }
+ else
+ {
+ *x = fb->fixed_w;
+ *y = fb->fixed_h;
+ }
+}
+
+/*
+ * Bind framebuffer for drawing to
+ */
+VG_STATIC void render_fb_bind( framebuffer *fb )
+{
+ int x, y;
+ render_fb_get_current_res( fb, &x, &y );
+ glBindFramebuffer( GL_FRAMEBUFFER, fb->fb );
+ glViewport( 0, 0, x, y );
+}
+
+/*
+ * Bind framebuffer attachment's texture
+ */
+VG_STATIC void render_fb_bind_texture( framebuffer *fb,
+ int attachment, int slot )
+{
+ struct framebuffer_attachment *at = &fb->attachments[attachment];
+
+ if( at->purpose != k_framebuffer_attachment_type_colour )
+ {
+ vg_fatal_exit_loop( "illegal operation: bind non-colour framebuffer"
+ " attachment to texture slot" );
+ }
+
+ glActiveTexture( GL_TEXTURE0 + slot );
+ glBindTexture( GL_TEXTURE_2D, fb->attachments[attachment].id );
+}
+
+
+/*
+ * Shaders
+ */
+VG_STATIC void shader_link_standard_ub( GLuint shader, int texture_id )
+{
+ GLuint idx = glGetUniformBlockIndex( shader, "ub_world_lighting" );
+ glUniformBlockBinding( shader, idx, 0 );
+
+ render_fb_bind_texture( gpipeline.fb_heightmap, 0, texture_id );
+ glUniform1i( glGetUniformLocation( shader, "g_world_depth" ), texture_id );
+}
+
+VG_STATIC void render_update_lighting_ub(void)
+{
+ struct ub_world_lighting *winf = &gpipeline.ub_world_lighting;
+ int c = 0;
+
+ for( int i=0; i<3; i++ )
+ {
+ struct light_widget *lw = &gpipeline.widgets[i];
+
+ if( lw->enabled )
+ {
+ float pitch = lw->dir[0],
+ yaw = lw->dir[1],
+ xz = cosf( pitch );
+
+ v3_copy( (v3f){ xz*cosf(yaw), sinf(pitch), xz*sinf(yaw) },
+ winf->g_light_directions[c] );
+ v3_copy( lw->colour, winf->g_light_colours[c] );
+
+ c ++;
+ }
+ }
+
+ winf->g_light_count = c;
+ winf->g_light_directions[0][3] = gpipeline.shadow_length;
+ winf->g_light_colours[0][3] = gpipeline.shadow_spread;
+
+ if( vg.quality_profile == k_quality_profile_low )
+ winf->g_shadow_samples = 0;
+ else
+ winf->g_shadow_samples = 8;
+
+ glBindBuffer( GL_UNIFORM_BUFFER, gpipeline.ubo_world_lighting );
+ glBufferSubData( GL_UNIFORM_BUFFER, 0, sizeof(struct ub_world_lighting),
+ &gpipeline.ub_world_lighting );
+}
+
+#define FB_FORMAT_STR( E ) { E, #E },
+
+/*
+ * Convert OpenGL attachment ID enum to string
+ */
+VG_STATIC const char *render_fb_attachment_str( GLenum e )
+{
+ struct { GLenum e; const char *str; }
+ formats[] =
+ {
+ FB_FORMAT_STR(GL_COLOR_ATTACHMENT0)
+ FB_FORMAT_STR(GL_COLOR_ATTACHMENT1)
+ FB_FORMAT_STR(GL_COLOR_ATTACHMENT2)
+ FB_FORMAT_STR(GL_COLOR_ATTACHMENT3)
+ FB_FORMAT_STR(GL_COLOR_ATTACHMENT4)
+ FB_FORMAT_STR(GL_DEPTH_STENCIL_ATTACHMENT)
+ };
+
+ for( int i=0; i<vg_list_size(formats); i++ )
+ if( formats[i].e == e )
+ return formats[i].str;
+
+ return "UNDEFINED";
+}
+
+/*
+ * Convert OpenGL texture format enums from TexImage2D table 1,2 &
+ * RenderBufferStorage Table 1, into strings
+ */
+VG_STATIC const char *render_fb_format_str( GLenum format )