diff --git a/as5/CMakeLists.txt b/as5/CMakeLists.txt new file mode 100644 index 0000000..4edb7ab --- /dev/null +++ b/as5/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.18) +project(as3 CXX) + +set(CMAKE_CXX_STANDARD 20) + +# adding this option to make clangd work +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_subdirectory(../raylib-cpp raylib) + +include(../assets/includeable.cmake) + +add_executable(as5 as5.cpp skybox.cpp) +target_link_libraries(as5 PUBLIC raylib raylib_cpp) + +make_includeable(../assets/shaders/skybox.vs generated/skybox.vs) +make_includeable(../assets/shaders/skybox.fs generated/skybox.fs) +make_includeable(../assets/shaders/cubemap.vs generated/cubemap.vs) +make_includeable(../assets/shaders/cubemap.fs generated/cubemap.fs) + +configure_file(../assets/models/penguin.glb models/penguin.glb COPYONLY) +configure_file(../assets/models/eagle.glb models/eagle.glb COPYONLY) +configure_file(../assets/textures/skybox.png textures/skybox.png COPYONLY) +configure_file(../assets/textures/snow.jpg textures/snow.jpg COPYONLY) diff --git a/as5/README.md b/as5/README.md new file mode 100644 index 0000000..0ac1e67 --- /dev/null +++ b/as5/README.md @@ -0,0 +1,71 @@ +# Building and Running + +Clone the repository, navigate to the root of the project, and initialize the +submodules: + +```sh +git clone https://github.com/humanoidsandvichdispenser/cs381.git +cd cs381 +git submodule update --init --recursive +``` + +Navigate to the `as4` directory, create a build directory, and run CMake to +generate the build files: + +```sh +cd as4 +mkdir -p build +cd build +cmake .. +``` + +Compile the code using `make`: + +```sh +make +``` + +This should create an executable named `as4` in the `build` directory. You can +run the executable with the following command: + +```sh +./as4 +``` + +# Instructions on how to use the program + +Hold W and S to accelerate the selected entity forward and backward. Use A and +D to change heading direction. This allows you to steer the entity around the +environment. + +Use TAB to switch between entities. The currently selected entity will +be drawn with a bounding box. + +When the eagle is selected, press Q and Z to change the pitch. + +# Readme Question + +The entity selection system works by when TAB is pressed (key down only on one +frame), the program increments the index of the currently selected entity. +Selection wraps around to the first entity when it exceeds the number of +entities in the game. Movement keys only apply to the selected entity, which is +determined by the selected index. When drawing, the selected index is compared +to the index of each entity, and if they match, a bounding box is drawn around +the entity to indicate that it is selected. + +Both monolithic and ad-hoc approaches to entity management are fast to setup +since they require the least amount of code/boilerplate to get something +working. However, monolithic entities are easier to scale, since in ad-hoc, you +have to write the entire state and behavior of the entity (its variables, +methods, etc) for each entity that exists. Ad-hoc is faster to work with only +when managing few entities with very different behavior. + +For this particular assignment, the monolithic approach is more suitable since +most of the entities share similar behavior (change velocity) and state +(position, speed, heading). This however still became more difficult to manage +when adding the eagle that had an additional pitch variable and flying +behavior. + +# Extra Credit + +The eagle can move in 3D space with an additional DOF compared to the penguins. diff --git a/as5/as5.cpp b/as5/as5.cpp new file mode 100644 index 0000000..eb71070 --- /dev/null +++ b/as5/as5.cpp @@ -0,0 +1,196 @@ +#include "AudioDevice.hpp" +#include "Color.hpp" +#include "Keyboard.hpp" +#include "Matrix.hpp" +#include "Mesh.hpp" +#include "Model.hpp" +#include "RadiansDegrees.hpp" +#include "Vector3.hpp" +#include "raylib.h" +#include +#include +#include +#include +#include +#include + +#define SKYBOX_IMPLEMENTATION +#include "skybox.hpp" + +void DrawBoundedModel(raylib::Model &model, auto transformer) { + // store the original transform to apply a different transform to the + // model without affecting the next time we draw + raylib::Matrix oldTransform = model.GetTransform(); + + // apply the transform that we get from whatever the transformer callback + // gives us + raylib::Matrix transform = transformer(model.GetTransform()); + + // apply the transform that we got from the transformer to the model + model.SetTransform(transform); + + // draw the model, passing the origin and default scale as arguments since + // the transform is already applied to the model + model.Draw({ 0, 0, 0 }, 1.0f, raylib::Color::White()); + + // get the bounding box of the model after applying the transform + auto box = model.GetTransformedBoundingBox(); + + // draw the bounding box of the model using raylib's built in function + DrawBoundingBox(box, raylib::Color::White()); + + // restore the model's transform to its original state so that the next time we + // draw the model, it doesn't have the previous transform applied to it + model.SetTransform(oldTransform); +} + +class Component { +public: + struct Entity *entity; + virtual void Setup() = 0; + virtual void Update(float dt) = 0; + virtual void Cleanup() = 0; +}; + +struct Entity { + std::vector> components; + + template T> + T &AddComponent() { + std::shared_ptr out = components.emplace_back(std::make_shared()); + out->entity = this; + return (T &)*out; + } + + template T> + std::optional> GetComponent() const { + for (auto &c : components) { + T *cast = dynamic_cast(c.get()); + if (cast) { + return *cast; + } + } + + return { }; + } +}; + +struct TransformComponent : public Component { + raylib::Vector3 position = { 0, 0, 0 }; + raylib::Quaternion rotation = raylib::Quaternion::Identity(); + + void Setup() override { } + + void Update(float dt) override { } + + void Cleanup() override { } +}; + +struct DrawModelComponent : public Component { + raylib::Model *model; + + void Setup() override { } + + void Update(float dt) override { + DrawBoundedModel(*model, [this](raylib::Transform old) { + auto t = entity->GetComponent(); + if (t) { + return old + .Translate(t->get().position) + .Rotate(t->get().rotation); + } + + return old; + }); + } + + void Cleanup() override { } +}; + +template +struct Delegate { }; + +template +struct Delegate { + std::vector> functions; + + void connect(const std::function &func) { + functions.push_back(func); + } + + void operator+=(const std::function &func) { + connect(func); + } + + void operator()(int arg) { + static_assert( + std::is_same_v, + "EC HW: Delegate only supports void return type"); + + for (const auto &func : functions) { + f(std::forward(arg)...); + } + } +}; + +raylib::Degree angle_normalize(raylib::Degree angle) { + float decimal = float(angle) - int(angle); + int normalized = (int(angle) % 360 + 360) % 360; + return raylib::Degree(normalized + decimal); +} + +int main() { + raylib::Window window(800, 600, "CS381 - Assignment 5"); + window.SetState(FLAG_WINDOW_RESIZABLE); + raylib::AudioDevice audio; + + raylib::Model penguin("models/penguin.glb"); + raylib::Transform penguinTransform = raylib::Transform::Identity() + .Scale(40, 40, 40); + penguin.SetTransform(penguinTransform); + + raylib::Camera3D camera( + { 0, 400, -40 }, + { 0, -0.25f, 1 }, + { 0, 1, 0 }, + 45.0f); + + raylib::Model ground = raylib::Mesh::Plane(10000, 10000, 50, 50, 25); + + raylib::Texture snowTexture("textures/snow.jpg"); + ground.GetMaterials()[0].maps[MATERIAL_MAP_DIFFUSE].texture = snowTexture; + + cs381::SkyBox skybox("textures/skybox.png"); + + std::vector entities; + + Entity &e = entities.emplace_back(); + e.AddComponent(); + e.AddComponent(); + e.GetComponent()->get().model = &penguin; + + window.SetTargetFPS(60); // save cpu cycles + + while (!window.ShouldClose()) { + window.BeginDrawing(); + window.ClearBackground(raylib::Color::Gray()); + + float dt = window.GetFrameTime(); + + camera.BeginMode(); + skybox.Draw(); + + ground.Draw({ 0, 0, 0 }, 1.0f, raylib::Color::White()); + + for (const Entity &e : entities) { + for (const std::shared_ptr &c : e.components) { + c->Update(dt); + } + } + + camera.EndMode(); + window.EndDrawing(); + } + + return 0; +} diff --git a/as5/generated/cubemap.fs b/as5/generated/cubemap.fs new file mode 100644 index 0000000..54392d2 --- /dev/null +++ b/as5/generated/cubemap.fs @@ -0,0 +1,30 @@ +R"for_C++_include(#version 330 + +// Input vertex attributes (from vertex shader) +in vec3 fragPosition; + +// Input uniform values +uniform sampler2D equirectangularMap; + +// Output fragment color +out vec4 finalColor; + +vec2 SampleSphericalMap(vec3 v) +{ + vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); + uv *= vec2(0.1591, 0.3183); + uv += 0.5; + return uv; +} + +void main() +{ + // Normalize local position + vec2 uv = SampleSphericalMap(normalize(fragPosition)); + + // Fetch color from texture map + vec3 color = texture(equirectangularMap, uv).rgb; + + // Calculate final fragment color + finalColor = vec4(color, 1.0); +})for_C++_include" \ No newline at end of file diff --git a/as5/generated/cubemap.vs b/as5/generated/cubemap.vs new file mode 100644 index 0000000..7f5e04f --- /dev/null +++ b/as5/generated/cubemap.vs @@ -0,0 +1,20 @@ +R"for_C++_include(#version 330 + +// Input vertex attributes +in vec3 vertexPosition; + +// Input uniform values +uniform mat4 matProjection; +uniform mat4 matView; + +// Output vertex attributes (to fragment shader) +out vec3 fragPosition; + +void main() +{ + // Calculate fragment position based on model transformations + fragPosition = vertexPosition; + + // Calculate final vertex position + gl_Position = matProjection*matView*vec4(vertexPosition, 1.0); +})for_C++_include" \ No newline at end of file diff --git a/as5/generated/skybox.fs b/as5/generated/skybox.fs new file mode 100644 index 0000000..c4cd6af --- /dev/null +++ b/as5/generated/skybox.fs @@ -0,0 +1,30 @@ +R"for_C++_include(#version 330 + +// Input vertex attributes (from vertex shader) +in vec3 fragPosition; + +// Input uniform values +uniform samplerCube environmentMap; +uniform bool vflipped; +uniform bool doGamma; + +// Output fragment color +out vec4 finalColor; + +void main() +{ + // Fetch color from texture map + vec3 color = vec3(0.0); + + if (vflipped) color = texture(environmentMap, vec3(fragPosition.x, -fragPosition.y, fragPosition.z)).rgb; + else color = texture(environmentMap, fragPosition).rgb; + + if (doGamma)// Apply gamma correction + { + color = color/(color + vec3(1.0)); + color = pow(color, vec3(1.0/2.2)); + } + + // Calculate final fragment color + finalColor = vec4(color, 1.0); +})for_C++_include" \ No newline at end of file diff --git a/as5/generated/skybox.vs b/as5/generated/skybox.vs new file mode 100644 index 0000000..00596e0 --- /dev/null +++ b/as5/generated/skybox.vs @@ -0,0 +1,24 @@ +R"for_C++_include(#version 330 + +// Input vertex attributes +in vec3 vertexPosition; + +// Input uniform values +uniform mat4 matProjection; +uniform mat4 matView; + +// Output vertex attributes (to fragment shader) +out vec3 fragPosition; + +void main() +{ + // Calculate fragment position based on model transformations + fragPosition = vertexPosition; + + // Remove translation from the view matrix + mat4 rotView = mat4(mat3(matView)); + vec4 clipPos = matProjection*rotView*vec4(vertexPosition, 1.0); + + // Calculate final vertex position + gl_Position = clipPos; +})for_C++_include" \ No newline at end of file diff --git a/as5/skybox.cpp b/as5/skybox.cpp new file mode 100644 index 0000000..fafdf84 --- /dev/null +++ b/as5/skybox.cpp @@ -0,0 +1,168 @@ +/******************************************************************************************* +* +* raylib [models] example - Skybox loading and drawing +* +* Example originally created with raylib 1.8, last time updated with raylib 4.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2017-2023 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "skybox.hpp" +#include + +#include "rlgl.h" + +namespace cs381 { + + SkyBox& SkyBox::Init() { + // Load skybox model + cube = raylib::Mesh::Cube(1.0f, 1.0f, 1.0f).LoadModelFrom(); + + // Load skybox shader and set required locations + // NOTE: Some locations are automatically set at shader loading + shader = raylib::Shader::LoadFromMemory(vertexShader, fragmentShader); + cube.materials[0].shader = shader; + shader.SetValue("environmentMap", (int)MATERIAL_MAP_CUBEMAP, SHADER_UNIFORM_INT); + + return *this; + } + + SkyBox& SkyBox::Load(const std::string_view filename, bool isEnviornment/* = false*/) { + if(shader.id == 0) Init(); + + shader.SetValue("doGamma", int(isEnviornment ? 1 : 0), SHADER_UNIFORM_INT); + shader.SetValue("vflipped", int(isEnviornment ? 1 : 0), SHADER_UNIFORM_INT); + + if(isEnviornment) { + if(cubemapShader.id == 0){ + cubemapShader = raylib::Shader::LoadFromMemory(cubemapVertexShader, cubemapFragmentShader); + cubemapShader.SetValue("equirectangularMap", int(0), SHADER_UNIFORM_INT); + } + + // Load HDR panorama (sphere) texture + texture.Load(filename); + // Make sure that things aren't sampled in a pixelated manor! + texture.SetFilter(TEXTURE_FILTER_BILINEAR); + + // Generate cubemap (texture with 6 quads-cube-mapping) from panorama HDR texture + // NOTE 1: New texture is generated rendering to texture, shader calculates the sphere->cube coordinates mapping + // NOTE 2: It seems on some Android devices WebGL, fbo does not properly support a FLOAT-based attachment, + // despite texture can be successfully created.. so using PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 instead of PIXELFORMAT_UNCOMPRESSED_R32G32B32A32 + cube.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture = GenTextureCubemap(cubemapShader, texture, 1024, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + } else { + raylib::Image img(filename); + texture.Load(img, CUBEMAP_LAYOUT_AUTO_DETECT); + // Make sure that things aren't sampled in a pixelated manor! + texture.SetFilter(TEXTURE_FILTER_BILINEAR); + cube.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture = texture; // CUBEMAP_LAYOUT_PANORAMA + } + + return *this; + } + + SkyBox& SkyBox::Draw() { + // We are inside the cube, we need to disable backface culling! + rlDisableBackfaceCulling(); + rlDisableDepthMask(); + cube.Draw({}); + rlEnableBackfaceCulling(); + rlEnableDepthMask(); + + return *this; + } + + // Generate cubemap texture from HDR texture + TextureCubemap SkyBox::GenTextureCubemap(Shader shader, Texture2D panorama, int size, int format) { + TextureCubemap cubemap = { 0 }; + + rlDisableBackfaceCulling(); // Disable backface culling to render inside the cube + + // STEP 1: Setup framebuffer + //------------------------------------------------------------------------------------------ + unsigned int rbo = rlLoadTextureDepth(size, size, true); + cubemap.id = rlLoadTextureCubemap(0, size, format, 1); + + // unsigned int fbo = rlLoadFramebuffer(size, size); + unsigned int fbo = rlLoadFramebuffer(); + rlFramebufferAttach(fbo, rbo, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_RENDERBUFFER, 0); + rlFramebufferAttach(fbo, cubemap.id, RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_CUBEMAP_POSITIVE_X, 0); + + // Check if framebuffer is complete with attachments (valid) + if (rlFramebufferComplete(fbo)) TraceLog(LOG_INFO, "FBO: [ID %i] Framebuffer object created successfully", fbo); + //------------------------------------------------------------------------------------------ + + // STEP 2: Draw to framebuffer + //------------------------------------------------------------------------------------------ + // NOTE: Shader is used to convert HDR equirectangular environment map to cubemap equivalent (6 faces) + rlEnableShader(shader.id); + + // Define projection matrix and send it to shader + Matrix matFboProjection = MatrixPerspective(90.0*DEG2RAD, 1.0, RL_CULL_DISTANCE_NEAR, RL_CULL_DISTANCE_FAR); + rlSetUniformMatrix(shader.locs[SHADER_LOC_MATRIX_PROJECTION], matFboProjection); + + // Define view matrix for every side of the cubemap + Matrix fboViews[6] = { + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ 1.0f, 0.0f, 0.0f }, Vector3{ 0.0f, -1.0f, 0.0f }), + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ -1.0f, 0.0f, 0.0f }, Vector3{ 0.0f, -1.0f, 0.0f }), + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ 0.0f, 1.0f, 0.0f }, Vector3{ 0.0f, 0.0f, 1.0f }), + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ 0.0f, -1.0f, 0.0f }, Vector3{ 0.0f, 0.0f, -1.0f }), + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ 0.0f, 0.0f, 1.0f }, Vector3{ 0.0f, -1.0f, 0.0f }), + MatrixLookAt(Vector3{ 0.0f, 0.0f, 0.0f }, Vector3{ 0.0f, 0.0f, -1.0f }, Vector3{ 0.0f, -1.0f, 0.0f }) + }; + + rlViewport(0, 0, size, size); // Set viewport to current fbo dimensions + + // Activate and enable texture for drawing to cubemap faces + rlActiveTextureSlot(0); + rlEnableTexture(panorama.id); + + for (int i = 0; i < 6; i++) { + // Set the view matrix for the current cube face + rlSetUniformMatrix(shader.locs[SHADER_LOC_MATRIX_VIEW], fboViews[i]); + + // Select the current cubemap face attachment for the fbo + // WARNING: This function by default enables->attach->disables fbo!!! + rlFramebufferAttach(fbo, cubemap.id, RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_CUBEMAP_POSITIVE_X + i, 0); + rlEnableFramebuffer(fbo); + + // Load and draw a cube, it uses the current enabled texture + rlClearScreenBuffers(); + rlLoadDrawCube(); + + // ALTERNATIVE: Try to use internal batch system to draw the cube instead of rlLoadDrawCube + // for some reason this method does not work, maybe due to cube triangles definition? normals pointing out? + // TODO: Investigate this issue... + //rlSetTexture(panorama.id); // WARNING: It must be called after enabling current framebuffer if using internal batch system! + //rlClearScreenBuffers(); + //DrawCubeV(Vector3Zero(), Vector3One(), WHITE); + //rlDrawRenderBatchActive(); + } + //------------------------------------------------------------------------------------------ + + // STEP 3: Unload framebuffer and reset state + //------------------------------------------------------------------------------------------ + rlDisableShader(); // Unbind shader + rlDisableTexture(); // Unbind texture + rlDisableFramebuffer(); // Unbind framebuffer + rlUnloadFramebuffer(fbo); // Unload framebuffer (and automatically attached depth texture/renderbuffer) + + // Reset viewport dimensions to default + rlViewport(0, 0, rlGetFramebufferWidth(), rlGetFramebufferHeight()); + rlEnableBackfaceCulling(); + //------------------------------------------------------------------------------------------ + + cubemap.width = size; + cubemap.height = size; + cubemap.mipmaps = 1; + cubemap.format = format; + + return cubemap; + } + + raylib::Shader SkyBox::cubemapShader(0); + +} \ No newline at end of file diff --git a/as5/skybox.hpp b/as5/skybox.hpp new file mode 100644 index 0000000..522e5c3 --- /dev/null +++ b/as5/skybox.hpp @@ -0,0 +1,58 @@ +/******************************************************************************************* +* +* raylib [models] example - Skybox loading and drawing +* +* Example originally created with raylib 1.8, last time updated with raylib 4.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2017-2023 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +#include "raylib-cpp.hpp" + +namespace cs381 { + struct SkyBox { + constexpr static std::string_view vertexShader = + #include "generated/skybox.vs" + ; + constexpr static std::string_view fragmentShader = + #include "generated/skybox.fs" + ; + constexpr static std::string_view cubemapVertexShader = + #include "generated/cubemap.vs" + ; + constexpr static std::string_view cubemapFragmentShader = + #include "generated/cubemap.fs" + ; + + static raylib::Shader cubemapShader; + + raylib::Texture texture; + raylib::Shader shader; + raylib::Model cube; + + SkyBox() : shader(0) {}; + SkyBox(SkyBox&) = delete; + SkyBox(SkyBox&&) = default; + SkyBox(const std::string_view filename, bool isEnviornment = false) : SkyBox() { + Load(filename, isEnviornment); + } + + ~SkyBox() { + if(cube.IsValid()) + UnloadTexture(cube.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture); + } + + SkyBox& Init(); + SkyBox& Load(const std::string_view filename, bool isEnviornment = false); + SkyBox& Draw(); + + + private: + // Generate cubemap texture from HDR texture + static TextureCubemap GenTextureCubemap(Shader shader, Texture2D panorama, int size, int format); + }; +} \ No newline at end of file