@ -1,88 +0,0 @@
|
||||
--- |
||||
title: "My First Blog Post" |
||||
description: "This is a test article." |
||||
navigation: true |
||||
date: "2025-03-01" |
||||
img: "/img/personal-website/login.png" |
||||
tags: |
||||
- C++20 |
||||
- GLSL |
||||
- Lua |
||||
- Software |
||||
--- |
||||
|
||||
Foobarbazbuz. |
||||
|
||||
## Component Rendering |
||||
|
||||
::ExampleComponent |
||||
#namedslot |
||||
This is a Named Slot |
||||
#default |
||||
This is the Default Slot |
||||
:: |
||||
|
||||
### Testing Some Markdown Features |
||||
|
||||
A link: [website-vue](https://github.com/riyyi/website-vue) |
||||
|
||||
A codeblock: |
||||
```js [file.js]{2} meta-info=val |
||||
export default () => { |
||||
console.log('Code block') |
||||
} |
||||
``` |
||||
|
||||
```cpp |
||||
int i = 0; |
||||
``` |
||||
|
||||
```php |
||||
protected static $router; |
||||
$path = parse_url($_SERVER['REQUEST_URI'])['path']; |
||||
``` |
||||
|
||||
Inline `hightlight` looks like so. |
||||
|
||||
Inline highlight with language `const code: string = 'highlighted code inline'`{lang="ts"} like this. |
||||
|
||||
- An |
||||
- Unordered |
||||
- List |
||||
|
||||
## Heading With No Subheading |
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum a tempor |
||||
dolor. Nullam mattis sapien vel finibus dignissim. Etiam et diam ultrices, |
||||
aliquam enim nec, commodo sem. Cras ut faucibus risus. Suspendisse vel faucibus |
||||
ipsum. Duis vel orci nec arcu porttitor fermentum eu quis est. Phasellus elit |
||||
odio, elementum ac placerat at, feugiat sit amet sapien. |
||||
|
||||
Vestibulum dapibus pharetra metus. Integer volutpat lacus nec enim euismod, id |
||||
dignissim felis rhoncus. Cras commodo tempus turpis, eu vehicula mi lacinia |
||||
eget. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per |
||||
inceptos himenaeos. Integer rhoncus dolor ut dolor gravida vestibulum. Ut |
||||
dignissim orci a ornare ullamcorper. Nam facilisis mauris sit amet nunc |
||||
fermentum pretium. Nullam in nisi at risus luctus viverra. Mauris non congue |
||||
dolor, vel finibus lectus. Nam quis leo pretium, sodales augue et, dictum sem. |
||||
|
||||
Curabitur velit ante, imperdiet in eros eu, iaculis gravida risus. Nullam ut |
||||
feugiat eros, viverra vehicula sem. Aliquam finibus mi magna, eu fringilla |
||||
tellus ullamcorper vel. Fusce eget auctor mi. Mauris venenatis pellentesque |
||||
arcu. Nam ac diam sem. Nulla suscipit sed risus non vehicula. Cras molestie |
||||
lectus et tincidunt tempor. Ut tempus lorem id augue semper convallis. Aliquam |
||||
vel dui quis dolor cursus faucibus eget a mi. Curabitur tempus justo diam, a |
||||
facilisis ligula tincidunt viverra. Donec sodales quis dolor at dignissim. |
||||
Nullam placerat vitae urna quis bibendum. |
||||
|
||||
Integer a magna a velit bibendum mollis. Donec lobortis molestie diam at rutrum. |
||||
Nunc viverra gravida metus at facilisis. Nulla sit amet erat sodales, commodo |
||||
elit sed, ultrices magna. Aliquam vel purus fringilla, gravida enim quis, |
||||
aliquam augue. Duis interdum et erat nec feugiat. Praesent vitae lacinia leo, |
||||
non suscipit purus. |
||||
|
||||
Aenean massa magna, imperdiet id ex quis, mollis auctor lectus. Cras velit |
||||
nulla, volutpat eu neque id, semper venenatis urna. Integer in blandit ex, non |
||||
scelerisque nisi. Ut sagittis tincidunt enim at volutpat. Sed hendrerit metus ac |
||||
interdum laoreet. In rutrum turpis in nulla vulputate suscipit. Nunc dictum nisl |
||||
id magna laoreet dapibus. |
@ -0,0 +1,432 @@
|
||||
--- |
||||
title: "Inferno" |
||||
description: "An open-source game engine." |
||||
navigation: true |
||||
date: "2019-12-10" |
||||
img: "/img/inferno/preview.png" |
||||
tags: |
||||
- C++20 |
||||
- GLSL |
||||
- Lua |
||||
- CMake |
||||
- Software |
||||
--- |
||||
|
||||
<small>An open-source game engine.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/inferno){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/inferno){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/inferno){target="_blank"}. |
||||
</small> |
||||
|
||||
Video games have always been a hobby of mine, but what if I could combine my |
||||
profession with this hobby? Then you get this project, a game engine! |
||||
|
||||
This open-source game engine is written in C++20, GLSL and Lua with the |
||||
libraries EnTT, glad, GLFW, GLM, sol3 and stb, using the build tool CMake. |
||||
|
||||
## Abstractions |
||||
|
||||
Making a game engine is not an easy task, if the project has any chance of |
||||
success it needs a solid foundation. Therefor, the main focus of the project |
||||
thus far has been to work on solid, easy-to-use abstractions. The additional |
||||
benefit of this approach is that I learned a lot about software design in the |
||||
process. |
||||
|
||||
### Events |
||||
|
||||
The library used for window and context creation and receiving input is GLFW, |
||||
this is abstracted away in a "Window" wrapper class. This "Window" is created in |
||||
the "Application" class, which also binds the callback function for event |
||||
handling. |
||||
|
||||
```cpp |
||||
// Make it more readable |
||||
#define NF_BIND_EVENT(f) std::bind(&f, this, std::placeholders::_1) |
||||
|
||||
m_window = std::make_unique<Window>(); |
||||
m_window->setEventCallback(NF_BIND_EVENT(Application::onEvent)); |
||||
``` |
||||
|
||||
The event callback is stored in a std::function. |
||||
|
||||
```cpp |
||||
inline void setEventCallback(const std::function<void(Event&)>& callback) { m_eventCallback = callback; } |
||||
``` |
||||
|
||||
Inside the "Window" wrapper, we first associate the wrapper to the internal "GLFWwindow" object. |
||||
```cpp |
||||
glfwSetWindowUserPointer(m_window, this); |
||||
``` |
||||
|
||||
Then, it can set the appropriate callback functions. In this example, we set the |
||||
position of the mouse. It gets the "Window" wrapper from the association, then |
||||
creates an event object, which is passed on to the callback function. |
||||
|
||||
```cpp |
||||
// Mouse position callback |
||||
glfwSetCursorPosCallback(m_window, [](GLFWwindow* window, double xPos, double yPos) { |
||||
Window& w = *(Window*)glfwGetWindowUserPointer(window); |
||||
|
||||
MousePositionEvent event(xPos, yPos); |
||||
w.m_eventCallback(event); |
||||
}); |
||||
``` |
||||
|
||||
All we have to do to handle these incoming events is create an event dispatcher |
||||
and call dispatch. The dispatcher simply checks if the current event is of the |
||||
same type as the provided event and calls the function if this is the case. |
||||
|
||||
```cpp |
||||
void Application::onEvent(Event& e) |
||||
{ |
||||
EventDispatcher dispatcher(e); |
||||
dispatcher.dispatch<MousePositionEvent>(NF_BIND_EVENT(Application::onMousePosition)); |
||||
} |
||||
``` |
||||
|
||||
### Logging |
||||
|
||||
I wanted logging to work with both `printf` and `std::cout` style logging. To |
||||
make logging fast and easy to use, it also has to be compatible with |
||||
user-defined types. The class hierarchy looks like the following: |
||||
|
||||
``` |
||||
. |
||||
└── LogStream |
||||
└── BufferedLogStream |
||||
├── DebugLogStream |
||||
└── StringLogStream |
||||
``` |
||||
|
||||
"LogStream" is an abstract class with a pure virtual function named "write", to |
||||
be used as the interface. We need both variants of the write function in order |
||||
to also print extended ASCII characters. |
||||
|
||||
```cpp |
||||
class LogStream { |
||||
public: |
||||
virtual void write(const char* characters, int length) const = 0; |
||||
virtual void write(const unsigned char* characters, int length) const = 0; |
||||
}; |
||||
``` |
||||
|
||||
To extend the functionality of the "LogStream" class, we override the bitwise |
||||
left shift `<<` operator. Support for user-defined types is added in the same way. |
||||
|
||||
```cpp |
||||
const LogStream& operator<<(const LogStream& stream, const std::string& value) |
||||
{ |
||||
stream.write(value.c_str(), value.length()); |
||||
return stream; |
||||
} |
||||
|
||||
const LogStream& operator<<(const LogStream& stream, bool value) |
||||
{ |
||||
return stream << (value ? "true": "false"); |
||||
} |
||||
``` |
||||
|
||||
In order to keep I/O performance high, logging is done in a buffered manner. |
||||
When a "BufferedLogStream" is created it allocates an array of 128 bytes in |
||||
length on the stack, which you can then write to. If the buffer size is |
||||
exceeded, the buffer grows automatically to the required size in chunks of 128, |
||||
this time on the heap. |
||||
|
||||
```cpp |
||||
// Append to buffer |
||||
memcpy(buffer() + m_count, characters, length); |
||||
|
||||
// Buffer is increased in chunks of 128 bytes |
||||
size_t newCapacity = (m_count + bytes + BUFFER_SIZE - 1) & ~(BUFFER_SIZE - 1); |
||||
``` |
||||
|
||||
The "DebugLogStream" class is used to display messages. It has two variables to |
||||
configure this behavior, "newline" and "type", to indicate if a newline should |
||||
be printed and the color of the message. When the object goes out of scope, it |
||||
will print everything that was added to the buffer in one go. |
||||
|
||||
```cpp |
||||
fwrite(buffer(), 1, count(), stdout); |
||||
``` |
||||
|
||||
Now we get to the actual usage of the system. Using helper functions, printing |
||||
colored and formatted messages is easy. Each helper function also has a |
||||
non-newline variant as newlines are optional. They simply create a |
||||
"DebugLogStream" and return it. |
||||
|
||||
```cpp |
||||
DebugLogStream dbg(bool newline); |
||||
DebugLogStream info(bool newline); |
||||
DebugLogStream warn(bool newline); |
||||
DebugLogStream danger(bool newline); |
||||
DebugLogStream success(bool newline); |
||||
|
||||
dbg() << "Hello World! " << 2.5 << " " << true; |
||||
info() << "This will be printed in blue."; |
||||
``` |
||||
|
||||
The printf style logging is more complicated than the previous use case. This is |
||||
because the function needs to be able to process any number of arguments and of |
||||
any type, this is accomplished using a template parameter pack. We use recursion |
||||
to loop through all the parameters in this pack, with an overload for when there |
||||
is no parameter expansion. Because the "DebugLogStream" is used for printing, |
||||
all the type overloads are available to us, so we don't have to specify the type |
||||
like usual with `printf`. |
||||
|
||||
```cpp |
||||
void dbgln(Log type, bool newline); |
||||
void dbgln(Log type, bool newline, const char* format); |
||||
|
||||
template<typename T, typename... P> |
||||
void dbgln(Log type, bool newline, const char* format, T value, P&&... parameters) |
||||
{ |
||||
std::string_view view { format }; |
||||
|
||||
for(uint32_t i = 0; format[i] != '\0'; i++) { |
||||
|
||||
if (format[i] == '{' && format[i + 1] == '}') { |
||||
DebugLogStream(type, false) << view.substr(0, i) << value; |
||||
dbgln(type, newline, format + i + 2, parameters...); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
This style of logging also has a bunch of helper functions to make using them |
||||
quick and easy. |
||||
|
||||
```cpp |
||||
template<typename... P> void dbgln(const char* format, P&&... parameters) { dbgln(Log::None, true, format, std::forward<P>(parameters)...); } |
||||
template<typename... P> void infoln(const char* format, P&&... parameters) { dbgln(Log::Info, true, format, std::forward<P>(parameters)...); } |
||||
template<typename... P> void warnln(const char* format, P&&... parameters) { dbgln(Log::Warn, true, format, std::forward<P>(parameters)...); } |
||||
template<typename... P> void dangerln(const char* format, P&&... parameters) { dbgln(Log::Danger, true, format, std::forward<P>(parameters)...); } |
||||
template<typename... P> void successln(const char* format, P&&... parameters) { dbgln(Log::Success, true, format, std::forward<P>(parameters)...); } |
||||
|
||||
dbgln("Hello {}, print anything! {} {}", "World", 2.5, true); |
||||
dangerln("This will print in {}.", "red"); |
||||
``` |
||||
|
||||
Finally, "StringLogStream" is used to convert any supported type into a |
||||
std::string. This is achieved by simply converting the buffer in |
||||
"BufferedLogStream" to a string and setting the provided string to it when the |
||||
object goes out of scope. |
||||
|
||||
```cpp |
||||
StringLogStream str(std::string* fill); |
||||
|
||||
std::string result; |
||||
str(&result) << "Add " << "anything " << 2.5 << " " << false; |
||||
``` |
||||
|
||||
### Shaders |
||||
|
||||
"Shader" functionality is split into two classes, "Shader" and "ShaderManager". |
||||
The manager does exactly what the name would suggest, it manages the resources. |
||||
Using convenient functions you can `add`, `load`, `check existence`, `remove` |
||||
shaders. The shaders get stored in a hash table (hash map), with the key being |
||||
its name and the value a `std::shared_ptr` to the shader object. Adding anything |
||||
to the manager that has already been loaded will simply return the existing |
||||
instance, to prevent duplication. The other pairs "Texture/TextureManager", |
||||
"Font/FontManager" and "Gltf/GltfManager" are structured similarly. |
||||
|
||||
```cpp |
||||
void add(const std::string& name, const std::shared_ptr<Shader>& shader); |
||||
std::shared_ptr<Shader> load(const std::string& name); |
||||
std::shared_ptr<Shader> load(const std::string& vertexSource, |
||||
const std::string& fragmentSource); |
||||
bool exists(const std::string& name); |
||||
|
||||
void remove(const std::string& name); |
||||
void remove(const std::shared_ptr<Shader>& shader); |
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Shader>> m_shaderList; |
||||
``` |
||||
|
||||
To construct a "Shader", only a name needs to be provided. It will then load, |
||||
compile and link both the vertex and fragment shader files. Any errors like |
||||
files not existing or GLSL syntax errors will be printed to the console. |
||||
|
||||
```cpp |
||||
// Get file contents |
||||
std::string vertexSrc = File::read(name + ".vert"); |
||||
std::string fragmentSrc = File::read(name + ".frag"); |
||||
|
||||
// Compile shaders |
||||
uint32_t vertexID = compileShader(GL_VERTEX_SHADER, vertexSrc.c_str()); |
||||
uint32_t fragmentID = compileShader(GL_FRAGMENT_SHADER, fragmentSrc.c_str()); |
||||
|
||||
// Link shaders |
||||
if (vertexID > 0 && fragmentID > 0) { |
||||
m_id = linkShader(vertexID, fragmentID); |
||||
} |
||||
``` |
||||
|
||||
An uniform is a global "Shader" variable, to set uniform data, there are |
||||
functions for the most common used data types. ASSERT is a wrapper for `dbgln`, |
||||
which can print any registered data types, explained in the |
||||
[Logging](#logging) |
||||
section. |
||||
|
||||
```cpp |
||||
int32_t findUniform(const std::string& name) const; |
||||
|
||||
void setInt(const std::string& name, int value); |
||||
void setInt(const std::string& name, int* values, uint32_t count); |
||||
void setFloat(const std::string& name, float value) const; |
||||
void setFloat(const std::string& name, float v1, float v2, float v3, float v4) const; |
||||
void setFloat(const std::string& name, glm::vec2 value) const; |
||||
void setFloat(const std::string& name, glm::vec3 value) const; |
||||
void setFloat(const std::string& name, glm::vec4 value) const; |
||||
void setFloat(const std::string& name, glm::mat3 matrix) const; |
||||
void setFloat(const std::string& name, glm::mat4 matrix) const; |
||||
|
||||
void bind() const; |
||||
void unbind() const; |
||||
|
||||
int32_t Shader::findUniform(const std::string& name) const |
||||
{ |
||||
int32_t location = glGetUniformLocation(m_id, name.c_str()); |
||||
ASSERT(location != -1, "Shader could not find uniform '{}'", name); |
||||
return location; |
||||
} |
||||
|
||||
void Shader::setInt(const std::string& name, int value) |
||||
{ |
||||
// Set unifrom int |
||||
glUniform1i(findUniform(name), value); |
||||
} |
||||
``` |
||||
|
||||
In the renderer we only need to do the following. The `the` function in |
||||
"ShaderManager" is a static function that gets the instance of the singleton. |
||||
|
||||
```cpp |
||||
m_shader = ShaderManager::the().load("assets/glsl/batch-quad"); |
||||
|
||||
m_shader->bind(); |
||||
m_shader->setFloat("u_projectionView", cameraProjectionView); |
||||
m_shader->unbind(); |
||||
``` |
||||
|
||||
### Buffers |
||||
|
||||
Rendering in OpenGL is done using a set of two buffers, the vertex buffer and |
||||
the index buffer. The vertex buffer is used to store geometry data, in the |
||||
format of points. From these points, you can construct triangles, which can |
||||
actually be rendered. The constructed triangles are stored as indexes to points |
||||
in the index buffer, the simplest form of an index buffer of a single triangle |
||||
looks like the following: `[0, 1, 2]`. When you have these two buffers set up |
||||
correctly, you can draw a triangle. |
||||
|
||||
```cpp |
||||
void RenderCommand::drawIndexed(const VertexArray& vertexArray, uint32_t indexCount) |
||||
{ |
||||
uint32_t count = indexCount ? indexCount : vertexArray.getIndexBuffer()->getCount(); |
||||
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr); |
||||
} |
||||
``` |
||||
|
||||
To create a vertex buffer a lot of manual setup is needed. This includes offset |
||||
calculation, which is very prone to errors, so these steps are abstracted away |
||||
in the "VertexBuffer" class. A "VertexBuffer" object stores a "BufferLayout", |
||||
which stores a `std::vector` of "BufferElements". "BufferElements" are what |
||||
OpenGL calls vertex attributes, which allows us to specify any input we want, |
||||
because of this we also have to specify how the data should be interpreted. The |
||||
"BufferElement" objects hold the variables `type`, the types `size`, `offset` in |
||||
the buffer and if the data is normalized, these can be accessed via getter |
||||
functions. Now for the fun part of vertex buffers, the "BufferLayout", via a |
||||
`std::initializer_list` they can be constructed very easily. |
||||
|
||||
```cpp |
||||
BufferLayout::BufferLayout(const std::initializer_list<BufferElement>& elements) |
||||
: m_elements(elements) |
||||
{ |
||||
calculateOffsetsAndStride(); |
||||
} |
||||
|
||||
void BufferLayout::calculateOffsetsAndStride() |
||||
{ |
||||
m_stride = 0; |
||||
for (auto& element : m_elements) { |
||||
element.setOffset(m_stride); |
||||
m_stride += element.getSize(); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Because of OpenGL's C-like API, rendering requires manual binding of the vertex |
||||
attribute configurations and vertex buffers, which is a lot of boilerplate. In |
||||
order to simplify this, a new concept was added called a vertex array object |
||||
(also known as VAO), which is actually required in OpenGL's core profile. VAOs |
||||
store the vertex attribute configuration and the associated vertex buffers, they |
||||
are abstracted away in the "VertexArray" class which stores the "VertexBuffers" |
||||
and an "IndexBuffer". When adding a "VertexBuffer" to a "VertexArray", it will |
||||
set up the vertex attribute configuration. |
||||
|
||||
```cpp |
||||
void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer) |
||||
{ |
||||
const auto& layout = vertexBuffer->getLayout(); |
||||
ASSERT(layout.getElements().size(), "VertexBuffer has no layout"); |
||||
|
||||
bind(); |
||||
vertexBuffer->bind(); |
||||
|
||||
uint32_t index = 0; |
||||
for (const auto& element : layout) { |
||||
glEnableVertexAttribArray(index); |
||||
glVertexAttribPointer( |
||||
index, |
||||
element.getTypeCount(), |
||||
element.getTypeGL(), |
||||
element.getNormalized() ? GL_TRUE : GL_FALSE, |
||||
layout.getStride(), |
||||
reinterpret_cast<const void*>(element.getOffset())); |
||||
|
||||
index++; |
||||
} |
||||
m_vertexBuffers.push_back(std::move(vertexBuffer)); |
||||
|
||||
unbind(); |
||||
vertexBuffer->unbind(); |
||||
} |
||||
``` |
||||
|
||||
The final usage looks like the following, notice how the layout is created |
||||
easily using the `std::initializer_list` in the `setLayout` function. |
||||
|
||||
```cpp |
||||
// Create vertex array |
||||
m_vertexArray = std::make_shared<VertexArray>(); |
||||
|
||||
// Create vertex buffer |
||||
auto vertexBuffer = std::make_shared<VertexBuffer>(sizeof(QuadVertex) * vertexCount); |
||||
vertexBuffer->setLayout({ |
||||
{ BufferElementType::Vec3, "a_position" }, |
||||
{ BufferElementType::Vec4, "a_color" }, |
||||
{ BufferElementType::Vec2, "a_textureCoordinates" }, |
||||
{ BufferElementType::Float, "a_textureIndex" }, |
||||
}); |
||||
m_vertexArray->addVertexBuffer(vertexBuffer); |
||||
|
||||
uint32_t indices[] { 0, 1, 2, 2, 3, 0 }; |
||||
|
||||
// Create index buffer |
||||
auto indexBuffer = std::make_shared<IndexBuffer>(indices, sizeof(uint32_t) * indexCount); |
||||
m_vertexArray->setIndexBuffer(indexBuffer); |
||||
``` |
||||
|
||||
## Design Structure |
||||
|
||||
Pictured below are all the classes currently in the codebase and the |
||||
relationships between them. |
||||
|
||||
 |
||||
|
||||
## Preview |
||||
|
||||
There isn't much visual to show as of now. |
||||
|
||||
 |
@ -0,0 +1,63 @@
|
||||
--- |
||||
title: "Manafiles" |
||||
description: "Config file and package tracking utility." |
||||
navigation: false |
||||
date: "2021-09-04" |
||||
img: "/img/manafiles-unit-test.png" |
||||
tags: |
||||
- C++20 |
||||
- CMake |
||||
- Software |
||||
--- |
||||
|
||||
<small>Config file and package tracking utility.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/manafiles){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/manafiles){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/manafiles){target="_blank"}. |
||||
</small> |
||||
|
||||
Written in C++20, using the build tool CMake. |
||||
|
||||
The goal of this project is simple reproducibility of a Linux system, without |
||||
using symlinks. This is achieved by managing configuration files and keeping |
||||
track of what packages were installed. |
||||
|
||||
In order to use the same bundle of files for multiple systems, the program |
||||
allows to specify variables inside of the configuration files. These |
||||
configuration lines will then get commented or uncommented when pushing the |
||||
configuration to the system, depending on the value of the variables. The |
||||
variables that are supported are the `distribution` the `hostname`, the |
||||
`username`, and the display `session`, which is either X.Org or Wayland. |
||||
|
||||
Below an example of a variable block, where I set the amount of jobs the |
||||
compiler will use, depending on the hostname, because my desktop has more cores |
||||
than my laptop. |
||||
|
||||
``` |
||||
# >>> hostname=arch-desktop |
||||
MAKEFLAGS="-j8" |
||||
# <<< |
||||
# >>> hostname=arch-laptop |
||||
# MAKEFLAGS="-j4" |
||||
# <<< |
||||
``` |
||||
|
||||
List of features: |
||||
|
||||
- Manage dotfiles and system config files. |
||||
- Selectively comment and uncomment depending on machine configuration. |
||||
- Store a list of all installed packages. |
||||
- Install packages from a stored list. |
||||
- Pattern matching in the config file and cli arguments. |
||||
- Test suite with unit tests, using my own macros. |
||||
|
||||
Pictured below is the output of running the test suite: |
||||
|
||||
<div class="row"> |
||||
<div class="col-6"> |
||||
|
||||
 |
||||
|
||||
</div> |
||||
</div> |
@ -0,0 +1,36 @@
|
||||
--- |
||||
title: "OpenGL Test" |
||||
description: "OpenGL test." |
||||
navigation: false |
||||
date: "2021-02-20" |
||||
img: "/img/opengl-test-preview.png" |
||||
tags: |
||||
- C++14 |
||||
- GLSL |
||||
- CMake |
||||
- Software |
||||
--- |
||||
|
||||
<small>Config file and package tracking utility.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/opengl-test){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/opengl-test){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/opengl-test){target="_blank"}. |
||||
</small> |
||||
|
||||
Written in C++ with glad, glfw, glm and stb, using the build tool CMake. |
||||
Created for the Computer Graphics course at Hogeschool Rotterdam. |
||||
|
||||
Design structure. |
||||
|
||||
 |
||||
|
||||
Preview. |
||||
|
||||
 |
||||
|
||||
Demo showcasing translation, rotation, scaling and textures working. You can |
||||
also fly around, which is not shown in the video. |
||||
|
||||
::VideoLazy{:src="https://riyyi.com/media/opengl-test-demo.webm"} |
||||
:: |
@ -1,12 +0,0 @@
|
||||
--- |
||||
title: "My Second Blog Post" |
||||
description: "This is another article." |
||||
navigation: false |
||||
date: "2025-03-02" |
||||
img: "/img/personal-website/reset-password.png" |
||||
tags: |
||||
- Bash |
||||
- Hardware |
||||
--- |
||||
|
||||
This is another article. |
@ -0,0 +1,28 @@
|
||||
--- |
||||
title: "Simple 2D RPG" |
||||
description: "Simple 2D RPG." |
||||
navigation: false |
||||
date: "2015-03-09" |
||||
img: "/img/simple-2d-rpg.png" |
||||
tags: |
||||
- C++11 |
||||
- CMake |
||||
- Software |
||||
--- |
||||
|
||||
<small>Simple 2D RPG.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/rpg){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/rpg){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/rpg){target="_blank"}. |
||||
</small> |
||||
|
||||
Written in C++ with SFML and RapidJSON, using the build tool CMake. |
||||
|
||||
This is a very old project, the only things that are implemented are walking and |
||||
tile collision. Mainly used for exploring the SFML library and Tiled tool for |
||||
level design. |
||||
|
||||
Preview. |
||||
|
||||
 |
@ -0,0 +1,65 @@
|
||||
--- |
||||
title: "Space Walk" |
||||
description: "Space Walk board game." |
||||
navigation: false |
||||
date: "2021-02-18" |
||||
img: "/img/space-walk/phase-1.png" |
||||
tags: |
||||
- C++11 |
||||
- Software |
||||
--- |
||||
|
||||
<small>Board game.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/space-walk){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/space-walk){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/space-walk){target="_blank"}. |
||||
</small> |
||||
|
||||
This is an implementation of the [Space |
||||
Walk](https://mancala.fandom.com/wiki/Space_Walk){target="_blank"} board game, |
||||
written in C++ with ncurses for the UI, built using GNU Make. Created for the |
||||
C++ course at Hogeschool Rotterdam. |
||||
|
||||
UML design. |
||||
 |
||||
|
||||
<div class="row"> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Title screen |
||||
 |
||||
|
||||
</div> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Setting player names. |
||||
 |
||||
|
||||
</div> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Message box informing the players of the current phase. |
||||
 |
||||
|
||||
</div> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Players take turns putting their space ships onto the planets. |
||||
 |
||||
|
||||
</div> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Message box informing the players of the current phase. |
||||
 |
||||
|
||||
</div> |
||||
<div class="col-12 col-lg-6"> |
||||
|
||||
Players take turns evacuating the planets, where ships that pass the edges of |
||||
the board will get sucked into black holes (top and bottom square). |
||||
 |
||||
|
||||
</div> |
||||
</div> |
@ -0,0 +1,200 @@
|
||||
--- |
||||
title: "Utility Library" |
||||
description: "Utility library." |
||||
navigation: true |
||||
date: "2022-08-17" |
||||
img: "/img/ruc-example-unit-test.png" |
||||
tags: |
||||
- C++20 |
||||
- CMake |
||||
- Software |
||||
--- |
||||
|
||||
<small>Utility library.<br> |
||||
Repository at |
||||
[GitHub](https://github.com/riyyi/ruc){target="_blank"}, |
||||
[GitLab](https://gitlab.com/riyyi/ruc){target="_blank"} or |
||||
[Gitea](https://git.riyyi.com/riyyi/ruc){target="_blank"}. |
||||
</small> |
||||
|
||||
C++20 utility library without any dependencies, using build tool CMake. |
||||
|
||||
This is an attempt at deduplicating all the commonly used functionality across |
||||
my projects and create one cohesive style. |
||||
|
||||
## Argument parsing |
||||
|
||||
placeholder |
||||
|
||||
## Formatting library |
||||
|
||||
placeholder |
||||
|
||||
## JSON parsing |
||||
|
||||
This is a full implementation of the JSON |
||||
[RFC 7159](https://datatracker.ietf.org/doc/html/rfc7159) |
||||
specification. Created mostly |
||||
for fun, but also for the convenient API. |
||||
|
||||
First, lets specify some JSON that we want to parse. |
||||
|
||||
```js |
||||
{ |
||||
"window": { |
||||
"title": "Inferno", |
||||
"width": 1280 |
||||
"height": 720, |
||||
"fullscreen": "windowed", |
||||
"vsync": false, |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Then, define the structs the JSON will get serialized into. |
||||
```cpp |
||||
struct WindowProperties { |
||||
std::string title { "Inferno" }; |
||||
uint32_t width { 1280 }; |
||||
uint32_t height { 720 }; |
||||
std::string fullscreen { "borderless" }; |
||||
bool vsync { true }; |
||||
}; |
||||
|
||||
struct SettingsProperties { |
||||
WindowProperties window; |
||||
}; |
||||
``` |
||||
|
||||
To deserialize the JSON into the struct defined above, we first need to read the |
||||
contents of the JSON file. Then we call the parse function on it, which will |
||||
return an instance of the base class of the JSON library. Calling the get |
||||
function on this instance will try to convert it to the specified type, using |
||||
user-declared conversion functions, more on this later. |
||||
|
||||
```cpp |
||||
ruc::Json object = ruc::Json::parse(ruc::File("assets/settings.json").data()); |
||||
|
||||
if (object.type() != ruc::Json::Type::Object) { |
||||
ruc::warn("Settings invalid formatting"); |
||||
return false; |
||||
} |
||||
|
||||
SettingsProperties properties = object.get<SettingsProperties>(); |
||||
``` |
||||
|
||||
Serializing back into JSON is simple: just create an instance of the JSON base |
||||
class, whose constructor takes in any type. After, call the dump function on it. |
||||
|
||||
```cpp |
||||
ruc::Json object = properties; |
||||
|
||||
auto file = ruc::File("assets/settings.json"); |
||||
file.clear(); |
||||
file.append(object.dump(1, '\t')); |
||||
file.append("\n"); |
||||
file.flush(); |
||||
``` |
||||
|
||||
So how does this work, how are the JSON objects mapped to the C++ instances? The |
||||
library is using a [clever |
||||
trick](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html){target="_blank"} |
||||
with Argument-Dependent Lookup (ADL). "Lookup" refers to name lookup, which is |
||||
the process a C++ compiler uses to resolve identifiers to their declarations. We |
||||
let the compiler search for a function of a specific shape, anywhere in the |
||||
project, that allows the library to find functions declared by the user of that |
||||
library! What this effectively means is that all a user has to do is declare a |
||||
`to` and `from` conversion function anywhere in his project and these will get |
||||
used automatically by the library. |
||||
|
||||
Below the implementation of the window settings. |
||||
|
||||
```cpp |
||||
void fromJson(const ruc::Json& object, WindowProperties& window) |
||||
{ |
||||
VERIFY(object.type() == ruc::Json::Type::Object); |
||||
|
||||
if (object.exists("title")) |
||||
object.at("title").getTo(window.title); |
||||
if (object.exists("width")) |
||||
object.at("width").getTo(window.width); |
||||
if (object.exists("height")) |
||||
object.at("height").getTo(window.height); |
||||
if (object.exists("fullscreen")) |
||||
object.at("fullscreen").getTo(window.fullscreen); |
||||
if (object.exists("vsync")) |
||||
object.at("vsync").getTo(window.vsync); |
||||
} |
||||
|
||||
void toJson(ruc::Json& object, const WindowProperties& window) |
||||
{ |
||||
object = ruc::Json { |
||||
{ "title", window.title }, |
||||
{ "width", window.width }, |
||||
{ "height", window.height }, |
||||
{ "fullscreen", window.fullscreen }, |
||||
{ "vsync", window.vsync }, |
||||
}; |
||||
} |
||||
|
||||
void fromJson(const ruc::Json& object, SettingsProperties& settings) |
||||
{ |
||||
VERIFY(object.type() == ruc::Json::Type::Object); |
||||
|
||||
if (object.exists("window")) |
||||
object.at("window").getTo(settings.window); |
||||
} |
||||
|
||||
void toJson(ruc::Json& object, const SettingsProperties& settings) |
||||
{ |
||||
object = ruc::Json { |
||||
{ "window", settings.window } |
||||
}; |
||||
} |
||||
``` |
||||
|
||||
## Unit test macros |
||||
|
||||
These are some macros to quickly setup a unit test. The usage is made very |
||||
simple, as there is no need to setup a main function entrypoint. |
||||
|
||||
To accomplish this, the macro has a trick to declare the unit test inside of a |
||||
struct and that also has a static variable of that struct type. Because its |
||||
static it will get allocated on program startup and in the constructor of the |
||||
struct it will register the unit test function in the `TestSuite` class. |
||||
|
||||
What this effectively means, is that after the CMake configuration is setup, all |
||||
a user has to do is create a `.cpp` file and put a test inside of it with the |
||||
`TEST_CASE` macro. |
||||
|
||||
```cpp |
||||
#include "macro.h" |
||||
#include "testcase.h" |
||||
#include "testsuite.h" |
||||
|
||||
TEST_CASE(ExampleUnitTest) |
||||
{ |
||||
// Test a boolean value |
||||
EXPECT(true); |
||||
} |
||||
|
||||
TEST_CASE(ExampleUnitTestTrue) |
||||
{ |
||||
// Test 2 values, true |
||||
int leftside = 3; |
||||
int rightside = 3; |
||||
EXPECT_EQ(leftside, rightside); |
||||
} |
||||
|
||||
TEST_CASE(ExampleUnitTestFalse) |
||||
{ |
||||
// Test 2 values, false |
||||
int leftside = 3; |
||||
int rightside = 5; |
||||
EXPECT_EQ(leftside, rightside); |
||||
} |
||||
``` |
||||
|
||||
Output of the testcases above: |
||||
|
||||
 |
After Width: | Height: | Size: 582 KiB |
After Width: | Height: | Size: 776 KiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 461 KiB |
@ -0,0 +1,57 @@
|
||||
<template> |
||||
<video ref="videoRef" class="w-100" controls> |
||||
<source :src="loadedSrc" type="video/webm"> |
||||
Your browser does not support the video tag. |
||||
</video> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { onMounted, onUnmounted } from "vue" |
||||
|
||||
const props = defineProps({ |
||||
src: { |
||||
type: String, |
||||
default: "" |
||||
}, |
||||
}); |
||||
|
||||
const loadedSrc = ref<string>(); |
||||
const refinedSrc = computed(() => { |
||||
return getPublicPath(props.src); |
||||
}); |
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null) |
||||
|
||||
function lazyLoadVideos() { |
||||
const video = videoRef.value; |
||||
if (!video || !isInViewport(video)) return; |
||||
|
||||
removeEvents(); |
||||
loadedSrc.value = refinedSrc.value; |
||||
video.load(); |
||||
} |
||||
|
||||
function removeEvents() { |
||||
window.removeEventListener("scroll", lazyLoadVideos); |
||||
window.removeEventListener('resize', lazyLoadVideos); |
||||
} |
||||
|
||||
onMounted(() => { |
||||
setTimeout(() => { |
||||
window.addEventListener("scroll", lazyLoadVideos); |
||||
window.addEventListener("resize", lazyLoadVideos); |
||||
lazyLoadVideos(); // trigger immediately |
||||
}, 500); |
||||
}); |
||||
|
||||
onUnmounted(() => { |
||||
removeEvents(); |
||||
}); |
||||
</script> |
||||
|
||||
<!-- |
||||
Usage: |
||||
|
||||
::VideoLazy{:src="/img/path-to-video.webm"} |
||||
:: |
||||
--> |