Загрузка данных
#define _CRT_SECURE_NO_WARNINGS
#define NOMINMAX
#include <Windows.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <vector>
#include <queue>
#include <string>
#include <algorithm>
/* ═══════════════════════════════════════════════════════
КОНСТАНТЫ
═══════════════════════════════════════════════════════ */
constexpr int MAZE_W = 13; /* размер лабиринта (нечётное!) */
constexpr int MAZE_H = 13;
constexpr float CELL_W = 3.5f; /* ширина клетки в мировых единицах */
constexpr float WALL_H = 2.8f; /* высота стены */
constexpr float EYE_H = 1.4f; /* высота камеры (рост игрока) */
constexpr float PLAYER_R = 0.28f; /* радиус коллизии игрока */
constexpr float PI_F = 3.14159265358979f;
constexpr int SHADOW_SIZE = 1024; /* разрешение карты теней */
/* Биты стен клетки */
constexpr uint8_t W_N = 0x01, W_S = 0x02, W_E = 0x04, W_W = 0x08, VIS = 0x10;
constexpr int DX[4] = { 0, 0, 1, -1 };
constexpr int DY[4] = { -1, 1, 0, 0 };
constexpr uint8_t DIR_W[4] = { W_N, W_S, W_E, W_W };
constexpr uint8_t OPP_W[4] = { W_S, W_N, W_W, W_E };
/* ═══════════════════════════════════════════════════════
GLSL — ШЕЙДЕР ГЛУБИНЫ (первый проход: карта теней)
═══════════════════════════════════════════════════════ */
static const char* DEPTH_VERT = R"GLSL(
#version 120
attribute vec3 aPos;
uniform mat4 uLightSpace;
uniform mat4 uModel;
void main() {
gl_Position = uLightSpace * uModel * vec4(aPos, 1.0);
}
)GLSL";
static const char* DEPTH_FRAG = R"GLSL(
#version 120
void main() { /* Глубина пишется автоматически */ }
)GLSL";
/* ═══════════════════════════════════════════════════════
GLSL — ОСНОВНОЙ ШЕЙДЕР (второй проход: освещение + тени)
═══════════════════════════════════════════════════════ */
static const char* MAIN_VERT = R"GLSL(
#version 120
attribute vec3 aPos;
attribute vec3 aNormal;
uniform mat4 uProj;
uniform mat4 uView;
uniform mat4 uModel;
uniform mat4 uLightSpace;
varying vec3 vFragPos;
varying vec3 vNormal;
varying vec4 vFragPosLight;
void main() {
vec4 world = uModel * vec4(aPos, 1.0);
vFragPos = world.xyz;
vNormal = normalize(mat3(uModel) * aNormal);
vFragPosLight = uLightSpace * world;
gl_Position = uProj * uView * world;
}
)GLSL";
static const char* MAIN_FRAG = R"GLSL(
#version 120
varying vec3 vFragPos;
varying vec3 vNormal;
varying vec4 vFragPosLight;
uniform vec3 uColor;
uniform vec3 uLightDir; /* нормаль: ОТ фрагмента К источнику */
uniform vec3 uSunColor;
uniform vec3 uTorchPos; /* позиция факела (камеры игрока) */
uniform vec3 uTorchColor;
uniform float uTorchBright; /* яркость с флик-эффектом */
uniform vec3 uViewPos;
uniform sampler2D uShadowMap;
uniform float uFogDensity;
uniform vec3 uFogColor;
/* PCF: мягкие тени 3x3 */
float calcShadow() {
vec3 proj = vFragPosLight.xyz / vFragPosLight.w * 0.5 + 0.5;
if (proj.z > 1.0) return 0.0;
float bias = max(0.012 * (1.0 - dot(vNormal, uLightDir)), 0.004);
float shadow = 0.0;
float ts = 1.0 / float(1024);
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
float d = texture2D(uShadowMap, proj.xy + vec2(float(x), float(y)) * ts).r;
shadow += (proj.z - bias > d) ? 1.0 : 0.0;
}
}
return shadow / 9.0;
}
void main() {
/* Процедурные текстуры для поверхностей (с увеличенным контрастом!) */
vec3 texColor = uColor;
if (vNormal.y > 0.5) { // Пол: средняя шахматка
float ch = mod(floor(vFragPos.x * 1.0) + floor(vFragPos.z * 1.0), 2.0);
texColor = mix(uColor, uColor * 0.4, ch);
} else if (vNormal.y < -0.5) { // Потолок: крупная плита
float ch = mod(floor(vFragPos.x * 0.5) + floor(vFragPos.z * 0.5), 2.0);
texColor = mix(uColor, uColor * 0.3, ch);
} else { // Стены: кирпичи
if (uColor.b < 0.6) { // Пропускаем игрока (у одежды b=0.9)
float bx = floor(vFragPos.x * 2.0 + vFragPos.z * 2.0);
float by = floor(vFragPos.y * 2.0);
float ch = mod(bx + by, 2.0);
texColor = mix(uColor, uColor * 0.55, ch);
}
}
/* Фоновое освещение: увеличили с 0.20 до 0.45, чтобы черных зон не было вообще! */
vec3 color = 0.45 * uSunColor;
/* Направленный свет с тенями */
float diff = max(dot(vNormal, uLightDir), 0.0);
float sh = calcShadow();
color += (1.0 - sh * 0.8) * diff * uSunColor * 0.6;
/* Факел — точечный источник без тени */
vec3 toT = uTorchPos - vFragPos;
float tDist = length(toT);
/* Еще больше увеличил дальность: коэффициент 0.025 */
float tAtt = uTorchBright / (1.0 + 0.08*tDist + 0.025*tDist*tDist);
float tDiff = max(dot(vNormal, normalize(toT)), 0.0);
color += tDiff * tAtt * uTorchColor;
/* Блик от факела (Blinn-Phong) */
vec3 viewDir = normalize(uViewPos - vFragPos);
vec3 halfV = normalize(normalize(toT) + viewDir);
float spec = pow(max(dot(vNormal, halfV), 0.0), 32.0);
color += spec * 0.5 * tAtt * uTorchColor;
color *= texColor;
/* Туман (exponential²) */
float dist = length(uViewPos - vFragPos);
float fogFactor = 1.0 - exp(-uFogDensity * uFogDensity * dist * dist);
color = mix(color, uFogColor, clamp(fogFactor, 0.0, 1.0));
gl_FragColor = vec4(color, 1.0);
}
)GLSL";
/* ═══════════════════════════════════════════════════════
МАТЕМАТИКА: Vec3 и Mat4
═══════════════════════════════════════════════════════ */
struct Vec3 {
float x = 0, y = 0, z = 0;
Vec3() = default;
Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
Vec3 operator+(const Vec3& o) const { return { x+o.x, y+o.y, z+o.z }; }
Vec3 operator-(const Vec3& o) const { return { x-o.x, y-o.y, z-o.z }; }
Vec3 operator*(float s) const { return { x*s, y*s, z*s }; }
Vec3& operator+=(const Vec3& o) { x+=o.x; y+=o.y; z+=o.z; return *this; }
float dot (const Vec3& o) const { return x*o.x + y*o.y + z*o.z; }
Vec3 cross(const Vec3& o) const {
return { y*o.z-z*o.y, z*o.x-x*o.z, x*o.y-y*o.x };
}
float len() const { return sqrtf(x*x + y*y + z*z); }
Vec3 norm() const { float l=len(); return l>0.f ? (*this)*(1.f/l) : *this; }
};
/* Матрица 4×4, столбцовый порядок (column-major, как в OpenGL) */
struct Mat4 {
float m[16] = {};
static Mat4 identity() {
Mat4 r; r.m[0]=r.m[5]=r.m[10]=r.m[15]=1.f; return r;
}
static Mat4 perspective(float fovRad, float aspect, float near_, float far_) {
Mat4 r;
float f = 1.0f / tanf(fovRad * 0.5f);
r.m[0] = f / aspect;
r.m[5] = f;
r.m[10] = (far_ + near_) / (near_ - far_);
r.m[11] = -1.0f;
r.m[14] = (2.0f * far_ * near_) / (near_ - far_);
return r;
}
static Mat4 ortho(float l, float r_, float b, float t, float n, float f) {
Mat4 m;
m.m[0] = 2.f/(r_-l); m.m[5] = 2.f/(t-b);
m.m[10] = -2.f/(f-n);
m.m[12] = -(r_+l)/(r_-l); m.m[13] = -(t+b)/(t-b);
m.m[14] = -(f+n)/(f-n); m.m[15] = 1.f;
return m;
}
static Mat4 lookAt(Vec3 eye, Vec3 center, Vec3 up) {
Vec3 f = (center - eye).norm();
Vec3 r = f.cross(up).norm();
Vec3 u = r.cross(f);
Mat4 m;
m.m[0]=r.x; m.m[4]=r.y; m.m[8] =r.z;
m.m[1]=u.x; m.m[5]=u.y; m.m[9] =u.z;
m.m[2]=-f.x;m.m[6]=-f.y;m.m[10]=-f.z;
m.m[12]=-r.dot(eye); m.m[13]=-u.dot(eye);
m.m[14]= f.dot(eye); m.m[15]= 1.f;
return m;
}
static Mat4 translation(Vec3 t) {
Mat4 m = identity();
m.m[12] = t.x; m.m[13] = t.y; m.m[14] = t.z;
return m;
}
static Mat4 rotationY(float angleRad) {
Mat4 m = identity();
float c = cosf(angleRad), s = sinf(angleRad);
m.m[0] = c; m.m[2] = -s;
m.m[8] = s; m.m[10] = c;
return m;
}
Mat4 operator*(const Mat4& o) const {
Mat4 r;
for (int col=0; col<4; col++)
for (int row=0; row<4; row++) {
r.m[col*4+row] = 0;
for (int k=0; k<4; k++)
r.m[col*4+row] += m[k*4+row] * o.m[col*4+k];
}
return r;
}
const float* ptr() const { return m; }
};
/* ═══════════════════════════════════════════════════════
КЛАСС: Shader
═══════════════════════════════════════════════════════ */
class Shader {
public:
GLuint prog = 0;
bool build(const char* vs, const char* fs,
const char* attr0 = "aPos", const char* attr1 = "aNormal") {
auto compile = [](GLenum type, const char* src) -> GLuint {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512]; glGetShaderInfoLog(s, 512, nullptr, log);
fprintf(stderr, "Shader compile error:\n%s\n", log);
}
return s;
};
GLuint v = compile(GL_VERTEX_SHADER, vs);
GLuint f = compile(GL_FRAGMENT_SHADER, fs);
prog = glCreateProgram();
glAttachShader(prog, v); glAttachShader(prog, f);
glBindAttribLocation(prog, 0, attr0);
if (attr1) glBindAttribLocation(prog, 1, attr1);
glLinkProgram(prog);
glDeleteShader(v); glDeleteShader(f);
GLint ok; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char log[512]; glGetProgramInfoLog(prog, 512, nullptr, log);
fprintf(stderr, "Program link error:\n%s\n", log);
return false;
}
return true;
}
void use() const { glUseProgram(prog); }
void mat4(const char* n, const Mat4& m) const {
glUniformMatrix4fv(glGetUniformLocation(prog,n),1,GL_FALSE,m.ptr());
}
void vec3(const char* n, Vec3 v) const {
glUniform3f(glGetUniformLocation(prog,n), v.x, v.y, v.z);
}
void f1(const char* n, float f) const {
glUniform1f(glGetUniformLocation(prog,n), f);
}
void i1(const char* n, int i) const {
glUniform1i(glGetUniformLocation(prog,n), i);
}
};
/* ═══════════════════════════════════════════════════════
КЛАСС: ShadowMap — FBO с текстурой глубины
═══════════════════════════════════════════════════════ */
class ShadowMap {
public:
GLuint fbo = 0, tex = 0;
void init() {
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
SHADOW_SIZE, SHADOW_SIZE, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float bc[] = {1,1,1,1};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bc);
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, tex, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void beginCapture() {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, SHADOW_SIZE, SHADOW_SIZE);
glClear(GL_DEPTH_BUFFER_BIT);
}
void endCapture(int winW, int winH) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, winW, winH);
}
};
/* ═══════════════════════════════════════════════════════
КЛАСС: Maze — генерация лабиринта (Recursive Backtracker)
═══════════════════════════════════════════════════════ */
struct Point { int x=0, y=0; bool operator==(const Point& o) const { return x==o.x&&y==o.y; } };
class Maze {
public:
uint8_t cells[MAZE_H][MAZE_W] = {};
void generate() {
for (int y=0;y<MAZE_H;y++)
for (int x=0;x<MAZE_W;x++)
cells[y][x] = W_N|W_S|W_E|W_W;
std::vector<Point> stack;
stack.reserve(MAZE_W * MAZE_H);
cells[0][0] |= VIS;
stack.push_back({0,0});
while (!stack.empty()) {
Point cur = stack.back();
std::vector<int> dirs;
for (int d=0;d<4;d++) {
int nx=cur.x+DX[d], ny=cur.y+DY[d];
if (nx>=0&&nx<MAZE_W&&ny>=0&&ny<MAZE_H&&!(cells[ny][nx]&VIS))
dirs.push_back(d);
}
if (dirs.empty()) { stack.pop_back(); }
else {
int d = dirs[rand()%dirs.size()];
int nx = cur.x+DX[d], ny = cur.y+DY[d];
cells[cur.y][cur.x] &= ~DIR_W[d];
cells[ny][nx] &= ~OPP_W[d];
cells[ny][nx] |= VIS;
stack.push_back({nx,ny});
}
}
for (int y=0;y<MAZE_H;y++)
for (int x=0;x<MAZE_W;x++)
cells[y][x] &= ~VIS;
}
bool hasWall(int x, int y, int d) const {
if (x<0||x>=MAZE_W||y<0||y>=MAZE_H) return true;
return (cells[y][x] & DIR_W[d]) != 0;
}
};
/* ═══════════════════════════════════════════════════════
КЛАСС: Camera — камера от первого лица
═══════════════════════════════════════════════════════ */
class Camera {
public:
Vec3 pos = { CELL_W*0.5f, EYE_H, CELL_W*0.5f };
float yaw = 0.0f; /* горизонтальный поворот (градусы) */
float pitch = 0.0f; /* вертикальный поворот */
float speed = 4.0f;
Vec3 front() const {
float y = yaw * PI_F / 180.0f;
float p = pitch * PI_F / 180.0f;
return Vec3{ cosf(p)*sinf(y), sinf(p), cosf(p)*cosf(y) }.norm();
}
Vec3 right() const {
float y = yaw * PI_F / 180.0f;
return Vec3{ cosf(y), 0.f, -sinf(y) }.norm();
}
Mat4 viewMatrix() const {
Vec3 f = front();
return Mat4::lookAt(pos, pos+f, {0,1,0});
}
void mouseMove(float dx, float dy, float sens = 0.14f) {
yaw += dx * sens;
pitch -= dy * sens;
if (pitch > 80.f) pitch = 80.f;
if (pitch < -80.f) pitch = -80.f;
}
/* Движение с коллизией стен */
void move(const Maze& maze, int dir, float dt) {
Vec3 fwd = front(); fwd.y=0; fwd=fwd.norm();
Vec3 rgt = right();
Vec3 d;
if (dir==0) d=fwd;
else if (dir==1) d=fwd*-1.f;
else if (dir==2) d=rgt;
else d=rgt*-1.f;
Vec3 delta = d * (speed * dt);
/* Коллизия по X */
float nx = pos.x + delta.x;
int cx=(int)(pos.x/CELL_W), cy=(int)(pos.z/CELL_W);
cx=std::max(0,std::min(MAZE_W-1,cx)); cy=std::max(0,std::min(MAZE_H-1,cy));
if (delta.x > 0 && maze.hasWall(cx,cy,2)) {
float wall = (cx+1)*CELL_W;
if (nx+PLAYER_R > wall) nx = wall-PLAYER_R;
}
if (delta.x < 0 && maze.hasWall(cx,cy,3)) {
float wall = cx*CELL_W;
if (nx-PLAYER_R < wall) nx = wall+PLAYER_R;
}
pos.x = nx;
/* Коллизия по Z */
float nz = pos.z + delta.z;
cx=(int)(pos.x/CELL_W); cy=(int)(pos.z/CELL_W);
cx=std::max(0,std::min(MAZE_W-1,cx)); cy=std::max(0,std::min(MAZE_H-1,cy));
if (delta.z > 0 && maze.hasWall(cx,cy,1)) {
float wall = (cy+1)*CELL_W;
if (nz+PLAYER_R > wall) nz = wall-PLAYER_R;
}
if (delta.z < 0 && maze.hasWall(cx,cy,0)) {
float wall = cy*CELL_W;
if (nz-PLAYER_R < wall) nz = wall+PLAYER_R;
}
pos.z = nz;
pos.y = EYE_H;
}
};
/* ═══════════════════════════════════════════════════════
ВЕРШИНА геометрии: позиция + нормаль
═══════════════════════════════════════════════════════ */
struct Vtx { float x,y,z, nx,ny,nz; };
static void addQuad(std::vector<Vtx>& v,
float x0,float y0,float z0,
float x1,float y1,float z1,
float x2,float y2,float z2,
float x3,float y3,float z3,
float nx,float ny,float nz) {
auto add = [&](float ax,float ay,float az) {
v.push_back({ax,ay,az, nx,ny,nz});
};
add(x0,y0,z0); add(x1,y1,z1); add(x2,y2,z2);
add(x0,y0,z0); add(x2,y2,z2); add(x3,y3,z3);
}
/* ═══════════════════════════════════════════════════════
КЛАСС: Game — главный контроллер
═══════════════════════════════════════════════════════ */
class Game {
public:
static Game& instance() { static Game g; return g; }
/* ── Инициализация ─────────────────────────────────── */
void init(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(winW, winH);
glutInitWindowPosition(80, 60);
glutCreateWindow("3D Labirint — OpenGL Shadow Mapping");
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) { fprintf(stderr,"GLEW error\n"); exit(1); }
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
depthShader.build(DEPTH_VERT, DEPTH_FRAG, "aPos", nullptr);
mainShader .build(MAIN_VERT, MAIN_FRAG, "aPos", "aNormal");
shadowMap.init();
srand((unsigned)time(nullptr));
newGame();
// При старте показываем меню, поэтому мышь пока не захватываем
// captureMouse();
glutDisplayFunc (cbDisplay);
glutReshapeFunc (cbReshape);
glutKeyboardFunc(cbKey);
glutKeyboardUpFunc(cbKeyUp);
glutMotionFunc (cbMotion);
glutPassiveMotionFunc(cbMotion);
glutTimerFunc(16, cbTimer, 0);
lastTime = glutGet(GLUT_ELAPSED_TIME);
}
void run() {
printf("=== 3D Labirint — Shadow Mapping + Torch ===\n");
printf("WASD — dvizhenie\n");
printf("Mouse — obzor\n");
printf("N — novaya igra\n");
printf("M — minikarта\n");
printf("F — fakel vkl/otkl\n");
printf("+/- — gustota tumana\n");
printf("ESC — kursor / vyhod\n");
glutMainLoop();
}
private:
Game() = default;
/* ── Члены ─────────────────────────────────────────── */
Maze maze;
Camera camera;
Shader depthShader, mainShader;
ShadowMap shadowMap;
Point exitPt = { MAZE_W-1, MAZE_H-1 };
GLuint vbo = 0;
int wallOff=0, wallCnt=0;
int floorOff=0, floorCnt=0;
int playerOff=0,playerCnt=0;
int ceilOff=0, ceilCnt=0;
int exitOff=0, exitCnt=0;
Mat4 lightSpace;
int torchOff=0, torchCnt=0;
Vec3 lightDir;
Vec3 fogColor = { 0.03f, 0.03f, 0.05f };
float fogDensity = 0.025f;
float torchBright = 1.0f;
bool torchOn = true;
int winW=1024, winH=700;
bool mouseCaptured = false;
int lastMX=0, lastMY=0;
bool firstMouse = true;
bool showMap = true;
float fps=0.f; int frameCount=0, lastTime=0;
float walkTime = 0.0f; // Переменная для анимации шагов
/* Клавиши WASD */
bool keys[4] = {};
bool won = false;
enum GameState { MENU, PLAYING };
GameState gameState = MENU;
void newGame() {
maze.generate();
camera.pos = { CELL_W*0.5f, EYE_H, CELL_W*0.5f };
camera.yaw = 0.f; camera.pitch = 0.f;
won = false;
buildGeometry();
computeLightMatrix();
}
/* ── Построение VBO геометрии ───────────────────────── */
void buildGeometry() {
std::vector<Vtx> verts;
/* --- Стены --- */
wallOff = 0;
for (int cy=0; cy<MAZE_H; cy++) {
for (int cx=0; cx<MAZE_W; cx++) {
float x0=cx*CELL_W, x1=(cx+1)*CELL_W;
float z0=cy*CELL_W, z1=(cy+1)*CELL_W;
if (maze.hasWall(cx,cy,0)) // Север
addQuad(verts, x0,0,z0, x1,0,z0, x1,WALL_H,z0, x0,WALL_H,z0, 0,0,1);
if (maze.hasWall(cx,cy,1)) // Юг
addQuad(verts, x1,0,z1, x0,0,z1, x0,WALL_H,z1, x1,WALL_H,z1, 0,0,-1);
if (maze.hasWall(cx,cy,3)) // Запад
addQuad(verts, x0,0,z1, x0,0,z0, x0,WALL_H,z0, x0,WALL_H,z1, 1,0,0);
if (maze.hasWall(cx,cy,2)) // Восток
addQuad(verts, x1,0,z0, x1,0,z1, x1,WALL_H,z1, x1,WALL_H,z0,-1,0,0);
}
}
wallCnt = (int)verts.size() - wallOff;
/* --- Пол --- */
floorOff = (int)verts.size();
addQuad(verts,
0,0,0, MAZE_W*CELL_W,0,0,
MAZE_W*CELL_W,0,MAZE_H*CELL_W, 0,0,MAZE_H*CELL_W, 0,1,0);
floorCnt = (int)verts.size() - floorOff;
/* --- Потолок --- */
ceilOff = (int)verts.size();
addQuad(verts,
0,WALL_H,MAZE_H*CELL_W, MAZE_W*CELL_W,WALL_H,MAZE_H*CELL_W,
MAZE_W*CELL_W,WALL_H,0, 0,WALL_H,0, 0,-1,0);
ceilCnt = (int)verts.size() - ceilOff;
/* --- Маркер выхода (горизонтальная плита) --- */
exitOff = (int)verts.size();
float ex0=exitPt.x*CELL_W+0.1f, ex1=(exitPt.x+1)*CELL_W-0.1f;
float ez0=exitPt.y*CELL_W+0.1f, ez1=(exitPt.y+1)*CELL_W-0.1f;
addQuad(verts, ex0,0.01f,ez0, ex1,0.01f,ez0,
ex1,0.01f,ez1, ex0,0.01f,ez1, 0,1,0);
exitCnt = (int)verts.size() - exitOff;
/* --- Персонаж (игрок) --- */
playerOff = (int)verts.size();
auto addBox = [&](float cx, float cy, float cz, float dx, float dy, float dz) {
float x0=cx-dx, x1=cx+dx, y0=cy-dy, y1=cy+dy, z0=cz-dz, z1=cz+dz;
addQuad(verts, x0,y0,z0, x1,y0,z0, x1,y1,z0, x0,y1,z0, 0,0,-1); // Back
addQuad(verts, x1,y0,z1, x0,y0,z1, x0,y1,z1, x1,y1,z1, 0,0, 1); // Front
addQuad(verts, x0,y0,z1, x0,y0,z0, x0,y1,z0, x0,y1,z1,-1,0, 0); // Left
addQuad(verts, x1,y0,z0, x1,y0,z1, x1,y1,z1, x1,y1,z0, 1,0, 0); // Right
addQuad(verts, x0,y1,z0, x1,y1,z0, x1,y1,z1, x0,y1,z1, 0,1, 0); // Top
addQuad(verts, x0,y0,z1, x1,y0,z1, x1,y0,z0, x0,y0,z0, 0,-1,0); // Bottom
};
// Тело (центр локальных координат 0,0,0)
// Ноги
addBox(-0.1f, 0.35f, 0.0f, 0.08f, 0.35f, 0.08f); // Левая нога
addBox( 0.1f, 0.35f, 0.0f, 0.08f, 0.35f, 0.08f); // Правая нога
// Туловище
addBox( 0.0f, 0.95f, 0.0f, 0.15f, 0.25f, 0.1f);
// Левая рука (опущена)
addBox(-0.25f, 0.9f, 0.0f, 0.05f, 0.2f, 0.05f);
// Правая рука (вытянута вперед с фонариком)
// Модель ориентируем так, что вперед (взгляд камеры) - это +Z
addBox( 0.20f, 1.0f, 0.25f, 0.04f, 0.04f, 0.25f);
playerCnt = (int)verts.size() - playerOff;
// Фонарик (в правой руке)
torchOff = (int)verts.size();
addBox( 0.20f, 1.0f, 0.55f, 0.05f, 0.05f, 0.08f);
torchCnt = (int)verts.size() - torchOff;
/* Загрузка в VBO */
if (!vbo) glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
verts.size()*sizeof(Vtx),
verts.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
/* ── Матрица источника света для shadow map ─────────── */
void computeLightMatrix() {
float halfW = MAZE_W * CELL_W * 0.5f;
float halfH = MAZE_H * CELL_W * 0.5f;
Vec3 lPos = { halfW + 8.f, 28.f, -6.f }; /* над сценой, с севера */
Vec3 lTgt = { halfW, 0.f, halfH };
lightDir = (lPos - lTgt).norm(); /* нормаль к свету */
Mat4 lView = Mat4::lookAt(lPos, lTgt, {0,1,0});
Mat4 lProj = Mat4::ortho(-40,40,-40,40, 0.5f, 90.f);
lightSpace = lProj * lView;
}
/* ── Привязка VBO к атрибутам ───────────────────────── */
void bindVBO(bool posOnly = false) {
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)0);
if (!posOnly) {
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx),
(void*)(3*sizeof(float)));
} else {
glDisableVertexAttribArray(1);
}
}
/* ── ПЕРВЫЙ ПРОХОД: генерация карты теней ───────────── */
void passShadow() {
shadowMap.beginCapture();
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.f, 4.f);
depthShader.use();
depthShader.mat4("uLightSpace", lightSpace);
depthShader.mat4("uModel", Mat4::identity());
bindVBO(true);
glDrawArrays(GL_TRIANGLES, wallOff, wallCnt);
glDrawArrays(GL_TRIANGLES, floorOff, floorCnt);
glDrawArrays(GL_TRIANGLES, ceilOff, ceilCnt);
glDrawArrays(GL_TRIANGLES, exitOff, exitCnt);
// Тень от игрока и фонарика с анимацией
float bobY = sinf(walkTime) * 0.05f; // Плавное движение вверх-вниз
float swayArm = cosf(walkTime) * 0.03f;
Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
depthShader.mat4("uModel", pModel);
glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);
Mat4 torchModel = Mat4::translation(Vec3{camera.pos.x + swayArm, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
depthShader.mat4("uModel", torchModel);
glDrawArrays(GL_TRIANGLES, torchOff, torchCnt);
glDisable(GL_POLYGON_OFFSET_FILL);
shadowMap.endCapture(winW, winH);
}
/* ── ВТОРОЙ ПРОХОД: основная отрисовка ─────────────── */
void passMain() {
glClearColor(fogColor.x, fogColor.y, fogColor.z, 1.f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Mat4 proj = Mat4::perspective(75.f*PI_F/180.f, (float)winW/winH, 0.1f, 80.f);
Mat4 view = camera.viewMatrix();
/* Флик факела */
float t = glutGet(GLUT_ELAPSED_TIME) * 0.001f;
float flicker = torchOn
? (1.0f + 0.08f*sinf(t*7.f) + 0.04f*sinf(t*13.f) + 0.03f*(rand()%10/100.f))
: 0.0f;
Vec3 torchPos = camera.pos + Vec3{0, 0.1f, 0};
mainShader.use();
mainShader.mat4("uProj", proj);
mainShader.mat4("uView", view);
mainShader.mat4("uModel", Mat4::identity());
mainShader.mat4("uLightSpace", lightSpace);
mainShader.vec3("uLightDir", lightDir);
mainShader.vec3("uSunColor", {1.0f, 0.95f, 0.85f}); // Увеличили яркость солнца
mainShader.vec3("uTorchPos", torchPos);
mainShader.vec3("uTorchColor", {1.0f, 0.85f, 0.50f}); // Цвет факела ярче
mainShader.f1 ("uTorchBright", flicker * 4.0f); // Максимальная яркость света факела усилена
mainShader.vec3("uViewPos", camera.pos);
mainShader.f1 ("uFogDensity", fogDensity);
mainShader.vec3("uFogColor", fogColor);
mainShader.i1 ("uShadowMap", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, shadowMap.tex);
bindVBO(false);
/* Пол */
mainShader.vec3("uColor", {0.65f, 0.60f, 0.55f}); // Сделали намного светлее
glDrawArrays(GL_TRIANGLES, floorOff, floorCnt);
/* Потолок */
mainShader.vec3("uColor", {0.45f, 0.45f, 0.45f}); // Сделали светлее
glDrawArrays(GL_TRIANGLES, ceilOff, ceilCnt);
/* Стены */
mainShader.vec3("uColor", {0.70f, 0.65f, 0.60f}); // И стены светлее
glDrawArrays(GL_TRIANGLES, wallOff, wallCnt);
/* Маркер выхода — золотой */
mainShader.vec3("uColor", {0.90f, 0.72f, 0.10f});
glDrawArrays(GL_TRIANGLES, exitOff, exitCnt);
/* Игрок и Фонарик (тело поворачивается за камерой) */
float bobY = sinf(walkTime) * 0.05f; // Плавное раскачивание вверх-вниз
float swayArm = cosf(walkTime) * 0.03f; // Горизонтальное покачивание для фонарика
Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
mainShader.mat4("uModel", pModel);
mainShader.vec3("uColor", {0.2f, 0.5f, 0.9f}); // Ярко-синий цвет одежды ("текстура")
glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);
Mat4 torchModel = Mat4::translation(Vec3{camera.pos.x + swayArm, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
mainShader.mat4("uModel", torchModel);
mainShader.vec3("uColor", {0.7f, 0.7f, 0.7f}); // Серый корпус фонарика (светлый вместо черного)
glDrawArrays(GL_TRIANGLES, torchOff, torchCnt);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
/* ── МИНИКАРТА (2D оверлей) ─────────────────────────── */
void drawMinimap() {
constexpr float MS = 5.5f; /* пикселей на клетку */
constexpr float MX = 12.f; /* отступ от края */
float MY_off = winH - MAZE_H*MS - 12.f;
glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glPushMatrix(); glLoadIdentity();
gluOrtho2D(0, winW, winH, 0);
glMatrixMode(GL_MODELVIEW);
glPushMatrix(); glLoadIdentity();
/* Подложка */
glColor4f(0,0,0,0.55f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBegin(GL_QUADS);
glVertex2f(MX-3, MY_off-3);
glVertex2f(MX+MAZE_W*MS+3, MY_off-3);
glVertex2f(MX+MAZE_W*MS+3, MY_off+MAZE_H*MS+3);
glVertex2f(MX-3, MY_off+MAZE_H*MS+3);
glEnd();
glDisable(GL_BLEND);
/* Стены */
glColor3f(0.75f, 0.72f, 0.68f);
glLineWidth(1.0f);
for (int cy=0;cy<MAZE_H;cy++) {
for (int cx=0;cx<MAZE_W;cx++) {
float lx=MX+cx*MS, ly=MY_off+cy*MS;
glBegin(GL_LINES);
if (maze.hasWall(cx,cy,0)) {
glVertex2f(lx,ly); glVertex2f(lx+MS,ly);
}
if (maze.hasWall(cx,cy,1)) {
glVertex2f(lx,ly+MS); glVertex2f(lx+MS,ly+MS);
}
if (maze.hasWall(cx,cy,3)) {
glVertex2f(lx,ly); glVertex2f(lx,ly+MS);
}
if (maze.hasWall(cx,cy,2)) {
glVertex2f(lx+MS,ly); glVertex2f(lx+MS,ly+MS);
}
glEnd();
}
}
/* Выход */
glColor3f(0.9f,0.7f,0.1f);
glBegin(GL_QUADS);
glVertex2f(MX+exitPt.x*MS+1, MY_off+exitPt.y*MS+1);
glVertex2f(MX+(exitPt.x+1)*MS-1,MY_off+exitPt.y*MS+1);
glVertex2f(MX+(exitPt.x+1)*MS-1,MY_off+(exitPt.y+1)*MS-1);
glVertex2f(MX+exitPt.x*MS+1, MY_off+(exitPt.y+1)*MS-1);
glEnd();
/* Игрок */
float px = MX + (camera.pos.x/CELL_W)*MS;
float py = MY_off + (camera.pos.z/CELL_W)*MS;
glColor3f(0.3f,0.6f,1.0f);
glPointSize(5.0f);
glBegin(GL_POINTS); glVertex2f(px,py); glEnd();
/* Стрелка-направление */
Vec3 f = camera.front(); f.y=0; f=f.norm();
glColor3f(0.8f,0.9f,1.0f);
glLineWidth(1.5f);
glBegin(GL_LINES);
glVertex2f(px, py);
glVertex2f(px+f.x*MS*0.8f, py+f.z*MS*0.8f);
glEnd();
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
glEnable(GL_DEPTH_TEST);
glLineWidth(1.0f);
}
/* ── HUD (текстовые подсказки) ──────────────────────── */
void drawHUD() {
glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
gluOrtho2D(0, winW, winH, 0);
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
auto txt = [&](float x, float y, const char* s, void* font=GLUT_BITMAP_HELVETICA_12) {
glRasterPos2f(x, y);
for (; *s; s++) glutBitmapCharacter(font, *s);
};
char buf[128];
glColor3f(1.f, 1.f, 0.3f);
snprintf(buf,sizeof(buf),"FPS: %.1f", fps);
txt(10,20,buf);
glColor3f(0.75f,0.75f,0.75f);
snprintf(buf,sizeof(buf),"Kletka: (%d, %d) Vyhod: (%d, %d)",
(int)(camera.pos.x/CELL_W), (int)(camera.pos.z/CELL_W),
exitPt.x, exitPt.y);
txt(10,38,buf);
glColor3f(0.5f,1.f,0.5f);
txt(10, (float)winH-10,
"WASD - dvizhenie | Mouse - obzor | N - novaya igra | M - mapa | F - fakel | +/- - tuman | ESC - vyhod");
if (won) {
glColor3f(1.f,0.9f,0.2f);
txt(winW/2.f-90.f, winH/2.f, "VY NASHOHDITE VIHOD!", GLUT_BITMAP_HELVETICA_18);
glColor3f(1,1,1);
txt(winW/2.f-75.f, winH/2.f+30.f, "N - novaya igra");
}
if (!torchOn) {
glColor3f(0.5f,0.5f,0.5f);
txt(winW-110.f, 20, "[Fakel: OTKL]");
}
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
glEnable(GL_DEPTH_TEST);
}
/* ── Проверка победы ────────────────────────────────── */
void checkWin() {
float ex = (exitPt.x+0.5f)*CELL_W;
float ez = (exitPt.y+0.5f)*CELL_W;
float dx = camera.pos.x - ex, dz = camera.pos.z - ez;
if (sqrtf(dx*dx+dz*dz) < CELL_W*0.65f) won = true;
}
/* ── ГЛАВНОЕ МЕНЮ (2D) ──────────────────────────────── */
void drawMenu() {
glClearColor(0.1f, 0.1f, 0.15f, 1.0f); // Темно-синий фон меню
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
gluOrtho2D(0, winW, winH, 0);
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
auto txt = [&](float x, float y, const char* s, void* font) {
glRasterPos2f(x, y);
for (; *s; s++) glutBitmapCharacter(font, *s);
};
glColor3f(0.2f, 0.6f, 1.0f);
txt(winW / 2.0f - 80.0f, winH / 2.0f - 60.0f, "3D LABIRINT", GLUT_BITMAP_HELVETICA_18);
glColor3f(1.0f, 1.0f, 1.0f);
txt(winW / 2.0f - 110.0f, winH / 2.0f, "[ENTER] - Voyti v igru", GLUT_BITMAP_HELVETICA_18);
txt(winW / 2.0f - 110.0f, winH / 2.0f + 30.0f, "[ESC] - Vihod", GLUT_BITMAP_HELVETICA_18);
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
glEnable(GL_DEPTH_TEST);
}
/* ── Отображение ────────────────────────────────────── */
void display() {
if (gameState == MENU) {
drawMenu();
glutSwapBuffers();
return;
}
/* Обновление движения */
int now = glutGet(GLUT_ELAPSED_TIME);
float dt = std::min((now - lastTime) * 0.001f, 0.05f);
lastTime = now;
bool isMoving = false;
if (!won) {
for (int d=0;d<4;d++) {
if(keys[d]) {
camera.move(maze,d,dt);
isMoving = true;
}
}
checkWin();
}
if (isMoving) walkTime += dt * 10.0f; // Увеличиваем фазу анимации при ходьбе
else walkTime = 0.0f; // Сбрасываем в 0, когда стоим
passShadow();
passMain();
if (showMap) drawMinimap();
drawHUD();
frameCount++;
if (now - lastTime + (int)(dt*1000) >= 500) {
// update FPS every 0.5s from timer
}
glutSwapBuffers();
}
/* ── Захват / освобождение мыши ────────────────────── */
void captureMouse() {
mouseCaptured = true;
firstMouse = true;
glutSetCursor(GLUT_CURSOR_NONE);
}
void releaseMouse() {
mouseCaptured = false;
glutSetCursor(GLUT_CURSOR_INHERIT);
}
/* ── Колбэки ──────────────────────────────────────── */
void onKey(unsigned char k) {
if (gameState == MENU) {
if (k == 13 || k == ' ') { // Enter или Пробел
gameState = PLAYING;
lastTime = glutGet(GLUT_ELAPSED_TIME); // Сбрасываем таймер
captureMouse();
} else if (k == 27) { // ESC = Выход
exit(0);
}
glutPostRedisplay();
return;
}
switch (k) {
case 'w': case 'W': keys[0]=true; break;
case 's': case 'S': keys[1]=true; break;
case 'd': case 'D': keys[3]=true; break; // Исправлена инверсия вправо
case 'a': case 'A': keys[2]=true; break; // Исправлена инверсия влево
case 'n': case 'N': newGame(); break;
case 'm': case 'M': showMap = !showMap; break;
case 'f': case 'F': torchOn = !torchOn; break;
case '+': fogDensity = std::min(0.2f, fogDensity+0.005f); break;
case '-': fogDensity = std::max(0.01f,fogDensity-0.005f); break;
case 27: // ESC во время игры - выход в меню
releaseMouse();
gameState = MENU;
for(int i=0;i<4;i++) keys[i]=false;
break;
}
glutPostRedisplay();
}
void onKeyUp(unsigned char k) {
switch (k) {
case 'w': case 'W': keys[0]=false; break;
case 's': case 'S': keys[1]=false; break;
case 'd': case 'D': keys[3]=false; break; // Исправлена инверсия вправо
case 'a': case 'A': keys[2]=false; break; // Исправлена инверсия влево
}
}
void onMouse(int x, int y) {
if (gameState == MENU || !mouseCaptured) return;
int cx = winW/2, cy = winH/2;
if (firstMouse) { firstMouse=false; glutWarpPointer(cx,cy); return; }
float dx = (float)(x-cx), dy = (float)(y-cy);
if (fabsf(dx)+fabsf(dy) < 0.5f) return;
camera.mouseMove(dx, dy);
glutWarpPointer(cx, cy);
glutPostRedisplay();
}
void onReshape(int w, int h) {
if (h==0) h=1;
winW=w; winH=h;
glViewport(0,0,w,h);
}
void onTimer() {
int now = glutGet(GLUT_ELAPSED_TIME);
static int fpsPrev=0; static int fpsFrames=0;
fpsFrames++;
if (now - fpsPrev >= 500) {
fps = fpsFrames * 1000.f / (float)(now-fpsPrev);
fpsFrames=0; fpsPrev=now;
}
glutPostRedisplay();
glutTimerFunc(16, cbTimer, 0);
}
/* ── Статические колбэки ────────────────────────────── */
static void cbDisplay() { instance().display(); }
static void cbReshape(int w,int h) { instance().onReshape(w,h); }
static void cbKey(unsigned char k,int,int) { instance().onKey(k); }
static void cbKeyUp(unsigned char k,int,int) { instance().onKeyUp(k); }
static void cbMotion(int x,int y) { instance().onMouse(x,y); }
static void cbTimer(int) { instance().onTimer(); }
};
/* ═══════════════════════════════════════════════════════
ТОЧКА ВХОДА
═══════════════════════════════════════════════════════ */
int main(int argc, char** argv) {
Game& g = Game::instance();
g.init(argc, argv);
g.run();
return 0;
}