Compare commits

..

10 Commits

Author SHA1 Message Date
Pablo Rodriguez
7b0c9aad02 Switched to rotating cube 2025-07-31 07:12:50 -04:00
Pablo Rodriguez
ffe7731f10 Added some comments 2025-07-31 02:09:45 -04:00
Pablo Rodriguez
40b7639c66 Added some transformations 2025-07-31 00:46:52 -04:00
Pablo Rodriguez
651186f7bd Implemented textures 2025-07-30 08:16:37 -04:00
Pablo Rodriguez
aebd69a027 Minor refactoring 2025-07-29 17:17:28 -04:00
Pablo Rodriguez
472d35497d Implemented logging utilities 2025-07-28 08:29:53 -04:00
Pablo Rodriguez
9a38c557f0 Moved shaders to standalone source file 2025-07-28 05:22:56 -04:00
Pablo Rodriguez
6d8bf7c563 Implemented ShaderProgramBuilder 2025-07-27 16:13:48 -04:00
Pablo Rodriguez
e5ba7474e6 hello triangle 2025-07-27 13:58:37 -04:00
Pablo Rodriguez
06a9f74f2c Blank window 2025-07-27 12:38:06 -04:00
23 changed files with 8615 additions and 7 deletions

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"C_Cpp.default.cppStandard": "c++20",
"cmake.debugConfig": {
"cwd": "${workspaceFolder}"
}
}

View File

@@ -1,5 +1,15 @@
cmake_minimum_required(VERSION 3.31)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
project(Ugly)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
find_package(glm CONFIG REQUIRED)
add_subdirectory(./source/app)
add_subdirectory(./source/image)
add_subdirectory(./source/log)
add_subdirectory(./source/shader)

View File

@@ -6,7 +6,10 @@
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/build",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_VS_DEBUGGER_WORKING_DIRECTORY": "${sourceDir}"
}
}
]
}

View File

@@ -0,0 +1,9 @@
#version 150 core
in vec3 color;
out vec4 outColor;
void main() {
outColor = vec4(color, 1.0);
}

View File

@@ -0,0 +1,13 @@
#version 150 core
in vec3 inPosition;
in vec3 inColor;
out vec3 color;
uniform mat4 inTransformation;
void main() {
color = inColor;
gl_Position = inTransformation * vec4(inPosition, 1.0);
}

View File

@@ -1,3 +1,19 @@
project(UglyMain)
add_executable(${PROJECT_NAME} main.c)
add_executable(${PROJECT_NAME}
./main.cpp
./src/App.cpp
)
target_include_directories(${PROJECT_NAME}
PRIVATE ./src
)
target_link_libraries(${PROJECT_NAME}
GLEW::GLEW
glfw
glm::glm
stb_image
UglyLogLib
UglyShaderLib
)

View File

@@ -1,5 +0,0 @@
#include <stdio.h>
int main(void) {
printf("hello world!\n");
}

12
source/app/main.cpp Normal file
View File

@@ -0,0 +1,12 @@
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
#include "App.hpp"
int main(int argc, char *argv[]) {
auto args = std::vector<std::string>{};
std::copy_n(argv, argc, std::back_inserter(args));
return ugly::App::main(args);
}

197
source/app/src/App.cpp Normal file
View File

@@ -0,0 +1,197 @@
#include "App.hpp"
#include <array>
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "LogUtils.hpp"
#include "ShaderProgramBuilder.hpp"
#include "stb_image.h"
#include "TimestampLog.hpp"
constexpr std::array<std::array<float, 6>, 36> getCubeVertices();
namespace ugly {
int App::main(std::vector<std::string> &args) {
// Initialize global log.
auto cerrLog = TimestampLog{"ugly::log", std::cerr};
log = LogAlias{&cerrLog};
log("hello, world! app started.");
// Initialize GLFW.
glfwSetErrorCallback(error_callback_s);
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
// Create GLFW window and OpenGL context for GLEW.
auto window = glfwCreateWindow(640, 480, "uGLy", NULL, NULL);
glfwMakeContextCurrent(window);
glewExperimental = GL_TRUE;
if(glewInit() != GLEW_OK) {
log("glewInit() failed!");
}
// Run app.
auto app = App{window};
app.game_loop();
// Clean up.
glfwTerminate();
glfwSetErrorCallback(NULL);
log("app quitting.");
return 0;
}
void App::error_callback_s(int error, const char *description)
{
log("(glfw error): {}", description);
}
void App::key_callback_s(GLFWwindow *window, int key, int scancode, int action, int mods) {
auto app = static_cast<App*>(glfwGetWindowUserPointer(window));
app->key_callback(key, scancode, action, mods);
}
App::App(GLFWwindow *window):
mpWindow{window},
mShaderProgram{} {
// To allow key_callback_s to access member functions
glfwSetWindowUserPointer(mpWindow, this);
glfwSetKeyCallback(mpWindow, key_callback_s);
// Prepare shader
mShaderProgram = ShaderProgramBuilder{}
.attachFromFile(GL_VERTEX_SHADER, "assets/shaders/shader.vert")
.attachFromFile(GL_FRAGMENT_SHADER, "assets/shaders/shader.frag")
.link();
GLint posAttrib = glGetAttribLocation(mShaderProgram, "inPosition");
GLint colorAttrib = glGetAttribLocation(mShaderProgram, "inColor");
glCreateVertexArrays(1, &mVAO);
glBindVertexArray(mVAO);
// Upload vertice attributes (position and color)
const auto vertices = getCubeVertices();
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), (void*)(&vertices), GL_STATIC_DRAW);
// Vertex format = { x, y, z, r, g, b }
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), 0);
glVertexAttribPointer(colorAttrib, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(posAttrib);
glEnableVertexAttribArray(colorAttrib);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void App::game_loop() {
// Generate view and projection tranforms
int width, height;
glfwGetWindowSize(mpWindow, &width, &height);
auto projMatrix = glm::perspective(glm::radians(60.0f), float(width) / height, 0.01f, 1000.f);
auto viewMatrix = glm::lookAt(
glm::vec3{0.f, 0.f, 0.f},
glm::vec3{0.f, 0.f, 1.f},
glm::vec3{0.f, 1.f, 0.f}
);
GLint matrixUniform = glGetUniformLocation(mShaderProgram, "inTransformation");
float startTime = glfwGetTime();
while(!glfwWindowShouldClose(mpWindow)) {
glfwPollEvents();
// Rotate the cube each frame
float dt = glfwGetTime() - startTime;
auto modelMatrix = glm::identity<glm::mat4>();
modelMatrix = glm::translate(modelMatrix, glm::vec3{0.f, 0.f, 2.f});
modelMatrix = glm::rotate(modelMatrix, glm::radians(20.f * dt), glm::vec3{1.f, 1.f, 0.f});
auto finalMatrix = projMatrix * viewMatrix * modelMatrix;
// Draw logic
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glUseProgram(mShaderProgram);
glBindVertexArray(mVAO);
glUniformMatrix4fv(matrixUniform, 1, GL_FALSE, glm::value_ptr(finalMatrix));
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glUseProgram(0);
glDisable(GL_DEPTH_TEST);
glfwSwapBuffers(mpWindow);
}
}
void App::key_callback(int key, int scancode, int action, int mods) {
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
glfwSetWindowShouldClose(mpWindow, GLFW_TRUE);
}
}
}
constexpr std::array<std::array<float, 6>, 36> getCubeVertices() {
// The eight vertices of a cube
const float cubeVertices[8][3] = {
{ -0.5f, -0.5f, -0.5f },
{ -0.5f, +0.5f, -0.5f },
{ +0.5f, +0.5f, -0.5f },
{ +0.5f, -0.5f, -0.5f },
{ -0.5f, -0.5f, +0.5f },
{ -0.5f, +0.5f, +0.5f },
{ +0.5f, +0.5f, +0.5f },
{ +0.5f, -0.5f, +0.5f }
};
// Color for each of the six faces
const float cubeColors[6][3] = {
{ 1.0f, 0.0f, 0.0f },
{ 0.0f, 1.0f, 0.0f },
{ 0.0f, 0.0f, 1.0f },
{ 1.0f, 1.0f, 0.0f },
{ 1.0f, 0.0f, 1.0f },
{ 0.0f, 1.0f, 1.0f }
};
// Each face consists of two triangles, so 36 unique vertices total
const int vertexIndices[36] = {
0, 1, 2, 2, 3, 0, // B
4, 5, 1, 1, 0, 4, // L
7, 6, 5, 5, 4, 7, // F
3, 2, 6, 6, 7, 3, // R
0, 3, 7, 7, 4, 0, // D
2, 1, 5, 5, 6, 2 // U
};
// Construct an array suitable for OpenGL - concatenates position and color for each vertex
// { x, y, z, r, g, b }
std::array<std::array<float, 6>, 36> vertices;
for(int i = 0; i < 36; i++) {
vertices[i][0] = cubeVertices[vertexIndices[i]][0];
vertices[i][1] = cubeVertices[vertexIndices[i]][1];
vertices[i][2] = cubeVertices[vertexIndices[i]][2];
vertices[i][3] = cubeColors[i/6][0];
vertices[i][4] = cubeColors[i/6][1];
vertices[i][5] = cubeColors[i/6][2];
}
return vertices;
}

28
source/app/src/App.hpp Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <vector>
struct GLFWwindow;
namespace ugly {
class App {
public:
static int main(std::vector<std::string> &args);
private:
static void error_callback_s(int error, const char *description);
static void key_callback_s(GLFWwindow *window, int key, int scancode, int action, int mods);
private:
GLFWwindow *mpWindow;
unsigned int mShaderProgram;
unsigned int mVAO;
private:
App(GLFWwindow *window);
void game_loop();
void key_callback(int key, int scancode, int action, int mods);
};
}

View File

@@ -0,0 +1,9 @@
project(stb_image)
add_library(${PROJECT_NAME}
./src/stb_image.c
)
target_include_directories(${PROJECT_NAME}
PUBLIC ./include
)

37
source/image/LICENSE.txt Normal file
View File

@@ -0,0 +1,37 @@
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

11
source/log/CMakeLists.txt Normal file
View File

@@ -0,0 +1,11 @@
project(UglyLogLib)
add_library(${PROJECT_NAME}
./src/globals.cpp
./src/TimestampLog.cpp
)
target_include_directories(${PROJECT_NAME}
PUBLIC ./include
PRIVATE ./src
)

View File

@@ -0,0 +1,22 @@
#pragma once
#include <format>
#include <string_view>
namespace ugly {
/**
* \brief Interface for a generic log-like object.
*/
class ILogFacility {
public:
~ILogFacility() = default;
/**
* \brief Put a message into the log, \c std::vformat -style.
* \param fmt Format string.
* \param args Format arguments.
*/
virtual void vlog(std::string_view fmt, std::format_args args) = 0;
};
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include "ILogFacility.hpp"
#include <format>
namespace ugly {
/**
* \brief Provides \c std::format -style variadic template functions for \ref ILogFacility.
*/
class LogAlias {
public:
ILogFacility *mpLog;
template<typename ...Args>
LogAlias &operator()(std::format_string<Args...> fmt, Args &&...args) {
if(mpLog) {
mpLog->vlog(fmt.get(), std::make_format_args(args...));
}
return *this;
}
};
/**
* \brief Globally-shared log.
*
* Should be assigned to by the user, defaults to a null log.
*/
extern LogAlias log;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "ILogFacility.hpp"
#include <chrono>
#include <format>
#include <ostream>
#include <string_view>
#include <string>
namespace ugly {
/**
* \brief Log that automatically annotates messages with time and name.
*/
class TimestampLog: virtual public ILogFacility {
private:
std::chrono::high_resolution_clock::time_point mStartTime;
std::ostream &mrOutputStream;
std::string mDescription;
public:
/**
* \param description Name of the log to appear in the messages.
* \param os Output stream.
*/
TimestampLog(std::string_view description, std::ostream &os);
void vlog(std::string_view fmt, std::format_args args) override;
};
}

View File

@@ -0,0 +1,23 @@
#include "TimestampLog.hpp"
#include <format>
#include <chrono>
namespace ugly {
TimestampLog::TimestampLog(std::string_view description, std::ostream &os):
mStartTime{std::chrono::high_resolution_clock::now()},
mrOutputStream{os},
mDescription{description} {}
void TimestampLog::vlog(std::string_view fmt, std::format_args args) {
// Format message as "[123.4567] Name: User Message\n"
using namespace std::chrono;
auto now = high_resolution_clock::now();
auto timestamp = duration_cast<duration<float>>(now - mStartTime).count();
auto message = std::vformat(fmt, args);
mrOutputStream << std::format("[{:9.4f}] {}: {}\n", timestamp, mDescription, message);
}
}

View File

@@ -0,0 +1,7 @@
#include "LogUtils.hpp"
namespace ugly {
LogAlias log{nullptr};
}

View File

@@ -0,0 +1,15 @@
project(UglyShaderLib)
add_library(${PROJECT_NAME}
./src/ShaderProgramBuilder.cpp
)
target_include_directories(${PROJECT_NAME}
PUBLIC ./include
PRIVATE ./src
)
target_link_libraries(${PROJECT_NAME}
GLEW::GLEW
UglyLogLib
)

View File

@@ -0,0 +1,55 @@
#pragma once
#include <stdexcept>
#include <GL/glew.h>
namespace ugly{
/**
* \brief Builder class for OpenGL shader and programs.
*
* On initialization, creates a new OpenGL program. Attach and compile shader
* sources using `attachFrom*`. Link and get the program with `link()`. User is
* responsible of managing returned program's lifetime, including destroying it.
*/
class ShaderProgramBuilder {
private:
GLuint mProgram;
public:
ShaderProgramBuilder();
~ShaderProgramBuilder();
/**
* \brief Deletes program and starts from scratch.
*/
void reset();
/**
* \brief Creates, compiles, and attaches a shader.
* \param type Specifies the type of shader to be created. Must be one of
* \c GL_VERTEX_SHADER, \c GL_GEOMETRY_SHADER or
* \c GL_FRAGMENT_SHADER .
* \param string Shader source code.
* \param length Bytes to read from \p string, \c 0 reads until NUL byte.
* \returns *this
*/
ShaderProgramBuilder &attachFromMemory(GLenum type, const GLchar *string, GLint length = 0);
/**
* \brief Creates, compiles, and attaches a shader. Reads source from file.
* \param type Specifies the type of shader to be created. Must be one of
* \c GL_VERTEX_SHADER, \c GL_GEOMETRY_SHADER or
* \c GL_FRAGMENT_SHADER .
* \param filename Shader source code file.
* \returns \c *this
*/
ShaderProgramBuilder &attachFromFile(GLenum type, const char *filename);
/**
* \brief Links OpenGL program after attaching sources.
*
* After this function is called the builder object is reset.
* \return OpenGL object name. User is reponsible for managing it.
*/
[[nodiscard]] GLuint link();
};
}

View File

@@ -0,0 +1,87 @@
#include "ShaderProgramBuilder.hpp"
#include <fstream>
#include <sstream>
#include <utility>
#include "LogUtils.hpp"
namespace ugly {
ShaderProgramBuilder::ShaderProgramBuilder():
mProgram{glCreateProgram()} {}
ShaderProgramBuilder::~ShaderProgramBuilder() {
// link() sets mProgram=0 to prevent it from being deleted after being given to the user.
if(mProgram != 0) {
glDeleteProgram(mProgram);
}
}
void ShaderProgramBuilder::reset() {
using namespace std;
ShaderProgramBuilder newBuilder;
swap(*this, newBuilder);
}
ShaderProgramBuilder &ShaderProgramBuilder::attachFromMemory(GLenum type, const GLchar *string, GLint length) {
auto shader = glCreateShader(type);
glShaderSource(shader, 1, &string, length != 0 ? &length : NULL);
glCompileShader(shader);
// Check for shader compilation errors
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if(status != GL_TRUE) {
char buf[512];
glGetShaderInfoLog(shader, 512, NULL, buf);
glDeleteShader(shader);
log("failed to compile shader:\n\t{}", buf);
}
// After the shader is attached, it can safely be marked for deletion and OpenGL will handle it.
glAttachShader(mProgram, shader);
glDeleteShader(shader);
return *this;
}
ShaderProgramBuilder &ShaderProgramBuilder::attachFromFile(GLenum type, const char *filename) {
// Load entire file to memory then let attachFromMemory handle it.
auto file = std::filebuf{};
auto ss = std::ostringstream{};
log("loading shader from file {}", filename);
file.open(filename, std::ios_base::in);
if(!file.is_open()) {
log("couldn't open file {}", filename);
}
ss << &file;
return attachFromMemory(type, ss.str().c_str());
}
GLuint ShaderProgramBuilder::link() {
glLinkProgram(mProgram);
// Check for program linking errors
GLint status;
glGetProgramiv(mProgram, GL_LINK_STATUS, &status);
if(status != GL_TRUE) {
char buf[512];
glGetProgramInfoLog(mProgram, 512, NULL, buf);
log("failed to link program:\n\t{}", buf);
}
// reset() this object but take care of not deleting the program returned to the caller.
auto result = mProgram;
mProgram = 0;
reset();
return result;
}
}