Compare commits

..

15 Commits

Author SHA1 Message Date
SeanOMik e6da582ee3
Merge branch 'feature/gltf-loading'
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-10-21 22:30:21 -04:00
SeanOMik 970e3ac8a3
Add woodpecker ci pipeline
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-10-21 22:28:08 -04:00
SeanOMik 7ae59c0415
Loading textures from gltf blob and gltf.bin's, fix loading multiple meshses in a single model 2023-10-21 22:19:34 -04:00
SeanOMik fd9f4bee2a
Implement loading material textures from gltf and rendering them 2023-10-17 22:04:25 -04:00
SeanOMik 02a0eea7b3
Don't force loaded model indicies to U32 2023-10-08 00:03:53 -04:00
SeanOMik 8f7288339d
Add default texture which fixes render error, fix cube rendering 2023-10-05 11:42:24 -04:00
SeanOMik a5b145c9b3
Create a testbed example to make developing the engine easier 2023-09-29 14:57:22 -04:00
SeanOMik fdf1c4d338
Start implementing the new Model and Mesh types with the renderer 2023-09-29 14:46:08 -04:00
SeanOMik 9d6d51af83
Load materials from gltf 2023-09-29 14:20:28 -04:00
SeanOMik 792596078d
Create a material type for loading materials 2023-09-29 13:00:33 -04:00
SeanOMik dabc051b58
Add ModelComponent, add model loader to resource manager's default loaders 2023-09-26 17:14:38 -04:00
SeanOMik 64817b6142
Add MeshVertexAttribute instead of directly storing positions 2023-09-22 12:42:36 -04:00
SeanOMik e76ca1ec50
Write a very experimental gltf loader 2023-09-21 23:11:09 -04:00
SeanOMik dddf6123c4
Continue working on gltf loader 2023-09-21 17:27:21 -04:00
SeanOMik 38e7b543c0
Start working on gltf 2023-09-21 14:22:46 -04:00
47 changed files with 5590 additions and 318 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
/target
target

10
.vscode/launch.json vendored
View File

@ -7,20 +7,20 @@
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'lyra-engine'",
"name": "Debug lyra testbed",
"cargo": {
"args": [
"build",
"--bin=lyra-engine",
"--package=lyra-engine"
"--manifest-path", "${workspaceFolder}/examples/testbed/Cargo.toml"
//"--bin=testbed",
],
"filter": {
"name": "lyra-engine",
"name": "testbed",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder}/examples/testbed"
},
{
"type": "lldb",

6
.woodpecker.yml Normal file
View File

@ -0,0 +1,6 @@
steps:
build:
image: rust:1.73
commands:
- cargo build --release
- cargo test

156
Cargo.lock generated
View File

@ -297,6 +297,18 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
[[package]]
name = "bit-set"
version = "0.5.3"
@ -890,6 +902,44 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gltf"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2dcfb6dd7a66f9eb3d181a29dcfb22d146b0bcdc2e1ed1713cbf03939a88ea"
dependencies = [
"base64 0.13.1",
"byteorder",
"gltf-json",
"image",
"lazy_static",
"urlencoding",
]
[[package]]
name = "gltf-derive"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cbcea5dd47e7ad4e9ee6f040384fcd7204bbf671aa4f9e7ca7dfc9bfa1de20"
dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.26",
]
[[package]]
name = "gltf-json"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5b810806b78dde4b71a95cc0e6fdcab34c4c617da3574df166f9987be97d03"
dependencies = [
"gltf-derive",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "gpu-alloc"
version = "0.5.4"
@ -1041,6 +1091,18 @@ dependencies = [
"hashbrown 0.14.0",
]
[[package]]
name = "infer"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
[[package]]
name = "inflections"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
[[package]]
name = "instant"
version = "0.1.12"
@ -1232,6 +1294,7 @@ dependencies = [
"tracing-appender",
"tracing-log",
"tracing-subscriber",
"uuid",
"wgpu",
"winit",
]
@ -1241,8 +1304,16 @@ name = "lyra-resource"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.21.4",
"edict",
"glam",
"gltf",
"image",
"infer",
"mime",
"percent-encoding",
"thiserror",
"tracing",
"uuid",
]
@ -1311,6 +1382,12 @@ dependencies = [
"objc",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1476,7 +1553,7 @@ checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1"
dependencies = [
"num-integer",
"num-traits",
"rand",
"rand 0.4.6",
"rustc-serialize",
]
@ -1791,6 +1868,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-easy"
version = "0.3.0"
@ -1858,6 +1941,27 @@ dependencies = [
"winapi",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.3.1"
@ -1873,6 +1977,15 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "range-alloc"
version = "0.1.3"
@ -1963,6 +2076,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "scoped-tls"
version = "1.0.1"
@ -1994,6 +2113,28 @@ version = "1.0.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31"
[[package]]
name = "serde_derive"
version = "1.0.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
]
[[package]]
name = "serde_json"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
@ -2372,12 +2513,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "uuid"
version = "1.4.1"
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "uuid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
dependencies = [
"getrandom",
"rand 0.8.5",
]
[[package]]

View File

@ -36,3 +36,4 @@ aligned-vec = "0.5.0"
tracing-appender = "0.2.2"
stopwatch = "0.0.7"
petgraph = "0.6.4"
uuid = { version = "1.5.0", features = ["v4", "fast-rng"] }

3121
examples/testbed/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
[package]
name = "testbed"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lyra-engine = { path = "../../", version = "0.0.1" }
anyhow = "1.0.75"
async-std = "1.12.0"
tracing = "0.1.37"
fps_counter = "2.0.0"
[workspace]

Binary file not shown.

View File

@ -0,0 +1,121 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.5",
"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,
"TEXCOORD_0":1,
"NORMAL":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":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"buffers":[
{
"byteLength":840,
"uri":"data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAQAOABQAAQAUAAcACgAGABIACgASABYAFwATAAwAFwAMABAADwADAAkADwAJABUABQACAAgABQAIAAsAEQANAAAAEQAAAAQA"
}
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

View File

@ -0,0 +1,137 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.6",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Cube"
}
],
"materials":[
{
"doubleSided":true,
"name":"Material",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"Cube",
"primitives":[
{
"attributes":{
"POSITION":0,
"TEXCOORD_0":1,
"NORMAL":2
},
"indices":3,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"uvgrid",
"uri":"uvgrid.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":24,
"max":[
1,
1,
1
],
"min":[
-1,
-1,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":24,
"type":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":840,
"uri":"texture-sep.bin"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,214 @@
use std::ops::DerefMut;
use lyra_engine::{math, ecs::{World, components::{mesh::MeshComponent, transform::TransformComponent, camera::CameraComponent, model::ModelComponent}, atomicell::{Ref, RefMut}, EventQueue}, render::{mesh::Mesh, material::Material, vertex::Vertex, window::{CursorGrabMode, WindowOptions}}, math::Transform, input::{KeyCode, InputButtons, MouseMotion}, game::Game, change_tracker::Ct};
use lyra_engine::assets::{ResourceManager, Texture, Model};
use tracing::debug;
/* pub const VERTICES: &[Vertex] = &[
Vertex { position: [-0.0868241, 0.49240386, 0.0], tex_coords: [0.4131759, 0.00759614], }, // A
Vertex { position: [-0.49513406, 0.06958647, 0.0], tex_coords: [0.0048659444, 0.43041354], }, // B
Vertex { position: [-0.21918549, -0.44939706, 0.0], tex_coords: [0.28081453, 0.949397], }, // C
Vertex { position: [0.35966998, -0.3473291, 0.0], tex_coords: [0.85967, 0.84732914], }, // D
Vertex { position: [0.44147372, 0.2347359, 0.0], tex_coords: [0.9414737, 0.2652641], }, // E
]; */
pub const INDICES: &[u16] = &[
0, 1, 4,
1, 2, 4,
2, 3, 4,
];
#[derive(Debug, Default)]
struct Point2d {
x: i32,
y: i32,
}
impl std::fmt::Display for Point2d {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(x={}, y={})", self.x, self.y)
}
}
impl Point2d {
pub fn new(x: i32, y: i32) -> Self {
Self {
x,
y,
}
}
}
#[derive(Debug, Default)]
struct Point3d {
x: i32,
y: i32,
z: i32,
}
impl std::fmt::Display for Point3d {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(x={}, y={}, z={})", self.x, self.y, self.z)
}
}
impl Point3d {
pub fn new(x: i32, y: i32, z: i32) -> Self {
Self {
x,
y,
z,
}
}
}
#[async_std::main]
async fn main() {
let setup_sys = |world: &mut World| -> anyhow::Result<()> {
/* {
let mut window_options = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
window_options.cursor_grab = CursorGrabMode::Confined;
window_options.cursor_visible = false;
} */
let mut resman = world.get_resource_mut::<ResourceManager>().unwrap();
let diffuse_texture = resman.request::<Texture>("assets/happy-tree.png").unwrap();
let antique_camera_model = resman.request::<Model>("assets/AntiqueCamera.glb").unwrap();
let cube_model = resman.request::<Model>("assets/texture-sep/texture-sep.gltf").unwrap();
drop(resman);
/* world.spawn((
ModelComponent(cube_model.clone()),
TransformComponent::from(Transform::from_xyz(3.0, 0.5, -2.2)),
)); */
world.spawn((
ModelComponent(antique_camera_model),
TransformComponent::from(Transform::from_xyz(0.0, -5.0, -10.0)),
));
let mut camera = CameraComponent::new_3d();
camera.transform.translation += math::Vec3::new(0.0, 0.0, 7.5);
//camera.transform.rotate_y(Angle::Degrees(-25.0));
camera.transform.rotate_z(math::Angle::Degrees(-90.0));
world.spawn((camera,));
Ok(())
};
let fps_system = |world: &mut World| -> anyhow::Result<()> {
let mut counter: RefMut<fps_counter::FPSCounter> = world.get_resource_mut().unwrap();
let fps = counter.tick();
debug!("FPS: {fps}");
Ok(())
};
let fps_plugin = move |game: &mut Game| {
let world = game.world();
world.insert_resource(fps_counter::FPSCounter::new());
game.with_system("fps", fps_system, &["input"]);
};
let jiggle_system = |world: &mut World| -> anyhow::Result<()> {
let keys = world.get_resource::<InputButtons<KeyCode>>();
if keys.is_none() {
return Ok(());
}
let keys = keys.unwrap();
let speed = 0.01;
let rot_speed = 1.0;
let mut dir_x = 0.0;
let mut dir_y = 0.0;
let mut dir_z = 0.0;
let mut rot_x = 0.0;
let mut rot_y = 0.0;
if keys.is_pressed(KeyCode::A) {
dir_x += speed;
}
if keys.is_pressed(KeyCode::D) {
dir_x -= speed;
}
if keys.is_pressed(KeyCode::S) {
dir_y += speed;
}
if keys.is_pressed(KeyCode::W) {
dir_y -= speed;
}
if keys.is_pressed(KeyCode::E) {
dir_z += speed;
}
if keys.is_pressed(KeyCode::Q) {
dir_z -= speed;
}
if keys.is_pressed(KeyCode::Left) {
rot_y -= rot_speed;
}
if keys.is_pressed(KeyCode::Right) {
rot_y += rot_speed;
}
if keys.is_pressed(KeyCode::Up) {
rot_x -= rot_speed;
}
if keys.is_pressed(KeyCode::Down) {
rot_x += rot_speed;
}
drop(keys);
if dir_x == 0.0 && dir_y == 0.0 && dir_z == 0.0 && rot_x == 0.0 && rot_y == 0.0 {
return Ok(());
}
//debug!("moving by ({}, {})", dir_x, dir_y);
for transform in world.query_mut::<(&mut TransformComponent,)>().iter_mut() {
let t = &mut transform.transform;
//debug!("Translation: {}", t.translation);
//debug!("Rotation: {}", t.rotation);
/* t.translation += glam::Vec3::new(0.0, 0.001, 0.0);
t.translation.x *= -1.0; */
t.translation.x += dir_x;
t.translation.y += dir_y;
t.translation.z += dir_z;
t.rotate_x(math::Angle::Degrees(rot_x));
t.rotate_y(math::Angle::Degrees(rot_y));
}
let events = world.get_resource_mut::<EventQueue>().unwrap();
if let Some(mm) = events.read_events::<MouseMotion>() {
debug!("Mouse motion: {:?}", mm);
}
Ok(())
};
let jiggle_plugin = move |game: &mut Game| {
game.with_system("jiggle", jiggle_system, &["input"]);
};
Game::initialize().await
.with_plugin(lyra_engine::DefaultPlugins)
.with_startup_system(setup_sys)
//.with_plugin(fps_plugin)
.with_plugin(jiggle_plugin)
.run().await;
}

View File

@ -7,6 +7,15 @@ edition = "2021"
[dependencies]
anyhow = "1.0.75"
base64 = "0.21.4"
edict = "0.5.0"
glam = "0.24.1"
gltf = { version = "1.3.0", features = ["KHR_materials_pbrSpecularGlossiness"] }
image = "0.24.7"
# not using custom matcher, or file type from file path
infer = { version = "0.15.0", default-features = false }
mime = "0.3.17"
percent-encoding = "2.3.0"
thiserror = "1.0.48"
tracing = "0.1.37"
uuid = { version = "1.4.1", features = ["v4"] }

View File

@ -9,3 +9,11 @@ pub use texture::*;
pub mod loader;
pub use loader::*;
pub mod model;
pub use model::*;
pub mod material;
pub use material::*;
pub(crate) mod util;

View File

@ -0,0 +1,115 @@
use std::{fs::File, sync::Arc, io::Read};
use image::ImageError;
use crate::{resource_manager::ResourceStorage, texture::Texture, resource::Resource, ResourceManager};
use super::{LoaderError, ResourceLoader};
impl From<ImageError> for LoaderError {
fn from(value: ImageError) -> Self {
LoaderError::DecodingError(value.into())
}
}
/// A struct that implements the `ResourceLoader` trait used for loading textures.
#[derive(Default)]
pub struct ImageLoader;
impl ResourceLoader for ImageLoader {
fn extensions(&self) -> &[&str] {
&[
// the extensions of these are the names of the formats
"bmp", "dds", "gif", "ico", "jpeg", "jpg", "png", "qoi", "tga", "tiff", "webp",
// farbfeld
"ff",
// pnm
"pnm", "pbm", "pgm", "ppm",
]
}
fn mime_types(&self) -> &[&str] {
&[
"image/bmp", "image/vnd.ms-dds", "image/gif", "image/x-icon", "image/jpeg",
"image/png", "image/qoi", "image/tga", "image/tiff", "image/webp",
// no known mime for farbfeld
// pnm, pbm, pgm, ppm
"image/x-portable-anymap", "image/x-portable-bitmap", "image/x-portable-graymap", "image/x-portable-pixmap",
]
}
fn load(&self, _resource_manager: &mut ResourceManager, path: &str) -> Result<Arc<dyn ResourceStorage>, LoaderError> {
// check if the file is supported by this loader
if !self.does_support_file(path) {
return Err(LoaderError::UnsupportedExtension(path.to_string()));
}
// read file bytes
let mut file = File::open(path)?;
let mut buf = vec![];
file.read_to_end(&mut buf)?;
// load the image and construct Resource<Texture>
let image = image::load_from_memory(&buf)
.map_err(|e| match e {
ImageError::IoError(e) => LoaderError::IoError(e),
_ => LoaderError::DecodingError(e.into()),
})?;
let texture = Texture {
image,
};
let res = Resource::with_data(path, texture);
Ok(Arc::new(res))
}
fn load_bytes(&self, _resource_manager: &mut ResourceManager, bytes: Vec<u8>, offset: usize, length: usize) -> Result<Arc<dyn ResourceStorage>, LoaderError> {
let image = image::load_from_memory(&bytes[offset..(length-offset)])
.map_err(|e| match e {
ImageError::IoError(e) => LoaderError::IoError(e),
_ => LoaderError::DecodingError(e.into()),
})?;
let texture = Texture {
image,
};
let res = Resource::with_data(&uuid::Uuid::new_v4().to_string(), texture);
Ok(Arc::new(res))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_image(path: &str) -> String {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
format!("{manifest}/test_files/img/{path}")
}
#[test]
fn check_unsupport() {
let loader = ImageLoader::default();
assert_eq!(loader.does_support_file("test.gltf"), false);
}
/// Tests loading an image
#[test]
fn image_load() {
let mut manager = ResourceManager::new();
let loader = ImageLoader::default();
loader.load(&mut manager, &get_image("squiggles.png")).unwrap();
}
#[test]
fn image_load_unsupported() {
let mut manager = ResourceManager::new();
let loader = ImageLoader::default();
assert!(loader.load(&mut manager, &get_image("squiggles.gltf")).is_err());
}
}

View File

@ -1,10 +1,11 @@
pub mod texture;
pub mod image;
pub mod model;
use std::{io, sync::Arc, fs::File};
use std::{io, sync::Arc, path::Path, ffi::OsStr};
use thiserror::Error;
use crate::resource_manager::ResourceStorage;
use crate::{resource_manager::ResourceStorage, ResourceManager};
#[derive(Error, Debug)]
pub enum LoaderError {
@ -15,7 +16,7 @@ pub enum LoaderError {
UnsupportedExtension(String),
#[error("IOError: '{0}'")]
IOError(io::Error),
IoError(io::Error),
// From is implemented for this field in each loader module
#[error("Decoding error: '{0}'")]
@ -24,12 +25,50 @@ pub enum LoaderError {
impl From<io::Error> for LoaderError {
fn from(value: io::Error) -> Self {
LoaderError::IOError(value)
LoaderError::IoError(value)
}
}
pub trait ResourceLoader: Send + Sync {
/// Returns the extensions that this loader supports.
fn extensions(&self) -> &[&str];
fn does_support_file(&self, path: &str) -> bool;
fn load(&self, path: &str) -> Result<Arc<dyn ResourceStorage>, LoaderError>;
/// Returns the mime types that this loader supports.
fn mime_types(&self) -> &[&str];
/// Returns true if this loader supports the file.
fn does_support_file(&self, path: &str) -> bool {
match Path::new(path).extension().and_then(OsStr::to_str) {
Some(ext) => {
self.extensions().contains(&ext)
},
_ => false,
}
}
/// Returns true if this loader supports the mime type.
fn does_support_mime(&self, mime: &str) -> bool {
self.mime_types().contains(&mime)
}
/// Load a resource from a path.
fn load(&self, resource_manager: &mut ResourceManager, path: &str) -> Result<Arc<dyn ResourceStorage>, LoaderError>;
/// Load a resource from bytes.
fn load_bytes(&self, resource_manager: &mut ResourceManager, bytes: Vec<u8>, offset: usize, length: usize) -> Result<Arc<dyn ResourceStorage>, LoaderError>;
}
#[cfg(test)]
mod tests {
use super::{*, image::ImageLoader};
/// Ensure that `does_support_file` works
#[test]
fn check_support() {
let loader = ImageLoader::default();
let extensions = loader.extensions();
let fake_paths: Vec<String> = extensions.iter().map(|e| format!("a.{}", e)).collect();
for path in fake_paths.iter() {
assert!(loader.does_support_file(&path));
}
}
}

View File

@ -0,0 +1,230 @@
use std::{sync::Arc, path::{Path, PathBuf}};
use base64::Engine;
use thiserror::Error;
use crate::{ResourceLoader, LoaderError, Mesh, Model, MeshVertexAttribute, VertexAttributeData, Resource, Material, MeshIndices, ResourceManager, util};
use tracing::debug;
impl From<gltf::Error> for LoaderError {
fn from(value: gltf::Error) -> Self {
LoaderError::DecodingError(value.into())
}
}
#[derive(Error, Debug)]
enum ModelLoaderError {
#[error("The model ({0}) is missing the BIN section in the gltf file")]
MissingBin(String),
#[error("There was an error with decoding a uri defined in the model: '{0}'")]
UriDecodingError(util::UriReadError),
}
impl From<ModelLoaderError> for LoaderError {
fn from(value: ModelLoaderError) -> Self {
LoaderError::DecodingError(value.into())
}
}
pub(crate) struct GltfLoadContext<'a> {
pub resource_manager: &'a mut ResourceManager,
pub gltf: &'a gltf::Gltf,
/// Path to the gltf
pub gltf_path: &'a str,
/// The path to the directory that the gltf is contained in.
pub gltf_parent_path: &'a str,
/// List of buffers in the gltf
pub buffers: &'a Vec<Vec<u8>>,
}
#[derive(Default)]
pub struct ModelLoader;
impl ModelLoader {
/* fn parse_uri(containing_path: &str, uri: &str) -> Option<Vec<u8>> {
let uri = uri.strip_prefix("data")?;
let (mime, data) = uri.split_once(",")?;
let (_mime, is_base64) = match mime.strip_suffix(";base64") {
Some(mime) => (mime, true),
None => (mime, false),
};
if is_base64 {
Some(base64::engine::general_purpose::STANDARD.decode(data).unwrap())
} else {
let full_path = format!("{containing_path}/{data}");
let buf = std::fs::read(&full_path).unwrap();
Some(buf)
}
} */
fn process_node(&self, buffers: &Vec<Vec<u8>>, materials: &Vec<Material>, node: gltf::Node<'_>) -> Vec<Mesh> {
let mut meshes = vec![];
if let Some(mesh) = node.mesh() {
for prim in mesh.primitives() {
let reader = prim.reader(|buf| Some(buffers[buf.index()].as_slice()));
let mut new_mesh = Mesh::default();
// read the positions
if let Some(pos) = reader.read_positions() {
if prim.mode() != gltf::mesh::Mode::Triangles {
todo!("Load position primitives that aren't triangles"); // TODO
}
let pos: Vec<glam::Vec3> = pos.map(|t| t.into()).collect();
new_mesh.add_attribute(MeshVertexAttribute::Position, VertexAttributeData::Vec3(pos));
}
// read the normals
if let Some(norms) = reader.read_normals() {
let norms: Vec<glam::Vec3> = norms.map(|t| t.into()).collect();
new_mesh.add_attribute(MeshVertexAttribute::Normals, VertexAttributeData::Vec3(norms));
}
// read the tangents
if let Some(tangents) = reader.read_tangents() {
let tangents: Vec<glam::Vec4> = tangents.map(|t| t.into()).collect();
new_mesh.add_attribute(MeshVertexAttribute::Tangents, VertexAttributeData::Vec4(tangents));
}
// read tex coords
if let Some(tex_coords) = reader.read_tex_coords(0) {
let tex_coords: Vec<glam::Vec2> = tex_coords.into_f32().map(|t| t.into()).collect();
new_mesh.add_attribute(MeshVertexAttribute::TexCoords, VertexAttributeData::Vec2(tex_coords));
}
// read the indices
if let Some(indices) = reader.read_indices() {
let indices: MeshIndices = match indices {
// wpgu doesn't support u8 indices, so those must be converted to u16
gltf::mesh::util::ReadIndices::U8(i) => MeshIndices::U16(i.map(|i| i as u16).collect()),
gltf::mesh::util::ReadIndices::U16(i) => MeshIndices::U16(i.collect()),
gltf::mesh::util::ReadIndices::U32(i) => MeshIndices::U32(i.collect()),
};
new_mesh.indices = Some(indices);
}
let mat = materials.get(prim.material().index().unwrap()).unwrap();
new_mesh.set_material(mat.clone());
//prim.material().
meshes.push(new_mesh);
}
}
for child in node.children() {
let mut child_meshes = self.process_node(buffers, materials, child);
meshes.append(&mut child_meshes);
}
meshes
}
}
impl ResourceLoader for ModelLoader {
fn extensions(&self) -> &[&str] {
&[
"gltf", "glb"
]
}
fn mime_types(&self) -> &[&str] {
&[]
}
fn load(&self, resource_manager: &mut ResourceManager, path: &str) -> Result<std::sync::Arc<dyn crate::ResourceStorage>, crate::LoaderError> {
// check if the file is supported by this loader
if !self.does_support_file(path) {
return Err(LoaderError::UnsupportedExtension(path.to_string()));
}
let mut parent_path = PathBuf::from(path);
parent_path.pop();
let parent_path = parent_path.display().to_string();
/* let (document, buffers, images) = gltf::import(&path)?;
let buffers: Vec<Vec<u8>> = buffers.into_iter().map(|b| b.0).collect();
let scene = document.scenes().next().unwrap();
let materials: Vec<Material> = document.materials()
.map(|mat| Material::from_gltf(resource_manager, &parent_path, mat)).collect();
let meshes: Vec<Mesh> = scene.nodes()
.map(|node| self.process_node(&buffers, &materials, node))
.flatten().collect(); */
let gltf = gltf::Gltf::open(path)?;
let mut use_bin = false;
let buffers: Vec<Vec<u8>> = gltf.buffers().map(|b| match b.source() {
gltf::buffer::Source::Bin => {
use_bin = true;
gltf.blob.as_deref().map(|v| v.to_vec())
.ok_or(ModelLoaderError::MissingBin(path.to_string()))
},
gltf::buffer::Source::Uri(uri) => util::gltf_read_buffer_uri(&parent_path, uri)
.map_err(|e| ModelLoaderError::UriDecodingError(e)),
}).flatten().collect();
// TODO: Read in multiple scenes
let scene = gltf.scenes().next().unwrap();
let mut context = GltfLoadContext {
resource_manager,
gltf: &gltf,
gltf_path: path,
gltf_parent_path: &parent_path,
buffers: &buffers,
};
let materials: Vec<Material> = gltf.materials()
.map(|mat| Material::from_gltf(&mut context, mat)).collect();
let meshes: Vec<Mesh> = scene.nodes()
.map(|node| self.process_node(&buffers, &materials, node))
.flatten().collect();
debug!("Loaded {} meshes, and {} materials from '{}'", meshes.len(), materials.len(), path);
Ok(Arc::new(Resource::with_data(path, Model::new(meshes))))
}
fn load_bytes(&self, resource_manager: &mut ResourceManager, bytes: Vec<u8>, offset: usize, length: usize) -> Result<Arc<dyn crate::ResourceStorage>, LoaderError> {
todo!()
}
}
#[cfg(test)]
mod tests {
use crate::ResourceLoader;
use super::*;
fn test_file_path(path: &str) -> String {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
format!("{manifest}/test_files/gltf/{path}")
}
#[test]
fn test_loading() {
let path = test_file_path("texture-embedded.gltf");
let mut manager = ResourceManager::new();
let loader = ModelLoader::default();
let model = loader.load(&mut manager, &path).unwrap();
let model = Arc::downcast::<Resource<Model>>(model.as_arc_any()).unwrap();
let model = model.data.as_ref().unwrap();
assert_eq!(model.meshes.len(), 1); // There should only be 1 mesh
let mesh = &model.meshes[0];
assert!(mesh.position().unwrap().len() > 0);
assert!(mesh.normals().unwrap().len() > 0);
assert!(mesh.tex_coords().unwrap().len() > 0);
assert!(mesh.indices.clone().unwrap().len() > 0);
assert!(mesh.material().base_color_texture.is_some());
let _mesh_mat = mesh.material(); // inner panic if material was not loaded
}
}

View File

@ -1,107 +0,0 @@
use std::{fs::File, sync::Arc, path::Path, ffi::OsStr, io::Read};
use image::ImageError;
use crate::{resource_manager::ResourceStorage, texture::Texture, resource::Resource};
use super::{LoaderError, ResourceLoader};
impl From<ImageError> for LoaderError {
fn from(value: ImageError) -> Self {
LoaderError::DecodingError(anyhow::Error::from(value))
}
}
/// A struct that implements the `ResourceLoader` trait used for loading textures.
#[derive(Default)]
pub struct TextureLoader;
impl ResourceLoader for TextureLoader {
fn extensions(&self) -> &[&str] {
&[
// the extensions of these are the names of the formats
"bmp", "dds", "gif", "ico", "jpeg", "jpg", "png", "qoi", "tga", "tiff", "webp",
// farbfeld
"ff",
// pnm
"pnm", "pbm", "pgm", "ppm", "pam",
]
}
fn does_support_file(&self, path: &str) -> bool {
match Path::new(path).extension().and_then(OsStr::to_str) {
Some(ext) => {
self.extensions().contains(&ext)
},
_ => false,
}
}
fn load(&self, path: &str) -> Result<Arc<dyn ResourceStorage>, LoaderError> {
// check if the file is supported by this loader
if !self.does_support_file(path) {
return Err(LoaderError::UnsupportedExtension(path.to_string()));
}
// read file bytes
let mut file = File::open(path)?;
let mut buf = vec![];
file.read_to_end(&mut buf)?;
// load the image and construct Resource<Texture>
let image = image::load_from_memory(&buf)
.map_err(|e| match e {
ImageError::IoError(e) => LoaderError::IOError(e),
_ => LoaderError::DecodingError(e.into()),
})?;
let texture = Texture {
image,
};
let res = Resource::with_data(path, texture);
Ok(Arc::new(res))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_image(path: &str) -> String {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
format!("{manifest}/test_files/img/{path}")
}
/// Ensure that `does_support_file` works
#[test]
fn check_support() {
let loader = TextureLoader::default();
let extensions = loader.extensions();
let fake_paths: Vec<String> = extensions.iter().map(|e| format!("a.{}", e)).collect();
for path in fake_paths.iter() {
assert!(loader.does_support_file(&path));
}
}
#[test]
fn check_unsupport() {
let loader = TextureLoader::default();
assert_eq!(loader.does_support_file("test.gltf"), false);
}
/// Tests loading an image
#[test]
fn image_load() {
let loader = TextureLoader::default();
loader.load(&get_image("squiggles.png")).unwrap();
}
#[test]
fn image_load_unsupported() {
let loader = TextureLoader::default();
assert!(loader.load(&get_image("squiggles.gltf")).is_err());
}
}

View File

@ -0,0 +1,244 @@
use std::{fs::File, io::{BufReader, Read}, collections::hash_map::DefaultHasher, hash::{Hash, Hasher}};
use crate::{Texture, ResHandle, ResourceManager, util, loader::model::GltfLoadContext};
/// PBR metallic roughness
#[derive(Clone, Debug, Default)]
pub struct PbrRoughness {
/// The rgba base color of the PBR material
pub base_color: [f32; 4],
/// The metalness of the material
/// From 0.0 (non-metal) to 1.0 (metal)
pub metallic: f32,
/// The roughness of the material
/// From 0.0 (smooth) to 1.0 (rough)
pub roughness: f32,
// TODO: base_color_texture and metallic_roughness_texture
}
impl From<gltf::material::PbrMetallicRoughness<'_>> for PbrRoughness {
fn from(value: gltf::material::PbrMetallicRoughness) -> Self {
PbrRoughness {
base_color: value.base_color_factor(),
metallic: value.metallic_factor(),
roughness: value.roughness_factor(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct PbrGlossiness {
/// The rgba diffuse color of the material
pub diffuse_color: glam::Vec4,
// The base color texture
// pub diffuse_texture // TODO
pub specular: glam::Vec3,
/// The glossiness factor of the material.
/// From 0.0 (no glossiness) to 1.0 (full glossiness)
pub glossiness: f32,
// pub glossiness_texture // TODO
}
impl From<gltf::material::PbrSpecularGlossiness<'_>> for PbrGlossiness {
fn from(value: gltf::material::PbrSpecularGlossiness) -> Self {
PbrGlossiness {
diffuse_color: value.diffuse_factor().into(),
specular: value.specular_factor().into(),
glossiness: value.glossiness_factor()
}
}
}
/// The alpha rendering mode of a material.
/// This is essentially a re-export of gltf::material::AlphaMode
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
pub enum AlphaMode {
/// The alpha value is ignored and the rendered output is fully opaque.
#[default]
Opaque = 1,
/// The rendered output is either fully opaque or fully transparent depending on
/// the alpha value and the specified alpha cutoff value.
Mask,
/// The alpha value is used, to determine the transparency of the rendered output.
/// The alpha cutoff value is ignored.
Blend,
}
impl From<gltf::material::AlphaMode> for AlphaMode {
fn from(value: gltf::material::AlphaMode) -> Self {
match value {
gltf::material::AlphaMode::Opaque => AlphaMode::Opaque,
gltf::material::AlphaMode::Mask => AlphaMode::Mask,
gltf::material::AlphaMode::Blend => AlphaMode::Blend,
}
}
}
#[derive(Clone, Default)]
pub struct Material {
pub shader_uuid: Option<u64>,
pub name: Option<String>,
pub double_sided: bool,
//pub pbr_roughness: PbrRoughness,
/// The RGBA base color of the model. If a texture is supplied with `base_color_texture`, this value
/// will tint the texture. If a texture is not provided, this value would be the color of the Material.
pub base_color: glam::Vec4,
/// The metalness of the material
/// From 0.0 (non-metal) to 1.0 (metal)
pub metallic: f32,
/// The roughness of the material
/// From 0.0 (smooth) to 1.0 (rough)
pub roughness: f32,
/// The base color texture of the model.
pub base_color_texture: Option<ResHandle<Texture>>,
/// The metallic-roughness texture.
///
/// The metalness values are sampled from the B channel. The roughness values are sampled from
/// the G channel. These values are linear. If other channels are present (R or A), they are
/// ignored for metallic-roughness calculations.
pub metallic_roughness_texture: Option<ResHandle<Texture>>,
/// A set of parameter values that are used to define the specular-glossiness material model
/// from Physically-Based Rendering (PBR) methodology.
/// GLTF extension: [KHR_materials_pbrSpecularGlossiness](https://kcoley.github.io/glTF/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness)
pub pbr_glossiness: Option<PbrGlossiness>,
/// The optional alpha cutoff value of the material.
pub alpha_cutoff: Option<f32>,
/// The alpha rendering mode of the material. The material's alpha rendering
/// mode enumeration specifying the interpretation of the alpha value of the main
/// factor and texture.
///
/// * In `Opaque` mode (default) the alpha value is ignored
/// and the rendered output is fully opaque.
/// * In `Mask` mode, the rendered
/// output is either fully opaque or fully transparent depending on the alpha
/// value and the specified alpha cutoff value.
/// * In `Blend` mode, the alpha value is used to composite the source and
/// destination areas and the rendered output is combined with the background
/// using the normal painting operation (i.e. the Porter and Duff over
/// operator).
pub alpha_mode: AlphaMode,
//pub texture: Option<ResHandle<Texture>>,
}
impl Material {
/// Get a uri's identifier
///
/// I'm not actually sure how identifiable this would be
fn uri_ident(gltf_rel_path: &str, uri: &str) -> String {
let mut hasher = DefaultHasher::new();
uri.hash(&mut hasher);
let hash = hasher.finish();
format!("{gltf_rel_path};{hash}")
}
fn source_ident(gltf_rel_path: &str, src: &gltf::image::Source) -> Option<String> {
match src {
gltf::image::Source::View { view, mime_type } => {
let buf = view.buffer();
let src = buf.source();
match src {
gltf::buffer::Source::Bin => None,
gltf::buffer::Source::Uri(uri) => {
Some(Material::uri_ident(gltf_rel_path, uri))
}
}
},
gltf::image::Source::Uri { uri, mime_type } => {
Some(Material::uri_ident(gltf_rel_path, uri))
},
}
}
fn read_source(context: &mut GltfLoadContext, src: gltf::image::Source) -> Result<Vec<u8>, util::UriReadError> {
let gltf_rel_path = context.gltf_parent_path;
// TODO: Don't copy sources
match src {
gltf::image::Source::View { view, mime_type: _ } => {
let buf = view.buffer();
let src = buf.source();
let offset = view.offset();
let len = view.length();
match src {
gltf::buffer::Source::Bin => {
let mut b = context.gltf.blob.clone().unwrap();
b.drain(0..offset);
b.truncate(len);
Ok(b)
},
gltf::buffer::Source::Uri(uri) => {
util::gltf_read_buffer_uri(gltf_rel_path, uri)
.map(|mut buf| {
buf.drain(0..offset);
buf.truncate(len);
buf
})
}
}
},
gltf::image::Source::Uri { uri, mime_type: _ } => {
util::gltf_read_buffer_uri(gltf_rel_path, uri)
},
}
}
fn load_texture(context: &mut GltfLoadContext, texture_info: gltf::texture::Info<'_>) -> ResHandle<Texture> {
// TODO: texture_info.tex_coord()
let tex = texture_info.texture();
let img = tex.source();
let src = img.source();
let buf = Material::read_source(context, src).unwrap();
let buflen = buf.len();
let mime_type = infer::get(&buf).expect("Failed to get file type").mime_type();
context.resource_manager.load_bytes::<Texture>(&uuid::Uuid::new_v4().to_string(), mime_type,
buf, 0, buflen).unwrap()
}
/// Load the Material from a gltf::Material.
///
/// `gltf_rel_path`: The relative path of the gltf file,
/// e.g. gltf model path is "resource/models/player.gltf", the relative path would be "resource/models/"
pub(crate) fn from_gltf(context: &mut GltfLoadContext, gltf_mat: gltf::Material) -> Self {
let pbr_rough = gltf_mat.pbr_metallic_roughness();
let base_color = pbr_rough.base_color_factor().into();
let metallic = pbr_rough.metallic_factor();
let roughness = pbr_rough.roughness_factor();
let base_color_texture = pbr_rough.base_color_texture()
.map(|info| Material::load_texture(context, info));
let metallic_roughness_texture = pbr_rough.metallic_roughness_texture()
.map(|info| Material::load_texture(context, info));
Material {
name: gltf_mat.name()
.map(|s| s.to_string()),
double_sided: gltf_mat.double_sided(),
base_color,
metallic,
roughness,
pbr_glossiness: gltf_mat.pbr_specular_glossiness()
.map(|o| o.into()),
alpha_cutoff: gltf_mat.alpha_cutoff(),
alpha_mode: gltf_mat.alpha_mode().into(),
shader_uuid: None,
// TODO
base_color_texture,
metallic_roughness_texture,
}
}
}

147
lyra-resource/src/model.rs Normal file
View File

@ -0,0 +1,147 @@
use std::collections::HashMap;
use crate::Material;
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub enum MeshIndices {
//U8(Vec<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
}
impl MeshIndices {
pub fn len(&self) -> usize {
match self {
MeshIndices::U16(v) => v.len(),
MeshIndices::U32(v) => v.len(),
}
}
}
/* impl From<Vec<u8>> for MeshIndices {
fn from(value: Vec<u8>) -> Self {
MeshIndices::U8(value)
}
} */
impl From<Vec<u16>> for MeshIndices {
fn from(value: Vec<u16>) -> Self {
MeshIndices::U16(value)
}
}
impl From<Vec<u32>> for MeshIndices {
fn from(value: Vec<u32>) -> Self {
MeshIndices::U32(value)
}
}
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub enum VertexAttributeData {
Vec2(Vec<glam::Vec2>),
Vec3(Vec<glam::Vec3>),
Vec4(Vec<glam::Vec4>),
}
impl VertexAttributeData {
/// Take self as a list of Vec2's
pub fn as_vec2(&self) -> &Vec<glam::Vec2> {
match self {
VertexAttributeData::Vec2(v) => v,
_ => panic!("Attempt to get {self:?} as `Vec2`"),
}
}
pub fn as_vec3(&self) -> &Vec<glam::Vec3> {
match self {
VertexAttributeData::Vec3(v) => v,
_ => panic!("Attempt to get {self:?} as `Vec3`"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum MeshVertexAttribute {
Position,
Normals,
Tangents,
Colors, // TODO: Figure out best way to store color data
Joints, // TODO: Animation
TexCoords,
Weights, // TODO: Animation
MorphTargets, // TODO: Animation
/// Used during loading of the Mesh to process the materials taht it
MaterialRef,
Other(String),
}
#[derive(Clone, edict::Component)]
pub struct Mesh {
pub uuid: uuid::Uuid,
pub attributes: HashMap<MeshVertexAttribute, VertexAttributeData>,
pub indices: Option<MeshIndices>,
material: Option<Material>,
}
impl Default for Mesh {
fn default() -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
attributes: Default::default(),
indices: Default::default(),
material: Default::default()
}
}
}
impl Mesh {
pub fn add_attribute(&mut self, attribute: MeshVertexAttribute, data: VertexAttributeData) {
self.attributes.insert(attribute, data);
}
/// Try to get the position attribute of the Mesh
pub fn position(&self) -> Option<&Vec<glam::Vec3>> {
self.attributes.get(&MeshVertexAttribute::Position)
.map(|p| p.as_vec3())
}
pub fn add_position(&mut self, pos: Vec<glam::Vec3>) {
self.attributes.insert(MeshVertexAttribute::Position, VertexAttributeData::Vec3(pos));
}
/// Try to get the normals attribute of the Mesh
pub fn normals(&self) -> Option<&Vec<glam::Vec3>> {
self.attributes.get(&MeshVertexAttribute::Normals)
.map(|p| p.as_vec3())
}
/// Try to get the texture coordinates attribute of the Mesh
pub fn tex_coords(&self) -> Option<&Vec<glam::Vec2>> {
self.attributes.get(&MeshVertexAttribute::TexCoords)
.map(|p| p.as_vec2())
}
pub fn material(&self) -> Material {
self.material.clone().expect("This mesh is missing a material!")
}
pub fn set_material(&mut self, val: Material) {
self.material = Some(val);
}
}
#[derive(Clone, Default)]
pub struct Model {
pub meshes: Vec<Mesh>,
//pub material
}
impl Model {
pub fn new(meshes: Vec<Mesh>) -> Self {
Self {
meshes,
}
}
}

View File

@ -16,6 +16,9 @@ pub struct Resource<T: Send + Sync + 'static> {
pub state: ResourceState,
}
/// A helper type to make it easier to use resources
pub type ResHandle<T> = Arc<Resource<T>>;
impl<T: Send + Sync + 'static> Resource<T> {
/// Create the resource with data, its assumed the state is `Ready`
pub fn with_data(path: &str, data: T) -> Self {

View File

@ -2,7 +2,7 @@ use std::{sync::Arc, collections::{HashMap, hash_map::DefaultHasher}, hash::{Has
use thiserror::Error;
use crate::{resource::Resource, loader::{ResourceLoader, LoaderError, texture::TextureLoader}};
use crate::{resource::Resource, loader::{ResourceLoader, LoaderError, image::ImageLoader, model::ModelLoader}};
pub trait ResourceStorage: Send + Sync + Any + 'static {
fn as_any(&self) -> &dyn Any;
@ -31,6 +31,10 @@ pub enum RequestError {
Loader(LoaderError),
#[error("The file extension is unsupported: '{0}'")]
UnsupportedFileExtension(String),
#[error("The mimetype is unsupported: '{0}'")]
UnsupportedMime(String),
#[error("The identifier is not found: '{0}'")]
IdentNotFound(String),
}
impl From<LoaderError> for RequestError {
@ -39,16 +43,19 @@ impl From<LoaderError> for RequestError {
}
}
/// A struct that stores all Manager data. This is requried for sending
//struct ManagerStorage
pub struct ResourceManager {
resources: HashMap<String, Arc<dyn ResourceStorage>>,
loaders: Vec<Box<dyn ResourceLoader>>,
loaders: Vec<Arc<dyn ResourceLoader>>,
}
impl ResourceManager {
pub fn new() -> Self {
Self {
resources: HashMap::new(),
loaders: vec![ Box::new(TextureLoader::default()) ],
loaders: vec![ Arc::new(ImageLoader::default()), Arc::new(ModelLoader::default()) ],
}
}
@ -65,12 +72,14 @@ impl ResourceManager {
.find(|l| l.does_support_file(path)) {
// Load the resource and store it
let res = loader.load(path)?;
let loader = Arc::clone(&loader); // stop borrowing from self
let res = loader.load(self, path)?;
self.resources.insert(path.to_string(), res.clone());
// convert Arc<dyn ResourceStorage> to Arc<Resource<T>
// cast Arc<dyn ResourceStorage> to Arc<Resource<T>
let res = res.as_arc_any();
let res = res.downcast::<Resource<T>>().expect("Failure to downcast resource");
let res = res.downcast::<Resource<T>>()
.expect("Failure to downcast resource! Does the loader return an `Arc<Resource<T>>`?");
Ok(res)
} else {
@ -79,6 +88,49 @@ impl ResourceManager {
}
}
}
/// Store bytes in the manager. If there is already an entry with the same identifier it will be updated.
///
/// Panics: If there is already an entry with the same `ident`, and the entry is not bytes, this function will panic.
///
/// Parameters:
/// * `ident` - The identifier to store along with these bytes. Make sure its unique to avoid overriding something.
/// * `bytes` - The bytes to store.
///
/// Returns: The `Arc` to the now stored resource
pub fn load_bytes<T: Send + Sync + Any + 'static>(&mut self, ident: &str, mime_type: &str, bytes: Vec<u8>, offset: usize, length: usize) -> Result<Arc<Resource<T>>, RequestError> {
if let Some(loader) = self.loaders.iter()
.find(|l| l.does_support_mime(mime_type)) {
let loader = loader.clone();
let res = loader.load_bytes(self, bytes, offset, length)?;
self.resources.insert(ident.to_string(), res.clone());
// code here...
// cast Arc<dyn ResourceStorage> to Arc<Resource<T>
let res = res.as_arc_any();
let res = res.downcast::<Resource<T>>()
.expect("Failure to downcast resource! Does the loader return an `Arc<Resource<T>>`?");
Ok(res)
} else {
Err(RequestError::UnsupportedMime(mime_type.to_string()))
}
}
/// Requests bytes from the manager.
pub fn request_loaded_bytes<T: Send + Sync + Any + 'static>(&mut self, ident: &str) -> Result<Arc<Resource<T>>, RequestError> {
match self.resources.get(&ident.to_string()) {
Some(res) => {
let res = res.clone().as_arc_any();
let res = res.downcast::<Resource<T>>().expect("Failure to downcast resource");
Ok(res)
},
None => {
Err(RequestError::IdentNotFound(ident.to_string()))
}
}
}
}
#[cfg(test)]
@ -125,7 +177,7 @@ mod tests {
assert!(
match err {
// make sure the error is NotFound
RequestError::Loader(LoaderError::IOError(e)) if e.kind() == io::ErrorKind::NotFound => true,
RequestError::Loader(LoaderError::IoError(e)) if e.kind() == io::ErrorKind::NotFound => true,
_ => false
}
);

41
lyra-resource/src/util.rs Normal file
View File

@ -0,0 +1,41 @@
use base64::Engine;
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum UriReadError {
#[error("IOError: '{0}'")]
IoError(io::Error),
// From is implemented for this field in each loader module
#[error("Base64 decoding error: '{0}'")]
Base64Decode(base64::DecodeError),
#[error("Some data was missing from the uri")]
None
}
/// Read a buffer's uri string into a byte buffer.
///
/// * `containing_path`: The path of the containing folder of the buffers "parent",
/// the parent being where this buffer is defined in,
/// i.e. parent="resources/models/player.gltf", containing="resource/models"
pub(crate) fn gltf_read_buffer_uri(containing_path: &str, uri: &str) -> Result<Vec<u8>, UriReadError> {
if let Some((mime, data)) = uri.strip_prefix("data")
.and_then(|uri| uri.split_once(",")) {
let (_mime, is_base64) = match mime.strip_suffix(";base64") {
Some(mime) => (mime, true),
None => (mime, false),
};
if is_base64 {
base64::engine::general_purpose::STANDARD.decode(data)
.map_err(|e| UriReadError::Base64Decode(e))
} else {
Ok(data.as_bytes().to_vec())
}
} else {
let full_path = format!("{containing_path}/{uri}");
std::fs::read(&full_path).map_err(|e| UriReadError::IoError(e))
}
}

Binary file not shown.

View File

@ -0,0 +1,121 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.5",
"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,
"TEXCOORD_0":1,
"NORMAL":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":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"buffers":[
{
"byteLength":840,
"uri":"data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAQAOABQAAQAUAAcACgAGABIACgASABYAFwATAAwAFwAMABAADwADAAkADwAJABUABQACAAgABQAIAAsAEQANAAAAEQAAAAQA"
}
]
}

Binary file not shown.

View File

@ -0,0 +1,121 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.5",
"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,
"TEXCOORD_0":1,
"NORMAL":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":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"buffers":[
{
"byteLength":840,
"uri":"test-sep.bin"
}
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,137 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.6",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Cube"
}
],
"materials":[
{
"doubleSided":true,
"name":"Material",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"Cube",
"primitives":[
{
"attributes":{
"POSITION":0,
"TEXCOORD_0":1,
"NORMAL":2
},
"indices":3,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"uvgrid",
"uri":"uvgrid.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":24,
"max":[
1,
1,
1
],
"min":[
-1,
-1,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":24,
"type":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":24,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5123,
"count":36,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":288,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":192,
"byteOffset":288,
"target":34962
},
{
"buffer":0,
"byteLength":288,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":72,
"byteOffset":768,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":840,
"uri":"texture-sep.bin"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly

View File

@ -1,18 +1,16 @@
use edict::Component;
use crate::render::{vertex::Vertex, mesh::Mesh, material::Material};
use lyra_resource::Mesh;
#[derive(Clone, Component)]
pub struct MeshComponent {
pub mesh: Mesh,
pub material: Material,
}
impl MeshComponent {
pub fn new(mesh: Mesh, material: Material) -> Self {
pub fn new(mesh: Mesh) -> Self {
Self {
mesh,
material
}
}
}

View File

@ -1,3 +1,4 @@
pub mod mesh;
pub mod model;
pub mod transform;
pub mod camera;

View File

@ -0,0 +1,39 @@
use lyra_resource::ResHandle;
use crate::assets::Model;
#[derive(Clone, edict::Component)]
pub struct ModelComponent(pub ResHandle<Model>);
impl From<ResHandle<Model>> for ModelComponent {
fn from(value: ResHandle<Model>) -> Self {
ModelComponent(value)
}
}
/* impl From<ResHandle<Model> for ModelComponent {
} */
impl std::ops::Deref for ModelComponent {
type Target = ResHandle<Model>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ModelComponent {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/* impl ModelComponent {
pub fn new(model, material: Material) -> Self {
Self {
mesh,
material
}
}
} */

View File

@ -297,7 +297,7 @@ impl Game {
.with(fmt::layer().with_writer(stdout_layer))
.with(filter::Targets::new()
.with_target("lyra_engine", Level::TRACE)
.with_target("wgpu_core", Level::INFO)
.with_target("wgpu_core", Level::WARN)
.with_default(Level::DEBUG))
.init();

View File

@ -1,3 +1,5 @@
#![feature(hash_extract_if)]
pub mod game;
pub mod render;
pub mod input_event;

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

View File

@ -2,53 +2,23 @@ use edict::EntityId;
use crate::math::Transform;
use super::{mesh::Mesh, material::Material};
pub struct RenderJob {
mesh: Mesh,
material: Material,
entity: EntityId,
pub entity: EntityId,
pub shader_id: u64,
pub mesh_buffer_id: uuid::Uuid,
transform: Transform,
last_transform: Option<Transform>, // TODO: render interpolation
pub transform: Transform,
pub last_transform: Option<Transform>, // TODO: render interpolation
}
impl RenderJob {
pub fn new(mesh: Mesh, material: Material, entity: EntityId, transform: Transform, last_transform: Option<Transform>) -> Self {
pub fn new(entity: EntityId, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform: Transform, last_transform: Option<Transform>) -> Self {
Self {
mesh,
material,
entity,
shader_id,
mesh_buffer_id,
transform,
last_transform,
}
}
pub fn mesh(&self)-> &Mesh {
&self.mesh
}
pub fn material(&self)-> &Material {
&self.material
}
pub fn entity(&self)-> EntityId {
self.entity
}
pub fn transform(&self)-> &Transform {
&self.transform
}
pub fn set_transform(&mut self, transform: Transform){
self.transform = transform;
}
pub fn last_transform(&self)-> Option<&Transform> {
self.last_transform.as_ref()
}
pub fn set_last_transform(&mut self, last_transform: Transform){
self.last_transform = Some(last_transform);
}
}

View File

@ -1,8 +1,8 @@
use std::{ops::Range, cell::Ref};
use std::ops::Range;
use wgpu::{PipelineLayout, RenderPipeline, RenderPass, VertexBufferLayout, BindGroupLayout};
use super::{render_job::RenderJob, vertex::Vertex, desc_buf_lay::DescVertexBufferLayout, texture::RenderTexture};
use super::{render_job::RenderJob, texture::RenderTexture};
pub struct FullRenderPipeline {
layout: PipelineLayout,

View File

@ -5,30 +5,27 @@ use std::num::NonZeroU64;
use std::sync::Arc;
use std::borrow::Cow;
use aligned_vec::AVec;
use async_std::sync::Mutex;
use async_trait::async_trait;
use atomicell::{AtomicCell, RefMut};
use edict::query::EpochOf;
use edict::{EntityId, Entities};
use glam::Mat4;
use glam::Vec3;
use tracing::{debug, warn};
use wgpu::{BindGroup, BindGroupLayout, Limits, BufferBinding};
use wgpu::{BindGroup, BindGroupLayout, Limits};
use wgpu::util::DeviceExt;
use winit::window::Window;
use crate::ecs::components::camera::CameraComponent;
use crate::ecs::components::mesh::MeshComponent;
use crate::ecs::components::model::ModelComponent;
use crate::ecs::components::transform::TransformComponent;
use crate::math::{Transform, Angle};
use crate::resources;
use crate::math::Transform;
use super::camera::RenderCamera;
use super::desc_buf_lay::DescVertexBufferLayout;
use super::texture::RenderTexture;
use super::vertex::Vertex;
use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob, mesh::Mesh};
use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob};
use lyra_resource::Mesh;
pub trait Renderer {
fn prepare(&mut self, main_world: &mut edict::World);
@ -36,16 +33,15 @@ pub trait Renderer {
fn on_resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>);
fn surface_size(&self) -> winit::dpi::PhysicalSize<u32>;
fn add_render_pipeline(&mut self, shader_id: u32, pipeline: Arc<FullRenderPipeline>);
fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc<FullRenderPipeline>);
}
struct RenderBufferStorage {
buffer_vertex: BufferStorage,
buffer_indices: Option<BufferStorage>,
buffer_indices: Option<(wgpu::IndexFormat, BufferStorage)>,
render_texture: Option<RenderTexture>,
texture_bindgroup: Option<BindGroup>,
texture_layout: Option<BindGroupLayout>,
/// The index of the transform for this entity.
/// The tuple is structured like this: (transform index, index of transform inside the buffer)
@ -73,13 +69,15 @@ struct TransformBuffers {
impl TransformBuffers {
/// Update an entity's buffer with the new transform. Will panic if the entity isn't stored
fn update_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: EntityId, transform: glam::Mat4) {
fn update_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: EntityId, transform: glam::Mat4) -> TransformBufferIndices {
let indices = self.not_updated.remove(&entity)
.or_else(|| self.just_updated.remove(&entity))
.expect("Use 'insert_entity' for new entities");
self.just_updated.insert(entity, indices);
let (_, buffer, _) = self.buffer_bindgroups.get(indices.buffer_index).unwrap();
queue.write_buffer(buffer, indices.transform_index as u64 * limits.min_uniform_buffer_offset_alignment as u64, bytemuck::bytes_of(&transform));
indices
}
/// Insert a new entity into the buffer, returns where it was stored.
@ -109,6 +107,22 @@ impl TransformBuffers {
indices
}
/// Update or insert an entities transform
fn update_or_insert<TFn>(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: EntityId, transform_fn: TFn) -> TransformBufferIndices
where TFn: Fn() -> glam::Mat4
{
if self.contains(entity) {
self.update_entity(queue, limits, entity, transform_fn())
} else {
self.insert_entity(queue, limits, entity, transform_fn())
}
}
/// Returns true if the entity's transform is stored (does not mean its up-to-date).
fn contains(&self, entity: EntityId) -> bool {
self.not_updated.contains_key(&entity) || self.just_updated.contains_key(&entity)
}
/// Collect the dead entities, mark entities and not updated for next updates.
fn tick(&mut self) {
// take the dead entities, these were ones that were not updated this tick
@ -116,7 +130,6 @@ impl TransformBuffers {
.map(|t| t.clone()).collect();
self.dead_indices = dead;
// swap just_updated into not_updated
self.not_updated = self.just_updated.clone();
self.just_updated.clear();
}
@ -145,10 +158,11 @@ pub struct BasicRenderer {
pub clear_color: wgpu::Color,
pub render_pipelines: HashMap<u32, Arc<FullRenderPipeline>>,
pub render_pipelines: HashMap<u64, Arc<FullRenderPipeline>>,
pub render_jobs: VecDeque<RenderJob>,
buffer_storage: HashMap<EntityId, RenderBufferStorage>, // TODO: clean up left over buffers from deleted entities/components
mesh_buffers: HashMap<uuid::Uuid, RenderBufferStorage>, // TODO: clean up left over buffers from deleted entities/components
entity_meshes: HashMap<EntityId, uuid::Uuid>,
transform_buffers: TransformBuffers,
transform_bind_group_layout: BindGroupLayout,
@ -160,6 +174,8 @@ pub struct BasicRenderer {
camera_buffer: wgpu::Buffer,
camera_bind_group: wgpu::BindGroup,
texture_bind_group_layout: BindGroupLayout,
default_texture_bind_group: BindGroup,
depth_buffer_texture: RenderTexture,
}
@ -208,7 +224,7 @@ impl BasicRenderer {
false => surface_caps.present_modes[0]
}; */
println!("present mode: {:?}", present_mode);
debug!("present mode: {:?}", present_mode);
let surface_format = surface_caps.formats.iter()
.copied()
@ -348,12 +364,27 @@ impl BasicRenderer {
let depth_texture = RenderTexture::create_depth_texture(&device, &config, "Depth Buffer");
let mut pipelines = HashMap::new();
pipelines.insert(0, Arc::new(FullRenderPipeline::new(&device, &config, &shader,
vec![super::vertex::Vertex::desc(),],
vec![&texture_bind_group_layout, &transform_bind_group_layout, &camera_bind_group_layout])));
// load the default texture
let bytes = include_bytes!("default_texture.png");
let tex = RenderTexture::from_bytes(&device, &queue, bytes, "default_texture").unwrap();
let default_tex_bindgroup = device.create_bind_group(
&wgpu::BindGroupDescriptor {
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(tex.view()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(tex.sampler()),
}
],
label: Some("default_texture"),
}
);
Self {
let mut s = Self {
window,
surface,
device,
@ -366,9 +397,10 @@ impl BasicRenderer {
b: 0.3,
a: 1.0,
},
render_pipelines: pipelines,
render_pipelines: HashMap::new(),
render_jobs: VecDeque::new(),
buffer_storage: HashMap::new(),
mesh_buffers: HashMap::new(),
entity_meshes: HashMap::new(),
render_limits,
transform_buffers,
@ -378,29 +410,31 @@ impl BasicRenderer {
camera_buffer,
camera_bind_group,
texture_bind_group_layout,
default_texture_bind_group: default_tex_bindgroup,
depth_buffer_texture: depth_texture,
}
};
// create the default pipelines
let mut pipelines = HashMap::new();
pipelines.insert(0, Arc::new(FullRenderPipeline::new(&s.device, &s.config, &shader,
vec![super::vertex::Vertex::desc(),],
vec![&s.texture_bind_group_layout, &s.transform_bind_group_layout, &camera_bind_group_layout])));
s.render_pipelines = pipelines;
s
}
fn find_next_multiple(n: u32, mul: u32) -> u32 {
if n % mul == 0 {
n
} else {
n + (mul - n % mul)
}
}
// TODO: minimize how often model buffers are updated by checking if they changed
fn update_model_buffers(&mut self, entity: EntityId, model: &MeshComponent) {
if let Some(buffers) = self.buffer_storage.get_mut(&entity) {
fn update_mesh_buffers(&mut self, _entity: EntityId, mesh: &Mesh) {
if let Some(buffers) = self.mesh_buffers.get_mut(&mesh.uuid) {
// check if the buffer sizes dont match. If they dont, completely remake the buffers
let vertices = &model.mesh.vertices;
let vertices = mesh.position().unwrap();
if buffers.buffer_vertex.count() != vertices.len() {
drop(buffers);
let (vert, idx) = self.create_vertex_index_buffers(&model.mesh);
debug!("Recreating buffers for mesh {}", mesh.uuid.to_string());
let (vert, idx) = self.create_vertex_index_buffers(mesh);
// have to re-get buffers because of borrow checker
let buffers = self.buffer_storage.get_mut(&entity).unwrap();
let buffers = self.mesh_buffers.get_mut(&mesh.uuid).unwrap();
buffers.buffer_indices = idx;
buffers.buffer_vertex = vert;
@ -411,109 +445,104 @@ impl BasicRenderer {
let vertex_buffer = buffers.buffer_vertex.buffer();
let vertices = vertices.as_slice();
// align the vertices to 4 bytes (u32 is 4 bytes, which is wgpu::COPY_BUFFER_ALIGNMENT)
let (_, vertices, _) = bytemuck::pod_align_to::<Vertex, u32>(vertices);
let (_, vertices, _) = bytemuck::pod_align_to::<Vec3, u32>(vertices);
self.queue.write_buffer(&vertex_buffer, 0, bytemuck::cast_slice(&vertices));
// update the indices if they're given
if let Some(index_buffer) = buffers.buffer_indices.as_ref() {
let index_buffer = index_buffer.buffer();
let indices = model.mesh.indices.as_ref().unwrap().as_slice();
let (_, indices, _) = bytemuck::pod_align_to::<u16, u32>(indices);
self.queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(&indices));
let aligned_indices = match mesh.indices.as_ref().unwrap() {
// U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size.
lyra_resource::MeshIndices::U16(v) => bytemuck::pod_align_to::<u16, u32>(v).1,
lyra_resource::MeshIndices::U32(v) => bytemuck::pod_align_to::<u32, u32>(v).1,
};
let index_buffer = index_buffer.1.buffer();
self.queue.write_buffer(index_buffer, 0, bytemuck::cast_slice(&aligned_indices));
}
}
}
fn create_vertex_index_buffers(&mut self, mesh: &Mesh) -> (BufferStorage, Option<BufferStorage>) {
fn create_vertex_index_buffers(&mut self, mesh: &Mesh) -> (BufferStorage, Option<(wgpu::IndexFormat, BufferStorage)>) {
let positions = mesh.position().unwrap();
let tex_coords: Vec<glam::Vec2> = mesh.tex_coords().cloned()
.unwrap_or_else(|| vec![glam::Vec2::new(0.0, 0.0); positions.len()]);
assert!(positions.len() == tex_coords.len());
let vertex_inputs: Vec<Vertex> = std::iter::zip(positions, tex_coords.into_iter())
.map(|(v, t)| Vertex::new(v.clone(), t))
.collect();
let vertex_buffer = self.device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(mesh.vertices.as_slice()),
contents: bytemuck::cast_slice(vertex_inputs.as_slice()),//vertex_combined.as_slice(),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages:: COPY_DST,
}
);
let vertex_buffer = BufferStorage::new(vertex_buffer, 0, mesh.vertices.len());
let vertex_buffer = BufferStorage::new(vertex_buffer, 0, vertex_inputs.len());
let buffer_indices = match mesh.indices.as_ref() {
let indices = match mesh.indices.as_ref() {
Some(indices) => {
let (idx_type, len, contents) = match indices {
lyra_resource::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(&v)),
lyra_resource::MeshIndices::U32(v) => (wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(&v)),
};
let index_buffer = self.device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&indices),
contents,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages:: COPY_DST,
}
);
let buffer_indices = BufferStorage::new(index_buffer, 0, indices.len());
let buffer_indices = BufferStorage::new(index_buffer, 0, len);
Some(buffer_indices)
Some((idx_type, buffer_indices))
},
None => {
None
}
};
( vertex_buffer, buffer_indices )
( vertex_buffer, indices )
}
fn create_model_buffers(&mut self, model: &MeshComponent, transform_indices: TransformBufferIndices) -> RenderBufferStorage {
let mesh = &model.mesh;
fn create_mesh_buffers(&mut self, mesh: &Mesh, transform_indices: TransformBufferIndices) -> RenderBufferStorage {
let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(mesh);
let model_texture = &model.material.texture;
let image = &model_texture.data.as_ref().unwrap().image;
let diffuse_texture = RenderTexture::from_image(&self.device, &self.queue, image, None).unwrap();
let diffuse_bindgroup = if let Some(model_texture) = &mesh.material().base_color_texture {
let image = &model_texture.data.as_ref().unwrap().image;
let diffuse_texture = RenderTexture::from_image(&self.device, &self.queue, image, None).unwrap();
let texture_bind_group_layout =
self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
let diffuse_bind_group = self.device.create_bind_group(
&wgpu::BindGroupDescriptor {
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(diffuse_texture.view()),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
// This should match the filterable field of the
// corresponding Texture entry above.
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(diffuse_texture.sampler()),
}
],
label: Some("diffuse_bind_group"),
}
);
let diffuse_bind_group = self.device.create_bind_group(
&wgpu::BindGroupDescriptor {
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(diffuse_texture.view()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(diffuse_texture.sampler()),
}
],
label: Some("diffuse_bind_group"),
}
);
Some(diffuse_bind_group)
} else {
None
};
RenderBufferStorage {
buffer_vertex: vertex_buffer,
buffer_indices,
render_texture: None,
texture_layout: None,
texture_bindgroup: Some(diffuse_bind_group),
texture_bindgroup: diffuse_bindgroup,
transform_index: transform_indices
}
}
@ -553,6 +582,28 @@ impl BasicRenderer {
buffers.next_indices = indices;
indices
}
/// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed.
fn process_mesh(&mut self, entity: EntityId, transform: Transform, mesh: &Mesh) -> bool {
let indices = self.transform_buffers.update_or_insert(&self.queue, &self.render_limits,
entity, || transform.calculate_mat4());
if self.mesh_buffers.contains_key(&mesh.uuid) {
false
} else {
// check if the transform buffers need to be expanded
if self.transform_buffers.should_expand() {
self.expand_transform_buffers();
}
// create the mesh's buffers
let buffers = self.create_mesh_buffers(mesh, indices);
self.mesh_buffers.insert(mesh.uuid, buffers);
self.entity_meshes.insert(entity, mesh.uuid);
true
}
}
}
impl Renderer for BasicRenderer {
@ -561,45 +612,39 @@ impl Renderer for BasicRenderer {
let mut alive_entities = HashSet::new();
for (entity, model, model_epoch, transform) in main_world.query::<(Entities, &MeshComponent, EpochOf<MeshComponent>, &TransformComponent)>().iter() {
// Create the render job and push it to the queue
let job = RenderJob::new(model.mesh.clone(), model.material.clone(), entity, transform.transform, None);
self.render_jobs.push_back(job);
for (entity, model, model_epoch, transform) in main_world.query::<(Entities, &ModelComponent, EpochOf<ModelComponent>, &TransformComponent)>().iter() {
alive_entities.insert(entity);
if self.buffer_storage.get(&entity).is_none() {
// check if the transform buffers need to be expanded
if self.transform_buffers.should_expand() {
self.expand_transform_buffers();
let model = model.data.as_ref().unwrap().as_ref();
for mesh in model.meshes.iter() {
if !self.process_mesh(entity, transform.transform, mesh) {
if model_epoch == last_epoch {
self.update_mesh_buffers(entity, mesh);
}
}
// insert transform into buffers
let indices = self.transform_buffers.insert_entity(&self.queue, &self.render_limits,
entity, transform.transform.calculate_mat4());
// create the mesh's buffers
let buffers = self.create_model_buffers(model, indices);
self.buffer_storage.insert(entity, buffers);
} else {
// update entity transforms
self.transform_buffers.update_entity(&self.queue, &self.render_limits,
entity, transform.transform.calculate_mat4());
// if the model was updated, update its buffers
if model_epoch == last_epoch {
self.update_model_buffers(entity, model);
}
let shader = mesh.material().shader_uuid.unwrap_or(0);
let job = RenderJob::new(entity, shader, mesh.uuid, transform.transform, None);
self.render_jobs.push_back(job);
}
}
for (entity, mesh, mesh_epoch, transform) in main_world.query::<(Entities, &MeshComponent, EpochOf<MeshComponent>, &TransformComponent)>().iter() {
debug!("TODO: Process MeshComponents"); // TODO: Process MeshComponents
}
// collect dead entities
self.transform_buffers.tick();
// when buffer storage length does not match the amount of iterated entities,
// remove all dead entities, and their buffers, if they weren't iterated over
if self.buffer_storage.len() != alive_entities.len() {
self.buffer_storage.retain(|e, _| alive_entities.contains(e));
if self.mesh_buffers.len() != alive_entities.len() {
let removed_entities: Vec<uuid::Uuid> = self.entity_meshes
.extract_if(|e, _| !alive_entities.contains(e))
.map(|(_, v)| v)
.collect();
self.mesh_buffers.retain(|u, _| !removed_entities.contains(u));
}
if let Some(camera) = main_world.query_mut::<(&mut CameraComponent,)>().into_iter().next() {
@ -644,17 +689,18 @@ impl Renderer for BasicRenderer {
// Pop off jobs from the queue as they're being processed
while let Some(job) = self.render_jobs.pop_front() {
if let Some(pipeline) = self.render_pipelines.get(&job.material().shader_id) {
if let Some(pipeline) = self.render_pipelines.get(&job.shader_id) {
// specify to use this pipeline
render_pass.set_pipeline(pipeline.get_wgpu_pipeline());
// get the mesh (containing vertices) and the buffers from storage
let mesh = job.mesh();
let buffers = self.buffer_storage.get(&job.entity()).unwrap();
let buffers = self.mesh_buffers.get(&job.mesh_buffer_id).unwrap();
// Bind the optional texture
if let Some(tex) = buffers.texture_bindgroup.as_ref() {
render_pass.set_bind_group(0, &tex, &[]);
} else {
render_pass.set_bind_group(0, &self.default_texture_bind_group, &[]);
}
// Get the bindgroup for job's transform and bind to it using an offset.
@ -667,18 +713,18 @@ impl Renderer for BasicRenderer {
render_pass.set_bind_group(2, &self.camera_bind_group, &[]);
// if this mesh uses indices, use them to draw the mesh
if let Some(indices) = buffers.buffer_indices.as_ref() {
if let Some((idx_type, indices)) = buffers.buffer_indices.as_ref() {
let indices_len = indices.count() as u32;
render_pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..));
render_pass.set_index_buffer(indices.buffer().slice(..), wgpu::IndexFormat::Uint16);
render_pass.set_index_buffer(indices.buffer().slice(..), idx_type.clone());
render_pass.draw_indexed(0..indices_len, 0, 0..1);
} else {
let vertex_count = buffers.buffer_vertex.count();
render_pass.set_vertex_buffer(buffers.buffer_vertex.slot(), buffers.buffer_vertex.buffer().slice(..));
render_pass.draw(0..mesh.vertices.len() as u32, 0..1);
render_pass.draw(0..vertex_count as u32, 0..1);
}
} else {
warn!("Failure to find RenderPipeline with shader id of '{}'!", job.material().shader_id);
}
}
}
@ -706,7 +752,7 @@ impl Renderer for BasicRenderer {
self.size
}
fn add_render_pipeline(&mut self, shader_id: u32, pipeline: Arc<FullRenderPipeline>) {
fn add_render_pipeline(&mut self, shader_id: u64, pipeline: Arc<FullRenderPipeline>) {
self.render_pipelines.insert(shader_id, pipeline);
}
}

View File

@ -3,9 +3,17 @@ use super::desc_buf_lay::DescVertexBufferLayout;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pub position: [f32; 3],
pub position: glam::Vec3,
pub tex_coords: glam::Vec2
//pub color: [f32; 3], // TODO: add color again
pub tex_coords: [f32; 2]
}
impl Vertex {
pub fn new(position: glam::Vec3, tex_coords: glam::Vec2) -> Self {
Self {
position, tex_coords
}
}
}
impl DescVertexBufferLayout for Vertex {
@ -17,12 +25,12 @@ impl DescVertexBufferLayout for Vertex {
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
format: wgpu::VertexFormat::Float32x3, // Vec3
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
format: wgpu::VertexFormat::Float32x2, // Vec2
}
]
}