diff --git a/Cargo.lock b/Cargo.lock index bfff70b..df2430f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bit_field" version = "0.10.2" @@ -224,6 +230,12 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -571,8 +583,12 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" name = "fps" version = "0.1.0" dependencies = [ + "anyhow", + "glam", "glium", + "gltf", "glutin", + "hecs", "image", ] @@ -636,6 +652,12 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glam" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11" + [[package]] name = "glium" version = "0.36.0" @@ -653,6 +675,45 @@ dependencies = [ "winit", ] +[[package]] +name = "gltf" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" +dependencies = [ + "base64", + "byteorder", + "gltf-json", + "image", + "lazy_static", + "serde_json", + "urlencoding", +] + +[[package]] +name = "gltf-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" +dependencies = [ + "inflections", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "glutin" version = "0.32.3" @@ -729,6 +790,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -741,6 +811,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hecs" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cbc675ee8d97b4d206a985137f8ad59666538f56f906474f554467a63c776d" +dependencies = [ + "hashbrown 0.14.5", + "spin", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -793,9 +873,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + [[package]] name = "interpolate_name" version = "0.2.4" @@ -816,6 +902,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jni" version = "0.21.1" @@ -870,6 +962,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lebe" version = "0.5.2" @@ -1718,6 +1816,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -1766,6 +1870,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1842,6 +1958,12 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strict-num" version = "0.1.1" @@ -2002,6 +2124,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "v_frame" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 8b84c8b..8657bbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] -glium = "0.36.0" -glutin = "0.32.3" -image = "0.25.6" +anyhow = "1.0.98" +glam = "0.30.4" + +# glium already re-exports glutin/winit, but we enable the helper features explicitly +glium = { version = "0.36.0", features = ["glutin_backend", "simple_window_builder"] } +gltf = { version = "1.4.1", features = ["import"] } + +# explicit glutin for raw types (same semver as glium’s internal one) +glutin = { version = "0.32.3", default-features = false } + +hecs = "0.10.5" +image = "0.25.6" diff --git a/resources/cube.bin b/resources/cube.bin new file mode 100644 index 0000000..29df9d9 Binary files /dev/null and b/resources/cube.bin differ diff --git a/resources/cube.gltf b/resources/cube.gltf new file mode 100644 index 0000000..8db6d67 --- /dev/null +++ b/resources/cube.gltf @@ -0,0 +1,121 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Cube" + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.5 + } + } + ], + "meshes":[ + { + "name":"Cube", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":24, + "max":[ + 1, + 1, + 1 + ], + "min":[ + -1, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":24, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":24, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":36, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":288, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":288, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":576, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":768, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":840, + "uri":"cube.bin" + } + ] +} diff --git a/resources/cube.obj b/resources/cube.obj new file mode 100644 index 0000000..c4d834c --- /dev/null +++ b/resources/cube.obj @@ -0,0 +1,33 @@ +# cube.obj +# + +g cube + +v 0.0 0.0 0.0 +v 0.0 0.0 1.0 +v 0.0 1.0 0.0 +v 0.0 1.0 1.0 +v 1.0 0.0 0.0 +v 1.0 0.0 1.0 +v 1.0 1.0 0.0 +v 1.0 1.0 1.0 + +vn 0.0 0.0 1.0 +vn 0.0 0.0 -1.0 +vn 0.0 1.0 0.0 +vn 0.0 -1.0 0.0 +vn 1.0 0.0 0.0 +vn -1.0 0.0 0.0 + +f 1//2 7//2 5//2 +f 1//2 3//2 7//2 +f 1//6 4//6 3//6 +f 1//6 2//6 4//6 +f 3//3 8//3 7//3 +f 3//3 4//3 8//3 +f 5//5 7//5 8//5 +f 5//5 8//5 6//5 +f 1//4 5//4 6//4 +f 1//4 6//4 2//4 +f 2//1 6//1 8//1 +f 2//1 8//1 4//1 diff --git a/resources/monkey-smooth.bin b/resources/monkey-smooth.bin new file mode 100644 index 0000000..f6f4431 Binary files /dev/null and b/resources/monkey-smooth.bin differ diff --git a/resources/monkey-smooth.gltf b/resources/monkey-smooth.gltf new file mode 100644 index 0000000..21b6404 --- /dev/null +++ b/resources/monkey-smooth.gltf @@ -0,0 +1,104 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Suzanne" + } + ], + "meshes":[ + { + "name":"Suzanne", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":555, + "max":[ + 1.3671875, + 0.984375, + 0.8515625 + ], + "min":[ + -1.3671875, + -0.984375, + -0.8515625 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":555, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":555, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":2904, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":6660, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":6660, + "byteOffset":6660, + "target":34962 + }, + { + "buffer":0, + "byteLength":4440, + "byteOffset":13320, + "target":34962 + }, + { + "buffer":0, + "byteLength":5808, + "byteOffset":17760, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":23568, + "uri":"monkey-smooth.bin" + } + ] +} diff --git a/resources/monkey.bin b/resources/monkey.bin new file mode 100644 index 0000000..e7992f3 Binary files /dev/null and b/resources/monkey.bin differ diff --git a/resources/monkey.gltf b/resources/monkey.gltf new file mode 100644 index 0000000..d3e2438 --- /dev/null +++ b/resources/monkey.gltf @@ -0,0 +1,104 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Suzanne" + } + ], + "meshes":[ + { + "name":"Suzanne", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":1966, + "max":[ + 1.3671875, + 0.984375, + 0.8515625 + ], + "min":[ + -1.3671875, + -0.984375, + -0.8515625 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":1966, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":1966, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":2904, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":23592, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":23592, + "byteOffset":23592, + "target":34962 + }, + { + "buffer":0, + "byteLength":15728, + "byteOffset":47184, + "target":34962 + }, + { + "buffer":0, + "byteLength":5808, + "byteOffset":62912, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":68720, + "uri":"monkey.bin" + } + ] +} diff --git a/resources/uvsphere-smooth.bin b/resources/uvsphere-smooth.bin new file mode 100644 index 0000000..f8b6e5c Binary files /dev/null and b/resources/uvsphere-smooth.bin differ diff --git a/resources/uvsphere-smooth.gltf b/resources/uvsphere-smooth.gltf new file mode 100644 index 0000000..0f65991 --- /dev/null +++ b/resources/uvsphere-smooth.gltf @@ -0,0 +1,109 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Sphere", + "translation":[ + 0, + 0, + 0.003377079963684082 + ] + } + ], + "meshes":[ + { + "name":"Sphere", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":559, + "max":[ + 0.9999997019767761, + 1, + 0.9999993443489075 + ], + "min":[ + -0.9999990463256836, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":559, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":559, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":2880, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":6708, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":6708, + "byteOffset":6708, + "target":34962 + }, + { + "buffer":0, + "byteLength":4472, + "byteOffset":13416, + "target":34962 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":17888, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":23648, + "uri":"uvsphere-smooth.bin" + } + ] +} diff --git a/resources/uvsphere.bin b/resources/uvsphere.bin new file mode 100644 index 0000000..dda76ba Binary files /dev/null and b/resources/uvsphere.bin differ diff --git a/resources/uvsphere.gltf b/resources/uvsphere.gltf new file mode 100644 index 0000000..edf10c7 --- /dev/null +++ b/resources/uvsphere.gltf @@ -0,0 +1,104 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Sphere" + } + ], + "meshes":[ + { + "name":"Sphere", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":1984, + "max":[ + 0.9999997019767761, + 1, + 0.9999993443489075 + ], + "min":[ + -0.9999990463256836, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":1984, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":1984, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":2880, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":23808, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":23808, + "byteOffset":23808, + "target":34962 + }, + { + "buffer":0, + "byteLength":15872, + "byteOffset":47616, + "target":34962 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":63488, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":69248, + "uri":"uvsphere.bin" + } + ] +} diff --git a/resources/uvsphere2.bin b/resources/uvsphere2.bin new file mode 100644 index 0000000..c71a8be Binary files /dev/null and b/resources/uvsphere2.bin differ diff --git a/resources/uvsphere2.gltf b/resources/uvsphere2.gltf new file mode 100644 index 0000000..6657f6e --- /dev/null +++ b/resources/uvsphere2.gltf @@ -0,0 +1,190 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.4.56", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Sphere", + "translation":[ + 0, + 0, + -1.0871706008911133 + ] + }, + { + "mesh":1, + "name":"Cube", + "translation":[ + 0, + 0, + 1.0190757513046265 + ] + } + ], + "meshes":[ + { + "name":"Sphere", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + }, + { + "name":"Cube.001", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":7 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":1984, + "max":[ + 0.9999997019767761, + 1, + 0.9999993443489075 + ], + "min":[ + -0.9999990463256836, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":1984, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":1984, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":2880, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":24, + "max":[ + 1, + 1, + 1 + ], + "min":[ + -1, + -1, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":24, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":24, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5123, + "count":36, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":23808, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":23808, + "byteOffset":23808, + "target":34962 + }, + { + "buffer":0, + "byteLength":15872, + "byteOffset":47616, + "target":34962 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":63488, + "target":34963 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":69248, + "target":34962 + }, + { + "buffer":0, + "byteLength":288, + "byteOffset":69536, + "target":34962 + }, + { + "buffer":0, + "byteLength":192, + "byteOffset":69824, + "target":34962 + }, + { + "buffer":0, + "byteLength":72, + "byteOffset":70016, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":70088, + "uri":"uvsphere2.bin" + } + ] +} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..53b3599 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,24 @@ +use glam::{Mat4, Vec3}; + +#[derive(Copy, Clone)] +pub struct Camera { + pub eye: Vec3, + pub center: Vec3, + pub up: Vec3, + pub fovy: f32, + pub aspect: f32, + pub znear: f32, + pub zfar: f32, +} + +impl Camera { + pub fn view(&self) -> Mat4 { + Mat4::look_at_rh(self.eye, self.center, self.up) + } + pub fn projection(&self) -> Mat4 { + Mat4::perspective_rh(self.fovy, self.aspect, self.znear, self.zfar) + } + pub fn view_proj(&self) -> Mat4 { + self.projection() * self.view() + } +} diff --git a/src/ecs.rs b/src/ecs.rs new file mode 100644 index 0000000..2fed082 --- /dev/null +++ b/src/ecs.rs @@ -0,0 +1,32 @@ +use glam::{Mat4, Quat, Vec3}; +use hecs::World; + +/// ------------ components ------------ +#[derive(Copy, Clone)] +pub struct Transform { + pub translation: Vec3, + pub rotation: Quat, + pub scale: Vec3, +} +impl Transform { + pub fn matrix(&self) -> Mat4 { + Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) + } +} + +#[derive(Clone)] +pub struct MeshHandle(pub usize); + +/// ------------ systems ------------ +pub fn rotation_system(world: &mut World, dt: f32) { + for (_, transform) in world.query_mut::<&mut Transform>() { + transform.rotation *= Quat::from_rotation_y(dt); + } +} + +/// Update the aspect ratio for all camera components in the world. +pub fn set_camera_aspect(world: &mut World, aspect: f32) { + for (_, cam) in world.query_mut::<&mut crate::camera::Camera>() { + cam.aspect = aspect; + } +} diff --git a/src/main.rs b/src/main.rs index b31ae36..7d0ac14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,127 +1,86 @@ -#[macro_use] -extern crate glium; -use glium::Surface; -mod teapot; +mod camera; +mod ecs; +mod model; +mod render; -fn main() { +use anyhow::Result; +use camera::Camera; +use ecs::{rotation_system, MeshHandle, Transform}; +use glam::{Quat, Vec3}; +use glium::backend::glutin::SimpleWindowBuilder; +use render::{Renderer, GliumRenderer}; +use std::time::Instant; + +fn main() -> Result<()> { let event_loop = glium::winit::event_loop::EventLoop::builder() .build() - .expect("event loop building"); - let (window, display) = glium::backend::glutin::SimpleWindowBuilder::new() - .with_title("Glium tutorial #3") + .expect("create event-loop"); + + let (window, display) = SimpleWindowBuilder::new() + .with_title("fps") + .with_inner_size(1280, 720) .build(&event_loop); + let mut world = hecs::World::new(); - let positions = glium::VertexBuffer::new(&display, &teapot::VERTICES).unwrap(); - let normals = glium::VertexBuffer::new(&display, &teapot::NORMALS).unwrap(); - let indices = glium::IndexBuffer::new(&display, glium::index::PrimitiveType::TrianglesList, - &teapot::INDICES).unwrap(); + let mesh = model::load_gltf("resources/monkey-smooth.gltf", &display)?; + // let mesh = model::cube(&display)?; + let mut renderer = GliumRenderer::new(display)?; + let mesh_id = renderer.meshes.len(); + renderer.meshes.push(mesh); - let vertex_shader_src = r#" - #version 140 + world.spawn(( + Transform { + translation: Vec3::ZERO, + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + }, + MeshHandle(mesh_id), + )); - in vec3 position; - in vec3 normal; + { + let (w, h): (u32, u32) = window.inner_size().into(); + world.spawn((Camera { + eye: Vec3::new(3.0, 2.0, 3.0), + center: Vec3::ZERO, + up: Vec3::Y, + fovy: 45_f32.to_radians(), + aspect: w as f32 / h as f32, + znear: 0.1, + zfar: 100.0, + },)); + } - out vec3 v_normal; + event_loop + .run(move |event, el| { + use glium::winit::event::{Event, WindowEvent}; - uniform mat4 matrix; - uniform mat4 perspective; - - void main() { - v_normal = transpose(inverse(mat3(matrix))) * normal; - gl_Position = perspective * matrix * vec4(position, 1.0); - } - "#; - let fragment_shader_src = r#" - #version 140 - - in vec3 v_normal; - out vec4 color; - uniform vec3 u_light; - - void main() { - float brightness = dot(normalize(v_normal), normalize(u_light)); - vec3 dark_color = vec3(0.6, 0.0, 0.0); - vec3 regular_color = vec3(1.0, 0.0, 0.0); - color = vec4(mix(dark_color, regular_color, brightness), 1.0); - } - "#; - let program = glium::Program::from_source(&display, vertex_shader_src, fragment_shader_src, None).unwrap(); - - let light = [-1.0, 0.4, 0.9f32]; - let mut t: f32 = 0.0; - #[allow(deprecated)] - event_loop.run(move |ev, window_target| { - match ev { - glium::winit::event::Event::WindowEvent { event, .. } => match event { - glium::winit::event::WindowEvent::CloseRequested => { - window_target.exit(); + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => el.exit(), + WindowEvent::Resized(sz) => { + ecs::set_camera_aspect(&mut world, sz.width as f32 / sz.height as f32); + } + WindowEvent::RedrawRequested => { + renderer.render(&world); + } + _ => {} }, - // We now need to render everyting in response to a RedrawRequested event due to the animation - glium::winit::event::WindowEvent::RedrawRequested => { - let mut target = display.draw(); - target.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0); - t += 0.01; - let x = t.sin() * 0.5; - let y = t.cos() * 0.5; - - let perspective = { - let (width, height) = target.get_dimensions(); - let aspect_ratio = height as f32 / width as f32; - - let fov: f32 = 3.141592 / 3.0; - let zfar = 1024.0; - let znear = 0.1; - - let f = 1.0 / (fov / 2.0).tan(); - - [ - [f * aspect_ratio , 0.0, 0.0 , 0.0], - [ 0.0 , f , 0.0 , 0.0], - [ 0.0 , 0.0, (zfar+znear)/(zfar-znear) , 1.0], - [ 0.0 , 0.0, -(2.0*zfar*znear)/(zfar-znear), 0.0], - ] + Event::AboutToWait => { + // -- update logic -- + let now = Instant::now(); + static mut LAST: Option = None; + let dt = unsafe { // FIXME + let last = LAST.replace(now).unwrap_or(now); + (now - last).as_secs_f32() }; + rotation_system(&mut world, dt); - let uniforms = uniform! { - matrix: [ - [0.01, 0.0, 0.0, 0.0], - [0.0, 0.01, 0.0, 0.0], - [0.0, 0.0, 0.01, 0.0], - [x, y, 2.0, 1.0f32 ], - ], - u_light: light, - perspective: perspective - }; - - let params = glium::DrawParameters { - depth: glium::Depth { - test: glium::draw_parameters::DepthTest::IfLess, - write: true, - .. Default::default() - }, - .. Default::default() - }; - - target.draw((&positions, &normals), &indices, &program, &uniforms, - ¶ms).unwrap(); - target.finish().unwrap(); - }, - // Because glium doesn't know about windows we need to resize the display - // when the window's size has changed. - glium::winit::event::WindowEvent::Resized(window_size) => { - display.resize(window_size.into()); - }, - _ => (), - }, - // By requesting a redraw in response to a RedrawEventsCleared event we get continuous rendering. - // For applications that only change due to user input you could remove this handler. - glium::winit::event::Event::AboutToWait => { - window.request_redraw(); - }, - _ => (), - } - }) - .unwrap(); + // ask for next frame + window.request_redraw(); + } + _ => {} + } + }) + .map_err(Into::into) } diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..815e2a2 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,124 @@ +//! GPU-ready mesh loader for **glTF 2.0** +//! +//! Loads the first mesh/primitive found in a .gltf/.glb file. + +use anyhow::{Context, Result}; +use glium::{backend::Facade, implement_vertex, IndexBuffer, VertexBuffer}; +use glium::index::PrimitiveType; +use gltf::mesh::util::ReadIndices; +use std::{fmt::Debug, path::Path}; + +#[derive(Copy, Clone)] +pub struct Vertex { + pub position: [f32; 3], + pub normal: [f32; 3], +} +implement_vertex!(Vertex, position, normal); + +pub struct Mesh { + pub vbuf: VertexBuffer, + pub ibuf: IndexBuffer, +} + +/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU. +pub fn load_gltf(path: P, facade: &F) -> Result +where + P: AsRef + Debug, // `gltf::import` wants Debug for diagnostics :contentReference[oaicite:3]{index=3} + F: Facade + ?Sized, +{ + // -- parse the asset & bring buffer blobs into memory -- + let (doc, buffers, _images) = + gltf::import(path.as_ref()).context("failed to import glTF file")?; // :contentReference[oaicite:4]{index=4} + + // -- grab the very first mesh / primitive -- + let mesh = doc.meshes().next().context("glTF has no meshes")?; + let primitive = mesh.primitives().next().context("mesh has no primitives")?; + + // -- read vertex and index streams using the util::Reader helper -- + let reader = primitive.reader(|buf| Some(&buffers[buf.index()].0)); // Reader pattern :contentReference[oaicite:5]{index=5} + + let positions : Vec<[f32; 3]> = reader + .read_positions() + .context("primitive is missing POSITION attribute")? // POSITION is mandatory :contentReference[oaicite:6]{index=6} + .collect(); + + let normals : Vec<[f32; 3]> = reader + .read_normals() + .context("primitive is missing NORMAL attribute")? + .collect(); + + let indices : Vec = reader + .read_indices() + .context("primitive has no indices")? + .into_u32() + .collect(); // ReadIndices enum :contentReference[oaicite:7]{index=7} + + // -- interleave into our engine's Vertex struct -- + let vertices: Vec = positions + .into_iter() + .zip(normals.into_iter()) + .map(|(p, n)| Vertex { position: p, normal: n }) + .collect(); + + // -- immutable GPU buffers (fast path in glium) -- + let vbuf = VertexBuffer::immutable(facade, &vertices)?; // Immutable VBO :contentReference[oaicite:8]{index=8} + let ibuf = IndexBuffer ::immutable(facade, PrimitiveType::TrianglesList, &indices)?; + + Ok(Mesh { vbuf, ibuf }) +} + +/// Create a unit cube (edge length = 2) with per-face normals. +pub fn cube(facade: &F) -> Result +where + F: Facade + ?Sized, +{ + // 24 unique vertices (4 per face) so that each face has a flat normal. + let vertices: [Vertex; 24] = [ + // Front (+Z) + Vertex { position: [-1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, + Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, + Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, + Vertex { position: [-1.0, 1.0, 1.0], normal: [ 0.0, 0.0, 1.0] }, + + // Back (-Z) + Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, + Vertex { position: [-1.0, -1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, + Vertex { position: [-1.0, 1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, + Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 0.0, 0.0, -1.0] }, + + // Left (-X) + Vertex { position: [-1.0, -1.0, -1.0], normal: [-1.0, 0.0, 0.0] }, + Vertex { position: [-1.0, -1.0, 1.0], normal: [-1.0, 0.0, 0.0] }, + Vertex { position: [-1.0, 1.0, 1.0], normal: [-1.0, 0.0, 0.0] }, + Vertex { position: [-1.0, 1.0, -1.0], normal: [-1.0, 0.0, 0.0] }, + + // Right (+X) + Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 1.0, 0.0, 0.0] }, + Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 1.0, 0.0, 0.0] }, + Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 1.0, 0.0, 0.0] }, + Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 1.0, 0.0, 0.0] }, + + // Top (+Y) + Vertex { position: [-1.0, 1.0, 1.0], normal: [ 0.0, 1.0, 0.0] }, + Vertex { position: [ 1.0, 1.0, 1.0], normal: [ 0.0, 1.0, 0.0] }, + Vertex { position: [ 1.0, 1.0, -1.0], normal: [ 0.0, 1.0, 0.0] }, + Vertex { position: [-1.0, 1.0, -1.0], normal: [ 0.0, 1.0, 0.0] }, + + // Bottom (-Y) + Vertex { position: [-1.0, -1.0, -1.0], normal: [ 0.0, -1.0, 0.0] }, + Vertex { position: [ 1.0, -1.0, -1.0], normal: [ 0.0, -1.0, 0.0] }, + Vertex { position: [ 1.0, -1.0, 1.0], normal: [ 0.0, -1.0, 0.0] }, + Vertex { position: [-1.0, -1.0, 1.0], normal: [ 0.0, -1.0, 0.0] }, + ]; + + let mut indices: Vec = Vec::with_capacity(36); + for face in 0..6 { + let o = (face * 4) as u32; + indices.extend_from_slice(&[o, o + 1, o + 2, o, o + 2, o + 3]); + } + + let vbuf = VertexBuffer::immutable(facade, &vertices)?; + let ibuf = IndexBuffer::immutable(facade, PrimitiveType::TrianglesList, &indices)?; + + Ok(Mesh { vbuf, ibuf }) +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..e6319b4 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,95 @@ +use crate::camera::Camera; +use crate::ecs::{MeshHandle, Transform}; +use crate::model::Mesh; +use glium::{uniform, Program, Surface}; +use glam::Vec3; +use hecs::World; +use glium::glutin::surface::WindowSurface; + +/// Generic rendering backend trait. +pub trait Renderer { + /// Render a single frame for the given `World`. + fn render(&mut self, world: &World); +} + +/// Concrete OpenGL (glium) renderer implementing `Renderer`. +pub struct GliumRenderer { + display: glium::Display, + program: Program, + pub meshes: Vec, + params: glium::DrawParameters<'static>, +} + +impl GliumRenderer { + /// Create a new OpenGL renderer consuming the provided `display`. + pub fn new(display: glium::Display) -> anyhow::Result { + const VERT: &str = r#" + #version 330 core + in vec3 position; + in vec3 normal; + uniform mat4 model; + uniform mat4 view; + uniform mat4 projection; + uniform vec3 light_dir; + out vec3 v_color; + void main() { + vec3 n = normalize(mat3(model) * normal); + float diff = max(dot(n, -light_dir), 0.0); + vec3 base = vec3(0.6, 0.6, 0.8); + v_color = base * diff + 0.1; + gl_Position = projection * view * model * vec4(position, 1.0); + }"#; + + const FRAG: &str = r#" + #version 330 core + in vec3 v_color; + out vec4 color; + void main() { color = vec4(v_color, 1.0); }"#; + + let program = Program::from_source(&display, VERT, FRAG, None)?; + + let params = glium::DrawParameters { + depth: glium::Depth { + test: glium::draw_parameters::DepthTest::IfLess, + write: true, + .. Default::default() + }, + .. Default::default() + }; + + Ok(Self { display, program, meshes: Vec::new(), params }) + } +} + +impl Renderer for GliumRenderer { + fn render(&mut self, world: &World) { + let mut frame = self.display.draw(); + frame.clear_color_and_depth((0.1, 0.1, 0.15, 1.0), 1.0); + + // Expect exactly one active camera in the world. + let cam = match world.query::<&Camera>().iter().next() { + Some((_, cam)) => *cam, + None => { + eprintln!("[renderer] No camera component found – skipping frame"); + return; + } + }; + + let light_dir: Vec3 = Vec3::new(-1.0, -1.0, -1.0).normalize(); + + for (_, (tr, mh)) in world.query::<(&Transform, &MeshHandle)>().iter() { + let mesh = &self.meshes[mh.0]; + let uniforms = uniform! { + model: tr.matrix().to_cols_array_2d(), + view: cam.view().to_cols_array_2d(), + projection: cam.projection().to_cols_array_2d(), + light_dir: [light_dir.x, light_dir.y, light_dir.z], + }; + + frame.draw(&mesh.vbuf, &mesh.ibuf, &self.program, &uniforms, &self.params) + .unwrap(); + } + + frame.finish().unwrap(); + } +}