Загрузка данных


#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 color = 0.05 * uSunColor;

    /* Направленный свет с тенями */
    float diff = max(dot(vNormal, uLightDir), 0.0);
    float sh   = calcShadow();
    color += (1.0 - sh * 0.88) * diff * uSunColor * 0.55;

    /* Факел — точечный источник без тени */
    vec3  toT   = uTorchPos - vFragPos;
    float tDist = length(toT);
    float tAtt  = uTorchBright / (1.0 + 0.4*tDist + 0.25*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), 40.0);
    color += spec * 0.35 * tAtt * uTorchColor;

    color *= uColor;

    /* Туман (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.045f;
    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;

    /* Клавиши WASD */
    bool keys[4] = {};
    float prevDt = 0.016f;

    bool  won = false;

    /* ── Новая игра ─────────────────────────────────────── */
    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);

        // Тень от игрока и фонарика (с вращением)
        Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, 0.0f, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
        depthShader.mat4("uModel", pModel);
        glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);
        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",   {0.85f, 0.80f, 0.70f});
        mainShader.vec3("uTorchPos",   torchPos);
        mainShader.vec3("uTorchColor", {1.0f, 0.70f, 0.35f});
        mainShader.f1 ("uTorchBright", flicker * 1.8f);
        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.38f, 0.32f, 0.27f});
        glDrawArrays(GL_TRIANGLES, floorOff, floorCnt);

        /* Потолок */
        mainShader.vec3("uColor", {0.18f, 0.18f, 0.20f});
        glDrawArrays(GL_TRIANGLES, ceilOff, ceilCnt);

        /* Стены */
        mainShader.vec3("uColor", {0.50f, 0.44f, 0.38f});
        glDrawArrays(GL_TRIANGLES, wallOff, wallCnt);

        /* Маркер выхода — золотой */
        mainShader.vec3("uColor", {0.90f, 0.72f, 0.10f});
        glDrawArrays(GL_TRIANGLES, exitOff, exitCnt);

        /* Игрок и Фонарик (тело поворачивается за камерой) */
        Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, 0.0f, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
        mainShader.mat4("uModel", pModel);
        
        mainShader.vec3("uColor", {0.2f, 0.4f, 0.8f}); // Цвет одежды (синий)
        glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);

        mainShader.vec3("uColor", {0.1f, 0.1f, 0.1f}); // Цвет фонарика (черный/темно-серый)
        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;
    }

    /* ── Отображение ────────────────────────────────────── */
    void display() {
        /* Обновление движения */
        int now = glutGet(GLUT_ELAPSED_TIME);
        float dt = std::min((now - lastTime) * 0.001f, 0.05f);
        lastTime = now;

        if (!won) {
            for (int d=0;d<4;d++) if(keys[d]) camera.move(maze,d,dt);
            checkWin();
        }

        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) {
        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:
            if (mouseCaptured) releaseMouse();
            else exit(0);
            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 (!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;
}