From 05acda46c198e7d1d82cee65ffe50c1f27d570a3 Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Sun, 3 Jul 2022 14:51:33 +0100
Subject: [PATCH] ray casting and demo

---
 demo/CMakeLists.txt                   |   1 +
 demo/data/topdown.yml                 |  76 ++++++++++++
 demo/demo/main.cpp                    |   8 ++
 demo/demo/topdown.cpp                 | 161 ++++++++++++++++++++++++++
 demo/include/topdown.hpp              |  45 +++++++
 fggl/gfx/ogl4/models.cpp              |  24 ++--
 fggl/input/camera_input.cpp           |  15 +++
 include/fggl/assets/loader.hpp        | 118 +++++++++++++++++++
 include/fggl/assets/manager.hpp       |  60 ++++++----
 include/fggl/assets/module.hpp        |  12 +-
 include/fggl/assets/types.hpp         |  50 ++++++++
 include/fggl/ecs3/prototype/world.hpp |   7 +-
 include/fggl/gfx/camera.hpp           |  30 +++++
 include/fggl/input/camera_input.hpp   |   2 +
 include/fggl/math/types.hpp           |  14 +++
 include/fggl/phys/types.hpp           |   6 +
 include/fggl/scenes/game.hpp          |   4 +
 17 files changed, 595 insertions(+), 38 deletions(-)
 create mode 100644 demo/data/topdown.yml
 create mode 100644 demo/demo/topdown.cpp
 create mode 100644 demo/include/topdown.hpp
 create mode 100644 include/fggl/assets/loader.hpp
 create mode 100644 include/fggl/assets/types.hpp

diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt
index 4c8f3dd..27ae418 100644
--- a/demo/CMakeLists.txt
+++ b/demo/CMakeLists.txt
@@ -6,6 +6,7 @@ add_executable(demo
         demo/main.cpp
         demo/GameScene.cpp
         demo/rollball.cpp
+        demo/topdown.cpp
         )
 
 
diff --git a/demo/data/topdown.yml b/demo/data/topdown.yml
new file mode 100644
index 0000000..defd6c0
--- /dev/null
+++ b/demo/data/topdown.yml
@@ -0,0 +1,76 @@
+---
+prefabs:
+  - name: "wallX"
+    components:
+      Transform:
+      StaticMesh:
+        pipeline: phong
+        shape:
+          type: box
+          scale: [1.0, 5.0, 41]
+      phys::Body:
+        type: static
+        shape:
+          type: box
+          extents: [0.5, 2.5, 20.5]
+  # Wall Z shorter to avoid z-fighting
+  - name: "wallZ"
+    components:
+      Transform:
+      StaticMesh:
+        pipeline: phong
+        shape:
+          type: box
+          scale: [39, 5, 1]
+      phys::Body:
+        type: static
+        shape:
+          type: box
+          extents: [ 19.5, 2.5, 0.5 ]
+  - name: "floor"
+    components:
+      Transform:
+      StaticMesh:
+        pipeline: phong
+        shape:
+          type: box # we don't (currently) support planes...
+          scale: [39, 0.5, 39]
+      phys::Body:
+        type: static
+        shape:
+          type: box # we don't (currently) support planes...
+          extents: [19.5, 0.25, 19.5]
+  - name: player
+    components:
+      Transform:
+      StaticMesh:
+        pipeline: phong
+        shape:
+          type: sphere
+      gfx::material:
+        ambient: [0.25, 0.25, 0.25]
+        diffuse: [0.4, 0.4, 0.4]
+        specular: [0.774597,0.774597,0.774597]
+        shininess: 0.6
+      phys::Body:
+        shape:
+          type: sphere
+          radius: 1
+  - name: collectable
+    components:
+      Transform:
+      StaticMesh:
+        pipeline: phong
+        shape:
+          type: box
+      gfx::material:
+        ambient: [0.0215, 0.1754, 0.0215]
+        diffuse: [1, 1, 1]
+        specular: [0.0633, 0.727811, 0.633]
+        shininess: 0.6
+      phys::Body:
+        type: kinematic
+        shape:
+          type: box
+      phys::Callbacks:
+      phys::Cache:
\ No newline at end of file
diff --git a/demo/demo/main.cpp b/demo/demo/main.cpp
index 3bc9aff..2783ab2 100644
--- a/demo/demo/main.cpp
+++ b/demo/demo/main.cpp
@@ -41,6 +41,7 @@
 
 #include "GameScene.h"
 #include "rollball.hpp"
+#include "topdown.hpp"
 
 static void setup_menu(fggl::App& app) {
 	auto *menu = app.addState<fggl::scenes::BasicMenu>("menu");
@@ -58,6 +59,12 @@ static void setup_menu(fggl::App& app) {
 		app.change_state("rollball");
 	});
 
+	menu->add("Top Down", [&app]() {
+		auto* audio = app.service<fggl::audio::AudioService>();
+		audio->play("click.ogg", false);
+		app.change_state("topdown");
+	});
+
 	menu->add("quit", [&app]() {
 		auto* audio = app.service<fggl::audio::AudioService>();
 		audio->play("click.ogg", false);
@@ -100,6 +107,7 @@ int main(int argc, const char* argv[]) {
 	setup_menu(app);
     app.addState<GameScene>("game");
 	app.addState<demo::RollBall>("rollball");
+	app.addState<demo::TopDown>("topdown");
 
 	return app.run(argc, argv);
 }
diff --git a/demo/demo/topdown.cpp b/demo/demo/topdown.cpp
new file mode 100644
index 0000000..25f7a23
--- /dev/null
+++ b/demo/demo/topdown.cpp
@@ -0,0 +1,161 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 03/07/22.
+//
+
+#include "topdown.hpp"
+
+#include "fggl/data/storage.hpp"
+#include "fggl/gfx/camera.hpp"
+#include "fggl/input/camera_input.hpp"
+#include "fggl/ecs3/prototype/loader.hpp"
+
+namespace demo {
+
+	static void create_topdown_camera(fggl::ecs3::World& world) {
+		auto prototype = world.create(false);
+
+		// setup camera position/transform
+		auto* transform = world.add<fggl::math::Transform>(prototype);
+		if ( transform != nullptr) {
+			transform->origin(glm::vec3(10.0f, 50.0f, 10.0f));
+		}
+
+		// setup camera components
+		auto* camera = world.add<fggl::gfx::Camera>(prototype);
+		camera->target = glm::vec3(0.0f, 0.0f, 0.0f);
+
+		auto* cameraKeys = world.add<fggl::input::FreeCamKeys>(prototype);
+		if ( cameraKeys != nullptr ) {
+			cameraKeys->forward = glfwGetKeyScancode(GLFW_KEY_W);
+			cameraKeys->backward = glfwGetKeyScancode(GLFW_KEY_S);
+			cameraKeys->left = glfwGetKeyScancode(GLFW_KEY_A);
+			cameraKeys->right = glfwGetKeyScancode(GLFW_KEY_D);
+			cameraKeys->rotate_cw = glfwGetKeyScancode(GLFW_KEY_Q);
+			cameraKeys->rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
+		}
+	}
+
+	static void place_cover_boxes(fggl::ecs3::World& world) {
+		std::array<fggl::math::vec3,8> boxPos = {{
+													 {-10.0F, 0.0F, -10.0F},
+													 {-10.0F, 0.0F,  10.0F},
+													 { 10.0F, 0.0F, -10.0F},
+													 { 10.0F, 0.0F,  10.0F},
+													 {-10.0F, 0.0F,   0.0F},
+													 {  0.0F, 0.0F, -10.0F},
+													 { 10.0F, 0.0F,   0.0F},
+													 {  0.0F, 0.0F,  10.0F},
+												 }};
+		for (auto pos : boxPos) {
+			auto box = world.createFromPrototype("collectable");
+			auto* transform = world.get<fggl::math::Transform>(box);
+			transform->origin(pos);
+		}
+	}
+
+	static void build_arena(fggl::ecs3::World& world) {
+		{
+			auto floor = world.createFromPrototype("floor");
+			auto* transform = world.get<fggl::math::Transform>(floor);
+			transform->origin({0.0F, -2.5F, 0.0F});
+			fggl::debug::log("created floor: {}", floor);
+		}
+
+		fggl::math::vec2 size{40.0F, 40.0F};
+		for (auto side : {-1.0F, 1.0F})
+		{
+			{
+				auto northWall = world.createFromPrototype("wallX");
+				auto *transform = world.get<fggl::math::Transform>(northWall);
+				transform->origin({size.x / 2 * side, 0.0F, 0.0F});
+			}
+			{
+				auto westWall = world.createFromPrototype("wallZ");
+				auto* transform = world.get<fggl::math::Transform>(westWall);
+				transform->origin({0.0F, 0.0F, size.y/2 * side });
+			}
+		}
+
+		place_cover_boxes(world);
+	}
+
+static void process_camera(fggl::ecs3::World& ecs, const fggl::input::Input& input) {
+	auto cameras = ecs.findMatching<fggl::gfx::Camera>();
+	if ( !cameras.empty() ) {
+		fggl::ecs3::entity_t cam = cameras[0];
+		fggl::input::process_scroll(ecs, input, cam);
+		fggl::input::process_freecam(ecs, input, cam);
+		fggl::input::process_edgescroll(ecs, input, cam);
+	}
+}
+
+static void populate_sample_level(fggl::ecs3::World& world) {
+	create_topdown_camera(world);
+
+	build_arena(world);
+}
+
+TopDown::TopDown(fggl::App& app) : fggl::scenes::Game(app) {
+
+}
+
+void TopDown::activate() {
+	Game::activate();
+
+	auto* storage = m_owner.service<fggl::data::Storage>();
+	fggl::ecs3::load_prototype_file(world(), *storage, "topdown.yml");
+
+	// create a sample level
+	populate_sample_level(world());
+}
+
+void TopDown::update() {
+	Game::update();
+
+	process_camera(world(), input());
+	if ( input().mouse.pressed(fggl::input::MouseButton::LEFT) ) {
+		pick_object();
+	}
+}
+
+void TopDown::pick_object() {
+	auto cameras = world().findMatching<fggl::gfx::Camera>();
+	if ( cameras.empty() ) {
+		return;
+	}
+
+	fggl::ecs3::entity_t cam = cameras[0];
+
+	fggl::math::vec2 position {
+		input().mouse.axis(fggl::input::MouseAxis::X),
+		input().mouse.axis(fggl::input::MouseAxis::Y),
+	};
+
+	auto ray = fggl::gfx::get_camera_ray(world(), cam, position);
+	auto hit = phys().raycast(ray);
+	if ( hit != fggl::ecs3::NULL_ENTITY) {
+		fggl::debug::log("hit: {}", hit);
+	} else {
+		fggl::debug::log("no hit");
+	}
+}
+
+void TopDown::render(fggl::gfx::Graphics& gfx) {
+	Game::render(gfx);
+}
+
+}
\ No newline at end of file
diff --git a/demo/include/topdown.hpp b/demo/include/topdown.hpp
new file mode 100644
index 0000000..af4642f
--- /dev/null
+++ b/demo/include/topdown.hpp
@@ -0,0 +1,45 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 03/07/22.
+//
+
+#ifndef DEMO_TOPDOWN_HPP
+#define DEMO_TOPDOWN_HPP
+
+#include "fggl/scenes/game.hpp"
+
+namespace demo {
+
+	class TopDown : public fggl::scenes::Game {
+
+		public:
+			explicit TopDown(fggl::App& app);
+			void activate() override;
+
+			void update() override;
+			void render(fggl::gfx::Graphics& gfx) override;
+
+		private:
+			constexpr static fggl::math::vec3 HINT_COLOUR{0.5f, 0.0f, 0.0f};
+			fggl::math::vec3 cameraOffset = {-15.0F, 15.0F, 0.0F};
+
+			void pick_object();
+
+	};
+
+}
+
+#endif //DEMO_TOPDOWN_HPP
diff --git a/fggl/gfx/ogl4/models.cpp b/fggl/gfx/ogl4/models.cpp
index 4cf44b4..ed30c5c 100644
--- a/fggl/gfx/ogl4/models.cpp
+++ b/fggl/gfx/ogl4/models.cpp
@@ -137,25 +137,29 @@ namespace fggl::gfx::ogl4 {
 
 			// TODO lighting needs to not be this...
 			math::vec3 lightPos{0.0f, 10.0f, 0.0f};
+			std::shared_ptr<ogl::Shader> shader = nullptr;
 
 			auto renderables = world.findMatching<StaticModel>();
 			for ( const auto& entity : renderables ){
-				auto* transform = world.get<math::Transform>(entity);
+				// ensure that the model pipeline actually exists...
 				StaticModel* model = world.get<StaticModel>(entity);
-
-				// grouping by shader would mean we only need to send the model matrix...
-				// TODO clean shader API
-				auto shader = model->pipeline;
-				if ( shader == nullptr ) {
+				if ( model->pipeline == nullptr ) {
 					spdlog::warn("shader was null, aborting render");
 					continue;
 				}
 
-				// setup shader uniforms
-				shader->use();
+				// check if we switched shaders
+				if ( shader != model->pipeline ) {
+					// new shader - need to re-send the view and projection matrices
+					shader = model->pipeline;
+					shader->use();
+					shader->setUniformMtx(shader->uniform("view"), viewMatrix);
+					shader->setUniformMtx(shader->uniform("projection"), projectionMatrix);
+				}
+
+				// set model transform
+				auto* transform = world.get<math::Transform>(entity);
 				shader->setUniformMtx(shader->uniform("model"), transform->model());
-				shader->setUniformMtx(shader->uniform("view"), viewMatrix);
-				shader->setUniformMtx(shader->uniform("projection"), projectionMatrix);
 
 				// material detection with fallback
 				auto* material = &gfx::DEFAULT_MATERIAL;
diff --git a/fggl/input/camera_input.cpp b/fggl/input/camera_input.cpp
index 24afeab..e0b0414 100644
--- a/fggl/input/camera_input.cpp
+++ b/fggl/input/camera_input.cpp
@@ -54,6 +54,21 @@ namespace fggl::input {
         camTransform->origin(finalPos);
     }
 
+	void process_scroll(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam, float minZoom, float maxZoom){
+		auto* camTransform = ecs.get<fggl::math::Transform>(cam);
+		auto* camComp = ecs.get<fggl::gfx::Camera>(cam);
+
+		const glm::vec3 dir = ( camTransform->origin() - camComp->target );
+		const glm::vec3 forward = glm::normalize( dir );
+
+		glm::vec3 motion(0.0F);
+		float delta = input.mouse.axis( fggl::input::MouseAxis::SCROLL_Y );
+		if ( (glm::length( dir ) < maxZoom && delta < 0.0f) || (glm::length( dir ) > minZoom && delta > 0.0f) ) {
+			motion -= (forward * delta);
+			camTransform->origin(camTransform->origin() + motion);
+		}
+	}
+
     void process_freecam(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam) {
         float rotationValue = 0.0f;
         glm::vec3 translation(0.0f);
diff --git a/include/fggl/assets/loader.hpp b/include/fggl/assets/loader.hpp
new file mode 100644
index 0000000..be85581
--- /dev/null
+++ b/include/fggl/assets/loader.hpp
@@ -0,0 +1,118 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 03/07/22.
+//
+
+#ifndef FGGL_ASSETS_LOADER_HPP
+#define FGGL_ASSETS_LOADER_HPP
+
+#include <map>
+#include <string>
+#include <memory>
+#include <functional>
+#include <queue>
+#include <variant>
+
+#include "fggl/assets/types.hpp"
+#include "fggl/data/storage.hpp"
+
+namespace fggl::assets {
+
+	enum class LoadType {
+		DIRECT, // given pointer to persistent memory
+		STAGED, // given pointer to temp memory
+		PATH // given filesystem::path
+	};
+
+	struct ResourceRequest {
+		AssetGUID m_guid;
+		AssetType m_type;
+	};
+
+	class Loader {
+		public:
+			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::Loader");
+			explicit inline Loader(data::Storage* storage)  : m_storage(storage), m_parent(nullptr) {}
+			explicit Loader(Loader* parent, data::Storage* storage) : m_parent(parent), m_storage(storage) {};
+
+			// no move, no copy.
+			Loader(const Loader&) = delete;
+			Loader& operator=(const Loader&) = delete;
+			Loader(Loader&&) = delete;
+			Loader& operator=(Loader&&) = delete;
+
+			inline void setFactory(AssetType type, Checkin fn, LoadType loading = LoadType::DIRECT) {
+				m_factories[type] = std::make_pair(fn, loading);
+			}
+			inline void unsetFactory(AssetType type) {
+				m_factories.erase(type);
+			}
+
+			inline void request(const AssetGUID& guid, const AssetType& type) {
+				m_requests.push(ResourceRequest{guid, type});
+			}
+
+			void load(const AssetGUID guid, const AssetType& type) {
+				auto path = m_storage->resolvePath(data::StorageType::Data, guid);
+
+				auto& config = m_factories.at(type);
+				switch (config.second) {
+					case LoadType::DIRECT:
+						// TODO we load the data into main memory and give a pointer to it.
+						break;
+					case LoadType::STAGED:
+						// TODO we load the data into temp memory and give a pointer to it.
+						break;
+					case LoadType::PATH:
+						config.first(guid, AssetData(&path));
+						break;
+				}
+			}
+
+			void progress() {
+				if (m_requests.empty()) {
+					return;
+				}
+				auto& request = m_requests.front();
+				load(request.m_guid, request.m_type);
+				m_requests.pop();
+			}
+
+			[[nodiscard]]
+			inline bool done() const {
+				return m_requests.empty();
+			}
+
+			/**
+			 * Complete all remaining loading requests, blocking until complete.
+			 */
+			inline void finish() {
+				while ( !done() ) {
+					progress();
+				}
+			}
+
+		private:
+			Loader* m_parent = nullptr;
+			using Config = std::pair<Checkin, LoadType>;
+			data::Storage* m_storage;
+			std::queue<ResourceRequest> m_requests;
+			std::map<AssetType, Config> m_factories;
+	};
+
+} // namespace fggl::assets
+
+#endif //FGGL_ASSETS_LOADER_HPP
diff --git a/include/fggl/assets/manager.hpp b/include/fggl/assets/manager.hpp
index 40cdf39..9963ec4 100644
--- a/include/fggl/assets/manager.hpp
+++ b/include/fggl/assets/manager.hpp
@@ -24,42 +24,54 @@
 #include <functional>
 #include <memory>
 
-#include "fggl/data/storage.hpp"
+#include "fggl/assets/types.hpp"
 #include "fggl/util/safety.hpp"
 
 namespace fggl::assets {
 
-	struct AssetTag{};
-	using AssetType = util::OpaqueName<std::string_view, AssetTag>;
-
-	struct AssetData {
-		void* data;
-		std::size_t size;
-	};
-
-	struct AssetCallbacks {
-		std::function<void(const AssetType&, AssetData)> init;
-		std::function<void(const AssetType&, AssetData)> destroy;
-	};
-
-	struct Asset{};
-
 	class AssetManager {
 		public:
-			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::AssetModule");
+			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::Manager");
 			using AssetGUID = std::string;
 
-			AssetManager(data::Storage* storage) : m_storage(storage) {}
+			AssetManager() = default;
 			virtual ~AssetManager() = default;
 
-			void load(const AssetType&, const AssetGUID& name);
-			void loadToTemp(const AssetType&, const AssetGUID& name);
-			void unload(const AssetGUID& name);
+			// no move, no copy.
+			AssetManager(const AssetManager&) = delete;
+			AssetManager& operator=(const AssetManager&) = delete;
+			AssetManager(AssetManager&&) = delete;
+			AssetManager& operator=(AssetManager&&) = delete;
+
+			inline AssetRefRaw getRaw(const AssetGUID& guid) const {
+				return m_registry.at(guid).asset;
+			}
+
+			template<typename T>
+			AssetRef<T> get(const AssetGUID& guid) const {
+				return std::dynamic_pointer_cast<T>(getRaw(guid));
+			}
+
+			inline void require(const AssetGUID& guid) {
+				m_registry.at(guid).refCount++;
+			}
+
+			void release(const AssetGUID& guid) {
+				m_registry.at(guid).refCount--;
+			}
 
 		private:
-			data::Storage* m_storage;
-			std::map<AssetGUID, std::shared_ptr<Asset>> m_registry;
-			std::map<AssetType, AssetCallbacks> m_callbacks;
+			struct AssetRecord {
+				std::shared_ptr<Asset> asset = nullptr;
+				std::size_t refCount;
+
+				inline ~AssetRecord(){
+					if ( asset != nullptr ) {
+						asset->release();
+					}
+				}
+			};
+			std::map<AssetGUID, AssetRecord> m_registry;
 	};
 
 
diff --git a/include/fggl/assets/module.hpp b/include/fggl/assets/module.hpp
index a4079cb..a925f64 100644
--- a/include/fggl/assets/module.hpp
+++ b/include/fggl/assets/module.hpp
@@ -22,12 +22,14 @@
 #include "fggl/modules/module.hpp"
 #include "fggl/data/module.hpp"
 #include "fggl/assets/manager.hpp"
+#include "fggl/assets/loader.hpp"
 
 namespace fggl::assets {
 
 	struct AssetFolders {
 		constexpr static const char* name = "fggl::assets::Folders";
-		constexpr static const std::array<modules::ModuleService, 1> provides = {
+		constexpr static const std::array<modules::ModuleService, 2> provides = {
+			Loader::service,
 			AssetManager::service
 		};
 		constexpr static const std::array<modules::ModuleService, 1> depends = {
@@ -37,9 +39,13 @@ namespace fggl::assets {
 	};
 
 	bool asset_factory(modules::ModuleService service, modules::Services& services) {
-		if (service == AssetManager::service) {
+		if ( service == Loader::service) {
 			auto storage = services.get<data::Storage>();
-			services.create<AssetManager>(storage);
+			services.create<Loader>(storage);
+			return true;
+		}
+		if (service == AssetManager::service) {
+			services.create<AssetManager>();
 			return true;
 		}
 		return false;
diff --git a/include/fggl/assets/types.hpp b/include/fggl/assets/types.hpp
new file mode 100644
index 0000000..d943d63
--- /dev/null
+++ b/include/fggl/assets/types.hpp
@@ -0,0 +1,50 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 03/07/22.
+//
+
+#ifndef FGGL_ASSETS_TYPES_HPP
+#define FGGL_ASSETS_TYPES_HPP
+
+#include <filesystem>
+#include "fggl/util/safety.hpp"
+
+namespace fggl::assets{
+	using AssetType = util::OpaqueName<std::string_view, struct AssetTag>;
+	using AssetGUID = std::string;
+	using AssetPath = std::filesystem::path;
+
+	struct Asset {
+		AssetType m_type;
+		virtual void release() = 0;
+		virtual bool active() = 0;
+	};
+
+	struct MemoryBlock {
+		void* data;
+		std::size_t size;
+	};
+
+	using AssetRefRaw = std::shared_ptr<Asset>;
+
+	template<typename T>
+	using AssetRef = std::shared_ptr<T>;
+
+	using AssetData = std::variant<MemoryBlock, AssetPath*, FILE*>;
+	using Checkin = std::function<AssetRefRaw(const AssetGUID&, const AssetData&)>;
+}
+
+#endif //FGGL_ASSETS_TYPES_HPP
diff --git a/include/fggl/ecs3/prototype/world.hpp b/include/fggl/ecs3/prototype/world.hpp
index db733e0..9e0a62a 100644
--- a/include/fggl/ecs3/prototype/world.hpp
+++ b/include/fggl/ecs3/prototype/world.hpp
@@ -139,7 +139,12 @@ namespace fggl::ecs3::prototype {
 			}
 
 			inline entity_t createFromPrototype(const std::string& name) {
-				return copy(findPrototype(name) );
+				auto prototype = findPrototype(name);
+				if ( prototype == NULL_ENTITY) {
+					debug::log(debug::Level::warning, "attempted to create from non-existant prototype: {}", name);
+					return NULL_ENTITY;
+				}
+				return copy( prototype );
 			}
 
 			entity_t copy(entity_t prototype) {
diff --git a/include/fggl/gfx/camera.hpp b/include/fggl/gfx/camera.hpp
index a03d430..c1fcf98 100644
--- a/include/fggl/gfx/camera.hpp
+++ b/include/fggl/gfx/camera.hpp
@@ -28,6 +28,36 @@ namespace fggl::gfx {
 		float farPlane = 100.0f;
 	};
 
+	inline math::mat4 calc_proj_matrix(const Camera* camera) {
+		return glm::perspective(camera->fov, camera->aspectRatio, camera->nearPlane, camera->farPlane);
+	}
+
+	inline math::Ray get_camera_ray(const ecs3::World& world, const ecs3::entity_t camera, math::vec2 position) {
+		auto* const camTransform = world.get<fggl::math::Transform>(camera);
+		auto* const camComp = world.get<fggl::gfx::Camera>(camera);
+
+		const auto projMatrix = fggl::gfx::calc_proj_matrix(camComp);
+		const auto viewMatrix = fggl::math::calc_view_matrix(camTransform);
+
+		glm::vec4 startNDC {
+			position.x,
+			position.y,
+			-1.0f,
+			1.0f
+		};
+		glm::vec4 endNDC {
+			position.x,
+			position.y,
+			0.0f,
+			1.0f
+		};
+
+		fggl::math::mat4 M = glm::inverse( projMatrix * viewMatrix );
+		glm::vec3 start = M * startNDC;
+		glm::vec3 end = M * endNDC;
+		return { start, glm::normalize(end - start) };
+	}
+
 };
 
 #endif
diff --git a/include/fggl/input/camera_input.hpp b/include/fggl/input/camera_input.hpp
index 1550a65..db85605 100644
--- a/include/fggl/input/camera_input.hpp
+++ b/include/fggl/input/camera_input.hpp
@@ -38,6 +38,8 @@ namespace fggl::input {
 		scancode_t rotate_ccw;
 	};
 
+	void process_scroll(fggl::ecs3::World &ecs, const Input &input, fggl::ecs::entity_t cam, float minZoom = 10.0F, float maxZoom = 50.0F);
+
 	/**
 	 * Process the camera based on rotation around a fixed point.
 	 *
diff --git a/include/fggl/math/types.hpp b/include/fggl/math/types.hpp
index 8349557..ff88aae 100644
--- a/include/fggl/math/types.hpp
+++ b/include/fggl/math/types.hpp
@@ -113,6 +113,11 @@ namespace fggl::math {
 		return modelMatrix(offset, glm::quat(eulerAngles));
 	}
 
+	struct Ray {
+		vec3 origin;
+		vec3 direction;
+	};
+
 	struct Transform {
 			constexpr static const char name[] = "Transform";
 
@@ -217,6 +222,15 @@ namespace fggl::math {
 			vec3 m_scale;
 	};
 
+
+	inline math::mat4 calc_view_matrix(const Transform* transform) {
+		return glm::lookAt(transform->origin(), transform->origin() + transform->forward(), transform->up());
+	}
+
+	inline math::mat4 calc_view_matrix(const Transform* transform, vec3 target) {
+		return glm::lookAt(transform->origin(), target, transform->up());
+	}
+
 }
 
 // feels a bit strange to be doing this...
diff --git a/include/fggl/phys/types.hpp b/include/fggl/phys/types.hpp
index bbc2b46..8d6bdee 100644
--- a/include/fggl/phys/types.hpp
+++ b/include/fggl/phys/types.hpp
@@ -106,6 +106,12 @@ namespace fggl::phys {
 			// query methods (first cut - unstable APIs)
 			virtual std::vector<ContactPoint> scanCollisions(ecs3::entity_t entity) = 0;
 			virtual ecs3::entity_t raycast(math::vec3 from, math::vec3 to) = 0;
+
+			inline ecs3::entity_t raycast(math::Ray ray, float maxDist = 1000.0F) {
+				return raycast(ray.origin, ray.origin + ray.direction * maxDist);
+			}
+
+
 			virtual std::vector<ecs3::entity_t> raycastAll(math::vec3 from, math::vec3 to) = 0;
 			virtual std::vector<ecs3::entity_t> sweep(PhyShape& shape, math::Transform& from, math::Transform& to) = 0;
 
diff --git a/include/fggl/scenes/game.hpp b/include/fggl/scenes/game.hpp
index 6d88372..5997060 100644
--- a/include/fggl/scenes/game.hpp
+++ b/include/fggl/scenes/game.hpp
@@ -40,6 +40,10 @@ namespace fggl::scenes {
 				return *m_world;
 			}
 
+			inline auto phys() -> phys::PhysicsEngine& {
+				return *m_phys;
+			}
+
 			inline auto input() -> input::Input& {
 				return *m_input;
 			}
-- 
GitLab