From b1bf0440267b2e21b78391a65124b3dc3bea33d5 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sun, 5 Apr 2026 11:21:04 -0700 Subject: [PATCH] Add AS8 --- as8/CMakeLists.txt | 24 ++++ as8/CO.cpp | 242 ++++++++++++++++++++++++++++++++++ as8/README.md | 71 ++++++++++ as8/as4.cpp | 264 ++++++++++++++++++++++++++++++++++++++ as8/component_counter.cpp | 3 + as8/generated/cubemap.fs | 30 +++++ as8/generated/cubemap.vs | 20 +++ as8/generated/skybox.fs | 30 +++++ as8/generated/skybox.vs | 24 ++++ as8/skybox.cpp | 168 ++++++++++++++++++++++++ as8/skybox.hpp | 58 +++++++++ 11 files changed, 934 insertions(+) create mode 100644 as8/CMakeLists.txt create mode 100644 as8/CO.cpp create mode 100644 as8/README.md create mode 100644 as8/as4.cpp create mode 100644 as8/component_counter.cpp create mode 100644 as8/generated/cubemap.fs create mode 100644 as8/generated/cubemap.vs create mode 100644 as8/generated/skybox.fs create mode 100644 as8/generated/skybox.vs create mode 100644 as8/skybox.cpp create mode 100644 as8/skybox.hpp diff --git a/as8/CMakeLists.txt b/as8/CMakeLists.txt new file mode 100644 index 0000000..1c19265 --- /dev/null +++ b/as8/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(as4 as4.cpp skybox.cpp) +target_link_libraries(as4 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/as8/CO.cpp b/as8/CO.cpp new file mode 100644 index 0000000..463c025 --- /dev/null +++ b/as8/CO.cpp @@ -0,0 +1,242 @@ +#include "AudioDevice.hpp" +#include "Color.hpp" +#include "Keyboard.hpp" +#include "Matrix.hpp" +#include "Mesh.hpp" +#include "Model.hpp" +#include "RadiansDegrees.hpp" +#include "raylib.h" +#include +#include +#include +#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 *e; + virtual void Setup() = 0; + virtual void Update(float dt) = 0; + virtual void Cleanup() = 0; +}; + +struct Entity { + std::vector> components; + + template T> + T &AddComponent() { + auto out = components.emplace_back(std::make_shared()); + out->e = this; + return out; + } + + template T> + std::optional> GetComponent() { + for (auto &c : components) { + T *cast = dynamic_cast(c.get()); + if (cast) { + return *cast; + } + } + + return { }; + } +}; + +struct TransformComponent : public Component { + raylib::Transform t; + + 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, []()); + } + + void Cleanup() override { } +}; + +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 3"); + window.SetState(FLAG_WINDOW_RESIZABLE); + raylib::AudioDevice audio; + + raylib::Model penguin("models/penguin.glb"); + + // behind and above the penguin (in penguin-local space) + const float CAM_DIST = 512.0f; + const float CAM_HEIGHT = 256.0f; + const float CAM_ANGULAR_VELOCITY = 2.0f; + const float CAM_PITCH_MIN = -0.5f; + const float CAM_PITCH_MAX = 1.5f; + + float camYaw = 3.14f; // offset by 90 deg so it faces in the proper direction + float camPitch = 0; + + raylib::Camera3D camera( + { 0, CAM_DIST * std::sin(camPitch), CAM_DIST * std::cos(camPitch) }, + { 0, 0, 0 }, + { 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(); + + // penguin physics + raylib::Vector3 position = { 0, 0, 0 }; + raylib::Vector3 velocity = { 0, 0, 0 }; + float heading = 0.0f; + float speed = 0.0f; + + // units/s + const float ACCELERATION = 100.0f; + + // in radians + const float ANGULAR_VELOCITY = 3.14f; + + window.SetTargetFPS(60); // save cpu cycles + + while (!window.ShouldClose()) { + window.BeginDrawing(); + window.ClearBackground(raylib::Color::Gray()); + + float dt = window.GetFrameTime(); + + position += velocity * dt * 0.5f; + + // movement for penguin + if (IsKeyDown(KEY_W)) { + speed += ACCELERATION * dt; + } + + if (IsKeyDown(KEY_S)) { + speed -= ACCELERATION * dt; + } + + if (IsKeyDown(KEY_A)) { + heading += ANGULAR_VELOCITY * dt; + } + + if (IsKeyDown(KEY_D)) { + heading -= ANGULAR_VELOCITY * dt; + } + + if (IsKeyDown(KEY_SPACE)) { + speed = 0.0f; + } + + velocity = raylib::Vector3 { + std::sin(heading) * speed, + 0.0f, + std::cos(heading) * speed + }; + + // ds = 1/2 * (v0 + v1) * dt + position += velocity * dt * 0.5f; + + // movement for camera + if (IsKeyDown(KEY_LEFT)) { + camYaw += CAM_ANGULAR_VELOCITY * dt; + } + if (IsKeyDown(KEY_RIGHT)) { + camYaw -= CAM_ANGULAR_VELOCITY * dt; + } + if (IsKeyDown(KEY_UP)) { + camPitch += CAM_ANGULAR_VELOCITY * dt; + } + if (IsKeyDown(KEY_DOWN)) { + camPitch -= CAM_ANGULAR_VELOCITY * dt; + } + + // clamp the angle between + camPitch = std::clamp(camPitch, CAM_PITCH_MIN, CAM_PITCH_MAX); + + // x = cos(pitch) * sin(yaw) + // y = sin(pitch) + // z = cos(pitch) * cos(yaw) + float yaw = camYaw + heading; // follow penguin + raylib::Vector3 camOffset = { + CAM_DIST * std::cos(camPitch) * std::sin(yaw), + CAM_DIST * std::sin(camPitch) + CAM_HEIGHT, + CAM_DIST * std::cos(camPitch) * std::cos(yaw) + }; + camera.SetPosition(position + camOffset); + camera.SetTarget(position); + + camera.BeginMode(); + skybox.Draw(); + + ground.Draw({ 0, 0, 0 }, 1.0f, raylib::Color::White()); + + DrawBoundedModel(penguin, [&position, &heading](raylib::Matrix transform) { + return transform + .RotateY(heading) + .Scale(40, 40, 40) + .Translate(position); + }); + + camera.EndMode(); + window.EndDrawing(); + } + + return 0; +} diff --git a/as8/README.md b/as8/README.md new file mode 100644 index 0000000..0ac1e67 --- /dev/null +++ b/as8/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/as8/as4.cpp b/as8/as4.cpp new file mode 100644 index 0000000..a2ac052 --- /dev/null +++ b/as8/as4.cpp @@ -0,0 +1,264 @@ +#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 +#include +#include +#include + +#define SKYBOX_IMPLEMENTATION +#include "skybox.hpp" + +extern size_t global_component_counter; + +template +size_t get_component_id(T reference = { }) { + static size_t id = global_component_counter++; + return id; +} + +void DrawBoundedModel(raylib::Model &model, bool drawBoundingBox, 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 + if (drawBoundingBox) { + 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); +} + +struct BaseComponentStorage { + virtual ~BaseComponentStorage() { }; + virtual size_t element_size() { return 0; } + virtual void *get(size_t idx) = 0; + virtual void *get_or_allocate(size_t idx) = 0; +}; + +template +struct ComponentStorage : BaseComponentStorage, std::vector { + using std::vector::vector; + + std::vector data; + + size_t element_size() override { + return sizeof(TComponent); + } + + void *get(size_t idx) override { + return &this->data[idx]; + } + + void *get_or_allocate(size_t idx) override { + if (this->size() <= idx) { + this->resize(idx + 1); + } + return get(idx); + } +}; + +using Entity = size_t; + +struct Context { + std::vector> entity_masks; + + std::vector> storages = { nullptr }; + + template + ComponentStorage &get_storage() { + size_t id = get_component_id(); + if (storages.size() <= id) { + storages.insert( + storages.end(), id - storages.size() + 1, nullptr); + } + + if (!storages[id] || storages[id]->element_size() == 0) { + storages[id] = std::make_shared>(); + } + return *storages[id]; + } + + Entity create_entity() { + Entity e = entity_masks.size(); + entity_masks.emplace_back(std::vector { false }); + return e; + } + + void remove_entity(Entity e) { + entity_masks[e].clear(); + } + + template + TComponent &add_component(Entity e, TComponent component) { + size_t id = get_component_id(); + auto &mask = entity_masks[e]; + if (mask.size() <= id) { + mask.resize(id + 1, false); + } + mask[id] = true; + auto &storage = get_storage(); + return *(TComponent *)get_storage().get_or_allocate(e); + } + + template + TComponent *get_component(Entity e) { + size_t id = get_component_id(); + if (e >= entity_masks.size() || id >= entity_masks[e].size() || !entity_masks[e][id]) { + return nullptr; + } + auto &storage = get_storage(); + return *(TComponent *)get_storage().get(e); + } + + template + bool has_component(Entity e) { + size_t id = get_component_id(); + return e < entity_masks.size() && id < entity_masks[e].size() && entity_masks[e][id]; + } +}; + +//struct Entity { +// raylib::Vector3 position = raylib::Vector3::Zero(); +// float speed = 0; +// raylib::Degree heading = 0; +// raylib::Degree pitch = 0; +// raylib::Model *model; +// enum EntityType { +// Penguin = 0, +// Eagle = 1, +// } type = EntityType::Penguin; +//}; +// + +struct ModelComponent { + raylib::Model* model; +}; + +struct PositionComponent { + raylib::Vector3 position; +}; + +void DrawModelFunction(Context& ctx, entity e) { + if(!ctx.HasComponent(e)) return; + if(!ctx.HasComponent(e)) return; + + auto model = ctx.GetComponent(e); + auto position = ctx.GetComponent(e); + model.model->Draw(position.position); +} + +auto sequential(auto func) { + return [func](Context& ctx) { + // Bulk process + for(entity e = 0; e < ctx.entityMasks.size(); ++e) { + func(ctx, e); + } + }; +} + +auto parallel(auto func) { + return [func](Context& ctx) { + std::vector entities(ctx.entityMasks.size()); + std::iota(entities.begin(), entities.end(), 0); + std::for_each(std::execution::par_unseq, entities.begin(), entities.end(), [func, &ctx](entity e){ + func(ctx, e); + }); + }; +} + +template... Tsystems> +auto sequential(Tsystems... systems) { + return [=](Context& ctx) { + (systems(ctx), ...); + }; +} + +int main() { + raylib::Window window(800, 600, "As0"); + window.SetState(FLAG_WINDOW_RESIZABLE); + raylib::AudioDevice audio; + + raylib::Model penguin("models/penguin.glb"); + penguin.transform = raylib::Transform(penguin.transform).Scale(30).RotateY(raylib::Degree(90)); + raylib::Camera camera({0, 120, 500}, {0, 0, 0}); + + raylib::Model ground = raylib::Mesh::Plane(10000, 10000, 50, 50, 25).LoadModelFrom(); + raylib::Texture snow("textures/snow.jpg"); + ground.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = snow; + + cs381::SkyBox skybox("textures/skybox.png"); + + // Add things to entity. + // std::vector entities; + // auto& e = entities.emplace_back(); + + // // raylib::Quaternion::Identity() + // auto q = raylib::Quaternion::FromEuler(raylib::Degree(45).RadianValue(), 0, 0); + // // raylib::Quaternion::FromAxisAngle(const ::Vector3 &axis, const Radian angle) + + // raylib::Quaternion targetRotation; + // targetRotation = targetRotation * raylib::Quaternion::FromEuler(raylib::Degree(45).RadianValue(), 0, 0); + + // raylib::Quaternion rotation; + // rotation = rotation.Slerp(targetRotation, angluarAcceleration * dt); + + // raylib::Vector3::Forward().RotateByQuaternion(rotation); + + const float acceleration = 10; + + Context ctx; + auto e = ctx.CreateEntity(); + ctx.AddComponent(e).model = &penguin; + ctx.AddComponent(e).position = raylib::Vector3{0, 0, 0}; + + auto schedule = sequential( + parallel(DrawModelFunction), + parallel(DrawModelFunction) + ); + + while(!window.ShouldClose()) { + window.BeginDrawing(); { + window.ClearBackground(raylib::Color::RayWhite()); + float dt = window.GetFrameTime(); + + camera.BeginMode(); { + skybox.Draw(); + ground.Draw({}); + + schedule(ctx); + + } camera.EndMode(); + + window.DrawFPS(); + } window.EndDrawing(); + } +} diff --git a/as8/component_counter.cpp b/as8/component_counter.cpp new file mode 100644 index 0000000..386a2e6 --- /dev/null +++ b/as8/component_counter.cpp @@ -0,0 +1,3 @@ +#include + +size_t global_component_counter = 0; diff --git a/as8/generated/cubemap.fs b/as8/generated/cubemap.fs new file mode 100644 index 0000000..54392d2 --- /dev/null +++ b/as8/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/as8/generated/cubemap.vs b/as8/generated/cubemap.vs new file mode 100644 index 0000000..7f5e04f --- /dev/null +++ b/as8/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/as8/generated/skybox.fs b/as8/generated/skybox.fs new file mode 100644 index 0000000..c4cd6af --- /dev/null +++ b/as8/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/as8/generated/skybox.vs b/as8/generated/skybox.vs new file mode 100644 index 0000000..00596e0 --- /dev/null +++ b/as8/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/as8/skybox.cpp b/as8/skybox.cpp new file mode 100644 index 0000000..fafdf84 --- /dev/null +++ b/as8/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/as8/skybox.hpp b/as8/skybox.hpp new file mode 100644 index 0000000..522e5c3 --- /dev/null +++ b/as8/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