From 8a30340202fd04e5c8362c0a0f03f1f5e11f122b Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Sun, 17 Nov 2024 16:13:37 -0700 Subject: [PATCH] Added test for rgb->hsv->rgb, and fixed function --- .gitignore | 2 + client/Makefile | 21 +++++-- client/include/hsv.h | 9 +++ client/src/hsv.c | 75 +++++++++++++++++++++++++ client/src/main.c | 131 ++++++++++++------------------------------- client/test/hsv.c | 70 +++++++++++++++++++++++ 6 files changed, 208 insertions(+), 100 deletions(-) create mode 100644 client/include/hsv.h create mode 100644 client/src/hsv.c create mode 100644 client/test/hsv.c diff --git a/.gitignore b/.gitignore index 8c09441..154d693 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ roleplay *.spv .DS_Store + +client/test/test_* diff --git a/client/Makefile b/client/Makefile index b00340f..29ea8db 100644 --- a/client/Makefile +++ b/client/Makefile @@ -2,8 +2,12 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) CFLAGS = -I $(ROOT_DIR)/include -I/usr/local/include -O0 -g -Wall -Wextra LDFLAGS = -lfreetype -lz -lglfw -lvulkan -ldl -Xlinker -rpath -Xlinker /opt/homebrew/lib -SOURCES = src/main.c src/draw.c src/ui.c src/gpu.c src/hex.c lib/spng.c lib/vma.cpp -OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) +SOURCES = src/draw.c src/ui.c src/gpu.c src/hex.c src/hsv.h lib/spng.c lib/vma.cpp +APP_SOURCES = src/main.c $(SOURCES) +TEST_SOURCES = test/hsv.c $(SOURCES) +APP_OBJECTS = $(addsuffix .o, $(basename $(APP_SOURCES))) +TEST_OBJECTS = $(addsuffix .o, $(basename $(TEST_SOURCES))) + VERT_SPV = $(addsuffix .vert.spv, $(basename $(wildcard shader/*.vert))) FRAG_SPV = $(addsuffix .frag.spv, $(basename $(wildcard shader/*.frag))) COMP_SPV = $(addsuffix .comp.spv, $(basename $(wildcard shader/*.comp))) @@ -29,9 +33,17 @@ CXX ?= clang++ .PHONY: all all: roleplay $(SPV_FILES) -roleplay: $(OBJECTS) +roleplay: $(APP_OBJECTS) + $(CXX) $(CFLAGS) $(LDFLAGS) -o $@ $^ + +test/test_hsv: $(TEST_OBJECTS) $(CXX) $(CFLAGS) $(LDFLAGS) -o $@ $^ +.PHONY: test +test: test/test_hsv + ./test/test_hsv + + %.o: %.cpp $(CXX) $(CFLAGS) -std=c++17 -Wno-nullability-completeness -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unused-private-field -Wno-unused-variable -c -o $@ $< @@ -42,7 +54,8 @@ roleplay: $(OBJECTS) clean: rm -f $(SPV_FILES) - rm -f $(OBJECTS) + rm -f $(APP_OBJECTS) + rm -f $(TEST_OBJECTS) rm -f roleplay rm -rf $(EXTRA_DEBUG_REQUIREMENTS) diff --git a/client/include/hsv.h b/client/include/hsv.h new file mode 100644 index 0000000..6f32a47 --- /dev/null +++ b/client/include/hsv.h @@ -0,0 +1,9 @@ +#ifndef HSV_H +#define HSV_H + +#include "cglm/types.h" + +void hsv_to_rgb(double hsv[3], vec3 rgb); +void rgb_to_hsv(vec3 rgb, double hsv[3]); + +#endif diff --git a/client/src/hsv.c b/client/src/hsv.c new file mode 100644 index 0000000..a5d5fc7 --- /dev/null +++ b/client/src/hsv.c @@ -0,0 +1,75 @@ +#include "hsv.h" +#include "math.h" + +#define max(a, b) ((a > b) ? a : b) +#define min(a, b) ((a < b) ? a : b) + +float floatmod(float a, float b) { + return a - b*floor(a/b); +} + +void rgb_to_hsv(vec3 rgb, double hsv[3]) { + if(rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0) { + hsv[0] = 0; + hsv[1] = 0; + hsv[2] = 0; + return; + } + + hsv[2] = max(rgb[0], max(rgb[1], rgb[2])); + float M = min(rgb[0], min(rgb[1], rgb[2])); + float C = hsv[2] - M; + hsv[1] = C / hsv[2]; + float X; + + if(hsv[2] == rgb[0] && M == rgb[2]) { + X = rgb[1] - M; + hsv[0] = (X/C + 0)/6; + } else if(hsv[2] == rgb[1] && M == rgb[2]) { + X = rgb[0] - M; + hsv[0] = (2 - X/C)/6; + } else if(hsv[2] == rgb[1] && M == rgb[0]) { + X = rgb[2] - M; + hsv[0] = (X/C + 2)/6; + } else if(hsv[2] == rgb[2] && M == rgb[0]) { + X = rgb[1] - M; + hsv[0] = (4 - X/C)/6; + } else if(hsv[2] == rgb[2] && M == rgb[1]) { + X = rgb[0] - M; + hsv[0] = (X/C + 4)/6; + } else if(hsv[2] == rgb[0] && M == rgb[1]) { + X = rgb[2] - M; + hsv[0] = (6 - X/C)/6; + } +} + +void hsv_to_rgb(double hsv[3], vec3 rgb) { + float C = hsv[1] * hsv[2]; + float H = hsv[0]*6; + float X = C * (1 - fabs(floatmod(H, 2) - 1)); + vec4 temp = {0, 0, 0, 0}; + if(0 <= H && H <= 1) { + temp[0] = C; + temp[1] = X; + } else if(1 <= H && H <= 2) { + temp[0] = X; + temp[1] = C; + } else if(2 <= H && H <= 3) { + temp[1] = C; + temp[2] = X; + } else if(3 <= H && H <= 4) { + temp[1] = X; + temp[2] = C; + } else if(4 <= H && H <= 5) { + temp[0] = X; + temp[2] = C; + } else if(5 <= H && H <= 6) { + temp[0] = C; + temp[2] = X; + } + + float m = hsv[2] - C; + rgb[0] = temp[0] + m; + rgb[1] = temp[1] + m; + rgb[2] = temp[2] + m; +} diff --git a/client/src/main.c b/client/src/main.c index cdbaf4b..fee9e9d 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -6,6 +6,7 @@ #include "gpu.h" #include "draw.h" #include "hex.h" +#include "hsv.h" #include "arpa/inet.h" #include "vk_mem_alloc.h" @@ -93,55 +94,21 @@ uint32_t add_hex_region(ClientContext* context) { #define COLOR_PICK_CONTAINER_ID 0x03 typedef struct ColorUIDataStruct { - vec3 current; + double current_hsv[3]; + vec4 current; vec3 saved[12]; - char string[8]; + char string[10]; int string_len; } ColorUIData; -float floatmod(float a, float b) { - return a - b*floor(a/b); -} - -void hsv_to_rgb(vec3 hsv, vec3 rgb) { - float C = hsv[1] * hsv[2]; - float H = hsv[0]*6; - float X = C * (1 - fabs(floatmod(H, 2) - 1)); - vec4 temp = {0, 0, 0, 0}; - if(0 <= H && H <= 1) { - temp[0] = C; - temp[1] = X; - } else if(1 <= H && H <= 2) { - temp[0] = X; - temp[1] = C; - } else if(2 <= H && H <= 3) { - temp[1] = C; - temp[2] = X; - } else if(3 <= H && H <= 4) { - temp[1] = X; - temp[2] = C; - } else if(4 <= H && H <= 5) { - temp[0] = X; - temp[2] = C; - } else if(5 <= H && H <= 6) { - temp[0] = C; - temp[2] = X; - } - - float m = hsv[2] - C; - rgb[0] = temp[0] + m; - rgb[1] = temp[1] + m; - rgb[2] = temp[2] + m; -} - -void rgb_string_set(UIContext* ui, RenderContext* gpu, ColorUIData* data, vec3 hsv) { - vec3 rgb; - hsv_to_rgb(hsv, rgb); - snprintf(data->string, 8, "#%02X%02X%02X", - (uint)(rgb[0]*255), - (uint)(rgb[1]*255), - (uint)(rgb[2]*255)); - data->string_len = 6; +void set_rgb_from_hsv(UIContext* ui, RenderContext* gpu, ColorUIData* data, double hsv[3]) { + hsv_to_rgb(hsv, data->current); + snprintf(data->string, 10, "#%02X%02X%02X%02X", + (uint)(data->current[0]*255), + (uint)(data->current[1]*255), + (uint)(data->current[2]*255), + (uint)(data->current[3]*255)); + data->string_len = 8; update_ui_string(data->string, COLOR_PICK_CONTAINER_ID, 0, 0, ui, gpu); } @@ -152,8 +119,8 @@ void sv_square_pick(UIContext* ui, RenderContext* gpu, ColorUIData* data, float if(v < 0) v = 0; if(v > 1) v = 1; - data->current[1] = s; - data->current[2] = 1-v; + data->current_hsv[1] = s; + data->current_hsv[2] = 1-v; Container* container = context_container(COLOR_PICK_CONTAINER_ID, ui); Layer* layer = &container->layers[0]; @@ -199,7 +166,7 @@ void sv_square_pick(UIContext* ui, RenderContext* gpu, ColorUIData* data, float 4*sizeof(vec4), gpu); - rgb_string_set(ui, gpu, data, select->color[0]); + set_rgb_from_hsv(ui, gpu, data, data->current_hsv); } void sv_square_button_callback(void* data, UIContext* ui, RenderContext* gpu, float x, float y, int button, int action, int mods) { @@ -226,7 +193,7 @@ void hue_bar_set(UIContext* ui, RenderContext* gpu, ColorUIData* data, float y) if(y < 0) y = 0; if(y > 1) y = 1; - data->current[0] = y; + data->current_hsv[0] = y; Container* container = context_container(COLOR_PICK_CONTAINER_ID, ui); Layer* layer = &container->layers[0]; @@ -267,7 +234,7 @@ void hue_bar_set(UIContext* ui, RenderContext* gpu, ColorUIData* data, float y) 1*sizeof(float), gpu); - rgb_string_set(ui, gpu, data, sv_select->color[0]); + set_rgb_from_hsv(ui, gpu, data, data->current_hsv); } void hue_bar_scroll_callback(void* data, UIContext* ui, RenderContext* gpu, double x, double y) { @@ -330,57 +297,22 @@ void hex_string_text_callback(void* ptr, UIContext* ui, RenderContext* gpu, unsi return; } - if(data->string_len < 6) { + if(data->string_len < 8) { data->string_len += 1; data->string[data->string_len] = codepoint; update_ui_string(data->string, COLOR_PICK_CONTAINER_ID, 0, 0, ui, gpu); } } -void rgb_to_hsv(vec3 rgb, vec3 hsv) { - hsv[2] = max(rgb[0], max(rgb[1], rgb[2])); - float M = min(rgb[0], min(rgb[1], rgb[2])); - float C = hsv[2] - M; - hsv[1] = C / hsv[2]; - float X; - - if(hsv[2] == rgb[0] && M == rgb[2]) { - X = rgb[1] - M; - hsv[0] = (X/C + 0)/6; - // H = [0, 1] - } else if(hsv[2] == rgb[1] && M == rgb[2]) { - X = 1 - rgb[0] - M; - hsv[0] = (X/C + 1)/6; - // H = [1, 2] - } else if(hsv[2] == rgb[1] && M == rgb[0]) { - X = rgb[2] - M; - hsv[0] = (X/C + 2)/6; - // H = [2, 3] - } else if(hsv[2] == rgb[2] && M == rgb[0]) { - X = 1 - rgb[1] - M; - hsv[0] = (X/C + 3)/6; - // H = [3, 4] - } else if(hsv[2] == rgb[2] && M == rgb[1]) { - X = rgb[0] - M; - hsv[0] = (X/C + 4)/6; - // H = [4, 5] - } else if(hsv[2] == rgb[0] && M == rgb[1]) { - X = 1 - rgb[2] - M; - hsv[0] = (X/C + 5)/6; - // H = [5, 6] - } -} - bool hex_string_key_callback(void* ptr, UIContext* ui, RenderContext* gpu, int key, int action, int mods) { (void)mods; ColorUIData* data = ptr; char tmp[3]; - vec3 rgb; if(action == GLFW_PRESS) { switch(key) { case GLFW_KEY_ESCAPE: - rgb_string_set(ui, gpu, data, data->current); + set_rgb_from_hsv(ui, gpu, data, data->current_hsv); clear_active_element(ui, gpu); break; case GLFW_KEY_ENTER: @@ -388,22 +320,26 @@ bool hex_string_key_callback(void* ptr, UIContext* ui, RenderContext* gpu, int k tmp[0] = data->string[1]; tmp[1] = data->string[2]; tmp[2] = 0; - rgb[0] = strtol(tmp, NULL, 16)/255.0; + data->current[0] = strtol(tmp, NULL, 16)/255.0; tmp[0] = data->string[3]; tmp[1] = data->string[4]; tmp[2] = 0; - rgb[1] = strtol(tmp, NULL, 16)/255.0; + data->current[1] = strtol(tmp, NULL, 16)/255.0; tmp[0] = data->string[5]; tmp[1] = data->string[6]; tmp[2] = 0; - rgb[2] = strtol(tmp, NULL, 16)/255.0; + data->current[2] = strtol(tmp, NULL, 16)/255.0; - rgb_to_hsv(rgb, data->current); + tmp[0] = data->string[7]; + tmp[1] = data->string[8]; + tmp[2] = 0; + data->current[3] = strtol(tmp, NULL, 16)/255.0; - hue_bar_set(ui, gpu, data, data->current[0]); - sv_square_pick(ui, gpu, data, data->current[1], 1-data->current[2]); + rgb_to_hsv(data->current, data->current_hsv); + hue_bar_set(ui, gpu, data, data->current_hsv[0]); + sv_square_pick(ui, gpu, data, data->current_hsv[1], 1-data->current_hsv[2]); clear_active_element(ui, gpu); break; @@ -443,7 +379,7 @@ VkResult color_ui(ClientContext* context) { .color = {1, 1, 1, 1}, .size = 16, .offset = 0, - .length = 7, + .length = 9, .font = 0, }, }; @@ -565,7 +501,7 @@ VkResult color_ui(ClientContext* context) { }; ColorUIData* data = malloc(sizeof(ColorUIData)); - data->string_len = 6; + data->string_len = 8; data->string[0] = '#'; data->string[1] = '0'; data->string[2] = '0'; @@ -573,10 +509,13 @@ VkResult color_ui(ClientContext* context) { data->string[4] = '0'; data->string[5] = '0'; data->string[6] = '0'; - data->string[7] = '\0'; + data->string[7] = '0'; + data->string[8] = '0'; + data->string[9] = '\0'; data->current[0] = 0; data->current[1] = 0; data->current[2] = 0; + data->current[3] = 0; UICallbacks callbacks[] = { { diff --git a/client/test/hsv.c b/client/test/hsv.c new file mode 100644 index 0000000..7c08740 --- /dev/null +++ b/client/test/hsv.c @@ -0,0 +1,70 @@ +#include "hsv.h" +#include +#include +#include + +#define FAIL -1 +#define OK 0 + +int test_conv(int r, int g, int b, double tolerance) { + vec3 rgb = {r/255.0, g/255.0, b/255.0}; + double hsv[3]; + vec3 out; + int test[3]; + rgb_to_hsv(rgb, hsv); + hsv_to_rgb(hsv, out); + test[0] = out[0]*255.0; + test[1] = out[1]*255.0; + test[2] = out[2]*255.0; + + if(fabs(rgb[0] - out[0]) > tolerance || fabs(rgb[1] - out[1]) > tolerance || fabs(rgb[2] - out[2]) > tolerance) { + fprintf(stderr, "FAIL:\t#%02X%02X%02X -> #%02X%02X%02X\nIN:\t(%.12f %.12f %.12f)\nOUT:\t(%.12f %.12f %.12f)\nDIF:\t(%.12f %.12f %.12f)\n", + r, g, b, + test[0], test[1], test[2], + rgb[0], rgb[1], rgb[2], + out[0], out[1], out[2], + rgb[0]-out[0], rgb[1]-out[1], rgb[2]-out[2]); + return FAIL; + } else { + return OK; + } +} + +int main() { + for(int r = 0; r < 256; r++) { + for(int g = 0; g < 256; g++) { + for(int b = 0; b < 256; b++) { + if(test_conv(r, g, b, 0.001) != OK) { + return -1; + } + } + } + } +} + +// To RGB steps: +// 1. C = V * S <- maps to 3 in inv +// 2. H' = H * 6 <- maps to 7 in inv +// 3. X = C * (1 - abs(H' - 2*floor(H'/2) - 1)) +// 4. M = V - C <- maps to 4 in inv +// 5. R = V, G = X + M, B = M + +// To HSV steps: +// 1. V = max(R, G, B) <- maps to 5,R in inv +// 2. M = min(R, G, B) <- maps to 5,B in inv +// 3. S = C / V <- maps to 1 in inv +// 4. C = V - M <- maps to 4 in inv +// 5. X = G - M +// 6. H' = X/C +// 7. H = H'/6 <- maps to 2 in inv +// +// So out of the two algos, the only step that doest match exactly so far is +// RGB: X = C * (1 - abs(H' - 2*floor(H'/2) - 1)) +// HSV: X = G - M, H' = X/C +// +// After floor(): +// RGB: X = C * (1 - abs(H' - 1)) +// +// After abs() assuming H' = [0, 1]: +// RGB: X = C * (1 - (1 - H')) +// RGB: X = C * H'