Merge: gamnit: Blinn-Phong specular lighting, 3 axes of rotation and more
authorJean Privat <jean@pryen.org>
Wed, 7 Jun 2017 20:11:08 +0000 (16:11 -0400)
committerJean Privat <jean@pryen.org>
Wed, 7 Jun 2017 20:11:08 +0000 (16:11 -0400)
Implement a Blinn-Phong specular lighting shader to replace the previous Lamber diffuse lighting. It creates a reflection effect either from `specular_color` (as on the shields) or `specular_texture` (as on the gold tower).

![screenshot from 2017-05-31 10 16 13](https://cloud.githubusercontent.com/assets/208057/26636318/5a017ffc-45ea-11e7-8571-0918f13f6d9c.png)
![screenshot from 2017-05-30 23 13 32](https://cloud.githubusercontent.com/assets/208057/26636316/59c940c4-45ea-11e7-9106-99f9344cecbf.png)

The required material attributes for the specular effect were already present but not implemented. This PR adds similar attributes for normal maps, but leave them as a TODO. The new shader is fine but not optimal, I'll probably need to rewrite it to gain some performance, support normal maps and improve normals per vertex.

Add two angles of rotation to actors for full 3D rotation with Euler angles. All rotation angles follow the right-hand rule and are consistent between the camera and actors.

Other changes:
* Intro `Cuboid` that can be created by `Boxed3d::to_mesh`.
* Better error management when loading models.
* Fix missing `lock_cursor` implementation on desktop. It allows creating FPS like cameras.
* Fix and update `angle_lerp` to follow the style of `lerp` (where `self` is the weight).
* Improve the template project so 3D objects don't blend in the background and explain the printed performance statistics.

In a future PR, I'll standardize the assets classes and move some service up to `Asset`. This would allow for easier loading of all assets at once, no matter their type, and offer a standard behavior regarding errors and lazy loading.

Pull-Request: #2471
Reviewed-by: Jean-Christophe Beaupré <jcbrinfo.public@gmail.com>

15 files changed:
contrib/action_nitro/src/action_nitro.nit
contrib/model_viewer/src/globe.nit
contrib/model_viewer/src/model_viewer.nit
contrib/tinks/assets/models/debris1.mtl
contrib/tinks/src/client/client3d.nit
lib/gamnit/depth/depth_core.nit
lib/gamnit/depth/more_materials.nit
lib/gamnit/depth/more_meshes.nit
lib/gamnit/depth/more_models.nit
lib/gamnit/depth/selection.nit
lib/gamnit/display_linux.nit
lib/gamnit/examples/template/src/template.nit
lib/gamnit/model_parsers/obj.nit
lib/geometry/angles.nit
lib/matrix/projection.nit

index e5a68f9..b8dd4f4 100644 (file)
@@ -140,6 +140,7 @@ redef class App
 
                # Load 3d models
                iss_model.load
+               if iss_model.errors.not_empty then print_error iss_model.errors.join("\n")
 
                # Setup cameras
                world_camera.reset_height 60.0
@@ -507,7 +508,7 @@ end
 redef class Boss
        redef var actor is lazy do
                var actor = new Actor(app.iss_model, center)
-               actor.rotation = pi/2.0
+               actor.yaw = pi/2.0
                return actor
        end
 
@@ -556,7 +557,7 @@ redef class Player
                        var splatter = new Actor(app.splatter_model,
                                new Point3d[Float](center.x, 0.05 & 0.04, center.y))
                        splatter.scale = 32.0
-                       splatter.rotation = 2.0 * pi.rand
+                       splatter.yaw = 2.0*pi.rand
                        app.actors.add splatter
                end
 
index 8fe1158..a5a9a62 100644 (file)
@@ -148,7 +148,7 @@ class GlobeMaterial
 
                # Set uniforms
                program.scale.uniform 1.0
-               program.rotation.uniform new Matrix.rotation(actor.rotation, 0.0, 1.0, 0.0)
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
                program.translation.uniform(actor.center.x, -actor.center.y, actor.center.z, 0.0)
                program.color.uniform(color[0], color[1], color[2], color[3])
                program.is_surface.uniform is_surface
index 9353ef5..b93bea4 100644 (file)
@@ -67,8 +67,10 @@ redef class App
                world_camera.near = 0.1
                world_camera.far = 100.0
 
-               for model in models do model.load
-               for texture in asset_textures_by_name.values do texture.load
+               for model in models do
+                       model.load
+                       if model.errors.not_empty then print_error model.errors.join("\n")
+               end
 
                # Display the first model
                model = models[model_index]
@@ -146,7 +148,7 @@ redef class App
                var t = clock.total.to_f
 
                # Rotate the model
-               actors.first.rotation = t
+               actors.first.yaw = t
 
                # Move the light source
                var dist_to_light = 20.0
index 174d51f..d8e32fc 100644 (file)
@@ -9,7 +9,7 @@ Ks 0.287480 0.287480 0.287480
 Ni 1.000000
 d 1.000000
 illum 2
-map_Kd textures/tread.jpg
+map_Kd textures/TTread.jpg
 
 newmtl Treads
 Ns 178.431373
index 5c3d6b9..b37fc69 100644 (file)
@@ -122,8 +122,15 @@ redef class App
                show_splash_screen logo
 
                # Load everything
-               for model in models do model.load
-               for texture in all_root_textures do texture.load
+               for texture in all_root_textures do
+                       texture.load
+                       var error = texture.error
+                       if error != null then print_error error
+               end
+               for model in models do
+                       model.load
+                       if model.errors.not_empty then print_error model.errors.join("\n")
+               end
 
                # Modify all textures so they have a higher ambient color
                for model in models do
@@ -359,7 +366,7 @@ redef class Feature
                # Apply a random model and rotation to new features
                actor = new Actor(rule.models.rand,
                        new Point3d[Float](pos.x, 0.0, pos.y))
-               actor.rotation = 2.0*pi.rand
+               actor.yaw = 2.0*pi.rand
                actor.scale = 0.75
 
                self.actor = actor
@@ -426,7 +433,7 @@ redef class ExplosionEvent
                # Blast mark on the ground
                var blast = new Actor(app.blast_model, new Point3d[Float](pos.x, 0.05 & 0.04, pos.y))
                blast.scale = 3.0
-               blast.rotation = 2.0*pi.rand
+               blast.yaw = 2.0*pi.rand
                app.actors.add blast
 
                # Smoke
@@ -474,8 +481,8 @@ redef class TankMoveEvent
                        actor.center.z = pos.y
                end
 
-               tank.actors[0].rotation = tank.heading + pi
-               tank.actors[1].rotation = tank.turret.heading + pi
+               tank.actors[0].yaw = -tank.heading + pi
+               tank.actors[1].yaw = -tank.turret.heading + pi
 
                # Keep going only for the local tank
                var local_player = app.context.local_player
index 994e8be..890395a 100644 (file)
@@ -46,8 +46,30 @@ class Actor
        # Position of this sprite in world coordinates
        var center: Point3d[Float] is writable
 
-       # Rotation on the Z axis
-       var rotation = 0.0 is writable
+       # Rotation around the X axis (+ looks up, - looks down)
+       #
+       # Positive values look up, and negative look down.
+       #
+       # All actor rotations follow the right hand rule.
+       # The default orientation of the model should look towards -Z.
+       var pitch = 0.0 is writable
+
+       # Rotation around the Y axis (+ turns left, - turns right)
+       #
+       # Positive values turn `self` to the left, and negative values to the right.
+       #
+       # All actor rotations follow the right hand rule.
+       # The default orientation of the model should look towards -Z.
+       var yaw = 0.0 is writable
+
+       # Rotation around the Z axis (looking to -Z: + turns counterclockwise, - clockwise)
+       #
+       # From the default camera point of view, looking down on the Z axis,
+       # positive values turn `self` counterclockwise, and negative values clockwise.
+       #
+       # All actor rotations follow the right hand rule.
+       # The default orientation of the model should look towards -Z.
+       var roll = 0.0 is writable
 
        # Scale applied to the model
        var scale = 1.0 is writable
@@ -83,6 +105,9 @@ abstract class Model
        # Load this model in memory
        fun load do end
 
+       # Errors raised at loading
+       var errors = new Array[Error]
+
        # All `LeafModel` composing this model
        #
        # Usually, there is one `LeafModel` per material.
index 4cc8dcb..746a952 100644 (file)
@@ -49,7 +49,7 @@ class SmoothMaterial
                # Actor specs
                program.translation.uniform(actor.center.x, actor.center.y, actor.center.z, 0.0)
                program.scale.uniform actor.scale
-               program.rotation.uniform new Matrix.rotation(actor.rotation, 0.0, 1.0, 0.0)
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
 
                # From mesh
                program.coord.array_enabled = true
@@ -97,6 +97,9 @@ class TexturedMaterial
        # Texture applied to the specular color
        var specular_texture: nullable Texture = null is writable
 
+       # Bump map TODO
+       private var normals_texture: nullable Texture = null is writable
+
        redef fun draw(actor, model)
        do
                var mesh = model.mesh
@@ -140,6 +143,17 @@ class TexturedMaterial
                        program.use_map_specular.uniform false
                end
 
+               texture = normals_texture
+               if texture != null then
+                       glActiveTexture gl_TEXTURE3
+                       glBindTexture(gl_TEXTURE_2D, texture.gl_texture)
+                       program.use_map_bump.uniform true
+                       program.map_bump.uniform 3
+                       sample_used_texture = texture
+               else
+                       program.use_map_bump.uniform false
+               end
+
                program.translation.uniform(actor.center.x, actor.center.y, actor.center.z, 0.0)
                program.scale.uniform actor.scale
 
@@ -171,7 +185,8 @@ class TexturedMaterial
 
                program.coord.array_enabled = true
                program.coord.array(mesh.vertices, 3)
-               program.rotation.uniform new Matrix.rotation(actor.rotation, 0.0, 1.0, 0.0)
+
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
 
                program.ambient_color.uniform(ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3]*actor.alpha)
                program.diffuse_color.uniform(diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3]*actor.alpha)
@@ -216,7 +231,8 @@ class NormalsMaterial
 
                program.coord.array_enabled = true
                program.coord.array(mesh.vertices, 3)
-               program.rotation.uniform new Matrix.rotation(actor.rotation, 0.0, 1.0, 0.0)
+
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
 
                program.normal.array_enabled = true
                program.normal.array(mesh.normals, 3)
@@ -229,8 +245,8 @@ class NormalsMaterial
        end
 end
 
-# Graphic program to display 3D models with Lamber diffuse lighting
-class LambertProgram
+# Graphic program to display 3D models with Blinn-Phong specular lighting
+class BlinnPhongProgram
        super GamnitProgramFromSource
 
        redef var vertex_shader_source = """
@@ -263,20 +279,19 @@ class LambertProgram
                // Output for the fragment shader
                varying vec2 v_tex_coord;
                varying vec3 v_normal;
-               varying vec4 v_light_center;
-               varying vec4 v_camera;
+               varying vec4 v_to_light;
+               varying vec4 v_to_camera;
 
                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);
-                       v_normal = normalize(vec4(normal, 0.0) * rotation * mvp).xyz;
-
-                       gl_Position = (vec4(coord.xyz * scale, 1.0) * rotation + translation) * mvp;
-
-                       // TODO compute v_light_center and v_camera on the CPU side and pass as uniforms
-                       v_light_center = vec4(light_center, 0.0) * mvp;
-                       v_camera = vec4(camera, 0.0) * mvp;
+                       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);
                }
                """ @ glsl_vertex_shader
 
@@ -286,8 +301,8 @@ class LambertProgram
                // Input from the vertex shader
                varying vec2 v_tex_coord;
                varying vec3 v_normal;
-               varying vec4 v_light_center;
-               varying vec4 v_camera;
+               varying vec4 v_to_light;
+               varying vec4 v_to_camera;
 
                // Colors
                uniform vec4 ambient_color;
@@ -316,21 +331,39 @@ class LambertProgram
 
                void main()
                {
-                       // Lambert diffusion
-                       vec3 light_dir = normalize(v_light_center.xyz);
-                       float lambert = max(dot(light_dir, v_normal), 0.0);
-
-                       if (use_map_ambient)
-                               gl_FragColor = ambient_color * texture2D(map_ambient, v_tex_coord);
-                       else
-                               gl_FragColor = ambient_color;
-
-                       if (use_map_diffuse)
-                               gl_FragColor += lambert * diffuse_color * texture2D(map_diffuse, v_tex_coord);
-                       else
-                               gl_FragColor += lambert * diffuse_color;
-
+                       // Normal
+                       vec3 normal = v_normal;
+                       if (use_map_bump) {
+                               // TODO
+                               vec3 bump = 2.0 * texture2D(map_bump, v_tex_coord).rgb - 1.0;
+                       }
+
+                       // Ambient light
+                       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);
+
+                       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) {
+                               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
+                       }
+
+                       vec4 specular = s * specular_color;
+                       if (use_map_specular) specular *= texture2D(map_specular, v_tex_coord).x;
+
+                       gl_FragColor = ambient + diffuse + specular;
                        if (gl_FragColor.a < 0.01) discard;
+
+                       //gl_FragColor = vec4(normalize(normal).rgb, 1.0); // Debug
                }
                """ @ glsl_fragment_shader
 
@@ -355,6 +388,12 @@ class LambertProgram
        # Specularity texture unit
        var map_specular = uniforms["map_specular"].as(UniformSampler2D) is lazy
 
+       # Should this program use the texture `map_bump`?
+       var use_map_bump = uniforms["use_map_bump"].as(UniformBool) is lazy
+
+       # Bump texture unit
+       var map_bump = uniforms["map_bump"].as(UniformSampler2D) is lazy
+
        # Normal per vertex
        var normal = attributes["normal"].as(AttributeVec3) is lazy
 
@@ -390,8 +429,10 @@ class LambertProgram
 end
 
 # Program to color objects from their normal vectors
+#
+# May be used in place of `BlinnPhongProgram` for debugging or effect.
 class NormalProgram
-       super LambertProgram
+       super BlinnPhongProgram
 
        redef var fragment_shader_source = """
                precision mediump float;
@@ -407,7 +448,7 @@ class NormalProgram
 end
 
 redef class App
-       private var versatile_program = new LambertProgram is lazy
+       private var versatile_program = new BlinnPhongProgram is lazy
 
        private var normals_program = new NormalProgram is lazy
 end
index 0a4d53c..e316b89 100644 (file)
@@ -79,22 +79,31 @@ class Plane
        # TODO use gl_TRIANGLE_FAN instead
 end
 
-# Cube, with 6 faces
+# Cuboid, or rectangular prism, with 6 faces and right angles
 #
-# Occupies `[-0.5..0.5]` on all three axes.
-class Cube
+# Can be created from a `Boxed3d` using `to_mesh`.
+class Cuboid
        super Mesh
 
+       # Width, on the X axis
+       var width: Float
+
+       # Height, on the Y axis
+       var height: Float
+
+       # Depth, on the Z axis
+       var depth: Float
+
        redef var vertices is lazy do
-               var a = [-0.5, -0.5, -0.5]
-               var b = [ 0.5, -0.5, -0.5]
-               var c = [-0.5,  0.5, -0.5]
-               var d = [ 0.5,  0.5, -0.5]
+               var a = [-0.5*width, -0.5*height, -0.5*depth]
+               var b = [ 0.5*width, -0.5*height, -0.5*depth]
+               var c = [-0.5*width,  0.5*height, -0.5*depth]
+               var d = [ 0.5*width,  0.5*height, -0.5*depth]
 
-               var e = [-0.5, -0.5,  0.5]
-               var f = [ 0.5, -0.5,  0.5]
-               var g = [-0.5,  0.5,  0.5]
-               var h = [ 0.5,  0.5,  0.5]
+               var e = [-0.5*width, -0.5*height,  0.5*depth]
+               var f = [ 0.5*width, -0.5*height,  0.5*depth]
+               var g = [-0.5*width,  0.5*height,  0.5*depth]
+               var h = [ 0.5*width,  0.5*height,  0.5*depth]
 
                var vertices = new Array[Float]
                for v in [a, c, d, a, d, b, # front
@@ -135,6 +144,35 @@ class Cube
        redef var center = new Point3d[Float](0.0, 0.0, 0.0) is lazy
 end
 
+# Cube, with 6 faces, edges of equal length and square angles
+#
+# Occupies `[-0.5..0.5]` on all three axes.
+class Cube
+       super Cuboid
+
+       noautoinit
+
+       init
+       do
+               width = 1.0
+               height = 1.0
+               depth = 1.0
+       end
+end
+
+redef class Boxed3d[N]
+       # Create a `Cuboid` mesh with the dimension of `self`
+       #
+       # Does not use the position of `self`, but it can be given to an `Actor`.
+       fun to_mesh: Cuboid
+       do
+               var width = right.to_f-left.to_f
+               var height = top.to_f-bottom.to_f
+               var depth = front.to_f-back.to_f
+               return new Cuboid(width, height, depth)
+       end
+end
+
 # Sphere with `radius` and a number of faces set by `n_meridians` and `n_parallels`
 class UVSphere
        super Mesh
index 8d7d8a1..eb148c9 100644 (file)
@@ -39,20 +39,24 @@ class ModelAsset
 
        init do models.add self
 
+       private var loaded = false
+
        redef fun load
        do
                var ext = path.file_extension
                if ext == "obj" then
                        load_obj_file
                else
-                       print_error "Model failed to load: Extension '{ext or else "null"}' unrecognized"
+                       errors.add new Error("Model at '{path}' failed to load: Extension '{ext or else "null"}' unrecognized")
                end
 
-               if leaves.is_empty then
+               if leaves_cache.is_empty then
                        # Nothing was loaded, use a cube with the default material
                        var leaf = placeholder_model
-                       leaves.add leaf
+                       leaves_cache.add leaf
                end
+
+               loaded = true
        end
 
        private fun load_obj_file
@@ -61,8 +65,8 @@ class ModelAsset
                var text_asset = new TextAsset(path)
                var content = text_asset.to_s
                if content.is_empty then
-                       print_error "Model failed to load: Asset empty at '{self.path}'"
-                       leaves.add new LeafModel(new Cube, new Material)
+                       errors.add new Error("Model failed to load: Asset empty at '{self.path}'")
+                       leaves_cache.add new LeafModel(new Cube, new Material)
                        return
                end
 
@@ -70,8 +74,8 @@ class ModelAsset
                var parser = new ObjFileParser(content)
                var obj_def = parser.parse
                if obj_def == null then
-                       print_error "Model failed to load: .obj format error on '{self.path}'"
-                       leaves.add new LeafModel(new Cube, new Material)
+                       errors.add new Error("Model failed to load: .obj format error on '{self.path}'")
+                       leaves_cache.add new LeafModel(new Cube, new Material)
                        return
                end
 
@@ -80,10 +84,29 @@ class ModelAsset
 
                # Build models
                var converter = new ModelFromObj(path, obj_def)
-               converter.models leaves
+               converter.models leaves_cache
+               errors.add_all converter.errors
        end
 
-       redef var leaves = new Array[LeafModel]
+       redef fun leaves
+       do
+               if not loaded then
+                       # Lazy load
+                       load
+
+                       # Print errors when lazy loading only
+                       if errors.length == 1 then
+                               print_error errors.first
+                       else if errors.length > 1 then
+                               print_error "Loading model at '{path}' raised {errors.length} errors:\n* "
+                               print_error errors.join("\n* ")
+                       end
+               end
+
+               return leaves_cache
+       end
+
+       private var leaves_cache = new Array[LeafModel]
 end
 
 # Short-lived service to convert an `ObjDef` to `models`
@@ -98,7 +121,11 @@ private class ModelFromObj
        # Parsed .obj definition
        var obj_def: ObjDef
 
-       fun models(array: Array[LeafModel])
+       # Errors raised by calls to `models`
+       var errors = new Array[Error]
+
+       # Fill `leaves` with models described in `obj_def`
+       fun models(leaves: Array[LeafModel])
        do
                # Sort faces by material
                var mtl_to_faces = new MultiHashMap[String, ObjFace]
@@ -113,23 +140,22 @@ private class ModelFromObj
                end
 
                # Load material libs
-               # TODO do not load each libs more than once
-               var mtl_libs = new Map[String, Map[String, MtlDef]]
+               var mtl_libs = sys.mtl_libs
                var lib_names = obj_def.material_libs
                for name in lib_names do
-                       var lib_path = self.path.dirname / name
-                       var lib_asset = new TextAsset(lib_path)
+                       var asset_path = self.path.dirname / name
+                       var lib_asset = new TextAsset(asset_path)
                        lib_asset.load
 
                        var error = lib_asset.error
                        if error != null then
-                               print_error error.to_s
+                               errors.add error
                                continue
                        end
 
                        var mtl_parser = new MtlFileParser(lib_asset.to_s)
                        var mtl_lib = mtl_parser.parse
-                       mtl_libs[name] = mtl_lib
+                       mtl_libs[asset_path] = mtl_lib
                end
 
                # Create 1 mesh per material, and prepare materials
@@ -149,7 +175,8 @@ private class ModelFromObj
                        var mtl_lib_name = faces.first.material_lib
                        var mtl_name = faces.first.material_name
                        if mtl_lib_name != null and mtl_name != null then
-                               var mtl_lib = mtl_libs[mtl_lib_name]
+                               var asset_path = self.path.dirname / mtl_lib_name
+                               var mtl_lib = mtl_libs[asset_path]
                                var mtl = mtl_lib.get_or_null(mtl_name)
                                if mtl != null then
                                        mtl_def = mtl
@@ -158,7 +185,7 @@ private class ModelFromObj
                                                texture_names.add self.path.dirname / e
                                        end
                                else
-                                       print_error "mtl '{mtl_name}' not found in '{mtl_lib_name}'"
+                                       errors.add new Error("Error loading model at '{path}': mtl '{mtl_name}' not found in '{asset_path}'")
                                end
                        end
 
@@ -170,6 +197,10 @@ private class ModelFromObj
                        if not asset_textures_by_name.keys.has(name) then
                                var tex = new TextureAsset(name)
                                asset_textures_by_name[name] = tex
+
+                               tex.load
+                               var error = tex.error
+                               if error != null then errors.add error
                        end
                end
 
@@ -215,7 +246,7 @@ private class ModelFromObj
                        if material == null then material = new Material
 
                        var model = new LeafModel(mesh, material)
-                       array.add model
+                       leaves.add model
                end
        end
 
@@ -381,6 +412,9 @@ redef class Sys
        # Textures loaded from .mtl files for models
        var asset_textures_by_name = new Map[String, TextureAsset]
 
+       # Loaded .mtl material definitions, sorted by path in assets and material name
+       private var mtl_libs = new Map[String, Map[String, MtlDef]]
+
        # All instantiated asset models
        var models = new Set[ModelAsset]
 
index 3d0e841..954b962 100644 (file)
@@ -157,7 +157,7 @@ redef class Material
 
                program.coord.array_enabled = true
                program.coord.array(mesh.vertices, 3)
-               program.rotation.uniform new Matrix.rotation(actor.rotation, 0.0, 1.0, 0.0)
+               program.rotation.uniform new Matrix.gamnit_euler_rotation(actor.pitch, actor.yaw, actor.roll)
 
                var display = app.display
                assert display != null
index 634656b..cb6bdf0 100644 (file)
@@ -36,6 +36,10 @@ redef class GamnitDisplay
 
        redef fun show_cursor=(val) do sdl.show_cursor = val
 
+       redef fun lock_cursor=(val) do sdl.relative_mouse_mode = val
+
+       redef fun lock_cursor do return sdl.relative_mouse_mode
+
        # Setup SDL, wm, EGL in order
        redef fun setup
        do
index 8439b29..9c2c220 100644 (file)
@@ -63,8 +63,8 @@ redef class App
                # cause glitches on mobiles devices with small depth buffer.
                world_camera.near = 1.0
 
-               # Make the background blue and opaque.
-               glClearColor(0.0, 0.0, 1.0, 1.0)
+               # Make the background sky blue and opaque.
+               glClearColor(0.5, 0.8, 1.0, 1.0)
 
                # If the first command line argument is an integer, add extra sprites.
                if args.not_empty and args.first.is_int then
@@ -109,8 +109,9 @@ redef class App
                if event isa QuitEvent or
                  (event isa KeyEvent and event.name == "escape" and event.is_up) then
                        # When window close button, escape or back key is pressed
-                       # show the average FPS over the last few seconds.
-                       print "{current_fps} fps"
+                       print "Ran at {current_fps} FPS in the last few seconds"
+
+                       print "Performance statistics to detect bottlenecks:"
                        print sys.perfs
 
                        # Quit abruptly
index abd573d..e9f8499 100644 (file)
@@ -155,7 +155,7 @@ class ObjDef
        # Faces
        var faces = new Array[ObjFace]
 
-       # Referenced material libraries
+       # Relative paths to referenced material libraries
        fun material_libs: Set[String] do
                var libs = new Set[String]
                for face in faces do
index f999637..1c8f10d 100644 (file)
@@ -31,8 +31,7 @@ redef class Point[N]
        do
                var dx = other.x.to_f - x.to_f
                var dy = other.y.to_f - y.to_f
-               var a = sys.atan2(dy.to_f, dx.to_f)
-               return a
+               return sys.atan2(dy.to_f, dx.to_f)
        end
 end
 
@@ -52,19 +51,21 @@ redef universal Float
                return s
        end
 
-       # Linear interpolation on the arc delimited by `self` and `other` at `p` out of 1.0
+       # Linear interpolation of between the angles `a` and `b`, in radians
        #
        # The result is normalized with `angle_normalize`.
        #
        # ~~~
-       # assert 0.0.angle_lerp(pi, 0.5).is_approx(0.5*pi, 0.0001)
-       # assert 0.0.angle_lerp(pi, 8.5).is_approx(0.5*pi, 0.0001)
-       # assert 0.0.angle_lerp(pi, 7.5).is_approx(-0.5*pi, 0.0001)
+       # assert 0.5.angle_lerp(0.0, pi).is_approx(0.5*pi, 0.0001)
+       # assert 8.5.angle_lerp(0.0, pi).is_approx(0.5*pi, 0.0001)
+       # assert 7.5.angle_lerp(0.0, pi).is_approx(-0.5*pi, 0.0001)
+       # assert 0.5.angle_lerp(0.2, 2.0*pi-0.1).is_approx(0.05, 0.0001)
        # ~~~
-       fun angle_lerp(other, p: Float): Float
+       fun angle_lerp(a, b: Float): Float
        do
-               var d = other - self
-               var a = self + d*p
-               return a.angle_normalize
+               var d = b - a
+               while d > pi do d -= 2.0*pi
+               while d < -pi do d += 2.0*pi
+               return (a + d*self).angle_normalize
        end
 end
index 908761a..a9bf8b8 100644 (file)
@@ -150,4 +150,27 @@ redef class Matrix
                var rotated = self * rotation
                self.items = rotated.items
        end
+
+       # Rotation matrix from Euler angles `pitch`, `yaw` and `roll` in radians
+       #
+       # Apply a composition of intrinsic rotations around the axes x-y'-z''.
+       # Or `pitch` around the X axis, `yaw` around Y and `roll` around Z,
+       # applied successively. All rotations follow the right hand rule.
+       #
+       # This service aims to respect the world axes and logic of `gamnit`,
+       # it may not correspond to all needs.
+       new gamnit_euler_rotation(pitch, yaw, roll: Float)
+       do
+               var c1 = pitch.cos
+               var s1 = pitch.sin
+               var c2 = yaw.cos
+               var s2 = yaw.sin
+               var c3 = roll.cos
+               var s3 = roll.sin
+               return new Matrix.from(
+                       [[          c2*c3,          -c2*s3,   -s2, 0.0],
+                        [ c1*s3+c3*s1*s2,  c1*c3-s1*s2*s3, c2*s1, 0.0],
+                        [-s1*s3+c1*c3*s2, -c3*s1-c1*s2*s3, c1*c2, 0.0],
+                        [            0.0,             0.0,   0.0, 1.0]])
+       end
 end