# 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
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
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
# 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
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]
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
Ni 1.000000
d 1.000000
illum 2
-map_Kd textures/tread.jpg
+map_Kd textures/TTread.jpg
newmtl Treads
Ns 178.431373
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
# 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
# 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
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
# 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
# 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.
# 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
# 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
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
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)
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)
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 = """
// 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
// 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;
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
# 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
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;
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
# 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
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
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
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
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
# 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`
# 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]
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
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
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
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
if material == null then material = new Material
var model = new LeafModel(mesh, material)
- array.add model
+ leaves.add model
end
end
# 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]
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
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
# 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
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
# 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
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
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
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