]> git.tdb.fi Git - libs/gl.git/commitdiff
A complex demo program to show off features of the engine
authorMikko Rasa <tdb@tdb.fi>
Wed, 12 Sep 2012 18:58:34 +0000 (21:58 +0300)
committerMikko Rasa <tdb@tdb.fi>
Wed, 12 Sep 2012 19:15:52 +0000 (22:15 +0300)
.gitignore
Build
demos/desertpillars.cpp [new file with mode: 0644]

index c7a1afbdce28079cbb849d5fe3fdadb1af350a51..62e48cab2a7d02f942034474cd4dcf19c18c2be2 100644 (file)
@@ -1,6 +1,7 @@
 .config
 temp
 /cubemap
 .config
 temp
 /cubemap
+/desertpillars
 /libmspgl.a
 /libmspgl.so
 /mesh2c
 /libmspgl.a
 /libmspgl.so
 /mesh2c
diff --git a/Build b/Build
index 4ea0aa793964f6e0141c196db46337b871e49f5b..8c537eb76051d49ad8d9cc8a0f8a2fb09f574720 100644 (file)
--- a/Build
+++ b/Build
@@ -63,6 +63,15 @@ package "mspgl"
                };
        };
 
                };
        };
 
+       program "desertpillars"
+       {
+               source "demos/desertpillars.cpp";
+               build_info
+               {
+                       library "mspgl";
+               };
+       };
+
        source_tarball
        {
                source "License.txt";
        source_tarball
        {
                source "License.txt";
diff --git a/demos/desertpillars.cpp b/demos/desertpillars.cpp
new file mode 100644 (file)
index 0000000..61db0ba
--- /dev/null
@@ -0,0 +1,951 @@
+#include <cmath>
+#include <cstdlib>
+#include <msp/core/application.h>
+#include <msp/core/getopt.h>
+#include <msp/gl/animatedobject.h>
+#include <msp/gl/bloom.h>
+#include <msp/gl/box.h>
+#include <msp/gl/cylinder.h>
+#include <msp/gl/environmentmap.h>
+#include <msp/gl/framebuffer.h>
+#include <msp/gl/grid.h>
+#include <msp/gl/instancescene.h>
+#include <msp/gl/light.h>
+#include <msp/gl/lighting.h>
+#include <msp/gl/material.h>
+#include <msp/gl/mesh.h>
+#include <msp/gl/meshbuilder.h>
+#include <msp/gl/object.h>
+#include <msp/gl/pipeline.h>
+#include <msp/gl/program.h>
+#include <msp/gl/programbuilder.h>
+#include <msp/gl/renderer.h>
+#include <msp/gl/shader.h>
+#include <msp/gl/shadowmap.h>
+#include <msp/gl/simplescene.h>
+#include <msp/gl/technique.h>
+#include <msp/gl/tests.h>
+#include <msp/gl/texturecube.h>
+#include <msp/graphics/simplewindow.h>
+#include <msp/input/keyboard.h>
+#include <msp/input/keys.h>
+#include <msp/time/timestamp.h>
+#include <msp/time/units.h>
+#include <msp/time/utils.h>
+
+using namespace std;
+using namespace Msp;
+
+/**
+This application demonstrates a variety of features of the mspgl library,
+including:
+- Creating meshes from multiple parts
+- Creating a mesh and then modifying it
+- Shadow mapping
+- Environment mapped reflections
+- Skybox
+- Nested scenes and pipelines
+- Complex multitexturing
+- Shader-based deformations
+- Creating a normalmapped texture through rendering
+
+To run the program in fullscreen mode, specify --fullscreen on the command
+line.
+
+During execution the following keys are available:
+esc    exit
+space  stop camera movement
+s      stop cube rotation
+f      freeze cube shape
+*/
+class DesertPillars: public RegisteredApplication<DesertPillars>
+{
+private:
+       struct Options
+       {
+               Graphics::WindowOptions window_opts;
+
+               Options(const Graphics::Display &, int, char **);
+       };
+
+       struct ObjectData
+       {
+               GL::Mesh *mesh;
+               GL::Object *object;
+
+               ObjectData();
+               ~ObjectData();
+       };
+
+       class Cube: public GL::AnimatedObject
+       {
+       private:
+               GL::ProgramData shdata;
+
+       public:
+               Cube(const GL::Object &);
+
+               void set_spherify(float);
+
+               virtual void setup_render(GL::Renderer &, const GL::Tag &) const;
+       };
+
+       Msp::Graphics::Display display;
+       Options options;
+       Msp::Graphics::Window window;
+       Msp::Graphics::GLContext gl_context;
+       Msp::Input::Keyboard keyboard;
+
+       GL::Program skybox_shprog;
+       GL::Technique skybox_tech;
+       GL::TextureCube skybox_tex;
+       ObjectData skybox_data;
+
+       GL::Program shadow_shprog;
+
+       GL::Program ground_shprog;
+       GL::ProgramData ground_shdata;
+       GL::Texture2D tiles_texture;
+       GL::Texture2D tiles_normalmap;
+       GL::Texture2D sand_texture;
+       GL::Texture2D sand_normalmap;
+       GL::Technique ground_tech;
+       ObjectData ground_data;
+
+       GL::Program pillar_shprog;
+       GL::Material pillar_material;
+       GL::Technique pillar_tech;
+       std::vector<ObjectData> pillar_data;
+       std::vector<GL::AnimatedObject *> pillars;
+
+       GL::VertexShader cube_transform;
+       GL::Program cube_shprog;
+       GL::Program cube_shadow_shprog;
+       GL::Material cube_material;
+       GL::Technique cube_tech;
+       ObjectData cube_data;
+       Cube *cube;
+       GL::EnvironmentMap *env_cube;
+
+       GL::Pipeline pipeline;
+       GL::Camera camera;
+       GL::SimpleScene sky_scene;
+       GL::InstanceScene scene;
+       GL::Lighting lighting;
+       GL::Light light;
+       GL::ShadowMap shadow_scene;
+       GL::Bloom bloom;
+
+       GL::Pipeline env_pipeline;
+
+       Time::TimeStamp last_tick;
+       float camera_angle;
+       bool camera_stopped;
+       float cube_angle;
+       bool cube_stopped;
+       unsigned cube_shape;
+       float cube_phase;
+       bool cube_frozen;
+
+       static const char texture_vertex_src[];
+       static const char texture_fragment_src[];
+       static const char skybox_vertex_src[];
+       static const char skybox_fragment_src[];
+       static const char ground_transform_src[];
+       static const char ground_colorify_src[];
+       static const char cube_transform_src[];
+       static const float cube_shapes[];
+
+public:
+       DesertPillars(int, char **);
+       ~DesertPillars();
+
+private:
+       void create_pipeline();
+       void create_skybox();
+       static void create_skybox_face(GL::TextureCube &, GL::TextureCubeFace);
+       void create_tiles_texture();
+       void create_sand_texture();
+       static void gaussian_blur(unsigned char *, unsigned, unsigned);
+       static void create_normalmap(const unsigned char *, unsigned char *, unsigned, unsigned, float);
+       void create_ground();
+       static float ground_height(float, float);
+       void create_pillars();
+       void create_cube();
+       static void create_cube_face(GL::MeshBuilder &, const GL::Vector3 &, const GL::Vector3 &, const GL::Vector3 &, unsigned);
+
+public:
+       virtual int main();
+private:
+       virtual void tick();
+
+       void key_press(unsigned);
+};
+
+const char DesertPillars::texture_vertex_src[] =
+       "varying vec3 v_normal;\n"
+       "varying vec3 v_color;\n"
+       "void main()\n"
+       "{\n"
+       "       gl_Position = vec4(gl_Vertex.xy*2.0-1.0, -gl_Vertex.z*2.0, 1.0);\n"
+       "       v_normal = gl_Normal;\n"
+       "       v_color = gl_Color.rgb;\n"
+       "}\n";
+
+const char DesertPillars::texture_fragment_src[] =
+       "varying vec3 v_normal;\n"
+       "varying vec3 v_color;\n"
+       "void main()\n"
+       "{\n"
+       "       gl_FragData[0] = vec4(v_color, 1.0);\n"
+       "       gl_FragData[1] = vec4(v_normal*0.5+0.5, 1.0);\n"
+       "}\n";
+
+const char DesertPillars::skybox_vertex_src[] =
+       "varying vec3 v_texcoord;\n"
+       "void main()\n"
+       "{\n"
+       "       gl_Position = gl_ProjectionMatrix*vec4(mat3(gl_ModelViewMatrix)*gl_Vertex.xyz, 1.0);\n"
+       "       v_texcoord = gl_Vertex;\n"
+       "}";
+
+const char DesertPillars::skybox_fragment_src[] =
+       "uniform samplerCube sky;\n"
+       "varying vec3 v_texcoord;\n"
+       "void main()\n"
+       "{\n"
+       "       gl_FragColor = textureCube(sky, v_texcoord);\n"
+       "}";
+
+// This exists only to transfer the ground type to fragment shader
+const char DesertPillars::ground_transform_src[] =
+       "attribute float ground_type;\n"
+       "varying float v_ground_type;\n"
+       "vec4 transform_vertex(vec4 vertex)\n"
+       "{\n"
+       "       v_ground_type = ground_type;\n"
+       "       return gl_ModelViewMatrix*vertex;\n"
+       "}\n"
+       "vec3 transform_normal(vec3 normal)\n"
+       "{\n"
+       "       return gl_NormalMatrix*normal;\n"
+       "}\n";
+
+const char DesertPillars::ground_colorify_src[] =
+       "uniform sampler2D texture1;\n"
+       "uniform sampler2D normalmap1;\n"
+       "uniform sampler2D texture2;\n"
+       "uniform sampler2D normalmap2;\n"
+       "varying float v_ground_type;\n"
+       "vec4 sample_texture(vec2 coord)\n"
+       "{\n"
+       "       return mix(texture2D(texture1, coord*3), texture2D(texture2, coord), v_ground_type);\n"
+       "}\n"
+       "vec3 sample_normalmap(vec2 coord)\n"
+       "{\n"
+       "       return mix(texture2D(normalmap1, coord*3), texture2D(normalmap2, coord), v_ground_type);\n"
+       "}\n";
+
+const char DesertPillars::cube_transform_src[] =
+       "uniform float spherify;\n"
+       "attribute vec3 sphere_coord;\n"
+       "vec4 transform_vertex(vec4 vertex)\n"
+       "{\n"
+       "       return gl_ModelViewMatrix*vec4(mix(vertex, sphere_coord, spherify), 1.0);\n"
+       "}\n"
+       "vec3 transform_normal(vec3 normal)\n"
+       "{\n"
+       "       return gl_NormalMatrix*normalize(mix(normal, normalize(sphere_coord), spherify));\n"
+       "}\n";
+
+const float DesertPillars::cube_shapes[] = { -0.4, 0.5, 1.0, 0.3 };
+
+
+DesertPillars::Options::Options(const Graphics::Display &display, int argc, char **argv)
+{
+       GetOpt getopt;
+       getopt.add_option('f', "fullscreen", window_opts.fullscreen, GetOpt::NO_ARG).set_help("Run in fullscreen mode");
+       getopt(argc, argv);
+
+       if(window_opts.fullscreen)
+       {
+               const Graphics::VideoMode &mode = display.get_desktop_mode();
+               window_opts.width = mode.width;
+               window_opts.height = mode.height;
+       }
+       else
+       {
+               window_opts.width = 800;
+               window_opts.height = 600;
+       }
+}
+
+
+DesertPillars::DesertPillars(int argc, char **argv):
+       options(display, argc, argv),
+       window(display, options.window_opts),
+       gl_context(window),
+       keyboard(window),
+       shadow_shprog(GL::ProgramBuilder::StandardFeatures()),
+       pipeline(window.get_width(), window.get_height()),
+       shadow_scene(2048, scene, light),
+       bloom(window.get_width(), window.get_height()),
+       env_pipeline(512, 512),
+       camera_angle(0),
+       camera_stopped(false),
+       cube_angle(0),
+       cube_stopped(false),
+       cube_shape(0),
+       cube_phase(0),
+       cube_frozen(false)
+{
+       window.set_title("Desert Pillars");
+       window.signal_close.connect(sigc::bind(sigc::mem_fun(this, &DesertPillars::exit), 0));
+       if(options.window_opts.fullscreen)
+               window.show_cursor(false);
+       keyboard.signal_button_press.connect(sigc::mem_fun(this, &DesertPillars::key_press));
+
+       create_pipeline();
+       create_skybox();
+       create_ground();
+       create_pillars();
+       create_cube();
+}
+
+DesertPillars::~DesertPillars()
+{
+       delete env_cube;
+       delete cube;
+       for(vector<GL::AnimatedObject *>::iterator i=pillars.begin(); i!=pillars.end(); ++i)
+               delete *i;
+}
+
+void DesertPillars::create_pipeline()
+{
+       pipeline.set_multisample(8);
+
+       camera.set_aspect(float(window.get_width())/window.get_height());
+       camera.set_up_direction(GL::Vector3(0, 0, 1));
+       camera.set_depth_clip(1, 50);
+       pipeline.set_camera(&camera);
+
+       /* The shadow map is focused on the part of the scene that contains the
+       pillars and the cube.  Making the ground cast shadows as well would result
+       either in a very low spatial resolution of the shadow map, or ugly artifacts
+       as the ground crosses the shadow map boundary. */
+       shadow_scene.set_target(GL::Vector3(0, 0, 0), 10);
+       shadow_scene.set_texture_unit(5);
+       sky_scene.add(shadow_scene);
+       pipeline.add_renderable(sky_scene);
+
+       // Put the sun pretty high in the sky
+       light.set_position(GL::Vector4(0.5, -2, 3, 0));
+       lighting.attach(0, light);
+       lighting.set_ambient(GL::Color(0.5));
+
+       // The skybox is rendered first
+       pipeline.add_pass("sky");
+
+       GL::Pipeline::Pass *pass = &pipeline.add_pass(0);
+       pass->set_lighting(&lighting);
+       pass->set_depth_test(&GL::DepthTest::lequal());
+
+       /* A bloom filter enhances the realism of bright surfaces, even if there
+       isn't anything really glowy in the scene. */
+       bloom.set_strength(0.3);
+       pipeline.add_postprocessor(bloom);
+
+       /* Initialize a second pipeline to render the environment map.  It has the
+       same renderables and passes, but no postprocessors or camera. */
+       env_pipeline.add_renderable(sky_scene);
+       env_pipeline.add_pass("sky");
+       pass = &env_pipeline.add_pass(0);
+       pass->set_lighting(&lighting);
+       pass->set_depth_test(&GL::DepthTest::lequal());
+}
+
+void DesertPillars::create_skybox()
+{
+       skybox_tex.storage(GL::RGB, 128);
+       skybox_tex.set_min_filter(GL::LINEAR);
+       skybox_tex.set_wrap(GL::CLAMP_TO_EDGE);
+       for(unsigned i=0; i<6; ++i)
+               create_skybox_face(skybox_tex, skybox_tex.enumerate_faces(i));
+
+       skybox_shprog.attach_shader_owned(new GL::VertexShader(skybox_vertex_src));
+       skybox_shprog.attach_shader_owned(new GL::FragmentShader(skybox_fragment_src));
+       skybox_shprog.link();
+
+       GL::RenderPass &pass = skybox_tech.add_pass("sky");
+       pass.set_shader_program(&skybox_shprog, 0);
+       pass.set_texture(0, &skybox_tex);
+
+       // The shader will use the vertex coordinates to initialize texture coordinates as well
+       skybox_data.mesh = new GL::Mesh(GL::VERTEX3);
+       GL::BoxBuilder(10, 10, 10).build(*skybox_data.mesh);
+       skybox_data.object = new GL::Object(skybox_data.mesh, &skybox_tech);
+
+       sky_scene.add(*skybox_data.object);
+}
+
+void DesertPillars::create_skybox_face(GL::TextureCube &texture, GL::TextureCubeFace face)
+{
+       unsigned char *pixels = new unsigned char[128*128*3];
+       for(int y=0; y<128; ++y)
+               for(int x=0; x<128; ++x)
+               {
+                       unsigned i = (x+y*128)*3;
+                       GL::Vector3 v = texture.get_texel_direction(face, x, y);
+                       if(v.z>0)
+                       {
+                               float l = sqrt(v.x*v.x+v.y*v.y+v.z*v.z);
+                               // Render a sky-like gradient, with deeper blue at the zenith
+                               pixels[i] = 96-48*v.z/l;
+                               pixels[i+1] = 168-84*v.z/l;
+                               pixels[i+2] = 255;
+                       }
+                       else
+                       {
+                               // Fill with a desert-y color below horizon
+                               pixels[i] = 240;
+                               pixels[i+1] = 224;
+                               pixels[i+2] = 160;
+                       }
+               }
+       texture.image(face, 0, GL::RGB, GL::UNSIGNED_BYTE, pixels);
+       delete[] pixels;
+}
+
+void DesertPillars::create_tiles_texture()
+{
+       unsigned width = 256;
+       unsigned height = 256;
+       tiles_texture.storage(GL::RGB, width, height);
+       tiles_texture.set_min_filter(GL::LINEAR);
+       tiles_normalmap.storage(GL::RGB, width, height);
+       tiles_normalmap.set_min_filter(GL::LINEAR);
+
+       GL::Mesh tiles((GL::VERTEX3, GL::NORMAL3, GL::COLOR4_UBYTE));
+
+       // Prepare some lookup tables for rendering the tiles
+       float split = 1.0f/3;
+       float spacing = split*0.02f;
+       float bevel = split*0.1f;
+       float coords[] = { 0.0f, spacing, spacing+bevel, split-bevel, split, split+spacing, split+spacing+bevel, 1.0f-bevel, 1.0f };
+       unsigned order[] = { 4, 1, 3, 2, 1, 1, 2, 2,
+               1, 4, 2, 3, 4, 4, 3, 3,
+               1, 1, 2, 2, 1, 4, 2, 3,
+               4, 4, 3, 3, 4, 1, 3, 2,
+               2, 3, 2, 2, 3, 3, 3, 2 };
+
+       GL::MeshBuilder bld(tiles);
+
+       // Create a dark background
+       bld.color(0.2f, 0.2f, 0.2f);
+       bld.normal(0.0f, 0.0f, 1.0f);
+       bld.begin(GL::TRIANGLE_STRIP);
+       bld.vertex(0.0f, 1.0f);
+       bld.vertex(0.0f, 0.0f);
+       bld.vertex(1.0f, 1.0f);
+       bld.vertex(1.0f, 0.0f);
+       bld.end();
+
+       // Create the four tiles
+       bld.color(0.95f, 0.8f, 0.65f);
+       for(unsigned i=0; i<2; ++i)
+               for(unsigned j=0; j<2; ++j)
+               {
+                       for(unsigned k=0; k<4; ++k)
+                       {
+                               bld.begin(GL::TRIANGLE_STRIP);
+                               float facing = (k%2)*2-1.0f;
+                               if(k<2)
+                                       bld.normal(0.0f, 0.7071f*facing, 0.7071f);
+                               else
+                                       bld.normal(0.7071f*facing, 0.0f, 0.7071f);
+                               for(unsigned l=0; l<4; ++l)
+                                       bld.vertex(coords[i*4+order[k*8+l*2]], coords[j*4+order[k*8+l*2+1]], (l%2 ? bevel : 0.0f));
+                               bld.end();
+                       }
+
+                       bld.begin(GL::TRIANGLE_STRIP);
+                       bld.normal(0.0f, 0.0f, 1.0f);
+                       for(unsigned l=0; l<4; ++l)
+                               bld.vertex(coords[i*4+order[32+l*2]], coords[j*4+order[32+l*2+1]], bevel);
+                       bld.end();
+               }
+
+       GL::Program shprog(texture_vertex_src, texture_fragment_src);
+
+       // Use an FBO to turn the geometry into a normalmapped texture
+       GL::Framebuffer fbo;
+       fbo.attach(GL::COLOR_ATTACHMENT0, tiles_texture);
+       fbo.attach(GL::COLOR_ATTACHMENT1, tiles_normalmap);
+       GL::Bind bind_fbo(fbo);
+       GL::Renderer renderer(0);
+       renderer.set_shader_program(&shprog, 0);
+       tiles.draw(renderer);
+}
+
+void DesertPillars::create_sand_texture()
+{
+       unsigned width = 512;
+       unsigned height = 512;
+
+       sand_texture.storage(GL::RGB, width/16, height/16);
+       sand_texture.set_min_filter(GL::LINEAR_MIPMAP_LINEAR);
+       sand_texture.set_max_anisotropy(4);
+       sand_texture.set_generate_mipmap(true);
+       sand_normalmap.storage(GL::RGB, width, height);
+       sand_normalmap.set_min_filter(GL::LINEAR_MIPMAP_LINEAR);
+       sand_normalmap.set_max_anisotropy(4);
+       sand_normalmap.set_generate_mipmap(true);
+
+       unsigned char *pixels = new unsigned char[width*height*3];
+       unsigned char *bump = new unsigned char[width*height];
+       for(unsigned y=0; y<height; ++y)
+               for(unsigned x=0; x<width; ++x)
+               {
+                       unsigned i = (x+y*width)*3;
+                       unsigned c = rand()%16;
+                       pixels[i] = 224+c;
+                       pixels[i+1] = 208+c;
+                       pixels[i+2] = 160;
+                       bump[x+y*width] = rand();
+               }
+       sand_texture.image(0, GL::RGB, GL::UNSIGNED_BYTE, pixels);
+       gaussian_blur(bump, width, height);
+       create_normalmap(bump, pixels, width, height, 4);
+       sand_normalmap.image(0, GL::RGB, GL::UNSIGNED_BYTE, pixels);
+       delete[] pixels;
+       delete[] bump;
+}
+
+void DesertPillars::gaussian_blur(unsigned char *data, unsigned width, unsigned height)
+{
+       /* Create a gaussian blur kernel for σ=2.  Gaussian blur is separable, so a
+       1-dimensional kernel is enough. */
+       float kernel[9];
+       float sum = 0;
+       for(int i=-4; i<=4; ++i)
+               sum += (kernel[4+i] = exp(-i*i*0.5));
+       for(unsigned i=0; i<9; ++i)
+               kernel[i] /= sum;
+
+       unsigned char *line = new unsigned char[max(width, height)];
+       // Perform the blur in the X direction
+       for(unsigned y=0; y<height; ++y)
+       {
+               for(unsigned x=0; x<width; ++x)
+               {
+                       float value = 0;
+                       for(int i=-4; i<=4; ++i)
+                               value += data[(x+width+i)%width+y*width]*kernel[4+i];
+                       line[x] = value;
+               }
+               copy(line, line+width, data+y*width);
+       }
+       // And then in the Y direction
+       for(unsigned x=0; x<width; ++x)
+       {
+               for(unsigned y=0; y<height; ++y)
+               {
+                       float value = 0;
+                       for(int i=-4; i<=4; ++i)
+                               value += data[x+((y+height+i)%height)*width]*kernel[4+i];
+                       line[y] = value;
+               }
+               for(unsigned y=0; y<height; ++y)
+                       data[x+y*width] = line[y];
+       }
+       delete[] line;
+}
+
+void DesertPillars::create_normalmap(const unsigned char *bump, unsigned char *normals, unsigned width, unsigned height, float depth)
+{
+       for(unsigned y=0; y<height; ++y)
+               for(unsigned x=0; x<width; ++x)
+               {
+                       float dz_x = (bump[(x+1)%width+y*width]-bump[(x+width-1)%width+y*width])*depth/510;
+                       float dz_y = (bump[x+((y+1)%height)*width]-bump[x+((y+height-1)%height)*width])*depth/510;
+                       float l = sqrt(dz_x*dz_x+dz_y*dz_y+1);
+                       unsigned i = (x+y*width)*3;
+                       normals[i] = (0.5-0.5*dz_x/l)*255;
+                       normals[i+1] = (0.5-0.5*dz_y/l)*255;
+                       normals[i+2] = (0.5+0.5/l)*255;
+               }
+}
+
+void DesertPillars::create_ground()
+{
+       create_tiles_texture();
+       create_sand_texture();
+
+       GL::ProgramBuilder::StandardFeatures features;
+       features.lighting = true;
+       features.shadow = true;
+       features.texture = true;
+       features.normalmap = true;
+       features.transform = true;
+       features.colorify = true;
+       GL::ProgramBuilder(features).add_shaders(ground_shprog);
+       ground_shprog.attach_shader_owned(new GL::VertexShader(ground_transform_src));
+       ground_shprog.attach_shader_owned(new GL::FragmentShader(ground_colorify_src));
+       ground_shprog.bind_attribute(7, "ground_type");
+       ground_shprog.link();
+
+       ground_shdata.uniform("texture1", 0);
+       ground_shdata.uniform("normalmap1", 1);
+       ground_shdata.uniform("texture2", 2);
+       ground_shdata.uniform("normalmap2", 3);
+
+       GL::RenderPass *pass = &ground_tech.add_pass(0);
+       pass->set_shader_program(&ground_shprog, &ground_shdata);
+       pass->set_texture(0, &tiles_texture);
+       pass->set_texture(1, &tiles_normalmap);
+       pass->set_texture(2, &sand_texture);
+       pass->set_texture(3, &sand_normalmap);
+
+       /* No shadow pass here; the ground only receives shadows, but doesn't cast
+       them. */
+
+       GL::VertexFormat vfmt = (GL::VERTEX3, GL::NORMAL3, GL::TANGENT3, GL::BINORMAL3, GL::TEXCOORD2, GL::ATTRIB1,7);
+       ground_data.mesh = new GL::Mesh(vfmt);
+
+       // Create a base grid
+       GL::GridBuilder(80, 80, 200, 200).tbn().build(*ground_data.mesh);
+
+       // And modify it with a heightmap
+       unsigned n_vertices = ground_data.mesh->get_n_vertices();
+       unsigned pos = vfmt.offset(GL::VERTEX3);
+       unsigned nor = vfmt.offset(GL::NORMAL3);
+       unsigned tan = vfmt.offset(GL::TANGENT3);
+       unsigned bin = vfmt.offset(GL::BINORMAL3);
+       unsigned tex = vfmt.offset(GL::TEXCOORD2);
+       unsigned gt = vfmt.offset(GL::make_indexed_component(GL::ATTRIB1, 7));
+       for(unsigned i=0; i<n_vertices; ++i)
+       {
+               float *v = ground_data.mesh->modify_vertex(i);
+               v[pos+2] = ground_height(v[pos], v[pos+1]);
+
+               float dz_x = (ground_height(v[pos]+0.01, v[pos+1])-ground_height(v[pos]-0.01, v[pos+1]))/0.02;
+               float dz_y = (ground_height(v[pos], v[pos+1]+0.01)-ground_height(v[pos], v[pos+1]-0.01))/0.02;
+               float l = sqrt(dz_x*dz_x+dz_y*dz_y+1);
+               v[nor] = -dz_x/l;
+               v[nor+1] = -dz_y/l;
+               v[nor+2] = 1/l;
+
+               l = sqrt(dz_x*dz_x+1);
+               v[tan] = 1/l;
+               v[tan+2] = dz_x/l;
+
+               l = sqrt(dz_y*dz_y+1);
+               v[bin+1] = 1/l;
+               v[bin+2] = dz_y/l;
+
+               v[gt] = min(v[pos+2]*100, 1.0f);
+
+               v[tex] *= 20;
+               v[tex+1] *= 20;
+       }
+       ground_data.object = new GL::Object(ground_data.mesh, &ground_tech);
+
+       scene.add(*ground_data.object);
+}
+
+float DesertPillars::ground_height(float x, float y)
+{
+       // Leave a flat area in the middle
+       float d = sqrt(x*x+y*y);
+       if(d<=6.0f)
+               return 0;
+
+       // This results in concentric rings of low hills
+       int i = (d-6)/(M_PI*2);
+       float a = atan2(y, x);
+       a *= i*2+5;
+       a += M_PI*(i%3)/2;
+       float h = (i%2) ? 0.5 : 0.3;
+       return (d-6)*0.001f+(1-cos(d-6))*(1-cos(a))*h;
+}
+
+void DesertPillars::create_pillars()
+{
+       // The pillars are a matt off-white
+       pillar_material.set_diffuse(GL::Color(0.9, 0.88, 0.8));
+       pillar_material.set_ambient(GL::Color(0.9, 0.88, 0.8));
+
+       GL::ProgramBuilder::StandardFeatures features;
+       features.lighting = true;
+       features.material = true;
+       features.shadow = true;
+       GL::ProgramBuilder(features).add_shaders(pillar_shprog);
+       pillar_shprog.link();
+
+       GL::RenderPass *pass = &pillar_tech.add_pass(0);
+       pass->set_material(&pillar_material);
+       pass->set_shader_program(&pillar_shprog, 0);
+
+       pass = &pillar_tech.add_pass("shadow");
+       pass->set_shader_program(&shadow_shprog, 0);
+
+       pillar_data.reserve(7);
+       for(unsigned i=3; i<=20; ++i)
+       {
+               unsigned height = 2;
+               for(unsigned j=2; j<i; ++j)
+                       if(i%j==0)
+                               ++height;
+
+               if(pillar_data.size()<=height)
+                       pillar_data.resize(height+1);
+
+               ObjectData &pd = pillar_data[height];
+               if(!pd.object)
+               {
+                       pd.mesh = new GL::Mesh((GL::VERTEX3, GL::NORMAL3));
+                       GL::MeshBuilder bld(*pd.mesh);
+
+                       // Produce a fluted cylinder
+                       unsigned n_flutes = 12;
+                       float r_bottom = cos(M_PI/n_flutes)*0.4;
+                       float flute_depth = (0.4-r_bottom)*2;
+                       float half_w = sin(M_PI/n_flutes)*0.4;
+                       for(unsigned j=0; j<n_flutes; ++j)
+                       {
+                               float a = j*M_PI*2/n_flutes;
+                               bld.begin(GL::TRIANGLE_STRIP);
+                               for(int k=-3; k<=3; k+=2)
+                               {
+                                       float t = k/3.0f;
+                                       float x = cos(a)*(r_bottom-(1-t*t)*flute_depth)-sin(a)*t*half_w;
+                                       float y = sin(a)*(r_bottom-(1-t*t)*flute_depth)+cos(a)*t*half_w;
+                                       float d = -t*2*flute_depth;
+                                       float l = sqrt(d*d+1);
+                                       bld.normal(cos(a)/l-sin(a)*d/l, sin(a)/l+cos(a)*d/l, 0);
+                                       bld.vertex(x, y, 0.6+height);
+                                       bld.vertex(x, y, 0.6);
+                               }
+                               bld.end();
+                       }
+
+                       // Create a square plinth and capitel
+                       bld.matrix() = GL::Matrix::translation(0, 0, 0.3);
+                       GL::BoxBuilder(1.0, 1.0, 0.6).build(bld);
+                       bld.matrix() = GL::Matrix::translation(0, 0, height+0.8);
+                       GL::BoxBuilder(1.0, 1.0, 0.4).build(bld);
+
+                       pd.object = new GL::Object(pd.mesh, &pillar_tech);
+               }
+
+               GL::AnimatedObject *pillar = new GL::AnimatedObject(*pd.object);
+               GL::Matrix matrix;
+               float a = (i-3)*2*M_PI/18;
+               matrix.translate(cos(a)*5, sin(a)*5, 0);
+               matrix.rotate(a, 0, 0, 1);
+               pillar->set_matrix(matrix);
+
+               pillars.push_back(pillar);
+               scene.add(*pillar);
+       }
+}
+
+void DesertPillars::create_cube()
+{
+       /* The cube is bluish-gray, with a hard specular reflection to produce a
+       sun-like spot */
+       cube_material.set_diffuse(GL::Color(0.5, 0.5, 0.55));
+       cube_material.set_ambient(GL::Color(0.5, 0.5, 0.55));
+       cube_material.set_specular(GL::Color(1.0));
+       cube_material.set_shininess(150);
+
+       cube_transform.source(cube_transform_src);
+       cube_transform.compile();
+
+       // First create a simplified shader for rendering the shadow map
+       GL::ProgramBuilder::StandardFeatures features;
+       features.transform = true;
+       GL::ProgramBuilder(features).add_shaders(cube_shadow_shprog);
+       cube_shadow_shprog.attach_shader(cube_transform);
+       cube_shadow_shprog.bind_attribute(7, "sphere_coord");
+       cube_shadow_shprog.link();
+
+       // Then add the rest of the features for normal rendering
+       features.lighting = true;
+       features.specular = true;
+       features.material = true;
+       features.shadow = true;
+       features.reflection = true;
+       GL::ProgramBuilder(features).add_shaders(cube_shprog);
+       cube_shprog.attach_shader(cube_transform);
+       cube_shprog.bind_attribute(7, "sphere_coord");
+       cube_shprog.link();
+
+       GL::RenderPass *pass = &cube_tech.add_pass(0);
+       pass->set_material(&cube_material);
+       pass->set_shader_program(&cube_shprog, 0);
+
+       pass = &cube_tech.add_pass("shadow");
+       pass->set_shader_program(&cube_shadow_shprog, 0);
+
+       cube_data.mesh = new GL::Mesh((GL::VERTEX3, GL::NORMAL3, GL::ATTRIB3,7));
+       GL::MeshBuilder bld(*cube_data.mesh);
+       create_cube_face(bld, GL::Vector3(-1, -1, -1), GL::Vector3(2, 0, 0), GL::Vector3(0, 2, 0), 16);
+       bld.offset(cube_data.mesh->get_n_vertices());
+       create_cube_face(bld, GL::Vector3(-1, 1, -1), GL::Vector3(2, 0, 0), GL::Vector3(0, 0, 2), 16);
+       bld.offset(cube_data.mesh->get_n_vertices());
+       create_cube_face(bld, GL::Vector3(-1, 1, 1), GL::Vector3(2, 0, 0), GL::Vector3(0, -2, 0), 16);
+       bld.offset(cube_data.mesh->get_n_vertices());
+       create_cube_face(bld, GL::Vector3(1, -1, -1), GL::Vector3(-2, 0, 0), GL::Vector3(0, 0, 2), 16);
+       bld.offset(cube_data.mesh->get_n_vertices());
+       create_cube_face(bld, GL::Vector3(-1, -1, 1), GL::Vector3(0, 0, -2), GL::Vector3(0, 2, 0), 16);
+       bld.offset(cube_data.mesh->get_n_vertices());
+       create_cube_face(bld, GL::Vector3(1, -1, -1), GL::Vector3(0, 0, 2), GL::Vector3(0, 2, 0), 16);
+       cube_data.object = new GL::Object(cube_data.mesh, &cube_tech);
+
+       cube = new Cube(*cube_data.object);
+       env_cube = new GL::EnvironmentMap(512, *cube, env_pipeline);
+       scene.add(*env_cube);
+}
+
+void DesertPillars::create_cube_face(GL::MeshBuilder &bld, const GL::Vector3 &base, const GL::Vector3 &side1, const GL::Vector3 &side2, unsigned div)
+{
+       /* The sides follow the cube map convention where the cross product points
+       inwards.  Since the normal has to point outwards, reverse the order. */
+       GL::Vector3 n;
+       n.x = side2.y*side1.z-side2.z*side1.y;
+       n.y = side2.z*side1.x-side2.x*side1.z;
+       n.z = side2.x*side1.y-side2.y*side1.x;
+       float l = sqrt(n.x*n.x+n.y*n.y+n.z*n.z);
+       bld.normal(n.x/l, n.y/l, n.z/l);
+
+       // Create vertices, with precomputed spherified coordinates
+       for(unsigned i=0; i<=div; ++i)
+               for(unsigned j=0; j<=div; ++j)
+               {
+                       GL::Vector3 v;
+                       v.x = base.x+side1.x*i/div+side2.x*j/div;
+                       v.y = base.y+side1.y*i/div+side2.y*j/div;
+                       v.z = base.z+side1.z*i/div+side2.z*j/div;
+
+                       l = sqrt(v.x*v.x+v.y*v.y+v.z*v.z);
+                       l /= 1.732;
+                       bld.attrib(7, v.x/l, v.y/l, v.z/l);
+
+                       bld.vertex(v);
+               }
+
+       for(unsigned i=0; i<div; ++i)
+       {
+               bld.begin(GL::TRIANGLE_STRIP);
+               for(unsigned j=0; j<=div; ++j)
+               {
+                       bld.element(i*(div+1)+j);
+                       bld.element((i+1)*(div+1)+j);
+               }
+               bld.end();
+       }
+}
+
+int DesertPillars::main()
+{
+       window.show();
+       return Application::main();
+}
+
+void DesertPillars::tick()
+{
+       Time::TimeStamp t = Time::now();
+       Time::TimeDelta dt;
+       if(last_tick)
+               dt = t-last_tick;
+       last_tick = t;
+
+       if(!camera_stopped)
+       {
+               camera_angle += (dt/Time::sec)*M_PI*2/30;
+               if(camera_angle>M_PI*4)
+                       camera_angle -= M_PI*4;
+               float h = 3+(1-cos(camera_angle*1.5))*3;
+               float r = sqrt(225-h*h);
+               camera.set_position(GL::Vector3(cos(camera_angle)*r, sin(camera_angle)*r, 1.5+h));
+               camera.look_at(GL::Vector3(0, 0, 2));
+       }
+
+       if(!cube_stopped)
+       {
+               cube_angle += (dt/Time::sec)*M_PI*2/20;
+               GL::Matrix cube_matrix;
+               cube_matrix.translate(0, 0, 2.5);
+               cube_matrix.rotate(cube_angle, 0, 0, 1);
+               cube_matrix.rotate(cube_angle*0.5, 1, 0, 0);
+               cube->set_matrix(cube_matrix);
+       }
+
+       if(!cube_frozen)
+       {
+               cube_phase += (dt/Time::sec)/5;
+               if(cube_phase>1)
+               {
+                       cube_phase -= 1;
+                       ++cube_shape;
+                       if(cube_shape>=4)
+                               cube_shape -= 4;
+               }
+               if(cube_phase<0.2)
+               {
+                       float x = cube_phase*5;
+                       x = (3-2*x)*x*x;
+                       cube->set_spherify((1-x)*cube_shapes[(cube_shape+3)%4]+x*cube_shapes[cube_shape]);
+               }
+               else
+                       cube->set_spherify(cube_shapes[cube_shape]);
+       }
+
+       display.tick();
+       GL::Framebuffer::system().clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT);
+       pipeline.render();
+       gl_context.swap_buffers();
+}
+
+void DesertPillars::key_press(unsigned key)
+{
+       if(key==Input::KEY_ESC)
+               exit(0);
+       else if(key==Input::KEY_SPACE)
+               camera_stopped = !camera_stopped;
+       else if(key==Input::KEY_F)
+               cube_frozen = !cube_frozen;
+       else if(key==Input::KEY_S)
+               cube_stopped = !cube_stopped;
+}
+
+
+DesertPillars::ObjectData::ObjectData():
+       mesh(0),
+       object(0)
+{ }
+
+DesertPillars::ObjectData::~ObjectData()
+{
+       delete object;
+       delete mesh;
+}
+
+
+DesertPillars::Cube::Cube(const GL::Object &obj):
+       GL::AnimatedObject(obj)
+{
+       shdata.uniform("reflectivity", 0.5f);
+}
+
+void DesertPillars::Cube::set_spherify(float s)
+{
+       shdata.uniform("spherify", s);
+}
+
+void DesertPillars::Cube::setup_render(GL::Renderer &renderer, const GL::Tag &tag) const
+{
+       AnimatedObject::setup_render(renderer, tag);
+       renderer.add_shader_data(shdata);
+}