working but missing a bunch of features and slightly buggy

This commit is contained in:
Gwendolyn 2023-05-21 11:35:46 +02:00
parent 091005f8b8
commit dee767a487
33 changed files with 7337 additions and 185 deletions

1
.clang-format Normal file
View file

@ -0,0 +1 @@
ColumnLimit: 0

1
.envrc Normal file
View file

@ -0,0 +1 @@
eval "$(lorri direnv)"

View file

@ -1,11 +1,137 @@
cmake_minimum_required(VERSION 3.24)
cmake_minimum_required(VERSION 3.25)
project(shader-automaton)
set(CMAKE_CXX_STANDARD 23)
set(VERBOSE yes)
find_package(OpenGL REQUIRED)
find_package(GLEW REQUIRED)
find_package(glfw3 3.3 REQUIRED)
include(FetchContent)
include(EmbedResources.cmake)
FetchContent_Declare(
perlin
GIT_REPOSITORY https://github.com/Reputeless/PerlinNoise.git
GIT_TAG bdf39fe92b2a585cdef485bcec2bca8ab5614095
)
FetchContent_Declare(
imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
GIT_TAG 70cca1eac07aa3809bdd3717253c3754a5b7cfcc
)
FetchContent_Declare(
gtest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG bc860af08783b8113005ca7697da5f5d49a8056f
)
FetchContent_Declare(
dejavufonts
URL https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2
URL_HASH SHA1=7fa15e7b9676fc3915338c41e76ad454c344fff5
)
FetchContent_MakeAvailable(perlin)
FetchContent_MakeAvailable(imgui)
add_executable(shader-automaton main.cpp)
target_link_libraries(shader-automaton PRIVATE OpenGL::GL GLEW::GLEW glfw)
add_executable(shader-automaton
src/main.cpp
src/options.cpp
src/colormaps/viridis.cpp
src/rule_presets.cpp
src/argparser.h
src/rule_presets.h
src/options.h
src/shader-helper.cpp
src/shader-helper.h
src/color.cpp
src/color.h
src/colormaps/colormaps.cpp
src/colormaps/dualcolor.cpp
src/colormaps/viridis.cpp
src/colormaps/rainbow.cpp
src/program.cpp
src/program.h
)
target_compile_definitions(shader-automaton PRIVATE PROGRAM_NAME="${CMAKE_PROJECT_NAME}" PROGRAM_VERSION="${CMAKE_PROJECT_VERSION}")
function(preprocess_shader name)
add_custom_command(
OUTPUT "preprocessed-shaders/${name}"
COMMAND ./preprocess-shader.sh "${name}"
DEPENDS "shaders/${name}"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
PRE_BUILD
VERBATIM
)
endfunction()
preprocess_shader("automaton.comp")
preprocess_shader("automaton.vert")
preprocess_shader("automaton.frag")
# TODO: this whole shader preprocessing and embedding is just broken
# it happens half at configure time rather than build time so the preprocessed files are outdated
# TODO: maybe rewrite this tool to put multiple namespaces into one target
# and to maybe define the contents separately from the target, if possible
add_embedded_binary_resources(
shader-automaton-shaders
OUT_DIR embedded-resources
HEADER shaders.h
NAMESPACE resources::shaders
RESOURCE_NAMES automaton_comp automaton_vert automaton_frag
RESOURCES
"${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.comp"
"${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.vert"
"${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.frag"
)
FetchContent_MakeAvailable(dejavufonts)
add_embedded_binary_resources(
shader-automaton-fonts
OUT_DIR embedded-resources
HEADER fonts.h
NAMESPACE resources::fonts
RESOURCE_NAMES DejaVuSansMono
RESOURCES
"${dejavufonts_SOURCE_DIR}/ttf/DejaVuSansMono.ttf"
)
target_link_libraries(shader-automaton OpenGL::GL GLEW::GLEW glfw shader-automaton-shaders shader-automaton-fonts)
file(COPY_FILE "${perlin_SOURCE_DIR}/PerlinNoise.hpp" "${perlin_BINARY_DIR}/PerlinNoise.hpp")
target_include_directories(shader-automaton PRIVATE ${perlin_BINARY_DIR})
target_include_directories(shader-automaton PRIVATE "${imgui_SOURCE_DIR}/backends" "${imgui_SOURCE_DIR}")
target_sources(shader-automaton PRIVATE
${imgui_SOURCE_DIR}/imgui.cpp
${imgui_SOURCE_DIR}/imgui_demo.cpp
${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp
${imgui_SOURCE_DIR}/imgui_widgets.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp
)
enable_testing()
FetchContent_MakeAvailable(gtest)
include(GoogleTest)
add_executable(ColorTest tests/color.cpp src/color.cpp)
target_link_libraries(ColorTest GTest::gtest_main)
target_include_directories(ColorTest PRIVATE src)
gtest_discover_tests(ColorTest)

336
EmbedResources.cmake Normal file
View file

@ -0,0 +1,336 @@
# Module for embedding resources in binaries
find_program(HEXDUMP_COMMAND NAMES xxd)
#[==[
Adds an object library target containing embedded binary resources and
generates a header file, where you can access them as `span<byte const>`.
To use with older C++ standards, you can override both the `span` template
and the `byte` type to use with e.g. span-lite, Microsoft.GSL or range-v3
(don't forget to link your span library of choice to the generated target).
Usage:
add_embedded_binary_resources(
name # Name of the created target
OUT_DIR <dir> # Directory where the header and sources will be created
# (relative to the build directory)
HEADER <header> # Name of the generated header
NAMESPACE [namespace] # Namespace of created symbols (optional)
RESOURCE_NAMES <names [...]> # Names (symbols) of the resources
RESOURCES <resources [...]> # Resource files
SPAN_TEMPLATE [template name] # Name of the `span` template, default `std::span`
SPAN_HEADER [header] # Header with the `span` template, default `<span>`
BYTE_TYPE [type name] # Name of the `byte` type, default `std::byte`
BYTE_HEADER [header] # Header with the `byte` type, default `<cstddef>`
)
]==]
function(add_embedded_binary_resources NAME)
set(OPTIONS "")
set(ONE_VALUE_ARGS OUT_DIR HEADER NAMESPACE SPAN_TEMPLATE SPAN_HEADER BYTE_TYPE BYTE_HEADER)
set(MULTI_VALUE_ARGS RESOURCE_NAMES RESOURCES)
cmake_parse_arguments(ARGS "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN})
if(NOT HEXDUMP_COMMAND)
message(FATAL_ERROR "Cannot embed resources - xxd not found.")
endif()
set(FULL_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${ARGS_HEADER}")
if(NOT DEFINED ARGS_SPAN_TEMPLATE)
set(ARGS_SPAN_TEMPLATE "std::span")
endif()
if(NOT DEFINED ARGS_SPAN_HEADER)
set(ARGS_SPAN_HEADER "<span>")
endif()
if(NOT DEFINED ARGS_BYTE_TYPE)
set(ARGS_BYTE_TYPE "std::byte")
endif()
if(NOT DEFINED ARGS_BYTE_HEADER)
set(ARGS_BYTE_HEADER "<cstddef>")
endif()
add_library("${NAME}" OBJECT)
target_include_directories("${NAME}" PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
if(ARGS_SPAN_HEADER STREQUAL "<span>")
target_compile_features("${NAME}" PUBLIC cxx_std_20)
elseif(ARGS_BYTE_HEADER STREQUAL "<cstddef>")
target_compile_features("${NAME}" PUBLIC cxx_std_17)
endif()
# fPIC not added automatically to object libraries due to defect in CMake
set_target_properties(
"${NAME}"
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
file(
WRITE "${FULL_HEADER_PATH}"
"#pragma once\n"
"\n"
"#include ${ARGS_BYTE_HEADER}\n"
"#include ${ARGS_SPAN_HEADER}\n"
"\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_HEADER_PATH}"
"namespace ${ARGS_NAMESPACE}\n"
"{\n"
"\n"
)
endif()
foreach(RESOURCE_NAME RESOURCE IN ZIP_LISTS ARGS_RESOURCE_NAMES ARGS_RESOURCES)
set(FULL_RESOURCE_UNIT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.cpp")
set(FULL_RESOURCE_HEX_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.inc")
# Add symbol to header
file(
APPEND "${FULL_HEADER_PATH}"
"[[nodiscard]] ${ARGS_SPAN_TEMPLATE}<${ARGS_BYTE_TYPE} const>\n"
"${RESOURCE_NAME}() noexcept;\n"
"\n"
)
# Write .cpp
file(
WRITE "${FULL_RESOURCE_UNIT_PATH}"
"#include \"${ARGS_HEADER}\"\n"
"\n"
"#include <cstdint>\n"
"\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"namespace ${ARGS_NAMESPACE}\n"
"{\n"
"\n"
)
endif()
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"namespace\n"
"{\n"
"\n"
"std::uint8_t const ${RESOURCE_NAME}_data[] = {\n"
"#include \"${RESOURCE_NAME}.inc\"\n"
"};\n"
"\n"
"} // namespace\n"
"\n"
"${ARGS_SPAN_TEMPLATE}<${ARGS_BYTE_TYPE} const>\n"
"${RESOURCE_NAME}() noexcept\n"
"{\n"
" return as_bytes(${ARGS_SPAN_TEMPLATE}<std::uint8_t const>{${RESOURCE_NAME}_data, sizeof(${RESOURCE_NAME}_data)});\n"
"}\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"\n"
"} // namespace ${ARGS_NAMESPACE}\n"
)
endif()
target_sources("${NAME}" PRIVATE "${FULL_RESOURCE_UNIT_PATH}")
add_custom_command(
OUTPUT "${FULL_RESOURCE_HEX_PATH}"
COMMAND "${HEXDUMP_COMMAND}" -i < "${RESOURCE}" > "${FULL_RESOURCE_HEX_PATH}"
MAIN_DEPENDENCY "${RESOURCE}"
)
list(APPEND RESOURCES_HEX_FILES "${FULL_RESOURCE_HEX_PATH}")
endforeach()
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_HEADER_PATH}"
"} // namespace ${ARGS_NAMESPACE}\n"
)
endif()
target_sources("${NAME}" PUBLIC "${FULL_HEADER_PATH}")
add_custom_target("${NAME}_hexdump" DEPENDS "${RESOURCES_HEX_FILES}")
add_dependencies("${NAME}" "${NAME}_hexdump")
endfunction()
#[==[
Adds an object library target containing embedded text resources and
generates a header file, where you can access them as `string_view`.
To use with older C++ standards, you can override the `string_view` type
to use with e.g. string-view-lite (don't forget to link your string view
library of choice to the generated target).
Usage:
add_embedded_text_resources(
name # Name of the created target
OUT_DIR <dir> # Directory where the header and sources will be created
# (relative to the build directory)
HEADER <header> # Name of the generated header
NAMESPACE [namespace] # Namespace of created symbols (optional)
RESOURCE_NAMES <names [...]> # Names (symbols) of the resources
RESOURCES <resources [...]> # Resource files
STRING_VIEW_TYPE [type name] # Name of the `string_view` type, default `std::string_view`
STRING_VIEW_HEADER [header] # Header with the `string_view` type, default `<string_view>`
)
]==]
function(add_embedded_text_resources NAME)
set(OPTIONS "")
set(ONE_VALUE_ARGS OUT_DIR HEADER NAMESPACE STRING_VIEW_TYPE STRING_VIEW_HEADER)
set(MULTI_VALUE_ARGS RESOURCE_NAMES RESOURCES)
cmake_parse_arguments(ARGS "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN})
if(NOT HEXDUMP_COMMAND)
message(FATAL_ERROR "Cannot embed resources - xxd not found.")
endif()
set(FULL_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${ARGS_HEADER}")
if(NOT DEFINED ARGS_STRING_VIEW_TYPE)
set(ARGS_STRING_VIEW_TYPE "std::string_view")
endif()
if(NOT DEFINED ARGS_STRING_VIEW_HEADER)
set(ARGS_STRING_VIEW_HEADER "<string_view>")
endif()
add_library("${NAME}" OBJECT)
target_include_directories("${NAME}" PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
if(ARGS_STRING_VIEW_HEADER STREQUAL "<string_view>")
target_compile_features("${NAME}" PUBLIC cxx_std_17)
endif()
# fPIC not added automatically to object libraries due to defect in CMake
set_target_properties(
"${NAME}"
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
file(
WRITE "${FULL_HEADER_PATH}"
"#pragma once\n"
"\n"
"#include ${ARGS_STRING_VIEW_HEADER}\n"
"\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_HEADER_PATH}"
"namespace ${ARGS_NAMESPACE}\n"
"{\n"
"\n"
)
endif()
foreach(RESOURCE_NAME RESOURCE IN ZIP_LISTS ARGS_RESOURCE_NAMES ARGS_RESOURCES)
set(FULL_RESOURCE_UNIT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.cpp")
set(FULL_RESOURCE_HEX_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.inc")
# Add symbol to header
file(
APPEND "${FULL_HEADER_PATH}"
"[[nodiscard]] ${ARGS_STRING_VIEW_TYPE}\n"
"${RESOURCE_NAME}() noexcept;\n"
"\n"
)
# Write .cpp
file(
WRITE "${FULL_RESOURCE_UNIT_PATH}"
"#include \"${ARGS_HEADER}\"\n"
"\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"namespace ${ARGS_NAMESPACE}\n"
"{\n"
"\n"
)
endif()
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"namespace\n"
"{\n"
"\n"
"char const ${RESOURCE_NAME}_data[] = {\n"
"#include \"${RESOURCE_NAME}.inc\"\n"
"};\n"
"\n"
"} // namespace\n"
"\n"
"${ARGS_STRING_VIEW_TYPE}\n"
"${RESOURCE_NAME}() noexcept\n"
"{\n"
" return ${ARGS_STRING_VIEW_TYPE}{${RESOURCE_NAME}_data, sizeof(${RESOURCE_NAME}_data)};\n"
"}\n"
)
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_RESOURCE_UNIT_PATH}"
"\n"
"} // namespace ${ARGS_NAMESPACE}\n"
)
endif()
target_sources("${NAME}" PRIVATE "${FULL_RESOURCE_UNIT_PATH}")
add_custom_command(
OUTPUT "${FULL_RESOURCE_HEX_PATH}"
COMMAND "${HEXDUMP_COMMAND}" -i < "${RESOURCE}" > "${FULL_RESOURCE_HEX_PATH}"
MAIN_DEPENDENCY "${RESOURCE}"
)
list(APPEND RESOURCES_HEX_FILES "${FULL_RESOURCE_HEX_PATH}")
endforeach()
if(DEFINED ARGS_NAMESPACE)
file(
APPEND "${FULL_HEADER_PATH}"
"} // namespace ${ARGS_NAMESPACE}\n"
)
endif()
target_sources("${NAME}" PUBLIC "${FULL_HEADER_PATH}")
add_custom_target("${NAME}_hexdump" DEPENDS "${RESOURCES_HEX_FILES}")
add_dependencies("${NAME}" "${NAME}_hexdump")
endfunction()

BIN
main

Binary file not shown.

182
main.cpp
View file

@ -1,182 +0,0 @@
#include <cstdio>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <cstdlib>
#include <cstring>
#include <vector>
GLuint loadShader(const char *source, GLenum shader_type) {
GLuint shaderId = glCreateShader(shader_type);
GLint result = GL_FALSE;
int infoLogLength;
// Compile Vertex Shader
glShaderSource(shaderId, 1, &source, nullptr);
glCompileShader(shaderId);
glGetShaderiv(shaderId, GL_COMPILE_STATUS, &result);
glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0) {
std::vector<char> VertexShaderErrorMessage(infoLogLength + 1);
glGetShaderInfoLog(shaderId, infoLogLength, nullptr,
&VertexShaderErrorMessage[0]);
printf("%s\n", &VertexShaderErrorMessage[0]);
exit(-1);
}
return shaderId;
}
GLuint loadProgram(const std::vector<GLuint> &shaders) {
GLuint programId = glCreateProgram();
for (const auto &item : shaders) {
glAttachShader(programId, item);
}
glLinkProgram(programId);
GLint result = GL_FALSE;
int infoLogLength;
glGetProgramiv(programId, GL_LINK_STATUS, &result);
glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0) {
std::vector<char> ProgramErrorMessage(infoLogLength + 1);
glGetProgramInfoLog(programId, infoLogLength, nullptr,
&ProgramErrorMessage[0]);
printf("%s\n", &ProgramErrorMessage[0]);
}
return programId;
}
static const char *vertex_shader =
"#version 460\n"
"layout(location = 0) in vec3 vertexPosition_modelspace;\n"
"out vec2 coord;\n"
"void main(){\n"
" gl_Position.xyz = vertexPosition_modelspace;\n"
" gl_Position.w = 1.0;\n"
" coord.xy = vertexPosition_modelspace.xy;\n"
"}";
static const char *fragment_shader =
"#version 460\n"
"out vec3 color;\n"
"in vec2 coord;\n"
"layout (std430, binding = 2) restrict readonly buffer shader_data\n"
"{\n"
" int c[];\n"
"};\n"
"void main(){\n"
" int x = int(255. * (coord.x + 1.) / 2.);"
" color = vec3(c[x]/255., 0.5,0.5);\n"
"}";
static const char *compute_shader =
"#version 460\n"
"layout(local_size_x = 1, local_size_y = 1) in;\n"
"layout (std430, binding = 2) restrict buffer shader_data\n"
"{\n"
" int c[];\n"
"};\n"
"void main(){\n"
" c[gl_GlobalInvocationID.x] = (c[gl_GlobalInvocationID.x] + 1) % 256;\n"
"}\n";
int main() {
glewExperimental = true;
if (!glfwInit()) {
fprintf(stderr, "Failed to initialize GLFW\n");
return -1;
}
glfwWindowHint(GLFW_SAMPLES, 0);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window =
glfwCreateWindow(1024, 768, "Tutorial 01", nullptr, nullptr);
if (window == nullptr) {
fprintf(stderr,
"Failed to open GLFW window. If you have an Intel GPU, they are "
"not 3.3 compatible. Try the 2.1 version of the tutorials.\n");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // Initialize GLEW
glewExperimental = true; // Needed in core profile
if (glewInit() != GLEW_OK) {
fprintf(stderr, "Failed to initialize GLEW\n");
return -1;
}
glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);
const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
};
GLuint vertexbuffer;
glGenBuffers(1, &vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data),
g_vertex_buffer_data, GL_STATIC_DRAW);
GLuint programId = loadProgram({
loadShader(vertex_shader, GL_VERTEX_SHADER),
loadShader(fragment_shader, GL_FRAGMENT_SHADER),
});
GLuint computeProgramId = loadProgram({
loadShader(compute_shader, GL_COMPUTE_SHADER),
});
int c[256];
for (size_t i = 0; i < 256; ++i) {
c[i] = i;
}
GLuint ssbo = 0;
glGenBuffers(1, &ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int) * 256, &c,
GL_DYNAMIC_COPY);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
unsigned int block_index = glGetProgramResourceIndex(
programId, GL_SHADER_STORAGE_BLOCK, "shader_data");
GLuint ssbo_binding_point_index = 2;
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ssbo_binding_point_index, ssbo);
glShaderStorageBlockBinding(programId, block_index, ssbo_binding_point_index);
glUseProgram(computeProgramId);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ssbo_binding_point_index, ssbo);
unsigned int compute_block_index = glGetProgramResourceIndex(
programId, GL_SHADER_STORAGE_BLOCK, "shader_data");
glShaderStorageBlockBinding(computeProgramId, compute_block_index,
ssbo_binding_point_index);
do {
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(computeProgramId);
glDispatchCompute(255, 1, 1);
glUseProgram(programId);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);
glfwSwapBuffers(window);
glfwPollEvents();
} // Check if the ESC key was pressed or the window was closed
while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&
glfwWindowShouldClose(window) == 0);
}

26
preprocess-shader.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
if [ "$#" -ne 1 ]; then
echo "exactly one argument required"
exit 1
fi
mkdir -p preprocessed-shaders
preprocess_file() {
input_file="shaders/$1"
while IFS= read -r line || [ -n "$line" ]; do
if [[ $line = \#include* ]]; then
name=$( echo "$line" | cut -d'"' -f2)
preprocess_file "$name"
else
echo -e "$line"
fi
done < "$input_file"
}
input_name="$1"
output_file="preprocessed-shaders/$input_name"
preprocess_file "$input_name" > "$output_file"

87
readme.md Normal file
View file

@ -0,0 +1,87 @@
# TODO
- imgui
- split up into commands: view, preview-colormap, save-image, save-video
- colormap invert option
- pass colormap values as command line option?
- see if the rendering can be made better, maybe dynamically adjust generation duration
# Options
## subcommands
- `view`
- `preview-colormaps`
- `save-image`
- `save-video`
### `view`
#### automaton options
-[x] `--preset` `-p` `<preset-name>`
-[x] `--size` `-s` `<uint>x<uint>`
-[ ] `--birth` `-b` `<uint...>`
-[ ] `--survive` `-u` `<uint...>`
-[ ] `--max-age` `-a` `<max-age>`
-[ ] `--starve-delay` `-d` `<uint>`
-[ ] `--starve-recover` `-r` `<bool>`
-[ ] `--initialise` `-i` `<init>`
#### display options
-[ ] `--display-size` `-S` `<uint>x<uint>`
-[ ] `--fullscreen` `-F`
-[ ] `--color-alive` `-A` `<color-or-colormap>`
-[ ] `--color-dead` `-D` `<color>`
-[ ] `--generation-duration` `-G` `<uint>`
-[ ] `--blend` `-B`
## formats
### `color`
possible values:
-[x] `#<hex>`
-[x] `rgb(uint,uint,uint)`
-[x] `lab(float,float,float)`
-[x] `luv(float,float,float)`
-[x] `oklab(float,float,float)`
### `colormap`
TODO: this `:` syntax is inconsistent with the function-style syntax
-[x] `<colormapdef>[,<colormap-scale>][,invert]` name of a predefined colormap, and optional scale, and optional invert flag
### `colormapdef`:
-[x] `<name>` name of a predefined colormap
-[x] `map(<lab|luv|oklab>, (float,float,float),...)`
-[x] `map([rgb,] <colorlist-rgb>)`
### `colorlist-rgb`:
-[x] `<colorlist-color-rgb, ...>`
### `colorlist-color-rgb`:
-[x] `#<hex>`
-[x] `(<uint>,<uint>,<uint>)`
### `color-or-colormap`
-[x] `<color>`
-[x] `<colormap>`
### `colormap-scale`
-[x] `<uint>`
-[x] `inherent`
-[x] `global-max-age` only possible if max-age is set, otherwise fallback to `inherent`
-[x] `max-age` same as before
### `init`
-[x] `rect(<uint>,<uint>)`
-[x] `square(<uint>)`
-[x] `ellipse(<uint>,<uint>)`
-[x] `circle(<uint>)`
-[x] `perlin(<float>,<float>)` scale and cutoff
### `max-age`
-[x] `static(<uint>)`
-[x] `radial(<uint>,<uint>)` center and corner max age, circular
-[x] `radial-fit(<uint>,<uint>)` center and corner max age, elliptic fit into size
-[x] `perlin-static(<float>,<uint>,<uint>)` scale and min/max
-[x] `perlin-dynamic(<float>, <uint>, <uint>, <float>, <uint>)` scale, min, max, time-scale, time-step

104
shaders/automaton.comp Normal file
View file

@ -0,0 +1,104 @@
#version 460
layout (local_size_x = 1, local_size_y = 1) in;
uniform uvec2 dimensions;
uniform uint birth_rule;
uniform uint survive_rule;
uniform uint starve_delay;
uniform bool starve_recover;
uniform bool has_max_age;
layout (std430) restrict readonly buffer buffer_data_in
{
uint data_in[];
};
layout (std430) restrict buffer buffer_data_out
{
uint data_out[];
};
#include "cellstate_common.glsl"
bool check_condition(uint rule, uint count) {
return ((rule >> count) & uint(1)) == 1;
}
void main() {
uint data_size = dimensions.x * dimensions.y;
uint neighbour_count = 0;
for (int ix = -1; ix < 2; ix++) {
for (int iy = -1; iy < 2; iy++) {
int cx = int(gl_GlobalInvocationID.x) + ix;
int cy = int(gl_GlobalInvocationID.y) + iy;
if (cx < 0) {
cx += int(dimensions.x);
} else if (cx >= dimensions.x) {
cx -= int(dimensions.x);
}
if (cy < 0) {
cy += int(dimensions.y);
} else if (cy >= dimensions.y) {
cy -= int(dimensions.y);
}
uint pos = cx + cy * dimensions.x;
neighbour_count += cellstate_from_data(data_in[pos]).alive ? 1 : 0;
}
}
uint pos = gl_GlobalInvocationID.x + gl_GlobalInvocationID.y * dimensions.x;
CellState state = cellstate_from_data(data_in[pos]);
uint max_age = data_in[data_size + pos];
if (state.alive) {
if (starve_recover) {
if (check_condition(birth_rule, neighbour_count)) {
state.starving = false;
} else {
if (state.starving) {
state.starve_duration = state.starve_duration + 1;
} else {
state.starving = true;
state.starve_duration = 0;
}
}
} else {
if (!check_condition(survive_rule, neighbour_count)) {
if (state.starving) {
state.starve_duration = state.starve_duration + 1;
} else {
state.starving = true;
state.starve_duration = 0;
}
}
}
if (has_max_age && state.age >= max_age) {
state.alive = false;
state.starving = false;
state.age = 0;
state.starve_duration = 0;
}
if (state.starving && state.starve_duration == starve_delay) {
state.starving = false;
state.alive = false;
state.age = 0;
state.starve_duration = 0;
} else {
state.age = state.age + 1;
}
} else {
if (check_condition(birth_rule, neighbour_count)) {
state.alive = true;
state.age = 0;
}
}
data_out[pos] = cellstate_to_data(state);
// propagate max-age
data_out[data_size + pos] = data_in[data_size + pos];
}

108
shaders/automaton.frag Normal file
View file

@ -0,0 +1,108 @@
#version 460
in vec2 coord;
uniform bool preview_colormap;
uniform uvec2 dimensions;
uniform uint show_max_age;
uniform uint global_max_age;
uniform uint global_minimum_max_age;
uniform float blend_step;
uniform vec3 dead_color;
uniform bool dead_color_is_cielab;
uniform vec3 living_color;
uniform sampler1D living_colormap;
uniform bool living_use_colormap;
uniform bool living_colormap_invert;
uniform uint living_colormap_scale;
uniform bool living_colormap_scale_is_max_age;
uniform bool living_color_is_cielab;
out vec3 color;
layout (std430) restrict readonly buffer buffer_data_from
{
uint data_from[];
};
layout (std430) restrict readonly buffer buffer_data_to
{
uint data_to[];
};
#include "cellstate_common.glsl"
#include "color_conversion.glsl"
vec3 blend_color(vec3 from, vec3 to, float step) {
return vec3(
from.x * (1. - step) + to.x * step,
from.y * (1. - step) + to.y * step,
from.z * (1. - step) + to.z * step
);
}
// TODO: support cielab, cieluv and oklab?
vec3 get_color(CellState state, uint living_colormap_scale_) {
vec3 color;
if (state.alive) {
if (living_use_colormap) {
float pos = float(state.age) / float(living_colormap_scale_);
if (living_colormap_invert) {
pos = 1. - pos;
}
return texture(living_colormap, pos).rgb;
} else {
return living_color;
}
} else {
return dead_color;
}
}
void main() {
uint x = uint(float(dimensions.x) * (coord.x + 1.) / 2.);
uint y = uint(float(dimensions.y) * (coord.y + 1.) / 2.);
uint pos = x + y * dimensions.x;
uint max_age = data_from[dimensions.x * dimensions.y + pos];
float max_age_val = float(max_age - global_minimum_max_age) / float(global_max_age);
CellState state_from = cellstate_from_data(data_from[pos]);
CellState state_to = cellstate_from_data(data_to[pos]);
uint living_colormap_scale_from;
uint living_colormap_scale_to;
if (living_colormap_scale_is_max_age) {
living_colormap_scale_from = data_from[dimensions.x * dimensions.y + pos];
living_colormap_scale_to = data_to[dimensions.x * dimensions.y + pos];
} else {
living_colormap_scale_from = living_colormap_scale;
living_colormap_scale_to = living_colormap_scale;
}
vec3 color_from = get_color(state_from, living_colormap_scale_from);
vec3 color_to = get_color(state_to, living_colormap_scale_to);
vec3 color_lab = blend_color(color_from, color_to, blend_step);
vec3 color_rgb = convert_cielab_to_rgb(color_lab);
if (show_max_age == 0) {
color = color_rgb;
} else if (show_max_age == 1) {
color = vec3(max_age_val,max_age_val,max_age_val);
} else if (show_max_age == 2) {
float r = color_rgb.r * .1 + max_age_val * .9;
float g = color_rgb.g * .1 + max_age_val * .9;
float b = color_rgb.b * .1 + max_age_val * .9;
color = vec3(r,g,b);
}
if (preview_colormap) {
color = convert_cielab_to_rgb(texture(living_colormap, (coord.x+1.)/2.).rgb);
}
}
//void main(){
//// color = texture(living_colormap, (coord.x+1.)/2.).rgb;
// color = convert_cielab_to_rgb(texture(living_colormap, (coord.x+1.)/2.).rgb);
//}

9
shaders/automaton.vert Normal file
View file

@ -0,0 +1,9 @@
#version 460
layout (location = 0) in vec3 vertexPosition_modelspace;
out vec2 coord;
void main() {
gl_Position.xyz = vertexPosition_modelspace;
gl_Position.w = 1.0;
coord.xy = vertexPosition_modelspace.xy;
}

View file

@ -0,0 +1,22 @@
struct CellState {
bool alive;
bool starving;
uint age; // max 65025
uint starve_duration; // max 255
};
CellState cellstate_from_data(uint data) {
bool alive = (data & uint(0x1)) == 1;
bool starving = ((data >> 1) & uint(0x1)) == 1;
uint age = (data >> 8) & uint(0xFFFF);
uint starve_duration = (data >> 24) & uint(0xFF);
return CellState(alive, starving, age, starve_duration);
}
uint cellstate_to_data(CellState cellstate) {
return (cellstate.starve_duration << 24)
| (cellstate.age << 8)
| (cellstate.starving ? uint(0x2) : uint(0x0))
| (cellstate.alive ? uint(0x1) : uint(0x0));
}

View file

@ -0,0 +1,80 @@
vec3 convert_ciexyz_to_linear_rgb(vec3 xyz) {
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html sRGB M^-1 matrix
// mat3 M = mat3(
// 3.2404542, -1.5371385, -0.4985314,
// -0.9692660, 1.8760108, 0.0415560,
// 0.0556434, -0.2040259, 1.0572252
// );
// TODO: figure out which is correct, GLSL uses column-major for matrices
mat3 M = mat3(
3.2404542, -0.9692660, 0.0556434,
-1.5371385, 1.8760108, -0.2040259,
-0.4985314, 0.0415560, 1.0572252
);
return M * xyz;
}
vec3 convert_cielab_to_xyz(vec3 cielab, vec3 ref_white) {
float Xn = ref_white.x;
float Yn = ref_white.y;
float Zn = ref_white.z;
float L = cielab.x;
float a = cielab.y;
float b = cielab.z;
float epsilon = 216./24289.;
float kappa = 24389./27.;
float fy = (L + 16.) / 116.;
float fz = fy - b / 200.;
float fx = a / 500. + fy;
float xr;
float yr;
float zr;
if (pow(fx, 3.) > epsilon) {
xr = pow(fx, 3.);
} else {
xr = (116. * fx - 16.) / kappa;
}
if (L > kappa * epsilon) {
yr = pow((L + 16.) / 116., 3.);
} else {
yr = L / kappa;
}
if (pow(fz, 3.) > epsilon) {
zr = pow(fz, 3.);
} else {
zr = (116. * fz - 16.) / kappa;
}
float X = xr * Xn;
float Y = yr * Yn;
float Z = zr * Zn;
return vec3(X, Y, Z);
}
vec3 convert_cielab_to_rgb(vec3 cielab) {
// using D65
float Xn = .950489;
float Yn = 1.;
float Zn = 1.0888;
vec3 xyz = convert_cielab_to_xyz(cielab, vec3(Xn, Yn, Zn));
vec3 linear_rgb = convert_ciexyz_to_linear_rgb(xyz);
return linear_rgb;
}
vec3 convert_cieluv_to_rgb(vec3 cieluv) {
return cieluv;// TODO
}
vec3 convert_oklab_to_rgb(vec3 oklab) {
return oklab;// TODO
}

7
shell.nix Normal file
View file

@ -0,0 +1,7 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.clang_16
];
}

1679
src/argparser.h Normal file

File diff suppressed because it is too large Load diff

321
src/color.cpp Normal file
View file

@ -0,0 +1,321 @@
#include "color.h"
#include <algorithm>
#include <cmath>
constexpr double cie_epsilon = 216. / 24389.;
constexpr double cie_kappa = 24389. / 27.;
constexpr double D65_X = 0.95047;
constexpr double D65_Y = 1.;
constexpr double D65_Z = 1.08883;
static std::array<double, 3> linrgb_to_srgb(std::array<double, 3> linrgb);
static std::array<double, 3> srgb_to_linrgb(std::array<double, 3> srgb);
static std::array<double, 3> linrgb_to_xyz(std::array<double, 3> linrgb);
static std::array<double, 3> xyz_to_linrgb(std::array<double, 3> xyz);
static std::array<double, 3> cielab_to_xyz(std::array<double, 3> cielab);
static std::array<double, 3> xyz_to_cielab(std::array<double, 3> xyz);
static std::array<double, 3> cieluv_to_xyz(std::array<double, 3> cieluv);
static std::array<double, 3> xyz_to_cieluv(std::array<double, 3> xyz);
static std::array<double, 3> linrgb_to_oklab(std::array<double, 3> linrgb);
static std::array<double, 3> oklab_to_linrgb(std::array<double, 3> oklab);
static std::array<double, 3> xyzmatrix_mult(const std::array<std::array<double, 3>, 3> M, const std::array<double, 3> v);
ColorSpace Color::default_colorspace = ColorSpace::CIELAB;
std::array<double, 3> Color::values() const {
return {v1, v2, v3};
}
std::array<uint8_t, 3> Color::to_rgb() const {
std::array<double, 3> normalised = this->to_normalized_rgb();
return {
static_cast<uint8_t>(std::round(normalised[0]*255.)),
static_cast<uint8_t>(std::round(normalised[1]*255.)),
static_cast<uint8_t>(std::round(normalised[2]*255.)),
};
}
std::array<double, 3> Color::to_normalized_rgb() const {
std::array<double, 3> linrgb{};
switch (colorspace) {
case ColorSpace::CIELAB:
linrgb = xyz_to_linrgb(cielab_to_xyz({v1, v2, v3}));
break;
case ColorSpace::CIELUV:
linrgb = xyz_to_linrgb(cieluv_to_xyz({v1, v2, v3}));
break;
case ColorSpace::OKLAB:
linrgb = oklab_to_linrgb({v1, v2, v3});
break;
}
return linrgb_to_srgb(linrgb);
}
Color Color::from_rgb(const std::array<uint8_t, 3> rgb, ColorSpace colorspace) {
return from_rgb(rgb[0], rgb[1], rgb[2], colorspace);
}
Color Color::from_rgb(double red, double green, double blue, ColorSpace colorspace) {
return from_normalized_rgb(red / 255., green / 255., blue / 255., colorspace);
}
Color Color::from_rgb(const std::array<double, 3> rgb, ColorSpace colorspace) {
return from_rgb(rgb[0], rgb[1], rgb[2], colorspace);
}
Color Color::from_normalized_rgb(double red, double green, double blue, ColorSpace colorspace) {
return from_normalized_rgb({red, green, blue}, colorspace);
}
Color Color::from_normalized_rgb(const std::array<double, 3> rgb, ColorSpace colorspace) {
auto linrgb = srgb_to_linrgb(rgb);
std::array<double, 3> values{};
switch (colorspace) {
case ColorSpace::CIELAB:
values = xyz_to_cielab(linrgb_to_xyz(linrgb));
break;
case ColorSpace::CIELUV:
values = xyz_to_cieluv(linrgb_to_xyz(linrgb));
break;
case ColorSpace::OKLAB:
values = linrgb_to_oklab(linrgb);
break;
}
return {values, colorspace};
}
Color Color::from_rgb(uint8_t red, uint8_t green, uint8_t blue) {
return from_rgb(red, green, blue, default_colorspace);
}
Color Color::from_rgb(const std::array<uint8_t, 3> rgb) {
return from_rgb(rgb, default_colorspace);
}
Color Color::from_rgb(double red, double green, double blue) {
return from_rgb(red, green, blue, default_colorspace);
}
Color Color::from_rgb(const std::array<double, 3> rgb) {
return from_rgb(rgb, default_colorspace);
}
Color Color::from_normalized_rgb(double red, double green, double blue) {
return from_normalized_rgb(red, green, blue, default_colorspace);
}
Color Color::from_normalized_rgb(const std::array<double, 3> rgb) {
return from_normalized_rgb(rgb, default_colorspace);
}
void Color::set_default_colorspace(ColorSpace colorspace) {
default_colorspace = colorspace;
}
std::array<double, 3> linrgb_to_srgb(const std::array<double, 3> linrgb) {
std::array<double, 3> srgb{};
std::transform(linrgb.begin(), linrgb.end(), srgb.begin(), [](double v) {
if (v <= 0.0031308) {
return v * 12.92;
} else {
return 1.055 * pow(v, 1. / 2.4) - 0.055;
}
});
return srgb;
}
std::array<double, 3> srgb_to_linrgb(const std::array<double, 3> srgb) {
std::array<double, 3> linrgb{};
std::transform(srgb.begin(), srgb.end(), linrgb.begin(), [](double v) {
if (v <= 0.04045) {
return v / 12.92;
} else {
return pow(((v + 0.055) / 1.055), 2.4);
}
});
return linrgb;
}
static std::array<double, 3> xyzmatrix_mult(const std::array<std::array<double, 3>, 3> M, const std::array<double, 3> v) {
return {
M[0][0] * v[0] + M[0][1] * v[1] + M[0][2] * v[2],
M[1][0] * v[0] + M[1][1] * v[1] + M[1][2] * v[2],
M[2][0] * v[0] + M[2][1] * v[1] + M[2][2] * v[2],
};
}
constexpr std::array<std::array<double, 3>, 3> RGB_TO_XYZ_M = {
std::array<double, 3>{0.4124564, 0.3575761, 0.1804375},
std::array<double, 3>{0.2126729, 0.7151522, 0.0721750},
std::array<double, 3>{0.0193339, 0.1191920, 0.9503041},
};
constexpr std::array<std::array<double, 3>, 3> XYZ_TO_RGB_M = {
std::array<double, 3>{3.2404542, -1.5371385, -0.4985314},
std::array<double, 3>{-0.9692660, 1.8760108, 0.0415560},
std::array<double, 3>{0.0556434, -0.2040259, 1.0572252},
};
std::array<double, 3> linrgb_to_xyz(const std::array<double, 3> linrgb) {
return xyzmatrix_mult(RGB_TO_XYZ_M, linrgb);
}
std::array<double, 3> xyz_to_linrgb(const std::array<double, 3> xyz) {
return xyzmatrix_mult(XYZ_TO_RGB_M, xyz);
}
std::array<double, 3> cielab_to_xyz(const std::array<double, 3> cielab) {
auto L = cielab[0];
auto a = cielab[1];
auto b = cielab[2];
auto fy = (L + 16.) / 116.;
auto fz = fy - b / 200.;
auto fx = a / 500. + fy;
double xr, yr, zr;
if (pow(fx, 3.) > cie_epsilon) {
xr = pow(fx, 3.);
} else {
xr = (116. * fx - 16.) / cie_kappa;
}
if (L > cie_kappa * cie_epsilon) {
yr = pow((L + 16.) / 116., 3.);
} else {
yr = L / cie_kappa;
}
if (pow(fz, 3.) > cie_epsilon) {
zr = pow(fz, 3.);
} else {
zr = (116. * fz - 16.) / cie_kappa;
}
auto X = xr * D65_X;
auto Y = yr * D65_Y;
auto Z = zr * D65_Z;
return {X, Y, Z};
}
std::array<double, 3> xyz_to_cielab(const std::array<double, 3> xyz) {
double xr = xyz[0] / D65_X;
double yr = xyz[1] / D65_Y;
double zr = xyz[2] / D65_Z;
double fx, fy, fz;
if (xr > cie_epsilon) {
fx = cbrt(xr);
} else {
fx = (cie_kappa * xr + 16.) / 116.;
}
if (yr > cie_epsilon) {
fy = cbrt(yr);
} else {
fy = (cie_kappa * yr + 16.) / 116.;
}
if (zr > cie_epsilon) {
fz = cbrt(zr);
} else {
fz = (cie_kappa * zr + 16.) / 116.;
}
double L = 116. * fy - 16.;
double a = 500. * (fx - fy);
double b = 200. * (fy - fz);
return {L, a, b};
}
std::array<double, 3> cieluv_to_xyz(const std::array<double, 3> cieluv) {
constexpr double ur_ = (4. * D65_X) / (D65_X + 15. * D65_Y + 3. * D65_Z);
constexpr double vr_ = (9. * D65_Y) / (D65_X + 15. * D65_Y + 3. * D65_Z);
double L = cieluv[0];
double u = cieluv[1];
double v = cieluv[2];
double Y;
if (L > cie_kappa * cie_epsilon) {
Y = pow((L + 16.) / 116., 3.);
} else {
Y = L / cie_kappa;
}
double a_ = u + 13. * L * ur_;
double a = a_ != 0. ? 1. / 3. * (52. * L / a_ - 1.) : 0.;
double b = -5. * Y;
double c = -1. / 3.;
double d_ = v + 13. * L * vr_;
double d = d_ != 0. ? Y * (39. * L / d_ - 5.) : 0.;
double X = (d - b) / (a - c);
double Z = X * a + b;
return {X, Y, Z};
}
std::array<double, 3> xyz_to_cieluv(const std::array<double, 3> xyz) {
constexpr double ur_ = (4. * D65_X) / (D65_X + 15. * D65_Y + 3. * D65_Z);
constexpr double vr_ = (9. * D65_Y) / (D65_X + 15. * D65_Y + 3. * D65_Z);
double X = xyz[0];
double Y = xyz[1];
double Z = xyz[2];
double yr = Y / D65_Y;
double D = (X + 15. * Y + 3. * Z);
double u_, v_;
if (D != 0) {
u_ = (4. * X) / D;
v_ = (9. * Y) / D;
} else {
u_ = 0;
v_ = 0;
}
double L;
if (yr > cie_epsilon) {
L = 116. * cbrt(yr) - 16.;
} else {
L = cie_kappa * yr;
}
double u = 13. * L * (u_ - ur_);
double v = 13. * L * (v_ - vr_);
return {L, u, v};
}
std::array<double, 3> linrgb_to_oklab(const std::array<double, 3> linrgb) {
// https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
double l = 0.4122214708 * linrgb[0] + 0.5363325363 * linrgb[1] + 0.0514459929 * linrgb[2];
double m = 0.2119034982 * linrgb[0] + 0.6806995451 * linrgb[1] + 0.1073969566 * linrgb[2];
double s = 0.0883024619 * linrgb[0] + 0.2817188376 * linrgb[1] + 0.6299787005 * linrgb[2];
double l_ = cbrt(l);
double m_ = cbrt(m);
double s_ = cbrt(s);
return {
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
};
}
std::array<double, 3> oklab_to_linrgb(const std::array<double, 3> oklab) {
double l_ = oklab[0] + 0.3963377774 * oklab[1] + 0.2158037573 * oklab[2];
double m_ = oklab[0] - 0.1055613458 * oklab[1] - 0.0638541728 * oklab[2];
double s_ = oklab[0] - 0.0894841775 * oklab[1] - 1.2914855480 * oklab[2];
double l = l_ * l_ * l_;
double m = m_ * m_ * m_;
double s = s_ * s_ * s_;
return {
+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
};
}

108
src/color.h Normal file
View file

@ -0,0 +1,108 @@
#ifndef SHADER_AUTOMATON_COLOR_H
#define SHADER_AUTOMATON_COLOR_H
#include <GL/glew.h>
#include <cstdint>
#include <format>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
enum class ColorSpace {
CIELAB,
CIELUV,
OKLAB,
};
struct Color {
Color() : colorspace(default_colorspace){}
Color(const Color&)=default;
Color(double v1, double v2, double v3, ColorSpace colorspace) : v1(v1), v2(v2), v3(v3), colorspace(colorspace) {}
Color(double v1, double v2, double v3) : Color(v1, v2, v3, default_colorspace) {}
Color(std::array<double, 3> values, ColorSpace colorspace) : Color(values[0], values[1], values[2], colorspace) {}
explicit Color(std::array<double, 3> values) : Color(values, default_colorspace) {}
double v1{};
double v2{};
double v3{};
ColorSpace colorspace;
[[nodiscard]] std::array<double, 3> values() const;
[[nodiscard]] std::array<uint8_t, 3> to_rgb() const;
[[nodiscard]] std::array<double, 3> to_normalized_rgb() const;
static Color from_rgb(double red, double green, double blue, ColorSpace colorspace);
static Color from_rgb(std::array<uint8_t, 3> rgb, ColorSpace colorspace);
static Color from_rgb(std::array<double, 3> rgb, ColorSpace colorspace);
static Color from_normalized_rgb(double red, double green, double blue, ColorSpace colorspace);
static Color from_normalized_rgb(std::array<double, 3> rgb, ColorSpace colorspace);
static Color from_rgb(uint8_t red, uint8_t green, uint8_t blue);
static Color from_rgb(std::array<uint8_t, 3> rgb);
static Color from_rgb(double red, double green, double blue);
static Color from_rgb(std::array<double, 3> rgb);
static Color from_normalized_rgb(double red, double green, double blue);
static Color from_normalized_rgb(std::array<double, 3> rgb);
static void set_default_colorspace(ColorSpace colorspace);
private:
static ColorSpace default_colorspace;
};
//struct ColorMap {
// ~ColorMap() {
// if (this->owned_data) {
// delete this->data;
// }
// }
//
// virtual std::string convertToRgbCode(const std::string &val) {
// throw std::runtime_error("not implemented in base class");
// };
//
// size_t length;
// const float *data{};
// bool owned_data = false;
//};
//
//struct RgbColorMap : ColorMap {
// RgbColorMap(size_t length, const float data[], bool normalized = false, bool take_ownership = false) {
// this->length = length;
// if (normalized) {
// if (take_ownership) {
// this->owned_data = true;
// }
// this->data = data;
// } else {
// auto normalized_data = new float[length * 3];
// for (size_t i = 0; i < length * 3; ++i) {
// normalized_data[i] = data[i] / 255.f;
// }
// this->data = normalized_data;
// this->owned_data = true;
// if (take_ownership) {
// delete data;
// }
// }
// }
//
// std::string convertToRgbCode(const std::string &val) override {
// return val;
// };
//};
//struct CieLabColorMap : ColorMap {
// CieLabColorMap(size_t length, const float data[], bool take_ownership = false) {
// this->length = length;
// this->data = data;
// this->owned_data = take_ownership;
// }
//
// std::string convertToRgbCode(const std::string &val) override {
// return CieLabColor::convertToRgbCode(val);
// };
//};
#endif // SHADER_AUTOMATON_COLOR_H

View file

@ -0,0 +1,37 @@
#include "colormaps.h"
#include <memory>
extern const float colormap_rainbow_data[];
extern const float colormap_redgreen_data[];
extern const float colormap_orangeblue_data[];
extern const float colormap_magentacyan_data[];
extern const float colormap_purpleyellow_data[];
extern const float colormap_blackwhite_data[];
extern const float colormap_viridis_magma_data[];
extern const float colormap_viridis_inferno_data[];
extern const float colormap_viridis_plasma_data[];
extern const float colormap_viridis_viridis_data[];
extern const float colormap_viridis_cividis_data[];
extern const float colormap_viridis_rocket_data[];
extern const float colormap_viridis_mako_data[];
extern const float colormap_viridis_turbo_data[];
const std::map<std::string, ColorMapData> colormaps = {
{"redgreen", ColorMapData::fromArray(colormap_redgreen_data, 2, ColorSpace::CIELAB)},
{"orangeblue", ColorMapData::fromArray(colormap_orangeblue_data, 2, ColorSpace::CIELAB)},
{"magentacyan", ColorMapData::fromArray(colormap_magentacyan_data, 2, ColorSpace::CIELAB)},
{"purpleyellow", ColorMapData::fromArray(colormap_purpleyellow_data, 2, ColorSpace::CIELAB)},
{"blackwhite", ColorMapData::fromArray(colormap_blackwhite_data, 2, ColorSpace::CIELAB)},
{"rainbow", ColorMapData::fromArray(colormap_rainbow_data, 7, ColorSpace::CIELAB)},
{"magma", ColorMapData::fromArray(colormap_viridis_magma_data, 255, std::nullopt)},
{"inferno", ColorMapData::fromArray(colormap_viridis_inferno_data, 255, std::nullopt)},
{"plasma", ColorMapData::fromArray(colormap_viridis_plasma_data, 255, std::nullopt)},
{"viridis", ColorMapData::fromArray(colormap_viridis_viridis_data, 255, std::nullopt)},
{"cividis", ColorMapData::fromArray(colormap_viridis_cividis_data, 255, std::nullopt)},
{"rocket", ColorMapData::fromArray(colormap_viridis_rocket_data, 255, std::nullopt)},
{"mako", ColorMapData::fromArray(colormap_viridis_mako_data, 255, std::nullopt)},
{"turbo", ColorMapData::fromArray(colormap_viridis_turbo_data, 255, std::nullopt)},
};

58
src/colormaps/colormaps.h Normal file
View file

@ -0,0 +1,58 @@
#ifndef SHADER_AUTOMATON_COLORMAPS_H
#define SHADER_AUTOMATON_COLORMAPS_H
#include "../color.h"
#include <map>
struct ColorMapData {
private:
std::vector<float> data_{};
size_t size_{};
ColorSpace color_space_{};
public:
ColorMapData() : color_space_(ColorSpace::CIELAB) {}
explicit ColorMapData(const std::vector<Color>& colors) {
size_ = colors.size();
data_ = std::vector<float>(size_ * 3);
size_t i = 0;
for (const auto &item : colors) {
data_[i++] = static_cast<float>(item.v1);
data_[i++] = static_cast<float>(item.v2);
data_[i++] = static_cast<float>(item.v3);
}
}
static ColorMapData fromArray(const float *data, size_t num_colors, std::optional<ColorSpace> color_space) {
ColorMapData cmd{};
if (color_space.has_value()) {
cmd.color_space_ = color_space.value();
cmd.data_.assign(data, data + num_colors * 3);
} else {
cmd.data_ = std::vector<float>(num_colors * 3);
for (size_t i = 0; i < num_colors; ++i) {
auto c = Color::from_normalized_rgb(data[i * 3], data[i * 3 + 1], data[i * 3 + 2]);
cmd.data_[i * 3] = static_cast<float>(c.v1);
cmd.data_[i * 3 + 1] = static_cast<float>(c.v2);
cmd.data_[i * 3 + 2] = static_cast<float>(c.v3);
}
}
cmd.size_ = num_colors;
return cmd;
}
public:
[[nodiscard]] size_t size() const {
return size_;
}
[[nodiscard]] const float *data() const {
return data_.data();
}
};
extern const std::map<std::string, ColorMapData> colormaps;
#endif // SHADER_AUTOMATON_COLORMAPS_H

View file

@ -0,0 +1,33 @@
extern const float colormap_redgreen_data[];
extern const float colormap_orangeblue_data[];
extern const float colormap_magentacyan_data[];
extern const float colormap_purpleyellow_data[];
extern const float colormap_blackwhite_data[];
const float colormap_redgreen_data[] = {
53.f, 80.f, 67.f,
88.f, -86.f, 83.f,
};
const float colormap_orangeblue_data[] = {
67.f, 43.f, 74.f,
21.f, 79.f, -108.f,
};
const float colormap_magentacyan_data[] = {
55.f,85.f,4.f,
91.f,-48.f,-14.f
};
const float colormap_purpleyellow_data[] = {
97.f,-22.f,94.f,
41.f,83.f,-93.f,
};
const float colormap_blackwhite_data[] = {
0.f,0.f,0.f,
100.f,0.f,0.f,
};

12
src/colormaps/rainbow.cpp Normal file
View file

@ -0,0 +1,12 @@
extern const float colormap_rainbow_data[];
const float colormap_rainbow_data[] = {
53.f, 80.f, 67.f, // red
75.f, 24.f, 79.f, // orange
97.f, -22.f, 94.f, // yellow
46.f, -52.f, 50.f, // green
32.f, 79.f, -108.f, // blue
20.f, 52.f, -53.f, // indigo
70.f, 56.f, -37.f, // violet
};

2081
src/colormaps/viridis.cpp Normal file

File diff suppressed because it is too large Load diff

52
src/distinct-types.h Normal file
View file

@ -0,0 +1,52 @@
#ifndef SHADER_AUTOMATON_DISTINCT_TYPES_H
#define SHADER_AUTOMATON_DISTINCT_TYPES_H
#include <variant>
// #include <type_traits>
namespace distinct_types_internal {
template <typename TList, typename... Ts>
struct UniqueTypes;
template <template <typename...> typename X, typename... Ts>
struct UniqueTypes<X<Ts...>> {
using type = X<Ts...>;
};
template <template <typename...> typename X, typename... Ts, typename T, typename... Us>
struct UniqueTypes<X<Ts...>, T, Us...> {
using type = typename UniqueTypes<
typename std::conditional<
std::disjunction<std::is_same<T, Ts>...>::value,
X<Ts...>,
X<Ts..., T>>::type,
Us...>::type;
};
template <template <typename...> typename X, typename... Ts>
struct Distinct {
using type = typename UniqueTypes<X<>, Ts...>::type;
};
} // namespace distinct_types_internal
template <typename... Ts>
using distinct_types_variant = typename distinct_types_internal::Distinct<std::variant, Ts...>::type;
template <typename... Ts>
using distinct_types_tuple = typename distinct_types_internal::Distinct<std::tuple, Ts...>::type;
template <typename T, typename... Ts>
using if_single_type = std::enable_if_t<std::tuple_size<distinct_types_tuple<Ts...>>::value == 1, T>;
template <typename T, typename... Ts>
using if_not_single_type = std::enable_if_t<(std::tuple_size<distinct_types_tuple<Ts...>>::value > 1), T>;
template <typename T, typename F, typename... Ts>
using single_type_conditional = std::conditional<(std::tuple_size<distinct_types_tuple<Ts...>>::value == 1), T, F>;
template <typename... Ts>
using single_type = if_single_type<typename std::tuple_element<0, std::tuple<Ts...>>::type, Ts...>;
#endif // SHADER_AUTOMATON_DISTINCT_TYPES_H

16
src/main.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "options.h"
#include "program.h"
#include <GLFW/glfw3.h>
#include <iostream>
int main(int argc, const char **argv) {
Options options = parse_arguments(argc, argv);
try {
Program program(options);
program.run();
} catch (const std::exception &e) {
std::cout << e.what();
glfwTerminate();
return -1;
}
}

184
src/options.cpp Normal file
View file

@ -0,0 +1,184 @@
#include "options.h"
#include "argparser.h"
#include "colormaps/colormaps.h"
#include "rule_presets.h"
#include <format>
#include <stdexcept>
#include <string>
static Color parse_hex_color(const char *begin, const char *end, const char *&parse_end, argparser::internal::parser_allow_undelimited) {
if (begin >= end) {
throw std::runtime_error("cannot parse empty string");
}
if (*begin == '#') {
throw std::runtime_error("input is not a hex color");
}
if (end - begin < 7) {
throw std::runtime_error("input is too short");
}
uint8_t red, green, blue;
auto res = std::from_chars(begin + 1, begin + 3, red, 16);
if (res.ec != std::errc{} || res.ptr < begin + 3) {
throw std::runtime_error("input is not a hex color");
}
res = std::from_chars(begin + 3, begin + 5, green, 16);
if (res.ec != std::errc{} || res.ptr < begin + 5) {
throw std::runtime_error("input is not a hex color");
}
res = std::from_chars(begin + 5, begin + 7, blue, 16);
if (res.ec != std::errc{} || res.ptr < begin + 7) {
throw std::runtime_error("input is not a hex color");
}
return Color::from_rgb(red, green, blue);
}
Options do_parse_arguments(const std::vector<std::string> &args) {
std::vector<std::string> colormap_names = {};
for (const auto &c : colormaps) {
colormap_names.push_back(c.first);
}
std::vector<std::string> preset_names = {};
for (const auto &preset_ruleset : rule_presets) {
preset_names.push_back(preset_ruleset.first);
}
argparser::parser p{};
auto uint8 = p.basic_type<uint8_t>("uint8");
auto uint16 = p.basic_type<uint16_t>("uint16");
auto uint32 = p.basic_type<uint32_t>("uint32");
auto doubleType = p.basic_type<double>("float");
auto neighbourcount = p.basic_type<uint8_t>("neighbourcount")->validate([](uint8_t val) { return val >= 0 && val <= 8; });
auto neighbourList = p.list_type("neighbourcount-list", neighbourcount);
auto sizeType = p.tuple_type("size", uint16, uint16);
auto boolType = p.enum_type<bool>("bool", {{"true", true}, {"false", false}});
auto hexColor = p.basic_type<Color>("hex-color", parse_hex_color);
auto rgbColor = p.tuple_type<Color>("rgb", std::make_tuple(uint8, uint8, uint8), [](uint8_t r, uint8_t g, uint8_t b) {
return Color::from_rgb(r, g, b);
});
auto normalizedRgbColor = p.tuple_type<Color>("rgb-n", std::make_tuple(doubleType, doubleType, doubleType), [](double r, double g, double b) {
return Color::from_rgb(r, g, b);
});
auto labColor = p.tuple_type<Color>("lab", std::make_tuple(doubleType, doubleType, doubleType), [](double L, double a, double b) {
return Color{L, a, b, ColorSpace::CIELAB};
});
auto luvColor = p.tuple_type<Color>("luv", std::make_tuple(doubleType, doubleType, doubleType), [](double L, double u, double v) {
return Color{L, u, v, ColorSpace::CIELUV};
});
auto oklabColor = p.tuple_type<Color>("oklab", std::make_tuple(doubleType, doubleType, doubleType), [](double L, double a, double b) {
return Color{L, a, b, ColorSpace::OKLAB};
});
auto color = p.union_type("color", hexColor, rgbColor, normalizedRgbColor, labColor, luvColor, oklabColor);
auto colorList = p.list_type("colorlist", color);
auto colormapScaleEnum = p.enum_type<ColorMapScale>("colormap-scale-enum", {{"inherent", ColorMapScale::INHERENT},
{"max-age", ColorMapScale::MAX_AGE},
{"global-max-age", ColorMapScale::GLOBAL_MAX_AGE}});
auto colormapScale = p.union_type("colormap-scale", colormapScaleEnum, uint16);
auto map = p.tuple_type<ColorMapData>("map", colorList);
auto colormapName = p.enum_type("colormap-name", colormap_names);
auto colormapDef = p.union_type("colormapdef", colormapName, map);
auto colormapInvert = p.enum_type<bool>("colormap-invert", {{"invert", true}});
auto colormap1 = p.tuple_type<ColorMapOption>("colormapopt1", colormapDef);
auto colormap2 = p.tuple_type<ColorMapOption>("colormapopt2", colormapDef, colormapScale);
auto colormap3 = p.tuple_type<ColorMapOption>("colormapopt3", colormapDef, colormapInvert);
auto colormap4 = p.tuple_type<ColorMapOption>("colormapopt4", colormapDef, colormapScale, colormapInvert);
auto colormap5 = p.tuple_type<ColorMapOption>("colormapopt5", colormapDef, colormapInvert, colormapScale);
auto colormap = p.union_type("colormapopt", colormap1, colormap2, colormap3, colormap4, colormap5);
auto colorOrColormap = p.union_type("color-or-colormap", color, colormap);
auto initRect = p.tuple_type<std::shared_ptr<GridInitialiser>>("rect", std::make_tuple(uint16, uint16),
[](uint16_t w, uint16_t h) { return std::make_shared<RectGridInitialiser>(w, h); });
auto initSquare = p.tuple_type<std::shared_ptr<GridInitialiser>>("square", std::make_tuple(uint16),
[](uint16_t w) { return std::make_shared<RectGridInitialiser>(w, w); });
;
auto initEllipse = p.tuple_type<std::shared_ptr<GridInitialiser>>("ellipse", std::make_tuple(uint16, uint16),
[](uint16_t a, uint16_t b) { return std::make_shared<EllipseGridInitialiser>(a, b); });
auto initCircle = p.tuple_type<std::shared_ptr<GridInitialiser>>("circle", std::make_tuple(uint16),
[](uint16_t r) { return std::make_shared<EllipseGridInitialiser>(r, r); });
auto initPerlin = p.tuple_type<std::shared_ptr<GridInitialiser>>("perlin", std::make_tuple(doubleType, doubleType),
[](double scale, double cutoff) { return std::make_shared<PerlinGridInitialiser>(scale, cutoff); });
auto init = p.union_type("init", initRect, initSquare, initEllipse, initCircle, initPerlin);
auto noMaxAge = p.enum_type<std::shared_ptr<MaxAgeProvider>>("none", {{"none", nullptr}});
auto uniformMaxAge = p.tuple_type<std::shared_ptr<MaxAgeProvider>>("uniform", std::make_tuple(uint16),
[](uint16_t v) { return std::make_shared<UniformMaxAgeProvider>(v); });
auto radialMaxAge = p.tuple_type<std::shared_ptr<MaxAgeProvider>>("radial", std::make_tuple(uint16, uint16),
[](uint16_t center, uint16_t corners) { return std::make_shared<RadialMaxAgeProvider>(center, corners); });
auto radialFitMaxAge = p.tuple_type<std::shared_ptr<MaxAgeProvider>>("radial-fit", std::make_tuple(uint16, uint16),
[](uint16_t center, uint16_t corners) { return std::make_shared<RadialFitMaxAgeProvider>(center, corners); });
auto perlinStaticMaxAge = p.tuple_type<std::shared_ptr<MaxAgeProvider>>("perlin-static", std::make_tuple(doubleType, uint16, uint16),
[](double scale, uint16_t min, uint16_t max) { return std::make_shared<PerlinStaticMaxAgeProvider>(scale, min, max); });
auto perlinDynamicMaxAge = p.tuple_type<std::shared_ptr<MaxAgeProvider>>("perlin-dynamic", std::make_tuple(doubleType, uint16, uint16, doubleType, uint16),
[](double scale, uint16_t min, uint16_t max, double time_scale, uint16_t time_step) { return std::make_shared<PerlinDynamicMaxAgeProvider>(scale, min, max, time_scale, time_step); });
auto maxAge = p.union_type("max-age", noMaxAge, uniformMaxAge, radialMaxAge, radialFitMaxAge, perlinStaticMaxAge, perlinDynamicMaxAge);
auto presetName = p.enum_type("preset-name", preset_names);
auto presetOpt = p.option("--preset", "-p", presetName);
auto sizeOpt = p.option("--size", "-s", sizeType)->default_value(std::make_tuple(32, 32));
auto birthOpt = p.option("--birth", "-b", neighbourList)->default_value({});
auto surviveOpt = p.option("--survive", "-u", neighbourList)->default_value({});
auto maxAgeOpt = p.option("--max-age", "-a", maxAge)->default_value(nullptr);
auto starveDelayOpt = p.option("--starve-delay", "-d", uint16)->default_value(0);
auto starveRecoverOpt = p.option("--starve-recover", "-r", boolType)->default_value(false);
auto initialiseOpt = p.option("--init", "-i", init)->default_value(std::make_shared<RectGridInitialiser>(4, 4));
auto displaySizeOpt = p.option("--display-size", "-S", sizeType)->default_value(std::make_tuple(256, 256));
auto fullscreenFlag = p.flag("--fullscreen", "-F");
auto colorAliveOpt = p.option("--color-alive", "-A", colorOrColormap)->default_value(Color::from_rgb(1., 1., 1.));
auto colorDeadOpt = p.option("--color-dead", "-D", color)->default_value(Color::from_rgb(0., 0., 0.));
auto generationDurationOpt = p.option("--generation-duration", "-G", uint16)->default_value(0);
auto blendFlag = p.flag("--blend", "-B");
auto pr = p.parse(args);
Options options{};
auto size = sizeOpt->get(pr);
options.automaton_options.width = std::get<0>(size);
options.automaton_options.height = std::get<1>(size);
if (initialiseOpt->has(pr)) {
options.automaton_options.initialiser = initialiseOpt->get(pr);
}
bool uses_preset = false;
if (presetOpt->has(pr)) {
uses_preset = true;
options.automaton_options.rule = rule_presets.at(presetOpt->get(pr));
}
if (birthOpt->has(pr) || !uses_preset) {
options.automaton_options.rule.birth = birthOpt->get(pr);
}
if (surviveOpt->has(pr) || !uses_preset) {
options.automaton_options.rule.survive = surviveOpt->get(pr);
}
if (maxAgeOpt->has(pr) || !uses_preset) {
options.automaton_options.rule.max_age = maxAgeOpt->get(pr);
}
if (starveDelayOpt->has(pr) || !uses_preset) {
options.automaton_options.rule.starve_delay = starveDelayOpt->get(pr);
}
if (starveRecoverOpt->has(pr) || !uses_preset) {
options.automaton_options.rule.starve_recover = starveRecoverOpt->get(pr);
}
auto displaySize = displaySizeOpt->get(pr);
options.display_options.width = std::get<0>(displaySize);
options.display_options.height = std::get<1>(displaySize);
options.display_options.fullscreen = fullscreenFlag->get(pr);
options.display_options.dead_color = colorDeadOpt->get(pr);
options.display_options.alive_color = colorAliveOpt->get(pr);
options.display_options.generation_duration_ms = generationDurationOpt->get(pr);
options.display_options.blend = blendFlag->get(pr);
return options;
}
Options parse_arguments(int argc, const char *const *argv) {
std::vector<std::string> args(argc - 1);
for (size_t i = 1; i < argc; ++i) {
args[i - 1] = std::string(argv[i]);
}
return do_parse_arguments(args);
}

300
src/options.h Normal file
View file

@ -0,0 +1,300 @@
#ifndef SHADER_AUTOMATON_CLI_PARSE_H
#define SHADER_AUTOMATON_CLI_PARSE_H
#include "color.h"
#include "colormaps/colormaps.h"
#include <PerlinNoise.hpp>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <format>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>
enum class ColorMapScale {
INHERENT,
MAX_AGE,
GLOBAL_MAX_AGE,
};
struct ColorMapOption {
ColorMapOption(const ColorMapOption &) = default;
explicit ColorMapOption(std::variant<std::string, ColorMapData> source) : ColorMapOption(source, false, ColorMapScale::INHERENT) {}
ColorMapOption(std::variant<std::string, ColorMapData> source, std::variant<ColorMapScale, uint16_t> scale) : ColorMapOption(source, false, scale) {}
ColorMapOption(std::variant<std::string, ColorMapData> source, bool invert) : ColorMapOption(source, invert, ColorMapScale::INHERENT) {}
ColorMapOption(std::variant<std::string, ColorMapData> source, std::variant<ColorMapScale, uint16_t> scale, bool invert) : ColorMapOption(source, invert, scale) {}
ColorMapOption(std::variant<std::string, ColorMapData> source, bool invert, std::variant<ColorMapScale, uint16_t> scale) : invert(invert), scale(scale), source(source) {}
std::variant<std::string, ColorMapData> source;
std::variant<ColorMapScale, uint16_t> scale;
bool invert;
};
struct GridInitialiser {
virtual void setup(unsigned int width, unsigned int height) {
this->grid_width = width;
this->grid_height = height;
}
virtual bool getCell(unsigned int x, unsigned int y) = 0;
unsigned int grid_width{};
unsigned int grid_height{};
};
struct RectGridInitialiser : GridInitialiser {
RectGridInitialiser(unsigned int width, unsigned int height) {
this->width = width;
this->height = height;
}
unsigned int width, height;
bool getCell(unsigned int x, unsigned int y) override {
auto margin_x = (grid_width - this->width) / 2;
auto margin_y = (grid_height - this->height) / 2;
return x >= margin_x && x < grid_width - margin_x && y >= margin_y && y < grid_height - margin_y;
}
};
struct EllipseGridInitialiser : GridInitialiser {
EllipseGridInitialiser(double rx, double ry) {
this->radius_x = rx;
this->radius_y = ry;
}
double radius_x;
double radius_y;
bool getCell(unsigned int x, unsigned int y) override {
auto width = static_cast<double>(grid_width);
auto height = static_cast<double>(grid_height);
auto X = static_cast<double>(x) - (width / 2);
auto Y = static_cast<double>(y) - (height / 2);
auto z = X * X / (radius_x * radius_x) + Y * Y / (radius_y * radius_y);
return z <= 1;
}
};
struct PerlinGridInitialiser : GridInitialiser {
PerlinGridInitialiser(double scale, double density) : scale(scale), density(density) {
std::random_device r;
std::default_random_engine e1(r());
const siv::PerlinNoise::seed_type seed = e1();
noise = siv::PerlinNoise{seed};
}
bool getCell(unsigned int x, unsigned int y) override {
auto val = noise.noise2D_01(x * scale, y * scale);
return val >= (1. - density);
}
double scale, density;
siv::PerlinNoise noise{};
};
struct MaxAgeProvider {
public:
virtual void setup(unsigned int width, unsigned int height) {
this->grid_width = width;
this->grid_height = height;
}
virtual uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) = 0;
virtual uint16_t global_max() const = 0;
virtual uint16_t global_min() const = 0;
virtual bool is_dynamic() const {
return false;
}
virtual uint16_t dynamic_step() const {
return 0;
}
protected:
unsigned int grid_width{};
unsigned int grid_height{};
};
struct UniformMaxAgeProvider : MaxAgeProvider {
private:
uint16_t value_;
public:
explicit UniformMaxAgeProvider(uint16_t value) : value_(value) {}
uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) override {
return value_;
}
[[nodiscard]] uint16_t global_max() const override {
return value_;
}
[[nodiscard]] uint16_t global_min() const override {
return value_;
}
[[nodiscard]] uint16_t value() const { return value_; }
};
struct RadialMaxAgeProvider : MaxAgeProvider {
private:
uint16_t center_;
uint16_t corners_;
public:
RadialMaxAgeProvider(uint16_t center, uint16_t corners) : center_(center), corners_(corners) {}
uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) override {
auto cx = static_cast<double>(grid_width) / 2.;
auto cy = static_cast<double>(grid_height) / 2.;
auto corner_center_distance = sqrt(pow(cx, 2.) + pow(cy, 2.));
auto px = static_cast<double>(x);
auto py = static_cast<double>(y);
auto point_center_distance = sqrt(pow(px - cx, 2.) + pow(py - cy, 2.));
auto center_val = static_cast<double>(center_);
auto corner_val = static_cast<double>(corners_);
auto val = center_val + point_center_distance / corner_center_distance * (corner_val - center_val);
return static_cast<uint16_t>(round(val));
}
[[nodiscard]] uint16_t global_max() const override {
return center_ > corners_ ? center_ : corners_;
}
[[nodiscard]] uint16_t global_min() const override {
return center_ < corners_ ? center_ : corners_;
}
[[nodiscard]] uint16_t center() const { return center_; }
[[nodiscard]] uint16_t corners() const { return corners_; }
};
struct RadialFitMaxAgeProvider : MaxAgeProvider {
private:
uint16_t center_;
uint16_t corners_;
public:
RadialFitMaxAgeProvider(uint16_t center, uint16_t corners) : center_(center), corners_(corners) {}
uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) override {
auto cx = static_cast<double>(grid_width) / 2.;
auto cy = static_cast<double>(grid_height) / 2.;
auto px = static_cast<double>(x);
auto py = static_cast<double>(y);
auto point_center_distance = sqrt(pow((px - cx) / cx, 2.) + pow((py - cy) / cy, 2.));
auto center_val = static_cast<double>(center_);
auto corner_val = static_cast<double>(corners_);
auto val = center_val + point_center_distance * (corner_val - center_val);
return static_cast<uint16_t>(round(val));
}
[[nodiscard]] uint16_t global_max() const override {
return center_ > corners_ ? center_ : corners_;
}
[[nodiscard]] uint16_t global_min() const override {
return center_ < corners_ ? center_ : corners_;
}
[[nodiscard]] uint16_t center() const { return center_; }
[[nodiscard]] uint16_t corners() const { return corners_; }
};
struct PerlinStaticMaxAgeProvider : MaxAgeProvider {
private:
double scale_;
uint16_t min_;
uint16_t max_;
siv::PerlinNoise noise{};
public:
PerlinStaticMaxAgeProvider(double scale, uint16_t min, uint16_t max) : scale_(scale), min_(min), max_(max) {
std::random_device r;
std::default_random_engine e1(r());
const siv::PerlinNoise::seed_type seed = e1();
noise = siv::PerlinNoise{seed};
}
uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) override {
auto x_ = static_cast<double>(x) * scale_;
auto y_ = static_cast<double>(y) * scale_;
auto v = noise.noise2D_01(x_, y_);
return static_cast<uint16_t>(round(min_ + (max_ - min_) * v));
}
[[nodiscard]] uint16_t global_max() const override {
return max_;
}
[[nodiscard]] uint16_t global_min() const override {
return min_;
}
[[nodiscard]] double scale() const { return scale_; }
[[nodiscard]] uint16_t min() const { return min_; }
[[nodiscard]] uint16_t max() const { return max_; }
};
struct PerlinDynamicMaxAgeProvider : MaxAgeProvider {
private:
double scale_;
uint16_t min_;
uint16_t max_;
double time_scale_;
uint16_t time_step_;
siv::PerlinNoise noise{};
public:
PerlinDynamicMaxAgeProvider(double scale, uint16_t min, uint16_t max, double time_scale, uint16_t time_step)
: scale_(scale), min_(min), max_(max), time_scale_(time_scale), time_step_(time_step) {
std::random_device r;
std::default_random_engine e1(r());
const siv::PerlinNoise::seed_type seed = e1();
noise = siv::PerlinNoise{seed};
}
uint16_t getAt(uint16_t x, uint16_t y, uint64_t generation) override {
auto x_ = static_cast<double>(x) * scale_;
auto y_ = static_cast<double>(y) * scale_;
auto t_ = static_cast<double>(generation) / static_cast<double>(time_step_) * time_scale_;
auto v = noise.noise3D_01(x_, y_, t_);
return static_cast<uint16_t>(round(min_ + (max_ - min_) * v));
}
[[nodiscard]] uint16_t global_max() const override {
return max_;
}
[[nodiscard]] uint16_t global_min() const override {
return min_;
}
[[nodiscard]] bool is_dynamic() const override {
return true;
}
[[nodiscard]] uint16_t dynamic_step() const override {
return time_step_;
}
[[nodiscard]] double scale() const { return scale_; }
[[nodiscard]] uint16_t min() const { return min_; }
[[nodiscard]] uint16_t max() const { return max_; }
[[nodiscard]] double time_scale() const { return time_scale_; }
[[nodiscard]] uint16_t time_step() const { return time_step_; }
};
struct DisplayOptions {
uint16_t width;
uint16_t height;
bool fullscreen;
size_t fullscreen_screen_number;
Color dead_color;
std::variant<Color, ColorMapOption> alive_color;
uint64_t generation_duration_ms;
bool blend;
};
struct AutomatonRule {
std::vector<uint8_t> birth;
std::vector<uint8_t> survive;
std::shared_ptr<MaxAgeProvider> max_age;
uint8_t starve_delay;
bool starve_recover;
};
struct AutomatonOptions {
uint16_t width;
uint16_t height;
// bool wrap_edges;
AutomatonRule rule;
std::shared_ptr<GridInitialiser> initialiser;
};
struct Options {
AutomatonOptions automaton_options;
DisplayOptions display_options;
};
Options parse_arguments(int argc, const char *const *argv);
#endif // SHADER_AUTOMATON_CLI_PARSE_H

1099
src/program.cpp Normal file

File diff suppressed because it is too large Load diff

118
src/program.h Normal file
View file

@ -0,0 +1,118 @@
#ifndef SHADER_AUTOMATON_PROGRAM_H
#define SHADER_AUTOMATON_PROGRAM_H
#include "options.h"
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <map>
#include <queue>
class Program {
public:
explicit Program(Options opts);
~Program();
void run();
void keyCallback(int key, int scancode, int action, int mods);
private:
struct BufferInfo {
GLuint bufferName;
GLuint bindingPoint;
};
void createWindow();
[[nodiscard]] GLsync startCompute(BufferInfo in, BufferInfo out) const;
void renderFrame(BufferInfo from, BufferInfo to, double step) const;
void renderUI();
void initializeFirstBuffer(BufferInfo buffer) const;
void toggleFullscreen();
void imguiMainWindow();
void imguiCloseWindow();
Options options;
GLFWwindow *window{};
int window_pos_x{};
int window_pos_y{};
int window_width{};
int window_height{};
GLuint renderProgram{};
GLuint renderBlockIndexFrom{};
GLuint renderBlockIndexTo{};
GLint renderPreviewColormapLoc{};
GLint renderDimensionsLoc{};
GLint renderShowMaxAgeLoc{};
GLint renderGlobalMaxAgeLoc{};
GLint renderGlobalMinimumMaxAgeLoc{};
GLint renderBlendStepLoc{};
GLint renderDeadColorLoc{};
GLint renderDeadColorIsCielabLoc{};
GLint renderLivingColorLoc{};
GLint renderLivingColormapLoc{};
GLint renderLivingUseColormapLoc{};
GLint renderLivingColormapInvertLoc{};
GLint renderLivingColormapScaleLoc{};
GLint renderLivingColormapScaleIsMaxAgeLoc{};
GLint renderLivingColorIsCielabLoc{};
GLuint computeProgram{};
GLuint computeBlockIndexInput{};
GLuint computeBlockIndexOutput{};
GLint computeDimensionsLoc{};
GLint computeBirthRuleLoc{};
GLint computeSurviveRuleLoc{};
GLint computeStarveDelayLoc{};
GLint computeStarveRecoverLoc{};
GLint computeHasMaxAgeLoc{};
std::map<std::string, GLuint> colormap_textures{};
GLuint vertexBuffer{};
GLFWmonitor *fullscreenMonitor{};
bool is_fullscreen = false;
bool show_ui = false;
bool show_close_popup = false;
bool is_paused{}; // todo
uint8_t render_max_age{};
bool preview_colormap{};
void debugPrintBuffer(const std::string &text, BufferInfo buffer) const {
return;
auto width = options.automaton_options.width;
auto height = options.automaton_options.height;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer.bufferName);
auto *bufferData = static_cast<unsigned int *>(glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_ONLY));
std::cout << "=====[" << text << " (" << buffer.bufferName << ")]=====" << std::endl;
auto offset = width * height;
for (size_t ix = 0; ix < width; ++ix) {
for (size_t iy = 0; iy < height; ++iy) {
std::cout << std::format("max-age at {}, {}: {}",ix, iy, bufferData[offset + ix + iy * width]) << std::endl;
}
}
std::cout << "===============" << std::endl
<< std::endl;
std::cout << std::flush;
glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}
void imguiAutomatonTab();
void imguiDisplayTab();
bool imguiBirthConditionTable(bool apply_changes, bool revert_changes);
bool imguiMaxAge(bool apply_changes, bool revert_changes);
bool imguiSurviveConditionTable(bool apply_changes, bool revert_changes);
bool imguiStarve(bool apply_changes, bool revert_changes);
void imguiDeadCells();
void imguiLivingCells();
uint32_t makeRuleBitfield(const std::vector<uint8_t> &rule) const;
void initializeMaxAge(BufferInfo buffer, uint64_t generation) const;
};
#endif // SHADER_AUTOMATON_PROGRAM_H

25
src/rule_presets.cpp Normal file
View file

@ -0,0 +1,25 @@
//
// Created by gwendolyn on 5/4/23.
//
#include "rule_presets.h"
const std::map<std::string, AutomatonRule> rule_presets = {
{"maze",
{.birth = {3},
.survive = {1, 2, 3, 4, 5},
.max_age = nullptr,
.starve_delay = 0,
.starve_recover = false}},
{"castle",
{.birth = {4, 5, 6, 7, 8},
.survive = {2, 3, 4, 5},
.max_age = nullptr,
.starve_delay = 0,
.starve_recover = false}},
{"conway",
{.birth = {3},
.survive = {2, 3},
.max_age = nullptr,
.starve_delay = 0,
.starve_recover = false}}};

11
src/rule_presets.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef SHADER_AUTOMATON_RULE_PRESETS_H
#define SHADER_AUTOMATON_RULE_PRESETS_H
#include <map>
#include <string>
#include "options.h"
extern const std::map<std::string, AutomatonRule> rule_presets;
#endif // SHADER_AUTOMATON_RULE_PRESETS_H

51
src/shader-helper.cpp Normal file
View file

@ -0,0 +1,51 @@
#include "shader-helper.h"
#include <cstdlib>
#include <iostream>
#include <span>
GLuint loadShader(std::string_view name, std::span<const std::byte> source, GLenum shader_type) {
GLuint shaderId = glCreateShader(shader_type);
GLint result = GL_FALSE;
int infoLogLength;
auto data = reinterpret_cast<const char*>(source.data());
auto size = static_cast<GLint>(source.size());
glShaderSource(shaderId, 1, &data, &size);
glCompileShader(shaderId);
glGetShaderiv(shaderId, GL_COMPILE_STATUS, &result);
glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0) {
std::cout << "errors in shader " << name << std::endl;
std::vector<char> VertexShaderErrorMessage(infoLogLength + 1);
glGetShaderInfoLog(shaderId, infoLogLength, nullptr,
&VertexShaderErrorMessage[0]);
std::cout << &VertexShaderErrorMessage[0] << std::endl;
std::cout << "=====" << std::endl;
std::cout << std::string_view(data, size) << std::endl;
exit(-1);
}
return shaderId;
}
GLuint loadProgram(std::string_view name, const std::vector<GLuint> &shaders) {
GLuint programId = glCreateProgram();
for (const auto &item : shaders) {
glAttachShader(programId, item);
}
glLinkProgram(programId);
GLint result = GL_FALSE;
int infoLogLength;
glGetProgramiv(programId, GL_LINK_STATUS, &result);
glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &infoLogLength);
if (infoLogLength > 0) {
std::cout << "errors in program " << name << std::endl;
std::vector<char> ProgramErrorMessage(infoLogLength + 1);
glGetProgramInfoLog(programId, infoLogLength, nullptr,
&ProgramErrorMessage[0]);
std::cout << &ProgramErrorMessage[0] << std::endl;
}
return programId;
}

13
src/shader-helper.h Normal file
View file

@ -0,0 +1,13 @@
#ifndef SHADER_AUTOMATON_SHADER_HELPER_H
#define SHADER_AUTOMATON_SHADER_HELPER_H
#include <GL/glew.h>
#include <vector>
#include <string_view>
#include <span>
GLuint loadShader(std::string_view name, std::span<const std::byte> sources, GLenum shader_type);
GLuint loadProgram(std::string_view name, const std::vector<GLuint> &shaders);
#endif // SHADER_AUTOMATON_SHADER_HELPER_H

229
tests/color.cpp Normal file
View file

@ -0,0 +1,229 @@
#include "gtest/gtest.h"
#include <cmath>
#include "color.h"
static std::array<uint8_t, 3> white_rgb = {255, 255, 255};
static std::array<double, 3> white_cielab = {100, 0, 0};
static std::array<double, 3> white_cieluv = {100, 0, 0};
static std::array<double, 3> white_oklab = {1, 0, 0};
static std::array<uint8_t, 3> black_rgb = {0, 0, 0};
static std::array<double, 3> black_cielab = {0, 0, 0};
static std::array<double, 3> black_cieluv = {0, 0, 0};
static std::array<double, 3> black_oklab = {0, 0, 0};
static std::array<uint8_t, 3> tiny_rgb = {4, 31, 13};
static std::array<double, 3> tiny_cielab = {9.2779, -14.8940, 8.1560};
static std::array<double, 3> tiny_cieluv = {9.2779, -7.2368, 6.6930};
static std::array<double, 3> tiny_oklab = {0.2125, -0.0429, 0.023};
static std::array<uint8_t, 3> big_rgb = {247, 252, 255};
static std::array<double, 3> big_cielab = {98.6759, -1.1049, -2.006};
static std::array<double, 3> big_cieluv = {98.6759, -2.9261, -2.9267};
static std::array<double, 3> big_oklab = {0.9882, -0.0039, -0.0054};
static std::array<uint8_t, 3> medium_rgb = {89, 69, 219};
static std::array<double, 3> medium_cielab = {40.3991, 49.4085, -73.8982};
static std::array<double, 3> medium_cieluv = {40.3991, -2.9720, -108.8107};
static std::array<double, 3> medium_oklab = {0.5074, 0.0429, -0.2129};
static double round_to_4_decimals(double val) {
return std::round(val * 10000.) / 10000.;
}
static void assert_equal_color_arr(Color c, std::array<double, 3> a) {
ASSERT_DOUBLE_EQ(round_to_4_decimals(c.v1), a[0]);
ASSERT_DOUBLE_EQ(round_to_4_decimals(c.v2), a[1]);
ASSERT_DOUBLE_EQ(round_to_4_decimals(c.v3), a[2]);
}
static void assert_equal_rgb(std::array<double, 3> a1, std::array<uint8_t, 3> a2) {
ASSERT_EQ(static_cast<int>(round(a1[0]*255.)), a2[0]);
ASSERT_EQ(static_cast<int>(round(a1[1]*255.)), a2[1]);
ASSERT_EQ(static_cast<int>(round(a1[2]*255.)), a2[2]);
}
////
TEST(RgbToOklab, White) {
auto color = Color::from_rgb(white_rgb, ColorSpace::OKLAB);
assert_equal_color_arr(color, white_oklab);
}
TEST(RgbToOklab, Black) {
auto color = Color::from_rgb(black_rgb, ColorSpace::OKLAB);
assert_equal_color_arr(color, black_oklab);
}
TEST(RgbToOklab, TinyValues) {
auto color = Color::from_rgb(tiny_rgb, ColorSpace::OKLAB);
assert_equal_color_arr(color, tiny_oklab);
}
TEST(RgbToOklab, BigValues) {
auto color = Color::from_rgb(big_rgb, ColorSpace::OKLAB);
assert_equal_color_arr(color, big_oklab);
}
TEST(RgbToOklab, MediumValues) {
auto color = Color::from_rgb(medium_rgb, ColorSpace::OKLAB);
assert_equal_color_arr(color, medium_oklab);
}
////
TEST(OklabToRgb, White) {
auto color = Color{white_oklab, ColorSpace::OKLAB}.to_normalized_rgb();
assert_equal_rgb(color, white_rgb);
}
TEST(OklabToRgb, Black) {
auto color = Color{black_oklab, ColorSpace::OKLAB}.to_normalized_rgb();
assert_equal_rgb(color, black_rgb);
}
TEST(OklabToRgb, TinyValues) {
auto color = Color{tiny_oklab, ColorSpace::OKLAB}.to_normalized_rgb();
assert_equal_rgb(color, tiny_rgb);
}
TEST(OklabToRgb, BigValues) {
auto color = Color{big_oklab, ColorSpace::OKLAB}.to_normalized_rgb();
assert_equal_rgb(color, big_rgb);
}
TEST(OklabToRgb, MediumValues) {
auto color = Color{medium_oklab, ColorSpace::OKLAB}.to_normalized_rgb();
assert_equal_rgb(color, medium_rgb);
}
////
TEST(RgbToCielab, White) {
auto color = Color::from_rgb(white_rgb, ColorSpace::CIELAB);
assert_equal_color_arr(color, white_cielab);
}
TEST(RgbToCielab, Black) {
auto color = Color::from_rgb(black_rgb, ColorSpace::CIELAB);
assert_equal_color_arr(color, black_cielab);
}
TEST(RgbToCielab, TinyValues) {
auto color = Color::from_rgb(tiny_rgb, ColorSpace::CIELAB);
assert_equal_color_arr(color, tiny_cielab);
}
TEST(RgbToCielab, BigValues) {
auto color = Color::from_rgb(big_rgb, ColorSpace::CIELAB);
assert_equal_color_arr(color, big_cielab);
}
TEST(RgbToCielab, MediumValues) {
auto color = Color::from_rgb(medium_rgb, ColorSpace::CIELAB);
assert_equal_color_arr(color, medium_cielab);
}
////
TEST(CielabToRgb, White) {
auto color = Color{white_cielab, ColorSpace::CIELAB}.to_normalized_rgb();
assert_equal_rgb(color, white_rgb);
}
TEST(CielabToRgb, Black) {
auto color = Color{black_cielab, ColorSpace::CIELAB}.to_normalized_rgb();
assert_equal_rgb(color, black_rgb);
}
TEST(CielabToRgb, TinyValues) {
auto color = Color{tiny_cielab, ColorSpace::CIELAB}.to_normalized_rgb();
assert_equal_rgb(color, tiny_rgb);
}
TEST(CielabToRgb, BigValues) {
auto color = Color{big_cielab, ColorSpace::CIELAB}.to_normalized_rgb();
assert_equal_rgb(color, big_rgb);
}
TEST(CielabToRgb, MediumValues) {
auto color = Color{medium_cielab, ColorSpace::CIELAB}.to_normalized_rgb();
assert_equal_rgb(color, medium_rgb);
}
/////
TEST(RgbToCieluv, White) {
auto color = Color::from_rgb(white_rgb, ColorSpace::CIELUV);
assert_equal_color_arr(color, white_cieluv);
}
TEST(RgbToCieluv, Black) {
auto color = Color::from_rgb(black_rgb, ColorSpace::CIELUV);
assert_equal_color_arr(color, black_cieluv);
}
TEST(RgbToCieluv, TinyValues) {
auto color = Color::from_rgb(tiny_rgb, ColorSpace::CIELUV);
assert_equal_color_arr(color, tiny_cieluv);
}
TEST(RgbToCieluv, BigValues) {
auto color = Color::from_rgb(big_rgb, ColorSpace::CIELUV);
assert_equal_color_arr(color, big_cieluv);
}
TEST(RgbToCieluv, MediumValues) {
auto color = Color::from_rgb(medium_rgb, ColorSpace::CIELUV);
assert_equal_color_arr(color, medium_cieluv);
}
////
TEST(CieluvToRgb, White) {
auto color = Color{white_cieluv, ColorSpace::CIELUV}.to_normalized_rgb();
assert_equal_rgb(color, white_rgb);
}
TEST(CieluvToRgb, Black) {
auto color = Color{black_cieluv, ColorSpace::CIELUV}.to_normalized_rgb();
assert_equal_rgb(color, black_rgb);
}
TEST(CieluvToRgb, TinyValues) {
auto color = Color{tiny_cieluv, ColorSpace::CIELUV}.to_normalized_rgb();
assert_equal_rgb(color, tiny_rgb);
}
TEST(CieluvToRgb, BigValues) {
auto color = Color{big_cieluv, ColorSpace::CIELUV}.to_normalized_rgb();
assert_equal_rgb(color, big_rgb);
}
TEST(CieluvToRgb, MediumValues) {
auto color = Color{medium_cieluv, ColorSpace::CIELUV}.to_normalized_rgb();
assert_equal_rgb(color, medium_rgb);
}
////
TEST(RgbNormalisation, FromRgb) {
auto color1 = Color::from_rgb(medium_rgb);
auto color2 = Color::from_normalized_rgb({
static_cast<double>(medium_rgb[0])/255.,
static_cast<double>(medium_rgb[1])/255.,
static_cast<double>(medium_rgb[2])/255.,
});
ASSERT_DOUBLE_EQ(color1.v1, color2.v1);
ASSERT_DOUBLE_EQ(color1.v2, color2.v2);
ASSERT_DOUBLE_EQ(color1.v3, color2.v3);
}
TEST(RgbNormalisation, ToRgb) {
auto color1 = Color{medium_cielab}.to_rgb();
auto color2 = Color{medium_cielab}.to_normalized_rgb();
assert_equal_rgb(color2, color1);
}