diff --git a/CMakeLists.txt b/CMakeLists.txt index edbc75d4e2d1a840c657a8f8003ebd40327870dc..ace25c76b966374a3f12914c084d5ca955598877 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED NO) set(CMAKE_CXX_EXTENSIONS OFF) # major, minor, patch, and _STAGE_ (0 = alpha, 1 = beta, 2 = gamma/release) -project(radiance_cascades VERSION 0.10.8.0) +project(radiance_cascades VERSION 0.10.9.0) configure_file(src/config.h.in config.h) diff --git a/README.md b/README.md index 99329c65a7a9596d40391b53448f0f26bafe170d..1431b439df55255b86af8e02077300264dad5b4c 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,27 @@ 1. [Requirements](#requirements) 2. [Setup (macOS, Linux)](#setup-macos-linux) 3. [Setup (Windows)](#setup-windows-visual-studio) +3. [Controls](#controls) -2D lighting demo. +2D lighting demo. Intended for 1080p displays. -Maze image texture initially generated via [mazegenerator.net](https://www.mazegenerator.net/). +Maze image texture initially generated via [mazegenerator.net](https://www.mazegenerator.net/). ## Requirements - CMake 3.25 - A C++11 (or higher) compiler - Raylib 5.5 (provided via CMake if not installed system-wide) + - If Raylib is not [installed](https://github.com/raysan5/raylib#build-and-installation) on your machine, CMake will download and build Raylib for you in the build directory. This currently builds and runs on macOS, Arch Linux (on X11), and Windows. No planned support for Wayland :P -## Setup (macOS, Linux) - -If Raylib is not installed on your machine, CMake will download and build Raylib for you in the build directory. +## Setup (macOS, Linux) ```bash -# run the convenience build script +# run the convenience build script ./build.sh # to build without running -./build.sh -r # to build and run the resulting binary +./build.sh -r # to build and run the resulting binary # or build it manually via CMake mkdir build @@ -36,8 +36,6 @@ cd .. # run the resulting binary in the source directory, not the build director ## Setup (Windows Visual Studio) -If Raylib is not installed on your machine, CMake will download and build Raylib for you in the build directory. - Via command prompt: ```bash @@ -49,3 +47,21 @@ cmake .. This will generate a VS solution file (.sln) for you to use for compilation. Make sure to run any resulting .exe from the project root directory. + +## Controls + +1, 2, & 3 toggles between drawing, lighting, and viewing mode. + +Scrolling up or down increase brush/light size depending on if you are in drawing or lighting mode. + +C clears the canvas if in drawing mode, and removes all lights if in lighting mode. + +R resets the canvas to the original maze layout if in drawing mode, and resets to the starting lights if in lighting mode. + +F3 to open the debug UI. When the debug UI is open, scrolling the mouse will change the amount of shadow cascades. + +Left-mouse-button erases in drawing mode, or deletes nearby lights when in lighting mode. + +Right-mouse-button draws in drawing mode, or places a light when in lighting mode. + +Middle-mouse-button can be used to move lights around in any mode. diff --git a/build.sh b/build.sh index 220a62f6a1c4446b4c7cfb77b12c00588a9a3c41..d1e91756b1e3e3b55340f69885f8dc18d2d5729a 100755 --- a/build.sh +++ b/build.sh @@ -9,6 +9,10 @@ cmake .. make popd -while getopts "r" arg; do - ./build/radiance_cascades +while getopts ":r" arg; do + case ${arg} in + r) + ./build/radiance_cascades + ;; + esac done diff --git a/res/shaders/broken.frag b/res/shaders/broken.frag index ae255d6333a9bb4f88a3b37aa46549b0ced875a5..a9c500977f0e45c37173ca99c9dc5df29f7c989c 100644 --- a/res/shaders/broken.frag +++ b/res/shaders/broken.frag @@ -4,21 +4,23 @@ #define PRIMARY vec4(1.0f, 0.0f, 1.0f, 1.0f) #define SECONDARY vec4(0.0f, 0.0f, 0.0f, 1.0f) +out vec4 fragColor; + // checkerboard pattern void main() { vec2 pos = mod(gl_FragCoord.xy,vec2(N)); if ((pos.x > N/2) && (pos.y > N/2)){ - gl_FragColor = PRIMARY; + fragColor = PRIMARY; } else if ((pos.x < N/2) && (pos.y < N/2)){ - gl_FragColor = PRIMARY; + fragColor = PRIMARY; } else if ((pos.x < N/2) && (pos.y > N/2)){ - gl_FragColor = SECONDARY; + fragColor = SECONDARY; } else if ((pos.x > N/2) && (pos.y < N/2)){ - gl_FragColor = SECONDARY; + fragColor = SECONDARY; } } diff --git a/res/shaders/lighting.frag b/res/shaders/lighting.frag index 2b640ad030c27c868090a0c576fd91fda382a8fd..d413e38f0466c77cbf1dff0b50df6366d744c620 100644 --- a/res/shaders/lighting.frag +++ b/res/shaders/lighting.frag @@ -1,81 +1,96 @@ #version 330 core +#define VIEWER_OUT_OF_SIGHT_BRIGHTNESS 0.015 +#define VIEWER_GRADIENT_RADIUS 450 + struct Light { vec2 position; vec3 color; - float size; + float radius; }; in vec2 fragTexCoord; -out vec4 finalColor; +out vec4 fragColor; +// data +uniform Light lights[128]; +uniform int uLightsAmount; uniform sampler2D uOcclusionMask; uniform float uTime; -uniform int uLightsAmount; uniform vec2 uResolution; uniform vec2 uPlayerLocation; -uniform int uApple; -uniform int uSmoothShadows; -uniform int uCascadeAmount; +// config +uniform int uCascadeAmount; // the amount of cascades primarily affects performance & light leakage. Thicker walls = less cascades needed, thinner walls = more cascades needed uniform int uViewing; -uniform Light lights[64]; - // sourced from https://gist.github.com/companje/29408948f1e8be54dd5733a74ca49bb9 float map(float value, float min1, float max1, float min2, float max2) { return min2 + (value - min1) * (max2 - min2) / (max1 - min1); } // this technique is sourced from https://www.shadertoy.com/view/tddXzj -float terrain(vec2 p, vec2 normalisedLightPos, int smoothShadows) +float terrain(vec2 p, vec2 position, bool smoothShadows) { - if (smoothShadows == 1) { - if (uApple == 0) return mix(map(distance(uResolution * normalisedLightPos, gl_FragCoord.xy), 0.0, uResolution.x, 0.9, 1.0), 1.0, step(0.25, texture(uOcclusionMask, p).x)); + if (smoothShadows) { + // increased opacity closer to the light source + return mix( + map( + distance(position, gl_FragCoord.xy), + 0.0, + ((uResolution.x < uResolution.y) ? uResolution.y : uResolution.x) * 500, + 0.9, + 1.0 + ), + 1.0, + step(0.25, texture(uOcclusionMask, p).x) + ); } return step(0.25, texture(uOcclusionMask, p).x); // hard shadows } -void main() { - vec3 result = vec3(0.0); - - for (int i = 0; i < uLightsAmount; i++) { - vec2 normalisedLightPos = lights[i].position/uResolution; - float brightness = 1.0; - - for (float j = 0.0; j < uCascadeAmount; j++) { - float t = j / uCascadeAmount; - float h = terrain(mix(fragTexCoord, normalisedLightPos, t), normalisedLightPos, uSmoothShadows); - brightness *= h; - } +vec3 calcVisibility(vec2 position, float gradientRadius = 300, vec3 rgb = vec3(1.0), bool smoothShadows = true) { + // no need to bother calculating light if the light value will be overridden by the gradient anyway + if (distance(fragTexCoord*uResolution, position) > gradientRadius) return vec3(0); - // radial gradient - adapted from https://www.shadertoy.com/view/4tjSWh + float brightness = 1.0; - if (uApple == 1) { - brightness *= 1.0 - smoothstep(0.0, 0.5, length(fragTexCoord - normalisedLightPos)); - } else { - brightness *= 1.0 - distance(uResolution.xy * vec2(normalisedLightPos.x, 1.0 - normalisedLightPos.y), gl_FragCoord.xy) * 2/lights[i].size; - } + vec2 normalisedPos = position/uResolution; - if (brightness > 0) result += brightness * lights[i].color; + for (float j = 0.0; j < uCascadeAmount; j++) { + float t = j / uCascadeAmount; + float h = terrain(mix(fragTexCoord, normalisedPos, t), position, smoothShadows); + brightness *= h; } - if (uViewing == 1) { - vec2 normalisedPlayerPos = uPlayerLocation/uResolution; - vec3 visible = vec3(1.0); + // radial gradient - adapted from https://www.shadertoy.com/view/4tjSWh + brightness *= 1.0 - distance(vec2(position.x, uResolution.y - position.y), gl_FragCoord.xy) * 1/gradientRadius; + + return (brightness > 0) ? brightness * rgb : vec3(0); +} - for (float j = 0.0; j < uCascadeAmount; j++) { - float t = j / uCascadeAmount; - float h = terrain(mix(fragTexCoord, normalisedPlayerPos, t), normalisedPlayerPos, 0); - visible *= h; - } +void main() { + vec3 result = vec3(0.0); - visible = mix(texture(uOcclusionMask, fragTexCoord).xyz * 0.015, result.xyz, visible); + for (int i = 0; i < uLightsAmount; i++) { + result += calcVisibility(lights[i].position, lights[i].radius, lights[i].color); + } - gl_FragColor = vec4(visible, 1.0f); + if (uViewing == 1) { + fragColor = vec4( + mix( + texture(uOcclusionMask, fragTexCoord).xyz * VIEWER_OUT_OF_SIGHT_BRIGHTNESS, + result.xyz, + calcVisibility(uPlayerLocation, VIEWER_GRADIENT_RADIUS, vec3(1.0), false) + ), + 1.0 + ); } else { // combine light result w/ underlying occlusion mask texture - gl_FragColor = vec4(result * step(0.25, texture(uOcclusionMask, fragTexCoord)).xxx, 1.0f); + fragColor = vec4( + result * step(0.25, texture(uOcclusionMask, fragTexCoord)).xxx, + 1.0 + ); } } diff --git a/res/brush.png b/res/textures/brush.png similarity index 100% rename from res/brush.png rename to res/textures/brush.png diff --git a/res/canvas.png b/res/textures/canvas.png similarity index 100% rename from res/canvas.png rename to res/textures/canvas.png diff --git a/res/cursor.png b/res/textures/cursor.png similarity index 100% rename from res/cursor.png rename to res/textures/cursor.png diff --git a/res/textures/icons/drawing.png b/res/textures/icons/drawing.png new file mode 100644 index 0000000000000000000000000000000000000000..8cd2f5971f37e44bd716860169bc6cb2dd0413b9 Binary files /dev/null and b/res/textures/icons/drawing.png differ diff --git a/res/textures/icons/lighting.png b/res/textures/icons/lighting.png new file mode 100644 index 0000000000000000000000000000000000000000..199857ea2cf6adfa1641e7cc3cd663aa9fcb18e9 Binary files /dev/null and b/res/textures/icons/lighting.png differ diff --git a/res/textures/icons/viewing.png b/res/textures/icons/viewing.png new file mode 100644 index 0000000000000000000000000000000000000000..98febe04add1536acc25f9b8f85b3d5de9351fbf Binary files /dev/null and b/res/textures/icons/viewing.png differ diff --git a/src/config.h.in b/src/config.h.in index 0ab63c31931f983bc21a6fbb5307ac202d6f9d2c..08dd400cbb281d2f9908c13d759403e40bce5004 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -18,3 +18,5 @@ #define SCREEN_WIDTH 800 #define SCREEN_HEIGHT 600 + +#define CURSOR_SIZE 0.1 diff --git a/src/game.cpp b/src/game.cpp index 97aff39f38f8e728159c5bf425fa5d3a26ffdeae..dd49af3140ea527668fe5b28b9231ab21278a440 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -10,19 +10,16 @@ void placeLights(std::vector<Light>* lights, int N = 4, float d = 256.0) { Light l; l.position = Vector2{ SCREEN_WIDTH/2 + std::sin(t) * d, SCREEN_HEIGHT/2 + std::cos(t) * d}; l.color = Vector3{ std::sin(t), std::cos(t), 1.0 }; - l.size = 600; + l.radius = 300; lights->push_back(l); } } Game::Game() { debug = false; - smoothShadows = true; mode = DRAWING; - cascadeAmount = 512; - #if __APPLE__ - cascadeAmount = 128; - #endif + + currentToolIcon = LoadTextureFromImage(LoadImage("res/textures/icons/drawing.png")); lightingShader = LoadShader(0, "res/shaders/lighting.frag"); if (!IsShaderValid(lightingShader)) { @@ -31,14 +28,16 @@ Game::Game() { lightingShader = LoadShader(0, "res/shaders/broken.frag"); } - cursor.img = LoadImage("res/cursor.png"); + cascadeAmount = 512; + + cursor.img = LoadImage("res/textures/cursor.png"); cursor.tex = LoadTextureFromImage(cursor.img); - brush.img = LoadImage("res/brush.png"); + brush.img = LoadImage("res/textures/brush.png"); brush.tex = LoadTextureFromImage(brush.img); brush.scale = 0.25; - canvas.img = LoadImage("res/canvas.png"); + canvas.img = LoadImage("res/textures/canvas.png"); canvas.tex = LoadTextureFromImage(canvas.img); placeLights(&lights); @@ -60,13 +59,6 @@ void Game::update() { SetShaderValue(lightingShader, GetShaderLocation(lightingShader, "uLightsAmount"), &lightsAmount, SHADER_UNIFORM_INT); SetShaderValue(lightingShader, GetShaderLocation(lightingShader, "uCascadeAmount"), &cascadeAmount, SHADER_UNIFORM_INT); - int apple = 0; - #ifdef __APPLE__ - apple = 1; - #endif - SetShaderValue(lightingShader, GetShaderLocation(lightingShader, "uApple"), &apple, SHADER_UNIFORM_INT); - SetShaderValue(lightingShader, GetShaderLocation(lightingShader, "uSmoothShadows"), &smoothShadows, SHADER_UNIFORM_INT); - int viewing = 0; if (mode == VIEWING) viewing = 1; SetShaderValue(lightingShader, GetShaderLocation(lightingShader, "uViewing"), &viewing, SHADER_UNIFORM_INT); @@ -84,8 +76,7 @@ void Game::render() { for (int i = 0; i < lights.size(); i++) { SetShaderValue(lightingShader, GetShaderLocation(lightingShader, TextFormat("lights[%i].position", i)), &lights[i].position, SHADER_UNIFORM_VEC2); SetShaderValue(lightingShader, GetShaderLocation(lightingShader, TextFormat("lights[%i].color", i)), &lights[i].color, SHADER_UNIFORM_VEC3); - SetShaderValue(lightingShader, GetShaderLocation(lightingShader, TextFormat("lights[%i].size", i)), &lights[i].size, SHADER_UNIFORM_FLOAT); - if (debug) DrawCircleLinesV(lights[i].position, lights[i].size/64, GREEN); + SetShaderValue(lightingShader, GetShaderLocation(lightingShader, TextFormat("lights[%i].radius", i)), &lights[i].radius, SHADER_UNIFORM_FLOAT); } } @@ -97,7 +88,9 @@ void Game::renderUI() { if (mode == LIGHTING) str = "LIGHTING"; else if (mode == VIEWING) str = "VIEWING"; - DrawText(str.c_str(), 0, SCREEN_HEIGHT - 8, 1, ColorFromHSV(0.0, 0.0, 1.0 - v)); + DrawTexture(currentToolIcon, 0, SCREEN_HEIGHT-8, WHITE); + + DrawText(str.c_str(), 12, SCREEN_HEIGHT - 8, 1, ColorFromHSV(0.0, 0.0, 1.0 - v)); switch (mode) { case DRAWING: @@ -109,12 +102,13 @@ void Game::renderUI() { Color{ 0, 0, 0, 64} ); break; case LIGHTING: - DrawCircleLines(GetMouseX(), GetMouseY(), (brush.scale*1200)/64, ColorFromNormalized(Vector4{ std::sin(time), std::cos(time), 1.0, 0.5 })); + DrawCircleLines(GetMouseX(), GetMouseY(), (brush.scale*600*2)/64, ColorFromNormalized(Vector4{ std::sin(time), std::cos(time), 1.0, 0.5 })); + for (int i = 0; i < lights.size(); i++) { + DrawCircleLinesV(lights[i].position, lights[i].radius/64*2, GREEN); + } break; } - #define CURSOR_SIZE 0.1 - DrawTextureEx(cursor.tex, Vector2{ (float)(GetMouseX() - cursor.img.width/2*CURSOR_SIZE), (float)(GetMouseY() - cursor.img.height/2*CURSOR_SIZE) }, @@ -127,17 +121,22 @@ void Game::renderUI() { DrawText(TextFormat("%i FPS", GetFPS()), 0, 0, 1, GREEN); DrawText(TextFormat("%i lights", lights.size()), 0, 8, 1, GREEN); - if (mode == DRAWING) DrawText(TextFormat("%f brush scale", brush.scale), 0, 32, 1, GREEN); - else if (mode == LIGHTING) DrawText(TextFormat("%f light size", brush.scale * 2400), 0, 32, 1, GREEN); + if (mode == DRAWING) DrawText(TextFormat("%f brush scale", brush.scale), 0, 32, 1, GREEN); + else if (mode == LIGHTING) DrawText(TextFormat("%f light size (diameter)", brush.scale * 600 * 2), 0, 32, 1, GREEN); DrawText(TextFormat("%i cascades", cascadeAmount), 0, 40, 1, GREEN); - if (smoothShadows) DrawText("smoothShadows ON", 0, 48, 1, GREEN); } void Game::processKeyboardInput() { auto changeMode = [this](Mode m) { mode = m; timeSinceModeSwitch = GetTime(); + if (m == DRAWING) + currentToolIcon = LoadTextureFromImage(LoadImage("res/textures/icons/drawing.png")); + else if (m == LIGHTING) + currentToolIcon = LoadTextureFromImage(LoadImage("res/textures/icons/lighting.png")); + else + currentToolIcon = LoadTextureFromImage(LoadImage("res/textures/icons/viewing.png")); }; if (IsKeyPressed(KEY_ONE)) changeMode(DRAWING); @@ -150,7 +149,6 @@ void Game::processKeyboardInput() { if (!DirectoryExists("screenshots")) MakeDirectory("screenshots"); TakeScreenshot("screenshots/screenshot.png"); } - if (IsKeyPressed(KEY_S)) (smoothShadows == 0) ? smoothShadows = 1 : smoothShadows = 0; // clearing if (IsKeyPressed(KEY_C)) { @@ -169,9 +167,18 @@ void Game::processKeyboardInput() { // replacing if (IsKeyPressed(KEY_R)) { - if (mode == DRAWING) { + if (IsKeyDown(KEY_LEFT_CONTROL)) { + // reloading + printf("Reloading shaders.\n"); + UnloadShader(lightingShader); + lightingShader = LoadShader(0, "res/shaders/lighting.frag"); + if (!IsShaderValid(lightingShader)) { + UnloadShader(lightingShader); + lightingShader = LoadShader(0, "res/shaders/broken.frag"); + } + } else if (mode == DRAWING) { printf("Replacing canvas.\n"); - canvas.img = LoadImage("res/canvas.png"); + canvas.img = LoadImage("res/textures/canvas.png"); RELOAD_CANVAS(); } else if (mode == LIGHTING) { printf("Replacing lights.\n"); @@ -179,19 +186,6 @@ void Game::processKeyboardInput() { placeLights(&lights); } } - - if (IsKeyDown(KEY_LEFT_CONTROL)) { - // reloading - if (IsKeyPressed(KEY_R)) { - printf("Reloading shaders.\n"); - UnloadShader(lightingShader); - lightingShader = LoadShader(0, "res/shaders/lighting.frag"); - if (!IsShaderValid(lightingShader)) { - UnloadShader(lightingShader); - lightingShader = LoadShader(0, "res/shaders/broken.frag"); - } - } - } } void Game::processMouseInput() { @@ -219,9 +213,9 @@ void Game::processMouseInput() { ImageDraw(&canvas.img, brush.img, Rectangle{ 0, 0, (float)canvas.img.width, (float)canvas.img.height }, - Rectangle{ static_cast<float>(GetMouseX() - brush.img.width/2 * brush.scale), + Rectangle{ static_cast<float>(GetMouseX() - brush.img.width/2 * brush.scale), static_cast<float>(GetMouseY() - brush.img.height/2 * brush.scale), - static_cast<float>(brush.img.width * brush.scale), + static_cast<float>(brush.img.width * brush.scale), static_cast<float>(brush.img.height * brush.scale) }, BLACK); RELOAD_CANVAS(); @@ -229,9 +223,9 @@ void Game::processMouseInput() { ImageDraw(&canvas.img, brush.img, Rectangle{ 0, 0, (float)canvas.img.width, (float)canvas.img.height }, - Rectangle{ static_cast<float>(GetMouseX() - brush.img.width/2 * brush.scale), + Rectangle{ static_cast<float>(GetMouseX() - brush.img.width/2 * brush.scale), static_cast<float>(GetMouseY() - brush.img.height/2 * brush.scale), - static_cast<float>(brush.img.width * brush.scale), + static_cast<float>(brush.img.width * brush.scale), static_cast<float>(brush.img.height * brush.scale) }, WHITE); RELOAD_CANVAS(); @@ -242,7 +236,7 @@ void Game::processMouseInput() { Light l; l.position = Vector2{ static_cast<float>(GetMouseX()), static_cast<float>(GetMouseY()) }; l.color = Vector3{ std::sin(time), std::cos(time), 1.0 }; - l.size = brush.scale * 1200; + l.radius = brush.scale * 600; lights.push_back(l); } else if (IsMouseButtonDown(1)) { for (int i = 0; i < lights.size(); i++) { diff --git a/src/game.h b/src/game.h index 4f35038536b7c95e976fc33bf2e37d3e6e8134f4..4ddaad82e8237fd66512ce4054336591c10b7f69 100644 --- a/src/game.h +++ b/src/game.h @@ -11,7 +11,7 @@ struct Light { Vector2 position; Vector3 color; - float size; // in pixels + float radius; // in pixels }; class Game { @@ -25,13 +25,14 @@ class Game { private: Shader lightingShader; - int smoothShadows; int cascadeAmount; bool debug; float time; double timeSinceModeSwitch; + Texture2D currentToolIcon; + std::vector<Light> lights; enum Mode { diff --git a/src/main.cpp b/src/main.cpp index 2a18daa5abae551074602c49fda6e5716babf738..1d1c5de355f204a354f73faba89cba785235d525 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ int main() { InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, title.c_str()); SetTargetFPS(GetMonitorRefreshRate(GetCurrentMonitor())); - SetTraceLogLevel(LOG_ERROR); + SetTraceLogLevel(LOG_WARNING); Game game;