From d869edfaa8d4579994dcdcc72d1ee28c0e19a9f1 Mon Sep 17 00:00:00 2001 From: Kiyooru Date: Sun, 2 Aug 2026 17:05:27 +0200 Subject: [PATCH] Initial Vectorail Core library --- .gitignore | 12 +++ CMakeLists.txt | 71 ++++++++++++ LICENSE | 30 ++++++ README.md | 26 +++++ cmake/VectorailCoreConfig.cmake.in | 10 ++ include/vectorail/core/DdsTexture.hpp | 18 ++++ include/vectorail/core/PngTexture.hpp | 15 +++ include/vectorail/core/Shader.hpp | 30 ++++++ include/vectorail/core/Spline.hpp | 36 +++++++ include/vectorail/core/gl_loader.hpp | 119 ++++++++++++++++++++ src/DdsTexture.cpp | 117 ++++++++++++++++++++ src/PngTexture.cpp | 150 ++++++++++++++++++++++++++ src/Shader.cpp | 32 ++++++ src/Spline.cpp | 81 ++++++++++++++ src/gl_loader.cpp | 2 + 15 files changed, 749 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/VectorailCoreConfig.cmake.in create mode 100644 include/vectorail/core/DdsTexture.hpp create mode 100644 include/vectorail/core/PngTexture.hpp create mode 100644 include/vectorail/core/Shader.hpp create mode 100644 include/vectorail/core/Spline.hpp create mode 100644 include/vectorail/core/gl_loader.hpp create mode 100644 src/DdsTexture.cpp create mode 100644 src/PngTexture.cpp create mode 100644 src/Shader.cpp create mode 100644 src/Spline.cpp create mode 100644 src/gl_loader.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b71ebb --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/build/ +/cmake-build-*/ +/.cache/ +/.clangd/ +/compile_commands.json +*.o +*.a +*.so +*.dll +*.dylib +*.exe + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..528625c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.20) + +project(vectorail-core VERSION 0.1.0 LANGUAGES CXX) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +find_package(OpenGL REQUIRED) +find_package(PNG REQUIRED) +find_package(SDL3 CONFIG REQUIRED) +find_package(glm CONFIG REQUIRED) + +add_library(vectorail-core + src/DdsTexture.cpp + src/PngTexture.cpp + src/Shader.cpp + src/Spline.cpp + src/gl_loader.cpp +) +add_library(Vectorail::Core ALIAS vectorail-core) + +target_compile_features(vectorail-core PUBLIC cxx_std_23) +target_include_directories(vectorail-core + PUBLIC + $ + $ +) +target_link_libraries(vectorail-core + PUBLIC + SDL3::SDL3 + OpenGL::GL + PNG::PNG + glm::glm +) + +set_target_properties(vectorail-core PROPERTIES + EXPORT_NAME Core + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +install(TARGETS vectorail-core + EXPORT VectorailCoreTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(EXPORT VectorailCoreTargets + FILE VectorailCoreTargets.cmake + NAMESPACE Vectorail:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailCore +) + +configure_package_config_file( + cmake/VectorailCoreConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/VectorailCoreConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailCore +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/VectorailCoreConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/VectorailCoreConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/VectorailCoreConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailCore +) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8417c4e --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2026, Kiyooru Takasaki +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..cca6b32 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Vectorail Core + +Reusable SDL 3 and OpenGL composition primitives extracted from Vectorail. +The library deliberately contains no game rules and no Groove Coaster format +knowledge. + +## Scope + +- OpenGL function loading and shader helpers; +- spline evaluation used by rail renderers; +- PNG and uncompressed DDS texture upload helpers; +- portable SDL/OpenGL integration shared by applications built on Vectorail. + +## Build + +```sh +cmake -S . -B build +cmake --build build +cmake --install build --prefix /desired/prefix +``` + +Dependencies are SDL 3, OpenGL, libpng, and GLM. Consumers link the exported +`Vectorail::Core` CMake target. + +Vectorail Core is distributed under the BSD 3-Clause License. + diff --git a/cmake/VectorailCoreConfig.cmake.in b/cmake/VectorailCoreConfig.cmake.in new file mode 100644 index 0000000..0927a12 --- /dev/null +++ b/cmake/VectorailCoreConfig.cmake.in @@ -0,0 +1,10 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(OpenGL) +find_dependency(PNG) +find_dependency(SDL3 CONFIG) +find_dependency(glm CONFIG) + +include("${CMAKE_CURRENT_LIST_DIR}/VectorailCoreTargets.cmake") + diff --git a/include/vectorail/core/DdsTexture.hpp b/include/vectorail/core/DdsTexture.hpp new file mode 100644 index 0000000..3129b9f --- /dev/null +++ b/include/vectorail/core/DdsTexture.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +struct DdsTexture { + unsigned int id = 0; + int width = 0; + int height = 0; +}; + +// Groove Coaster's stage/menu atlases use uncompressed RGB(A) DDS surfaces. +// This intentionally small loader accepts those surfaces without introducing +// a conversion step or an external image dependency. +bool loadDdsTexture(const std::string& path, DdsTexture& out, std::string* error = nullptr); +bool loadDdsTextureBytes(const std::vector& bytes, DdsTexture& out, + std::string* error = nullptr); diff --git a/include/vectorail/core/PngTexture.hpp b/include/vectorail/core/PngTexture.hpp new file mode 100644 index 0000000..3b30e88 --- /dev/null +++ b/include/vectorail/core/PngTexture.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "vectorail/core/DdsTexture.hpp" + +#include +#include + +// The arcade effect atlases have a .bin suffix but contain ordinary PNG +// streams. Keep loading based on the file signature rather than its suffix. +bool loadPngTexture(const std::string& path, DdsTexture& out, std::string* error = nullptr); + +// MtxTexture::LoadListData format used by data/skin/skinN/img.dat: a BE +// count/offset table followed by zero or more embedded PNG streams. +bool loadPngTextureList( + const std::string& path, std::vector& out, std::string* error = nullptr); diff --git a/include/vectorail/core/Shader.hpp b/include/vectorail/core/Shader.hpp new file mode 100644 index 0000000..b19a696 --- /dev/null +++ b/include/vectorail/core/Shader.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "vectorail/core/gl_loader.hpp" +#include +#include +#include + +class Shader { +public: + unsigned int ID; + Shader(const char* vertexPath, const char* fragmentPath); + void use() const { glUseProgram(ID); } + void setBool(const std::string& name, bool value) const { + glUniform1i(glGetUniformLocation(ID, (const GLchar*)name.c_str()), (int)value); + } + void setInt(const std::string& name, int value) const { + glUniform1i(glGetUniformLocation(ID, (const GLchar*)name.c_str()), value); + } + void setFloat(const std::string& name, float value) const { + glUniform1f(glGetUniformLocation(ID, (const GLchar*)name.c_str()), value); + } + void setVec3(const std::string& name, const glm::vec3& value) const { + glUniform3fv(glGetUniformLocation(ID, (const GLchar*)name.c_str()), 1, &value[0]); + } + void setVec4(const std::string& name, const glm::vec4& value) const { + glUniform4fv(glGetUniformLocation(ID, (const GLchar*)name.c_str()), 1, &value[0]); + } + void setMat4(const std::string& name, const glm::mat4& mat) const { + glUniformMatrix4fv(glGetUniformLocation(ID, (const GLchar*)name.c_str()), 1, GL_FALSE, glm::value_ptr(mat)); + } +}; diff --git a/include/vectorail/core/Spline.hpp b/include/vectorail/core/Spline.hpp new file mode 100644 index 0000000..d219c0d --- /dev/null +++ b/include/vectorail/core/Spline.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +struct SplinePoint { + glm::vec3 position; + int type; // 0 = Smooth, 1 = Linear +}; + +class Spline { +public: + Spline() = default; + void addPoint(const glm::vec3& pos, int type = 0); + void clear() { points.clear(); LUT.clear(); totalLength = 0; } + + // Получить позицию по физическому расстоянию (метрам) от начала + glm::vec3 getPositionAtDistance(float distance) const; + glm::vec3 getTangentAtDistance(float distance) const; + float getDistanceAtIndex(size_t index) const; + + float getTotalLength() const { return totalLength; } + void rebuildLUT(); // Предрасчет таблицы длин для константной скорости + +private: + std::vector points; + + struct LUTEntry { + float distance; + float t; + }; + std::vector LUT; + float totalLength = 0; + + glm::vec3 getPositionRaw(float t) const; + glm::vec3 getTangentRaw(float t) const; +}; diff --git a/include/vectorail/core/gl_loader.hpp b/include/vectorail/core/gl_loader.hpp new file mode 100644 index 0000000..6252937 --- /dev/null +++ b/include/vectorail/core/gl_loader.hpp @@ -0,0 +1,119 @@ +#ifndef GL_LOADER_HPP +#define GL_LOADER_HPP + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +#include +#include + +#ifndef APIENTRY +#define APIENTRY +#endif + +#ifndef GL_FRAGMENT_SHADER +#define GL_FRAGMENT_SHADER 0x8B30 +#endif +#ifndef GL_VERTEX_SHADER +#define GL_VERTEX_SHADER 0x8B31 +#endif +#ifndef GL_ARRAY_BUFFER +#define GL_ARRAY_BUFFER 0x8892 +#endif +#ifndef GL_STATIC_DRAW +#define GL_STATIC_DRAW 0x88E4 +#endif +#ifndef GL_DYNAMIC_DRAW +#define GL_DYNAMIC_DRAW 0x88E8 +#endif +#ifndef GL_ELEMENT_ARRAY_BUFFER +#define GL_ELEMENT_ARRAY_BUFFER 0x8893 +#endif +#ifndef GL_PROGRAM_POINT_SIZE +#define GL_PROGRAM_POINT_SIZE 0x8642 +#endif + +typedef GLuint (APIENTRY *PFNGLCREATESHADERPROC)(GLenum type); +typedef void (APIENTRY *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length); +typedef void (APIENTRY *PFNGLCOMPILESHADERPROC)(GLuint shader); +typedef GLuint (APIENTRY *PFNGLCREATEPROGRAMPROC)(void); +typedef void (APIENTRY *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader); +typedef void (APIENTRY *PFNGLLINKPROGRAMPROC)(GLuint program); +typedef void (APIENTRY *PFNGLUSEPROGRAMPROC)(GLuint program); +typedef void (APIENTRY *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint* arrays); +typedef void (APIENTRY *PFNGLBINDVERTEXARRAYPROC)(GLuint array); +typedef void (APIENTRY *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint* buffers); +typedef void (APIENTRY *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint* buffers); +typedef void (APIENTRY *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint* arrays); +typedef void (APIENTRY *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer); +typedef void (APIENTRY *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void* data, GLenum usage); +typedef void (APIENTRY *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void* data); +typedef void (APIENTRY *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index); +typedef void (APIENTRY *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer); +typedef GLint (APIENTRY *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar* name); +typedef void (APIENTRY *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0); +typedef void (APIENTRY *PFNGLUNIFORM1IPROC)(GLint location, GLint v0); +typedef void (APIENTRY *PFNGLUNIFORM3FVPROC)(GLint location, GLsizei count, const GLfloat* value); +typedef void (APIENTRY *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value); + +#ifdef GL_LOADER_IMPLEMENTATION +#define GL_EXTERN +#else +#define GL_EXTERN extern +#endif + +GL_EXTERN PFNGLCREATESHADERPROC glCreateShader; +GL_EXTERN PFNGLSHADERSOURCEPROC glShaderSource; +GL_EXTERN PFNGLCOMPILESHADERPROC glCompileShader; +GL_EXTERN PFNGLCREATEPROGRAMPROC glCreateProgram; +GL_EXTERN PFNGLATTACHSHADERPROC glAttachShader; +GL_EXTERN PFNGLLINKPROGRAMPROC glLinkProgram; +GL_EXTERN PFNGLUSEPROGRAMPROC glUseProgram; +GL_EXTERN PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; +GL_EXTERN PFNGLBINDVERTEXARRAYPROC glBindVertexArray; +GL_EXTERN PFNGLGENBUFFERSPROC glGenBuffers; +GL_EXTERN PFNGLDELETEBUFFERSPROC glDeleteBuffers; +GL_EXTERN PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays; +GL_EXTERN PFNGLBINDBUFFERPROC glBindBuffer; +GL_EXTERN PFNGLBUFFERDATAPROC glBufferData; +GL_EXTERN PFNGLBUFFERSUBDATAPROC glBufferSubData; +GL_EXTERN PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray; +GL_EXTERN PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer; +GL_EXTERN PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; +GL_EXTERN PFNGLUNIFORM1FPROC glUniform1f; +GL_EXTERN PFNGLUNIFORM1IPROC glUniform1i; +GL_EXTERN PFNGLUNIFORM3FVPROC glUniform3fv; +GL_EXTERN PFNGLUNIFORM4FVPROC glUniform4fv; +GL_EXTERN PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv; + +inline void load_gl_functions() { + glCreateShader = (PFNGLCREATESHADERPROC)SDL_GL_GetProcAddress("glCreateShader"); + glShaderSource = (PFNGLSHADERSOURCEPROC)SDL_GL_GetProcAddress("glShaderSource"); + glCompileShader = (PFNGLCOMPILESHADERPROC)SDL_GL_GetProcAddress("glCompileShader"); + glCreateProgram = (PFNGLCREATEPROGRAMPROC)SDL_GL_GetProcAddress("glCreateProgram"); + glAttachShader = (PFNGLATTACHSHADERPROC)SDL_GL_GetProcAddress("glAttachShader"); + glLinkProgram = (PFNGLLINKPROGRAMPROC)SDL_GL_GetProcAddress("glLinkProgram"); + glUseProgram = (PFNGLUSEPROGRAMPROC)SDL_GL_GetProcAddress("glUseProgram"); + glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glGenVertexArrays"); + glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)SDL_GL_GetProcAddress("glBindVertexArray"); + glGenBuffers = (PFNGLGENBUFFERSPROC)SDL_GL_GetProcAddress("glGenBuffers"); + glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)SDL_GL_GetProcAddress("glDeleteBuffers"); + glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)SDL_GL_GetProcAddress("glDeleteVertexArrays"); + glBindBuffer = (PFNGLBINDBUFFERPROC)SDL_GL_GetProcAddress("glBindBuffer"); + glBufferData = (PFNGLBUFFERDATAPROC)SDL_GL_GetProcAddress("glBufferData"); + glBufferSubData = (PFNGLBUFFERSUBDATAPROC)SDL_GL_GetProcAddress("glBufferSubData"); + glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)SDL_GL_GetProcAddress("glEnableVertexAttribArray"); + glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)SDL_GL_GetProcAddress("glVertexAttribPointer"); + glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)SDL_GL_GetProcAddress("glGetUniformLocation"); + glUniform1f = (PFNGLUNIFORM1FPROC)SDL_GL_GetProcAddress("glUniform1f"); + glUniform1i = (PFNGLUNIFORM1IPROC)SDL_GL_GetProcAddress("glUniform1i"); + glUniform3fv = (PFNGLUNIFORM3FVPROC)SDL_GL_GetProcAddress("glUniform3fv"); + glUniform4fv = (PFNGLUNIFORM4FVPROC)SDL_GL_GetProcAddress("glUniform4fv"); + glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)SDL_GL_GetProcAddress("glUniformMatrix4fv"); +} + +#endif diff --git a/src/DdsTexture.cpp b/src/DdsTexture.cpp new file mode 100644 index 0000000..b058e42 --- /dev/null +++ b/src/DdsTexture.cpp @@ -0,0 +1,117 @@ +#include "vectorail/core/DdsTexture.hpp" + +#include "vectorail/core/gl_loader.hpp" + +#include +#include +#include +#include + +namespace { + +uint32_t readU32le(const std::vector& bytes, size_t offset) { + return static_cast(bytes[offset + 0]) | + (static_cast(bytes[offset + 1]) << 8) | + (static_cast(bytes[offset + 2]) << 16) | + (static_cast(bytes[offset + 3]) << 24); +} + +uint8_t expandMasked(uint32_t pixel, uint32_t mask, uint8_t fallback) { + if (mask == 0) return fallback; + unsigned shift = 0; + while (((mask >> shift) & 1u) == 0u && shift < 31u) ++shift; + const uint32_t valueMask = mask >> shift; + const uint32_t value = (pixel & mask) >> shift; + return static_cast((value * 255u + valueMask / 2u) / valueMask); +} + +} // namespace + +bool loadDdsTexture(const std::string& path, DdsTexture& out, std::string* error) { + std::ifstream file(path, std::ios::binary); + if (!file) { + if (error) *error = "could not open DDS"; + return false; + } + file.seekg(0, std::ios::end); + const std::streamoff fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + if (fileSize < 128) { + if (error) *error = "DDS header is truncated"; + return false; + } + std::vector bytes(static_cast(fileSize)); + file.read(reinterpret_cast(bytes.data()), fileSize); + if (!file) { + if (error) *error = "could not read DDS"; + return false; + } + return loadDdsTextureBytes(bytes, out, error); +} + +bool loadDdsTextureBytes(const std::vector& bytes, DdsTexture& out, std::string* error) { + out = {}; + if (bytes.size() < 128) { + if (error) *error = "DDS header is truncated"; + return false; + } + if (bytes[0] != 'D' || bytes[1] != 'D' || bytes[2] != 'S' || bytes[3] != ' ') { + if (error) *error = "not a DDS file"; + return false; + } + + const uint32_t headerSize = readU32le(bytes, 4); + const uint32_t height = readU32le(bytes, 12); + const uint32_t width = readU32le(bytes, 16); + const uint32_t pixelFormatSize = readU32le(bytes, 76); + const uint32_t pixelFormatFlags = readU32le(bytes, 80); + const uint32_t fourCC = readU32le(bytes, 84); + const uint32_t bitsPerPixel = readU32le(bytes, 88); + const uint32_t rMask = readU32le(bytes, 92); + const uint32_t gMask = readU32le(bytes, 96); + const uint32_t bMask = readU32le(bytes, 100); + const uint32_t aMask = readU32le(bytes, 104); + if (headerSize != 124 || pixelFormatSize != 32 || width == 0 || height == 0 || + (bitsPerPixel != 24 && bitsPerPixel != 32) || fourCC != 0 || (pixelFormatFlags & 0x40u) == 0) { + if (error) *error = "unsupported DDS pixel format (expected uncompressed RGB24/RGBA32)"; + return false; + } + + const size_t bytesPerPixel = bitsPerPixel / 8; + if (width > std::numeric_limits::max() / height || + static_cast(width) * height > std::numeric_limits::max() / bytesPerPixel) { + if (error) *error = "DDS dimensions overflow"; + return false; + } + const size_t pixelCount = static_cast(width) * height; + const size_t payloadSize = pixelCount * bytesPerPixel; + if (payloadSize > bytes.size() - 128) { + if (error) *error = "DDS pixel payload is truncated"; + return false; + } + + std::vector rgba(pixelCount * 4); + for (size_t i = 0; i < pixelCount; ++i) { + uint32_t pixel = 0; + for (size_t byte = 0; byte < bytesPerPixel; ++byte) { + pixel |= static_cast(bytes[128 + i * bytesPerPixel + byte]) << (byte * 8); + } + rgba[i * 4 + 0] = expandMasked(pixel, rMask, 0); + rgba[i * 4 + 1] = expandMasked(pixel, gMask, 0); + rgba[i * 4 + 2] = expandMasked(pixel, bMask, 0); + rgba[i * 4 + 3] = expandMasked(pixel, aMask, 255); + } + + glGenTextures(1, &out.id); + glBindTexture(GL_TEXTURE_2D, out.id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast(width), static_cast(height), + 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + out.width = static_cast(width); + out.height = static_cast(height); + return true; +} diff --git a/src/PngTexture.cpp b/src/PngTexture.cpp new file mode 100644 index 0000000..1e2b505 --- /dev/null +++ b/src/PngTexture.cpp @@ -0,0 +1,150 @@ +#include "vectorail/core/PngTexture.hpp" + +#include "vectorail/core/gl_loader.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace { + +uint16_t u16be(std::span bytes, size_t offset) { + return static_cast((static_cast(bytes[offset]) << 8) | bytes[offset + 1]); +} + +uint32_t u32be(std::span bytes, size_t offset) { + return (static_cast(bytes[offset]) << 24) | + (static_cast(bytes[offset + 1]) << 16) | + (static_cast(bytes[offset + 2]) << 8) | + static_cast(bytes[offset + 3]); +} + +bool readFile(const std::string& path, std::vector& bytes) { + std::ifstream file(path, std::ios::binary); + if (!file) return false; + file.seekg(0, std::ios::end); + const std::streamoff size = file.tellg(); + if (size < 0) return false; + file.seekg(0, std::ios::beg); + bytes.assign(static_cast(size), 0); + if (!bytes.empty()) file.read(reinterpret_cast(bytes.data()), size); + return static_cast(file) || file.eof(); +} + +struct PngMemoryReader { + std::span bytes; + size_t offset = 0; +}; + +void readPngMemory(png_structp png, png_bytep destination, png_size_t length) { + auto* reader = static_cast(png_get_io_ptr(png)); + if (!reader || length > reader->bytes.size() - reader->offset) { + png_error(png, "truncated PNG stream"); + return; + } + std::memcpy(destination, reader->bytes.data() + reader->offset, length); + reader->offset += length; +} + +bool decodePng(std::span bytes, DdsTexture& out, std::string* error) { + out = {}; + if (bytes.size() < 8 || png_sig_cmp(bytes.data(), 0, 8) != 0) { + if (error) *error = "not a PNG stream"; + return false; + } + + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + png_infop info = png ? png_create_info_struct(png) : nullptr; + if (!png || !info || setjmp(png_jmpbuf(png))) { + if (error) *error = "could not decode PNG"; + if (png) png_destroy_read_struct(&png, info ? &info : nullptr, nullptr); + return false; + } + + PngMemoryReader reader{bytes}; + png_set_read_fn(png, &reader, readPngMemory); + png_read_info(png, info); + const png_uint_32 width = png_get_image_width(png, info); + const png_uint_32 height = png_get_image_height(png, info); + const int colorType = png_get_color_type(png, info); + const int bitDepth = png_get_bit_depth(png, info); + if (bitDepth == 16) png_set_strip_16(png); + if (colorType == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) png_set_expand_gray_1_2_4_to_8(png); + if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png); + if ((colorType & PNG_COLOR_MASK_ALPHA) == 0) png_set_add_alpha(png, 0xff, PNG_FILLER_AFTER); + png_read_update_info(png, info); + + std::vector rgba(static_cast(width) * height * 4); + std::vector rows(height); + for (png_uint_32 y = 0; y < height; ++y) rows[y] = rgba.data() + static_cast(y) * width * 4; + png_read_image(png, rows.data()); + png_destroy_read_struct(&png, &info, nullptr); + + glGenTextures(1, &out.id); + glBindTexture(GL_TEXTURE_2D, out.id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast(width), static_cast(height), + 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + out.width = static_cast(width); + out.height = static_cast(height); + return true; +} + +} // namespace + +bool loadPngTexture(const std::string& path, DdsTexture& out, std::string* error) { + std::vector bytes; + if (!readFile(path, bytes)) { + if (error) *error = "could not open PNG"; + return false; + } + return decodePng(bytes, out, error); +} + +bool loadPngTextureList(const std::string& path, std::vector& out, std::string* error) { + out.clear(); + std::vector bytes; + if (!readFile(path, bytes) || bytes.size() < 10) { + if (error) *error = "could not read texture list"; + return false; + } + const std::span view(bytes); + const uint16_t count = u16be(view, 4); + if (6u + (static_cast(count) + 1u) * 4u > bytes.size()) { + if (error) *error = "invalid texture-list offset table"; + return false; + } + + out.assign(count, {}); + int loaded = 0; + for (uint16_t index = 0; index < count; ++index) { + const size_t begin = u32be(view, 6 + static_cast(index) * 4); + const size_t end = u32be(view, 10 + static_cast(index) * 4); + if (begin == end) continue; + if (begin > end || end > bytes.size()) { + if (error) *error = "invalid embedded PNG range"; + return false; + } + std::string pngError; + if (!decodePng(view.subspan(begin, end - begin), out[index], &pngError)) { + if (error) *error = "texture " + std::to_string(index) + ": " + pngError; + return false; + } + ++loaded; + } + if (loaded == 0) { + if (error) *error = "texture list has no PNG entries"; + return false; + } + return true; +} diff --git a/src/Shader.cpp b/src/Shader.cpp new file mode 100644 index 0000000..536f5d9 --- /dev/null +++ b/src/Shader.cpp @@ -0,0 +1,32 @@ +#include "vectorail/core/Shader.hpp" +#include +#include +#include + +Shader::Shader(const char* vertexPath, const char* fragmentPath) { + std::string vCode, fCode; + std::ifstream vFile(vertexPath), fFile(fragmentPath); + if (!vFile || !fFile) { + std::cerr << "Shader source unavailable: " << vertexPath + << " / " << fragmentPath << std::endl; + } + std::stringstream vStr, fStr; + vStr << vFile.rdbuf(); fStr << fFile.rdbuf(); + vCode = vStr.str(); fCode = fStr.str(); + + const char* vPtr = vCode.c_str(); + const char* fPtr = fCode.c_str(); + + unsigned int v = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(v, 1, &vPtr, NULL); + glCompileShader(v); + + unsigned int f = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(f, 1, &fPtr, NULL); + glCompileShader(f); + + ID = glCreateProgram(); + glAttachShader(ID, v); + glAttachShader(ID, f); + glLinkProgram(ID); +} diff --git a/src/Spline.cpp b/src/Spline.cpp new file mode 100644 index 0000000..7124b09 --- /dev/null +++ b/src/Spline.cpp @@ -0,0 +1,81 @@ +#include "vectorail/core/Spline.hpp" +#include + +void Spline::addPoint(const glm::vec3& pos, int type) { + points.push_back({pos, type}); +} + +void Spline::rebuildLUT() { + LUT.clear(); + totalLength = 0; + if (points.size() < 2) return; + + LUT.push_back({0.0f, 0.0f}); + glm::vec3 prevPos = getPositionRaw(0.0f); + constexpr int samplesPerSegment = 50; // Same 0.02 resolution as before. + for (size_t segment = 0; segment + 1 < points.size(); ++segment) { + for (int sample = 1; sample <= samplesPerSegment; ++sample) { + // Sample every authored segment independently. The old cumulative + // t += 0.02 loop could step from just before an integer boundary + // to just after it and measure a chord across the corner. That + // shortened the LUT a little at every turn and moved later notes + // progressively ahead of their authored timestamp. + const float t = static_cast(segment) + + static_cast(sample) / static_cast(samplesPerSegment); + const glm::vec3 currPos = getPositionRaw(t); + totalLength += glm::distance(prevPos, currPos); + LUT.push_back({totalLength, t}); + prevPos = currPos; + } + } +} + +glm::vec3 Spline::getPositionAtDistance(float d) const { + if (LUT.empty()) return glm::vec3(0); + d = std::clamp(d, 0.0f, totalLength); + auto it = std::lower_bound(LUT.begin(), LUT.end(), d, [](const LUTEntry& e, float val) { return e.distance < val; }); + if (it == LUT.begin()) return getPositionRaw(it->t); + auto prev = std::prev(it); + float factor = (d - prev->distance) / (it->distance - prev->distance); + return getPositionRaw(glm::mix(prev->t, it->t, factor)); +} + +glm::vec3 Spline::getTangentAtDistance(float d) const { + if (LUT.empty()) return glm::vec3(0, 0, -1); + d = std::clamp(d, 0.0f, totalLength); + auto it = std::lower_bound(LUT.begin(), LUT.end(), d, [](const LUTEntry& e, float val) { return e.distance < val; }); + float t = (it == LUT.begin()) ? it->t : glm::mix(std::prev(it)->t, it->t, (d - std::prev(it)->distance) / (it->distance - std::prev(it)->distance)); + return getTangentRaw(t); +} + +float Spline::getDistanceAtIndex(size_t index) const { + if (LUT.empty() || points.empty()) return 0.0f; + float t = std::clamp((float)index, 0.0f, (float)points.size() - 1.0f); + auto it = std::lower_bound(LUT.begin(), LUT.end(), t, [](const LUTEntry& e, float val) { return e.t < val; }); + if (it == LUT.begin()) return it->distance; + if (it == LUT.end()) return LUT.back().distance; + auto prev = std::prev(it); + float factor = (t - prev->t) / (it->t - prev->t); + return prev->distance + (it->distance - prev->distance) * factor; +} + +glm::vec3 Spline::getPositionRaw(float t) const { + int p1 = (int)t; int p2 = p1 + 1; + if (p2 >= points.size()) return points.back().position; + float lt = t - (float)p1; + if (points[p1].type == 1) return glm::mix(points[p1].position, points[p2].position, lt); + int p0 = std::max(0, p1 - 1), p3 = std::min((int)points.size() - 1, p2 + 1); + float t2 = lt * lt, t3 = t2 * lt; + return 0.5f * ((2.0f * points[p1].position) + (-points[p0].position + points[p2].position) * lt + (2.0f * points[p0].position - 5.0f * points[p1].position + 4.0f * points[p2].position - points[p3].position) * t2 + (-points[p0].position + 3.0f * points[p1].position - 3.0f * points[p2].position + points[p3].position) * t3); +} + +glm::vec3 Spline::getTangentRaw(float t) const { + int p1 = (int)t; int p2 = p1 + 1; + if (p2 >= points.size()) return glm::normalize(points.back().position - points[std::max(0, (int)points.size()-2)].position); + float lt = t - (float)p1; + if (points[p1].type == 1) return glm::normalize(points[p2].position - points[p1].position); + int p0 = std::max(0, p1 - 1), p3 = std::min((int)points.size() - 1, p2 + 1); + float t2 = lt * lt; + glm::vec3 tangent = 0.5f * ((-points[p0].position + points[p2].position) + 2.0f * (2.0f * points[p0].position - 5.0f * points[p1].position + 4.0f * points[p2].position - points[p3].position) * lt + 3.0f * (-points[p0].position + 3.0f * points[p1].position - 3.0f * points[p2].position + points[p3].position) * t2); + return glm::normalize(tangent); +} diff --git a/src/gl_loader.cpp b/src/gl_loader.cpp new file mode 100644 index 0000000..fd07956 --- /dev/null +++ b/src/gl_loader.cpp @@ -0,0 +1,2 @@ +#define GL_LOADER_IMPLEMENTATION +#include "vectorail/core/gl_loader.hpp"