use std::{
f32::consts::TAU,
path::Path,
rc::Rc,
sync::{Arc, Mutex},
};
use anyhow::Result;
use glam::{UVec2, Vec2, Vec3, Vec3Swizzles};
use image::RgbaImage;
use crate::{
asset::{Asset, AssetFetchOutput, AssetFetchRequest, AssetManager, AssetType, FetchedTask},
camera::Camera,
color::Color,
commands::*,
font::{Font, FontConfig, FontManager},
handle::Handle,
input::InputState,
platform::types::CircleUniform,
prelude::Transform,
render::{BackendState, Render, ScaleMode},
renderer::post::ScreenShader,
renderer::shader2d::default::DefaultShader,
renderer::shader3d::draw3d::Draw3D,
renderer::traits::{PostShader, Shader},
sprite::Sprite,
tasks,
text::{Text, TextCache, TextRenderCache},
types::{DrawContent, Line, LinePattern},
utils::bytes::to_wgsl_bytes,
};
const ENGINE_MAX_TICK: f32 = 100.0;
const DEFAULT_FONT_BYTES: &[u8; 49432] = include_bytes!("../assets/C_C_Red_Alert.ttf");
const COMMANDS_BUFFER: usize = 1024;
pub trait Scene {
fn init(&mut self, _g: &mut Engine) {}
fn update(&mut self, _g: &mut Engine) {}
fn draw(&mut self, _g: &mut Engine) {}
fn cleanup(&mut self, _g: &mut Engine) {}
}
#[derive(Default, Debug)]
pub struct Perf {
pub entities: usize,
pub update: f32,
pub draw: f32,
pub total: f32,
}
pub trait Clock {
fn now(&mut self) -> f32;
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum CameraMode {
#[default]
Game,
Ui,
}
#[cfg(feature = "egui")]
type EGUILayer = Box<dyn FnMut(&egui::Context, &mut Engine)>;
pub struct Engine {
pub clock: Box<dyn Clock>,
pub time_real: f32,
pub time: f32,
pub time_scale: f32,
pub tick: f32,
pub frame: f32,
pub perf: Perf,
pub input: InputState,
pub camera_mode: CameraMode,
pub(crate) exit: bool,
is_running: bool,
is_window_resized: bool,
scene: Option<Box<dyn Scene>>,
scene_next: Option<Box<dyn Scene>>,
font_manager: FontManager,
text_cache: TextCache,
commands_buffer: Vec<Command>,
resource_commands: Vec<ResourceCommand>,
pub(crate) camera: Camera,
pub(crate) ui_camera: Camera,
draws3d: Vec<Draw3D>,
pub(crate) camera3d: crate::camera3d::Camera3D,
pub(crate) render: Render,
#[cfg(feature = "egui")]
egui_layers: Vec<EGUILayer>,
pub assets: AssetManager,
default_texture: Option<Handle>,
asset_fetch_job: Option<AssetFetchJob>,
}
impl Engine {
pub fn new<C: Clock + 'static>(clock: C, backend_state: BackendState) -> Self {
Self {
clock: Box::new(clock),
time_real: 0.0,
time: 0.0,
time_scale: 1.0,
tick: 0.0,
frame: 0.0,
camera: Camera::default(),
ui_camera: Camera::default(),
camera3d: crate::camera3d::Camera3D::default(),
camera_mode: CameraMode::default(),
perf: Perf::default(),
is_running: false,
is_window_resized: false,
scene: None,
scene_next: None,
font_manager: FontManager::default(),
text_cache: TextCache::default(),
commands_buffer: Vec::with_capacity(COMMANDS_BUFFER),
resource_commands: Vec::with_capacity(COMMANDS_BUFFER),
draws3d: Vec::with_capacity(COMMANDS_BUFFER),
render: Render::new(backend_state),
input: InputState::default(),
assets: AssetManager::new("assets"),
default_texture: None,
asset_fetch_job: None,
exit: false,
#[cfg(feature = "egui")]
egui_layers: Vec::new(),
}
}
pub fn exit(&mut self) {
self.exit = true;
}
pub fn load_default_font<P: AsRef<Path>>(&mut self, path: P) {
let handle = self.assets.load_font(path);
self.set_default_font(handle);
}
pub fn set_snap_to_px(&mut self, snap_to_px: bool) {
self.camera.set_snap_to_px(snap_to_px);
self.ui_camera.set_snap_to_px(snap_to_px);
}
pub fn set_default_font(&mut self, handle: Handle) {
self.font_manager.set_default_font(handle);
}
pub fn set_font_config(&mut self, config: FontConfig) {
self.font_manager.set_font_config(config);
}
pub fn render_text(&mut self, text: &Text) -> Result<(Handle, UVec2)> {
let (handle, size) = match self.text_cache.get(text) {
Some(TextRenderCache { texture, size }) => (texture.clone(), *size),
None => {
log::trace!("Render new text {text:?}");
let (handle, size) = self.create_text_texture(text)?;
self.text_cache.update(
text.clone(),
TextRenderCache {
texture: handle.clone(),
size,
},
);
(handle, size)
}
};
Ok((handle, size))
}
pub fn draw_text(&mut self, text: &Text, anchor: Option<Vec2>, mut transform: Transform) {
if let Ok((handle, size)) = self.render_text(text) {
let mut sprite = Sprite::new(handle, size);
sprite.color.a = 0.999;
if let Some(anchor) = anchor {
sprite.anchor = anchor;
}
transform.pos.x += self.font_manager.font_config.offset.x;
transform.pos.y += self.font_manager.font_config.offset.y;
self.draw(&sprite, transform);
}
}
pub fn draw3d(&mut self, draw: Draw3D) {
self.draws3d.push(draw);
}
pub fn create_text_texture(&mut self, text: &Text) -> Result<(Handle, UVec2)> {
let handle = self.assets.insert(Asset {
asset_type: AssetType::Texture,
bytes: None,
});
let (data, size) = self.render.render_text_texture(&self.font_manager, text)?;
let cmd = CreateTexture {
handle: handle.clone(),
data,
};
self.push_res_cmd(ResourceCommand::CreateTexture(cmd));
Ok((handle, size))
}
pub fn create_raw_texture(&mut self, texture: wgpu::Texture) -> Result<Handle> {
let handle = self.assets.insert(Asset {
asset_type: AssetType::Texture,
bytes: None,
});
self.push_res_cmd(ResourceCommand::CreateRawTexture(CreateRawTexture {
handle: handle.clone(),
texture,
}));
Ok(handle)
}
fn push_draw_cmd(&mut self, cmd: Command) {
self.commands_buffer.push(cmd);
}
fn push_res_cmd(&mut self, cmd: ResourceCommand) {
self.resource_commands.push(cmd);
}
pub(crate) fn take_draw_commands(&mut self) -> Vec<Command> {
std::mem::replace(
&mut self.commands_buffer,
Vec::with_capacity(COMMANDS_BUFFER),
)
}
pub(crate) fn take_resource_commands(&mut self) -> Vec<ResourceCommand> {
std::mem::replace(
&mut self.resource_commands,
Vec::with_capacity(COMMANDS_BUFFER),
)
}
pub(crate) fn take_3d_draws(&mut self) -> Vec<Draw3D> {
std::mem::replace(&mut self.draws3d, Vec::with_capacity(COMMANDS_BUFFER))
}
pub fn with_ui<R, F: FnOnce(&mut Engine) -> R>(&mut self, f: F) -> R {
let mode = self.camera_mode;
self.camera_mode = CameraMode::Ui;
let ret = f(self);
self.camera_mode = mode;
ret
}
pub fn draw_rect(
&mut self,
color: Color,
size: Vec2,
anchor: Option<Vec2>,
transform: Transform,
) {
let texture = self.default_texture_handle();
let mut image = Sprite::new(texture, size.as_uvec2());
image.color = color;
if let Some(anchor) = anchor {
image.anchor = anchor;
}
self.draw(&image, transform);
}
pub fn draw_line(&mut self, line: Line, z: f32) {
self.draw_lines(vec![line], z);
}
pub fn draw_line_ex(&mut self, line: Line, z: f32, pattern: LinePattern, offset: f32) {
let (dash_length, gap_length, dash_offset) = match pattern {
LinePattern::Solid => {
self.draw_line(line, z);
return;
}
LinePattern::Dashed {
dash_length,
gap_length,
} => (dash_length, gap_length, offset),
LinePattern::Dotted { spacing } => (spacing * 0.5, spacing * 0.5, offset),
};
let line_vec = line.end - line.start;
let line_length = line_vec.length();
let line_dir = line_vec.normalize_or_zero();
let pattern_length = dash_length + gap_length;
let mut current_dist = -dash_offset;
let mut lines = Vec::new();
while current_dist < line_length {
let dash_start_dist = current_dist.max(0.0);
let dash_end_dist = (current_dist + dash_length).min(line_length);
if dash_start_dist < dash_end_dist {
lines.push(Line {
start: line.start + line_dir * dash_start_dist,
end: line.start + line_dir * dash_end_dist,
thickness: line.thickness,
color: line.color,
});
}
current_dist += pattern_length;
}
self.draw_lines(lines, z);
}
pub fn draw_lines(&mut self, lines: Vec<Line>, z: f32) {
if lines.is_empty() {
return;
}
let shader = self.render.solid_shader.clone();
let mut min_p = lines[0].start;
let mut max_p = lines[0].end;
for line in &lines {
min_p = min_p.min(line.start).min(line.end);
max_p = max_p.max(line.start).max(line.end);
}
let size = max_p - min_p;
let center = (min_p + max_p) * 0.5;
let transparent = lines.iter().any(|l| l.color.a < 1.0);
let cmd = Draw {
shader: shader.clone(),
shader_data: None,
content: DrawContent::Lines(lines),
src: None,
transform: Transform::new(Vec3::new(center.x, center.y, z)),
size,
flip_x: false,
flip_y: false,
anchor: Vec2::splat(0.5),
transparent,
};
self.push_draw(cmd);
}
pub fn draw_poly(
&mut self,
center: Vec3,
sides: u32,
radius: f32,
rotation: f32,
color: Color,
) {
if sides < 3 || radius <= 0.0 {
return;
}
let (vertices, min, max) =
Self::polygon_vertices_with_bounds(center.xy(), sides, radius, rotation);
let size = max - min;
let polygon_renderer = self.render.solid_shader.clone();
let cmd = Draw {
shader: polygon_renderer,
shader_data: None,
content: DrawContent::Polygon { vertices, color },
src: None,
transform: Transform::new(Vec3::new(
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
center.z,
)),
size,
flip_x: false,
flip_y: false,
anchor: Vec2::splat(0.5),
transparent: color.a < 1.0,
};
self.push_draw(cmd);
}
pub fn draw_poly_textured(
&mut self,
center: Vec3,
sides: u32,
radius: f32,
rotation: f32,
sprite: &Sprite,
) {
if sides < 3 || radius <= 0.0 {
return;
}
let (vertices, min, max) =
Self::polygon_vertices_with_bounds(center.xy(), sides, radius, rotation);
let size = max - min;
let uvs = Self::polygon_uvs_for_sprite(&vertices, min, size, sprite);
let renderer = self.render.shader.clone();
let renderer_data = self.render.send_shader_data.take();
let cmd = Draw {
shader: renderer,
shader_data: renderer_data,
content: DrawContent::PolygonTextured {
vertices,
uvs,
color: sprite.color,
handle: sprite.texture.clone(),
},
src: None,
transform: Transform::new(Vec3::new(
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
center.z,
)),
size,
flip_x: false,
flip_y: false,
anchor: Vec2::splat(0.5),
transparent: sprite.color.a < 1.0,
};
self.push_draw(cmd);
}
pub fn draw_poly_lines(
&mut self,
center: Vec3,
sides: u32,
radius: f32,
rotation: f32,
thickness: f32,
color: Color,
) {
if sides < 3 || radius <= 0.0 {
return;
}
let vertices = Self::regular_polygon_vertices(center.xy(), radius, sides, rotation);
let mut lines = Vec::with_capacity(vertices.len());
for i in 0..vertices.len() {
let start = vertices[i];
let end = vertices[(i + 1) % vertices.len()];
lines.push(Line {
start,
end,
thickness,
color,
});
}
self.draw_lines(lines, center.z);
}
fn regular_polygon_vertices(center: Vec2, radius: f32, sides: u32, rotation: f32) -> Vec<Vec2> {
let step = TAU / sides as f32;
(0..sides)
.map(|i| {
let angle = rotation + step * i as f32;
center + Vec2::new(angle.cos(), angle.sin()) * radius
})
.collect()
}
fn polygon_vertices_with_bounds(
center: Vec2,
sides: u32,
radius: f32,
rotation: f32,
) -> (Vec<Vec2>, Vec2, Vec2) {
let vertices = Self::regular_polygon_vertices(center, radius, sides, rotation);
debug_assert!(vertices.len() >= 3);
let mut min = vertices[0];
let mut max = vertices[0];
for v in &vertices[1..] {
min = min.min(*v);
max = max.max(*v);
}
(vertices, min, max)
}
fn polygon_uvs_for_sprite(
vertices: &[Vec2],
min: Vec2,
size: Vec2,
sprite: &Sprite,
) -> Vec<Vec2> {
let tex_size = sprite.size.as_vec2().max(Vec2::splat(1.0));
let (tex_min, tex_max) = sprite
.src
.as_ref()
.map(|rect| (rect.min, rect.max))
.unwrap_or((Vec2::ZERO, tex_size));
let tex_extent = tex_max - tex_min;
vertices
.iter()
.map(|v| {
let rel_x = if size.x.abs() <= f32::EPSILON {
0.5
} else {
(v.x - min.x) / size.x
};
let rel_y = if size.y.abs() <= f32::EPSILON {
0.5
} else {
(v.y - min.y) / size.y
};
let mut rel = Vec2::new(rel_x, rel_y);
if sprite.flip_x {
rel.x = 1.0 - rel.x;
}
if sprite.flip_y {
rel.y = 1.0 - rel.y;
}
let uv_pixels = tex_min + tex_extent * rel;
uv_pixels / tex_size
})
.collect()
}
pub fn draw_circle(&mut self, center: Vec3, radius: f32, color: Color) {
fn calculate_circle_bounds(center: Vec2, radius: f32) -> (Vec2, Vec2) {
let min = center - Vec2::splat(radius);
let max = center + Vec2::splat(radius);
(min, max)
}
let (min, max) = calculate_circle_bounds(center.xy(), radius);
let size = max - min;
let circle_data = CircleUniform { center, radius };
let shader_data = to_wgsl_bytes(&circle_data);
let circle_renderer = self.render.circle_shader.clone();
let transform = Transform::new(center);
let cmd = Draw {
shader: circle_renderer,
shader_data: Some(shader_data),
content: DrawContent::Circle { radius, color },
src: None,
transform,
size,
flip_x: false,
flip_y: false,
anchor: Vec2::splat(0.5),
transparent: color.a < 1.0,
};
self.push_draw(cmd);
}
pub fn draw(&mut self, sprite: &Sprite, transform: Transform) {
let renderer = self.render.shader.clone();
let renderer_args = self.render.send_shader_data.take();
let cmd = Draw::new(sprite, transform, renderer, renderer_args);
self.push_draw(cmd);
}
fn push_draw(&mut self, cmd: Draw) {
let camera = match self.camera_mode {
CameraMode::Game => self.camera(),
CameraMode::Ui => self.ui_camera(),
};
let view = camera.viewport();
let size = cmd.size * cmd.transform.scale;
let b = cmd.transform.bounds_with_anchor_and_size(cmd.anchor, size);
if b.min.x > view.max.x
|| b.min.y > view.max.y
|| b.max.x < view.min.x
|| b.max.y < view.min.y
{
return;
}
match self.camera_mode {
CameraMode::Game => {
self.push_draw_cmd(Command::Draw(cmd));
}
CameraMode::Ui => {
self.push_draw_cmd(Command::DrawUi(cmd));
}
}
}
pub fn view_size(&self) -> Vec2 {
self.render.logical_size()
}
pub fn screen_size(&self) -> Vec2 {
self.render.screen_size()
}
pub fn is_window_resized(&self) -> bool {
self.is_window_resized
}
pub fn scale_mode(&self) -> ScaleMode {
self.render.scale_mode()
}
pub fn set_scale_mode(&mut self, mode: ScaleMode) {
self.render.set_scale_mode(mode)
}
pub fn now(&mut self) -> f32 {
self.clock.now()
}
pub fn camera(&self) -> &Camera {
&self.camera
}
pub fn camera_mut(&mut self) -> &mut Camera {
&mut self.camera
}
pub fn ui_camera(&self) -> &Camera {
&self.ui_camera
}
pub fn ui_camera_mut(&mut self) -> &mut Camera {
&mut self.ui_camera
}
pub fn camera3d(&self) -> &crate::camera3d::Camera3D {
&self.camera3d
}
pub fn camera3d_mut(&mut self) -> &mut crate::camera3d::Camera3D {
&mut self.camera3d
}
pub fn input(&self) -> &InputState {
&self.input
}
pub fn input_mut(&mut self) -> &mut InputState {
&mut self.input
}
pub(crate) fn init<Setup: FnOnce(&mut Engine)>(&mut self, setup: Setup) {
self.time_real = self.now();
self.init_default_font();
setup(self);
}
pub(crate) fn init_default_font(&mut self) {
let handle = self.assets.insert(Asset {
asset_type: AssetType::Font,
bytes: None,
});
let font =
Font::from_bytes(DEFAULT_FONT_BYTES.into()).expect("Failed to load default font");
self.font_manager.add_font(handle.id(), font);
self.set_default_font(handle);
}
pub(crate) fn on_resize(&mut self, physical_size: UVec2, dpi_scale_factor: f64) {
self.render.resize(physical_size, dpi_scale_factor);
self.camera.resize(self.render.logical_size());
self.ui_camera.resize(self.render.logical_size());
self.is_window_resized = true;
self.render.post_shader.resize(self);
}
pub(crate) fn update(&mut self) {
let time_frame_start = self.now();
if self.scene_next.is_some() {
self.is_running = false;
if let Some(mut scene) = self.scene.take() {
scene.cleanup(self);
}
self.time = 0.;
self.frame = 0.;
if let Some(mut scene) = self.scene_next.take() {
scene.init(self);
self.scene = Some(scene);
}
}
self.is_running = true;
let time_real_now = self.now();
let mut time_delta = time_real_now - self.time_real;
if time_delta > 1.0 {
time_delta = 1.0 / 30.0;
}
let tick = (time_delta * self.time_scale).min(ENGINE_MAX_TICK);
if tick == 0.0 {
return;
}
log::debug!("now {time_real_now} tick {tick}");
self.time_real = time_real_now;
self.tick = tick;
self.time += self.tick;
self.frame += 1.;
if let Some(mut scene) = self.scene.take() {
scene.update(self);
self.scene = Some(scene);
}
self.drive_asset_pipeline();
self.perf.update = self.now() - time_real_now;
if let Some(mut scene) = self.scene.take() {
scene.draw(self);
self.scene = Some(scene);
}
self.perf.draw = (self.now() - time_real_now) - self.perf.update;
self.input.clear();
self.is_window_resized = false;
self.perf.total = self.now() - time_frame_start;
log::trace!("Perf {:?}", &self.perf);
}
fn drive_asset_pipeline(&mut self) {
if self.asset_fetch_job.is_none() {
match self.assets.begin_fetch() {
Ok(Some(request)) => {
self.asset_fetch_job = Some(AssetFetchJob::new(request));
}
Ok(None) => {}
Err(err) => log::error!("Failed to start asset fetch: {err:?}"),
}
}
if let Some(job) = &self.asset_fetch_job
&& let Some(result) = job.try_take()
{
self.asset_fetch_job = None;
match result {
Ok(outputs) => match self.assets.finish_fetch(outputs) {
Ok(tasks) => self.apply_asset_tasks(tasks),
Err(err) => log::error!("Failed to finalize asset fetch: {err:?}"),
},
Err(err) => log::error!("Asset fetch failed: {err:?}"),
}
}
let ready_tasks = self.assets.take_ready_tasks();
if !ready_tasks.is_empty() {
self.apply_asset_tasks(ready_tasks);
}
let drop_tasks = self.assets.collect_drop_tasks();
if !drop_tasks.is_empty() {
self.apply_asset_tasks(drop_tasks);
}
}
fn apply_asset_tasks(&mut self, tasks: Vec<FetchedTask>) {
for task in tasks {
match task {
FetchedTask::CreateTexture { handle, data } => {
self.push_res_cmd(ResourceCommand::CreateTexture(CreateTexture {
handle,
data,
}));
}
FetchedTask::RemoveTexture { handle } => {
self.push_res_cmd(ResourceCommand::RemoveTexture(RemoveTexture(handle)));
}
FetchedTask::CreateFont { handle, font } => {
self.font_manager.add_font(handle.id(), font);
}
FetchedTask::RemoveFont { handle } => {
self.font_manager.remove_font(handle);
}
}
}
}
fn default_texture_handle(&mut self) -> Handle {
if let Some(handle) = &self.default_texture {
return handle.clone();
}
let data = RgbaImage::from_vec(1, 1, vec![255, 255, 255, 255])
.expect("Invalid RGBA image")
.into();
let handle = self.assets.insert(Asset {
asset_type: AssetType::Texture,
bytes: None,
});
self.push_res_cmd(ResourceCommand::CreateTexture(CreateTexture {
handle: handle.clone(),
data,
}));
self.default_texture = Some(handle.clone());
handle
}
pub fn set_scene(&mut self, scene: impl Scene + 'static) {
self.scene_next.replace(Box::new(scene));
}
pub fn is_running(&self) -> bool {
self.is_running
}
pub fn backend_state(&self) -> &BackendState {
&self.render.backend_state
}
pub fn with_renderer<Ret, F: FnOnce(&mut Engine) -> Ret>(
&mut self,
renderer: Rc<dyn Shader + 'static>,
f: F,
) -> Ret {
let old_renderer = self.render.set_shader(renderer);
let ret = f(self);
self.render.set_shader(old_renderer);
ret
}
pub fn set_renderer(&mut self, renderer: Option<Rc<dyn Shader + 'static>>) {
match renderer {
Some(renderer) => {
self.render.set_shader(renderer);
}
None => {
let renderer: Rc<dyn Shader> =
Rc::new(DefaultShader::new(&self.render.backend_state));
self.render.set_shader(renderer);
}
}
}
pub fn send_renderer_data(&mut self, data: Vec<u8>) {
self.render.send_shader_data(data);
}
#[cfg(feature = "egui")]
pub fn egui<F>(&mut self, f: F)
where
F: FnMut(&egui::Context, &mut Engine) + 'static,
{
self.egui_layers.push(Box::new(f));
}
#[cfg(feature = "egui")]
pub(crate) fn run_egui_layers(&mut self, ctx: &egui::Context) {
let layers = core::mem::take(&mut self.egui_layers);
for mut layer in layers {
(layer)(ctx, self);
}
}
pub fn set_post_renderer<R: PostShader + 'static>(&mut self, post_renderer: Option<R>) {
let renderer: Box<dyn PostShader> = if let Some(post_renderer) = post_renderer {
Box::new(post_renderer)
} else {
Box::new(ScreenShader::setup(&self.render.backend_state))
};
renderer.resize(self);
self.render.set_post_shader(renderer);
}
pub fn text_cache(&mut self) -> &mut FontManager {
&mut self.font_manager
}
}
struct AssetFetchJob {
result: Arc<Mutex<Option<Result<Vec<AssetFetchOutput>>>>>,
}
impl AssetFetchJob {
fn new(request: AssetFetchRequest) -> Self {
let result = Arc::new(Mutex::new(None));
let result_clone = Arc::clone(&result);
tasks::spawn_local(async move {
let AssetFetchRequest { reader, tasks } = request;
let mut outputs = Vec::with_capacity(tasks.len());
for task in tasks {
match reader.read(&task.path).await {
Ok(bytes) => outputs.push(AssetFetchOutput {
handle: task.handle,
asset_type: task.asset_type,
bytes,
}),
Err(err) => {
*result_clone.lock().unwrap() = Some(Err(err));
return;
}
}
}
*result_clone.lock().unwrap() = Some(Ok(outputs));
});
Self { result }
}
fn try_take(&self) -> Option<Result<Vec<AssetFetchOutput>>> {
self.result.lock().unwrap().take()
}
}