first commit
This commit is contained in:
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "external/glm"]
|
||||
path = external/glm
|
||||
url = https://github.com/g-truc/glm.git
|
||||
7
LICENSE
Normal file
7
LICENSE
Normal 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
64
Makefile
Normal 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
152
README.md
Normal 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
35
assets/shaders/mesh.frag
Normal 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
45
assets/shaders/mesh.vert
Normal 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;
|
||||
}
|
||||
33
assets/shaders/skybox.frag
Normal file
33
assets/shaders/skybox.frag
Normal 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; we’ll 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 isn’t sRGB)
|
||||
}
|
||||
31
assets/shaders/skybox.vert
Normal file
31
assets/shaders/skybox.vert
Normal 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);
|
||||
}
|
||||
39
assets/shaders/terrain.frag
Normal file
39
assets/shaders/terrain.frag
Normal 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);
|
||||
}
|
||||
45
assets/shaders/terrain.vert
Normal file
45
assets/shaders/terrain.vert
Normal 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
311
external/glad/include/KHR/khrplatform.h
vendored
Normal 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
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
1819
external/glad/src/glad.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
external/glm
vendored
Submodule
1
external/glm
vendored
Submodule
Submodule external/glm added at 2d4c4b4dd3
343
src/Asset.cpp
Normal file
343
src/Asset.cpp
Normal 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 (we’ll 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
39
src/Asset.hpp
Normal 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
483
src/Graphics.cpp
Normal 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
232
src/Graphics.hpp
Normal 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
20
src/Render.hpp
Normal 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
38
src/Scene.cpp
Normal 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
35
src/Scene.hpp
Normal 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
269
src/glfwx.hpp
Normal 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
178
src/main.cpp
Normal 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 it’s 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;
|
||||
}
|
||||
Reference in New Issue
Block a user