add a shader for a newline track

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Mitchell Marino 2026-05-02 22:38:12 -05:00
parent f509833262
commit db5946a7de
9 changed files with 1728 additions and 544 deletions

1861
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,5 +7,8 @@ edition = "2024"
aeronet = "0.19.0" aeronet = "0.19.0"
aeronet_replicon = { version = "0.19.0", features = ["client", "server"] } aeronet_replicon = { version = "0.19.0", features = ["client", "server"] }
avian2d = "0.5.0" avian2d = "0.5.0"
bevy = { version = "0.18.0", features = ["debug"] } bevy = { version = "0.18.1", features = ["debug"] }
bevy-inspector-egui = "0.36.0"
bevy_replicon = "0.38.2" bevy_replicon = "0.38.2"
component = "0.1.1"
rust-analyzer = "0.0.1"

29
assets/shaders/flow.wgsl Normal file
View File

@ -0,0 +1,29 @@
struct Material {
color_a: vec4<f32>,
color_b: vec4<f32>,
border_color: vec4<f32>,
time: f32,
speed: f32,
border_size: f32,
};
@group(2) @binding(0)
var<uniform> mat: Material;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
// animate
let x = uv.x + params.time * params.speed;
let g = fract(x);
let fill = mix(params.color_a, params.color_b, g);
// borders
let top = step(uv.y, params.border_size);
let bottom = step(1.0 - params.border_size, uv.y);
let border_mask = max(top, bottom);
return mix(fill, params.border_color, border_mask);
}

188
assets/shaders/track.wgsl Normal file
View File

@ -0,0 +1,188 @@
const PI: f32 = 3.14159265358979323846264338327950288;
struct Uniforms {
/// The size of the canvas
size: vec2<f32>,
/// The length of the first horizontal segment
len_start: f32,
/// The length of the last horizontal segment
len_end: f32,
/// The inner half thickness
thickness: f32,
/// The thickness of the border (added to the inner thickness for total thickness)
border: f32,
/// Current time
time: f32,
/// The period of the pulse effect (in seconds)
pulse_period: f32,
/// The speed at which the pulse travels along the path (in units per second)
pulse_speed: f32,
/// The width of the pulse effect (in units)
pulse_width: f32,
/// The color of the border
border_color: vec4<f32>,
/// The color of the base of the track
color_base: vec4<f32>,
/// The color of the pulsing
color_accent: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(0)
var<uniform> U: Uniforms;
// Distance to segment
fn sdSegment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
let pa = p - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
fn sdArc(
p: vec2<f32>,
c: vec2<f32>,
r: f32,
a0: f32,
a1: f32
) -> f32 {
let d = p - c;
var ang = atan2(d.y, d.x);
var s = a0;
var e = a1;
if (e < s) { e += 2.0 * PI; }
if (ang < s) { ang += 2.0 * PI; }
let on_arc = (ang >= s) && (ang <= e);
let circle = abs(length(d) - r);
let p0 = c + r * vec2<f32>(cos(s), sin(s));
let p1 = c + r * vec2<f32>(cos(e), sin(e));
let ends = min(length(p - p0), length(p - p1));
return select(ends, circle, on_arc);
}
// Compute SDF and normalized distance along path
fn trackSDF(p: vec2<f32>) -> vec2<f32> {
let top_offset = U.thickness + U.border;
let bottom_offset = U.size.y - top_offset;
let midline = U.size.y / 2.0;
let drop = midline - top_offset;
let radius = drop / 2.0;
// Construct points
let p0 = vec2<f32>(U.size.x - U.len_start, top_offset);
let p1 = vec2<f32>(U.size.x - radius, top_offset);
let arc0_center = vec2<f32>(p1.x, drop / 2.0 + top_offset);
let p2 = vec2<f32>(p1.x, midline);
let p3 = vec2<f32>(radius, midline);
let arc1_center = vec2<f32>(p3.x, drop / 2.0 + midline);
let p4 = vec2<f32>(radius, bottom_offset);
let p5 = vec2<f32>(U.len_end, bottom_offset);
// Total length
let L0 = length(p1 - p0);
let L1 = PI * length(p2 - p1);
let L2 = length(p3 - p2);
let L3 = PI * length(p4 - p3);
let L4 = length(p5 - p4);
let total = L0 + L1 + L2 + L3 + L4;
var best_d = 1e9;
var best_t = 0.0;
// Helper macro-like inline pattern
// segment i with accumulated offset
var accum = 0.0;
// p0->p1
{
let d = sdSegment(p, p0, p1);
let ba = p1 - p0;
let h = clamp(dot(p - p0, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L0) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L0;
}
// ARC p1->p2
{
let d = sdArc(p, arc0_center, radius, PI * 3.0 / 2.0, PI / 2.0);
let ba = p2 - p1;
let h = clamp(dot(p - p1, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L1) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L1;
}
// p2->p3 (full width)
{
let d = sdSegment(p, p2, p3);
let ba = p3 - p2;
let h = clamp(dot(p - p2, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L2) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L2;
}
// ARC p3->p4
{
let d = sdArc(p, arc1_center, radius, PI / 2.0, PI * 3.0 / 2.0);
let ba = p4 - p3;
let h = clamp(dot(p - p3, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L3) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L3;
}
// p4->p5
{
let d = sdSegment(p, p4, p5);
let ba = p5 - p4;
let h = clamp(dot(p - p4, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L4) / total;
if (d < best_d) { best_d = d; best_t = t; }
}
return vec2<f32>(best_d, best_t);
}
@fragment
fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
let p = frag_coord.xy;
let res = trackSDF(p);
let d = res.x;
let t = res.y;
// bounces back and forth between 0.0 and 1.0
let t_wrap = abs(((t + U.time) % 2.0) - 1.0);
// bounces back and forth between 0.0 and U.pulse_period
let t_wrap_pulse = abs(((t - U.pulse_speed * U.time) % (U.pulse_period * 2.0)) - U.pulse_period);
let inner = U.thickness;
let outer = U.thickness + U.border;
let aa = 1.0;
let inner_a = smoothstep(inner + aa, inner - aa, d);
let outer_a = smoothstep(outer + aa, outer - aa, d);
let border_mask = clamp(outer_a - inner_a, 0.0, 1.0);
// let color_mix = smoothstep(-0.1, 0.0, -abs(t - t_wrap_pulse));
let color_mix = smoothstep(-U.pulse_width, 0.0, -abs(t - t_wrap));
let inner_color = mix(U.color_base, U.color_accent, color_mix);
// let inner_color = mix(U.color_base, U.color_accent, d);
let color = inner_color * inner_a + U.border_color * border_mask;
let alpha = max(inner_a, outer_a);
return vec4<f32>(color.rgb, alpha);
// return vec4<f32>(d / 3000.0, t / 3000.0, 0.0, 1.0);
// return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

29
assets/shaders/uv.wgsl Normal file
View File

@ -0,0 +1,29 @@
struct Uniforms {
resolution: vec2<f32>, // viewport size (pixels)
origin: vec2<f32>, // start point (pixels)
len0: f32, // first horizontal segment (to the right)
len2: f32, // last horizontal segment (to the right)
drop: f32, // vertical drop between segments
thickness: f32, // inner half-width
border: f32, // border thickness
border_color: vec4<f32>,
color_start: vec4<f32>,
color_end: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> U: Uniforms;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) uv : vec2<f32>,
};
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let u = clamp(mesh.uv.x, 0.0, 1.0);
return vec4<f32>(mesh.position.x / 1000.0, mesh.position.y / 1000.0, 0.0, 1.0);
}

View File

@ -14,6 +14,8 @@ use bevy::{
prelude::*, prelude::*,
sprite_render::Material2dPlugin, sprite_render::Material2dPlugin,
}; };
use crate::shaders::track::{TrackMaterial, setup_track, update_track_time};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
pub mod avian; pub mod avian;
pub mod shaders; pub mod shaders;
@ -30,16 +32,20 @@ fn main() {
// The unit allows the engine to tune its parameters for the scale of the world, improving stability. // The unit allows the engine to tune its parameters for the scale of the world, improving stability.
PhysicsPlugins::default().with_length_unit(20.0), PhysicsPlugins::default().with_length_unit(20.0),
Material2dPlugin::<BezierMaterial>::default(), Material2dPlugin::<BezierMaterial>::default(),
Material2dPlugin::<TrackMaterial>::default(),
CharacterControllerPlugin, CharacterControllerPlugin,
)) ))
.add_plugins(EguiPlugin::default())
.add_plugins(WorldInspectorPlugin::new())
.init_resource::<InputFocus>() .init_resource::<InputFocus>()
.add_systems(Startup, (setup, setup_ui)) // .add_systems(Startup, (setup, setup_ui))
.add_systems(Update, (debug_border, do_menu, move_bezier)) .add_systems(Startup, (setup, setup_ui, setup_track))
.add_systems(Update, (debug_border, do_menu_interactions, move_bezier, update_track_time))
.insert_resource(Gravity(Vector::ZERO)) .insert_resource(Gravity(Vector::ZERO))
.run(); .run();
} }
fn do_menu( fn do_menu_interactions(
mut input_focus: ResMut<InputFocus>, mut input_focus: ResMut<InputFocus>,
mut interaction_query: Query< mut interaction_query: Query<
( (
@ -194,7 +200,7 @@ fn move_bezier(mut materials: ResMut<Assets<BezierMaterial>>, windows: Query<&Wi
// pos: Vec2, in logical pixels // pos: Vec2, in logical pixels
// (0, 0) is bottom-left of the window // (0, 0) is bottom-left of the window
println!("Mouse window position: {:?}", mouse_pos); // println!("Mouse window position: {:?}", mouse_pos);
} }
fn debug_border(mut gizmos: Gizmos) { fn debug_border(mut gizmos: Gizmos) {

0
src/menu.rs Normal file
View File

View File

@ -3,6 +3,8 @@ use bevy::render::render_resource::*;
use bevy::shader::ShaderRef; use bevy::shader::ShaderRef;
use bevy::sprite_render::{AlphaMode2d, Material2d}; use bevy::sprite_render::{AlphaMode2d, Material2d};
pub mod track;
// This is the struct that will be passed to your shader // This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct BezierMaterial { pub struct BezierMaterial {
@ -31,3 +33,34 @@ impl Material2d for BezierMaterial {
AlphaMode2d::Blend AlphaMode2d::Blend
} }
} }
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct FlowMaterial {
#[uniform(0)]
pub color_a: LinearRgba,
#[uniform(0)]
pub color_b: LinearRgba,
#[uniform(0)]
pub border_color: LinearRgba,
#[uniform(0)]
pub time: f32,
#[uniform(0)]
pub speed: f32,
#[uniform(0)]
pub border_size: f32,
}
/// 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 FlowMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/flow.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}

113
src/shaders/track.rs Normal file
View File

@ -0,0 +1,113 @@
use bevy::prelude::*;
use bevy::render::render_resource::*;
use bevy::shader::ShaderRef;
use bevy::sprite_render::{AlphaMode2d, Material2d};
/// The Uniform data passed to the track shader.
#[repr(C)]
#[derive(Clone, Copy, ShaderType)]
pub struct TrackUniform {
/// The size of the canvas
pub size: Vec2,
/// The length of the first horizontal segment
pub len_start: f32,
/// The length of the last horizontal segment
pub len_end: f32,
/// The inner half thickness
pub thickness: f32,
/// The thickness of the border (added to the inner thickness for total thickness)
pub border: f32,
/// Current time
pub time: f32,
/// The period of the pulse effect (in seconds)
pub pulse_period: f32,
/// The speed at which the pulse travels along the path (in units per second)
pub pulse_speed: f32,
/// The width of the pulse effect (in units)
pub pulse_width: f32,
/// The color of the border
pub border_color: LinearRgba,
/// The color of the base of the track
pub color_base: LinearRgba,
/// The color of the pulsing
pub color_accent: LinearRgba,
}
#[derive(AsBindGroup, Asset, TypePath, Clone)]
pub struct TrackMaterial {
#[uniform(0)]
pub data: TrackUniform,
}
impl Material2d for TrackMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/track.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}
pub fn setup_track(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<TrackMaterial>>,
// windows: Query<&Window>,
) {
// let window = windows.single().unwrap();
let width = 1920.0 / 2.0;
let height = 1080.0 / 2.0;
// let width = 1920.0;
// let height = 1080.0;
let material = materials.add(TrackMaterial {
data: TrackUniform {
size: Vec2::new(width, height),
len_start: 400.0,
len_end: 300.0,
thickness: 8.0,
border: 2.0,
time: 0.0,
pulse_period: 2.0,
pulse_speed: 0.5,
pulse_width: 0.5,
border_color: LinearRgba::BLACK,
color_base: LinearRgba::rgb(0.0, 0.9, 1.0),
color_accent: LinearRgba::rgb(0.8, 0.0, 0.0),
},
});
// commands.spawn(Camera2d);
// println!("Window: {}, {}", window.width(), window.height());
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(width * 2.0, height * 2.0))),
MeshMaterial2d(material),
Transform::from_translation(Vec3::new(
// window.width() * 0.5,
// window.height() * 0.5,
0.0,
0.0,
0.0,
)),
Name::new("Snake")
));
}
pub fn update_track_time(
time: Res<Time>,
mut materials: ResMut<Assets<TrackMaterial>>,
) {
for (_, mat) in materials.iter_mut() {
mat.data.time = time.elapsed_secs();
}
}