Implement a basic engine with separate modules
This commit is contained in:
parent
0135974d08
commit
95070f854c
20 changed files with 1253 additions and 117 deletions
24
src/camera.rs
Normal file
24
src/camera.rs
Normal 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
32
src/ecs.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
185
src/main.rs
185
src/main.rs
|
|
@ -1,127 +1,86 @@
|
|||
#[macro_use]
|
||||
extern crate glium;
|
||||
use glium::Surface;
|
||||
mod teapot;
|
||||
mod camera;
|
||||
mod ecs;
|
||||
mod model;
|
||||
mod render;
|
||||
|
||||
fn main() {
|
||||
use anyhow::Result;
|
||||
use camera::Camera;
|
||||
use ecs::{rotation_system, MeshHandle, Transform};
|
||||
use glam::{Quat, Vec3};
|
||||
use glium::backend::glutin::SimpleWindowBuilder;
|
||||
use render::{Renderer, GliumRenderer};
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let event_loop = glium::winit::event_loop::EventLoop::builder()
|
||||
.build()
|
||||
.expect("event loop building");
|
||||
let (window, display) = glium::backend::glutin::SimpleWindowBuilder::new()
|
||||
.with_title("Glium tutorial #3")
|
||||
.expect("create event-loop");
|
||||
|
||||
let (window, display) = SimpleWindowBuilder::new()
|
||||
.with_title("fps")
|
||||
.with_inner_size(1280, 720)
|
||||
.build(&event_loop);
|
||||
|
||||
let mut world = hecs::World::new();
|
||||
|
||||
let positions = glium::VertexBuffer::new(&display, &teapot::VERTICES).unwrap();
|
||||
let normals = glium::VertexBuffer::new(&display, &teapot::NORMALS).unwrap();
|
||||
let indices = glium::IndexBuffer::new(&display, glium::index::PrimitiveType::TrianglesList,
|
||||
&teapot::INDICES).unwrap();
|
||||
let mesh = model::load_gltf("resources/monkey-smooth.gltf", &display)?;
|
||||
// let mesh = model::cube(&display)?;
|
||||
let mut renderer = GliumRenderer::new(display)?;
|
||||
let mesh_id = renderer.meshes.len();
|
||||
renderer.meshes.push(mesh);
|
||||
|
||||
let vertex_shader_src = r#"
|
||||
#version 140
|
||||
world.spawn((
|
||||
Transform {
|
||||
translation: Vec3::ZERO,
|
||||
rotation: Quat::IDENTITY,
|
||||
scale: Vec3::ONE,
|
||||
},
|
||||
MeshHandle(mesh_id),
|
||||
));
|
||||
|
||||
in vec3 position;
|
||||
in vec3 normal;
|
||||
{
|
||||
let (w, h): (u32, u32) = window.inner_size().into();
|
||||
world.spawn((Camera {
|
||||
eye: Vec3::new(3.0, 2.0, 3.0),
|
||||
center: Vec3::ZERO,
|
||||
up: Vec3::Y,
|
||||
fovy: 45_f32.to_radians(),
|
||||
aspect: w as f32 / h as f32,
|
||||
znear: 0.1,
|
||||
zfar: 100.0,
|
||||
},));
|
||||
}
|
||||
|
||||
out vec3 v_normal;
|
||||
event_loop
|
||||
.run(move |event, el| {
|
||||
use glium::winit::event::{Event, WindowEvent};
|
||||
|
||||
uniform mat4 matrix;
|
||||
uniform mat4 perspective;
|
||||
|
||||
void main() {
|
||||
v_normal = transpose(inverse(mat3(matrix))) * normal;
|
||||
gl_Position = perspective * matrix * vec4(position, 1.0);
|
||||
}
|
||||
"#;
|
||||
let fragment_shader_src = r#"
|
||||
#version 140
|
||||
|
||||
in vec3 v_normal;
|
||||
out vec4 color;
|
||||
uniform vec3 u_light;
|
||||
|
||||
void main() {
|
||||
float brightness = dot(normalize(v_normal), normalize(u_light));
|
||||
vec3 dark_color = vec3(0.6, 0.0, 0.0);
|
||||
vec3 regular_color = vec3(1.0, 0.0, 0.0);
|
||||
color = vec4(mix(dark_color, regular_color, brightness), 1.0);
|
||||
}
|
||||
"#;
|
||||
let program = glium::Program::from_source(&display, vertex_shader_src, fragment_shader_src, None).unwrap();
|
||||
|
||||
let light = [-1.0, 0.4, 0.9f32];
|
||||
let mut t: f32 = 0.0;
|
||||
#[allow(deprecated)]
|
||||
event_loop.run(move |ev, window_target| {
|
||||
match ev {
|
||||
glium::winit::event::Event::WindowEvent { event, .. } => match event {
|
||||
glium::winit::event::WindowEvent::CloseRequested => {
|
||||
window_target.exit();
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => match event {
|
||||
WindowEvent::CloseRequested => el.exit(),
|
||||
WindowEvent::Resized(sz) => {
|
||||
ecs::set_camera_aspect(&mut world, sz.width as f32 / sz.height as f32);
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
renderer.render(&world);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
// We now need to render everyting in response to a RedrawRequested event due to the animation
|
||||
glium::winit::event::WindowEvent::RedrawRequested => {
|
||||
let mut target = display.draw();
|
||||
target.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0);
|
||||
t += 0.01;
|
||||
let x = t.sin() * 0.5;
|
||||
let y = t.cos() * 0.5;
|
||||
|
||||
let perspective = {
|
||||
let (width, height) = target.get_dimensions();
|
||||
let aspect_ratio = height as f32 / width as f32;
|
||||
|
||||
let fov: f32 = 3.141592 / 3.0;
|
||||
let zfar = 1024.0;
|
||||
let znear = 0.1;
|
||||
|
||||
let f = 1.0 / (fov / 2.0).tan();
|
||||
|
||||
[
|
||||
[f * aspect_ratio , 0.0, 0.0 , 0.0],
|
||||
[ 0.0 , f , 0.0 , 0.0],
|
||||
[ 0.0 , 0.0, (zfar+znear)/(zfar-znear) , 1.0],
|
||||
[ 0.0 , 0.0, -(2.0*zfar*znear)/(zfar-znear), 0.0],
|
||||
]
|
||||
Event::AboutToWait => {
|
||||
// -- update logic --
|
||||
let now = Instant::now();
|
||||
static mut LAST: Option<Instant> = None;
|
||||
let dt = unsafe { // FIXME
|
||||
let last = LAST.replace(now).unwrap_or(now);
|
||||
(now - last).as_secs_f32()
|
||||
};
|
||||
rotation_system(&mut world, dt);
|
||||
|
||||
let uniforms = uniform! {
|
||||
matrix: [
|
||||
[0.01, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.01, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.01, 0.0],
|
||||
[x, y, 2.0, 1.0f32 ],
|
||||
],
|
||||
u_light: light,
|
||||
perspective: perspective
|
||||
};
|
||||
|
||||
let params = glium::DrawParameters {
|
||||
depth: glium::Depth {
|
||||
test: glium::draw_parameters::DepthTest::IfLess,
|
||||
write: true,
|
||||
.. Default::default()
|
||||
},
|
||||
.. Default::default()
|
||||
};
|
||||
|
||||
target.draw((&positions, &normals), &indices, &program, &uniforms,
|
||||
¶ms).unwrap();
|
||||
target.finish().unwrap();
|
||||
},
|
||||
// Because glium doesn't know about windows we need to resize the display
|
||||
// when the window's size has changed.
|
||||
glium::winit::event::WindowEvent::Resized(window_size) => {
|
||||
display.resize(window_size.into());
|
||||
},
|
||||
_ => (),
|
||||
},
|
||||
// By requesting a redraw in response to a RedrawEventsCleared event we get continuous rendering.
|
||||
// For applications that only change due to user input you could remove this handler.
|
||||
glium::winit::event::Event::AboutToWait => {
|
||||
window.request_redraw();
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
// ask for next frame
|
||||
window.request_redraw();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
|
|
|||
124
src/model.rs
Normal file
124
src/model.rs
Normal 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
95
src/render.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue