diff options
author | tcmal <me@aria.rip> | 2024-08-25 17:44:24 +0100 |
---|---|---|
committer | tcmal <me@aria.rip> | 2024-08-25 17:44:24 +0100 |
commit | 09a777d42d6301028cd3a8967ff6a82e703c8800 (patch) | |
tree | 79ac93287a15e8b0f90e1ac5d6981fa6eaeb6229 /examples | |
parent | 6367a9ba5a549b62f01da61fb50323877b9f52ff (diff) |
refactor(all): remove levels and render
Diffstat (limited to 'examples')
-rw-r--r-- | examples/render-quad/Cargo.toml | 7 | ||||
-rw-r--r-- | examples/render-quad/src/data/shader.frag | 11 | ||||
-rw-r--r-- | examples/render-quad/src/data/shader.vert | 16 | ||||
-rw-r--r-- | examples/render-quad/src/draw_pass.rs | 335 | ||||
-rw-r--r-- | examples/render-quad/src/level.rs | 79 | ||||
-rw-r--r-- | examples/render-quad/src/main.rs | 252 | ||||
-rw-r--r-- | examples/render-quad/src/system.rs | 73 |
7 files changed, 486 insertions, 287 deletions
diff --git a/examples/render-quad/Cargo.toml b/examples/render-quad/Cargo.toml index 99cf38e..7e7f57c 100644 --- a/examples/render-quad/Cargo.toml +++ b/examples/render-quad/Cargo.toml @@ -6,15 +6,10 @@ edition = "2018" [dependencies] stockton-skeleton = { path = "../../stockton-skeleton", features = ["vulkan"] } -stockton-input = { path = "../../stockton-input" } -stockton-input-codegen = { path = "../../stockton-input-codegen" } -stockton-levels = { path = "../../stockton-levels" } -stockton-contrib = { path = "../../stockton-contrib", features = ["delta_time", "flycam"] } -stockton-render = { path = "../../stockton-render" } winit = "^0.21" log = "0.4.0" simplelog = "^0.10" image = "0.23.2" -egui = "^0.12" legion = { version = "^0.3" } anyhow = "1.0.40" +gfx-hal = "^0.8.0" diff --git a/examples/render-quad/src/data/shader.frag b/examples/render-quad/src/data/shader.frag new file mode 100644 index 0000000..929073f --- /dev/null +++ b/examples/render-quad/src/data/shader.frag @@ -0,0 +1,11 @@ +#version 450 + +layout (location = 0) in vec2 frag_pos; +layout (location = 1) in vec3 frag_color; + +layout (location = 0) out vec4 color; + +void main() +{ + color = vec4(frag_color, 1.0); +}
\ No newline at end of file diff --git a/examples/render-quad/src/data/shader.vert b/examples/render-quad/src/data/shader.vert new file mode 100644 index 0000000..5ca3090 --- /dev/null +++ b/examples/render-quad/src/data/shader.vert @@ -0,0 +1,16 @@ +#version 450 + +layout (location = 0) in vec2 position; +layout (location = 1) in vec3 color; + +out gl_PerVertex { + vec4 gl_Position; +}; +layout (location = 1) out vec3 frag_color; + + +void main() +{ + gl_Position = vec4(position, 0.5, 1.0); + frag_color = color; +}
\ No newline at end of file diff --git a/examples/render-quad/src/draw_pass.rs b/examples/render-quad/src/draw_pass.rs new file mode 100644 index 0000000..9b32fcb --- /dev/null +++ b/examples/render-quad/src/draw_pass.rs @@ -0,0 +1,335 @@ +//! Minimal code for drawing any level, based on traits from stockton-levels + +use anyhow::{Context, Result}; +use hal::{ + buffer::SubRange, + command::{ClearColor, ClearValue, RenderAttachmentInfo, SubpassContents}, + format::Format, + image::Layout, + pass::Attachment, + pso::{ + BlendDesc, BlendOp, BlendState, ColorBlendDesc, ColorMask, DepthStencilDesc, Face, Factor, + FrontFace, InputAssemblerDesc, LogicOp, PolygonMode, Primitive, Rasterizer, State, + VertexInputRate, + }, +}; +use legion::{Entity, IntoQuery}; +use std::{ + array::IntoIter, + iter::{empty, once}, +}; +use stockton_skeleton::{ + buffers::draw::DrawBuffers, + builders::{ + AttachmentSpec, CompletePipeline, PipelineSpecBuilder, RenderpassSpec, ShaderDesc, + ShaderKind, VertexBufferSpec, VertexPrimitiveAssemblerSpec, + }, + draw_passes::util::TargetSpecificResources, + mem::{DataPool, StagingPool}, + queue_negotiator::QueueFamilyNegotiator, + types::*, + DrawPass, IntoDrawPass, PassPosition, RenderingContext, Session, +}; + +use crate::ExampleState; + +/// The vertices that go to the shader (XY + RGB) +#[derive(Debug, Clone, Copy)] +#[repr(C)] +struct Vertex(pub Vector2, pub Vector3); + +/// An example draw pass +pub struct ExampleDrawPass<'a> { + /// Index and vertex buffer pair + draw_buffers: DrawBuffers<'a, Vertex, DataPool, StagingPool>, + + /// Resources that depend on the surface. This is seperate so that we can deal with surface changes more easily. + surface_resources: SurfaceDependentResources, + + /// Entity we get our state from + state_ent: Entity, +} + +/// Config for our draw pass. This is turned into our drawpass using [`IntoDrawPass`] +pub struct ExampleDrawPassConfig { + pub state_ent: Entity, +} + +impl<'a, P: PassPosition> DrawPass<P> for ExampleDrawPass<'a> { + /// Called every frame to queue actual drawing. + fn queue_draw( + &mut self, + session: &Session, + img_view: &ImageViewT, + cmd_buffer: &mut CommandBufferT, + ) -> anyhow::Result<()> { + // Commit any changes to our vertex buffers + // We queue this first so that it's executed before any draw commands + self.draw_buffers + .vertex_buffer + .record_commit_cmds(cmd_buffer)?; + self.draw_buffers + .index_buffer + .record_commit_cmds(cmd_buffer)?; + + // Get framebuffer + let fb = self.surface_resources.framebuffers.get_next(); + + // Get state + let (state,) = <(&ExampleState,)>::query().get(&session.world, self.state_ent)?; + + // Begin render pass & bind everything needed + unsafe { + cmd_buffer.begin_render_pass( + &self.surface_resources.pipeline.renderpass, + fb, + self.surface_resources.pipeline.render_area, + vec![RenderAttachmentInfo { + image_view: img_view, + clear_value: ClearValue { + color: ClearColor { + float32: [0.0, 0.0, 0.0, 1.0], + }, + }, + }] + .into_iter(), + SubpassContents::Inline, + ); + cmd_buffer.bind_graphics_pipeline(&self.surface_resources.pipeline.pipeline); + + // Bind buffers + cmd_buffer.bind_vertex_buffers( + 0, + once(( + self.draw_buffers.vertex_buffer.get_buffer(), + SubRange { + offset: 0, + size: None, + }, + )), + ); + cmd_buffer.bind_index_buffer( + self.draw_buffers.index_buffer.get_buffer(), + SubRange { + offset: 0, + size: None, + }, + hal::IndexType::U16, + ); + } + + // Draw an example + self.draw_buffers.index_buffer[0] = (0, 1, 2); + self.draw_buffers.vertex_buffer[0] = Vertex(Vector2::new(0.5, 0.5), state.color()); + self.draw_buffers.vertex_buffer[1] = Vertex(Vector2::new(0.0, -0.5), state.color()); + self.draw_buffers.vertex_buffer[2] = Vertex(Vector2::new(-0.5, 0.5), state.color()); + + unsafe { + cmd_buffer.draw_indexed(0..3, 0, 0..1); + } + + // Remember to clean up afterwards! + unsafe { + cmd_buffer.end_render_pass(); + } + + Ok(()) + } + + /// Destroy all our vulkan objects + fn deactivate(self, context: &mut RenderingContext) -> Result<()> { + self.draw_buffers.deactivate(context); + self.surface_resources.deactivate(context)?; + + Ok(()) + } + + /// Deal with a surface change + fn handle_surface_change( + mut self, + _session: &Session, + context: &mut RenderingContext, + ) -> Result<Self> { + // We need to make sure there's never an invalid value for self.surface_resources, + // and that we deactivate everything in case of an error (since we'll be dropped in that case). + let new_resources = match SurfaceDependentResources::new::<P>(context) { + Ok(x) => x, + Err(e) => { + <Self as DrawPass<P>>::deactivate(self, context)?; + + return Err(e); + } + }; + + let old_resources = self.surface_resources; + self.surface_resources = new_resources; + + match old_resources.deactivate(context) { + Ok(_) => Ok(self), + Err(e) => { + <Self as DrawPass<P>>::deactivate(self, context)?; + Err(e) + } + } + } +} + +impl<'a, P: PassPosition> IntoDrawPass<ExampleDrawPass<'a>, P> for ExampleDrawPassConfig { + /// Create our example draw pass + fn init( + self, + _session: &mut Session, + context: &mut RenderingContext, + ) -> Result<ExampleDrawPass<'a>> { + let surface_resources = SurfaceDependentResources::new::<P>(context)?; + let draw_buffers = + match DrawBuffers::from_context(context).context("Error creating draw buffers") { + Ok(x) => x, + Err(e) => { + surface_resources.deactivate(context)?; + return Err(e); + } + }; + + Ok(ExampleDrawPass { + draw_buffers, + surface_resources, + state_ent: self.state_ent, + }) + } + + fn find_aux_queues( + _adapter: &Adapter, + _queue_negotiator: &mut QueueFamilyNegotiator, + ) -> Result<()> { + // We don't need any queues, but we'd need code to find their families here if we did. + Ok(()) + } +} + +/// Used to store resources which depend on the surface, for convenience in handle_surface_change +struct SurfaceDependentResources { + pub pipeline: CompletePipeline, + pub framebuffers: TargetSpecificResources<FramebufferT>, +} + +impl SurfaceDependentResources { + pub fn new<P: PassPosition>(context: &mut RenderingContext) -> Result<Self> { + let (pipeline, framebuffers) = { + // Our graphics pipeline + // Vulkan has a lot of config, so this is basically always going to be a big builder block + let pipeline_spec = PipelineSpecBuilder::default() + .rasterizer(Rasterizer { + polygon_mode: PolygonMode::Fill, + cull_face: Face::NONE, + front_face: FrontFace::CounterClockwise, + depth_clamping: false, + depth_bias: None, + conservative: true, + line_width: State::Static(1.0), + }) + .depth_stencil(DepthStencilDesc { + depth: None, + depth_bounds: false, + stencil: None, + }) + .blender(BlendDesc { + logic_op: Some(LogicOp::Copy), + targets: vec![ColorBlendDesc { + mask: ColorMask::ALL, + blend: Some(BlendState { + color: BlendOp::Add { + src: Factor::SrcAlpha, + dst: Factor::OneMinusSrcAlpha, + }, + alpha: BlendOp::Add { + src: Factor::SrcAlpha, + dst: Factor::OneMinusSrcAlpha, + }, + }), + }], + }) + .primitive_assembler(VertexPrimitiveAssemblerSpec::with_buffers( + InputAssemblerDesc::new(Primitive::TriangleList), + vec![VertexBufferSpec { + attributes: vec![Format::Rg32Sfloat, Format::Rgb32Sfloat], + rate: VertexInputRate::Vertex, + }], + )) + .shader_vertex(ShaderDesc { + source: include_str!("./data/shader.vert").to_string(), + entry: "main".to_string(), + kind: ShaderKind::Vertex, + }) + .shader_fragment(ShaderDesc { + source: include_str!("./data/shader.frag").to_string(), + entry: "main".to_string(), + kind: ShaderKind::Fragment, + }) + .renderpass(RenderpassSpec { + colors: vec![AttachmentSpec { + attachment: Attachment { + format: Some(context.properties().color_format), + samples: 1, + // Here we use PassPosition to get the proper operations + // Since, for example, the last pass needs to finish in present mode. + ops: P::attachment_ops(), + stencil_ops: P::attachment_ops(), + layouts: P::layout_as_range(), + }, + // This is the layout we want to deal with in our `queue_draw` function. + // It's almost certainly `Layout::ColorAttachmentOptimal` + used_layout: Layout::ColorAttachmentOptimal, + }], + depth: None, + inputs: vec![], + resolves: vec![], + preserves: vec![], + }) + .build() + .context("Error building pipeline")?; + + // Lock our device to actually build it + // Try to lock the device for as little time as possible + let mut device = context.lock_device()?; + + let pipeline = pipeline_spec + .build(&mut device, context.properties().extent, empty()) + .context("Error building pipeline")?; + + // Our framebuffers just have the swapchain framebuffer attachment + // TargetSpecificResources makes sure we use a different one each frame. + let fat = context.properties().swapchain_framebuffer_attachment(); + let framebuffers = TargetSpecificResources::new( + || unsafe { + Ok(device.create_framebuffer( + &pipeline.renderpass, + IntoIter::new([fat.clone()]), + context.properties().extent, + )?) + }, + context.properties().image_count as usize, + )?; + + (pipeline, framebuffers) + }; + + Ok(Self { + pipeline, + framebuffers, + }) + } + + pub fn deactivate(self, context: &mut RenderingContext) -> Result<()> { + unsafe { + let mut device = context.lock_device()?; + for fb in self.framebuffers.dissolve() { + device.destroy_framebuffer(fb); + } + + self.pipeline.deactivate(&mut device); + } + + Ok(()) + } +} diff --git a/examples/render-quad/src/level.rs b/examples/render-quad/src/level.rs deleted file mode 100644 index 74d1cc3..0000000 --- a/examples/render-quad/src/level.rs +++ /dev/null @@ -1,79 +0,0 @@ -use stockton_levels::parts::{ - data::{FaceRef, Geometry, TextureRef}, - HasFaces, HasTextures, HasVisData, IsFace, IsTexture, -}; -use stockton_skeleton::components::{CameraSettings, Transform}; - -pub struct DemoLevel { - pub faces: Box<[Face]>, - pub textures: Box<[Texture]>, -} - -impl DemoLevel { - fn face_idx(&self, search: &Face) -> FaceRef { - for (idx, face) in self.faces.iter().enumerate() { - if face == search { - return idx as u32; - } - } - panic!("face not in level") - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Face { - pub geometry: Geometry, - pub texture_idx: TextureRef, -} - -impl HasFaces for DemoLevel { - type Face = Face; - - fn get_face(&self, index: FaceRef) -> Option<&Self::Face> { - self.faces.get(index as usize) - } -} - -impl IsFace<DemoLevel> for Face { - fn index(&self, container: &DemoLevel) -> stockton_levels::parts::data::FaceRef { - container.face_idx(self) - } - - fn geometry(&self, _container: &DemoLevel) -> Geometry { - self.geometry.clone() - } - - fn texture_idx(&self, _container: &DemoLevel) -> TextureRef { - self.texture_idx - } -} - -pub struct Texture { - pub name: String, -} - -impl HasTextures for DemoLevel { - type Texture = Texture; - - fn get_texture(&self, idx: TextureRef) -> Option<&Self::Texture> { - self.textures.get(idx as usize) - } -} - -impl IsTexture for Texture { - fn name(&self) -> &str { - &self.name - } -} - -impl<'a> HasVisData<'a> for DemoLevel { - type Faces = std::ops::Range<FaceRef>; - - fn get_visible( - &'a self, - _transform: &Transform, - _settings: &CameraSettings, - ) -> Self::Faces { - 0..self.faces.len() as u32 - } -} diff --git a/examples/render-quad/src/main.rs b/examples/render-quad/src/main.rs index c5d52a9..1955b51 100644 --- a/examples/render-quad/src/main.rs +++ b/examples/render-quad/src/main.rs @@ -1,82 +1,29 @@ //! Renders ./example.bsp geometry: (), texture_idx: () geometry: (), texture_idx: () -#[macro_use] -extern crate stockton_input_codegen; +extern crate gfx_hal as hal; #[macro_use] extern crate legion; -use std::{ - collections::BTreeMap, - path::Path, - sync::{Arc, RwLock}, -}; - -use stockton_contrib::{delta_time::*, flycam::*}; -use stockton_input::{Axis, InputManager, Mouse}; -use stockton_levels::{ - parts::{data::{Geometry, Vertex}, FsResolver}, - types::Rgba, -}; -use stockton_render::{ - level::{LevelDrawPass, LevelDrawPassConfig}, - ui::UiDrawPass, - window::{process_window_events_system, UiState, WindowEvent, WindowFlow}, -}; -use stockton_skeleton::{ - draw_passes::ConsDrawPass, error::full_error_display, Renderer, - Session, types::*, - components::{CameraSettings, Transform}, -}; - use anyhow::{Context, Result}; -use egui::{containers::CentralPanel, Frame}; use log::warn; +use stockton_skeleton::{error::full_error_display, Renderer, Session}; use winit::{ - event::Event, + event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; -mod level; -use level::*; - -type Dp<'a> = ConsDrawPass<LevelDrawPass<'a, DemoLevel>, UiDrawPass<'a>>; +mod draw_pass; +mod system; +use draw_pass::*; +use system::*; -#[derive(InputManager, Default, Clone, Debug)] -struct MovementInputs { - #[axis] - x: Axis, - - #[axis] - y: Axis, - - #[axis] - z: Axis, -} - -impl FlycamInput for MovementInputs { - fn get_x_axis(&self) -> &Axis { - &self.x - } - fn get_y_axis(&self) -> &Axis { - &self.y - } - fn get_z_axis(&self) -> &Axis { - &self.z - } -} - -#[system] -fn hello_world(#[resource] ui: &mut UiState) { - CentralPanel::default() - .frame(Frame::none()) - .show(ui.ctx(), |ui| { - ui.heading("Hello, World!"); - }); -} +/// Alias for our drawpass +type Dp<'a> = ExampleDrawPass<'a>; fn main() { + // Wrap for full error display if let Err(err) = try_main() { eprintln!("{}", full_error_display(err)); } @@ -101,165 +48,66 @@ fn try_main() -> Result<()> { .build(&event_loop) .context("Error creating window")?; + // Grab cursor + window.set_cursor_visible(false); if window.set_cursor_grab(true).is_err() { warn!("warning: cursor not grabbed"); } - window.set_cursor_visible(false); - - // TODO: Parse the map file - let map = Arc::new(RwLock::new(DemoLevel { - faces: vec![Face { - geometry: Geometry::Vertices( - Vertex { - position: Vector3::new(-128.0, 128.0, 128.0), - tex: Vector2::new(0.0, 0.0), - color: Rgba::from_slice(&[0, 0, 0, 1]), - }, - Vertex { - position: Vector3::new(-128.0, -128.0, 128.0), - tex: Vector2::new(0.0, 1.0), - color: Rgba::from_slice(&[0, 0, 0, 1]), - }, - Vertex { - position: Vector3::new(128.0, 128.0, 128.0), - tex: Vector2::new(1.0, 0.0), - color: Rgba::from_slice(&[0, 0, 0, 1]), - }, - ), - texture_idx: 0, - }] - .into_boxed_slice(), - textures: vec![Texture { - name: "example_texture".to_string(), - }] - .into_boxed_slice(), - })); - - // Create the UI State - let ui = UiState::default(); - - // Create the input manager - let manager = { - use stockton_input::InputMutation::*; - use MovementInputsFields::*; - - let mut actions = BTreeMap::new(); - actions.insert(17, (Z, PositiveAxis)); // W - actions.insert(30, (X, NegativeAxis)); // A - actions.insert(31, (Z, NegativeAxis)); // S - actions.insert(32, (X, PositiveAxis)); // D - actions.insert(29, (Y, NegativeAxis)); // Ctrl - actions.insert(57, (Y, PositiveAxis)); // Space - - MovementInputsManager::new(actions) - }; - - // Load everything into the session + // Our game world let mut session = Session::new(move |schedule| { - schedule - .add_system(update_deltatime_system()) - .add_system(process_window_events_system::<MovementInputsManager>()) - .flush() - .add_system(hello_world_system()) - .add_system(flycam_move_system::<MovementInputsManager>()); + schedule.add_system(mutate_state_system()); }); - session.resources.insert(map.clone()); - session.resources.insert(manager); - session.resources.insert(Timing::default()); - session.resources.insert(Mouse::default()); - session.resources.insert(ui); - - // Add our player entity - let player = session.world.push(( - Transform { - position: Vector3::new(0.0, 0.0, 0.0), - rotation: Vector3::new(0.0, 0.0, 0.0), - }, - CameraSettings { - far: 1024.0, - fov: 90.0, - near: 0.1, - }, - FlycamControlled::new(512.0, 400.0), - )); + // An entity to keep track of our state + let state_ent = session.world.push((ExampleState::default(),)); // Create the renderer - let renderer = Renderer::<Dp<'static>>::new( - &window, - &mut session, - ( - LevelDrawPassConfig { - active_camera: player, - tex_resolver: FsResolver::new(Path::new("./examples/render-quad/textures"), map), - }, - (), - ), - )?; - - let new_control_flow = Arc::new(RwLock::new(ControlFlow::Poll)); - let (window_flow, tx) = WindowFlow::new(new_control_flow.clone()); - session.resources.insert(window_flow); - - // Populate the initial UI state - { - let ui = &mut session.resources.get_mut::<UiState>().unwrap(); - ui.populate_initial_state(&renderer); - } + let renderer = + Renderer::<Dp<'static>>::new(&window, &mut session, ExampleDrawPassConfig { state_ent })?; + // We'll be moving it in/out of here, so we need an Option for safety. let mut renderer = Some(renderer); // Done loading - This is our main loop. // It just communicates events to the session and continuously ticks - event_loop.run(move |event, _, flow| { - match event { - Event::MainEventsCleared => { - window.request_redraw(); - } - Event::RedrawRequested(_) => { - session.do_update(); - let r = renderer.take().unwrap(); - match r.render(&session) { - Ok(r) => { - renderer = Some(r); - } - Err(e) => { - println!("Error drawing: {}", full_error_display(e)); - - // TODO: Not really sound - *(new_control_flow.write().unwrap()) = ControlFlow::Exit; - } + event_loop.run(move |event, _, flow| match event { + Event::MainEventsCleared => { + window.request_redraw(); + } + Event::RedrawRequested(_) => { + session.do_update(); + + // Render + let r = renderer.take().unwrap(); + match r.render(&session) { + Ok(r) => { + renderer = Some(r); } - } - _ => { - if let Some(we) = WindowEvent::from(&event) { - tx.send(we).unwrap(); + Err(e) => { + println!("Error drawing: {}", full_error_display(e)); - if let WindowEvent::SizeChanged(_, _) = we { - let r = renderer.take().unwrap(); - match r.recreate_surface(&session) { - Ok(r) => { - renderer = Some(r); - } - Err(e) => { - println!("Error resizing: {}", full_error_display(e)); - - // TODO: Not really sound - *(new_control_flow.write().unwrap()) = ControlFlow::Exit; - } - } - } + *flow = ControlFlow::Exit; } } } + Event::WindowEvent { + window_id: _, + event: WindowEvent::Resized(_), + } => { + // (Attempt) resize + let r = renderer.take().unwrap(); + match r.recreate_surface(&session) { + Ok(r) => { + renderer = Some(r); + } + Err(e) => { + println!("Error resizing: {}", full_error_display(e)); - // Update the control flow if the session has requested it. - { - let new_control_flow = new_control_flow.read().unwrap(); - if *new_control_flow != *flow { - *flow = *new_control_flow; + *flow = ControlFlow::Exit; + } } - }; + } + _ => (), }); } diff --git a/examples/render-quad/src/system.rs b/examples/render-quad/src/system.rs new file mode 100644 index 0000000..3342774 --- /dev/null +++ b/examples/render-quad/src/system.rs @@ -0,0 +1,73 @@ +//! An example system that just alternates the colours of our triangle + +use stockton_skeleton::types::Vector3; + +/// RGB Channels +#[derive(Debug, Clone, Copy)] +enum ColorChannel { + Red, + Green, + Blue, +} + +/// A component for our entity. +#[derive(Debug, Clone, Copy)] +pub struct ExampleState { + channel: ColorChannel, + falling: bool, + col_val: Vector3, +} + +impl ExampleState { + pub fn color(&self) -> Vector3 { + self.col_val + } +} + +impl Default for ExampleState { + fn default() -> Self { + Self { + channel: ColorChannel::Red, + falling: true, + col_val: Vector3::new(1.0, 1.0, 1.0), + } + } +} + +/// The speed at which we change colour +const TRANSITION_SPEED: f32 = 0.1; + +/// Keep changing the colour of any ExampleStates in our world. +#[system(for_each)] +pub fn mutate_state(state: &mut ExampleState) { + // Which value we're changing + let val = match state.channel { + ColorChannel::Red => &mut state.col_val.x, + ColorChannel::Green => &mut state.col_val.y, + ColorChannel::Blue => &mut state.col_val.z, + }; + + if state.falling { + *val -= TRANSITION_SPEED; + + // Fall, then rise + if *val <= 0.0 { + *val = 0.0; + state.falling = false; + } + } else { + *val += TRANSITION_SPEED; + + if *val >= 1.0 { + *val = 1.0; + + // Rather than going back to falling, go to the next channel + state.falling = true; + state.channel = match state.channel { + ColorChannel::Red => ColorChannel::Green, + ColorChannel::Green => ColorChannel::Blue, + ColorChannel::Blue => ColorChannel::Red, + } + } + } +} |