gamnit: intro basic shadow mapping
authorAlexis Laferrière <alexis.laf@xymus.net>
Sat, 19 Aug 2017 18:57:19 +0000 (14:57 -0400)
committerAlexis Laferrière <alexis.laf@xymus.net>
Thu, 31 Aug 2017 14:23:58 +0000 (10:23 -0400)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/gamnit/depth/depth.nit
lib/gamnit/depth/depth_core.nit
lib/gamnit/depth/more_lights.nit [new file with mode: 0644]
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/shadow.nit [new file with mode: 0644]

index 1fe507a..1230c4a 100644 (file)
@@ -21,6 +21,7 @@ import more_models
 import model_dimensions
 import particles
 import selection
+import shadow
 
 redef class App
 
@@ -49,10 +50,18 @@ redef class App
        # Draw all elements of `actors` and then call `frame_core_flat`
        protected fun frame_core_depth(display: GamnitDisplay)
        do
+               frame_core_depth_clock.lapse
+
+               # Compute shadows
+               if light isa LightCastingShadows then
+                       frame_core_shadow_prep display
+                       perfs["gamnit depth shadows"].add frame_core_depth_clock.lapse
+               end
+
                glViewport(0, 0, display.width, display.height)
                frame_core_dynamic_resolution_before display
+               perfs["gamnit depth dynres"].add frame_core_depth_clock.lapse
 
-               frame_core_depth_clock.lapse
                for actor in actors do
                        for leaf in actor.model.leaves do
                                leaf.material.draw(actor, leaf, app.world_camera)
@@ -73,6 +82,9 @@ redef class App
                perfs["gamnit depth ui_sprites"].add frame_core_depth_clock.lapse
 
                frame_core_dynamic_resolution_after display
+
+               # Debug, show the light point of view
+               #frame_core_shadow_debug display
        end
 
        private var frame_core_depth_clock = new Clock
index 67ac1c0..56e0575 100644 (file)
@@ -203,23 +203,32 @@ class Mesh
 end
 
 # Source of light
-#
-# Instances of this class define a light source position and type.
-class Light
-
-       # TODO point light, spotlight, directional light, etc.
+abstract class Light
 
        # Center of this light source in world coordinates
        var position = new Point3d[Float](0.0, 1000.0, 0.0)
 end
 
+# Basic light projected from a single point
+class PointLight
+       super Light
+end
+
+# Source of light casting shadows
+abstract class LightCastingShadows
+       super Light
+
+       # View from the camera, used for shadow mapping, implemented by sub-classes
+       fun camera: Camera is abstract
+end
+
 redef class App
 
        # Live actors to be drawn on screen
        var actors = new Array[Actor]
 
        # Single light of the scene
-       var light = new Light
+       var light: Light = new PointLight is writable
 
        # TODO move `actors & light` to a scene object
        # TODO support more than 1 light
diff --git a/lib/gamnit/depth/more_lights.nit b/lib/gamnit/depth/more_lights.nit
new file mode 100644 (file)
index 0000000..7637e02
--- /dev/null
@@ -0,0 +1,75 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# More implementations of `Light`
+module more_lights
+
+import depth_core
+
+# TODO
+#class PointLight
+#class Spotlight
+
+# Sun-like light projecting parallel rays
+class ParallelLight
+       super LightCastingShadows
+
+       # Angle to the light source, around the X axis
+       var pitch = 0.0 is writable
+
+       # Angle to the light source, around the Y axis
+       var yaw = 0.0 is writable
+
+       # Depth texture width, in world coordinates
+       var width = 100.0 is writable
+
+       # Depth texture height, in world coordinates
+       var height = 100.0 is writable
+
+       # Viewport depth, centered on `app.world_camera`
+       var depth = 500.0 is writable
+
+       redef var camera = new ParallelLightCamera(app.display.as(not null), self) is lazy
+end
+
+private class ParallelLightCamera
+       super Camera
+
+       var light: ParallelLight
+
+       # Rotation matrix produced by the current rotation of the camera
+       fun rotation_matrix: Matrix
+       do
+               var view = new Matrix.identity(4)
+               view.rotate(light.yaw,   0.0, 1.0, 0.0)
+               view.rotate(light.pitch, 1.0, 0.0, 0.0)
+               return view
+       end
+
+       redef fun mvp_matrix
+       do
+               # TODO cache
+
+               var near = -light.depth/2.0
+               var far = light.depth/2.0
+
+               var view = new Matrix.identity(4)
+               view.translate(-position.x, -position.y, -position.z)
+               view = view * rotation_matrix
+               var projection = new Matrix.orthogonal(-light.width/2.0, light.width/2.0,
+                                                      -light.height/2.0, light.height/2.0,
+                                                      near, far)
+               return view * projection
+       end
+end
index 9843b37..d153954 100644 (file)
@@ -17,6 +17,8 @@ module more_materials
 
 intrude import depth_core
 intrude import flat
+intrude import shadow
+import more_lights
 
 redef class Material
        # Get the default blueish material
@@ -71,9 +73,6 @@ class SmoothMaterial
                program.use_map_specular.uniform false
                program.tex_coord.array_enabled = false
 
-               # Lights
-               program.light_center.uniform(app.light.position.x, app.light.position.y, app.light.position.z)
-
                # Camera
                program.camera.uniform(camera.position.x, camera.position.y, camera.position.z)
 
@@ -86,6 +85,8 @@ class SmoothMaterial
                program.specular_color.uniform(specular_color[0]*a, specular_color[1]*a,
                                               specular_color[2]*a, specular_color[3]*a)
 
+               setup_lights(actor, model, camera, program)
+
                # Execute draw
                if mesh.indices.is_empty then
                        glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
@@ -93,6 +94,46 @@ class SmoothMaterial
                        glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
                end
        end
+
+       private fun setup_lights(actor: Actor, model: LeafModel, camera: Camera, program: BlinnPhongProgram)
+       do
+               # TODO use a list of lights
+
+               # Light, for Lambert and Blinn-Phong
+               var light = app.light
+               if light isa ParallelLight then
+                       program.light_kind.uniform 1
+
+                       # Vector parallel to the light source
+                       program.light_center.uniform(
+                               -light.pitch.sin * light.yaw.sin,
+                               light.pitch.cos,
+                               -light.yaw.cos)
+               else if light isa PointLight then
+                       program.light_kind.uniform 2
+
+                       # Position of the light source
+                       program.light_center.uniform(app.light.position.x, app.light.position.y, app.light.position.z)
+               else
+                       program.light_kind.uniform 0
+               end
+
+               # Draw projected shadows?
+               if not light isa LightCastingShadows or not app.shadow_depth_texture_available then
+                       program.use_shadows.uniform false
+                       return
+               else program.use_shadows.uniform true
+
+               # Light point of view
+               program.light_mvp.uniform light.camera.mvp_matrix
+
+               # Depth texture
+               glActiveTexture gl_TEXTURE4
+               glBindTexture(gl_TEXTURE_2D, app.shadow_context.depth_texture)
+               program.depth_texture.uniform 4
+               program.depth_texture_size.uniform app.shadow_resolution.to_f
+               program.depth_texture_taps.uniform 2 # TODO make configurable
+       end
 end
 
 # Material with potential `diffuse_texture` and `specular_texture`
@@ -182,8 +223,6 @@ class TexturedMaterial
                                var xd = sample_used_texture.offset_right - xa
                                var ya = sample_used_texture.offset_top
                                var yd = sample_used_texture.offset_bottom - ya
-                               xd *= 0.999
-                               yd *= 0.999
 
                                var tex_coords = new Array[Float].with_capacity(mesh.texture_coords.length)
                                for i in [0..mesh.texture_coords.length/2[ do
@@ -211,7 +250,8 @@ class TexturedMaterial
                program.normal.array_enabled = true
                program.normal.array(mesh.normals, 3)
 
-               program.light_center.uniform(app.light.position.x, app.light.position.y, app.light.position.z)
+               # Light
+               setup_lights(actor, model, camera, program)
 
                # Camera
                program.camera.uniform(camera.position.x, camera.position.y, camera.position.z)
@@ -283,13 +323,15 @@ class BlinnPhongProgram
                // Vertex normal
                attribute vec3 normal;
 
-               // Model view projection matrix
+               // Camera model view projection matrix
                uniform mat4 mvp;
 
                uniform mat4 rotation;
 
                // Lights config
+               uniform int light_kind;
                uniform vec3 light_center;
+               uniform mat4 light_mvp;
 
                // Coordinates of the camera
                uniform vec3 camera;
@@ -299,17 +341,28 @@ class BlinnPhongProgram
                varying vec3 v_normal;
                varying vec4 v_to_light;
                varying vec4 v_to_camera;
+               varying vec4 v_depth_pos;
 
                void main()
                {
                        vec4 pos = (vec4(coord.xyz * scale, 1.0) * rotation + translation);
                        gl_Position = pos * mvp;
+                       v_depth_pos = (pos * light_mvp) * 0.5 + 0.5;
 
                        // Pass varyings to the fragment shader
                        v_tex_coord = vec2(tex_coord.x, 1.0 - tex_coord.y);
                        v_normal = normalize(vec4(normal, 0.0) * rotation).xyz;
-                       v_to_light = normalize(vec4(light_center, 1.0) - pos);
                        v_to_camera = normalize(vec4(camera, 1.0) - pos);
+
+                       if (light_kind == 0) {
+                               // No light
+                       } else if (light_kind == 1) {
+                               // Parallel
+                               v_to_light = normalize(vec4(light_center, 1.0));
+                       } else {
+                               // Point light (and others?)
+                               v_to_light = normalize(vec4(light_center, 1.0) - pos);
+                       }
                }
                """ @ glsl_vertex_shader
 
@@ -321,6 +374,7 @@ class BlinnPhongProgram
                varying vec3 v_normal;
                varying vec4 v_to_light;
                varying vec4 v_to_camera;
+               varying vec4 v_depth_pos;
 
                // Colors
                uniform vec4 ambient_color;
@@ -347,6 +401,60 @@ class BlinnPhongProgram
                uniform bool use_map_normal;
                uniform sampler2D map_normal;
 
+               // Shadow
+               uniform int light_kind;
+               uniform bool use_shadows;
+               uniform sampler2D depth_texture;
+               uniform float depth_texture_size;
+               uniform int depth_texture_taps;
+
+               // Shadow effect on the diffuse colors of the fragment at offset `x, y`
+               float shadow_lookup(vec2 depth_coord, float x, float y) {
+                       float tap_width = 1.0;
+                       float pixel_size = tap_width/depth_texture_size;
+
+                       vec2 offset = vec2(x * pixel_size * v_depth_pos.w,
+                                          y * pixel_size * v_depth_pos.w);
+                       depth_coord += offset;
+
+                       float depth = v_depth_pos.z/v_depth_pos.w;
+                       //vec2 depth_coord = v_depth_pos.xy/v_depth_pos.w;
+                       if (depth_coord.x < 0.0 || depth_coord.x > 1.0 || depth_coord.y < 0.0 || depth_coord.y > 1.0) {
+                               // Out of the shadow map texture
+                               //gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // debug, red out of the light view
+                               return 1.0;
+                       }
+
+                       float shadow_depth = texture2D(depth_texture, depth_coord).r;
+                       float bias = 0.0001;
+                       if (shadow_depth == 1.0) {
+                               // Too far to be in depth texture
+                               return 1.0;
+                       } else if (shadow_depth <= depth - bias) {
+                               // In a shadow
+                               //gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0); // debug, blue shadows
+                               return 0.2; // TODO replace with a configurable ambient light
+                       }
+
+                       //gl_FragColor = vec4(0.0, 1.0-(shadow_depth-depth), 0.0, 1.0); // debug, green lit surfaces
+                       return 1.0;
+               }
+
+               // Shadow effect on the diffuse colors of the fragment
+               float shadow() {
+                       if (!use_shadows) return 1.0;
+
+                       vec2 depth_coord = v_depth_pos.xy/v_depth_pos.w;
+
+                       float taps = float(depth_texture_taps);
+                       float tap_step = 2.00/taps;
+                       float sum = 0.0;
+                       for (float x = -1.0; x <= 0.99; x += tap_step)
+                               for (float y = -1.0; y <= 0.99; y += tap_step)
+                                       sum += shadow_lookup(depth_coord, x, y);
+                       return sum / taps / taps;
+               }
+
                void main()
                {
                        // Normal
@@ -360,28 +468,44 @@ class BlinnPhongProgram
                        vec4 ambient = ambient_color;
                        if (use_map_ambient) ambient *= texture2D(map_ambient, v_tex_coord);
 
-                       // Diffuse Lambert light
-                       vec3 to_light = v_to_light.xyz;
-                       float lambert = clamp(dot(normal, to_light), 0.0, 1.0);
+                       if (light_kind == 0) {
+                               // No light, show diffuse and ambient
 
-                       vec4 diffuse = lambert * diffuse_color;
-                       if (use_map_diffuse) diffuse *= texture2D(map_diffuse, v_tex_coord);
+                               vec4 diffuse = diffuse_color;
+                               if (use_map_diffuse) diffuse *= texture2D(map_diffuse, v_tex_coord);
 
-                       // Specular Phong light
-                       float s = 0.0;
-                       if (lambert > 0.0) {
-                               vec3 l = reflect(-to_light, normal);
-                               s = clamp(dot(l, v_to_camera.xyz), 0.0, 1.0);
-                               s = pow(s, 8.0); // TODO make this `shininess` a material attribute
-                       }
+                               gl_FragColor = ambient + diffuse;
+                       } else {
+                               // Parallel light or point light (1 or 2)
+
+                               // Diffuse Lambert light
+                               vec3 to_light = v_to_light.xyz;
+                               float lambert = clamp(dot(normal, to_light), 0.0, 1.0);
 
-                       vec4 specular = s * specular_color;
-                       if (use_map_specular) specular *= texture2D(map_specular, v_tex_coord).x;
+                               vec4 diffuse = lambert * diffuse_color;
+                               if (use_map_diffuse) diffuse *= texture2D(map_diffuse, v_tex_coord);
+
+                               // Specular Phong light
+                               float s = 0.0;
+                               if (lambert > 0.0) {
+                                       // In light
+                                       vec3 l = reflect(-to_light, normal);
+                                       s = clamp(dot(l, v_to_camera.xyz), 0.0, 1.0);
+                                       s = pow(s, 8.0); // TODO make this `shininess` a material attribute
+
+                                       // Shadows
+                                       diffuse *= shadow();
+                               }
+
+                               vec4 specular = s * specular_color;
+                               if (use_map_specular) specular *= texture2D(map_specular, v_tex_coord).x;
+
+                               gl_FragColor = ambient + diffuse + specular;
+                       }
 
-                       gl_FragColor = ambient + diffuse + specular;
                        if (gl_FragColor.a < 0.01) discard;
 
-                       //gl_FragColor = vec4(normalize(normal).rgb, 1.0); // Debug
+                       //gl_FragColor = vec4(normalize(normal).rgb, 1.0); // Debug normals
                }
                """ @ glsl_fragment_shader
 
@@ -427,9 +551,27 @@ class BlinnPhongProgram
        # Specular color
        var specular_color = uniforms["specular_color"].as(UniformVec4) is lazy
 
-       # Center position of the light
+       # Kind of lights: 0 -> no light, 1 -> parallel, 2 -> point
+       var light_kind = uniforms["light_kind"].as(UniformInt) is lazy
+
+       # Center position of the light *or* vector to parallel light source
        var light_center = uniforms["light_center"].as(UniformVec3) is lazy
 
+       # Light model view projection matrix
+       var light_mvp = uniforms["light_mvp"].as(UniformMat4) is lazy
+
+       # Should shadow be drawn? Would use `depth_texture` and `light_mvp`.
+       var use_shadows = uniforms["use_shadows"].as(UniformBool) is lazy
+
+       # Diffuse texture unit
+       var depth_texture = uniforms["depth_texture"].as(UniformSampler2D) is lazy
+
+       # Size, in pixels, of `depth_texture`
+       var depth_texture_size = uniforms["depth_texture_size"].as(UniformFloat) is lazy
+
+       # Times to tap the `depth_texture`, square root (set to 3 for a total of 9 taps)
+       var depth_texture_taps = uniforms["depth_texture_taps"].as(UniformInt) is lazy
+
        # Camera position
        var camera = uniforms["camera"].as(UniformVec3) is lazy
 
@@ -442,7 +584,7 @@ class BlinnPhongProgram
        # Scaling per vertex
        var scale = uniforms["scale"].as(UniformFloat) is lazy
 
-       # Model view projection matrix
+       # Camera model view projection matrix
        var mvp = uniforms["mvp"].as(UniformMat4) is lazy
 end
 
diff --git a/lib/gamnit/depth/shadow.nit b/lib/gamnit/depth/shadow.nit
new file mode 100644 (file)
index 0000000..213b461
--- /dev/null
@@ -0,0 +1,477 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Shadow mapping using a depth texture
+#
+# The default light does not cast any shadows. It can be changed to a
+# `ParallelLight` in client games to cast sun-like shadows:
+#
+# ~~~
+# import more_lights
+#
+# var sun = new ParallelLight
+# sun.pitch = 0.25*pi
+# sun.yaw = 0.25*pi
+# app.light = sun
+# ~~~
+module shadow
+
+intrude import gamnit::depth_core
+
+redef class App
+
+       # Resolution of the shadow texture, defaults to 4096 pixels
+       #
+       # TODO make configurable / ask the hardware for gl_MAX_TEXTURE_SIZE
+       var shadow_resolution = 4096
+
+       # Are shadows supported by the current hardware configuration?
+       #
+       # The implementation may change in the future, but it currently relies on
+       # the GL extension `GL_EOS_depth_texture`.
+       var supports_shadows: Bool is lazy do
+               return display.as(not null).gl_extensions.has("GL_OES_depth_texture")
+       end
+
+       # Is `shadow_context.depth_texture` ready to be used?
+       fun shadow_depth_texture_available: Bool
+       do return supports_shadows and shadow_context.depth_texture != -1
+
+       private var shadow_depth_program = new ShadowDepthProgram
+
+       private var perf_clock_shadow = new Clock is lazy
+
+       redef fun on_create
+       do
+               super
+
+               var program = shadow_depth_program
+               program.compile_and_link
+               var error = program.error
+               assert error == null else print_error error
+       end
+
+       private var shadow_context: ShadowContext = create_shadow_context is lazy
+
+       private fun create_shadow_context: ShadowContext
+       do
+               var display = display
+               assert display != null
+
+               var context = new ShadowContext
+               context.prepare_once(display, shadow_resolution)
+               return context
+       end
+
+       # Update the depth texture from the light point of view
+       #
+       # This method updates `shadow_context.depth_texture`.
+       protected fun frame_core_shadow_prep(display: GamnitDisplay)
+       do
+               if not supports_shadows then return
+
+               var light = app.light
+               if not light isa LightCastingShadows then return
+
+               perf_clock_shadow.lapse
+
+               # Make sure there's no errors pending
+               assert glGetError == gl_NO_ERROR
+
+               # Bind the framebuffer and make sure it is OK
+               glBindFramebuffer(gl_FRAMEBUFFER, shadow_context.light_view_framebuffer)
+               assert glGetError == gl_NO_ERROR
+               assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
+
+               # Draw to fill the depth texture and only the depth
+               glViewport(0, 0, shadow_resolution, shadow_resolution)
+               glColorMask(false, false, false, false)
+               glClear gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT
+               assert glGetError == gl_NO_ERROR
+
+               # Update light position
+               var camera = light.camera
+               camera.position.x = app.world_camera.position.x
+               camera.position.y = app.world_camera.position.y
+               camera.position.z = app.world_camera.position.z
+
+               # Draw all actors
+               for actor in actors do
+                       for leaf in actor.model.leaves do
+                               leaf.material.draw_depth(actor, leaf, camera)
+                       end
+               end
+
+               # Take down, bring back default values
+               glBindFramebuffer(gl_FRAMEBUFFER, shadow_context.screen_framebuffer)
+               glColorMask(true, true, true, true)
+
+               perfs["gamnit shadows prep"].add perf_clock_shadow.lapse
+       end
+
+       # ---
+       # Debug: show light view in the bottom left of the screen
+
+       # Lazy load the debugging program
+       private var shadow_debug_program: LightPointOfViewProgram is lazy do
+               var program = new LightPointOfViewProgram
+               program.compile_and_link
+               var error = program.error
+               assert error == null else print_error error
+               return program
+       end
+
+       # Draw the light view in the bottom left of the screen, for debugging only
+       #
+       # The shadow depth texture is a square that can be deformed by this projection.
+       protected fun frame_core_shadow_debug(display: GamnitDisplay)
+       do
+               if not supports_shadows then
+                       print_error "Error: Shadows are not supported by the current hardware configuration"
+                       return
+               end
+
+               perf_clock_shadow.lapse
+
+               var program = shadow_debug_program
+
+               glBindBuffer(gl_ARRAY_BUFFER, shadow_context.buffer_array)
+               glViewport(0, 0, display.width/3, display.height/3)
+               glClear gl_DEPTH_BUFFER_BIT
+               program.use
+
+               # Uniforms
+               glActiveTexture gl_TEXTURE0
+               glBindTexture(gl_TEXTURE_2D, shadow_context.depth_texture)
+               program.texture.uniform 0
+
+               # Attributes
+               var sizeof_gl_float = 4
+               var n_floats = 3
+               glEnableVertexAttribArray program.coord.location
+               glVertexAttribPointeri(program.coord.location, n_floats, gl_FLOAT, false, 0, 0)
+               var offset = 4 * n_floats * sizeof_gl_float
+
+               n_floats = 2
+               glEnableVertexAttribArray program.tex_coord.location
+               glVertexAttribPointeri(program.tex_coord.location, n_floats, gl_FLOAT, false, 0, offset)
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Draw
+               glDrawArrays(gl_TRIANGLE_STRIP, 0, 4)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Take down
+               glBindBuffer(gl_ARRAY_BUFFER, 0)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               sys.perfs["gamnit shadow debug"].add app.perf_clock_shadow.lapse
+       end
+end
+
+# Handles to reused GL buffers and texture
+private class ShadowContext
+
+       # Real screen framebuffer
+       var screen_framebuffer: Int = -1
+
+       # Framebuffer for the light point of view
+       var light_view_framebuffer: Int = -1
+
+       # Depth attached to `light_view_framebuffer`
+       var depth_texture: Int = -1
+
+       # Buffer name for vertex data
+       var buffer_array: Int = -1
+
+       # Prepare all attributes once per resolution change
+       fun prepare_once(display: GamnitDisplay, shadow_resolution: Int)
+       do
+               assert display.gl_extensions.has("GL_OES_depth_texture")
+
+               # Set aside the real screen framebuffer name
+               var screen_framebuffer = glGetIntegerv(gl_FRAMEBUFFER_BINDING, 0)
+               self.screen_framebuffer = screen_framebuffer
+
+               # Framebuffer
+               var framebuffer = glGenFramebuffers(1).first
+               glBindFramebuffer(gl_FRAMEBUFFER, framebuffer)
+               assert glIsFramebuffer(framebuffer)
+               self.light_view_framebuffer = framebuffer
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Depth & texture/color
+               var textures = glGenTextures(1)
+               self.depth_texture = textures[0]
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               resize(display, shadow_resolution)
+               assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
+
+               # Array buffer
+               buffer_array = glGenBuffers(1).first
+               glBindBuffer(gl_ARRAY_BUFFER, buffer_array)
+               assert glIsBuffer(buffer_array)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               ## coord
+               var data = new Array[Float]
+               data.add_all([-1.0, -1.0, 0.0,
+                          1.0, -1.0, 0.0,
+                         -1.0,  1.0, 0.0,
+                          1.0,  1.0, 0.0])
+               ## tex_coord
+               data.add_all([0.0, 0.0,
+                             1.0, 0.0,
+                             0.0, 1.0,
+                             1.0, 1.0])
+               var c_data = new GLfloatArray.from(data)
+               glBufferData(gl_ARRAY_BUFFER, data.length*4, c_data.native_array, gl_STATIC_DRAW)
+
+               glBindBuffer(gl_ARRAY_BUFFER, 0)
+
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+       end
+
+       # Init size or resize `depth_texture`
+       fun resize(display: GamnitDisplay, shadow_resolution: Int)
+       do
+               glBindFramebuffer(gl_FRAMEBUFFER, light_view_framebuffer)
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Depth texture
+               var depth_texture = self.depth_texture
+               glActiveTexture gl_TEXTURE0
+               glBindTexture(gl_TEXTURE_2D, depth_texture)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_NEAREST)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE)
+               glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # TODO support hardware shadows with GL ES 3.0 or GL_EXT_shadow_samplers
+               #glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_COMPARE_MODE, ...)
+
+               glTexImage2D(gl_TEXTURE_2D, 0, gl_DEPTH_COMPONENT,
+                            shadow_resolution, shadow_resolution,
+                            0, gl_DEPTH_COMPONENT, gl_UNSIGNED_SHORT, new Pointer.nul)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               glFramebufferTexture2D(gl_FRAMEBUFFER, gl_DEPTH_ATTACHMENT, gl_TEXTURE_2D, depth_texture, 0)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               # Check if the framebuffer is complete and valid
+               assert glCheckFramebufferStatus(gl_FRAMEBUFFER) == gl_FRAMEBUFFER_COMPLETE
+
+               # Take down
+               glBindTexture(gl_TEXTURE_2D, 0)
+               glBindFramebuffer(gl_FRAMEBUFFER, 0)
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+       end
+
+       var destroyed = false
+
+       fun destroy
+       do
+               if destroyed then return
+               destroyed = true
+
+               # Free the buffer
+               glDeleteBuffers([buffer_array])
+               var gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+               buffer_array = -1
+
+               # Free the array and framebuffer plus its attachments
+               glDeleteBuffers([buffer_array])
+               glDeleteFramebuffers([light_view_framebuffer])
+               glDeleteTextures([depth_texture])
+       end
+end
+
+redef class Material
+       # Optimized draw of `model`, a part of `actor`, from the view of `camera`
+       #
+       # This drawing should only produce usable depth data. The default behavior,
+       # uses `shadow_depth_program`.
+       protected fun draw_depth(actor: Actor, model: LeafModel, camera: Camera)
+       do
+               var program = app.shadow_depth_program
+               program.use
+               program.mvp.uniform camera.mvp_matrix
+
+               var mesh = model.mesh
+
+               program.translation.uniform(actor.center.x, actor.center.y, actor.center.z, 0.0)
+               program.scale.uniform actor.scale
+               program.use_map_diffuse.uniform false
+
+               program.tex_coord.array_enabled = true
+               program.tex_coord.array(mesh.texture_coords, 2)
+
+               program.coord.array_enabled = true
+               program.coord.array(mesh.vertices, 3)
+
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
+
+               if mesh.indices.is_empty then
+                       glDrawArrays(mesh.draw_mode, 0, mesh.vertices.length/3)
+               else
+                       glDrawElements(mesh.draw_mode, mesh.indices.length, gl_UNSIGNED_SHORT, mesh.indices_c.native_array)
+               end
+       end
+
+end
+
+# Efficiently draw actors from the light view
+class ShadowDepthProgram
+       super GamnitProgramFromSource
+
+       redef var vertex_shader_source = """
+               // Vertex coordinates
+               attribute vec4 coord;
+
+               // Vertex translation
+               uniform vec4 translation;
+
+               // Vertex scaling
+               uniform float scale;
+
+               // Vertex coordinates on textures
+               attribute vec2 tex_coord;
+
+               // Vertex normal
+               attribute vec3 normal;
+
+               // Model view projection matrix
+               uniform mat4 mvp;
+
+               // Rotation matrix
+               uniform mat4 rotation;
+
+               // Output for the fragment shader
+               varying vec2 v_tex_coord;
+
+               void main()
+               {
+                       vec4 pos = (vec4(coord.xyz * scale, 1.0) * rotation + translation);
+                       gl_Position = pos * mvp;
+
+                       // Pass varyings to the fragment shader
+                       v_tex_coord = vec2(tex_coord.x, 1.0 - tex_coord.y);
+               }
+               """ @ glsl_vertex_shader
+
+       redef var fragment_shader_source = """
+               precision mediump float;
+
+               // Diffuse map
+               uniform bool use_map_diffuse;
+               uniform sampler2D map_diffuse;
+
+               varying vec2 v_tex_coord;
+
+               void main()
+               {
+                       if (use_map_diffuse && texture2D(map_diffuse, v_tex_coord).a <= 0.01) {
+                               discard;
+                       }
+               }
+               """ @ glsl_fragment_shader
+
+       # Vertices coordinates
+       var coord = attributes["coord"].as(AttributeVec4) is lazy
+
+       # Should this program use the texture `map_diffuse`?
+       var use_map_diffuse = uniforms["use_map_diffuse"].as(UniformBool) is lazy
+
+       # Diffuse texture unit
+       var map_diffuse = uniforms["map_diffuse"].as(UniformSampler2D) is lazy
+
+       # Coordinates on the textures, per vertex
+       var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy
+
+       # Diffuse color
+       var diffuse_color = uniforms["diffuse_color"].as(UniformVec4) is lazy
+
+       # Translation applied to each vertex
+       var translation = uniforms["translation"].as(UniformVec4) is lazy
+
+       # Rotation matrix
+       var rotation = uniforms["rotation"].as(UniformMat4) is lazy
+
+       # Scaling per vertex
+       var scale = uniforms["scale"].as(UniformFloat) is lazy
+
+       # Model view projection matrix
+       var mvp = uniforms["mvp"].as(UniformMat4) is lazy
+end
+
+# Draw the camera point of view on screen
+private class LightPointOfViewProgram
+       super GamnitProgramFromSource
+
+       redef var vertex_shader_source = """
+               // Vertex coordinates
+               attribute vec3 coord;
+
+               // Vertex coordinates on textures
+               attribute vec2 tex_coord;
+
+               // Output to the fragment shader
+               varying vec2 v_coord;
+
+               void main()
+               {
+                       gl_Position = vec4(coord, 1.0);
+                       v_coord = tex_coord;
+               }
+               """ @ glsl_vertex_shader
+
+       redef var fragment_shader_source = """
+               precision mediump float;
+
+               // Virtual screen texture / color attachment
+               uniform sampler2D texture0;
+
+               // Input from the vertex shader
+               varying vec2 v_coord;
+
+               void main()
+               {
+                       gl_FragColor = texture2D(texture0, v_coord);
+               }
+               """ @ glsl_fragment_shader
+
+       # Vertices coordinates
+       var coord = attributes["coord"].as(AttributeVec3) is lazy
+
+       # Coordinates on the textures, per vertex
+       var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy
+
+       # Visible texture
+       var texture = uniforms["texture0"].as(UniformSampler2D) is lazy
+end