gamnit: add animations to Sprites
[nit.git] / lib / gamnit / flat.nit
index d5cb28e..3c1a42a 100644 (file)
 #   devices. It is used to position the sprites in `App::ui_sprites`.
 #
 # See the sample game at `contrib/asteronits/` and the basic project template
-# at `lib/gamnit/examples/template_flat/`.
+# at `lib/gamnit/examples/template/`.
 module flat
 
 import glesv2
 intrude import geometry::points_and_lines # For _x, _y and _z
+intrude import matrix
 import matrix::projection
 import more_collections
 import performance_analysis
 
 import gamnit
-import gamnit::cameras
+intrude import gamnit::cameras
+intrude import gamnit::cameras_cache
+import gamnit::dynamic_resolution
 import gamnit::limit_fps
+import gamnit::camera_control
 
-import android_two_fingers_motion is conditional(android)
-
-# Draw a `texture` at `center`
+# Visible 2D entity in the game world or UI
+#
+# Similar to `gamnit::Actor` which is in 3D.
+#
+# Each sprite associates a `texture` to the position `center`.
+# The appearance is modified by `rotation`, `invert_x`,
+# `scale`, `red`, `green`, `blue` and `alpha`.
+# These values can be changed at any time and will trigger an update
+# of the data on the GPU side, having a small performance cost.
 #
-# An instance of `Sprite` can only belong to a single `SpriteSet` at
-# a time. The on screen position depends on the camera associated
+# For a sprite to be visible, it must be added to either the world `sprites`
+# or the `ui_sprites`.
+# However, an instance of `Sprite` can only belong to a single `SpriteSet`
+# at a time. The final on-screen position depends on the camera associated
 # to the `SpriteSet`.
+#
+# ~~~
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, new Point3d[Float](0.0, 0.0, 0.0))
+#
+# # Add sprite to the visible game world
+# app.sprites.add sprite
+#
+# # Extra configuration of the sprite
+# sprite.rotation = pi/2.0
+# sprite.scale = 2.0
+#
+# # Show only the blue colors
+# sprite.red = 0.0
+# sprite.green = 0.0
+# ~~~
+#
+# To add a sprite to the UI it can be anchored to screen borders
+# with `ui_camera.top_left` and the likes.
+#
+# ~~~nitish
+# # Place it a bit off the top left of the screen
+# var pos = app.ui_camera.top_left.offset(128.0, -128.0, 0)
+#
+# # Load texture and create sprite
+# var texture = new Texture("path/in/assets.png")
+# var sprite = new Sprite(texture, pos)
+#
+# # Add it to the UI (above world sprites)
+# app.ui_sprites.add sprite
+# ~~~
 class Sprite
 
        # Texture drawn to screen
@@ -79,10 +123,59 @@ class Sprite
                center_direct = value
        end
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Last animation set with `animate`
+       var animation: nullable Animation = null
+
+       # Animation on the shader, if this changes it `needs_remap`
+       private var shader_animation: nullable Animation = null
+
+       # Animation start time, relative to `sprite_set.time`
+       #
+       # At -1.0 if animation started before being assigned a `sprite_set`.
+       private var animation_start = 0.0
+
+       # Number of loops to show `animation`
+       private var animation_loops = 0.0
+
+       # Start the `animation` for `n_loops`, replacing the static `texture`
+       #
+       # By default, if `n_loops` is not set, the animation plays once.
+       # If `n_loops == -1.0` then the animation loops infinitely.
+       # Otherwise, the animation repeats, e.g. it repeats twice and a half
+       # if `n_loops == 2.5`.
+       #
+       # The animation can be stopped using `animate_stop`.
+       fun animate(animation: Animation, n_loops: nullable Float)
+       do
+               if not animation.valid then print_error "{class_name}::animate: invalid animation {animation}"
+
+               var shader_animation = shader_animation
+               if shader_animation == null or animation.frames.first.root != shader_animation.frames.first.root then
+                       # Resort with the new animation texture
+                       needs_remap
+               else
+                       needs_update
+               end
+
+               var sprite_set = sprite_set
+               animation_start = if sprite_set != null then sprite_set.time else -1.0
+               animation_loops = n_loops or else 1.0
+               self.shader_animation = animation
+               self.animation = animation
+       end
+
+       # Stop any active `animation` to display the static `texture`
+       fun animate_stop
+       do
+               if animation == null then return
+               needs_update
+               animation = null
+       end
+
+       # Rotation on the Z axis, positive values turn counterclockwise
        var rotation = 0.0 is writable(rotation_direct=)
 
-       # Rotation on the Z axis, positive values go counterclockwise
+       # Rotation on the Z axis, positive values turn counterclockwise
        fun rotation=(value: Float)
        do
                if isset _rotation and value != rotation then needs_update
@@ -101,10 +194,12 @@ class Sprite
 
        # Scale applied to this sprite
        #
-       # The default size of `self` depends on the size in pixels of `texture`.
+       # The basic size of `self` depends on the size in pixels of `texture`.
        var scale = 1.0 is writable(scale_direct=)
 
-       # Scale applied to this sprite, see `scale`
+       # Scale applied to this sprite
+       #
+       # The basic size of `self` depends on the size in pixels of `texture`.
        fun scale=(value: Float)
        do
                if isset _scale and value != scale then needs_update
@@ -211,31 +306,69 @@ class Sprite
        private var sprite_set: nullable SpriteSet = null
 end
 
+# Animation for sprites, set with `Sprite.animate`
+#
+# Two main services create animations:
+# * The constructors accepts an array of textures and the number of frames per
+#   seconds: `new Animation(array_of_subtextures, 10.0)`
+# * The method `Texture::to_animation` uses the whole texture
+#   dividing it in frames either on X or Y:
+#   `new Texture("path/in/assets.png").to_animation(30.0, 0, 12)`
+class Animation
+
+       # Frames composing this animation
+       #
+       # All frames must share the same `Texture::root`, be on a vertical or
+       # horizontal line, be spaced equally and share the same dimensions.
+       var frames: SequenceRead[Texture]
+
+       # Frames per seconds, a higher value makes this animation faster
+       #
+       # The animation speed is also affected by `SpriteSet::time_mod`.
+       var fps: Float
+
+       # Are the `frames` valid for an animation? (see the requirements in `frames`)
+       var valid: Bool is lazy do
+               var r: nullable RootTexture = null
+               for f in frames do
+                       if r == null then
+                               r = f.root
+                       else
+                               if r != f.root then return false
+                       end
+               end
+
+               # TODO check for line, constant distance, and same aspect ratio.
+
+               return true
+       end
+end
+
 redef class App
        # Default graphic program to draw `sprites`
        private var simple_2d_program = new Simple2dProgram is lazy
 
        # Camera for world `sprites` and `depth::actors` with perspective
        #
-       # By default, the camera is configured to respect the resolution
-       # of the screen in world coordinates at `z == 0.0`.
+       # By default, the camera is configured to a height of 1080 units
+       # of world coordinates at `z == 0.0`.
        var world_camera: EulerCamera is lazy do
                var camera = new EulerCamera(app.display.as(not null))
 
-               # Aim for pixel resolution at level 0
-               camera.reset_height
-               camera.near = 100.0
+               # Aim for full HD pixel resolution at level 0
+               camera.reset_height 1080.0
+               camera.near = 10.0
 
                return camera
        end
 
        # Camera for `ui_sprites` using an orthogonal view
-       var ui_camera: UICamera = new UICamera(app.display.as(not null)) is lazy
+       var ui_camera = new UICamera(app.display.as(not null)) is lazy
 
-       # World sprites to draw as seen by `world_camera`
+       # World sprites drawn as seen by `world_camera`
        var sprites: Set[Sprite] = new SpriteSet
 
-       # UI sprites to draw as seen by `ui_camera`, drawn over world `sprites`
+       # UI sprites drawn as seen by `ui_camera`, over world `sprites`
        var ui_sprites: Set[Sprite] = new SpriteSet
 
        # Main method to refine in clients to update game logic and `sprites`
@@ -248,9 +381,7 @@ redef class App
        do
                texture.load
 
-               ui_camera.reset_height 1080.0
-
-               var splash = new Sprite(texture, ui_camera.center)
+               var splash = new Sprite(texture, ui_camera.center.offset(0.0, 0.0, 0.0))
                ui_sprites.add splash
 
                var display = display
@@ -328,11 +459,19 @@ redef class App
                if display != null then display.close
        end
 
-       redef fun frame_core(display)
+       redef fun on_resize(display)
        do
-               # Prepare to draw, clear buffers
-               glClear(gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT)
+               super
+
+               world_camera.mvp_matrix_cache = null
+               ui_camera.mvp_matrix_cache = null
 
+               # Update all sprites in the UI
+               for sprite in ui_sprites do sprite.needs_update
+       end
+
+       redef fun frame_core(display)
+       do
                # Check errors
                var gl_error = glGetError
                assert gl_error == gl_NO_ERROR else print_error gl_error
@@ -341,6 +480,7 @@ redef class App
                perf_clock_main.lapse
                var dt = clock.lapse.to_f
                update dt
+               frame_dt = dt
                sys.perfs["gamnit flat update client"].add perf_clock_main.lapse
 
                # Draw and flip screen
@@ -352,16 +492,21 @@ redef class App
                assert gl_error == gl_NO_ERROR else print_error gl_error
        end
 
+       private var frame_dt = 0.0
+
        # Draw the whole screen, all `glDraw...` calls should be executed here
        protected fun frame_core_draw(display: GamnitDisplay)
        do
-               perf_clock_main.lapse
+               frame_core_dynamic_resolution_before display
 
+               perf_clock_main.lapse
                frame_core_world_sprites display
                perfs["gamnit flat world_sprites"].add perf_clock_main.lapse
 
                frame_core_ui_sprites display
                perfs["gamnit flat ui_sprites"].add perf_clock_main.lapse
+
+               frame_core_dynamic_resolution_after display
        end
 
        private fun frame_core_sprites(display: GamnitDisplay, sprite_set: SpriteSet, camera: Camera)
@@ -370,6 +515,9 @@ redef class App
                simple_2d_program.use
                simple_2d_program.mvp.uniform camera.mvp_matrix
 
+               sprite_set.time += frame_dt*sprite_set.time_mod
+               simple_2d_program.time.uniform sprite_set.time
+
                # draw
                sprite_set.draw
        end
@@ -427,6 +575,34 @@ redef class Texture
                        r, b,
                        l, b]
        end
+
+       # Convert to a sprite animation at `fps` speed with `x` or `y` frames
+       #
+       # The arguments `x` and `y` set the number of frames in the texture.
+       # Use `x` for an horizontal arrangement or `y` for vertical.
+       # One and only one of the arguments must be different than 0,
+       # as an animation can only be on a line and cannot wrap.
+       fun to_animation(fps: Float, x, y: Int): Animation
+       do
+               assert (x == 0) != (y == 0)
+
+               var n_frames = x.max(y)
+               var frames = new Array[Texture]
+
+               var dx = (x/n_frames).to_f/n_frames.to_f
+               var dy = (y/n_frames).to_f/n_frames.to_f
+               var w = if x == 0 then 1.0 else dx
+               var h = if y == 0 then 1.0 else dy
+               var left = 0.0
+               var top = 0.0
+               for i in n_frames.times do
+                       frames.add new RelativeSubtexture(root, left, top, left+w, top+h)
+                       left += dx
+                       top += dy
+               end
+
+               return new Animation(frames, fps)
+       end
 end
 
 # Graphic program to display simple models with a texture, translation, rotation and scale
@@ -452,12 +628,36 @@ private class Simple2dProgram
                // Model view projection matrix
                uniform mat4 mvp;
 
+               // Current world time, in seconds
+               uniform float time;
+
                // Rotation matrix
                attribute vec4 rotation_row0;
                attribute vec4 rotation_row1;
                attribute vec4 rotation_row2;
                attribute vec4 rotation_row3;
 
+               // Animation speed, frames per seconds
+               attribute float a_fps;
+
+               // Number of frames in the animation
+               attribute float a_n_frames;
+
+               // World coordinate of the animation (for aspect ratio)
+               attribute vec2 a_coord;
+
+               // Animation texture coordinates of the first frame
+               attribute vec2 a_tex_coord;
+
+               // Animation texture coordinates difference between frames
+               attribute vec2 a_tex_diff;
+
+               // Animation start time, in reference to `time`
+               attribute float a_start;
+
+               // Number of loops to play of the animation
+               attribute float a_loops;
+
                mat4 rotation()
                {
                        return mat4(rotation_row0, rotation_row1, rotation_row2, rotation_row3);
@@ -467,11 +667,29 @@ private class Simple2dProgram
                varying vec4 v_color;
                varying vec2 v_coord;
 
+               // Is there an active animation?
+               varying float v_animated;
+
                void main()
                {
-                       gl_Position = (vec4(coord.xyz * scale, 1.0) * rotation() + translation)* mvp;
+                       vec3 c; // coords
+
+                       float end = a_start + a_loops/a_fps*a_n_frames;
+                       if (a_loops == -1.0 || time < end) {
+                               // in animation
+                               float frame = mod(floor((time-a_start)*a_fps), a_n_frames);
+                               v_coord = a_tex_coord + a_tex_diff*frame;
+                               c = vec3(a_coord, coord.z);
+                               v_animated = 1.0;
+                       } else {
+                               // static
+                               v_coord = tex_coord;
+                               c = coord.xyz;
+                               v_animated = 0.0;
+                       }
+
+                       gl_Position = (vec4(c * scale, 1.0) * rotation() + translation)* mvp;
                        v_color = color;
-                       v_coord = tex_coord;
                }
                """ @ glsl_vertex_shader
 
@@ -484,15 +702,22 @@ private class Simple2dProgram
                // Texture to apply on this object
                uniform sampler2D texture0;
 
+               // Texture to apply on this object
+               uniform sampler2D animation;
+
                // Input from the vertex shader
                varying vec4 v_color;
                varying vec2 v_coord;
+               varying float v_animated;
 
                void main()
                {
-                       if(use_texture) {
+                       if (v_animated > 0.5) {
+                               gl_FragColor = v_color * texture2D(animation, v_coord);
+                               if (gl_FragColor.a <= 0.01) discard;
+                       } else if (use_texture) {
                                gl_FragColor = v_color * texture2D(texture0, v_coord);
-                               if (gl_FragColor.a <= 0.1) discard;
+                               if (gl_FragColor.a <= 0.01) discard;
                        } else {
                                gl_FragColor = v_color;
                        }
@@ -534,15 +759,39 @@ private class Simple2dProgram
 
        # Model view projection matrix
        var mvp = uniforms["mvp"].as(UniformMat4) is lazy
+
+       # World time, in seconds
+       var time = uniforms["time"].as(UniformFloat) is lazy
+
+       # ---
+       # Animations
+
+       # Texture of all the frames of the animation
+       var animation_texture = uniforms["animation"].as(UniformSampler2D) is lazy
+
+       # Frame per second of the animation
+       var animation_fps = attributes["a_fps"].as(AttributeFloat) is lazy
+
+       # Number of frames in the animation
+       var animation_n_frames = attributes["a_n_frames"].as(AttributeFloat) is lazy
+
+       # Coordinates of each frame (mush be shared by all frames)
+       var animation_coord = attributes["a_coord"].as(AttributeVec2) is lazy
+
+       # Texture coordinates of the first frame
+       var animation_tex_coord = attributes["a_tex_coord"].as(AttributeVec2) is lazy
+
+       # Coordinate difference between each frame
+       var animation_tex_diff = attributes["a_tex_diff"].as(AttributeVec2) is lazy
+
+       # Animation start time, in seconds and in reference to `dt`
+       var animation_start = attributes["a_start"].as(AttributeFloat) is lazy
+
+       # Number of loops of the animation, -1 for infinite
+       var animation_loops = attributes["a_loops"].as(AttributeFloat) is lazy
 end
 
 redef class Point3d[N]
-       # Get a new `Point3d[Float]` with an offset of each axis of `x, y, z`
-       fun offset(x, y, z: Numeric): Point3d[Float]
-       do
-               return new Point3d[Float](self.x.to_f+x.to_f, self.y.to_f+y.to_f, self.z.to_f+z.to_f)
-       end
-
        # ---
        # Associate each point to its sprites
 
@@ -593,12 +842,32 @@ redef class Point3d[N]
        end
 end
 
+redef class OffsetPoint3d
+       redef fun x=(v)
+       do
+               if isset _x and v != x then needs_update
+               super
+       end
+
+       redef fun y=(v)
+       do
+               if isset _y and v != y then needs_update
+               super
+       end
+
+       redef fun z=(v)
+       do
+               if isset _z and v != z then needs_update
+               super
+       end
+end
+
 # Set of sprites sorting them into different `SpriteContext`
 private class SpriteSet
        super HashSet[Sprite]
 
        # Map texture then static vs dynamic to a `SpriteContext`
-       var contexts_map = new HashMap2[GamnitRootTexture, Bool, SpriteContext]
+       var contexts_map = new HashMap3[RootTexture, nullable RootTexture, Bool, SpriteContext]
 
        # Contexts in `contexts_map`
        var contexts_items = new Array[SpriteContext]
@@ -606,19 +875,29 @@ private class SpriteSet
        # Sprites needing resorting in `contexts_map`
        var sprites_to_remap = new Array[Sprite]
 
+       # Animation speed multiplier (0.0 to pause, 1.0 for normal speed, etc.)
+       var time_mod = 1.0 is writable
+
+       # Seconds elapsed since the launch of the program, in world time responding to `time_mod`
+       var time = 0.0
+
        # Add a sprite to the appropriate context
        fun map_sprite(sprite: Sprite)
        do
                assert sprite.context == null else print_error "Sprite {sprite} belongs to another SpriteSet"
 
+               # Sort by texture and animation texture
                var texture = sprite.texture.root
-               var context = contexts_map[texture, sprite.static]
+               var animation = sprite.animation
+               var animation_texture = if animation != null then
+                       animation.frames.first.root else null
+               var context = contexts_map[texture, animation_texture, sprite.static]
 
                if context == null then
                        var usage = if sprite.static then gl_STATIC_DRAW else gl_DYNAMIC_DRAW
-                       context = new SpriteContext(texture, usage)
+                       context = new SpriteContext(texture, animation_texture, usage)
 
-                       contexts_map[texture, sprite.static] = context
+                       contexts_map[texture, animation_texture, sprite.static] = context
                        contexts_items.add context
                end
 
@@ -627,6 +906,11 @@ private class SpriteSet
 
                sprite.context = context
                sprite.sprite_set = self
+
+               if animation != null and sprite.animation_start == -1.0 then
+                       # Start animation
+                       sprite.animation_start = time
+               end
        end
 
        # Remove a sprite from its context
@@ -673,6 +957,10 @@ private class SpriteSet
 
        redef fun clear
        do
+               for sprite in self do
+                       sprite.context = null
+                       sprite.sprite_set = null
+               end
                super
                for c in contexts_items do c.destroy
                contexts_map.clear
@@ -689,7 +977,10 @@ private class SpriteContext
        # Context config and state
 
        # Only root texture drawn by this context
-       var texture: nullable GamnitRootTexture
+       var texture: nullable RootTexture
+
+       # Only animation texture drawn by this context
+       var animation_texture: nullable RootTexture
 
        # OpenGL ES usage of `buffer_array` and `buffer_element`
        var usage: GLBufferUsage
@@ -729,10 +1020,11 @@ private class SpriteContext
 
        # Number of GL_FLOAT per vertex of `Simple2dProgram`
        var float_per_vertex: Int is lazy do
-               # vec4 translation, vec4 color, vec4 coord,
-               # float scale, vec2 tex_coord, vec4 rotation_row*
-               return 4 + 4 + 4 +
-                      1 + 2 + 4*4
+               return 4 + 4 + 4 +   # vec4 translation, vec4 color, vec4 coord,
+                      1 + 2 + 4*4 + # float scale, vec2 tex_coord, vec4 rotation_row*,
+                      1 + 1 +       # float a_fps, float a_n_frames,
+                      2 + 2 + 2 +   # vec2 a_coord, vec2 a_tex_coord, vec2 a_tex_diff,
+                      1 + 1         # float a_start, float a_loops
        end
 
        # Number of bytes per vertex of `Simple2dProgram`
@@ -852,7 +1144,41 @@ private class SpriteContext
                        else
                                rot = new Matrix.rotation(sprite.rotation, 0.0, 0.0, 1.0)
                        end
-                       data.fill_from(rot.items, o+15)
+                       data.fill_from_matrix(rot, o+15)
+
+                       var animation = sprite.animation
+                       if animation == null then
+                               for i in [31..40] do data[o+i] = 0.0
+                       else
+                               # a_fps
+                               data[o+31] = animation.fps
+
+                               # a_n_frames
+                               data[o+32] = animation.frames.length.to_f
+
+                               # a_coord
+                               data[o+33] = animation.frames.first.vertices[v*3+0]
+                               data[o+34] = animation.frames.first.vertices[v*3+1]
+
+                               # a_tex_coord
+                               var tc = if sprite.invert_x then
+                                               animation.frames.first.texture_coords_invert_x
+                                       else animation.frames.first.texture_coords
+                               data[o+35] = tc[v*2]
+                               data[o+36] = tc[v*2+1]
+
+                               # a_tex_diff
+                               var dx = animation.frames[1].texture_coords[0] - animation.frames[0].texture_coords[0]
+                               var dy = animation.frames[1].texture_coords[1] - animation.frames[0].texture_coords[1]
+                               data[o+37] = dx
+                               data[o+38] = dy
+
+                               # a_start
+                               data[o+39] = sprite.animation_start
+
+                               # a_loops
+                               data[o+40] = sprite.animation_loops
+                       end
 
                        o += float_per_vertex
                end
@@ -946,8 +1272,19 @@ private class SpriteContext
                var gl_error = glGetError
                assert gl_error == gl_NO_ERROR else print_error gl_error
 
+               var animation = animation_texture
+               if animation != null then
+                       glActiveTexture gl_TEXTURE1
+                       glBindTexture(gl_TEXTURE_2D, animation.gl_texture)
+                       app.simple_2d_program.animation_texture.uniform 1
+               end
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
                # Configure attributes, in order:
-               # vec4 translation, vec4 color, float scale, vec4 coord, vec2 tex_coord, vec4 rotation_row*
+               # vec4 translation, vec4 color, float scale, vec4 coord, vec2 tex_coord, vec4 rotation_row*,
+               # a_fps, a_n_frames, a_coord, a_tex_coord, a_tex_diff, a_start, a_loops
+
                var offset = 0
                var p = app.simple_2d_program
                var sizeof_gl_float = 4 # sizeof(GL_FLOAT)
@@ -998,6 +1335,55 @@ private class SpriteContext
                        assert gl_error == gl_NO_ERROR else print_error gl_error
                end
 
+               size = 1
+               glEnableVertexAttribArray p.animation_fps.location
+               glVertexAttribPointeri(p.animation_fps.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 1
+               glEnableVertexAttribArray p.animation_n_frames.location
+               glVertexAttribPointeri(p.animation_n_frames.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 2
+               glEnableVertexAttribArray p.animation_coord.location
+               glVertexAttribPointeri(p.animation_coord.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 2
+               glEnableVertexAttribArray p.animation_tex_coord.location
+               glVertexAttribPointeri(p.animation_tex_coord.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 2
+               glEnableVertexAttribArray p.animation_tex_diff.location
+               glVertexAttribPointeri(p.animation_tex_diff.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 1
+               glEnableVertexAttribArray p.animation_start.location
+               glVertexAttribPointeri(p.animation_start.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
+               size = 1
+               glEnableVertexAttribArray p.animation_loops.location
+               glVertexAttribPointeri(p.animation_loops.location, size, gl_FLOAT, false, bytes_per_vertex, offset)
+               offset += size * sizeof_gl_float
+               gl_error = glGetError
+               assert gl_error == gl_NO_ERROR else print_error gl_error
+
                # Actual draw
                for s in sprites.starts, e in sprites.ends do
                        var l = e-s
@@ -1032,6 +1418,8 @@ end
 # The data can be compressed by a call to `defragment`.
 #
 # ~~~
+# intrude import gamnit::flat
+#
 # var array = new GroupedArray[String]
 # assert array.to_s == ""
 #
@@ -1070,7 +1458,7 @@ end
 #
 # array.add "E"
 # assert array.to_s == "[A,B,c,D,E]"
-# assert array.capacity == 5
+# assert array.capacity == 6
 # assert array.length == 5
 #
 # array.remove "A"
@@ -1191,6 +1579,8 @@ private class GroupedArray[E]
        # Returns the elements that moved as a list.
        #
        # ~~~
+       # intrude import gamnit::flat
+       #
        # var array = new GroupedArray[String]
        # array.add "a"
        # array.add "b"
@@ -1250,3 +1640,21 @@ private class GroupedArray[E]
                return ss.join
        end
 end
+
+redef class GLfloatArray
+       private fun fill_from_matrix(matrix: Matrix, dst_offset: nullable Int)
+       do
+               dst_offset = dst_offset or else 0
+               var mat_len = matrix.width*matrix.height
+               assert length >= mat_len + dst_offset
+               native_array.fill_from_matrix_native(matrix.items, dst_offset, mat_len)
+       end
+end
+
+redef class NativeGLfloatArray
+       private fun fill_from_matrix_native(matrix: matrix::NativeDoubleArray, dst_offset, len: Int) `{
+               int i;
+               for (i = 0; i < len; i ++)
+                       self[i+dst_offset] = (GLfloat)matrix[i];
+       `}
+end