From 1bb875c972bfe1729a28d9e5a3f5965f0d7427e4 Mon Sep 17 00:00:00 2001 From: Connor Slade Date: Wed, 24 Jul 2024 15:20:07 -0400 Subject: [PATCH] Revamp model lighting --- Cargo.lock | 32 ++++++++ common/src/lib.rs | 1 + common/src/oklab.rs | 107 ++++++++++++++++++++++++++ mslicer/Cargo.toml | 3 + mslicer/src/main.rs | 7 +- mslicer/src/render/camera.rs | 8 +- mslicer/src/render/pipelines/model.rs | 8 +- mslicer/src/render/rendered_mesh.rs | 24 +++++- mslicer/src/shaders/model.wgsl | 15 +++- mslicer/src/windows/models.rs | 10 +++ mslicer/src/windows/top_bar.rs | 8 +- slicer/src/segments.rs | 4 + 12 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 common/src/oklab.rs diff --git a/Cargo.lock b/Cargo.lock index fe9e875..ca9d3a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1157,6 +1157,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2 1.0.85", + "quote 1.0.36", + "unicode-xid 0.2.4", +] + [[package]] name = "const_panic" version = "0.2.8" @@ -1456,6 +1476,15 @@ dependencies = [ "nohash-hasher", ] +[[package]] +name = "egui-phosphor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c873785789bc94a14dcbbd361cda1e498957fa1b4ec389ba650fd651849463b" +dependencies = [ + "egui", +] + [[package]] name = "egui-wgpu" version = "0.27.2" @@ -2678,8 +2707,10 @@ dependencies = [ "bytemuck", "clone-macro", "common", + "const_format", "eframe", "egui", + "egui-phosphor", "egui-wgpu", "egui_dock", "encase", @@ -2688,6 +2719,7 @@ dependencies = [ "nalgebra 0.32.6", "parking_lot", "plexus", + "rand 0.8.5", "rfd", "slicer", "tracing", diff --git a/common/src/lib.rs b/common/src/lib.rs index 9e3548d..39f7f51 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,4 +1,5 @@ pub mod config; pub mod image; pub mod misc; +pub mod oklab; pub mod serde; diff --git a/common/src/oklab.rs b/common/src/oklab.rs new file mode 100644 index 0000000..69c393b --- /dev/null +++ b/common/src/oklab.rs @@ -0,0 +1,107 @@ +use std::f32::consts::PI; + +#[derive(Debug, Clone, Copy)] +pub struct OkLab { + pub l: T, + pub a: T, + pub b: T, +} + +#[derive(Debug, Clone, Copy)] +pub struct Rgb { + pub r: T, + pub g: T, + pub b: T, +} + +/// A good starting color for hue shifting +pub const START_COLOR: OkLab = OkLab { + l: 0.773, + a: 0.1131, + b: 0.0, +}; + +impl OkLab { + pub fn new(l: f32, a: f32, b: f32) -> Self { + OkLab { l, a, b } + } + + pub fn to_srgb(&self) -> Rgb { + oklab_to_linear_srgb(*self) + } + + pub fn from_srgb(c: Rgb) -> Self { + linear_srgb_to_oklab(c) + } + + pub fn to_lrgb(&self) -> Rgb { + let srgb = self.to_srgb(); + Rgb { + r: (to_gamma(srgb.r) * 255.0).round() as u8, + g: (to_gamma(srgb.g) * 255.0).round() as u8, + b: (to_gamma(srgb.b) * 255.0).round() as u8, + } + } + + pub fn hue_shift(&self, shift: f32) -> Self { + let hue = self.b.atan2(self.a); + let chroma = (self.a * self.a + self.b * self.b).sqrt(); + + let hue = (hue + shift) % (2.0 * PI); + + let a = chroma * hue.cos(); + let b = chroma * hue.sin(); + + Self { l: self.l, a, b } + } +} + +impl Rgb { + pub fn map U>(self, f: F) -> Rgb { + Rgb { + r: f(self.r), + g: f(self.g), + b: f(self.b), + } + } +} + +pub fn linear_srgb_to_oklab(c: Rgb) -> OkLab { + let l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b; + let m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b; + let s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b; + + let l_ = l.cbrt(); + let m_ = m.cbrt(); + let s_ = s.cbrt(); + + OkLab { + l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_, + } +} + +pub fn oklab_to_linear_srgb(c: OkLab) -> Rgb { + let l_ = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b; + let m_ = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b; + let s_ = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b; + + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + + Rgb { + r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, + } +} + +fn to_gamma(u: f32) -> f32 { + if u >= 0.0031308 { + (1.055) * u.powf(1.0 / 2.4) - 0.055 + } else { + 12.92 * u + } +} diff --git a/mslicer/Cargo.toml b/mslicer/Cargo.toml index b0c7c08..de03f3d 100644 --- a/mslicer/Cargo.toml +++ b/mslicer/Cargo.toml @@ -7,15 +7,18 @@ edition = "2021" anyhow = "1.0.86" bytemuck = "1.16.1" clone-macro = "0.1.0" +const_format = "0.2.32" eframe = { version = "0.27.2", features = ["wgpu"] } egui = "0.27.2" egui_dock = "0.12.0" +egui-phosphor = "0.5.0" egui-wgpu = "0.27.2" encase = { version = "0.8.0", features = ["nalgebra"] } image = "0.25.1" nalgebra = "0.32.6" parking_lot = "0.12.3" plexus = "0.0.11" +rand = "0.8.5" rfd = "0.14.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/mslicer/src/main.rs b/mslicer/src/main.rs index f41a7f8..c3a6858 100644 --- a/mslicer/src/main.rs +++ b/mslicer/src/main.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::Result; use eframe::NativeOptions; -use egui::{IconData, Vec2, ViewportBuilder}; +use egui::{FontDefinitions, IconData, Vec2, ViewportBuilder}; use egui_wgpu::WgpuConfiguration; use tracing::level_filters::LevelFilter; use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt}; @@ -56,6 +56,11 @@ fn main() -> Result<()> { }, Box::new(|cc| { render::init_wgpu(cc); + + let mut fonts = FontDefinitions::default(); + egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); + cc.egui_ctx.set_fonts(fonts); + Box::new(App::default()) }), ) diff --git a/mslicer/src/render/camera.rs b/mslicer/src/render/camera.rs index fa29a0e..2bd72f2 100644 --- a/mslicer/src/render/camera.rs +++ b/mslicer/src/render/camera.rs @@ -35,9 +35,9 @@ impl Camera { } if response.dragged_by(PointerButton::Secondary) { - self.target -= - self.position().neg().normalize().cross(&Vector3::z_axis()) * drag_delta.x; - self.target += Vector3::new(0.0, 0.0, drag_delta.y * 0.5); + let facing = self.position().neg().normalize(); + self.target -= facing.cross(&Vector3::z_axis()) * drag_delta.x; + self.target += facing.cross(&Vector3::x_axis()) * drag_delta.y; } if response.hovered() { @@ -46,7 +46,7 @@ impl Camera { } } - fn position(&self) -> Vector3 { + pub fn position(&self) -> Vector3 { Vector3::new(self.angle.x.sin(), self.angle.x.cos(), self.angle.y.tan()).normalize() * self.distance } diff --git a/mslicer/src/render/pipelines/model.rs b/mslicer/src/render/pipelines/model.rs index 36f182e..78f1c67 100644 --- a/mslicer/src/render/pipelines/model.rs +++ b/mslicer/src/render/pipelines/model.rs @@ -1,6 +1,6 @@ use egui_wgpu::ScreenDescriptor; use encase::{ShaderType, UniformBuffer}; -use nalgebra::Matrix4; +use nalgebra::{Matrix4, Vector3, Vector4}; use wgpu::{ util::{BufferInitDescriptor, DeviceExt}, BindGroup, BindGroupEntry, BindGroupLayout, BufferUsages, ColorTargetState, ColorWrites, @@ -31,6 +31,9 @@ pub struct ModelPipeline { struct ModelUniforms { transform: Matrix4, model_transform: Matrix4, + model_color: Vector4, + camera_position: Vector3, + camera_target: Vector3, render_style: u32, } @@ -114,6 +117,9 @@ impl Pipeline for ModelPipeline { let uniforms = ModelUniforms { transform: resources.transform * model_transform, model_transform, + model_color: model.color.to_normalized_gamma_f32().into(), + camera_position: resources.camera.position(), + camera_target: resources.camera.target, render_style: resources.render_style as u32, }; diff --git a/mslicer/src/render/rendered_mesh.rs b/mslicer/src/render/rendered_mesh.rs index cef472a..39b5685 100644 --- a/mslicer/src/render/rendered_mesh.rs +++ b/mslicer/src/render/rendered_mesh.rs @@ -1,5 +1,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; +use common::oklab::START_COLOR; +use egui::Color32; use nalgebra::Vector3; use wgpu::{ util::{BufferInitDescriptor, DeviceExt}, @@ -14,6 +16,7 @@ pub struct RenderedMesh { pub name: String, pub id: u32, pub mesh: Mesh, + pub color: Color32, pub hidden: bool, pub locked_scale: bool, @@ -58,9 +61,10 @@ impl RenderedMesh { } Self { - mesh, - id: next_id(), name: String::new(), + id: next_id(), + mesh, + color: Color32::WHITE, hidden: false, locked_scale: true, vertices, @@ -73,6 +77,21 @@ impl RenderedMesh { self } + pub fn with_random_color(mut self) -> Self { + self.randomize_color(); + self + } + + pub fn randomize_color(&mut self) -> &mut Self { + let shift = rand::random::() * std::f32::consts::PI; + let color = START_COLOR + .hue_shift(shift) + .to_srgb() + .map(|x| (x.clamp(0.0, 1.0) * 255.0) as u8); + self.color = Color32::from_rgb(color.r, color.g, color.b); + self + } + pub fn align_to_bed(&mut self) { let (bottom, _) = self.mesh.minmax_point(); @@ -116,6 +135,7 @@ impl Clone for RenderedMesh { name: self.name.clone(), id: next_id(), mesh: self.mesh.clone(), + color: self.color, hidden: self.hidden, locked_scale: self.locked_scale, vertices: self.vertices.clone(), diff --git a/mslicer/src/shaders/model.wgsl b/mslicer/src/shaders/model.wgsl index 6a7d9b0..4812b77 100644 --- a/mslicer/src/shaders/model.wgsl +++ b/mslicer/src/shaders/model.wgsl @@ -3,6 +3,9 @@ struct Context { transform: mat4x4, model_transform: mat4x4, + model_color: vec4, + camera_position: vec3, + camera_target: vec3, render_style: u32, } @@ -36,8 +39,14 @@ fn frag(in: VertexOutput) -> @location(0) vec4 { if context.render_style == 0 { return vec4(in.normal, 1.0); } else { - let light_dir = normalize(vec3(0.0, 0.0, 1.0)); - let intensity = max(dot(in.normal, light_dir), 0.0); - return vec4(intensity, intensity, intensity, 1.0); + let camera_direction = normalize(context.camera_position + context.camera_target); + + let diffuse = max(dot(in.normal, camera_direction), 0.0); + + let reflect_dir = reflect(-camera_direction, in.normal); + let specular = pow(max(dot(camera_direction, reflect_dir), 0.0), 32.0); + + let intensity = (diffuse + specular + 0.03) * context.model_color.rgb; + return vec4(intensity, context.model_color.a); } } \ No newline at end of file diff --git a/mslicer/src/windows/models.rs b/mslicer/src/windows/models.rs index 018449d..b40cdc3 100644 --- a/mslicer/src/windows/models.rs +++ b/mslicer/src/windows/models.rs @@ -1,4 +1,6 @@ +use const_format::concatcp; use egui::{Context, Grid, Id, Ui}; +use egui_phosphor::regular::DICE_THREE; use slicer::Pos; use crate::{ @@ -106,6 +108,14 @@ pub fn ui(app: &mut App, ui: &mut Ui, _ctx: &Context) { .then(|| mesh.mesh.set_rotation(deg_to_rad(rotation))); ui.end_row(); + ui.label("Color"); + ui.horizontal(|ui| { + ui.color_edit_button_srgba(&mut mesh.color); + ui.button(concatcp!(DICE_THREE, " Random")) + .clicked() + .then(|| mesh.randomize_color()); + }); + ui.label("Name"); ui.text_edit_singleline(&mut mesh.name); ui.end_row(); diff --git a/mslicer/src/windows/top_bar.rs b/mslicer/src/windows/top_bar.rs index 5f000e2..70b2eb8 100644 --- a/mslicer/src/windows/top_bar.rs +++ b/mslicer/src/windows/top_bar.rs @@ -31,9 +31,11 @@ pub fn ui(app: &mut App, ctx: &Context) { let model = slicer::mesh::load_mesh(&mut buf, &format).unwrap(); info!("Loaded model `{name}` with {} faces", model.faces.len()); - app.meshes - .write() - .push(RenderedMesh::from_mesh(model).with_name(name)); + app.meshes.write().push( + RenderedMesh::from_mesh(model) + .with_name(name) + .with_random_color(), + ); } } diff --git a/slicer/src/segments.rs b/slicer/src/segments.rs index 3c36d0f..23ab85a 100644 --- a/slicer/src/segments.rs +++ b/slicer/src/segments.rs @@ -57,6 +57,10 @@ impl Segments { let mut out = Vec::new(); let layer = (height - self.start_height) / self.layer_height; + if layer < 0.0 || layer >= self.layers.len() as f32 { + return out; + } + for &face in self.layers[layer as usize].iter() { intersect_triangle(mesh, &self.transformed_points, face, height, &mut out); }