diff --git a/assets/shaders/bezier.wgsl b/assets/shaders/bezier.wgsl new file mode 100644 index 0000000..c2e113f --- /dev/null +++ b/assets/shaders/bezier.wgsl @@ -0,0 +1,61 @@ +struct Material { + p0: vec2, + c0: vec2, + c1: vec2, + p1: vec2, + width: f32, + color: vec4, +}; + +@group(2) @binding(0) +var mat: Material; + +fn bezier(p0: vec2, c0: vec2, c1: vec2, p1: vec2, t: f32) -> vec2 { + let u = 1.0 - t; + return + u*u*u*p0 + + 3.0*u*u*t*c0 + + 3.0*u*t*t*c1 + + t*t*t*p1; +} + +fn distance_to_bezier(p: vec2) -> f32 { + var min_d = 1e9; + let steps = 32; + + var prev = mat.p0; + for (var i = 1; i <= steps; i++) { + let t = f32(i) / f32(steps); + let cur = bezier(mat.p0, mat.c0, mat.c1, mat.p1, t); + + // distance to segment + let v = cur - prev; + let w = p - prev; + let t_seg = clamp(dot(w, v) / dot(v, v), 0.0, 1.0); + let proj = prev + t_seg * v; + + min_d = min(min_d, distance(p, proj)); + prev = cur; + } + + return min_d; +} + +@fragment +fn fragment( + @location(0) world_pos: vec3, +) -> @location(0) vec4 { + let d = distance_to_bezier(world_pos.xy); + + let half_w = mat.width * 0.5; + let aa = fwidth(d); + + let alpha = smoothstep(half_w + aa, half_w - aa, d); + + if (alpha <= 0.001) { + discard; + } + + return vec4(mat.color.rgb, mat.color.a * alpha); +} + diff --git a/src/main.rs b/src/main.rs index bcd53a5..e76b459 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use crate::avian::{CharacterControllerBundle, CharacterControllerPlugin}; +use crate::{ + avian::{CharacterControllerBundle, CharacterControllerPlugin}, + shaders::BezierMaterial, +}; use avian2d::{ PhysicsPlugins, math::Vector, @@ -6,12 +9,14 @@ use avian2d::{ }; use bevy::{ camera::ScalingMode, - color::palettes::css::{GREEN, RED}, + color::palettes::css::{GREEN, RED, WHITE}, input_focus::InputFocus, prelude::*, + sprite_render::Material2dPlugin, }; pub mod avian; +pub mod shaders; const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); @@ -24,12 +29,12 @@ fn main() { // Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels. // The unit allows the engine to tune its parameters for the scale of the world, improving stability. PhysicsPlugins::default().with_length_unit(20.0), + Material2dPlugin::::default(), CharacterControllerPlugin, )) .init_resource::() - .add_systems(Startup, setup) - .add_systems(Update, debug_border) - .add_systems(Update, do_menu) + .add_systems(Startup, (setup, setup_ui)) + .add_systems(Update, (debug_border, do_menu, move_bezier)) .insert_resource(Gravity(Vector::ZERO)) .run(); } @@ -81,8 +86,41 @@ fn do_menu( } } -fn button(asset_server: &AssetServer) -> impl Bundle { - ( +fn setup(mut commands: Commands, asset_server: Res) { + let mut projection = OrthographicProjection::default_2d(); + projection.scaling_mode = ScalingMode::AutoMin { + min_width: 1920., + min_height: 1080., + }; + commands.spawn((Camera2d, Projection::Orthographic(projection))); + + // player + commands.spawn(( + Sprite::from_image(asset_server.load("player.png")), + Transform::from_xyz(0.0, 0.0, 0.0), + CharacterControllerBundle::new(Collider::capsule(15., 27.5)), + )); + + // A cube to move around + commands.spawn(( + Sprite { + color: Color::srgb(0.0, 0.4, 0.7), + custom_size: Some(Vec2::new(30.0, 30.0)), + ..default() + }, + Transform::from_xyz(50.0, -100.0, 0.0), + RigidBody::Dynamic, + Collider::rectangle(30.0, 30.0), + )); +} + +fn setup_ui( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( Node { width: percent(100), height: percent(100), @@ -116,37 +154,47 @@ fn button(asset_server: &AssetServer) -> impl Bundle { TextShadow::default(), )] )], - ) + )); + + // Curve points (world-space) + let p0 = Vec2::new(-200.0, -100.0); + let p1 = Vec2::new(200.0, 100.0); + let c0 = p0 + Vec2::new(150.0, 0.0); + let c1 = p1 + Vec2::new(-150.0, 0.0); + + // Bounding quad (must contain entire curve + width) + let center = (p0 + p1) * 0.5; + let size = Vec2::new(500.0, 300.0); + + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(size.x, size.y))), + MeshMaterial2d(materials.add(BezierMaterial { + p0, + c0, + c1, + p1, + width: 12.0, + color: WHITE.into(), + })), + Transform::from_translation(center.extend(0.0)), + )); } -fn setup(mut commands: Commands, asset_server: Res, assets: Res) { - let mut projection = OrthographicProjection::default_2d(); - projection.scaling_mode = ScalingMode::AutoMin { - min_width: 1920., - min_height: 1080., +fn move_bezier(mut materials: ResMut>, windows: Query<&Window>) { + let Ok(window) = windows.single() else { + return; + }; + let Some(mouse_pos) = window.cursor_position() else { + return; }; - commands.spawn((Camera2d, Projection::Orthographic(projection))); - // player - commands.spawn(( - Sprite::from_image(asset_server.load("player.png")), - Transform::from_xyz(0.0, 0.0, 0.0), - CharacterControllerBundle::new(Collider::capsule(15., 27.5)), - )); + for (_, mat) in materials.iter_mut() { + mat.p0 = mouse_pos; + } - // A cube to move around - commands.spawn(( - Sprite { - color: Color::srgb(0.0, 0.4, 0.7), - custom_size: Some(Vec2::new(30.0, 30.0)), - ..default() - }, - Transform::from_xyz(50.0, -100.0, 0.0), - RigidBody::Dynamic, - Collider::rectangle(30.0, 30.0), - )); - - commands.spawn(button(&assets)); + // pos: Vec2, in logical pixels + // (0, 0) is bottom-left of the window + println!("Mouse window position: {:?}", mouse_pos); } fn debug_border(mut gizmos: Gizmos) { diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs new file mode 100644 index 0000000..e49c8ea --- /dev/null +++ b/src/shaders/mod.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; +use bevy::render::render_resource::*; +use bevy::shader::ShaderRef; +use bevy::sprite_render::{AlphaMode2d, Material2d}; + +// This is the struct that will be passed to your shader +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +pub struct BezierMaterial { + #[uniform(0)] + pub p0: Vec2, + #[uniform(0)] + pub c0: Vec2, + #[uniform(0)] + pub c1: Vec2, + #[uniform(0)] + pub p1: Vec2, + #[uniform(0)] + pub width: f32, + #[uniform(0)] + pub color: LinearRgba, +} + +/// The Material2d trait is very configurable, but comes with sensible defaults for all methods. +/// You only need to implement functions for features that need non-default behavior. See the Material2d api docs for details! +impl Material2d for BezierMaterial { + fn fragment_shader() -> ShaderRef { + "shaders/bezier.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Blend + } +}