//! Stage 4 example: load primitive meshes and render them lit, in 3D. //! //! Run with: //! cargo run -p oxide-examples --bin hello_mesh //! //! Shows a spinning cube, a sphere, and a ground plane drawn through the //! [`ForwardRenderer`] with a single directional light. Esc quits. #![deny(warnings)] use oxide_engine::math::Quat; use oxide_engine::prelude::*; use oxide_engine::window::event::{ElementState, Key, NamedKey, WindowEvent}; use oxide_engine::window::RenderCtx; /// GPU resources, built lazily on the first frame (once the surface format is /// known) and reused thereafter. struct Gpu3d { pipeline: RenderPipeline, cube: GpuMesh, sphere: GpuMesh, plane: GpuMesh, } #[derive(Default)] struct HelloMesh { angle: f32, gpu: Option, } impl WindowApp for HelloMesh { fn init(&mut self, ctx: &mut AppCtx<'_>) { ctx.set_clear_color(Color::rgb(0.05, 0.06, 0.09)); log::info!("hello_mesh: spinning cube + sphere + ground plane (Esc quits)"); } fn event(&mut self, ctx: &mut AppCtx<'_>, event: &WindowEvent) { if let WindowEvent::KeyboardInput { event: key, .. } = event { if key.state == ElementState::Pressed && key.logical_key == Key::Named(NamedKey::Escape) { ctx.request_exit(); } } } fn update(&mut self, ctx: &mut AppCtx<'_>) { self.angle += ctx.dt; } fn render(&mut self, ctx: &RenderCtx<'_>) { let device = ctx.gpu.device(); let queue = ctx.gpu.queue(); let gpu = self.gpu.get_or_insert_with(|| { // The window runner already clears the surface to the configured // clear color before `render`, so the viewport pipeline is just the // forward pass (no clear pass needed here). let mut pipeline = RenderPipeline::new(); pipeline.add_pass("forward", ForwardPass::new(device, ctx.surface_format)); Gpu3d { pipeline, cube: Mesh::cube().upload(device, "cube"), sphere: Mesh::uv_sphere(0.8, 32, 16).upload(device, "sphere"), plane: Mesh::plane(12.0).upload(device, "plane"), } }); // Orbit the camera slowly around the scene. let eye = Vec3::new( 4.0 * (self.angle * 0.3).cos(), 2.6, 4.0 * (self.angle * 0.3).sin(), ); let view = Transform::looking_at(eye, Vec3::new(0.0, 0.2, 0.0), Vec3::Y); let camera = Camera::default(); let objects = [ RenderObject { mesh: &gpu.plane, material: Material::diffuse(Color::rgb(0.25, 0.27, 0.30)), transform: Transform::from_translation(Vec3::new(0.0, -1.0, 0.0)), }, RenderObject { mesh: &gpu.cube, material: Material::diffuse(Color::rgb(0.85, 0.20, 0.15)), transform: Transform::from_trs( Vec3::new(-1.3, 0.0, 0.0), Quat::from_euler(oxide_engine::math::EulerRot::YXZ, self.angle, 0.4, 0.0), Vec3::ONE, ), }, RenderObject { mesh: &gpu.sphere, material: Material::metal(Color::rgb(0.55, 0.65, 0.95), 0.35), transform: Transform::from_translation(Vec3::new(1.3, 0.2, 0.0)), }, ]; let lighting = Lighting::default(); gpu.pipeline.render(&mut FrameContext { device, queue, color: ctx.view, size: ctx.size, viewport_rect: None, clear_color: Color::rgb(0.05, 0.06, 0.09), camera: &camera, view_transform: &view, lighting: &lighting, objects: &objects, }); } } fn main() -> anyhow::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let config = WindowConfig { title: "Oxide — hello_mesh".to_string(), width: 960, height: 540, ..Default::default() }; run(config, HelloMesh::default()) }