Implement a basic engine with separate modules

This commit is contained in:
reo 2025-06-29 23:17:53 +03:00
parent 0135974d08
commit 95070f854c
20 changed files with 1253 additions and 117 deletions

130
Cargo.lock generated
View file

@ -173,6 +173,12 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -224,6 +230,12 @@ version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "byteorder-lite" name = "byteorder-lite"
version = "0.1.0" version = "0.1.0"
@ -571,8 +583,12 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
name = "fps" name = "fps"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"glam",
"glium", "glium",
"gltf",
"glutin", "glutin",
"hecs",
"image", "image",
] ]
@ -636,6 +652,12 @@ dependencies = [
"xml-rs", "xml-rs",
] ]
[[package]]
name = "glam"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
[[package]] [[package]]
name = "glium" name = "glium"
version = "0.36.0" version = "0.36.0"
@ -653,6 +675,45 @@ dependencies = [
"winit", "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]] [[package]]
name = "glutin" name = "glutin"
version = "0.32.3" version = "0.32.3"
@ -729,6 +790,15 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.4" version = "0.15.4"
@ -741,6 +811,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@ -793,9 +873,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [ dependencies = [
"equivalent", "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]] [[package]]
name = "interpolate_name" name = "interpolate_name"
version = "0.2.4" version = "0.2.4"
@ -816,6 +902,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.21.1" version = "0.21.1"
@ -870,6 +962,12 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lebe" name = "lebe"
version = "0.5.2" version = "0.5.2"
@ -1718,6 +1816,12 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1766,6 +1870,18 @@ dependencies = [
"syn", "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]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.9" version = "0.6.9"
@ -1842,6 +1958,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "strict-num" name = "strict-num"
version = "0.1.1" version = "0.1.1"
@ -2002,6 +2124,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "v_frame" name = "v_frame"
version = "0.3.9" version = "0.3.9"

View file

@ -4,6 +4,15 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
glium = "0.36.0" anyhow = "1.0.98"
glutin = "0.32.3" 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 gliums internal one)
glutin = { version = "0.32.3", default-features = false }
hecs = "0.10.5"
image = "0.25.6" image = "0.25.6"

BIN
resources/cube.bin Normal file

Binary file not shown.

121
resources/cube.gltf Normal file
View file

@ -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"
}
]
}

33
resources/cube.obj Normal file
View file

@ -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

BIN
resources/monkey-smooth.bin Normal file

Binary file not shown.

View file

@ -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"
}
]
}

BIN
resources/monkey.bin Normal file

Binary file not shown.

104
resources/monkey.gltf Normal file
View file

@ -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"
}
]
}

Binary file not shown.

View file

@ -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"
}
]
}

BIN
resources/uvsphere.bin Normal file

Binary file not shown.

104
resources/uvsphere.gltf Normal file
View file

@ -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"
}
]
}

BIN
resources/uvsphere2.bin Normal file

Binary file not shown.

190
resources/uvsphere2.gltf Normal file
View file

@ -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"
}
]
}

24
src/camera.rs Normal file
View file

@ -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()
}
}

32
src/ecs.rs Normal file
View file

@ -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;
}
}

View file

@ -1,127 +1,86 @@
#[macro_use] mod camera;
extern crate glium; mod ecs;
use glium::Surface; mod model;
mod teapot; 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() let event_loop = glium::winit::event_loop::EventLoop::builder()
.build() .build()
.expect("event loop building"); .expect("create event-loop");
let (window, display) = glium::backend::glutin::SimpleWindowBuilder::new()
.with_title("Glium tutorial #3") let (window, display) = SimpleWindowBuilder::new()
.with_title("fps")
.with_inner_size(1280, 720)
.build(&event_loop); .build(&event_loop);
let mut world = hecs::World::new();
let positions = glium::VertexBuffer::new(&display, &teapot::VERTICES).unwrap(); let mesh = model::load_gltf("resources/monkey-smooth.gltf", &display)?;
let normals = glium::VertexBuffer::new(&display, &teapot::NORMALS).unwrap(); // let mesh = model::cube(&display)?;
let indices = glium::IndexBuffer::new(&display, glium::index::PrimitiveType::TrianglesList, let mut renderer = GliumRenderer::new(display)?;
&teapot::INDICES).unwrap(); let mesh_id = renderer.meshes.len();
renderer.meshes.push(mesh);
let vertex_shader_src = r#" world.spawn((
#version 140 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 {
out vec3 v_normal; eye: Vec3::new(3.0, 2.0, 3.0),
center: Vec3::ZERO,
uniform mat4 matrix; up: Vec3::Y,
uniform mat4 perspective; fovy: 45_f32.to_radians(),
aspect: w as f32 / h as f32,
void main() { znear: 0.1,
v_normal = transpose(inverse(mat3(matrix))) * normal; zfar: 100.0,
gl_Position = perspective * matrix * vec4(position, 1.0); },));
} }
"#;
let fragment_shader_src = r#"
#version 140
in vec3 v_normal; event_loop
out vec4 color; .run(move |event, el| {
uniform vec3 u_light; use glium::winit::event::{Event, WindowEvent};
void main() { match event {
float brightness = dot(normalize(v_normal), normalize(u_light)); Event::WindowEvent { event, .. } => match event {
vec3 dark_color = vec3(0.6, 0.0, 0.0); WindowEvent::CloseRequested => el.exit(),
vec3 regular_color = vec3(1.0, 0.0, 0.0); WindowEvent::Resized(sz) => {
color = vec4(mix(dark_color, regular_color, brightness), 1.0); ecs::set_camera_aspect(&mut world, sz.width as f32 / sz.height as f32);
} }
"#; WindowEvent::RedrawRequested => {
let program = glium::Program::from_source(&display, vertex_shader_src, fragment_shader_src, None).unwrap(); renderer.render(&world);
}
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();
}, },
// We now need to render everyting in response to a RedrawRequested event due to the animation Event::AboutToWait => {
glium::winit::event::WindowEvent::RedrawRequested => { // -- update logic --
let mut target = display.draw(); let now = Instant::now();
target.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0); static mut LAST: Option<Instant> = None;
t += 0.01; let dt = unsafe { // FIXME
let x = t.sin() * 0.5; let last = LAST.replace(now).unwrap_or(now);
let y = t.cos() * 0.5; (now - last).as_secs_f32()
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],
]
}; };
rotation_system(&mut world, dt);
let uniforms = uniform! { // ask for next frame
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,
&params).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(); window.request_redraw();
}, }
_ => (), _ => {}
} }
}) })
.unwrap(); .map_err(Into::into)
} }

124
src/model.rs Normal file
View file

@ -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<Vertex>,
pub ibuf: IndexBuffer<u32>,
}
/// Load a glTF 2.0 file from disk and upload the first primitive to the GPU.
pub fn load_gltf<P, F>(path: P, facade: &F) -> Result<Mesh>
where
P: AsRef<Path> + 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<u32> = 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<Vertex> = 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<F>(facade: &F) -> Result<Mesh>
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<u32> = 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 })
}

95
src/render.rs Normal file
View file

@ -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<WindowSurface>,
program: Program,
pub meshes: Vec<Mesh>,
params: glium::DrawParameters<'static>,
}
impl GliumRenderer {
/// Create a new OpenGL renderer consuming the provided `display`.
pub fn new(display: glium::Display<WindowSurface>) -> anyhow::Result<Self> {
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();
}
}