#![feature(clamp, slice_patterns, box_syntax, const_fn, drain_filter, cell_update)]
#![allow(unused)]
#![warn(unused_imports)]
#[macro_use]
extern crate glium;
use std::cell::{Cell, RefCell};
use std::convert::TryInto;
use std::fs::{create_dir_all, File};
use std::io::Read;
use std::ops::Sub;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::sleep;
use std::time::{Duration, Instant};
use float_pretty_print::PrettyPrintFloat;
use glium::{BackfaceCullingMode, DepthTest, Display, DrawError, DrawParameters, Program, Surface, VertexBuffer};
use glium::backend::glutin::glutin::platform::desktop::EventLoopExtDesktop;
use glium::glutin::{ContextBuilder, GlProfile, GlRequest};
use glium::glutin::Api::OpenGl;
use glium::glutin::dpi::LogicalSize;
use glium::glutin::event::{Event, WindowEvent};
use glium::glutin::event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget};
use glium::glutin::window::WindowBuilder;
use glium::index::IndicesSource::NoIndices;
use glium::index::PrimitiveType;
use glium::uniforms::UniformsStorage;
use itertools::Itertools;
use log::{info, LevelFilter, warn};
use simplelog::{CombinedLogger, Config, ConfigBuilder, TerminalMode, TermLogger, WriteLogger};
use crate::cam::Camera;
use crate::cmd::{command, CommandDispatcher, ConsoleEngine, ConVar, EngineBuilder, ExecSource, expand_command, SimpleExecutor};
use crate::font::{bdf, FontRenderer};
use crate::font::bdf::draw::BakedFont;
use crate::font::multifont::{Multifont, Selector};
use crate::i18n::I18n;
use crate::kb::Keyboard;
use crate::math::*;
use crate::res::{DataSource, ResourceLoader};
use crate::res::merge::MergeModeTable;
use crate::shader::ShaderManager;
use crate::testui::TestUiController;
use crate::ui::graphics::{DrawPreview, Graphics, StringDrawProps};
use crate::ui::host::UiHost;
use crate::ui::settings::UiSettings;
use crate::util::{AnySurface, apply, Color, LogPipe};
mod cam;
mod cmd;
mod font;
mod i18n;
mod kb;
mod math;
mod res;
mod shader;
mod tex;
mod util;
#[macro_use]
mod ui;
mod testui;
const GAME_NAME: &str = "Untitled Game";
const GAME_VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
let mut data_dir: Option<Vec<_>> = None;
let mut save_dir = None;
let mut v_sync = false;
let mut language = None;
let mut init_script = None;
let mut data_merge = MergeModeTable::new();
{
let mut cd = CommandDispatcher::new(SimpleExecutor::new(|cmd, args| {
match cmd {
"data_dir" => data_dir = Some(args.iter().map(|el| PathBuf::from(el)).collect()),
"save_dir" => save_dir = Some(PathBuf::from(args[0])),
"v_sync" => v_sync = args[0].parse::<i32>().unwrap_or(0) != 0,
"language" => language = Some(args.iter().map(|&s| s.to_owned()).collect::<Vec<_>>()),
"data_merge_mode" => data_merge.add_entry(args[0], args[1].try_into().unwrap()),
"exec_init" => init_script = Some(PathBuf::from(args[0])),
_ => eprintln!("Ignoring invalid bootstrap command '{}'", cmd),
}
}));
cd.scheduler().exec_path("boot.cfg", ExecSource::Event);
cd.resume_until_empty();
}
let data_dir = data_dir.expect("data_dir was not set!");
let save_dir = save_dir.expect("save_dir was not set!");
let language = language.expect("language was not set!");
let init_script = init_script.expect("exec_init was not set!");
create_dir_all(&save_dir).expect("Could not create save directory.");
let logfile = File::create(save_dir.join("system.log")).expect("Failed to create log file.");
CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed).unwrap(),
WriteLogger::new(LevelFilter::Debug, ConfigBuilder::new().set_time_format_str("%Y-%m-%d %H:%M:%S").build(), logfile),
]).unwrap();
info!("Data directory: {}", data_dir.iter().map(|el| format!("'{}'", el.to_string_lossy())).join(" "));
info!("Save directory: '{}'", save_dir.as_os_str().to_string_lossy());
let sources = data_dir.iter().map(|el| {
let is_res_file = el.extension().and_then(|s| s.to_str()).map_or(false, |s| s == "res");
if is_res_file { DataSource::Archive(el.clone()) } else { DataSource::Filesystem(el.clone()) }
}).collect();
let rl = ResourceLoader::new(data_merge, sources);
let i18n = language.into_iter()
.map(|p| I18n::parse_from_res(&rl, &format!("strs/{}.str", p)).unwrap())
.fold(I18n::new(), |acc, a| acc.merge(a));
start_game(Environment { rl, i18n, data_dir, save_dir, v_sync, init_script });
}
fn start_game(env: Environment) -> ! {
let env = Rc::new(env);
let title = format!("{} v{} {}", GAME_NAME, GAME_VERSION, if cfg!(debug_assertions) { "Debug" } else { "Release" });
let mut event_loop = EventLoop::new();
let display = glium::Display::new(
WindowBuilder::new()
.with_title(&title)
.with_visible(true)
.with_inner_size(LogicalSize::new(640.0, 480.0)),
ContextBuilder::new()
.with_gl(GlRequest::Specific(OpenGl, (4, 0)))
.with_gl_profile(GlProfile::Core)
.with_vsync(env.v_sync),
&event_loop,
).expect("Failed to create window");
let gl_window = display.gl_window();
let gl_window: &glium::glutin::window::Window = gl_window.window();
let running = Arc::new(AtomicBool::new(true));
let ui_settings = Rc::new(Cell::new(UiSettings::default()));
let mut ce = {
fn parse_color(args: &[&str]) -> Color {
let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
Color::from_rgb8(r, g, b)
}
let env = env.clone();
let running = running.clone();
EngineBuilder::new(LogPipe::info())
.with_default()
.with_cvar("test_var", ConVar::of_num(10.0, Some(0.0), Some(20.0)))
.with_help_text("test_var", "a test variable")
.with_cvar("string_test", ConVar::of_string("yeah"))
.with_command("quit", box command(move |_, _, _| { running.store(false, Ordering::Relaxed); }))
.with_help_text("quit", "quit the game")
.with_cvar("fps_limit", ConVar::of_int(120, Some(0), None))
.with_cvar("debug_info", ConVar::of_bool(false))
.with_help_text("debug_info", "whether to show debug info like fps and player position")
.with_command("ui_col_border", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.border_color = parse_color(args))); })
})
.with_command("ui_col_normal", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.normal_color = parse_color(args))); })
})
.with_command("ui_col_hilight", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.highlighted_color = parse_color(args))); })
})
.with_command("ui_col_act", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.active_color = parse_color(args))); })
})
.with_command("ui_col_disabled", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.disabled_color = parse_color(args))); })
})
.with_command("ui_col_query", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.query_text_color = parse_color(args))); })
})
.with_command("ui_col_text", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.text_color = parse_color(args))); })
})
.with_command("ui_col_tsel", {
let ui_settings = ui_settings.clone();
box command(move |args, _, _| { ui_settings.update(|s| apply(s, |s| s.text_selected_background_color = parse_color(args))); })
})
.with_command("exec", box expand_command(move |args, _, out| {
let file = format!("cfg/{}.cfg", *args.get(0)?);
let script = std::fs::read_to_string(env.save_dir.join(&file)).ok()
.or(env.rl.open(&file).ok().and_then(|mut it| {
let mut script = String::new();
it.read_to_string(&mut script).ok().map(|_| script)
}));
if script.is_none() {
writeln!(out, "Script not found: '{}'", &file).unwrap();
}
script
}))
.with_help_text("exec", "execute a script")
.build()
};
ce.scheduler().exec(&format!("exec \"{}\"", env.init_script.to_string_lossy()), ExecSource::Event);
ce.dispatcher_mut().resume_until_empty();
let mut sm = ShaderManager::new(&display, &env.rl);
let mut window = {
let size = gl_window.inner_size();
Window {
scaled_width: size.width,
scaled_height: size.height,
dpi: gl_window.hidpi_factor(),
}
};
let model = Rc::new(get_test_model(&mut sm, &display));
let mut camera = Rc::new(RefCell::new(Camera::new()));
let mut kb = Keyboard::new();
camera.borrow_mut().set_pos(vec3(0.0, 10.0, -5.0));
// camera.borrow_mut().set_pitch(-30.0);
camera.borrow_mut().set_yaw(10.0);
let fr = load_fonts(&env.rl, &display, &mut sm);
let mut game = Game {
env: env.clone(),
ce,
running,
display: &display,
sm,
window,
ui_host: UiHost::new(),
kb,
fps: FpsCounter::new(),
camera,
fr,
ui_settings,
};
game.ui_host.open_ui(TestUiController::new(game.camera.clone(), model.clone(), game.ce.scheduler().clone()));
event_loop.run_return(|event, target, cf| game.handle_event(event, target, cf));
exit(0);
}
pub struct GameContainer<'a> {
event_loop: EventLoop<()>,
game: Game<'a>,
}
pub struct Game<'a> {
env: Rc<Environment>,
ce: ConsoleEngine<LogPipe<'static>>,
running: Arc<AtomicBool>,
display: &'a Display,
sm: ShaderManager<'a>,
window: Window,
ui_host: UiHost,
kb: Keyboard,
fps: FpsCounter,
camera: Rc<RefCell<Camera>>,
fr: Multifont<(i32, bool, f64), (i32, bool), BakedFont<'a>, SelImpl>,
ui_settings: Rc<Cell<UiSettings>>,
}
const FONT_SIZE: i32 = 16;
impl Game<'_> {
fn handle_event(&mut self, event: Event<()>, target: &EventLoopWindowTarget<()>, cf: &mut ControlFlow) {
match event {
Event::WindowEvent { event: WindowEvent::RedrawRequested, .. } => {
self.cycle(false);
}
Event::EventsCleared => {
if self.ui_host.is_animated() {
self.cycle(true);
}
}
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
self.running.store(false, Ordering::Relaxed);
}
Event::WindowEvent { event: WindowEvent::Resized(LogicalSize { width, height }), .. } => {
self.window.on_resize(width, height);
self.ui_host.resize(self.window.width(), self.window.height());
}
Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } => {
self.ui_host.on_key(input.state, input.virtual_keycode, input.scancode, input.modifiers);
}
Event::WindowEvent { event: WindowEvent::ReceivedCharacter(c), .. } => {
self.ui_host.on_key_typed(c);
}
Event::WindowEvent { event: WindowEvent::Focused(state), .. } => {
// if !gui_mode {
// gl_window.set_cursor_grab(state).unwrap();
// gl_window.set_cursor_visible(!state);
// gl_window.set_cursor_position(center).unwrap();
// }
}
Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, .. } => {
// mdiff = Some((position.x - center.x, position.y - center.y));
self.ui_host.on_mouse_move(&DrawPreview::from(self.fr.choose((FONT_SIZE, false, self.window.dpi))),
(position.x * self.window.dpi) as i32, (position.y * self.window.dpi) as i32);
}
Event::WindowEvent { event: WindowEvent::MouseInput { device_id, state, button, modifiers }, .. } => {
self.ui_host.on_click(&DrawPreview::from(self.fr.choose((FONT_SIZE, false, self.window.dpi))),
state, button, modifiers)
}
Event::WindowEvent { event: WindowEvent::HiDpiFactorChanged(f), .. } => {
self.window.on_dpi_changed(f);
self.ui_host.resize(self.window.width(), self.window.height());
}
_ => ()
}
if !self.running.load(Ordering::Relaxed) {
*cf = ControlFlow::Exit;
}
}
fn cycle(&mut self, limit: bool) {
self.fps.end_frame(if limit { self.ce.get_cvar("fps_limit").unwrap().get_int_value().unwrap() as u64 } else { 0 });
self.draw();
// let center = LogicalPosition::new(self.window.scaled_width as f64 / 2.0, self.window.scaled_height as f64 / 2.0);
// let mut mdiff = None;
//
// if !gui_mode {
// let mut camera = self.camera.borrow_mut();
// if let Some((mdiff_x, mdiff_y)) = mdiff {
// camera.rotate(mdiff_x as f32 * 0.1, -mdiff_y as f32 * 0.1, 0.0);
// self.gl_window.set_cursor_position(center).unwrap();
// }
//
// if self.kb.esc { self.running.store(false, Ordering::Relaxed); }
//
// let cam_mat = camera.get_matrix().into_mat3();
// let mut movement = Vec3::origin();
// if self.kb.fwd { movement += vec3(0.0, 0.0, 1.0); }
// if self.kb.back { movement += vec3(0.0, 0.0, -1.0); }
// if self.kb.left { movement += vec3(1.0, 0.0, 0.0); }
// if self.kb.right { movement += vec3(-1.0, 0.0, 0.0); }
//
// camera.shift(cam_mat * movement * 0.1);
// }
self.ce.dispatcher_mut().resume();
}
fn draw(&mut self) {
let Game {
env,
ce,
running,
sm,
window,
ui_host,
kb,
camera,
display,
fps,
fr,
ui_settings,
} = self;
let display = *display;
let time_start = Instant::now();
let mut frame = display.draw();
frame.clear(None, Some((0.0, 0.0, 0.0, 0.0)), false, Some(1.0), None);
let mut props = RenderProperties::new(&camera.borrow(), 90.0, window.width(), window.height());
let gui_mat = Mat4::get_ortho(0.0, window.width() as f32, window.height() as f32, 0.0, -1000.0, 1000.0);
let mut surface = AnySurface::Frame(&mut frame);
let mut g = Graphics::new(display, &mut surface, self.fr.choose((FONT_SIZE, false, window.dpi)), &mut self.sm, gui_mat, ui_settings.get());
self.ui_host.draw(&mut g);
let fr = self.fr.choose((14, false, window.dpi));
let color = Color::white();
if self.ce.get_cvar("debug_info").unwrap().get_bool_value() {
g.push_matrix(|g| {
g.translatei(0, 0, 100);
g.draw_string(&format!("fps: {} ({} actual)", fps.smooth_fps(), fps.current_fps()), 2, 2, StringDrawProps::default());
let camera = camera.borrow();
g.draw_string(&format!("pos: {:5.3} {:5.3} {:5.3}",
PrettyPrintFloat(camera.pos().x() as f64),
PrettyPrintFloat(camera.pos().y() as f64),
PrettyPrintFloat(camera.pos().z() as f64)),
2, 2 + fr.get_height() as i32, StringDrawProps::default());
g.draw_string(&format!("ang: {:5.3} {:5.3} {:5.3}",
PrettyPrintFloat(camera.yaw() as f64),
PrettyPrintFloat(camera.pitch() as f64),
PrettyPrintFloat(camera.roll() as f64)),
2, 2 + 2 * fr.get_height() as i32, StringDrawProps::default());
});
}
drop(g);
frame.finish().unwrap();
}
fn mouse_move(&mut self) {}
}
pub struct FpsCounter {
last_frame_start: Instant,
current_fps: u64,
smooth_fps: u64,
last_smooth_fps: Instant,
frames_since_last: u64,
}
impl FpsCounter {
pub fn new() -> Self {
FpsCounter {
last_frame_start: Instant::now(),
current_fps: 0,
smooth_fps: 0,
last_smooth_fps: Instant::now(),
frames_since_last: 0,
}
}
pub fn end_frame(&mut self, fps_target: u64) {
if fps_target > 0 {
let frame_time = Duration::from_micros(1000000 / fps_target);
let elapsed = self.last_frame_start.elapsed();
if elapsed < frame_time {
sleep(frame_time.clone().sub(elapsed));
}
}
self.current_fps = (1000000000 / self.last_frame_start.elapsed().as_nanos()) as u64;
self.frames_since_last += 1;
let last_smooth = self.last_smooth_fps.elapsed();
if last_smooth.as_secs() > 0 {
self.smooth_fps = self.frames_since_last * 1000000000 / last_smooth.as_nanos() as u64;
self.frames_since_last = 0;
self.last_smooth_fps = Instant::now();
}
self.last_frame_start = Instant::now();
}
pub fn smooth_fps(&self) -> u64 { self.smooth_fps }
pub fn current_fps(&self) -> u64 { self.current_fps }
}
fn draw(surface: &mut AnySurface, model: &Model<Vertex>, props: &RenderProperties) {
surface.clear_color_and_depth((0.01, 0.01, 0.01, 1.0), 1.0);
model.draw(surface, props)
.expect("Failed to draw triangle!");
}
fn get_test_model(sm: &mut ShaderManager, display: &Display) -> Model<Vertex> {
let prog = sm.get("shader/tri.sdef").unwrap();
let vb = VertexBuffer::immutable(display, &[
Vertex { pos: [1.0, 1.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [1.0, 0.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [0.0, 1.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [1.0, 1.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [0.0, 0.0, -1.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [1.0, 0.0, 0.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [1.0, 0.0, 1.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [1.0, 0.0, 1.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [0.0, 0.0, 1.0], normal: [0.0, -1.0, 0.0] },
Vertex { pos: [0.0, 1.0, 1.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 1.0, 0.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 0.0, 1.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 1.0, 1.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 0.0, 0.0], normal: [-1.0, 0.0, 0.0] },
Vertex { pos: [0.0, 0.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [1.0, 0.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [0.0, 0.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [0.0, 1.0, 1.0], normal: [0.0, 0.0, 1.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [1.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [0.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [0.0, 1.0, 1.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [0.0, 1.0, 0.0], normal: [0.0, 1.0, 0.0] },
Vertex { pos: [1.0, 0.0, 0.0], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [1.0, 1.0, 0.0], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [1.0, 0.0, 0.0], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [1.0, 1.0, 1.0], normal: [1.0, 0.0, 0.0] },
Vertex { pos: [1.0, 0.0, 1.0], normal: [1.0, 0.0, 0.0] },
]).unwrap();
Model::of(prog, vb)
}
struct SelImpl;
impl Selector<(i32, bool, f64), (i32, bool)> for SelImpl {
fn select(&self, (r_size, r_bold, dpi): &(i32, bool, f64), (f_size, f_bold): &(i32, bool)) -> i32 {
((*r_size as f64 * dpi) as i32 - f_size).abs() * -10 +
if r_bold == f_bold { 5 } else { 0 }
}
}
fn load_fonts<'a>(rl: &ResourceLoader, facade: &'a Display, sm: &mut ShaderManager) -> Multifont<(i32, bool, f64), (i32, bool), BakedFont<'a>, SelImpl> {
let mut fonts = vec![];
for &i in [12, 14, 16, 18, 20, 22, 24, 28, 32].iter() {
for &b in [true, false].iter() {
let mut source = String::new();
rl.open(format!("font/terminus/ter-u{}{}.bdf", i, if b { "b" } else { "n" })).unwrap().read_to_string(&mut source).unwrap();
let bdf = bdf::parse(&source.lines().collect::<Vec<_>>()).unwrap();
let fr = BakedFont::new(facade, bdf, sm.get("shader/font.sdef").unwrap());
fonts.push(((i, b), fr));
}
}
Multifont::new(fonts, SelImpl)
}
implement_vertex!(Vertex, pos, normal);
#[derive(Copy, Clone)]
pub struct Vertex {
pos: [f32; 3],
normal: [f32; 3],
}
struct Window {
scaled_width: f64,
scaled_height: f64,
dpi: f64,
}
impl Window {
pub fn on_resize(&mut self, width: f64, height: f64) {
self.scaled_width = width;
self.scaled_height = height;
}
pub fn on_dpi_changed(&mut self, f: f64) {
self.dpi = f;
}
pub fn width(&self) -> u32 { (self.scaled_width * self.dpi) as u32 }
pub fn height(&self) -> u32 { (self.scaled_height * self.dpi) as u32 }
}
pub struct Model<T: Copy> {
shader: Rc<Program>,
buf: VertexBuffer<T>,
}
impl<T: Copy> Model<T> {
pub fn of(shader: Rc<Program>, buf: VertexBuffer<T>) -> Self {
Model { shader, buf }
}
pub fn draw(&self, surface: &mut impl Surface, props: &RenderProperties) -> Result<(), DrawError> {
let mut dp = DrawParameters::default();
dp.backface_culling = BackfaceCullingMode::CullClockwise;
dp.depth.write = true;
dp.depth.test = DepthTest::IfLess;
surface.draw(
&self.buf,
NoIndices { primitives: PrimitiveType::TrianglesList },
&self.shader,
&UniformsStorage::new("mvp", *props.mvp.as_ref()),
&dp,
)
}
}
pub struct RenderProperties {
p: Mat4,
mv: Mat4,
mvp: Mat4,
}
impl RenderProperties {
pub fn new(camera: &Camera, fov: f32, width: u32, height: u32) -> Self {
let p = Mat4::get_perspective(fov, width as f32 / height as f32, 0.1, 1000.0);
let mv = camera.get_matrix();
RenderProperties {
p,
mv,
mvp: p * mv,
}
}
}
struct Environment {
rl: ResourceLoader,
i18n: I18n,
data_dir: Vec<PathBuf>,
save_dir: PathBuf,
v_sync: bool,
init_script: PathBuf,
}