added all I already made for this engine prototype

This commit is contained in:
2025-08-27 20:37:15 +02:00
parent 236b875870
commit 7ea933a04d
18 changed files with 2822 additions and 0 deletions

View File

@ -1,2 +1,8 @@
# Engine.rs
The Goal of this repo is to make a simple engine like program in Rust
It is mainly for learning purposes and should be the base for a later, more fleshed out Engine made in Rust (with the possibility to create logic in Julia)

1
opengl_beginnings/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
opengl_beginnings/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>nebulix</w>
</words>
</dictionary>
</component>

8
opengl_beginnings/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/opengl_beginnings.iml" filepath="$PROJECT_DIR$/.idea/opengl_beginnings.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
opengl_beginnings/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2099
opengl_beginnings/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
[package]
name = "opengl_beginnings"
version = "0.1.0"
edition = "2024"
[dependencies]
glutin = "0.32.3"
glutin-winit = "0.5.0"
raw-window-handle = "0.6.2"
winit = {version = "0.30.11", features = ["rwh_06"]}
glow = "0.16.0"
nalgebra = "0.33.2"

View File

@ -0,0 +1,2 @@
pub mod perspective_camera;

View File

@ -0,0 +1,90 @@
use nalgebra::{Matrix3, Matrix4, Point3, Vector2, Vector3};
use winit::dpi::LogicalSize;
pub struct PerspectiveCamera {
position: Vector3<f32>,
world_up: Vector3<f32>,
yaw: f32,
pitch: f32,
front: Vector3<f32>,
intrinsic_coordinates: Matrix3<f32>, // right (x) | up (y) | front (z)
aspect_ratio: Option<f32>,
pub fov: f32,
pub movement_speed: f32, // In general, all the movement stuff should be moved to a "GameObject" that has the camera as a component, as well a movement script. However, for this I would need to build an entire Entity Component System, so thats for way farther into the future
}
impl PerspectiveCamera {
pub fn new(position: Vector3<f32>, yaw: Option<f32>, pitch: Option<f32>, fov: Option<f32>, movement_speed: Option<f32>) -> PerspectiveCamera {
let yaw = yaw.unwrap_or(-90.0);
let pitch = pitch.unwrap_or(0.0);
let world_up = Vector3::y_axis().into_inner();
let front = Vector3::new(
yaw.to_radians().cos() * pitch.to_radians().cos(),
pitch.to_radians().sin(),
yaw.to_radians().sin() * pitch.to_radians().cos(),
).normalize();
let right = Vector3::cross(&front, &world_up).normalize();
let up = Vector3::cross(&right, &front).normalize();
let coordinates = Matrix3::new(
front.x, right.x, up.x,
front.y, right.y, up.y,
front.z, right.z, up.z);
Self {
yaw,
pitch,
world_up,
front,
position,
intrinsic_coordinates: coordinates,
fov: fov.unwrap_or(45.0),
movement_speed: movement_speed.unwrap_or(2.5),
aspect_ratio: None,
}
}
pub fn view_matrix(&self) -> Matrix4<f32> {
Matrix4::look_at_rh(&Point3::from(self.position), &Point3::from(self.position + self.front), &self.world_up)
}
pub fn projection_matrix(&self) -> Matrix4<f32> {
Matrix4::new_perspective(self.aspect_ratio.unwrap(), self.fov, 0.1, 100.0) // TODO: make clipping plane configureable. probably through public property
}
pub fn update_aspect(&mut self, logical_size: &LogicalSize<i32>) {
self.aspect_ratio = Some(logical_size.width as f32 / logical_size.height as f32);
}
pub fn process_movement(&mut self, delta_time: f32, movement_direction: Vector3<f32>) {
let velocity = self.movement_speed * delta_time; // velocity not really the right term, but oh well
self.position += self.intrinsic_coordinates * (movement_direction * velocity);
}
pub fn process_mouse(&mut self, offset: Vector2<f32>) {
self.yaw += offset.x;
self.pitch += offset.y;
if self.pitch > 89.0 {self.pitch = 89.0;}
if self.pitch < -89.0 {self.pitch = -89.0;}
self.update_camera_vectors();
}
fn update_camera_vectors(&mut self) {
let front = Vector3::new(
self.yaw.to_radians().cos() * self.pitch.to_radians().cos(),
self.pitch.to_radians().sin(),
self.yaw.to_radians().sin() * self.pitch.to_radians().cos(),
).normalize();
let right = Vector3::cross(&front, &self.world_up).normalize();
let up = Vector3::cross(&right, &front).normalize();
self.front = front;
self.intrinsic_coordinates = Matrix3::new(
right.x, up.x, front.x,
right.y, up.y, front.y,
right.z, up.z, front.z);
}
}

View File

@ -0,0 +1,5 @@
pub mod nebulix;
pub mod time;
pub mod input;
mod shader;
mod mesh;

View File

@ -0,0 +1,96 @@
use nalgebra::Vector2;
use std::collections::HashSet;
use winit::dpi::LogicalPosition;
use winit::keyboard::{NamedKey, SmolStr};
enum Keys {
/// Shift, Control, etc.
SpecialKey(NamedKey),
/// A, B, 1, 2, etc. case-insensitive. If case is required, also sheck for special key "Shift"
StandardKey(SmolStr)
}
pub struct Input {
previous_mouse_position: Option<LogicalPosition<f32>>,
current_mouse_position: Option<LogicalPosition<f32>>,
mouse_offset: Vector2<f32>,
mouse_left_screen: bool,
pressed_keys: HashSet<String>,
pressed_special_keys: HashSet<NamedKey>,
pub mouse_sensitivity: f32,
}
impl Input {
/// param ```mouse_sensitivity``` Defaults to 0.1
pub fn new(mouse_sensitivity: Option<f32>) -> Self {
Self {
previous_mouse_position: None,
current_mouse_position: None,
mouse_offset: Vector2::zeros(),
mouse_left_screen: false,
pressed_keys: HashSet::new(),
pressed_special_keys: HashSet::new(),
mouse_sensitivity: mouse_sensitivity.unwrap_or(0.1),
}
}
pub fn get_mouse_offset(&self) -> Vector2<f32> {
self.mouse_offset
}
/// Case insensitive
pub fn is_key_pressed(&self, key: &str) -> bool {
self.pressed_keys.contains(&key.to_uppercase())
}
pub fn is_special_key_pressed(&self, key: NamedKey) -> bool {
self.pressed_special_keys.contains(&key)
}
pub(crate) fn mouse_moved(&mut self, mouse_position: LogicalPosition<f32>) {
if self.mouse_left_screen {
self.previous_mouse_position = Some(mouse_position);
}
else {
self.previous_mouse_position = match self.current_mouse_position {
None => None,
Some(mp) => Some(mp)
};
}
self.current_mouse_position = Some(mouse_position);
self.calculate_mouse_offset();
}
pub(crate) fn mouse_left_screen(&mut self) {
self.mouse_left_screen = true;
}
pub(crate) fn special_key_pressed(&mut self, key: NamedKey) {
self.pressed_special_keys.insert(key);
}
pub(crate) fn special_key_released(&mut self, key: NamedKey) {
self.pressed_special_keys.remove(&key);
}
pub(crate) fn key_pressed(&mut self, key: SmolStr) {
self.pressed_keys.insert(key.to_uppercase());
}
pub(crate) fn key_released(&mut self, key: SmolStr) {
self.pressed_keys.remove(&key.to_uppercase());
}
fn calculate_mouse_offset(&mut self) {
match self.previous_mouse_position {
None => {}
Some(prev_pos) => { // previous can only have a value if current also has one. so we only need to match previous
self.mouse_offset = Vector2::new((self.current_mouse_position.unwrap().x - prev_pos.x),
(self.current_mouse_position.unwrap().y - prev_pos.y)) * self.mouse_sensitivity;
}
}
}
}

View File

@ -0,0 +1,98 @@
use glow::{Context, HasContext, NativeBuffer, NativeVertexArray};
use nalgebra::Vector3;
// TODO: create a "MeshRenderer" which contains a mesh and a shader
// the MeshRenderer uses the mesh of this file as the data for rendering and ensures the shader is enabled so it is rendered correctly
// Down the line the MeshRenderer should have a material. The material then holds the data that is needed by the shader (so the actual texture or data for variables inside the shader)
// End Goal: Shader belongs to Material; Material belongs to MeshRenderer; Mesh belongs to MeshRenderer; MeshRenderer renders the mesh using the shader and data provided by the material
struct Mesh {
vertices: Vec<Vector3<f32>>,
normals: Vec<Vector3<f32>>,
indices: Vec<u32>,
// TODO: implement optional uv coordinates, which are added to the mesh data in the function "generate" if they are actually set
vao: Option<NativeVertexArray>,
vbo: Option<NativeBuffer>,
ebo: Option<NativeBuffer>,
regenerate: bool // TODO: Set to "true" everytime vertices or indices are changed
}
impl Mesh {
pub fn new(vertices: Vec<Vector3<f32>>, indices: Vec<u32>) -> Self {
let num_vertices = vertices.len();
Self {
vertices,
indices,
normals: Vec::with_capacity(num_vertices),
vao: None,
vbo: None,
ebo: None,
regenerate: true
}
}
pub(crate) fn render(&mut self, gl: &Context) {
if self.regenerate {
self.generate(gl)
}
unsafe {
gl.bind_vertex_array(self.vao);
gl.draw_elements(glow::TRIANGLES, self.indices.len() as i32, glow::UNSIGNED_INT, 0);
}
}
fn generate(&mut self, gl: &Context) {
self.regenerate = false;
unsafe {
match self.vao {
None => {self.vao = Some(gl.create_vertex_array().unwrap())}
_ => {}
}
match self.vbo {
None => {self.vbo = Some(gl.create_buffer().unwrap())}
_ => {}
}
match self.ebo {
None => {self.ebo = Some(gl.create_buffer().unwrap())}
_ => {}
}
gl.bind_vertex_array(self.vao);
let mut mesh_data: Vec<f32> = Vec::with_capacity(self.vertices.len() * 3 + self.normals.len() * 3);
for i in 0..self.vertices.len() {
mesh_data.push(self.vertices[i].x);
mesh_data.push(self.vertices[i].y);
mesh_data.push(self.vertices[i].z);
mesh_data.push(self.normals[i].x);
mesh_data.push(self.normals[i].y);
mesh_data.push(self.normals[i].z);
}
Self::fill_buffer(mesh_data.as_ptr() as *const u8, mesh_data.len(), self.vbo, gl);
Self::fill_buffer(self.indices.as_ptr() as *const u8, self.indices.len(), self.ebo, gl);
// vertices
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_f32(0, 3, glow::FLOAT, false, 6 * size_of::<f32>() as i32, 0); // stride is (6 * size_of::<f32>) because 3 for vertices and 3 for normals
// normals
gl.enable_vertex_attrib_array(1);
gl.vertex_attrib_pointer_f32(0, 3, glow::FLOAT, false, 6 * size_of::<f32>() as i32, 0); // stride is (6 * size_of::<f32>) because 3 for vertices and 3 for normals
}
}
unsafe fn fill_buffer(data: *const u8, num_elements: usize, dst_buffer: Option<NativeBuffer>, gl: &Context) {
unsafe {
let mesh_data_u8: &[u8] = std::slice::from_raw_parts(data, num_elements * size_of::<f32>());
gl.bind_buffer(glow::ARRAY_BUFFER, dst_buffer);
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, mesh_data_u8, glow::STATIC_DRAW);
}
}
}

View File

@ -0,0 +1,208 @@
use crate::camera::perspective_camera::PerspectiveCamera;
use crate::engine::input::Input;
use crate::engine::time::Time;
use crate::Extension;
use glow::{Context, HasContext};
use glutin::config::{ConfigTemplateBuilder, GlConfig};
use glutin::context::{ContextApi, ContextAttributesBuilder, NotCurrentContext, NotCurrentGlContext, PossiblyCurrentContext};
use glutin::display::{GetGlDisplay, GlDisplay};
use glutin::prelude::GlSurface;
use glutin::surface::{Surface, SwapInterval, WindowSurface};
use glutin_winit::{DisplayBuilder, GlWindow};
use nalgebra::{Matrix4, Vector3};
use raw_window_handle::HasWindowHandle;
use std::num::NonZeroU32;
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::ActiveEventLoop;
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowId};
pub struct Nebulix {
window: Option<Window>,
gl: Option<Context>,
gl_context: Option<PossiblyCurrentContext>,
gl_surface: Option<Surface<WindowSurface>>,
camera: PerspectiveCamera,
time: Option<Time>,
input: Input,
}
impl Nebulix {
pub fn new() -> Nebulix {
let input = Input::new(None);
Self {
window: None,
gl: None,
gl_context: None,
gl_surface: None,
camera: PerspectiveCamera::new(
Vector3::zeros(),
None,
None,
None,
None
),
time: None,
input,
}
}
}
impl ApplicationHandler for Nebulix {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let(gl, gl_context, gl_surface, window) = create_window_with_gl_context(event_loop);
unsafe {
gl.clear_color(0.0, 0.0, 0.0, 1.0);
}
self.camera.update_aspect(&window.inner_size().to_logical(window.scale_factor()));
self.window = Some(window);
self.gl = Some(gl);
self.gl_context = Some(gl_context);
self.gl_surface = Some(gl_surface);
self.time = Some(Time::new());
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {event_loop.exit()}
WindowEvent::Destroyed => {}
WindowEvent::Focused(_) => {}
WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer } => {
self.camera.update_aspect(&self.window.as_ref().unwrap().inner_size().to_logical(scale_factor));
}
WindowEvent::Resized(size) => {
self.gl_surface.as_ref().unwrap()
.resize(self.gl_context.as_ref().unwrap(), NonZeroU32::new(size.width).unwrap(), NonZeroU32::new(size.height).unwrap());
let logical_size: LogicalSize<i32> = size.to_logical(self.window.as_ref().unwrap().scale_factor());
unsafe {
self.gl.as_ref().unwrap().viewport(0, 0, logical_size.width, logical_size.height)
}
self.camera.update_aspect(&logical_size);
}
// Input system stuff
WindowEvent::KeyboardInput { event, ..} => {
match event.logical_key {
Key::Named(special) => { match event.state {
ElementState::Pressed => {self.input.special_key_pressed(special)}
ElementState::Released => {self.input.special_key_released(special)}
}}
Key::Character(char) => { match event.state {
ElementState::Pressed => {self.input.key_pressed(char)}
ElementState::Released => {self.input.key_released(char)}
}}
_ => ()
}
}
WindowEvent::ModifiersChanged(_) => {} // if lshift etc has been pressed
WindowEvent::CursorMoved { position, .. } => {
self.input.mouse_moved(position.to_logical(self.window.as_ref().unwrap().scale_factor()));
}
WindowEvent::CursorEntered { .. } => {}
WindowEvent::CursorLeft { .. } => {self.input.mouse_left_screen()}
WindowEvent::MouseWheel { .. } => {}
WindowEvent::MouseInput { .. } => {}
// Update loop
WindowEvent::RedrawRequested => {
self.time.as_mut().unwrap().update_delta_time();
self.camera.process_mouse(self.input.get_mouse_offset());
let mut move_direction: Vector3<f32> = Vector3::zeros();
if self.input.is_key_pressed("a") { move_direction.x -= 1.0f32; }
if self.input.is_key_pressed("d") { move_direction.x += 1.0f32; }
if self.input.is_key_pressed("w") { move_direction.z -= 1.0f32; }
if self.input.is_key_pressed("s") { move_direction.z += 1.0f32; }
if self.input.is_special_key_pressed(NamedKey::Space) { move_direction.y += 1.0f32; }
if self.input.is_special_key_pressed(NamedKey::Control) { move_direction.z += 1.0f32; }
self.camera.process_movement(self.time.as_mut().unwrap().delta_time(), move_direction.normalize_checked());
let gl = self.gl.as_ref().unwrap();
let gl_surface = self.gl_surface.as_ref().unwrap();
let gl_context = self.gl_context.as_ref().unwrap();
let model_matrix = Matrix4::new_translation(&Vector3::new(0.0f32, 0.0f32, -3.0f32));
let view_matrix = self.camera.view_matrix();
let projection_matrix = self.camera.projection_matrix();
unsafe {
gl.clear(glow::COLOR_BUFFER_BIT);
}
// Custom code HERE
// TODO: Next step is:
// - dynamically generate a mesh
// - use a shader to render it
// - and fly around in the scene
// After I confirmed that the above is working -> add textures
gl_surface.swap_buffers(&gl_context).unwrap();
self.window.as_ref().unwrap().request_redraw();
}
_ => ()
}
}
}
fn create_window_with_gl_context(event_loop: &ActiveEventLoop) -> (Context, PossiblyCurrentContext, Surface<WindowSurface>, Window) {
let window_attributes = Window::default_attributes().with_transparent(true);
let template = ConfigTemplateBuilder::new().with_alpha_size(8);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attributes));
let (window, gl_config) = display_builder.build(event_loop, template, |configs| {
configs.reduce(|accum, config| {
if config.num_samples() > accum.num_samples() {
config
}
else {
accum
}
}).unwrap()
}).unwrap();
let raw_window_handle = window
.as_ref()
.and_then(|window| window.window_handle().map(|handle| handle.as_raw()).ok());
let gl_display = gl_config.display();
let context_attributes = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(Some(glutin::context::Version {
major: 3,
minor: 3,
})))
.build(raw_window_handle);
let not_current_gl_context: NotCurrentContext;
unsafe {
not_current_gl_context = gl_display.create_context(&gl_config, &context_attributes).expect("OpenGL context creation failed");
};
let window = window.unwrap();
let attrs = window
.build_surface_attributes(<_>::default())
.expect("Failed to build surface attributes");
let gl_surface = unsafe {
gl_config.display().create_window_surface(&gl_config, &attrs).unwrap()
};
let gl_context = not_current_gl_context.make_current(&gl_surface).unwrap();
let gl: Context;
unsafe {
gl = Context::from_loader_function_cstr(|s| gl_display.get_proc_address(s));
};
gl_surface
.set_swap_interval(&gl_context, SwapInterval::Wait(NonZeroU32::new(1).unwrap())) // vsync on
// .set_swap_interval(&gl_context, SwapInterval::DontWait) // vsync off (let her rip)
.unwrap();
(gl, gl_context, gl_surface, window)
}

View File

@ -0,0 +1,102 @@
use glow::{Context, HasContext, NativeProgram, NativeShader};
use nalgebra::{Matrix4, Vector3};
enum ShaderType {
Vertex,
Fragment,
}
impl ShaderType {
fn to_u32(&self) -> u32 {
match self {
ShaderType::Vertex => {glow::VERTEX_SHADER}
ShaderType::Fragment => {glow::FRAGMENT_SHADER}
}
}
}
struct Shader<'a> {
gl: &'a Context,
program: NativeProgram,
}
impl<'a> Shader<'a> {
pub fn new(&self, gl: &'a Context, vertex_shader_file: String, fragment_shader_file: String) -> Self {
let vertex_shader = self.compile_shader(vertex_shader_file, ShaderType::Vertex);
let fragment_shader = self.compile_shader(fragment_shader_file, ShaderType::Fragment);
let program = self.create_program(vertex_shader, fragment_shader);
Self {
gl,
program,
}
}
pub fn use_shader(&self) {
unsafe {
self.gl.use_program(Some(self.program));
}
}
pub fn set_i32(&self, name: &str, value: i32) {
unsafe {
let location = self.gl.get_uniform_location(self.program, name);
self.gl.uniform_1_i32(location.as_ref(), value)
}
}
pub fn set_f32(&self, name: &str, value: f32) {
unsafe {
let location = self.gl.get_uniform_location(self.program, name);
self.gl.uniform_1_f32(location.as_ref(), value)
}
}
pub fn set_vector3_f32(&self, name: &str, value: Vector3<f32>) {
unsafe {
let location = self.gl.get_uniform_location(self.program, name);
self.gl.uniform_3_f32(location.as_ref(), value.x, value.y, value.z)
}
}
pub fn set_matrix4_f32(&self, name: &str, value: Matrix4<f32>) {
unsafe {
let location = self.gl.get_uniform_location(self.program, name);
self.gl.uniform_matrix_4_f32_slice(location.as_ref(), false, value.as_slice());
}
}
fn compile_shader(&self, shader_code: String, shader_type: ShaderType) -> NativeShader {
unsafe {
let shader = self.gl.create_shader(shader_type.to_u32()).unwrap();
self.gl.shader_source(shader, shader_code.as_str());
self.gl.compile_shader(shader);
if self.gl.get_shader_compile_status(shader) {
panic!("{}", self.gl.get_shader_info_log(shader));
}
shader
}
}
fn create_program(&self, vertex_shader: NativeShader, fragment_shader: NativeShader) -> NativeProgram {
unsafe {
let program = self.gl.create_program().unwrap();
self.gl.attach_shader(program, vertex_shader);
self.gl.attach_shader(program, fragment_shader);
self.gl.link_program(program);
if(!self.gl.get_program_link_status(program)) {
panic!("Error while linking shaders with message: \n{}", self.gl.get_program_info_log(program));
}
self.gl.detach_shader(program, vertex_shader);
self.gl.detach_shader(program, fragment_shader);
self.gl.delete_shader(vertex_shader);
self.gl.delete_shader(fragment_shader);
program
}
}
}

View File

@ -0,0 +1,25 @@
use std::time::Instant;
pub struct Time {
previous_time: Instant,
delta_time: f32,
}
impl Time {
pub fn delta_time(&mut self) -> f32 {
self.delta_time
}
pub(crate) fn new() -> Time {
Self {
previous_time: Instant::now(),
delta_time: 0.0,
}
}
/// Call this at the start of each frame
pub(crate) fn update_delta_time(&mut self) {
let frame_start_time = Instant::now();
self.delta_time = (frame_start_time - self.previous_time).as_secs_f32();
self.previous_time = frame_start_time;
}
}

View File

@ -0,0 +1,38 @@
use crate::engine::nebulix::Nebulix;
use nalgebra::Vector3;
use winit::event_loop::{ControlFlow, EventLoop};
mod camera;
mod engine;
trait Extension {
fn normalize_checked(&self) -> Self;
fn normalize_checked_mut(&mut self);
}
impl Extension for Vector3<f32> {
/// If the vector is equal to Vector3::zeroes() it will be returned. Otherwise, a normalised version of it will be returned, without mutating the vector itself.
fn normalize_checked(&self) -> Self {
if self.eq(&Vector3::zeros()) {
*self
} else {
self.normalize()
}
}
/// If the vector is equal to Vector3::zeroes() nothing will be done. Otherwise, this vector gets normalised in place.
fn normalize_checked_mut(&mut self) {
if self != &Vector3::zeros() {
self.normalize_mut();
}
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let mut engine = Nebulix::new();
event_loop.run_app(&mut engine).unwrap()
}