@ -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"} |
||||||
|
:: |
||||||
|
--> |