diff --git a/CMakeLists.txt b/CMakeLists.txt
index c806330973304b785780e9d644a6808b9e101149..70753536ad8a4081912073c2a5db6394005e0964 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,8 @@
 cmake_minimum_required(VERSION 3.16)
 set(namespace "fggl")
 
+set(CMAKE_CXX_STANDARD 20)
+
 option(FGGL_CONAN "Should we use conan to find missing dependencies?" ON)
 set(CONAN_BUILD_TYPE "Debug")
 
diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt
index 4318eb64b7f405717aa5e6fe7ca2b106fd6f99e9..f2e3b4c3ebef8b8eeb3b20700deae2c18df54cab 100644
--- a/demo/CMakeLists.txt
+++ b/demo/CMakeLists.txt
@@ -15,6 +15,9 @@ target_include_directories(demo
 )
 
 target_link_libraries(demo fggl)
+
+find_package(spdlog)
+target_link_libraries(demo spdlog::spdlog)
 #target_include_directories(FgglDemo PUBLIC ${PROJECT_BINARY_DIR})
 
 # rssources
diff --git a/demo/data/include/phong.glsl b/demo/data/include/phong.glsl
index 1cf3dfb7c05119a907673e6b19f06198ce32f3c3..65cc6a4f1e96bc18fc90ab1eaf7292161b074f46 100644
--- a/demo/data/include/phong.glsl
+++ b/demo/data/include/phong.glsl
@@ -1,5 +1,118 @@
 /**
  * OpenGL phong lighting model.
+ * Adapted from LearnOpenGL.com, by Joey de Vries, CC BY-NC 4.0
  */
 
-// TODO write script
\ No newline at end of file
+struct Material {
+    sampler2D diffuse;
+    sample2D specular;
+    float shininess;
+};
+
+struct DirectionalLight {
+    vec3 direction;
+
+    vec3 ambient;
+    vec3 diffuse;
+    vec3 specular;
+};
+
+struct PointLight {
+    vec3 position;
+
+    vec3 ambient;
+    vec3 diffuse;
+    vec3 specular;
+
+    float constant;
+    float linear;
+    float quadratic;
+};
+
+struct SpotLight {
+    vec3 position;
+    vec3 direction;
+
+    vec3 ambient;
+    vec3 diffuse;
+    vec3 specular;
+
+    float constant;
+    float linear;
+    float quadratic;
+
+    float cutOff;
+    float outerCutOff;
+};
+
+vec3 CalcDirectionalLight(DirectionalLight light, vec3 normal, vec3 viewDir) {
+    vec3 lightDir = normalize(-light.direction);
+
+    // diffuse shading
+    float diff = max(dot(normal, lightDir), 0.0);
+
+    // specular shading
+    vec3 reflectDir = reflect(-lightDir, normal);
+    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
+
+    // combine results
+    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
+    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
+    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
+    return (ambient + diffuse + specular);
+}
+
+vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) {
+    vec3 lightDir = normalize(light.position - fragPos);
+
+    // diffuse shading
+    float diff = max(dot(normal, lightDir), 0.0);
+
+    // specular shading
+    vec3 reflectDir = reflect(-lightDir, normal);
+    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
+
+    // attenuation
+    float distance = length(light.position - fragPos);
+    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
+
+    // combine results
+    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
+    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
+    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
+
+    ambient *= attenuation;
+    diffuse *= attenuation;
+    specular *= attenuation;
+    return (ambient + diffuse + specular);
+}
+
+vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) {
+    vec3 lightDir = normalize(light.position - fragPos);
+
+    // diffuse shading
+    float diff = max(dot(normal, lightDir), 0.0);
+
+    // specular shading
+    vec3 reflectDir = reflect(-lightDir, normal);
+    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
+
+    // attenuation
+    float distance = length(light.position - fragPos);
+    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
+
+    // spotlight intensity
+    float theta = dot(lightDir, normalize(-light.direction));
+    float epsilon = light.cutOff - light.outerCutOff;
+    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
+
+    // combine results
+    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
+    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
+    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
+
+    ambient *= attenuation * intensity;
+    diffuse *= attenuation * intensity;
+    specular *= attenuation * intensity;
+    return (ambient + diffuse + specular);
+}
\ No newline at end of file
diff --git a/demo/demo/rollball.cpp b/demo/demo/rollball.cpp
index d20e2f3e784ad7ffeb309b5a05895f350b14dbb3..580c96541747377b3ee64d7b793593abcc1dc649 100644
--- a/demo/demo/rollball.cpp
+++ b/demo/demo/rollball.cpp
@@ -25,6 +25,7 @@
 #include "fggl/input/camera_input.h"
 #include "fggl/util/service.h"
 #include "fggl/ecs3/prototype/loader.hpp"
+#include "fggl/debug/draw.hpp"
 
 #include <array>
 
@@ -198,6 +199,10 @@ namespace demo {
 					moved = true;
 				}
 
+				if (input.keyboard.pressed(glfwGetKeyScancode(GLFW_KEY_SPACE))) {
+					state.mode = (DebugMode)( ((int)state.mode + 1) % 3 );
+				}
+
 				// setup dynamic force
 				if ( moved ) {
 					float forceFactor = 3.0F;
@@ -216,33 +221,96 @@ namespace demo {
 					camTransform->origin( camComp->target + cameraOffset );
 				}
 
-				// rotation
-				float closestDistance = FLT_MAX;
-				fggl::ecs::entity_t closestEntity = fggl::ecs::NULL_ENTITY;
-				auto playerPos = world.get<fggl::math::Transform>(state.player)->origin();
-
-				for ( auto& entity : state.collectables ) {
-					if ( world.alive(entity) ) {
-						auto* transform = world.get<fggl::math::Transform>(entity);
-
-						// rotate the cubes
-						fggl::math::vec3 angles{1.0F, 2.0F, 3.0F};
-						transform->rotateEuler( angles * (60.0F / 1000) );
-
-						auto distance = glm::distance(transform->origin(), playerPos);
-						if ( distance < closestDistance) {
-							closestDistance = distance;
-							closestEntity = entity;
+				// distance mode
+				if ( state.mode == DebugMode::DISTANCE ) {
+					closestPickup(world);
+				} else {
+					if ( state.closestPickup != fggl::ecs::NULL_ENTITY ) {
+						if ( world.alive(state.closestPickup) ){
+							auto *renderer = world.tryGet<fggl::gfx::PhongMaterial>(state.closestPickup);
+							if (renderer != nullptr) {
+								renderer->diffuse = {1.0f, 1.0f, 1.0f};
+							}
 						}
 
-						//auto* renderer = world.get<fggl::data::StaticMesh>( entity );
-						//renderer->hintColour = fggl::math::vec3(1.0F, 1.0F, 1.0F);
+						state.closestPickup = fggl::ecs::NULL_ENTITY;
 					}
 				}
+
+				spinCubes(world);
 				state.time += (60.0f / 1000);
 		}
 	}
 
+	void RollBall::closestPickup(fggl::ecs3::World &world) {
+		float closestDistance = FLT_MAX;
+		fggl::ecs::entity_t closestEntity = fggl::ecs::NULL_ENTITY;
+		auto playerPos = world.get<fggl::math::Transform>(state.player)->origin();
+
+		for ( const auto& pickup : state.collectables ) {
+			if ( !world.alive(pickup) ) {
+				continue;
+			}
+
+			auto* transform = world.get<fggl::math::Transform>(pickup);
+			auto distance = glm::distance(transform->origin(), playerPos);
+			if ( distance < closestDistance) {
+				closestDistance = distance;
+				closestEntity = pickup;
+			}
+		}
+
+		if ( state.closestPickup != closestEntity ) {
+			if ( state.closestPickup != fggl::ecs::NULL_ENTITY ){
+				// deal with the previous closest pickup
+				auto *renderer = world.tryGet<fggl::gfx::PhongMaterial>(state.closestPickup);
+				if (renderer != nullptr) {
+					renderer->diffuse = {1.0f, 1.0f, 1.0f};
+				}
+			}
+
+			// now, deal with the new closest
+			state.closestPickup = closestEntity;
+			if (closestEntity != fggl::ecs::NULL_ENTITY) {
+				auto *renderer = world.tryGet<fggl::gfx::PhongMaterial>(state.closestPickup);
+				if (renderer != nullptr) {
+					renderer->diffuse = {0.0f, 0.0f, 1.0f};
+				}
+			}
+		}
+
+		// closest pickup should face the player
+		if ( state.closestPickup != fggl::ecs::NULL_ENTITY) {
+			auto* transform = world.get<fggl::math::Transform>(state.closestPickup);
+			auto* playerTransform = world.get<fggl::math::Transform>( state.player );
+			transform->lookAt(playerTransform->origin());
+
+			// lazy, so using the debug draw line for this bit
+			dd::line( &playerTransform->origin()[0], &transform->origin()[0], dd::colors::White );
+		}
+
+	}
+
+	void RollBall::spinCubes(fggl::ecs3::World& world) {
+		// rotation
+		for ( const auto& entity : state.collectables ) {
+			if ( !world.alive(entity) || entity == state.closestPickup ) {
+				continue;
+			}
+
+			auto* transform = world.get<fggl::math::Transform>(entity);
+
+			// rotate the cubes
+			fggl::math::vec3 angles{15, 30, 45};
+			transform->rotateEuler( angles * (60.0F / 1000) );
+
+
+			//auto* renderer = world.get<fggl::data::StaticMesh>( entity );
+			//renderer->hintColour = fggl::math::vec3(1.0F, 1.0F, 1.0F);
+		}
+
+	}
+
 	void RollBall::render(fggl::gfx::Graphics &gfx) {
 		Game::render(gfx);
 	}
diff --git a/demo/include/rollball.hpp b/demo/include/rollball.hpp
index de86c6451899a82b4425126aa7e6220396c07e35..adda4f10469dfb62d8d89d127cd6e1dbe3dbc1af 100644
--- a/demo/include/rollball.hpp
+++ b/demo/include/rollball.hpp
@@ -29,8 +29,17 @@
 
 namespace demo {
 
+	enum class DebugMode {
+			NORMAL = 0,
+			DISTANCE = 1,
+			VISION = 2
+	};
+
 	struct RollState {
 		fggl::ecs::entity_t player = fggl::ecs::NULL_ENTITY;
+		fggl::ecs::entity_t closestPickup;
+		DebugMode mode = DebugMode::NORMAL;
+
 		std::array<fggl::ecs3::entity_t, 3> collectables {
 			fggl::ecs::NULL_ENTITY,
 			fggl::ecs::NULL_ENTITY,
@@ -41,6 +50,7 @@ namespace demo {
 	class RollBall : public fggl::scenes::Game {
 
 		public:
+
 			explicit RollBall(fggl::App& app);
 
 			void activate() override;
@@ -49,8 +59,12 @@ namespace demo {
 			void render(fggl::gfx::Graphics& gfx) override;
 
 		private:
+			constexpr static fggl::math::vec3 HINT_COLOUR{0.5f, 0.0f, 0.0f};
 			RollState state;
 			fggl::math::vec3 cameraOffset = {-15.0F, 15.0F, 0.0F};
+
+			void closestPickup(fggl::ecs3::World& world);
+			void spinCubes(fggl::ecs3::World& world);
 	};
 
 }
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index c9529e9d8bca4f0f182b53f5f22a79e3d8a6006b..99e994ab1a32af1f0cbe2ad90af9e2ff93e3e4c5 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -12,8 +12,8 @@ target_include_directories(fggl
         $<INSTALL_INTERFACE:include>
         )
 
-# users of this library need at least C++17
-target_compile_features(fggl PUBLIC cxx_std_17)
+# users of this library need at least C++20
+target_compile_features(fggl PUBLIC cxx_std_20)
 
 # IDE support for nice header files
 source_group(
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index c51a07058920489710f22cf8ff58c9fde11d05a7..c713cba9cd3fd8f4618c90a2ebb5ec45c5f131e7 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -1,5 +1,5 @@
 #include <fggl/util/service.h>
-#include <spdlog/spdlog.h>
+#include "fggl/debug/logging.hpp"
 
 #include "fggl/gfx/ogl/common.hpp"
 #include "fggl/gfx/window.hpp"
@@ -77,24 +77,25 @@ constexpr auto static fggl_ogl_type(GLenum type) -> const char * {
 	#pragma ide diagnostic ignored "bugprone-easily-swappable-parameters"
 #endif
 
+constexpr const char* OGL_LOG_FMT {"[GL] {}, {}: [{}]: {}"};
+
 void static fggl_ogl_to_spdlog(GLenum source, GLenum type, unsigned int msgId, GLenum severity, GLsizei  /*length*/,
 							   const char *message, const void * /*userParam*/) {
-	std::string fmt = "[GL] {}, {}: [{}]: {}";
 
 	const auto *const sourceStr = fggl_ogl_source(source);
 	const auto *const typeStr = fggl_ogl_type(type);
 
 	// table 20.3, GL spec 4.5
 	switch (severity) {
-	case GL_DEBUG_SEVERITY_HIGH: spdlog::error(fmt, sourceStr, typeStr, msgId, message);
-		break;
-	default:
-	case GL_DEBUG_SEVERITY_MEDIUM: spdlog::warn(fmt, sourceStr, typeStr, msgId, message);
-		break;
-	case GL_DEBUG_SEVERITY_LOW: spdlog::info(fmt, sourceStr, typeStr, msgId, message);
-		break;
-	case GL_DEBUG_SEVERITY_NOTIFICATION: spdlog::debug(fmt, sourceStr, typeStr, msgId, message);
-		break;
+		case GL_DEBUG_SEVERITY_HIGH: fggl::debug::error(OGL_LOG_FMT, sourceStr, typeStr, msgId, message);
+			break;
+		default:
+		case GL_DEBUG_SEVERITY_MEDIUM: fggl::debug::warning(OGL_LOG_FMT, sourceStr, typeStr, msgId, message);
+			break;
+		case GL_DEBUG_SEVERITY_LOW: fggl::debug::info(OGL_LOG_FMT, sourceStr, typeStr, msgId, message);
+			break;
+		case GL_DEBUG_SEVERITY_NOTIFICATION: fggl::debug::debug(OGL_LOG_FMT, sourceStr, typeStr, msgId, message);
+			break;
 	}
 }
 
@@ -134,7 +135,7 @@ namespace fggl::gfx {
 		GLint flags = 0;
 		glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
 		if ( (flags & GL_CONTEXT_FLAG_DEBUG_BIT) == GL_CONTEXT_FLAG_DEBUG_BIT) { // NOLINT(hicpp-signed-bitwise)
-			spdlog::info("enabling OpenGL debug output");
+			debug::info("enabling OpenGL debug output");
 			glEnable(GL_DEBUG_OUTPUT);
 			glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
 			glDebugMessageCallback(fggl_ogl_to_spdlog, nullptr);
@@ -209,13 +210,13 @@ namespace fggl::gfx {
 
 	void GlMeshRenderer::render(fggl::ecs3::World &ecs, ecs3::entity_t camera, float dt) {
 		if (camera == ecs::NULL_ENTITY) {
-			spdlog::warn("tried to render a scene, but no camera exists!");
+			debug::warning("tried to render a scene, but no camera exists!");
 			return;
 		}
 
 		auto entities = ecs.findMatching<GlRenderToken>();
 		if (entities.empty()) {
-			spdlog::warn("asked to render, but no entities are renderable");
+			debug::warning("asked to render, but no entities are renderable");
 			return;
 		}
 
diff --git a/fggl/gfx/ogl4/models.cpp b/fggl/gfx/ogl4/models.cpp
index db1facd20f19325af4cfdb11779403aece7e7677..e13e92736c9707fe3800ca951bc092585c6d72e6 100644
--- a/fggl/gfx/ogl4/models.cpp
+++ b/fggl/gfx/ogl4/models.cpp
@@ -74,7 +74,7 @@ namespace fggl::gfx::ogl4 {
 		// FIXME: this needs something reactive or performance will suck.
 		auto renderables = world.findMatching<data::StaticMesh>();
 		for (auto& renderable : renderables){
-			auto currModel = world.get<StaticModel>( renderable );
+			auto currModel = world.tryGet<StaticModel>( renderable );
 			if ( currModel != nullptr ){
 				continue;
 			}
diff --git a/fggl/phys/bullet/simulation.cpp b/fggl/phys/bullet/simulation.cpp
index e771e5d82ec6ab5a8b2a1f0d66427fbb49ea37a6..85cacddf965fc591dbe0d08d4d9180878bd6b631 100644
--- a/fggl/phys/bullet/simulation.cpp
+++ b/fggl/phys/bullet/simulation.cpp
@@ -19,9 +19,14 @@
  */
 
 #include "fggl/phys/bullet/types.hpp"
+#include "fggl/phys/bullet/motion.hpp"
 
 namespace fggl::phys::bullet {
 
+	inline btVector3 to_bullet(const math::vec3 fgglVec) {
+		return {fgglVec.x, fgglVec.y, fgglVec.z};
+	}
+
 	BulletPhysicsEngine::BulletPhysicsEngine(ecs3::World* world) : m_ecs(world), m_config(), m_world(nullptr) {
 		m_config.broadphase = new btDbvtBroadphase();
 		m_config.collisionConfiguration = new btDefaultCollisionConfiguration();
@@ -38,6 +43,9 @@ namespace fggl::phys::bullet {
 		m_world->setDebugDrawer( m_debug.get() );
 		m_debug->setDebugMode(1);
 
+		// callbacks (for handling bullet -> ecs)
+
+		// ensure we deal with ecs -> bullet changes
 		m_ecs->addDeathListener( [this](auto entity) { this->onEntityDeath(entity);} );
 	}
 
@@ -62,30 +70,13 @@ namespace fggl::phys::bullet {
 		}
 
 		m_world->stepSimulation(60.0f);
-		syncToECS();
-		dealWithCollisions();
+		//syncToECS();
+		pollCollisions();
 
 		m_world->debugDrawWorld();
 	}
 
-
-	static void build_motation_state(math::Transform* myState, BulletBody* btState) {
-		auto myPos = myState->origin();
-		btVector3 position{myPos.x, myPos.y, myPos.z};
-
-		auto myRot = myState->euler();
-		btQuaternion rotation;
-		rotation.setEulerZYX(myRot.x, myRot.y, myRot.z);
-
-		btTransform transform;
-		transform.setIdentity();
-		transform.setRotation(rotation);
-		transform.setOrigin(position);
-
-		btState->motion = new btDefaultMotionState(transform);
-	}
-
-	inline btCollisionShape* shapeToBullet(const phys::RigidBody* fgglBody) {
+	inline btCollisionShape* shape_to_bullet(const phys::RigidBody* fgglBody) {
 		if ( fgglBody->shape == nullptr ) {
 			// they forgot to put a shape, we'll assume a unit sphere to avoid crashes...
 			return new btSphereShape(0.5f);
@@ -109,20 +100,19 @@ namespace fggl::phys::bullet {
 		// FIXME without reactive-based approaches this is very slow
 		auto entsWantPhys = m_ecs->findMatching<phys::RigidBody>();
 		for (auto ent : entsWantPhys) {
-			auto* btComp = m_ecs->get<BulletBody>(ent);
+			auto* btComp = m_ecs->tryGet<BulletBody>(ent);
 			if ( btComp != nullptr ) {
-				// they are already in the simluation
+				// they are already in the simulation
 				continue;
 			}
 
+			// set up the bullet proxy for our object
 			btComp = m_ecs->add<BulletBody>(ent);
 			const auto* fgBody = m_ecs->get<phys::RigidBody>(ent);
+			btComp->motion = new FgglMotionState(m_ecs, ent);
 
-			// setup the starting motion state
-			auto* transform = m_ecs->get<math::Transform>(ent);
-			build_motation_state(transform, btComp);
-
-			btCollisionShape* colShape = shapeToBullet(fgBody);
+			// collisions
+			btCollisionShape* colShape = shape_to_bullet(fgBody);
 			btVector3 localInt(0, 0, 0);
 			if ( !fgBody->isStatic() ) {
 				colShape->calculateLocalInertia(fgBody->mass, localInt);
@@ -135,27 +125,31 @@ namespace fggl::phys::bullet {
 				colShape,
 				localInt
 			);
-			auto* body = new btRigidBody(bodyCI);
-			body->setUserIndex(static_cast<int>(ent));
+			btComp->body = new btRigidBody(bodyCI);
+			btComp->body->setUserIndex( static_cast<int>(ent) );
 
-			btComp->body = body;
 
 			if (!fgBody->isStatic()) {
-				body->setRestitution(0.5f);
-				body->setRollingFriction(0.0f);
+				btComp->body->setRestitution(0.5f);
+				btComp->body->setRollingFriction(0.0f);
 			}
 
 			if ( fgBody->type == BodyType::KINEMATIC ) {
-				body->setCollisionFlags( body->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT );
+				auto flags = btComp->body->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT;
+				btComp->body->setCollisionFlags( flags );
+
+				// we don't have a clean way of saying a kinematic object moved, so just prevent sleeping.
+				// if this turns out to be an issue, we'll need to revisit this.
+				btComp->body->setActivationState( DISABLE_DEACTIVATION );
 			}
 
-			m_world->addRigidBody(btComp->body );
+			m_world->addRigidBody( btComp->body );
 		}
 	}
 
-	void BulletPhysicsEngine::syncToECS() {
-		auto physEnts = m_ecs->findMatching<BulletBody>();
-		for (auto& ent : physEnts) {
+	void BulletPhysicsEngine::forceSyncToECS() {
+		const auto physEnts = m_ecs->findMatching<BulletBody>();
+		for (const auto& ent : physEnts) {
 			auto* transform = m_ecs->get<math::Transform>(ent);
 			auto* physBody = m_ecs->get<BulletBody>(ent);
 
@@ -185,9 +179,10 @@ namespace fggl::phys::bullet {
 		auto entRB = m_ecs->findMatching<BulletBody>();
 		for (auto& ent : entRB) {
 			auto* bulletBody = m_ecs->get<BulletBody>(ent);
-			delete bulletBody->motion;
 			m_world->removeCollisionObject(bulletBody->body);
-			delete bulletBody->body;
+
+			// release resources and delete
+			bulletBody->release();
 			m_ecs->remove<BulletBody>(ent);
 		}
 
@@ -223,7 +218,7 @@ namespace fggl::phys::bullet {
 		}
 	}
 
-	void BulletPhysicsEngine::dealWithCollisions() {
+	void BulletPhysicsEngine::pollCollisions() {
 		// flush collision caches
 		auto caches = m_ecs->findMatching<CollisionCache>();
 		for( auto& ent : caches) {
@@ -249,13 +244,21 @@ namespace fggl::phys::bullet {
 				continue;
 			}
 
-			std::cerr << "contact: " << body0->getUserIndex() << " on " << body1->getUserIndex() << std::endl;
+			int numContacts = contactManifold->getNumContacts();
+			for ( auto contactIdx = 0; contactIdx < numContacts; ++contactIdx ) {
+				auto& point = contactManifold->getContactPoint(contactIdx);
+				if ( point.getDistance() < 0.0F ) {
+					auto worldPosA = point.getPositionWorldOnA();
+					auto worldPosB = point.getPositionWorldOnB();
+					auto normal = point.m_normalWorldOnB;
+				}
+			}
 
 			handleCollisionCallbacks(m_ecs, body0->getUserIndex(), body1->getUserIndex());
 			handleCollisionCallbacks(m_ecs, body1->getUserIndex(), body0->getUserIndex());
 		}
 
-		// note conacts that have ended
+		// note contacts that have ended
 		caches = m_ecs->findMatching<CollisionCache>(); // re-fetch, entities can (and probably do) die in handlers.
 		for( auto& ent : caches) {
 			auto* cache = m_ecs->get<CollisionCache>(ent);
@@ -279,9 +282,74 @@ namespace fggl::phys::bullet {
 	void BulletPhysicsEngine::onEntityDeath(ecs::entity_t entity)  {
 			auto* btPhysics = m_ecs->tryGet<BulletBody>(entity);
 			if ( btPhysics != nullptr) {
+				// decouple physics from entity
 				btPhysics->body->setUserIndex( ecs::NULL_ENTITY );
 				m_world->removeRigidBody( btPhysics->body );
+				btPhysics->release();
 			}
 	}
 
+	void BulletPhysicsEngine::onCollisionCreate(ContactPoint point) {
+		m_contactCache.created.push_back(point);
+	}
+
+	void BulletPhysicsEngine::onCollisionProcess(ContactPoint point) {
+		m_contactCache.modified.push_back( point );
+	}
+
+	void BulletPhysicsEngine::onCollisionDestroyed(ContactPoint point) {
+		m_contactCache.removed.push_back(point);
+	}
+
+	ecs3::entity_t BulletPhysicsEngine::raycast(math::vec3 from, math::vec3 to) {
+		const auto btFrom = to_bullet(from);
+		const auto btTo = to_bullet(to);
+
+		btCollisionWorld::ClosestRayResultCallback rayTestResult(btFrom, btTo);
+		m_world->rayTest(btFrom, btTo, rayTestResult);
+
+		if ( !rayTestResult.hasHit() ) {
+			return ecs3::NULL_ENTITY;
+		}
+
+		// tell the user what it hit
+		return rayTestResult.m_collisionObject->getUserIndex();
+	}
+
+	std::vector<ContactPoint> BulletPhysicsEngine::scanCollisions(ecs3::entity_t entity) {
+		return std::vector<ContactPoint>();
+	}
+
+	std::vector<ecs3::entity_t> BulletPhysicsEngine::raycastAll(math::vec3 fromPos, math::vec3 toPos) {
+		const auto btFrom = to_bullet(fromPos);
+		const auto btTo = to_bullet(toPos);
+
+		btCollisionWorld::AllHitsRayResultCallback rayTestResult(btFrom, btTo);
+		m_world->rayTest(btFrom, btTo, rayTestResult);
+
+		// we didn't hit anything
+		if ( !rayTestResult.hasHit() ) {
+			return {};
+		}
+
+		// hit processing
+		const auto hits = rayTestResult.m_collisionObjects.size();
+
+		std::vector<ecs3::entity_t> hitVec;
+		hitVec.reserve(hits);
+
+		for ( auto i = 0; i < hits; ++i) {
+			ecs3::entity_t entity = rayTestResult.m_collisionObjects[i]->getUserIndex();
+			hitVec.push_back(entity);
+		}
+
+		return hitVec;
+	}
+
+	std::vector<ecs3::entity_t> BulletPhysicsEngine::sweep(PhyShape &shape,
+														   math::Transform &from,
+														   math::Transform &to) {
+		return std::vector<ecs3::entity_t>();
+	}
+
 } // namespace fggl::phys::bullet
\ No newline at end of file
diff --git a/include/fggl/debug/debug.h b/include/fggl/debug/debug.h
index 4e303f65f6a20a1fc874bf7f5e6a90da5f32adae..098616186f17f827bb68867d0942ced8ed31a264 100644
--- a/include/fggl/debug/debug.h
+++ b/include/fggl/debug/debug.h
@@ -1,11 +1,13 @@
 #ifndef FGGL_DEBUG_H
 #define FGGL_DEBUG_H
 
+#include <string>
+#include <memory>
 #include <utility>
 #include <functional>
 #include <unordered_map>
 
-#include <fggl/gfx/window.hpp>
+#include "fggl/gfx/window.hpp"
 
 namespace fggl::debug {
 
diff --git a/include/fggl/debug/impl/logging_spdlog.hpp b/include/fggl/debug/impl/logging_spdlog.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f641238e2dd6ad71cdc853c2a98c4794dccc3f92
--- /dev/null
+++ b/include/fggl/debug/impl/logging_spdlog.hpp
@@ -0,0 +1,99 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+//
+// Created by webpigeon on 08/06/22.
+//
+
+#ifndef FGGL_DEBUG_IMPL_LOGGING_SPDLOG_HPP
+#define FGGL_DEBUG_IMPL_LOGGING_SPDLOG_HPP
+
+#include <string>
+#include <iostream>
+#include <memory>
+
+#include <spdlog/spdlog.h>
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+namespace fggl::debug {
+
+	using FmtType = const std::string_view;
+
+	/**
+	 * Logging levels
+	 */
+	enum class Level {
+			critical = spdlog::level::critical,
+			error = spdlog::level::err,
+			warning = spdlog::level::warn,
+			info = spdlog::level::info,
+			debug = spdlog::level::debug,
+			trace = spdlog::level::trace
+	};
+
+	template<typename ...T>
+	void error(const FmtType& fmt, T&& ...args) {
+		spdlog::error(fmt, args...);
+	}
+
+	template<typename ...T>
+	void warning(const FmtType& fmt, T&& ...args ){
+		spdlog::warn(fmt, args...);
+	}
+
+	template<typename ...T>
+	void info(const FmtType& fmt, T&& ...args ) {
+		spdlog::info(fmt, args...);
+	}
+
+	template<typename ...T>
+	void debug(const FmtType& fmt, T&& ...args ) {
+		spdlog::debug(fmt, args...);
+	}
+
+	template<typename ...T>
+	void trace(const FmtType& fmt, T&& ...args ) {
+		spdlog::trace(fmt, args...);
+	}
+
+	template<typename ...T>
+	void log(const FmtType& fmt, T&& ...args) {
+		spdlog::log(Level::info, fmt, args...);
+	}
+
+	template<typename ...T>
+	void log(Level level, const FmtType& fmt, T&& ...args) {
+		spdlog::log(level, fmt, args...);
+	}
+
+	class Logger {
+			public:
+				Logger();
+
+				template<typename ...Args>
+				void log(Level level, const FmtType& fmt, Args&& ...args) {
+					spdlog::log(level, fmt, args...);
+				}
+	};
+
+}
+
+
+#endif //FGGL_DEBUG_IMPL_LOGGING_SPDLOG_HPP
diff --git a/include/fggl/debug/impl/logging_std20.hpp b/include/fggl/debug/impl/logging_std20.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b82d8523a588862bfe81440910b6619c0968fc94
--- /dev/null
+++ b/include/fggl/debug/impl/logging_std20.hpp
@@ -0,0 +1,121 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+//
+// Created by webpigeon on 11/06/22.
+//
+
+#ifndef FGGL_DEBUG_IMPL_LOGGING_STD20_HPP
+#define FGGL_DEBUG_IMPL_LOGGING_STD20_HPP
+
+#include <fmt/format.h>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <cstdio>
+
+constexpr std::string_view CERR_FMT{"[{}] {}\n"};
+
+namespace fggl::debug {
+	using FmtType = const std::string_view;
+
+	/**
+	 * Logging levels
+	 */
+	enum class Level {
+		critical = 0,
+		error = 1,
+		warning = 2,
+		info = 3,
+		debug = 4,
+		trace = 5
+	};
+
+	constexpr std::string_view level_to_string(Level level) {
+		switch (level) {
+			case Level::critical:
+				return "CRITITAL";
+			case Level::error:
+				return "ERROR";
+			case Level::warning:
+				return "WARNING";
+			case Level::info:
+				return "INFO";
+			case Level::debug:
+				return "DEBUG";
+			case Level::trace:
+				return "TRACE";
+			default:
+				return "UNKNOWN";
+		}
+	}
+
+	inline void vlog(const char* file, int line, fmt::string_view format, fmt::format_args args) {
+		fmt::print("{}: {}", file, line);
+		fmt::vprint(format, args);
+	}
+
+	template <typename S, typename... Args>
+	void logf(const char* file, int line, const S& format, Args&&... args) {
+		vlog( file, line, format, fmt::make_args_checked<Args...>(format, args...));
+	}
+
+	#define info_va(format, ...) \
+		logf(__FILE__, __LINE__, FMT_STRING(format), __VA_ARGS__)
+
+	template<typename ...T>
+	void log(Level level, FmtType fmt, T &&...args) {
+		auto fmtStr = fmt::format(fmt::runtime(fmt), args...);
+		fmt::print(CERR_FMT, level_to_string(level), fmtStr);
+	}
+
+	template<typename ...T>
+	void error(FmtType fmt, T &&...args) {
+		log( Level::error, fmt, args...);
+	}
+
+	template<typename ...T>
+	void warning(FmtType fmt, T &&...args) {
+		log( Level::warning, fmt, args...);
+	}
+
+	template<typename ...T>
+	void info(FmtType fmt, T &&...args) {
+		log( Level::info, fmt, args... );
+	}
+
+	template<typename ...T>
+	void log(FmtType fmt, T &&...args) {
+		log( Level::info, fmt, args...);
+	}
+
+	template<typename ...T>
+	void debug(FmtType fmt, T &&...args) {
+		log( Level::debug, fmt, args...);
+	}
+
+	template<typename ...T>
+	void trace(FmtType fmt, T &&...args) {
+		log( Level::trace, fmt, args...);
+	}
+
+}
+
+#endif //FGGL_DEBUG_IMPL_LOGGING_STD20_HPP
diff --git a/include/fggl/debug/logging.hpp b/include/fggl/debug/logging.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..dda37ce671507fb12eb04a0916a1628c4557e8c5
--- /dev/null
+++ b/include/fggl/debug/logging.hpp
@@ -0,0 +1,57 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+//
+// Created by webpigeon on 11/06/22.
+//
+
+#ifndef FGGL_DEBUG_LOGGING_HPP
+#define FGGL_DEBUG_LOGGING_HPP
+
+namespace fggl::debug {
+
+	using FmtType = const std::string_view;
+	enum class Level;
+
+	template<typename ...T>
+	void error(FmtType fmt, T&& ...args);
+
+	template<typename ...T>
+	void warning(FmtType fmt, T&& ...args );
+
+	template<typename ...T>
+	void info(FmtType fmt, T&& ...args );
+
+	template<typename ...T>
+	void debug(FmtType fmt, T&& ...args );
+
+	template<typename ...T>
+	void trace(FmtType fmt, T&& ...args );
+
+	template<typename ...T>
+	void log(FmtType fmt, T&& ...args);
+
+	template<typename ...T>
+	void log(Level level, FmtType fmt, T&& ...args);
+}
+
+#include "fggl/debug/impl/logging_std20.hpp"
+
+#endif //FGGL_DEBUG_LOGGING_HPP
diff --git a/include/fggl/ecs3/prototype/world.h b/include/fggl/ecs3/prototype/world.h
index c22ee52074bce6b33a5b793073943111fd0ce660..7cd6287556f0ec846d140341aaceedaba8330b58 100644
--- a/include/fggl/ecs3/prototype/world.h
+++ b/include/fggl/ecs3/prototype/world.h
@@ -9,7 +9,8 @@
 #include <functional>
 #include <unordered_set>
 
-#include <fggl/ecs3/types.hpp>
+#include "fggl/ecs3/types.hpp"
+#include "fggl/debug/logging.hpp"
 #include <yaml-cpp/yaml.h>
 
 /**
@@ -280,17 +281,14 @@ namespace fggl::ecs3::prototype {
 
 			template<typename C>
 			C* tryGet(entity_t entity_id) const {
-				assert( entity_id != NULL_ENTITY && "attempted to tryGet component on null entity" );
+				if ( entity_id == NULL_ENTITY) {
+					return nullptr;
+				}
 
 				try {
-					const auto &entity = m_entities.at(entity_id);
-					try {
-						return entity.get<C>();
-					} catch ( std::out_of_range& e ) {
-						return nullptr;
-					}
+					return get<C>(entity_id);
 				} catch ( std::out_of_range& e) {
-					std::cerr << "someone requested an entity that didn't exist, entity was: " << entity_id << std::endl;
+					fggl::debug::info("component {} on {} did not exist", C::name, entity_id);
 					return nullptr;
 				}
 			}
@@ -298,11 +296,9 @@ namespace fggl::ecs3::prototype {
 			template<typename C>
 			C *get(entity_t entity_id) const {
 				assert( exists(entity_id) && "attempted to get component on null entity" );
-				C* ptr = tryGet<C>(entity_id);
-				if (ptr == nullptr) {
-					std::cerr << "entity " << entity_id << " does not have component "<< C::name << std::endl;
-				}
-				return ptr;
+
+				const auto& entity = m_entities.at(entity_id);
+				return entity.get<C>();
 			}
 
 			template<typename C>
diff --git a/include/fggl/gfx/phong.hpp b/include/fggl/gfx/phong.hpp
index 2e59714fab9e8f223a2a2de267c690b7afedefbf..9276c7ce5cbc3ca7ff4b85af0a8128c6e4b77ac8 100644
--- a/include/fggl/gfx/phong.hpp
+++ b/include/fggl/gfx/phong.hpp
@@ -37,6 +37,28 @@ namespace fggl::gfx {
 		float shininess;
 	};
 
+	enum class LightType {
+			Directional,
+			Point,
+			Spot
+	};
+
+	struct Light {
+		constexpr static const char* name = "gfx::light";
+		LightType type;
+		math::vec3 position;
+
+		// colours
+		math::vec3 ambient;
+		math::vec3 diffuse;
+		math::vec3 specular;
+
+		// distance data
+		math::vec3 falloffs; // (constant, linear, quadratic)
+		float cutOff;
+		float outerCutOff;
+	};
+
 } // namesapce fggl::gfx
 
 #endif //FGGL_GFX_PHONG_HPP
diff --git a/include/fggl/math/types.hpp b/include/fggl/math/types.hpp
index 872e5db98b6c7c46e270724fb206f43cc592762e..7a7be0630e51e8548e78b0c4ec1de54562f13a9f 100644
--- a/include/fggl/math/types.hpp
+++ b/include/fggl/math/types.hpp
@@ -6,8 +6,9 @@
 
 #include <glm/glm.hpp>
 #include <glm/ext/matrix_transform.hpp>
-#include <glm/gtx/transform.hpp>
 #include <glm/gtc/quaternion.hpp>
+#include <glm/gtx/transform.hpp>
+#include <glm/gtx/euler_angles.hpp>
 #include <glm/gtx/quaternion.hpp>
 
 #ifndef M_PI
@@ -185,6 +186,15 @@ namespace fggl::math {
 				m_model = parent * local();
 			}
 
+			inline void lookAt(vec3 target) {
+//				auto direction = m_origin - target;
+				auto result = glm::lookAt(m_origin, target, math::AXIS_Y);
+
+				math::vec3 resultAngles;
+				glm::extractEulerAngleXYZ(result, resultAngles.x, resultAngles.y, resultAngles.z);
+				m_euler = glm::degrees(resultAngles);
+			}
+
 		private:
 			mat4 m_model; // us -> world
 			vec3 m_origin;
diff --git a/include/fggl/phys/bullet/motion.hpp b/include/fggl/phys/bullet/motion.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e38e6ec60d35e4bfdee4199586d0188e985aa55f
--- /dev/null
+++ b/include/fggl/phys/bullet/motion.hpp
@@ -0,0 +1,63 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+//
+// Created by webpigeon on 11/06/22.
+//
+
+#ifndef FGGL_PHYS_BULLET_MOTION_HPP
+#define FGGL_PHYS_BULLET_MOTION_HPP
+
+#include "fggl/phys/bullet/types.hpp"
+
+namespace fggl::phys::bullet {
+
+	class FgglMotionState : public btMotionState {
+		public:
+			FgglMotionState(fggl::ecs3::World* world, fggl::ecs3::entity_t entity) : m_world(world), m_entity(entity) {
+			}
+			virtual ~FgglMotionState() = default;
+
+			void getWorldTransform(btTransform& worldTrans) const override {
+				const auto* transform = m_world->get<fggl::math::Transform>(m_entity);
+				worldTrans.setFromOpenGLMatrix( glm::value_ptr(transform->model()) );
+			}
+
+			void setWorldTransform(const btTransform& worldTrans) override {
+				auto* transform = m_world->get<fggl::math::Transform>(m_entity);
+
+				// set position
+				auto btOrigin = worldTrans.getOrigin();
+				transform->origin( {btOrigin.x(), btOrigin.y(), btOrigin.z()} );
+
+				// set rotation
+				math::vec3 angles;
+				worldTrans.getRotation().getEulerZYX(angles.x, angles.y, angles.z);
+				transform->euler(angles);
+			}
+
+		private:
+			fggl::ecs3::World* m_world;
+			fggl::ecs3::entity_t m_entity;
+	};
+
+} // namespace fggl::phys::bullet
+
+#endif //FGGL_PHYS_BULLET_INTEGRATIONS_HPP
diff --git a/include/fggl/phys/bullet/types.hpp b/include/fggl/phys/bullet/types.hpp
index 04d8385edc93c41c441b6c0a58fc5434e6d011ad..8197759c2dd2eb278985974f432b691c7e580de3 100644
--- a/include/fggl/phys/bullet/types.hpp
+++ b/include/fggl/phys/bullet/types.hpp
@@ -48,6 +48,13 @@ namespace fggl::phys::bullet {
 		constexpr static const char* name = "phys::bullet::body";
 		btMotionState* motion;
 		btRigidBody* body;
+
+		inline void release() {
+			delete motion;
+			delete body;
+			motion = nullptr;
+			body = nullptr;
+		}
 	};
 
 	/**
@@ -62,13 +69,20 @@ namespace fggl::phys::bullet {
 			~BulletPhysicsEngine() override;
 
 			void step() override;
-
 			void onEntityDeath(ecs::entity_t entity);
+
+
+			std::vector<ContactPoint> scanCollisions(ecs3::entity_t entity) override;
+			ecs3::entity_t raycast(math::vec3 from, math::vec3 to) override;
+			std::vector<ecs3::entity_t> raycastAll(math::vec3 from, math::vec3 to) override;
+			std::vector<ecs3::entity_t> sweep(PhyShape& shape, math::Transform& from, math::Transform& to) override;
+
 		private:
 			ecs3::World* m_ecs;
 			BulletConfiguration m_config;
 			btDiscreteDynamicsWorld* m_world;
 			std::unique_ptr<debug::BulletDebugDrawList> m_debug;
+			ContactCache m_contactCache;
 
 			/**
 			 * Check for ECS components which aren't in the physics world and add them.
@@ -78,12 +92,17 @@ namespace fggl::phys::bullet {
 			/**
 			 * Sync the bullet world state back to the ECS.
 			 */
-			void syncToECS();
+			void forceSyncToECS();
 
 			/**
 			 * Deal with physics collisions
 			 */
-			void dealWithCollisions();
+			void pollCollisions();
+
+			// bullet callback functions
+			void onCollisionCreate( ContactPoint point );
+			void onCollisionProcess( ContactPoint point );
+			void onCollisionDestroyed( ContactPoint point );
 	};
 
 } // namespace fggl::phys::bullet
diff --git a/include/fggl/phys/types.hpp b/include/fggl/phys/types.hpp
index c3b68de411c6975473b06ee8dc47533cbd7ead2b..6204d62742f45655865016874c7ceef57d5eda08 100644
--- a/include/fggl/phys/types.hpp
+++ b/include/fggl/phys/types.hpp
@@ -76,6 +76,27 @@ namespace fggl::phys {
 		math::vec3 force = math::VEC3_ZERO;
 	};
 
+	struct ContactPoint {
+		ecs3::entity_t entityA;
+		ecs3::entity_t entityB;
+		math::vec3 localA;
+		math::vec3 localB;
+		math::vec3 normal;
+		float distance;
+	};
+
+	struct ContactCache {
+		std::vector<ContactPoint> created;
+		std::vector<ContactPoint> modified;
+		std::vector<ContactPoint> removed;
+
+		void clear() {
+			created.clear();
+			modified.clear();
+			removed.clear();
+		}
+	};
+
 	class PhysicsEngine {
 		public:
 			PhysicsEngine() = default;
@@ -87,6 +108,13 @@ namespace fggl::phys {
 			PhysicsEngine& operator=(PhysicsEngine&) = delete;
 			PhysicsEngine& operator=(PhysicsEngine&&) = delete;
 
+			// 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;
+			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;
+
+			// update
 			virtual void step() = 0;
 	};