first commit

This commit is contained in:
David Ali
2026-01-09 13:50:52 +01:00
commit 9f87935db1
24 changed files with 7925 additions and 0 deletions

47
.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Build artifacts
/build/
/bin/
/obj/
/out/
/dist/
*.o
*.obj
*.lo
*.la
*.a
*.so
*.so.*
*.dylib
*.dll
*.exe
*.lib
*.pdb
# Dependency/packaging caches
*.lock
*.log
# Compilers & coverage
*.d
*.gcno
*.gcda
*.gcov
# CMake / Make
CMakeFiles/
CMakeCache.txt
cmake-build-*/
Makefile.debug
Makefile.release
# Editors / OS
.vscode/
.idea/
*.code-workspace
*.swp
*~
.DS_Store
Thumbs.db
# Generated metadata
compile_commands.json

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "external/glm"]
path = external/glm
url = https://github.com/g-truc/glm.git

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2026 Dávid Ali
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.

64
Makefile Normal file
View File

@@ -0,0 +1,64 @@
# ---- config ----
TARGET := bin/main
BUILD ?= debug
CXX := g++
CC := gcc
CXXFLAGS := -std=c++20 -Wall -Wextra -Wshadow -Wconversion -Wno-unused-parameter
CFLAGS := -std=c17 -Wall -Wextra -Wno-unused-parameter
CPPFLAGS := -Iexternal/glad/include -Iexternal/glm -Isrc
PKG_CFLAGS := $(shell pkg-config --cflags glfw3 cglm)
PKG_LIBS := $(shell pkg-config --libs glfw3 cglm)
# auto-deps: generate .d files next to .o
DEPFLAGS := -MMD -MP
ifeq ($(BUILD),debug)
CXXFLAGS += -O0 -g
CFLAGS += -O0 -g
else
CXXFLAGS += -O2 -DNDEBUG
CFLAGS += -O2 -DNDEBUG
endif
LDFLAGS := $(PKG_LIBS) -ldl
# ---- sources ----
SRCS_CPP := $(shell find src -name '*.cpp')
SRCS_C := external/glad/src/glad.c
OBJS := $(SRCS_CPP:%.cpp=obj/%.o) $(SRCS_C:%.c=obj/%.o)
DEPS := $(OBJS:.o=.d)
# ---- rules ----
.PHONY: all run clean tidy rebuild
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(dir $@)
$(CXX) $^ -o $@ $(LDFLAGS)
# C++
obj/%.o: %.cpp
@mkdir -p $(dir $@)
$(CXX) $(CPPFLAGS) $(PKG_CFLAGS) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
# C (glad)
obj/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(PKG_CFLAGS) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# include auto-generated header dependencies (safe if missing on first run)
-include $(DEPS)
run: $(TARGET)
prime-run ./$(TARGET)
clean:
rm -rf obj
tidy:
rm -rf obj $(TARGET)
rebuild: clean all

152
README.md Normal file
View File

@@ -0,0 +1,152 @@
# gren
A small modern OpenGL (4.5 core) playground written in C++20.
Focus: a simple, hackable renderer with hot-reloadable shaders, an orbit camera, and utilities for building meshes (quad/cube/ico and a pluggable terrain mesher).
https://git.alidavid.hu/gren
---
## Highlights
- **OpenGL 4.5 core** via GLFW context.
- **Z-up world** (`+Z` is up). Terrain height is written to `Z`.
- **Orbit camera** with aspect updates and input wiring.
- **Uniform buffers**:
- `PerFrame` (view/proj, lighting dir),
- `PerObject` (model matrix),
- `Material` (base color, etc.).
- **Shader hot reload**: shaders are polled each frame; edit files under `assets/shaders` and see changes immediately.
- **Mesh helpers**: unit quad, cube, icosahedron; GPU wrapper (`Graphics::MeshGL`) and pipeline abstraction.
- **Skybox pass**: inside-out cube with depth/state tweaks.
- **Procedural terrain API**: build a grid from a user-supplied height function.
---
## Repository layout
```
assets/
shaders/ GLSL vertex/fragment shaders
bin/ Built binaries
external/ Third-party code (if any)
obj/ Intermediate objects
src/ C++ sources (entry point: main.cpp)
Makefile (if present) convenience build
```
---
## Build
### Requirements
- C++20 compiler (g++ 12+/clang 15+)
- OpenGL 4.5 capable GPU/driver
- GLFW 3.x (window/context/input)
- GLM (math)
- A GL loader (provided in the project via `glfwx::load_gl_or_throw()`)
On Debian/Ubuntu:
```bash
sudo apt update
sudo apt install build-essential pkg-config libglfw3-dev libglm-dev
```
### Compile
If a `Makefile` is present:
```bash
make -j
```
Otherwise, a minimal one-liner build (adjust include/lib paths as needed):
```bash
g++ -std=gnu++20 -O2 -Wall -Wextra -Iexternal -Isrc \
src/*.cpp -lglfw -ldl -lpthread -o bin/main
```
Run from the repo root (so `assets/` is found):
```bash
./bin/main
```
---
## Coordinate system
- **World up:** `+Z`
- **Camera/view:** right-handed
- **Front faces:** counter-clockwise (CCW)
If lighting looks inverted, verify your mesh winding and the Z-up assumption.
---
## Shader hot reload
`Graphics::ShaderManager` polls shader files every frame:
- Edit anything under `assets/shaders/*.vert|*.frag`.
- The program re-links on the next poll; errors are printed to stdout/stderr.
---
## Procedural terrain (plug-in height function)
Build a terrain mesh by supplying a height function `(x, y) -> z`:
```cpp
#include <cmath>
#include "Asset.hpp"
// Example: gentle ridges
auto height = [](float x, float y) {
using std::sin; using std::cos;
return sin(x * 0.05f) * cos(y * 0.05f) * 5.0f;
};
// Generate a 256x256 grid (1 unit spacing), centered-ish
asset::Mesh terrain = asset::make_terrain(
/*W=*/256, /*H=*/256, /*dx=*/1.0f, /*dy=*/1.0f,
height,
/*genNormals=*/true, /*genUVs=*/true,
/*x0=*/-128.0f, /*y0=*/-128.0f);
Graphics::MeshGL gpuTerrain;
gpuTerrain.upload(terrain);
```
Notes:
- The grid lies in the **XY** plane; height is written to **Z**.
- Indices use CCW winding in XY so normals point `+Z`.
- Normals are computed as area-weighted sums of face normals and normalized per vertex.
---
## Camera
An orbit camera is provided. Controls are installed through the GLFW hook utilities.
See the implementation files for the exact bindings.
---
## Development tips
- Keep VAO attribute layouts consistent across meshes to avoid shader variant recompiles on some drivers.
- After the skybox pass, restore depth writes and CCW front faces before drawing regular geometry.
- For constant-screen-size debug gizmos, scale by distance in the vertex shader rather than world scale.
---
## Roadmap
- Entity/placement layer on top of the tile map (sprite atlas already integrated).
- Terrain polish: domain-warped noise presets, LOD chunks, and skirts.
- Shadowing pass (directional light; start with simple shadow mapping).
- Optional HTTP read-only clone and tarball snapshots via cgit configuration.

35
assets/shaders/mesh.frag Normal file
View File

@@ -0,0 +1,35 @@
#version 450 core
layout(location = 0) out vec4 oColor;
layout(std140, binding = 0) uniform PerFrame {
mat4 uView;
mat4 uProj;
vec4 uLightDir;
};
layout(std140, binding = 2) uniform Material {
vec4 uAlbedo; // rgb used, a padding
};
in VS_OUT {
vec3 wPos;
vec3 wNrm;
vec2 uv;
flat int hasNrm;
flat int hasUV;
} v;
vec3 fallbackNormal(vec3 wPos) {
vec3 dx = dFdx(wPos), dy = dFdy(wPos);
vec3 c = cross(dx, dy);
float l2 = dot(c, c);
return (l2 < 1e-12) ? vec3(0, 0, 1) : normalize(c);
}
void main() {
vec3 N = (v.hasNrm != 0) ? normalize(v.wNrm) : fallbackNormal(v.wPos);
vec3 L = normalize(-uLightDir.xyz);
float ndl = max(dot(N, L), 0.0);
vec3 ambient = vec3(0.08);
vec3 colorLinear = ambient + uAlbedo.rgb * ndl;
oColor = vec4(pow(colorLinear, vec3(1.0/2.2)), 1.0);
}

45
assets/shaders/mesh.vert Normal file
View File

@@ -0,0 +1,45 @@
#version 450 core
out gl_PerVertex {
vec4 gl_Position;
};
layout(std140, binding = 0) uniform PerFrame {
mat4 uView;
mat4 uProj;
vec4 uLightDir; // xyz used, w padding
};
layout(std140, binding = 1) uniform PerObject {
mat4 uModel;
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUV;
out VS_OUT {
vec3 wPos;
vec3 wNrm;
vec2 uv;
flat int hasNrm;
flat int hasUV;
} v;
void main() {
vec4 wpos4 = uModel * vec4(aPos, 1.0);
v.wPos = wpos4.xyz;
float nLen2 = dot(aNormal, aNormal);
if(nLen2 > 1e-10) {
mat3 nrmMat = mat3(transpose(inverse(uModel)));
v.wNrm = normalize(nrmMat * aNormal);
v.hasNrm = 1;
} else {
v.wNrm = vec3(0.0);
v.hasNrm = 0;
}
v.uv = aUV;
v.hasUV = (abs(aUV.x) + abs(aUV.y)) > 1e-10 ? 1 : 0;
gl_Position = uProj * uView * wpos4;
}

View File

@@ -0,0 +1,33 @@
#version 450 core
layout(location = 0) out vec4 oColor;
layout(std140, binding = 0) uniform PerFrame {
mat4 uView;
mat4 uProj;
vec4 uLightDir; // unused
};
layout(std140, binding = 2) uniform Material {
vec4 uAlbedo; // unused
};
layout(location = 0) in vec3 vDir;
// sRGB-tuned colors; well convert to linear before mixing
vec3 srgb2lin(vec3 c){ return pow(c, vec3(2.2)); }
vec3 lin2srgb(vec3 c){ return pow(c, vec3(1.0/2.2)); }
void main() {
// nicer defaults (tweak to taste)
vec3 skyTopL = srgb2lin(vec3(0.15, 0.35, 0.75));
vec3 skyHorizonL = srgb2lin(vec3(0.75, 0.85, 0.98));
vec3 dir = normalize(vDir);
float t = clamp(dir.z * 0.5 + 0.5, 0.0, 1.0); // (B) world up = +Z
// shape the gradient a bit
t = pow(t, 1.45);
vec3 colorL = mix(skyHorizonL, skyTopL, t);
oColor = vec4(lin2srgb(colorL), 1.0); // manual gamma (your default FB isnt sRGB)
}

View File

@@ -0,0 +1,31 @@
#version 450 core
out gl_PerVertex { vec4 gl_Position; };
layout(std140, binding = 0) uniform PerFrame {
mat4 uView;
mat4 uProj;
vec4 uLightDir; // unused here
};
layout(std140, binding = 1) uniform PerObject {
mat4 uModel; // use this to scale the cube (e.g., 3.0)
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal; // present, unused
layout(location = 2) in vec2 aUV; // present, unused
layout(location = 0) out vec3 vDir; // explicit location for SSO
void main() {
// Use uModel to scale your standard cube (e.g., 3.0) from CPU.
vec3 pLocal = mat3(uModel) * aPos;
// Rotation-only view (drop translation)
mat3 R = mat3(uView);
vDir = pLocal;
// Glue the cube to the camera (no translation), push to far plane
vec4 p = uProj * vec4(R * pLocal, 1.0);
gl_Position = vec4(p.xy, 1.0, 1.0);
}

View File

@@ -0,0 +1,39 @@
#version 450 core
layout(location=0) out vec4 oColor;
layout(std140, binding=0) uniform PerFrame { mat4 uView; mat4 uProj; vec4 uLightDir; };
layout(std140, binding=2) uniform Material { vec4 uAlbedo; };
layout(location=0) in VS_OUT {
vec3 wPos;
vec2 uv;
} v;
// geometric normal from screen-space derivatives of the *world* position
vec3 normal_from_worldpos(vec3 w){
vec3 dx = dFdx(w), dy = dFdy(w);
vec3 n = cross(dx,dy);
float l2 = dot(n,n);
return (l2 < 1e-12) ? vec3(0,0,1) : normalize(n);
}
void main(){
vec3 N = normal_from_worldpos(v.wPos);
vec3 L = normalize(-uLightDir.xyz);
float ndl = max(dot(N,L), 0.0);
// cheap color: height-based gradient + slight darkening by slope
float h = v.wPos.z; // world height
float h01 = clamp(h * 0.1 + 0.5, 0.0, 1.0); // remap height to 0..1
vec3 low = vec3(0.35, 0.38, 0.40); // rock/soil
vec3 high = vec3(0.65, 0.75, 0.60); // grass
vec3 base = mix(low, high, h01);
float slope = 1.0 - abs(N.z); // 0 = flat up, 1 = vertical
base *= mix(1.0, 0.8, slope); // darker on steep slopes
vec3 ambient = vec3(0.08);
vec3 color = ambient + base * (uAlbedo.rgb * ndl);
oColor = vec4(color, 1.0);
}

View File

@@ -0,0 +1,45 @@
#version 450 core
out gl_PerVertex { vec4 gl_Position; };
layout(std140, binding=0) uniform PerFrame { mat4 uView; mat4 uProj; vec4 uLightDir; };
layout(std140, binding=1) uniform PerObject { mat4 uModel; };
layout(location=0) in vec3 aPos; // XY from grid, Z ignored
layout(location=2) in vec2 aUV; // 0..1 across the grid
layout(location=0) out VS_OUT {
vec3 wPos;
vec2 uv;
} v;
// ---- tiny fBm noise (tile-free, cheap) ----
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453); }
float noise(vec2 p){
vec2 i=floor(p), f=fract(p);
float a = hash(i + vec2(0,0));
float b = hash(i + vec2(1,0));
float c = hash(i + vec2(0,1));
float d = hash(i + vec2(1,1));
vec2 u = f*f*(3.0-2.0*f);
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 p){
float a=0.5, s=0.0;
for(int i=0;i<5;i++){ s += a*noise(p); p = p*2.02 + 17.0; a*=0.5; }
return s;
}
// tunables
uniform float uHeightScale = 3.0; // world Z amplitude
uniform float uFreq = 2.0; // base frequency (cycles over 0..1 uv)
void main(){
float h = fbm(aUV * uFreq) * uHeightScale; // Z-up displacement
vec3 p = vec3(aPos.xy, h);
vec4 w = uModel * vec4(p,1.0);
v.wPos = w.xyz;
v.uv = aUV;
gl_Position = uProj * uView * w;
}

311
external/glad/include/KHR/khrplatform.h vendored Normal file
View File

@@ -0,0 +1,311 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are 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 Materials.
**
** THE MATERIALS ARE 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
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
* should be located on your system and for more details of its use:
* http://www.khronos.org/registry/implementers_guide.pdf
*
* This file should be included as
* #include <KHR/khrplatform.h>
* by Khronos client API header files that use its types and defines.
*
* The types in khrplatform.h should only be used to define API-specific types.
*
* Types defined in khrplatform.h:
* khronos_int8_t signed 8 bit
* khronos_uint8_t unsigned 8 bit
* khronos_int16_t signed 16 bit
* khronos_uint16_t unsigned 16 bit
* khronos_int32_t signed 32 bit
* khronos_uint32_t unsigned 32 bit
* khronos_int64_t signed 64 bit
* khronos_uint64_t unsigned 64 bit
* khronos_intptr_t signed same number of bits as a pointer
* khronos_uintptr_t unsigned same number of bits as a pointer
* khronos_ssize_t signed size
* khronos_usize_t unsigned size
* khronos_float_t signed 32 bit floating point
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
* nanoseconds
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
* khronos_boolean_enum_t enumerated boolean type. This should
* only be used as a base type when a client API's boolean type is
* an enum. Client APIs which use an integer or other type for
* booleans cannot use this as the base type for their boolean.
*
* Tokens defined in khrplatform.h:
*
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
*
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
*
* Calling convention macros defined in this file:
* KHRONOS_APICALL
* KHRONOS_APIENTRY
* KHRONOS_APIATTRIBUTES
*
* These may be used in function prototypes as:
*
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
# define KHRONOS_STATIC 1
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
* This precedes the return type of the function in the function prototype.
*/
#if defined(KHRONOS_STATIC)
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
* header compatible with static linking. */
# define KHRONOS_APICALL
#elif defined(_WIN32)
# define KHRONOS_APICALL __declspec(dllimport)
#elif defined (__SYMBIAN32__)
# define KHRONOS_APICALL IMPORT_C
#elif defined(__ANDROID__)
# define KHRONOS_APICALL __attribute__((visibility("default")))
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
* This follows the return type of the function and precedes the function
* name in the function prototype.
*/
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
/* Win32 but not WinCE */
# define KHRONOS_APIENTRY __stdcall
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
* This follows the closing parenthesis of the function prototype arguments.
*/
#if defined (__ARMCC_2__)
#define KHRONOS_APIATTRIBUTES __softfp
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
/*
* To support platform where unsigned long cannot be used interchangeably with
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
* unsigned long long or similar (this results in different C++ name mangling).
* To avoid changes for existing platforms, we restrict usage of intptr_t to
* platforms where the size of a pointer is larger than the size of long.
*/
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
#define KHRONOS_USE_INTPTR_T
#endif
#endif
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
#include <inttypes.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
typedef __int32 khronos_int32_t;
typedef unsigned __int32 khronos_uint32_t;
typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#if defined(__arch64__) || defined(_LP64)
typedef long int khronos_int64_t;
typedef unsigned long int khronos_uint64_t;
#else
typedef long long int khronos_int64_t;
typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
* to be the only LLP64 architecture in current use.
*/
#ifdef KHRONOS_USE_INTPTR_T
typedef intptr_t khronos_intptr_t;
typedef uintptr_t khronos_uintptr_t;
#elif defined(_WIN64)
typedef signed long long int khronos_intptr_t;
typedef unsigned long long int khronos_uintptr_t;
#else
typedef signed long int khronos_intptr_t;
typedef unsigned long int khronos_uintptr_t;
#endif
#if defined(_WIN64)
typedef signed long long int khronos_ssize_t;
typedef unsigned long long int khronos_usize_t;
#else
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
* These types can be used to represent a time interval in nanoseconds or
* an absolute Unadjusted System Time. Unadjusted System Time is the number
* of nanoseconds since some arbitrary system event (e.g. since the last
* time the system booted). The Unadjusted System Time is an unsigned
* 64 bit value that wraps back to 0 every 584 years. Time intervals
* may be either signed or unsigned.
*/
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
* Values other than zero should be considered to be true. Therefore
* comparisons should not be made against KHRONOS_TRUE.
*/
typedef enum {
KHRONOS_FALSE = 0,
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */

3656
external/glad/include/glad/glad.h vendored Normal file

File diff suppressed because it is too large Load Diff

1819
external/glad/src/glad.c vendored Normal file

File diff suppressed because it is too large Load Diff

1
external/glm vendored Submodule

Submodule external/glm added at 2d4c4b4dd3

343
src/Asset.cpp Normal file
View File

@@ -0,0 +1,343 @@
#include "Asset.hpp"
#include <cmath>
#include <array>
#include <glm/glm.hpp>
namespace asset {
Mesh make_unit_quad(float size) {
Mesh m;
const float h = size * 0.5f;
m.positions = {-size, -size, 0.0f, size, -size, 0.0f,
size, size, 0.0f, -size, size, 0.0f};
m.uvs = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
m.indices = {0, 1, 2, 2, 3, 0};
return m;
}
Mesh make_unit_cube(float size) {
Mesh m;
const float h = size * 0.5f;
// 6 faces × 4 verts (CCW when viewed from outside)
const float P[] = {
// +X (right)
h,-h,-h, h, h,-h, h, h, h, h,-h, h,
// -X (left)
-h,-h, h, -h, h, h, -h, h,-h, -h,-h,-h,
// +Y (top) *** fixed winding ***
-h, h,-h, -h, h, h, h, h, h, h, h,-h,
// -Y (bottom) *** fixed winding ***
-h,-h,-h, h,-h,-h, h,-h, h, -h,-h, h,
// +Z (front)
-h,-h, h, h,-h, h, h, h, h, -h, h, h,
// -Z (back)
h,-h,-h, -h,-h,-h, -h, h,-h, h, h,-h
};
const float N[] = {
// +X
1,0,0, 1,0,0, 1,0,0, 1,0,0,
// -X
-1,0,0, -1,0,0, -1,0,0, -1,0,0,
// +Y
0,1,0, 0,1,0, 0,1,0, 0,1,0,
// -Y
0,-1,0, 0,-1,0, 0,-1,0, 0,-1,0,
// +Z
0,0,1, 0,0,1, 0,0,1, 0,0,1,
// -Z
0,0,-1, 0,0,-1, 0,0,-1, 0,0,-1
};
// 0..1 quad per face; orientation is fine with the corrected positions
const float UV[] = {
0,0, 1,0, 1,1, 0,1,
0,0, 1,0, 1,1, 0,1,
0,0, 1,0, 1,1, 0,1,
0,0, 1,0, 1,1, 0,1,
0,0, 1,0, 1,1, 0,1,
0,0, 1,0, 1,1, 0,1
};
// 2 triangles per face
const uint32_t I[] = {
0, 1, 2, 0, 2, 3, // +X
4, 5, 6, 4, 6, 7, // -X
8, 9, 10, 8, 10, 11, // +Y (fixed)
12, 13, 14, 12, 14, 15, // -Y (fixed)
16, 17, 18, 16, 18, 19, // +Z
20, 21, 22, 20, 22, 23 // -Z
};
m.positions.assign(std::begin(P), std::end(P));
m.normals.assign (std::begin(N), std::end(N));
m.uvs.assign (std::begin(UV), std::end(UV));
m.indices.assign (std::begin(I), std::end(I));
return m;
}
Mesh make_unit_ico(float size){
Mesh m;
// Golden ratio
const float phi = (1.0f + std::sqrt(5.0f)) * 0.5f;
// Canonical 12 vertices (right-handed)
const std::array<glm::vec3, 12> V = {
glm::vec3(-1, phi, 0),
glm::vec3( 1, phi, 0),
glm::vec3(-1, -phi, 0),
glm::vec3( 1, -phi, 0),
glm::vec3( 0, -1, phi),
glm::vec3( 0, 1, phi),
glm::vec3( 0, -1, -phi),
glm::vec3( 0, 1, -phi),
glm::vec3( phi, 0, -1),
glm::vec3( phi, 0, 1),
glm::vec3(-phi, 0, -1),
glm::vec3(-phi, 0, 1)
};
// Normalize to unit sphere and scale to radius = size * 0.5
const float r = size * 0.5f;
m.positions.reserve(12 * 3);
m.normals .reserve(12 * 3);
for (auto p : V) {
glm::vec3 n = glm::normalize(p);
glm::vec3 s = r * n;
m.positions.insert(m.positions.end(), {s.x, s.y, s.z});
m.normals .insert(m.normals .end(), {n.x, n.y, n.z});
}
// 20 faces (CCW)
static const uint32_t I[] = {
0, 11, 5,
0, 5, 1,
0, 1, 7,
0, 7, 10,
0, 10, 11,
1, 5, 9,
5, 11, 4,
11, 10, 2,
10, 7, 6,
7, 1, 8,
3, 9, 4,
3, 4, 2,
3, 2, 6,
3, 6, 8,
3, 8, 9,
4, 9, 5,
2, 4, 11,
6, 2, 10,
8, 6, 7,
9, 8, 1
};
m.indices.assign(std::begin(I), std::end(I));
// UVs omitted (non-trivial on icosahedra)
m.uvs.clear();
return m;
}
Mesh make_unit_ico_flat(float size) {
Mesh m;
m.positions.clear();
m.normals.clear();
m.uvs.clear();
m.indices.clear(); // draw non-indexed (or fill 0..N-1 if you prefer)
// Golden ratio
const float phi = (1.0f + std::sqrt(5.0f)) * 0.5f;
const float r = size * 0.5f; // radius
// 12 canonical vertices (right-handed)
const std::array<glm::vec3, 12> V = {
glm::vec3(-1, phi, 0),
glm::vec3( 1, phi, 0),
glm::vec3(-1, -phi, 0),
glm::vec3( 1, -phi, 0),
glm::vec3( 0, -1, phi),
glm::vec3( 0, 1, phi),
glm::vec3( 0, -1, -phi),
glm::vec3( 0, 1, -phi),
glm::vec3( phi, 0, -1),
glm::vec3( phi, 0, 1),
glm::vec3(-phi, 0, -1),
glm::vec3(-phi, 0, 1)
};
// 20 faces (CCW)
static const uint32_t F[20][3] = {
{ 0,11, 5}, { 0, 5, 1}, { 0, 1, 7}, { 0, 7,10}, { 0,10,11},
{ 1, 5, 9}, { 5,11, 4}, {11,10, 2}, {10, 7, 6}, { 7, 1, 8},
{ 3, 9, 4}, { 3, 4, 2}, { 3, 2, 6}, { 3, 6, 8}, { 3, 8, 9},
{ 4, 9, 5}, { 2, 4,11}, { 6, 2,10}, { 8, 6, 7}, { 9, 8, 1}
};
m.positions.reserve(20 * 3 * 3);
m.normals.reserve (20 * 3 * 3);
for (const auto& tri : F) {
// Sphere positions (normalized then scaled to radius r)
glm::vec3 p0 = glm::normalize(V[tri[0]]) * r;
glm::vec3 p1 = glm::normalize(V[tri[1]]) * r;
glm::vec3 p2 = glm::normalize(V[tri[2]]) * r;
// Flat face normal (one per triangle)
glm::vec3 n = glm::normalize(glm::cross(p1 - p0, p2 - p0));
// Duplicate vertices per face so normals don't interpolate across edges
const glm::vec3 P[3] = { p0, p1, p2 };
for (int i = 0; i < 3; ++i) {
m.positions.push_back(P[i].x);
m.positions.push_back(P[i].y);
m.positions.push_back(P[i].z);
m.normals.push_back(n.x);
m.normals.push_back(n.y);
m.normals.push_back(n.z);
}
}
// If you prefer indexed draw, uncomment to fill 0..N-1
// m.indices.resize(m.positions.size() / 3);
// std::iota(m.indices.begin(), m.indices.end(), 0u);
return m;
}
Mesh make_grid(int N, float extent) {
Mesh m;
const int V = (N+1);
m.positions.reserve(V*V*3);
m.uvs .reserve(V*V*2);
const float half = extent*0.5f;
for (int y=0; y<V; ++y){
for (int x=0; x<V; ++x){
float fx = float(x)/N, fy = float(y)/N;
float X = -half + fx*extent;
float Y = -half + fy*extent;
m.positions.insert(m.positions.end(), {X,Y,0.0f}); // Z = 0 (well displace in VS)
m.uvs.insert(m.uvs.end(), {fx, fy}); // 0..1 for height/albedo sampling
}
}
m.indices.reserve(N*N*6);
for (int y=0; y<N; ++y){
for (int x=0; x<N; ++x){
uint32_t i0 = y *V + x;
uint32_t i1 = y *V + (x+1);
uint32_t i2 = (y+1)*V + x;
uint32_t i3 = (y+1)*V + (x+1);
m.indices.insert(m.indices.end(), { i0,i2,i1, i1,i2,i3 }); // CCW
}
}
return m;
}
static inline void add3(std::vector<float>& v, size_t i, const glm::vec3& a) {
v[i+0] += a.x; v[i+1] += a.y; v[i+2] += a.z;
}
Mesh make_terrain(int W, int H, float dx, float dz,
HeightFn heightFn,
bool genNormals,
bool genUVs,
float x0, float z0)
{
assert(W > 0 && H > 0 && dx > 0.0f && dz > 0.0f);
Mesh m;
const int VX = (W + 1);
const int VZ = (H + 1);
const int N = VX * VZ;
m.positions.resize(3 * N);
if (genNormals) m.normals.assign(3 * N, 0.0f);
if (genUVs) m.uvs.resize(2 * N);
// --- Vertices (positions + uvs) ---
for (int j = 0; j < VZ; ++j) {
for (int i = 0; i < VX; ++i) {
const int v = j * VX + i;
const size_t p3 = 3 * v;
const size_t t2 = 2 * v;
// Grid spans the XY plane; height goes into Z
const float X = x0 + i * dx;
const float Y = z0 + j * dz; // reuse dz as grid step in Y
const float Z = heightFn ? heightFn(X, Y) : 0.0f;
m.positions[p3 + 0] = X; // x
m.positions[p3 + 1] = Y; // y (planar)
m.positions[p3 + 2] = Z; // z (height)
if (genUVs) {
m.uvs[t2 + 0] = (VX > 1) ? (float)i / (float)(VX - 1) : 0.0f;
m.uvs[t2 + 1] = (VZ > 1) ? (float)j / (float)(VZ - 1) : 0.0f;
}
}
}
// --- Indices (two triangles per quad), CCW in XY so normals point +Z ---
m.indices.reserve(W * H * 6);
for (int j = 0; j < H; ++j) {
for (int i = 0; i < W; ++i) {
const uint32_t i0 = (uint32_t)( j * VX + i );
const uint32_t i1 = (uint32_t)( j * VX + i + 1);
const uint32_t i2 = (uint32_t)((j+1) * VX + i );
const uint32_t i3 = (uint32_t)((j+1) * VX + i + 1);
m.indices.push_back(i0); m.indices.push_back(i1); m.indices.push_back(i2);
m.indices.push_back(i1); m.indices.push_back(i3); m.indices.push_back(i2);
}
}
// --- Smoothed per-vertex normals (area-weighted) ---
if (genNormals) {
// Accumulate face normals
for (size_t k = 0; k < m.indices.size(); k += 3) {
const uint32_t ia = m.indices[k+0];
const uint32_t ib = m.indices[k+1];
const uint32_t ic = m.indices[k+2];
const glm::vec3 A{ m.positions[3*ia+0], m.positions[3*ia+1], m.positions[3*ia+2] };
const glm::vec3 B{ m.positions[3*ib+0], m.positions[3*ib+1], m.positions[3*ib+2] };
const glm::vec3 C{ m.positions[3*ic+0], m.positions[3*ic+1], m.positions[3*ic+2] };
const glm::vec3 N = glm::cross(B - A, C - A); // CCW -> +Z on flat areas
add3(m.normals, 3*ia, N);
add3(m.normals, 3*ib, N);
add3(m.normals, 3*ic, N);
}
// Normalize
for (int v = 0; v < N; ++v) {
glm::vec3 n{ m.normals[3*v+0], m.normals[3*v+1], m.normals[3*v+2] };
const float len = glm::length(n);
if (len > 1e-20f) {
n /= len;
} else {
n = glm::vec3(0, 0, 1); // fallback for degenerate spots
}
m.normals[3*v+0] = n.x;
m.normals[3*v+1] = n.y;
m.normals[3*v+2] = n.z;
}
}
return m;
}
} // namespace asset

39
src/Asset.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <vector>
#include <cstdint>
#include <functional>
namespace asset {
struct Mesh {
std::vector<float> positions; // 3*N
std::vector<float> normals; // 3*N (optional)
std::vector<float> uvs; // 2*N (optional)
std::vector<uint32_t> indices;
// (Optional) utility
bool hasNormals() const { return !normals.empty(); }
bool hasUVs() const { return !uvs.empty(); }
bool hasIndices() const { return !indices.empty(); }
};
Mesh make_unit_quad(float size = 1.0f);
Mesh make_unit_cube(float size = 1.0f);
Mesh make_unit_ico(float size = 1.0f);
Mesh make_unit_ico_flat(float size = 1.0f);
Mesh make_grid(int N, float extent);
// A terrain generator function type: (x, y) → height
using HeightFn = std::function<float(float, float)>;
// Build a terrain mesh of size (W x H) with given scale.
// - W,H: number of grid cells
// - dx,dz: spacing between grid points in x/z directions
Mesh make_terrain(int W, int H, float dx, float dz,
HeightFn heightFn,
bool genNormals = true,
bool genUVs = true,
float x0 = 0.0f, float z0 = 0.0f);
} // namespace asset

483
src/Graphics.cpp Normal file
View File

@@ -0,0 +1,483 @@
#include "Graphics.hpp"
#include "Asset.hpp"
#include "Render.hpp"
#include <fstream>
namespace Graphics {
Shader::~Shader() { destroy(); }
// non-blocking file timestamp check
bool Shader::poll() {
const auto mt = std::filesystem::exists(path)
? std::filesystem::last_write_time(path)
: std::filesystem::file_time_type{};
if (mt == mtime) return false;
return reload();
}
bool Shader::reload() {
// 1) Load source
std::ifstream f(path, std::ios::binary);
if (!f) {
std::fprintf(stderr, "[GLSL] %s : cannot open file\n", path.c_str());
return false;
}
std::ostringstream ss;
ss << f.rdbuf();
std::string src = ss.str();
if (src.empty()) {
std::fprintf(stderr, "[GLSL] %s : empty source\n", path.c_str());
return false;
}
std::fprintf(stdout, "[GLSL] %s : file ok\n", path.c_str());
// 2) Compile shader
const char* csrc = src.c_str();
GLuint sh = glCreateShader(gl_shader_enum(type));
glShaderSource(sh, 1, &csrc, nullptr);
glCompileShader(sh);
GLint ok = GL_FALSE;
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
if (!ok) {
GLint n = 0;
glGetShaderiv(sh, GL_INFO_LOG_LENGTH, &n);
std::string log(n ? n : 1, '\0');
glGetShaderInfoLog(sh, n, nullptr, log.data());
std::fprintf(stderr, "[GLSL COMPILE] %s\n%s\n", path.c_str(), log.c_str());
glDeleteShader(sh);
return false;
}
std::fprintf(stdout, "[GLSL] %s : compiled\n", path.c_str());
// 3) Link separable program
GLuint p = glCreateProgram();
glProgramParameteri(p, GL_PROGRAM_SEPARABLE, GL_TRUE);
glAttachShader(p, sh);
glLinkProgram(p);
glDetachShader(p, sh);
glDeleteShader(sh);
glGetProgramiv(p, GL_LINK_STATUS, &ok);
if (!ok) {
GLint n = 0;
glGetProgramiv(p, GL_INFO_LOG_LENGTH, &n);
std::string log(n ? n : 1, '\0');
glGetProgramInfoLog(p, n, nullptr, log.data());
std::fprintf(stderr, "[GLSL LINK] %s\n%s\n", path.c_str(), log.c_str());
glDeleteProgram(p);
return false;
}
std::fprintf(stdout, "[GLSL] %s : linked\n", path.c_str());
// 4) Swap program
if (prog) glDeleteProgram(prog);
prog = p;
// optional nice debug name
if (glObjectLabel) glObjectLabel(GL_PROGRAM, prog, -1, path.c_str());
// 5) Update timestamp
mtime = std::filesystem::exists(path) ? std::filesystem::last_write_time(path)
: std::filesystem::file_time_type{};
// 6) Reflect uniforms
ucache.clear();
GLint count = 0, maxLen = 0;
glGetProgramiv(prog, GL_ACTIVE_UNIFORMS, &count);
glGetProgramiv(prog, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxLen);
std::string name(maxLen ? maxLen : 1, '\0');
for (GLuint i = 0; i < (GLuint)count; ++i) {
GLsizei len = 0;
GLint size = 0;
GLenum active_type = 0;
glGetActiveUniform(prog, i, maxLen, &len, &size, &active_type, name.data());
name[len] = '\0';
if (name.rfind("gl_", 0) == 0) continue;
// If this uniform is inside a block, skip (no location API)
GLint blk = -1;
glGetActiveUniformsiv(prog, 1, &i, GL_UNIFORM_BLOCK_INDEX, &blk);
if (blk != -1) continue;
GLint loc = glGetUniformLocation(prog, name.c_str());
if (loc < 0) continue;
ucache[name] = loc;
auto pos = name.find("[0]");
if (pos != std::string::npos) ucache[name.substr(0, pos)] = loc;
}
std::fprintf(stdout, "[GLSL] %s : uniforms:\n", path.c_str());
for (auto uni : ucache) {
std::fprintf(stdout, " %d - %s\n", (int)uni.second, uni.first.c_str());
}
GLint numBlocks = 0;
glGetProgramInterfaceiv(prog, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES,
&numBlocks);
for (GLint b = 0; b < numBlocks; ++b) {
char buf[128];
GLsizei len = 0;
glGetProgramResourceName(prog, GL_UNIFORM_BLOCK, b, sizeof buf, &len, buf);
GLenum prop = GL_BUFFER_BINDING;
GLint binding = -1;
glGetProgramResourceiv(prog, GL_UNIFORM_BLOCK, b, 1, &prop, 1, nullptr,
&binding);
std::fprintf(stdout, " [block] %.*s @binding %d\n", len, buf, binding);
}
// 7) Notify pipelines
for (auto* pipe : subscribers)
if (pipe) pipe->reapply(*this);
return true;
}
void Shader::destroy() {
// Detach self from all pipelines
for (auto* pipe : subscribers)
if (pipe) pipe->detach(*this);
subscribers.clear();
if (prog) {
glDeleteProgram(prog);
prog = 0;
}
}
void Shader::subscribe(Pipeline* p) { subscribers.insert(p); }
void Shader::unsubscribe(Pipeline* p) { subscribers.erase(p); }
Pipeline::Pipeline() {
glCreateProgramPipelines(1, &id);
if (glObjectLabel)
glObjectLabel(GL_PROGRAM_PIPELINE, id, -1, "gren4-pipeline");
}
Pipeline::~Pipeline() {
if (stage.vert) stage.vert->unsubscribe(this);
if (stage.tesc) stage.tesc->unsubscribe(this);
if (stage.tese) stage.tese->unsubscribe(this);
if (stage.geom) stage.geom->unsubscribe(this);
if (stage.frag) stage.frag->unsubscribe(this);
if (stage.comp) stage.comp->unsubscribe(this);
if (id) glDeleteProgramPipelines(1, &id);
}
void Pipeline::set(Shader& s) {
// unsubscribe old occupant for this stage
switch (s.type) {
case ShaderType::Vertex:
if (stage.vert) stage.vert->unsubscribe(this);
stage.vert = &s;
break;
case ShaderType::TessCtrl:
if (stage.tesc) stage.tesc->unsubscribe(this);
stage.tesc = &s;
break;
case ShaderType::TessEval:
if (stage.tese) stage.tese->unsubscribe(this);
stage.tese = &s;
break;
case ShaderType::Geometry:
if (stage.geom) stage.geom->unsubscribe(this);
stage.geom = &s;
break;
case ShaderType::Fragment:
if (stage.frag) stage.frag->unsubscribe(this);
stage.frag = &s;
break;
case ShaderType::Compute:
if (stage.comp) stage.comp->unsubscribe(this);
stage.comp = &s;
break;
}
s.subscribe(this);
if (s.prog) glUseProgramStages(id, gl_stage_bit(s.type), s.prog);
}
void Pipeline::detach(const Shader& s) {
glUseProgramStages(id, gl_stage_bit(s.type), 0);
switch (s.type) {
case ShaderType::Vertex:
stage.vert = nullptr;
break;
case ShaderType::TessCtrl:
stage.tesc = nullptr;
break;
case ShaderType::TessEval:
stage.tese = nullptr;
break;
case ShaderType::Geometry:
stage.geom = nullptr;
break;
case ShaderType::Fragment:
stage.frag = nullptr;
break;
case ShaderType::Compute:
stage.comp = nullptr;
break;
}
}
void Pipeline::reapply(const Shader& s) {
glUseProgramStages(id, gl_stage_bit(s.type), s.prog);
}
void Pipeline::bind() const { glBindProgramPipeline(id); }
// Optional: quick validator during bring-up
bool Pipeline::validate_and_log() const {
glValidateProgramPipeline(id);
GLint ok = GL_FALSE;
glGetProgramPipelineiv(id, GL_VALIDATE_STATUS, &ok);
if (!ok) {
GLchar log[4096];
GLsizei len = 0;
glGetProgramPipelineInfoLog(id, sizeof log, &len, log);
std::fprintf(stderr, "[PIPELINE] validate failed:\n%.*s\n", len, log);
return false;
}
return true;
}
// Turn (type, path) into a stable, canonicalized key
static std::string make_key(ShaderType t, const std::string& path) {
std::filesystem::path p = std::filesystem::weakly_canonical(path);
// If canonicalization fails (nonexistent), fallback to lexically_normal
if (p.empty()) p = std::filesystem::path(path).lexically_normal();
return std::to_string(static_cast<int>(t)) + "|" + p.string();
}
// Acquire (create if missing) and return a stable Shader&
Shader& ShaderManager::get(ShaderType t, const std::string& path) {
const std::string key = make_key(t, path);
auto it = pool.find(key);
if (it != pool.end()) return *it->second;
auto sh = make_shader(t, path);
Shader& ref = *sh; // stable reference (owned by unique_ptr)
pool.emplace(key, std::move(sh));
return ref;
}
// Returns nullptr if not loaded (no create)
Shader* ShaderManager::find(ShaderType t, const std::string& path) const {
const std::string key = make_key(t, path);
auto it = pool.find(key);
return (it == pool.end()) ? nullptr : it->second.get();
}
// Force reload a specific shader if loaded. Returns true if reloaded.
bool ShaderManager::reload(ShaderType t, const std::string& path) {
Shader* s = find(t, path);
return s ? s->reload() : false;
}
// Poll all shaders; reloads those whose source file changed.
// Returns number of shaders that were reloaded.
size_t ShaderManager::poll_all() {
size_t n = 0;
for (auto& kv : pool) {
Shader* s = kv.second.get();
if (s && s->poll()) ++n;
}
return n;
}
// Remove a shader from the manager (detaches from pipelines via
// Shader::destroy()). Returns true if something was erased.
bool ShaderManager::erase(ShaderType t, const std::string& path) {
const std::string key = make_key(t, path);
auto it = pool.find(key);
if (it == pool.end()) return false;
// unique_ptr dtor calls Shader::~Shader -> destroy() -> detaches
// subscribers
pool.erase(it);
return true;
}
// Iterate over all shaders (read-only)
template <class F>
void ShaderManager::for_each(F&& f) const {
for (const auto& kv : pool) {
const Shader* s = kv.second.get();
if (s) f(*s);
}
}
// Iterate over all shaders (mutable)
template <class F>
void ShaderManager::for_each_mut(F&& f) {
for (auto& kv : pool) {
Shader* s = kv.second.get();
if (s) f(*s);
}
}
// Clear everything (detaches all from pipelines via destroy)
void ShaderManager::clear() { pool.clear(); }
static GLenum choose_index_type(const std::vector<uint32_t>& idx) {
if (idx.empty()) return GL_NONE;
// use 16-bit if possible to cut bandwidth
uint32_t maxv = 0;
for (uint32_t v : idx)
if (v > maxv) maxv = v;
return (maxv <= 0xFFFFu) ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT;
}
MeshGL::MeshGL() {
glGenVertexArrays(1, &vao);
// vbos/ebo created lazily in upload()
}
MeshGL::~MeshGL() {
if (ebo) glDeleteBuffers(1, &ebo);
if (vbo_uv) glDeleteBuffers(1, &vbo_uv);
if (vbo_nrm) glDeleteBuffers(1, &vbo_nrm);
if (vbo_pos) glDeleteBuffers(1, &vbo_pos);
if (vao) glDeleteVertexArrays(1, &vao);
}
MeshGL::MeshGL(MeshGL&& o) noexcept { *this = std::move(o); }
MeshGL& MeshGL::operator=(MeshGL&& o) noexcept {
if (this != &o) {
std::swap(vao, o.vao);
std::swap(vbo_pos, o.vbo_pos);
std::swap(vbo_nrm, o.vbo_nrm);
std::swap(vbo_uv, o.vbo_uv);
std::swap(ebo, o.ebo);
std::swap(count, o.count);
std::swap(mode, o.mode);
std::swap(indexType, o.indexType);
std::swap(loc_pos, o.loc_pos);
std::swap(loc_nrm, o.loc_nrm);
std::swap(loc_uv, o.loc_uv);
}
return *this;
}
bool MeshGL::upload(const asset::Mesh& m, bool dynamic) {
if (m.positions.empty() || (m.positions.size() % 3) != 0) return false;
const GLenum usage = dynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW;
glBindVertexArray(vao);
// positions (required)
if (!vbo_pos) glGenBuffers(1, &vbo_pos);
glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
glBufferData(GL_ARRAY_BUFFER, m.positions.size() * sizeof(float),
m.positions.data(), usage);
if (loc_pos >= 0) {
glEnableVertexAttribArray(loc_pos);
glVertexAttribPointer(loc_pos, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
}
// normals (optional)
if (!m.normals.empty()) {
if (!vbo_nrm) glGenBuffers(1, &vbo_nrm);
glBindBuffer(GL_ARRAY_BUFFER, vbo_nrm);
glBufferData(GL_ARRAY_BUFFER, m.normals.size() * sizeof(float),
m.normals.data(), usage);
if (loc_nrm >= 0) {
glEnableVertexAttribArray(loc_nrm);
glVertexAttribPointer(loc_nrm, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
}
} else if (vbo_nrm && loc_nrm >= 0) {
glDisableVertexAttribArray(loc_nrm);
}
// uvs (optional)
if (!m.uvs.empty()) {
if (!vbo_uv) glGenBuffers(1, &vbo_uv);
glBindBuffer(GL_ARRAY_BUFFER, vbo_uv);
glBufferData(GL_ARRAY_BUFFER, m.uvs.size() * sizeof(float), m.uvs.data(),
usage);
if (loc_uv >= 0) {
glEnableVertexAttribArray(loc_uv);
glVertexAttribPointer(loc_uv, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float),
(void*)0);
}
} else if (vbo_uv && loc_uv >= 0) {
glDisableVertexAttribArray(loc_uv);
}
// indices (optional)
indexType = choose_index_type(m.indices);
if (indexType != GL_NONE) {
if (!ebo) glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
if (indexType == GL_UNSIGNED_SHORT) {
// pack to 16-bit
std::vector<uint16_t> idx16(m.indices.size());
for (size_t i = 0; i < m.indices.size(); ++i)
idx16[i] = static_cast<uint16_t>(m.indices[i]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx16.size() * sizeof(uint16_t),
idx16.data(), usage);
count = static_cast<GLsizei>(idx16.size());
} else {
glBufferData(GL_ELEMENT_ARRAY_BUFFER, m.indices.size() * sizeof(uint32_t),
m.indices.data(), usage);
count = static_cast<GLsizei>(m.indices.size());
}
} else {
// non-indexed draw
if (ebo) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
count = static_cast<GLsizei>(m.positions.size() / 3);
}
glBindVertexArray(0);
return true;
}
void MeshGL::draw() const {
glBindVertexArray(vao);
if (indexType != GL_NONE) {
glDrawElements(mode, count, indexType, (void*)0);
} else {
glDrawArrays(mode, 0, count);
}
}
void MeshGL::drawInstanced(GLsizei instances) const {
glBindVertexArray(vao);
if (indexType != GL_NONE) {
glDrawElementsInstanced(mode, count, indexType, (void*)0, instances);
} else {
glDrawArraysInstanced(mode, 0, count, instances);
}
}
UniformBuffer::UniformBuffer(GLsizeiptr sizeBytes, GLuint bindingPoint) {
create(sizeBytes, bindingPoint);
}
UniformBuffer::~UniformBuffer() {
if (id) glDeleteBuffers(1, &id);
}
void UniformBuffer::create(GLsizeiptr sizeBytes, GLuint bindingPoint) {
size = sizeBytes;
binding = bindingPoint;
glGenBuffers(1, &id);
glBindBuffer(GL_UNIFORM_BUFFER, id);
glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, binding, id);
}
void Graphics::UniformBuffer::update_bytes(const void* data, GLsizeiptr bytes) {
glBindBuffer(GL_UNIFORM_BUFFER, id);
glBufferSubData(GL_UNIFORM_BUFFER, 0, bytes, data);
}
void UboSet::init() {
perFrame.create(sizeof(Render::PerFrame), 0);
perObject.create(sizeof(Render::PerObject), 1);
material.create(sizeof(Render::Material), 2);
}
} // namespace Graphics

232
src/Graphics.hpp Normal file
View File

@@ -0,0 +1,232 @@
#pragma once
#include "glad/glad.h"
#include <string>
#include <filesystem>
#include <unordered_map>
#include <unordered_set>
// --- forward declare to avoid pulling Asset.hpp into this header ---
namespace asset {
struct Mesh;
}
namespace Graphics {
enum class ShaderType {
Vertex,
TessCtrl,
TessEval,
Geometry,
Fragment,
Compute
};
struct Pipeline; // fwd
inline GLenum gl_shader_enum(ShaderType t) {
switch (t) {
case ShaderType::Vertex:
return GL_VERTEX_SHADER;
case ShaderType::TessCtrl:
return GL_TESS_CONTROL_SHADER;
case ShaderType::TessEval:
return GL_TESS_EVALUATION_SHADER;
case ShaderType::Geometry:
return GL_GEOMETRY_SHADER;
case ShaderType::Fragment:
return GL_FRAGMENT_SHADER;
case ShaderType::Compute:
return GL_COMPUTE_SHADER;
}
return 0;
}
inline GLbitfield gl_stage_bit(ShaderType t) {
switch (t) {
case ShaderType::Vertex:
return GL_VERTEX_SHADER_BIT;
case ShaderType::TessCtrl:
return GL_TESS_CONTROL_SHADER_BIT;
case ShaderType::TessEval:
return GL_TESS_EVALUATION_SHADER_BIT;
case ShaderType::Geometry:
return GL_GEOMETRY_SHADER_BIT;
case ShaderType::Fragment:
return GL_FRAGMENT_SHADER_BIT;
case ShaderType::Compute:
return GL_COMPUTE_SHADER_BIT;
}
return 0;
}
struct Shader {
ShaderType type{};
std::string path;
GLuint prog{0};
std::filesystem::file_time_type mtime{};
std::unordered_map<std::string, GLint> ucache;
std::unordered_set<Pipeline*> subscribers;
Shader() = default;
~Shader();
Shader(const Shader&) = delete;
Shader& operator=(const Shader&) = delete;
Shader(Shader&&) = delete;
Shader& operator=(Shader&&) = delete;
bool poll();
bool reload();
void destroy();
void subscribe(Pipeline* p);
void unsubscribe(Pipeline* p);
// uniform helpers
inline GLint u(const char* name) const {
if (!prog) return -1;
if (auto it = ucache.find(name); it != ucache.end()) return it->second;
return glGetUniformLocation(prog, name);
}
inline void set1f(const char* n, float x) const {
if (prog) glProgramUniform1f(prog, u(n), x);
}
inline void set3f(const char* n, float x, float y, float z) const {
if (prog) glProgramUniform3f(prog, u(n), x, y, z);
}
inline void set1i(const char* n, int x) const {
if (prog) glProgramUniform1i(prog, u(n), x);
}
inline void setMat4(const char* n, const float* m) const {
if (prog) glProgramUniformMatrix4fv(prog, u(n), 1, GL_FALSE, m);
}
};
struct Pipeline {
GLuint id{0};
struct {
Shader* vert{};
Shader* tesc{};
Shader* tese{};
Shader* geom{};
Shader* frag{};
Shader* comp{};
} stage{};
Pipeline();
~Pipeline();
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
Pipeline(Pipeline&&) = delete;
Pipeline& operator=(Pipeline&&) = delete;
void set(Shader& s);
void detach(const Shader& s);
void reapply(const Shader& s);
void bind() const;
// Optional: quick validator during bring-up
bool validate_and_log() const;
};
inline std::unique_ptr<Shader> make_shader(ShaderType t, std::string path) {
auto sh = std::make_unique<Shader>();
sh->type = t;
sh->path = std::move(path);
if (!sh->reload()) throw std::runtime_error("shader compile failed");
return sh;
}
inline std::unique_ptr<Pipeline> make_pipeline() {
return std::make_unique<Pipeline>();
}
struct ShaderManager {
// key = "<type>|<abs-path>"
struct KeyHash {
using is_transparent = void;
size_t operator()(const std::string& s) const noexcept {
return std::hash<std::string>{}(s);
}
};
std::unordered_map<std::string, std::unique_ptr<Shader>, KeyHash> pool;
Shader& get(ShaderType t, const std::string& path);
Shader* find(ShaderType t, const std::string& path) const;
bool reload(ShaderType t, const std::string& path);
size_t poll_all();
bool erase(ShaderType t, const std::string& path);
template <class F>
void for_each(F&& f) const;
template <class F>
void for_each_mut(F&& f);
void clear();
};
struct MeshGL {
// GL objects
GLuint vao{0};
GLuint vbo_pos{0};
GLuint vbo_nrm{0};
GLuint vbo_uv{0};
GLuint ebo{0};
// draw info
GLsizei count{0}; // index count or vertex count
GLenum mode{GL_TRIANGLES};
GLenum indexType{
GL_NONE}; // GL_UNSIGNED_INT/SHORT, or GL_NONE if non-indexed
// attribute locations (match your shaders: aPos=0, aNormal=1, aUV=2)
GLint loc_pos{0};
GLint loc_nrm{1};
GLint loc_uv{2};
MeshGL();
~MeshGL();
MeshGL(const MeshGL&) = delete;
MeshGL& operator=(const MeshGL&) = delete;
MeshGL(MeshGL&& o) noexcept;
MeshGL& operator=(MeshGL&& o) noexcept;
// Upload from CPU mesh. 'dynamic' picks GL_STATIC_DRAW vs GL_DYNAMIC_DRAW.
// Returns false if positions are missing/invalid.
bool upload(const asset::Mesh& m, bool dynamic = false);
void draw() const;
void drawInstanced(GLsizei instances) const;
};
struct UniformBuffer {
GLuint id{0};
GLsizeiptr size{0};
GLuint binding{0};
UniformBuffer() = default;
UniformBuffer(GLsizeiptr sizeBytes, GLuint bindingPoint);
~UniformBuffer();
UniformBuffer(const UniformBuffer&) = delete;
UniformBuffer& operator=(const UniformBuffer&) = delete;
void create(GLsizeiptr sizeBytes, GLuint bindingPoint);
void update_bytes(const void* data, GLsizeiptr bytes);
template <class T>
void update(const T& data) {
static_assert(!std::is_pointer_v<T>, "Pass the object, not a pointer");
update_bytes(&data, static_cast<GLsizeiptr>(sizeof(T)));
}
};
struct UboSet {
UniformBuffer perFrame; // binding = 0
UniformBuffer perObject; // binding = 1
UniformBuffer material; // binding = 2
void init();
};
} // namespace Graphics

20
src/Render.hpp Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <glm/glm.hpp>
namespace Render {
struct alignas(16) PerFrame {
glm::mat4 V;
glm::mat4 P;
glm::vec4 LightDir; // std140: use vec4, vec3 would pad anyway
};
struct alignas(16) PerObject {
glm::mat4 M;
};
struct alignas(16) Material {
glm::vec4 Albedo; // rgb + pad
};
} // namespace render

38
src/Scene.cpp Normal file
View File

@@ -0,0 +1,38 @@
#include "Scene.hpp"
namespace Scene {
glm::vec3 OrbitCamera::eye() const {
const float ce = std::cos(elevation), se = std::sin(elevation);
const float ca = std::cos(azimuth), sa = std::sin(azimuth);
return target + radius * glm::vec3(ce * ca, ce * sa, se);
}
glm::mat4 OrbitCamera::view() const {
return glm::lookAt(eye(), target, glm::vec3(0, 0, 1)); // Z-up
}
glm::mat4 OrbitCamera::proj() const {
return glm::perspective(glm::radians(fov_deg), aspect, nearZ, farZ);
}
// controls (delta-based, feed from input)
void OrbitCamera::dolly(float dr) {
radius = std::max(0.05f, radius * std::exp(-dr));
}
void OrbitCamera::rotate(float dAzim, float dElev) {
azimuth += dAzim;
elevation = std::clamp(elevation + dElev, -1.55f, +1.55f);
}
void OrbitCamera::pan_world(const glm::vec2& d) {
const float ce = std::cos(elevation), se = std::sin(elevation);
const float ca = std::cos(azimuth), sa = std::sin(azimuth);
glm::vec3 right = glm::normalize(glm::vec3(-sa, ca, 0.0f));
glm::vec3 up = glm::normalize(glm::vec3(-ca * se, -sa * se, ce));
target += (-d.x * right + d.y * up);
}
// keep aspect updated on resize
void OrbitCamera::set_aspect_from_pixels(int W, int H) {
aspect = (H > 0) ? float(W) / float(H) : 1.0f;
}
} // namespace Scene

35
src/Scene.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
namespace Scene {
// Z-up orbit camera: XY ground, Z = up
struct OrbitCamera {
// orbit state
float radius = 5.0f; // distance from target
float azimuth = 0.7f; // radians, around +Z
float elevation = 0.35f; // radians, above XY plane
glm::vec3 target{0.0f, 0.0f, 0.0f};
// projection state
float fov_deg = 37.0f;
float aspect = 16.0f / 9.0f;
float nearZ = 0.1f;
float farZ = 100.0f;
// matrices
glm::mat4 view() const;
glm::mat4 proj() const;
glm::vec3 eye() const;
// controls (delta-based, feed from input)
void dolly(float dr);
void rotate(float dAzim, float dElev);
void pan_world(const glm::vec2& d_world);
// keep aspect updated on resize
void set_aspect_from_pixels(int W, int H);
};
} // namespace scene

269
src/glfwx.hpp Normal file
View File

@@ -0,0 +1,269 @@
#pragma once
#include "glad/glad.h"
#include "GLFW/glfw3.h"
#include <cassert>
#include <stdexcept>
#include <thread>
#include <cstdio>
#include "Scene.hpp"
namespace glfwx {
struct Guard {
Guard() {
glfwSetErrorCallback([](int c, const char* d) {
std::fprintf(stderr, "GLFW error %d: %s\n", c, d);
});
if (!glfwInit()) throw std::runtime_error("glfwInit failed");
}
~Guard() { glfwTerminate(); }
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
};
struct Window {
explicit Window(int w, int h, const char* title, GLFWmonitor* mon = nullptr,
GLFWwindow* share = nullptr) {
handle = glfwCreateWindow(w, h, title, mon, share);
if (!handle) throw std::runtime_error("glfwCreateWindow failed");
owner = std::this_thread::get_id();
}
~Window() {
if (handle) {
ensure("~Window");
glfwDestroyWindow(handle);
}
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
Window(Window&&) = delete;
Window& operator=(Window&&) = delete;
GLFWwindow* get() const {
ensure("get");
return handle;
}
void makeCurrent() const {
ensure("makeCurrent");
glfwMakeContextCurrent(handle);
}
void swap() const {
ensure("swap");
glfwSwapBuffers(handle);
}
bool shouldClose() const {
ensure("shouldClose");
return glfwWindowShouldClose(handle);
}
void vsync(int interval) const {
makeCurrent();
glfwSwapInterval(interval);
}
private:
void ensure(const char*) const {
assert(std::this_thread::get_id() == owner &&
"Window used from non-owner thread");
}
GLFWwindow* handle = nullptr;
std::thread::id owner{};
};
inline void set_common_hints() {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifndef NDEBUG
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#else
glfwWindowHint(GLFW_CONTEXT_NO_ERROR, GLFW_TRUE);
#endif
//glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE);
glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE);
glfwWindowHint(GLFW_SAMPLES, 4);
}
inline void load_gl_or_throw() {
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
throw std::runtime_error("gladLoadGLLoader failed");
GLint maj = 0, min = 0;
glGetIntegerv(GL_MAJOR_VERSION, &maj);
glGetIntegerv(GL_MINOR_VERSION, &min);
if (maj < 4 || (maj == 4 && min < 5))
throw std::runtime_error("OpenGL 4.5+ Core required");
printf("GL %d.%d | %s | %s\n", maj, min, glGetString(GL_VENDOR),
glGetString(GL_RENDERER));
}
#ifndef NDEBUG
inline void enable_khr_debug() {
GLint flags = 0;
glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) {
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(
[](GLenum, GLenum t, GLuint id, GLenum sev, GLsizei, const GLchar* msg,
const void*) {
if (sev == GL_DEBUG_SEVERITY_NOTIFICATION) return;
std::fprintf(stderr, "[GL %u] type=0x%x sev=0x%x: %s\n", id, t, sev,
msg);
},
nullptr);
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE,
GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, GL_FALSE);
}
}
#endif
inline void install_basic_callbacks(GLFWwindow* window) {
glfwSetFramebufferSizeCallback(window, [](GLFWwindow*, int W, int H) {
std::printf("Window resized to %d × %d\n", W, H);
glViewport(0, 0, W, H);
// If you rely on scissoring globally, keep it coherent:
// glScissor(0, 0, W, H);
});
glfwSetKeyCallback(window, [](GLFWwindow* win, int key, int /*scancode*/,
int action, int /*mods*/) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(win, GLFW_TRUE);
});
glfwSetInputMode(window, GLFW_LOCK_KEY_MODS, GLFW_TRUE);
if (glfwRawMouseMotionSupported())
glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
}
struct CameraInput {
Scene::OrbitCamera* cam{nullptr};
// state
bool rotating{false};
bool panning{false};
double lastX{0.0}, lastY{0.0};
// tunables
float rotSens{0.005f}; // radians per pixel
float panSpeed{1.0f}; // world units per normalized screen unit
float zoomSens{0.12f}; // dolly factor per wheel notch
// previous callbacks for chaining
GLFWcursorposfun prevCursor{};
GLFWscrollfun prevScroll{};
GLFWmousebuttonfun prevMouse{};
GLFWframebuffersizefun prevResize{};
};
inline CameraInput* camState(GLFWwindow* w) {
return static_cast<CameraInput*>(glfwGetWindowUserPointer(w));
}
inline void install_camera_controls(GLFWwindow* win, Scene::OrbitCamera& cam) {
auto* st = new CameraInput{};
st->cam = &cam;
// Chain existing callbacks
st->prevCursor = glfwSetCursorPosCallback(win, nullptr);
st->prevScroll = glfwSetScrollCallback(win, nullptr);
st->prevMouse = glfwSetMouseButtonCallback(win, nullptr);
st->prevResize = glfwSetFramebufferSizeCallback(win, nullptr);
glfwSetWindowUserPointer(win, st);
// Framebuffer resize -> keep camera aspect in sync (and chain)
glfwSetFramebufferSizeCallback(win, [](GLFWwindow* w, int W, int H) {
if (auto* s = camState(w)) {
s->cam->set_aspect_from_pixels(W, H);
if (s->prevResize) s->prevResize(w, W, H);
}
});
// Mouse button: start/stop rotate/pan
glfwSetMouseButtonCallback(
win, [](GLFWwindow* w, int button, int action, int mods) {
auto* s = camState(w);
if (!s) return;
if (action == GLFW_PRESS) {
double x, y;
glfwGetCursorPos(w, &x, &y);
s->lastX = x;
s->lastY = y;
bool shift = (mods & GLFW_MOD_SHIFT) != 0;
if (button == GLFW_MOUSE_BUTTON_LEFT && !shift) {
s->rotating = true;
glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
} else if (button == GLFW_MOUSE_BUTTON_MIDDLE ||
(button == GLFW_MOUSE_BUTTON_LEFT && shift)) {
s->panning = true;
// keep cursor visible for panning
}
} else if (action == GLFW_RELEASE) {
if (button == GLFW_MOUSE_BUTTON_LEFT) {
s->rotating = false;
glfwSetInputMode(w, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}
if (button == GLFW_MOUSE_BUTTON_MIDDLE ||
button == GLFW_MOUSE_BUTTON_LEFT) {
s->panning = false;
}
}
if (s->prevMouse) s->prevMouse(w, button, action, mods);
});
// Mouse move: apply rotate/pan
glfwSetCursorPosCallback(win, [](GLFWwindow* w, double x, double y) {
auto* s = camState(w);
if (!s) return;
double dx = x - s->lastX;
double dy = y - s->lastY;
s->lastX = x;
s->lastY = y;
if (s->rotating) {
s->cam->rotate(float(dx) * s->rotSens, float(-dy) * s->rotSens);
} else if (s->panning) {
int W, H;
glfwGetWindowSize(w, &W, &H); // <— not GetFramebufferSize
float vfov = glm::radians(s->cam->fov_deg);
float half_h = s->cam->radius * std::tan(vfov * 0.5f);
float world_per_px_y = (2.f * half_h) / float(std::max(H, 1));
float world_per_px_x = world_per_px_y * s->cam->aspect;
glm::vec2 d_world(float(dx) * world_per_px_x * s->panSpeed,
float(dy) * world_per_px_y * s->panSpeed);
s->cam->pan_world(d_world);
}
if (s->prevCursor) s->prevCursor(w, x, y);
});
// Scroll: dolly in/out (zoom)
glfwSetScrollCallback(win, [](GLFWwindow* w, double /*xoff*/, double yoff) {
auto* s = camState(w);
if (!s) return;
s->cam->dolly(float(yoff) * s->zoomSens);
if (s->prevScroll) s->prevScroll(w, 0.0, yoff);
});
// Initialize aspect once
int W, H;
glfwGetFramebufferSize(win, &W, &H);
cam.set_aspect_from_pixels(W, H);
}
inline void set_default_gl_state() {
glEnable(GL_DEPTH_TEST);
glEnable(GL_MULTISAMPLE);
glDisable(GL_FRAMEBUFFER_SRGB);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
}
} // namespace glfwx

178
src/main.cpp Normal file
View File

@@ -0,0 +1,178 @@
#include "glfwx.hpp"
#include "Graphics.hpp"
#include "Asset.hpp"
#include "Scene.hpp"
#include "Render.hpp"
#include <cmath>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <functional>
int main() try {
glfwx::Guard g;
glfwx::set_common_hints();
glfwx::Window win(1366, 768, "gren3");
win.makeCurrent();
glfwx::load_gl_or_throw();
#ifndef NDEBUG
glfwx::enable_khr_debug();
#endif
int fbw, fbh;
glfwGetFramebufferSize(win.get(), &fbw, &fbh);
glViewport(0, 0, fbw, fbh);
glfwx::install_basic_callbacks(win.get());
Scene::OrbitCamera cam;
glfwx::install_camera_controls(win.get(), cam);
glfwx::set_default_gl_state();
Graphics::ShaderManager shaders;
auto& vShader =
shaders.get(Graphics::ShaderType::Vertex, "assets/shaders/mesh.vert");
auto& fShader =
shaders.get(Graphics::ShaderType::Fragment, "assets/shaders/mesh.frag");
auto pipe = Graphics::make_pipeline();
pipe->set(vShader);
pipe->set(fShader);
auto& vTerrain =
shaders.get(Graphics::ShaderType::Vertex, "assets/shaders/terrain.vert");
auto& fTerrain = shaders.get(Graphics::ShaderType::Fragment,
"assets/shaders/terrain.frag");
auto terrainPipe = Graphics::make_pipeline();
terrainPipe->set(vTerrain);
terrainPipe->set(fTerrain);
auto& vSkybox =
shaders.get(Graphics::ShaderType::Vertex, "assets/shaders/skybox.vert");
auto& fSkybox =
shaders.get(Graphics::ShaderType::Fragment, "assets/shaders/skybox.frag");
auto skyPipe = Graphics::make_pipeline();
skyPipe->set(vSkybox);
skyPipe->set(fSkybox);
Graphics::UboSet ubos;
ubos.init();
auto terrain = asset::make_terrain(
256, 256, 1.0f, 1.0f,
[](float x, float z) {
return std::sin(x * 0.05f) * std::cos(z * 0.05f) * 5.0f;
},
/*genNormals=*/true,
/*genUVs=*/true,
/*x0=*/-128.0f, /*z0=*/-128.0f
);
Graphics::MeshGL gpuTerrain;
gpuTerrain.upload(terrain);
asset::Mesh quad = asset::make_unit_quad(10);
Graphics::MeshGL gpuQuad;
gpuQuad.upload(quad);
asset::Mesh cube = asset::make_unit_cube();
Graphics::MeshGL gpuCamTargetCube;
gpuCamTargetCube.upload(cube);
Graphics::MeshGL gpuSkyCube;
gpuSkyCube.upload(cube);
asset::Mesh ico2 = asset::make_unit_ico_flat();
Graphics::MeshGL gpuIco2;
gpuIco2.upload(ico2);
win.vsync(1);
double last = glfwGetTime(), acc = 0;
int frames = 0;
Render::Material whiteMaterial{glm::vec4(1.0f)};
Render::Material blueMaterial{glm::vec4(0, 0, 1, 1)};
Render::Material greenMaterial{glm::vec4(0, 0.4f, 0, 1)};
while (!win.shouldClose()) {
double now = glfwGetTime(), dt = now - last;
last = now;
acc += dt;
frames++;
if (acc >= 1.0) {
std::printf(" FPS = %.2f\r", frames / acc);
std::fflush(stdout);
acc = 0;
frames = 0;
}
glfwPollEvents();
// hot reload (manual polling or later via inotify)
shaders.poll_all();
int W, H;
glfwGetFramebufferSize(win.get(), &W, &H);
cam.set_aspect_from_pixels(W, H);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Render::PerFrame pf{cam.view(), cam.proj(),
glm::vec4(-1.0f, -1.0f, -1.0f, 0.0f)};
ubos.perFrame.update(pf);
ubos.material.update(whiteMaterial);
// SKY PASS
Render::PerObject poSky{glm::scale(glm::mat4(1.0f), glm::vec3(3.0f))};
ubos.perObject.update(poSky); // binding=1, same as meshes
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);
glFrontFace(GL_CW); // or flip front face for inside faces
skyPipe->bind();
gpuSkyCube.draw();
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glFrontFace(GL_CCW);
{
// Fetch the target point from your orbit cam.
// Adjust the accessor name if yours differs (e.g., cam.getTarget(), cam.focus()).
glm::vec3 target = cam.target;
// Build the model matrix: translate to the target and scale small so its a marker.
glm::mat4 M_target =
glm::translate(glm::mat4(1.0f), target) *
glm::scale(glm::mat4(1.0f), glm::vec3(0.05f)); // tweak size to taste
// Update UBOs for this object + material.
Render::PerObject poTarget{ M_target };
ubos.perObject.update(poTarget);
Render::Material targetMat{ glm::vec4(1, 0, 0, 1) }; // red so it pops
ubos.material.update(targetMat);
// Bind your regular mesh pipeline and draw the cube.
pipe->bind();
gpuCamTargetCube.draw();
}
// MESH PASS
Render::PerObject po{glm::mat4(1.0f)};
ubos.perObject.update(po);
pipe->bind();
ubos.material.update(greenMaterial);
gpuTerrain.draw();
ubos.material.update(whiteMaterial);
gpuIco2.draw();
win.swap();
}
std::puts("");
return 0;
} catch (const std::exception& e) {
std::fprintf(stderr, "%s\n", e.what());
return 1;
}