Initial Vectorail Core library

This commit is contained in:
2026-08-02 17:05:27 +02:00
commit d869edfaa8
15 changed files with 749 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
/build/
/cmake-build-*/
/.cache/
/.clangd/
/compile_commands.json
*.o
*.a
*.so
*.dll
*.dylib
*.exe
+71
View File
@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
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
)
+30
View File
@@ -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.
+26
View File
@@ -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.
+10
View File
@@ -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")
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <cstdint>
#include <vector>
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<uint8_t>& bytes, DdsTexture& out,
std::string* error = nullptr);
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "vectorail/core/DdsTexture.hpp"
#include <string>
#include <vector>
// 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<DdsTexture>& out, std::string* error = nullptr);
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "vectorail/core/gl_loader.hpp"
#include <string>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
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));
}
};
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <vector>
#include <glm/glm.hpp>
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<SplinePoint> points;
struct LUTEntry {
float distance;
float t;
};
std::vector<LUTEntry> LUT;
float totalLength = 0;
glm::vec3 getPositionRaw(float t) const;
glm::vec3 getTangentRaw(float t) const;
};
+119
View File
@@ -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 <windows.h>
#endif
#include <SDL3/SDL.h>
#include <SDL3/SDL_opengl.h>
#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
+117
View File
@@ -0,0 +1,117 @@
#include "vectorail/core/DdsTexture.hpp"
#include "vectorail/core/gl_loader.hpp"
#include <cstdint>
#include <fstream>
#include <limits>
#include <vector>
namespace {
uint32_t readU32le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint32_t>(bytes[offset + 0]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(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<uint8_t>((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<uint8_t> bytes(static_cast<size_t>(fileSize));
file.read(reinterpret_cast<char*>(bytes.data()), fileSize);
if (!file) {
if (error) *error = "could not read DDS";
return false;
}
return loadDdsTextureBytes(bytes, out, error);
}
bool loadDdsTextureBytes(const std::vector<uint8_t>& 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<size_t>::max() / height ||
static_cast<size_t>(width) * height > std::numeric_limits<size_t>::max() / bytesPerPixel) {
if (error) *error = "DDS dimensions overflow";
return false;
}
const size_t pixelCount = static_cast<size_t>(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<uint8_t> 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<uint32_t>(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<GLsizei>(width), static_cast<GLsizei>(height),
0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
glBindTexture(GL_TEXTURE_2D, 0);
out.width = static_cast<int>(width);
out.height = static_cast<int>(height);
return true;
}
+150
View File
@@ -0,0 +1,150 @@
#include "vectorail/core/PngTexture.hpp"
#include "vectorail/core/gl_loader.hpp"
#include <png.h>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <span>
#include <vector>
namespace {
uint16_t u16be(std::span<const uint8_t> bytes, size_t offset) {
return static_cast<uint16_t>((static_cast<uint16_t>(bytes[offset]) << 8) | bytes[offset + 1]);
}
uint32_t u32be(std::span<const uint8_t> bytes, size_t offset) {
return (static_cast<uint32_t>(bytes[offset]) << 24) |
(static_cast<uint32_t>(bytes[offset + 1]) << 16) |
(static_cast<uint32_t>(bytes[offset + 2]) << 8) |
static_cast<uint32_t>(bytes[offset + 3]);
}
bool readFile(const std::string& path, std::vector<uint8_t>& 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_t>(size), 0);
if (!bytes.empty()) file.read(reinterpret_cast<char*>(bytes.data()), size);
return static_cast<bool>(file) || file.eof();
}
struct PngMemoryReader {
std::span<const uint8_t> bytes;
size_t offset = 0;
};
void readPngMemory(png_structp png, png_bytep destination, png_size_t length) {
auto* reader = static_cast<PngMemoryReader*>(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<const uint8_t> 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<uint8_t> rgba(static_cast<size_t>(width) * height * 4);
std::vector<png_bytep> rows(height);
for (png_uint_32 y = 0; y < height; ++y) rows[y] = rgba.data() + static_cast<size_t>(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<GLsizei>(width), static_cast<GLsizei>(height),
0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
glBindTexture(GL_TEXTURE_2D, 0);
out.width = static_cast<int>(width);
out.height = static_cast<int>(height);
return true;
}
} // namespace
bool loadPngTexture(const std::string& path, DdsTexture& out, std::string* error) {
std::vector<uint8_t> 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<DdsTexture>& out, std::string* error) {
out.clear();
std::vector<uint8_t> bytes;
if (!readFile(path, bytes) || bytes.size() < 10) {
if (error) *error = "could not read texture list";
return false;
}
const std::span<const uint8_t> view(bytes);
const uint16_t count = u16be(view, 4);
if (6u + (static_cast<size_t>(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<size_t>(index) * 4);
const size_t end = u32be(view, 10 + static_cast<size_t>(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;
}
+32
View File
@@ -0,0 +1,32 @@
#include "vectorail/core/Shader.hpp"
#include <fstream>
#include <sstream>
#include <iostream>
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);
}
+81
View File
@@ -0,0 +1,81 @@
#include "vectorail/core/Spline.hpp"
#include <algorithm>
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<float>(segment) +
static_cast<float>(sample) / static_cast<float>(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);
}
+2
View File
@@ -0,0 +1,2 @@
#define GL_LOADER_IMPLEMENTATION
#include "vectorail/core/gl_loader.hpp"