render sliders

This commit is contained in:
Michael Zhang 2021-01-08 01:21:39 -06:00
parent ca6a819267
commit 74d63a216e
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
53 changed files with 441295 additions and 13 deletions

6
Cargo.lock generated
View file

@ -558,6 +558,7 @@ dependencies = [
"ggez",
"libosu",
"log",
"num",
"stderrlog",
]
@ -1130,9 +1131,8 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "libosu"
version = "0.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1211582bccd34afcda71e9a99d542d6880750b6c04f5506989a7042c5f06284"
version = "0.0.12"
source = "git+https://github.com/iptq/libosu?rev=7acc09af59b789f78c21c259d4e1e246e9cf0b08#7acc09af59b789f78c21c259d4e1e246e9cf0b08"
dependencies = [
"anyhow",
"bitflags",

View file

@ -13,6 +13,10 @@ members = [
anyhow = "1.0.37"
bass-sys = { path = "bass-sys" }
ggez = "0.5.1"
libosu = "0.0.11"
log = "0.4.11"
stderrlog = "0.5.0"
num = "0.3.1"
[dependencies.libosu]
git = "https://github.com/iptq/libosu"
rev = "7acc09af59b789f78c21c259d4e1e246e9cf0b08"

2
bass-sys/.ignore Normal file
View file

@ -0,0 +1,2 @@
/linux
/win

View file

@ -1,8 +1,21 @@
use std::env;
use std::path::PathBuf;
fn main() {
#[cfg(target_os = "linux")]
fn load_bass() {
let mut project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
project_dir.push("linux");
project_dir.push("bass24");
project_dir.push("x64");
println!("cargo:rustc-link-search={}", project_dir.to_str().unwrap());
println!("cargo:rustc-link-lib=bass");
}
#[cfg(target_os = "windows")]
fn load_bass() {
let mut project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
project_dir.push("win");
project_dir.push("bass24");
project_dir.push("c");
project_dir.push("x64");
@ -10,3 +23,7 @@ fn main() {
println!("cargo:rustc-link-search={}", project_dir.to_str().unwrap());
println!("cargo:rustc-link-lib=bass");
}
fn main() {
load_bass();
}

View file

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
run.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
export LD_LIBRARY_PATH=$(pwd)/bass-sys/linux/bass24/x64
echo $LD_LIBRARY_PATH
exec cargo run "$@"

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
nightly

View file

@ -5,13 +5,15 @@ use std::path::Path;
use anyhow::Result;
use ggez::{
event::{EventHandler, KeyCode, KeyMods},
graphics::{self, DrawMode, DrawParam, FillOptions, StrokeOptions, FilterMode, Mesh, Text, WHITE},
nalgebra::Point2,
graphics::{
self, DrawMode, DrawParam, FillOptions, FilterMode, Mesh, Rect, StrokeOptions, Text, WHITE,
},
Context, GameError, GameResult,
};
use libosu::{Beatmap, HitObject};
use libosu::{Beatmap, HitObject, HitObjectKind};
use crate::audio::{AudioEngine, Sound};
use crate::slider_render::render_slider;
pub struct Game {
is_playing: bool,
@ -26,6 +28,7 @@ impl Game {
let audio_engine = AudioEngine::new()?;
let beatmap = Beatmap::default();
let hit_objects = Vec::new();
Ok(Game {
is_playing: false,
audio_engine,
@ -60,6 +63,9 @@ impl Game {
}
fn priv_draw(&mut self, ctx: &mut Context) -> Result<()> {
// TODO: lol
const EDITOR_SCREEN: Rect = Rect::new(112.0, 84.0, 800.0, 600.0);
graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into());
let time = self.song.as_ref().unwrap().position()?;
@ -76,15 +82,42 @@ impl Game {
}
}
info!("# hitobjects: {} / {}", visible_hitobjects.len(), self.beatmap.hit_objects.len());
let osupx_scale_x = EDITOR_SCREEN.w / 512.0;
let osupx_scale_y = EDITOR_SCREEN.h / 384.0;
let cs_osupx = 54.4 - 4.48 * self.beatmap.difficulty.circle_size;
let cs_real = cs_osupx * osupx_scale_x;
for ho in visible_hitobjects.iter() {
let ho_time = (ho.start_time.0 as f64) / 1000.0;
let circ = Mesh::new_circle(ctx, DrawMode::Fill(FillOptions::default()), [ho.pos.0 as f32, ho.pos.1 as f32], 10.0, 1.0, WHITE)?;
let pos = [
EDITOR_SCREEN.x + osupx_scale_x * ho.pos.0 as f32,
EDITOR_SCREEN.y + osupx_scale_y * ho.pos.1 as f32,
];
if let HitObjectKind::Slider(_) = ho.kind {
render_slider(ctx, EDITOR_SCREEN, &self.beatmap, ho)?;
}
let circ = Mesh::new_circle(
ctx,
DrawMode::Fill(FillOptions::default()),
pos,
cs_real,
1.0,
WHITE,
)?;
graphics::draw(ctx, &circ, DrawParam::default())?;
let time_diff = ho_time - time;
let approach_r = 10.0 * (1.0 + 2.0 * time_diff as f32 / 0.75);
let approach = Mesh::new_circle(ctx, DrawMode::Stroke(StrokeOptions::default()), [ho.pos.0 as f32, ho.pos.1 as f32], approach_r, 1.0, WHITE)?;
let approach_r = cs_real * (1.0 + 2.0 * time_diff as f32 / 0.75);
let approach = Mesh::new_circle(
ctx,
DrawMode::Stroke(StrokeOptions::default()),
pos,
approach_r,
1.0,
WHITE,
)?;
graphics::draw(ctx, &approach, DrawParam::default())?;
}

View file

@ -1,3 +1,5 @@
#![feature(vec_into_raw_parts)]
#[macro_use]
extern crate anyhow;
#[macro_use]
@ -6,6 +8,10 @@ extern crate bass_sys as bass;
mod audio;
mod game;
mod math;
mod slider_render;
use std::env;
use anyhow::Result;
use ggez::{
@ -29,7 +35,8 @@ fn main() -> Result<()> {
let (ctx, event_loop) = &mut cb.build()?;
let mut game = Game::new()?;
game.load_beatmap("happy-time/Nanamori-chu Goraku-bu - Happy Time wa Owaranai (Cut Ver.) (-Keitaro) [Osu's Expert].osu")?;
let path = env::args().nth(1).unwrap();
game.load_beatmap(path)?;
event::run(ctx, event_loop, &mut game)?;
Ok(())

35
src/math.rs Normal file
View file

@ -0,0 +1,35 @@
use std::marker::PhantomData;
use libosu::Point;
use num::Float;
#[derive(Default)]
pub struct Math<T>(PhantomData<T>);
impl<T: Float> Math<T> {
pub fn circumcircle(p1: Point<T>, p2: Point<T>, p3: Point<T>) -> (Point<T>, T) {
let (x1, y1) = (p1.0, p1.1);
let (x2, y2) = (p2.0, p2.1);
let (x3, y3) = (p3.0, p3.1);
let two = num::cast::<_, T>(2.0).unwrap();
let d = two.mul_add(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2), T::zero());
let ux = ((x1 * x1 + y1 * y1) * (y2 - y3)
+ (x2 * x2 + y2 * y2) * (y3 - y1)
+ (x3 * x3 + y3 * y3) * (y1 - y2))
/ d;
let uy = ((x1 * x1 + y1 * y1) * (x3 - x2)
+ (x2 * x2 + y2 * y2) * (x1 - x3)
+ (x3 * x3 + y3 * y3) * (x2 - x1))
/ d;
let center = Point(ux, uy);
(center, Self::distance(center, p1))
}
pub fn distance(p1: Point<T>, p2: Point<T>) -> T {
let dx = p2.0 - p1.0;
let dy = p2.1 - p1.1;
(dx * dx + dy * dy).sqrt()
}
}

228
src/slider_render.rs Normal file
View file

@ -0,0 +1,228 @@
use std::collections::{HashMap, VecDeque};
use std::mem;
use std::sync::Arc;
use anyhow::Result;
use ggez::{
conf::NumSamples,
graphics::{
self, Canvas, Color, DrawMode, DrawParam, LineCap, LineJoin, Mesh, Rect, StrokeOptions,
},
nalgebra::Point2,
Context,
};
use libosu::{Beatmap, HitObject, HitObjectKind, Point, SliderSplineKind};
use crate::math::Math;
pub fn render_slider(
ctx: &mut Context,
rect: Rect,
beatmap: &Beatmap,
slider: &HitObject,
) -> Result<()> {
let mut control_points = vec![slider.pos];
let slider_info = match &slider.kind {
HitObjectKind::Slider(info) => info,
_ => panic!("retard"),
};
control_points.extend(&slider_info.control);
let spline = get_spline(&slider_info.kind, &control_points, slider_info.pixel_length);
let osupx_scale_x = rect.w as f64 / 512.0;
let osupx_scale_y = rect.h as f64 / 384.0;
let cs_osupx = 54.4 - 4.48 * beatmap.difficulty.circle_size as f64;
let cs_real = cs_osupx * osupx_scale_x;
let (mut boundx, mut boundy, mut boundw, mut boundh) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
let spline_mapped = spline
.iter()
.map(|point| {
let (x, y) = (point.0, point.1);
boundx = boundx.min(x - cs_osupx);
boundy = boundy.min(y - cs_osupx);
boundw = boundw.max(x + cs_osupx - boundx);
boundh = boundh.max(y + cs_osupx - boundy);
let x2 = rect.x as f64 + osupx_scale_x * x;
let y2 = rect.y as f64 + osupx_scale_y * y;
[x2 as f32, y2 as f32].into()
})
.collect::<Vec<Point2<f32>>>();
let opts = StrokeOptions::default()
.with_line_cap(LineCap::Round)
.with_line_join(LineJoin::Round)
.with_line_width(cs_real as f32 * 2.0);
let mesh = Mesh::new_polyline(ctx, DrawMode::Stroke(opts), &spline_mapped, graphics::WHITE)?;
graphics::draw(ctx, &mesh, DrawParam::default())?;
Ok(())
}
fn get_spline(
kind: &SliderSplineKind,
control_points: &[Point<i32>],
pixel_length: f64,
) -> Vec<Point<f64>> {
// no matter what, if there's 2 control points, it's linear
let mut kind = kind.clone();
if control_points.len() == 2 {
kind = SliderSplineKind::Linear;
}
let points = control_points
.iter()
.map(|p| Point(p.0 as f64, p.1 as f64))
.collect();
match kind {
SliderSplineKind::Linear => points,
SliderSplineKind::Perfect => {
let (p1, p2, p3) = (points[0], points[1], points[2]);
let (center, radius) = Math::circumcircle(p1, p2, p3);
// find the t-values of the start and end of the slider
let t0 = (center.1 - p1.1).atan2(p1.0 - center.0);
let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0);
// make sure t0 is less than t1
let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0);
while mid < t0 {
mid += 2.0 * std::f64::consts::PI
}
while t1 < mid {
t1 += 2.0 * std::f64::consts::PI
}
// circumference is 2*pi*r, slider length over circumference is length/(2*pi*r)
let direction_unit = (t1 - t0) / (t1 - t0).abs();
let new_t1 = t0 + direction_unit * (pixel_length / radius);
let mut t = t0;
let mut c = Vec::new();
loop {
if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) {
break;
}
let rel = Point(t.cos() * radius, -t.sin() * radius);
c.push(center + rel);
t += (new_t1 - t0) / pixel_length;
}
c
}
SliderSplineKind::Bezier => {
let mut idx = 0;
let mut whole = Vec::new();
for i in 1..points.len() {
if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 {
let spline = calculate_bezier(&points[idx..i - 1]);
whole.extend(spline);
idx = i;
continue;
}
}
let spline = calculate_bezier(&points[idx..]);
whole.extend(spline);
whole
}
_ => todo!(),
}
}
type P = Point<f64>;
type V<T> = (*mut T, usize, usize);
fn calculate_bezier(points: &[P]) -> Vec<P> {
let points = points.to_vec();
let mut output = Vec::new();
let n = points.len() - 1;
let last = points[n];
let mut to_flatten = VecDeque::new();
let mut free_buffers = VecDeque::new();
to_flatten.push_back(points.into_raw_parts());
let mut p = n;
let buf1 = vec![Point(0.0, 0.0); p + 1].into_raw_parts();
let buf2 = vec![Point(0.0, 0.0); p * 2 + 1].into_raw_parts();
let left_child = buf2;
while !to_flatten.is_empty() {
let parent = to_flatten.pop_front().unwrap();
let parent_slice = unsafe { std::slice::from_raw_parts_mut(parent.0, parent.1) };
if bezier_flat_enough(parent_slice) {
bezier_approximate(parent_slice, &mut output, buf1, buf2, p + 1);
free_buffers.push_front(parent);
continue;
}
let right_child = if free_buffers.is_empty() {
let buf = vec![Point(0.0, 0.0); p + 1];
buf.into_raw_parts()
} else {
free_buffers.pop_front().unwrap()
};
bezier_subdivide(parent_slice, left_child, right_child, buf1, p + 1);
let left_child = unsafe { std::slice::from_raw_parts(left_child.0, left_child.1) };
for i in 0..p + 1 {
parent_slice[i] = left_child[i];
}
to_flatten.push_front(right_child);
to_flatten.push_front(parent);
}
output.push(last);
output
}
const TOLERANCE: f64 = 0.25;
fn bezier_flat_enough(curve: &[P]) -> bool {
for i in 1..(curve.len() - 1) {
let p = curve[i - 1] - curve[i] * 2.0 + curve[i + 1];
if p.0 * p.0 + p.1 * p.1 > TOLERANCE * TOLERANCE / 4.0 {
return false;
}
}
true
}
fn bezier_approximate(curve: &[P], output: &mut Vec<P>, buf1: V<P>, buf2: V<P>, count: usize) {
let l = buf2;
let r = buf1;
bezier_subdivide(curve, l, r, buf1, count);
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
for i in 0..(count - 1) {
l[count + i] = r[i + 1];
}
output.push(curve[0]);
for i in 1..(count - 1) {
let idx = 2 * i;
let p = (l[idx - 1] + l[idx] * 2.0 + l[idx + 1]) * 0.25;
output.push(p);
}
}
fn bezier_subdivide(curve: &[P], l: V<P>, r: V<P>, subdiv: V<P>, count: usize) {
let midpoints = unsafe { std::slice::from_raw_parts_mut(subdiv.0, subdiv.1) };
for i in 0..count {
midpoints[i] = curve[i];
}
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
for i in 0..count {
l[i] = midpoints[0];
r[count - i - 1] = midpoints[count - i - 1];
for j in 0..(count - i - 1) {
midpoints[j] = (midpoints[j] + midpoints[j + 1]) * 0.5;
}
}
}