diff --git a/.gitignore b/.gitignore index 74e3f67..856dc60 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -wallpaper \ No newline at end of file +wallpaper +distance_field_generator \ No newline at end of file diff --git a/Makefile b/Makefile index 16d5c4f..e6fbeb2 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,7 @@ -wallpaper: src/main.c - gcc -Werror -Wall -Wextra -I./vendor/raylib-5.5_linux_amd64/include/ -o wallpaper src/main.c -L./vendor/raylib-5.5_linux_amd64/lib -l:libraylib.a -lm \ No newline at end of file +all: wallpaper distance_field_generator + +wallpaper: src/wallpaper.c + gcc -Werror -Wall -Wextra -I./vendor/raylib-5.5_linux_amd64/include/ -o wallpaper src/wallpaper.c -L./vendor/raylib-5.5_linux_amd64/lib -l:libraylib.a -lm + +distance_field_generator: src/distance_field_generator.c + gcc -Werror -Wall -Wextra -fopenmp -I./vendor/raylib-5.5_linux_amd64/include/ -o distance_field_generator src/distance_field_generator.c -L./vendor/raylib-5.5_linux_amd64/lib -l:libraylib.a -lm \ No newline at end of file diff --git a/README.md b/README.md index 43322b7..46bd4bf 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -# wallpaper \ No newline at end of file +# wallpaper + +## Quick start + +```console +$ make + +# generate distance field with 50px search radius +# note that i already ship a generated distance field +$ ./distance_field_generator ./resources/textures/background_mask.png ./resources/textures/background_distance_field.png 50 + +# run wallpaper, image paths are customizables +$ ./wallpaper +``` diff --git a/resources/shaders/basic.fs b/resources/shaders/basic.fs deleted file mode 100644 index a91c611..0000000 --- a/resources/shaders/basic.fs +++ /dev/null @@ -1,50 +0,0 @@ -#version 330 - -in vec2 fragTexCoord; -in vec4 fragColor; - -uniform vec2 resolution; -uniform vec2 mouse_pos; -uniform float ball_radius; -uniform vec2 polygon_points[6]; -uniform float polygon_influence; - -out vec4 finalColor; - -float distanceToPolygon(vec2 point, vec2 polyPoints[6]) { - float minDist = 100000.0; - - for (int i = 0; i < 6; i++) { - int j = (i + 1) % 6; - vec2 a = polyPoints[i]; - vec2 b = polyPoints[j]; - - vec2 pa = point - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); - float dist = length(pa - ba * h); - minDist = min(minDist, dist); - } - - return minDist; -} - -void main() -{ - vec2 pixel_pos = fragTexCoord * resolution; - - vec2 ball_pixel_pos = mouse_pos; - float ball_dist = distance(pixel_pos, ball_pixel_pos); - float ball_value = (ball_radius * ball_radius) / (ball_dist * ball_dist + 1.0); - - float poly_dist = distanceToPolygon(pixel_pos, polygon_points); - float poly_value = polygon_influence / (poly_dist * poly_dist + 1.0); - - float total_value = ball_value + poly_value; - - if (total_value > 0.7) { - finalColor = vec4(1.0, 1.0, 1.0, 1.0); // White - } else { - finalColor = vec4(0.0, 0.0, 0.0, 1.0); // Black - } -} \ No newline at end of file diff --git a/resources/shaders/distance_field.fs b/resources/shaders/distance_field.fs new file mode 100644 index 0000000..014e5ff --- /dev/null +++ b/resources/shaders/distance_field.fs @@ -0,0 +1,60 @@ +#version 330 + +in vec2 fragTexCoord; +in vec4 fragColor; + +uniform sampler2D texture0; +uniform sampler2D distanceFieldTex; +uniform vec2 resolution; +uniform vec2 mousePos; +uniform float ballRadius; +uniform float shapeInfluence; + +out vec4 finalColor; + +float getBallInfluence(vec2 pos, vec2 center, float radius) { + vec2 diff = pos - center; + float distSq = dot(diff, diff); + float radiusSq = radius * radius; + if (distSq > radiusSq) return 0.0; + float dist = sqrt(distSq); + return 1.0 - (dist / radius); +} + +float getShapeInfluence(vec2 pos) { + vec2 uv = pos / resolution; + uv.y = 1 - uv.y + 0.045; // invert y and offset to match with wallpaper texture + + if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) { + return 0.0; + } + + float signedDistance = texture(distanceFieldTex, uv).r * 255.0 - 128.0; + + float isInside = step(signedDistance, 0.0); + float isOutside = 1.0 - isInside; + + // inside calculation + float normalizedDist = abs(signedDistance) * 0.015625; // 1/64 + float insideInfluence = shapeInfluence * exp(-normalizedDist * 0.5) * 3.0; + + // outside calculation + float adjustedDistance = signedDistance * 0.5; + float shapeInfluenceSq = shapeInfluence * shapeInfluence; + float outsideInfluence = shapeInfluenceSq / (adjustedDistance * adjustedDistance + shapeInfluence) + + shapeInfluence * 0.8 * exp(-signedDistance * 0.025); // 1/40 + + return isInside * insideInfluence + isOutside * outsideInfluence; +} + +void main() +{ + vec2 pixelPos = fragTexCoord * resolution; + + float ballValue = getBallInfluence(pixelPos, mousePos, ballRadius); + float shapeValue = getShapeInfluence(pixelPos); + float totalValue = ballValue + shapeValue * 0.01; + + float mask = step(0.75, totalValue); + finalColor = vec4(mask, mask, mask, 1.0); +} \ No newline at end of file diff --git a/resources/textures/background.png b/resources/textures/background.png new file mode 100644 index 0000000..d8f1aae Binary files /dev/null and b/resources/textures/background.png differ diff --git a/resources/textures/background_distance_field.png b/resources/textures/background_distance_field.png new file mode 100644 index 0000000..405e0c7 Binary files /dev/null and b/resources/textures/background_distance_field.png differ diff --git a/resources/textures/background_mask.png b/resources/textures/background_mask.png new file mode 100644 index 0000000..11c2ac7 Binary files /dev/null and b/resources/textures/background_mask.png differ diff --git a/resources/textures/background_transparent.png b/resources/textures/background_transparent.png new file mode 100644 index 0000000..8a760e2 Binary files /dev/null and b/resources/textures/background_transparent.png differ diff --git a/src/distance_field_generator.c b/src/distance_field_generator.c new file mode 100644 index 0000000..3924851 --- /dev/null +++ b/src/distance_field_generator.c @@ -0,0 +1,124 @@ +#include +#include +#include +#include + +bool is_inside_shape(Color pixel) +{ + return pixel.r == 255 && pixel.g == 255 && pixel.b == 255; +} + +Image generate_distance_field(Image input_texture, int search_radius) +{ + int width = input_texture.width; + int height = input_texture.height; + + Image distance_field = GenImageColor(width, height, (Color){128, 128, 128, 255}); + Color *input_pixels = LoadImageColors(input_texture); + Color *output_pixels = LoadImageColors(distance_field); + + TraceLog(LOG_INFO, "Using %d threads for parallel processing", omp_get_max_threads()); + +#pragma omp parallel for schedule(dynamic, 64) + for (int y = 0; y < height; y++) + { + if (omp_get_thread_num() == 0 && y % (height / 10) == 0) + { + TraceLog(LOG_INFO, "Progress: %d%%", (y * 100) / height); + } + + for (int x = 0; x < width; x++) + { + int index = y * width + x; + bool is_inside = is_inside_shape(input_pixels[index]); + + float min_distance_sq = search_radius * search_radius; + bool found_boundary = false; + + for (int dy = -search_radius; dy <= search_radius; dy += 1) + { + for (int dx = -search_radius; dx <= search_radius; dx += 1) + { + int sample_x = x + dx; + int sample_y = y + dy; + + if (sample_x < 0 || sample_x >= width || sample_y < 0 || sample_y >= height) continue; + + int sample_index = sample_y * width + sample_x; + bool sample_is_inside = is_inside_shape(input_pixels[sample_index]); + + if (sample_is_inside != is_inside) + { + float distance_sq = dx * dx + dy * dy; + if (distance_sq < min_distance_sq) + { + min_distance_sq = distance_sq; + found_boundary = true; + } + } + } + } + + float distance = found_boundary ? sqrtf(min_distance_sq) : (float)search_radius; + float normalized_distance = distance / (float)search_radius; + + unsigned char distance_value; + if (is_inside) + { + distance_value = (unsigned char)(127.0f - 127.0f * normalized_distance); + } + else + { + distance_value = (unsigned char)(128.0f + 127.0f * normalized_distance); + } + + output_pixels[index] = (Color){distance_value, distance_value, distance_value, 255}; + } + } + + for (int i = 0; i < width * height; i++) + { + ImageDrawPixel(&distance_field, i % width, i / width, output_pixels[i]); + } + + UnloadImageColors(input_pixels); + UnloadImageColors(output_pixels); + + return distance_field; +} + +int main(int argc, char *argv[]) +{ + if (argc < 3) + { + TraceLog(LOG_ERROR, "Usage: %s [search_radius]", argv[0]); + return 1; + } + + const char *input_path = argv[1]; + const char *output_path = argv[2]; + int search_radius = (argc >= 4) ? atoi(argv[3]) : 100; + + Image input_image = LoadImage(input_path); + if (input_image.data == NULL) + { + TraceLog(LOG_ERROR, "Error: Failed to load input image '%s'", input_path); + return 1; + } + + Image distance_field_image = generate_distance_field(input_image, search_radius); + if (!ExportImage(distance_field_image, output_path)) + { + TraceLog(LOG_ERROR, "Failed to save output image '%s'", output_path); + UnloadImage(input_image); + UnloadImage(distance_field_image); + return 1; + } + + TraceLog(LOG_INFO, "Distance field saved to '%s'", output_path); + + UnloadImage(input_image); + UnloadImage(distance_field_image); + + return 0; +} \ No newline at end of file diff --git a/src/main.c b/src/main.c deleted file mode 100644 index bab43da..0000000 --- a/src/main.c +++ /dev/null @@ -1,92 +0,0 @@ -#include - -#define WIDTH 800 -#define HEIGHT 600 - -typedef struct -{ - Vector2 points[6]; - int numPoints; - float influence; -} Polygon; - -Polygon poly = { - {{WIDTH / 2 + 50, HEIGHT / 2 - 50}, - {WIDTH / 2 + 80, HEIGHT / 2}, - {WIDTH / 2 + 50, HEIGHT / 2 + 50}, - {WIDTH / 2 - 50, HEIGHT / 2 + 50}, - {WIDTH / 2 - 80, HEIGHT / 2}, - {WIDTH / 2 - 50, HEIGHT / 2 - 50}}, - 6, - 3000.0f}; - -int main(void) -{ - InitWindow(WIDTH, HEIGHT, "Wallpaper"); - SetWindowState(FLAG_WINDOW_RESIZABLE); - SetTargetFPS(60); - - // initialize shader - Shader shader = LoadShader("resources/shaders/basic.vs", "resources/shaders/basic.fs"); - - int resolutionLoc = GetShaderLocation(shader, "resolution"); - int mousePosLoc = GetShaderLocation(shader, "mouse_pos"); - int ballRadiusLoc = GetShaderLocation(shader, "ball_radius"); - int polygonPointsLoc = GetShaderLocation(shader, "polygon_points"); - int polygonInfluenceLoc = GetShaderLocation(shader, "polygon_influence"); - - Vector2 resolution = {WIDTH, HEIGHT}; - SetShaderValue(shader, resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); - - // send ball data - float ballRadius = 40.0f; - SetShaderValue(shader, ballRadiusLoc, &ballRadius, SHADER_UNIFORM_FLOAT); - - // send polygon data - float polygonInfluence = poly.influence; - SetShaderValue(shader, polygonInfluenceLoc, &polygonInfluence, SHADER_UNIFORM_FLOAT); - - float polygonPoints[12]; - for (int i = 0; i < poly.numPoints; i++) - { - polygonPoints[i * 2] = poly.points[i].x; - polygonPoints[i * 2 + 1] = poly.points[i].y; - } - SetShaderValueV(shader, polygonPointsLoc, polygonPoints, SHADER_UNIFORM_VEC2, poly.numPoints); - - // initializee render texture - RenderTexture2D target = LoadRenderTexture(WIDTH, HEIGHT); - - while (!WindowShouldClose()) - { - int screenWidth = GetScreenWidth(); - int screenHeight = GetScreenHeight(); - - if (IsWindowResized()) - { - UnloadRenderTexture(target); - target = LoadRenderTexture(screenWidth, screenHeight); - - Vector2 resolution = {screenWidth, screenHeight}; - SetShaderValue(shader, resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); - } - - Vector2 mousePos = GetMousePosition(); - mousePos.y = screenHeight - mousePos.y; - SetShaderValue(shader, mousePosLoc, &mousePos, SHADER_UNIFORM_VEC2); - - BeginDrawing(); - - BeginShaderMode(shader); - DrawTextureRec(target.texture, (Rectangle){0, 0, screenWidth, -screenHeight}, (Vector2){0, 0}, WHITE); - EndShaderMode(); - - EndDrawing(); - } - - UnloadRenderTexture(target); - UnloadShader(shader); - CloseWindow(); - - return 0; -} \ No newline at end of file diff --git a/src/wallpaper.c b/src/wallpaper.c new file mode 100644 index 0000000..92b141d --- /dev/null +++ b/src/wallpaper.c @@ -0,0 +1,91 @@ +#include +#include + +#define WIDTH 1920 +#define HEIGHT 1080 + +Texture2D load_texture_from_file(const char *file_path) +{ + Texture2D texture = LoadTexture(file_path); + if (texture.id == 0) + { + TraceLog(LOG_ERROR, "Failed to load texture from file: %s", file_path); + } + return texture; +} + +int main(void) +{ + InitWindow(WIDTH, HEIGHT, "wallpaper"); + SetWindowState(FLAG_WINDOW_RESIZABLE); + SetTargetFPS(60); + + // initialize textures + Texture2D distanceFieldTex = load_texture_from_file("resources/textures/background_distance_field.png"); + Texture2D backgroundTex = load_texture_from_file("resources/textures/background_transparent.png"); + RenderTexture2D target = LoadRenderTexture(WIDTH, HEIGHT); + + // initialize shader + Shader shader = LoadShader("resources/shaders/basic.vs", "resources/shaders/distance_field.fs"); + + int resolutionLoc = GetShaderLocation(shader, "resolution"); + int mousePosLoc = GetShaderLocation(shader, "mousePos"); + int ballRadiusLoc = GetShaderLocation(shader, "ballRadius"); + int shapeInfluenceLoc = GetShaderLocation(shader, "shapeInfluence"); + int distanceFieldTexLoc = GetShaderLocation(shader, "distanceFieldTex"); + + Vector2 resolution = {WIDTH, HEIGHT}; + SetShaderValue(shader, resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); + + float ballRadius = 100.0f; + SetShaderValue(shader, ballRadiusLoc, &ballRadius, SHADER_UNIFORM_FLOAT); + + float shapeInfluence = 200.0f; + SetShaderValue(shader, shapeInfluenceLoc, &shapeInfluence, SHADER_UNIFORM_FLOAT); + + int textureUnit = 1; + SetShaderValue(shader, distanceFieldTexLoc, &textureUnit, SHADER_UNIFORM_INT); + + while (!WindowShouldClose()) + { + int screenWidth = GetScreenWidth(); + int screenHeight = GetScreenHeight(); + + if (IsWindowResized()) + { + UnloadRenderTexture(target); + target = LoadRenderTexture(screenWidth, screenHeight); + + Vector2 resolution = {screenWidth, screenHeight}; + SetShaderValue(shader, resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); + } + + Vector2 mousePos = GetMousePosition(); + mousePos.y = screenHeight - mousePos.y; + SetShaderValue(shader, mousePosLoc, &mousePos, SHADER_UNIFORM_VEC2); + + BeginDrawing(); + + rlActiveTextureSlot(1); + rlEnableTexture(distanceFieldTex.id); + rlActiveTextureSlot(0); + + BeginShaderMode(shader); + DrawTextureRec(target.texture, (Rectangle){0, 0, screenWidth, -screenHeight}, (Vector2){0, 0}, WHITE); + EndShaderMode(); + + DrawTextureRec(backgroundTex, (Rectangle){0, 0, screenWidth, screenHeight}, (Vector2){0, 0}, WHITE); + + DrawText(TextFormat("Distance Field | x: %.0f, y: %.0f | frame: %.2f ms", mousePos.x, mousePos.y, GetFrameTime() * 1000), 10, 10, 20, WHITE); + + EndDrawing(); + } + + UnloadRenderTexture(target); + UnloadTexture(distanceFieldTex); + UnloadTexture(backgroundTex); + UnloadShader(shader); + CloseWindow(); + + return 0; +} \ No newline at end of file