xp12camera

Owner: IIIlllIIIllI URL: git@github.com:nyangkosense/xp12camera.git

FLIR_VisualEffects.cpp

/*
 * Visual effects system implementing monochrome filters, thermal effects, and military camera aesthetics for realistic FLIR simulation
 *
 * MIT License
 * 
 * Copyright (c) 2025 sebastian <sebastian@eingabeausgabe.io>
 * 
 * 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.
 */

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#include "XPLMDisplay.h"
#include "XPLMGraphics.h"
#include "XPLMDataAccess.h"
#include "XPLMUtilities.h"
#include "FLIR_VisualEffects.h"

#include <windows.h>
#include <GL/gl.h>

static int gMonochromeEnabled = 0;
static int gThermalEnabled = 1;
static int gIREnabled = 0;
static int gNoiseEnabled = 1;
static int gScanLinesEnabled = 1;
static float gBrightness = 1.0f;
static float gContrast = 1.2f;
static float gNoiseIntensity = 0.1f;
static float gScanLineOpacity = 0.05f;
static int gFrameCounter = 0;
static int gPostProcessingEnabled = 1;
static unsigned char* gPixelBuffer = NULL;
static unsigned char* gProcessedBuffer = NULL;
static int gBufferWidth = 0;
static int gBufferHeight = 0;
static int gProcessingCounter = 0;
static int gProcessingSkip = 5; // Process every 6th frame for caching
static float gProcessingScale = 0.25f; // Process at quarter resolution
static int gUseHybridMode = 1; // Use hybrid shader+overlay approach

// Forward declarations
void ProcessEOIROptimized(unsigned char* input, unsigned char* output, int width, int height, int mode);
void RenderHybridEffects(int screenWidth, int screenHeight, int mode);

void InitializeVisualEffects()
{
    srand(time(NULL));
}

void CleanupVisualEffects()
{
    if (gPixelBuffer) {
        free(gPixelBuffer);
        gPixelBuffer = NULL;
    }
    if (gProcessedBuffer) {
        free(gProcessedBuffer);
        gProcessedBuffer = NULL;
    }
}

// Safety function to allocate pixel buffers
int AllocatePixelBuffer(int width, int height)
{
    int fullSize = width * height * 3; // RGB for full resolution
    int processedSize = fullSize;
    
    if (!gPixelBuffer || gBufferWidth != width || gBufferHeight != height) {
        if (gPixelBuffer) {
            free(gPixelBuffer);
        }
        if (gProcessedBuffer) {
            free(gProcessedBuffer);
        }
        
        gPixelBuffer = (unsigned char*)malloc(fullSize);
        gProcessedBuffer = (unsigned char*)malloc(processedSize);
        
        if (!gPixelBuffer || !gProcessedBuffer) {
            CleanupVisualEffects();
            return 0; // Failed to allocate
        }
        
        gBufferWidth = width;
        gBufferHeight = height;
    }
    
    return 1; // Success
}

// Convert RGB to grayscale with EO/IR processing
void ProcessEOIR(unsigned char* pixels, int width, int height, int mode)
{
    for (int i = 0; i < width * height; i++) {
        int idx = i * 3;
        unsigned char r = pixels[idx];
        unsigned char g = pixels[idx + 1];
        unsigned char b = pixels[idx + 2];
        
        // Convert to grayscale (0.0 - 1.0 range)
        float gray = (0.299f * r + 0.587f * g + 0.114f * b) / 255.0f;
        
        // Apply EO/IR processing based on mode
        switch (mode) {
            case 1: // Monochrome with enhancement
                // Aggressive contrast curve
                gray = powf(gray, 0.6f);
                gray = gray * 1.8f - 0.4f;
                gray = fmaxf(0.0f, fminf(1.0f, gray));
                
                // Crush blacks and blow highlights for military look
                if (gray < 0.25f) gray = gray * 0.3f;        // Crush darks
                else if (gray > 0.75f) gray = 0.8f + (gray - 0.75f) * 0.8f; // Compress highlights
                break;
                
            case 2: // Thermal simulation
                // Thermal processing with inversion
                gray = powf(gray, 0.5f) * 1.6f;
                gray = 1.0f - gray; // Invert for thermal (hot = white)
                gray = fmaxf(0.0f, fminf(1.0f, gray));
                break;
                
            case 3: // Enhanced IR
                // Extreme contrast for IR look
                gray = powf(gray, 0.4f);
                gray = gray * 2.5f - 0.8f;
                gray = fmaxf(0.0f, fminf(1.0f, gray));
                
                // Quantize to simulate limited bit depth
                gray = floorf(gray * 32.0f) / 32.0f;
                break;
                
            default:
                // Standard - minimal processing
                break;
        }
        
        // Add very subtle noise for realism
        if (mode > 0) {
            float noise = ((rand() % 21) - 10) / 2000.0f; // -0.005 to +0.005
            gray += noise;
            gray = fmaxf(0.0f, fminf(1.0f, gray));
        }
        
        // Convert back to RGB
        unsigned char finalGray = (unsigned char)(gray * 255.0f);
        
        // For monochrome, add slight green tint
        if (mode == 1) {
            pixels[idx] = (unsigned char)(finalGray * 0.7f);     // R
            pixels[idx + 1] = finalGray;                         // G  
            pixels[idx + 2] = (unsigned char)(finalGray * 0.7f); // B
        } else {
            pixels[idx] = finalGray;     // R
            pixels[idx + 1] = finalGray; // G
            pixels[idx + 2] = finalGray; // B
        }
    }
}

// Optimized post-processing function
void RenderPostProcessing(int screenWidth, int screenHeight)
{
    // Safety check: skip if too small or too large
    if (screenWidth < 100 || screenHeight < 100 || 
        screenWidth > 4096 || screenHeight > 4096) {
        return;
    }
    
    // Skip frames for better performance
    gProcessingCounter++;
    int shouldProcess = (gProcessingCounter % (gProcessingSkip + 1)) == 0;
    
    // Determine processing mode
    int processingMode = 0;
    if (gMonochromeEnabled) processingMode = 1;
    else if (gThermalEnabled) processingMode = 2;
    else if (gIREnabled) processingMode = 3;
    
    if (processingMode == 0) return; // No processing needed
    
    // Allocate buffers if needed
    if (!AllocatePixelBuffer(screenWidth, screenHeight)) {
        return;
    }
    
    // Only do expensive processing every few frames
    if (shouldProcess) {
        // Clear any OpenGL errors
        while (glGetError() != GL_NO_ERROR) { }
        
        // Read framebuffer
        glReadPixels(0, 0, screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, gPixelBuffer);
        
        // Check for errors
        if (glGetError() != GL_NO_ERROR) {
            return;
        }
        
        // Process with optimized function
        ProcessEOIROptimized(gPixelBuffer, gProcessedBuffer, screenWidth, screenHeight, processingMode);
    }
    
    // Always draw the (possibly cached) processed result
    glRasterPos2f(0, 0);
    glDrawPixels(screenWidth, screenHeight, GL_RGB, GL_UNSIGNED_BYTE, gProcessedBuffer);
    
    // Check for errors
    if (glGetError() != GL_NO_ERROR) {
        gPostProcessingEnabled = 0; // Disable on error
    }
}

// Much faster processing function with fake heat signatures
void ProcessEOIROptimized(unsigned char* input, unsigned char* output, int width, int height, int mode)
{
    int totalPixels = width * height;
    int step = 1; // Process every pixel, but optimize the loop
    
    // Pre-calculate noise once per frame
    static int noiseFrame = 0;
    static float frameNoise = 0.0f;
    if (noiseFrame != gFrameCounter) {
        frameNoise = ((rand() % 21) - 10) / 3000.0f; // Even more subtle
        noiseFrame = gFrameCounter;
    }
    
    for (int i = 0; i < totalPixels; i += step) {
        int idx = i * 3;
        int x = i % width;
        int y = i / width;
        
        // Original RGB values
        unsigned char r = input[idx];
        unsigned char g = input[idx + 1];
        unsigned char b = input[idx + 2];
        
        // Fast integer-based grayscale conversion
        int gray = (r * 77 + g * 151 + b * 28) >> 8; // /256
        
        // Fake heat signature logic based on color analysis
        int heatBonus = 0;
        
        // Sky detection (blue-ish areas are cold)
        if (b > r && b > g && b > 100) {
            heatBonus = -30; // Sky is cold
        }
        // Vegetation detection (green areas are cooler)  
        else if (g > r && g > b && g > 80) {
            heatBonus = -15; // Vegetation is cooler
        }
        // Ground/concrete detection (neutral colors)
        else if (abs(r - g) < 20 && abs(g - b) < 20 && gray > 60) {
            heatBonus = 10; // Ground/concrete slightly warm
        }
        // Bright objects (could be hot engines, lights, etc)
        else if (gray > 200) {
            heatBonus = 25; // Bright objects assumed warm
        }
        // Very dark objects (shadows, cold areas)
        else if (gray < 40) {
            heatBonus = -20; // Deep shadows are cold
        }
        
        // Ground level is warmer than sky (simple atmospheric model)
        float skyFactor = (float)y / height; // 0 = top, 1 = bottom
        heatBonus += (int)(skyFactor * 15); // Ground +15, sky +0
        
        // Apply heat simulation based on mode
        switch (mode) {
            case 1: // Monochrome - enhanced but not too harsh
                gray += heatBonus / 2; // Subtle heat effect
                gray = (gray * 5) >> 2; // *1.25 instead of *1.5
                if (gray < 0) gray = 20; // Don't go pure black
                if (gray > 255) gray = 255;
                
                // Green tint for night vision
                output[idx] = (unsigned char)((gray * 180) >> 8);     // R * 0.7
                output[idx + 1] = (unsigned char)gray;               // G
                output[idx + 2] = (unsigned char)((gray * 180) >> 8); // B * 0.7
                break;
                
            case 2: // Thermal - with heat signatures
                gray += heatBonus; // Full heat effect
                if (gray < 0) gray = 30; // Minimum visible level
                if (gray > 255) gray = 255;
                
                // Don't fully invert - partial inversion looks more realistic
                gray = 200 - (gray * 3 >> 2); // Partial invert and enhance
                if (gray < 40) gray = 40; // Keep some visibility
                if (gray > 255) gray = 255;
                
                output[idx] = output[idx + 1] = output[idx + 2] = (unsigned char)gray;
                break;
                
            case 3: // Enhanced IR - high contrast but not crushing
                gray += heatBonus;
                if (gray < 0) gray = 25;
                if (gray > 255) gray = 255;
                
                // Less harsh threshold
                if (gray > 140) gray = 240;
                else if (gray > 80) gray = 160;
                else if (gray > 40) gray = 80;
                else gray = 30; // Minimum visibility
                
                output[idx] = output[idx + 1] = output[idx + 2] = (unsigned char)gray;
                break;
                
            default:
                output[idx] = input[idx];
                output[idx + 1] = input[idx + 1];
                output[idx + 2] = input[idx + 2];
                break;
        }
    }
}

// Hybrid approach: Smart overlays that mimic post-processing visually
void RenderHybridEffects(int screenWidth, int screenHeight, int mode)
{
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0, screenWidth, screenHeight, 0, -1, 1);
    
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    
    switch (mode) {
        case 1: // Monochrome mode
            RenderSmartMonochrome(screenWidth, screenHeight);
            break;
        case 2: // Thermal mode  
            RenderSmartThermal(screenWidth, screenHeight);
            break;
        case 3: // Enhanced IR mode
            RenderSmartIR(screenWidth, screenHeight);
            break;
    }
    
    // Add overlays
    if (gNoiseEnabled) {
        RenderCameraNoise(screenWidth, screenHeight);
    }
    if (gScanLinesEnabled) {
        RenderScanLines(screenWidth, screenHeight);
    }
    
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
}

// Smart monochrome that mimics the post-processing look
void RenderSmartMonochrome(int screenWidth, int screenHeight)
{
    // Base desaturation with green tint
    glBlendFunc(GL_DST_COLOR, GL_ZERO);
    glColor4f(0.7f, 1.0f, 0.7f, 1.0f); // Green night vision tint
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Contrast enhancement
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColor4f(0.0f, 0.0f, 0.0f, 0.3f); // Darken mid-tones
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Smooth atmospheric gradient (sky to ground)
    glBegin(GL_QUADS);
    // Sky (top) - darker/cooler
    glColor4f(0.0f, 0.0f, 0.0f, 0.25f);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    
    // Horizon (middle) - neutral
    glColor4f(0.0f, 0.0f, 0.0f, 0.05f);
    glVertex2f(screenWidth, screenHeight * 0.5f);
    glVertex2f(0, screenHeight * 0.5f);
    glEnd();
    
    glBegin(GL_QUADS);
    // Horizon (middle) - neutral  
    glColor4f(0.1f, 0.15f, 0.1f, 0.05f);
    glVertex2f(0, screenHeight * 0.5f);
    glVertex2f(screenWidth, screenHeight * 0.5f);
    
    // Ground (bottom) - warmer
    glColor4f(0.15f, 0.2f, 0.15f, 0.15f);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
}

// Smart thermal that mimics the post-processing look
void RenderSmartThermal(int screenWidth, int screenHeight)
{
    // Base inversion effect
    glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
    glColor4f(0.8f, 0.8f, 0.8f, 1.0f);
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Prevent pure black - add minimum brightness
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColor4f(0.2f, 0.2f, 0.2f, 0.6f);
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Smooth thermal gradient (cold sky to warm ground)
    glBegin(GL_QUADS);
    // Cold sky (top)
    glColor4f(0.0f, 0.0f, 0.0f, 0.3f);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    
    // Horizon (middle) - neutral
    glColor4f(0.0f, 0.0f, 0.0f, 0.05f);
    glVertex2f(screenWidth, screenHeight * 0.5f);
    glVertex2f(0, screenHeight * 0.5f);
    glEnd();
    
    glBegin(GL_QUADS);
    // Horizon (middle) - neutral
    glColor4f(0.1f, 0.1f, 0.1f, 0.05f);
    glVertex2f(0, screenHeight * 0.5f);
    glVertex2f(screenWidth, screenHeight * 0.5f);
    
    // Warm ground (bottom)
    glColor4f(0.2f, 0.2f, 0.2f, 0.2f);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
}

// Smart IR that mimics the post-processing look
void RenderSmartIR(int screenWidth, int screenHeight)
{
    // High contrast base
    glBlendFunc(GL_DST_COLOR, GL_ZERO);
    glColor4f(0.3f, 0.3f, 0.3f, 1.0f); // Darken everything
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Harsh contrast enhancement
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColor4f(1.0f, 1.0f, 1.0f, 0.4f); // Brighten highlights
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Minimum visibility floor
    glColor4f(0.15f, 0.15f, 0.15f, 0.7f);
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    // Add grid pattern for digital look
    glColor4f(1.0f, 1.0f, 1.0f, 0.03f);
    glLineWidth(1.0f);
    glBegin(GL_LINES);
    for (int x = 0; x < screenWidth; x += 16) {
        glVertex2f(x, 0);
        glVertex2f(x, screenHeight);
    }
    for (int y = 0; y < screenHeight; y += 16) {
        glVertex2f(0, y);
        glVertex2f(screenWidth, y);
    }
    glEnd();
}

void SetMonochromeFilter(int enabled)
{
    gMonochromeEnabled = enabled;
}

void SetThermalMode(int enabled)
{
    gThermalEnabled = enabled;
}

void SetIRMode(int enabled)
{
    gIREnabled = enabled;
}

void SetImageEnhancement(float brightness, float contrast)
{
    gBrightness = brightness;
    gContrast = contrast;
}

void RenderVisualEffects(int screenWidth, int screenHeight)
{
    gFrameCounter++;
    
    // Determine processing mode
    int processingMode = 0;
    if (gMonochromeEnabled) processingMode = 1;
    else if (gThermalEnabled) processingMode = 2;
    else if (gIREnabled) processingMode = 3;
    
    // Try hybrid approach first (much faster but same visual quality)
    if (gUseHybridMode && processingMode > 0) {
        RenderHybridEffects(screenWidth, screenHeight, processingMode);
        return;
    }
    
    // Fallback to post-processing (slower but works)
    if (gPostProcessingEnabled && processingMode > 0) {
        RenderPostProcessing(screenWidth, screenHeight);
        
        // Still add overlays like noise and scan lines
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();
        glOrtho(0, screenWidth, screenHeight, 0, -1, 1);
        
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadIdentity();
        
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        
        if (gNoiseEnabled) {
            RenderCameraNoise(screenWidth, screenHeight);
        }
        
        if (gScanLinesEnabled) {
            RenderScanLines(screenWidth, screenHeight);
        }
        
        glEnable(GL_DEPTH_TEST);
        glDisable(GL_BLEND);
        glPopMatrix();
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
        glMatrixMode(GL_MODELVIEW);
        
        return;
    }
    
    // Fallback to overlay mode if post-processing fails
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0, screenWidth, screenHeight, 0, -1, 1);
    
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    
    if (gMonochromeEnabled) {
        RenderMonochromeFilter(screenWidth, screenHeight);
    }
    
    if (gThermalEnabled) {
        RenderThermalEffects(screenWidth, screenHeight);
    }
    
    if (gNoiseEnabled) {
        RenderCameraNoise(screenWidth, screenHeight);
    }
    
    if (gScanLinesEnabled) {
        RenderScanLines(screenWidth, screenHeight);
    }
    
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
}

void RenderMonochromeFilter(int screenWidth, int screenHeight)
{
    glBlendFunc(GL_DST_COLOR, GL_ZERO);
    glColor4f(0.3f, 1.0f, 0.3f, 1.0f);
    
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    float brightness_adj = (gBrightness - 1.0f) * 0.3f;
    if (brightness_adj > 0) {
        glColor4f(brightness_adj, brightness_adj, brightness_adj, 0.5f);
    } else {
        glColor4f(0.0f, 0.0f, 0.0f, -brightness_adj * 0.5f);
    }
    
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    glColor4f(1.0f, 1.0f, 1.0f, 0.8f);
    glLineWidth(1.0f);
    
    glBegin(GL_LINES);
    for (int i = 0; i < 10; i++) {
        float y = 50 + i * (screenHeight - 100) / 10.0f;
        glVertex2f(10, y);
        glVertex2f(25, y);
    }
    glEnd();
}

void RenderThermalEffects(int screenWidth, int screenHeight)
{
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColor4f(1.0f, 0.4f, 0.0f, 0.15f);
    
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    glColor4f(1.0f, 1.0f, 1.0f, 0.8f);
    glLineWidth(1.0f);
    
    glBegin(GL_LINES);
    for (int i = 0; i < 10; i++) {
        float y = 50 + i * (screenHeight - 100) / 10.0f;
        glVertex2f(10, y);
        glVertex2f(25, y);
    }
    glEnd();
}

void RenderCameraNoise(int screenWidth, int screenHeight)
{
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    glColor4f(1.0f, 1.0f, 1.0f, gNoiseIntensity);
    glPointSize(1.0f);
    
    glBegin(GL_POINTS);
    
    srand(gFrameCounter / 2);
    
    int noisePoints = (screenWidth * screenHeight) / 2000;
    for (int i = 0; i < noisePoints; i++) {
        float x = rand() % screenWidth;
        float y = rand() % screenHeight;
        
        float intensity = (rand() % 100) / 100.0f * gNoiseIntensity;
        glColor4f(intensity, intensity, intensity, intensity);
        glVertex2f(x, y);
    }
    
    glEnd();
    
    if ((gFrameCounter % 120) < 3) {
        glColor4f(1.0f, 1.0f, 1.0f, 0.3f);
        glLineWidth(1.0f);
        
        glBegin(GL_LINES);
        for (int i = 0; i < 5; i++) {
            float y = rand() % screenHeight;
            glVertex2f(0, y);
            glVertex2f(screenWidth, y);
        }
        glEnd();
    }
}

void RenderScanLines(int screenWidth, int screenHeight)
{
    if (gScanLineOpacity <= 0.0f) return;
    
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glColor4f(0.0f, 0.0f, 0.0f, gScanLineOpacity);
    glLineWidth(1.0f);
    
    glBegin(GL_LINES);
    for (int y = 2; y < screenHeight; y += 3) {
        glVertex2f(0, y);
        glVertex2f(screenWidth, y);
    }
    glEnd();
}

void RenderIRFilter(int screenWidth, int screenHeight)
{
    glBlendFunc(GL_DST_COLOR, GL_ZERO);
    
    glColor4f(0.4f, 0.4f, 0.4f, 1.0f);
    
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    float contrast = gContrast * 1.5f;
    glColor4f(contrast, contrast, contrast, 0.3f);
    
    glBegin(GL_QUADS);
    glVertex2f(0, 0);
    glVertex2f(screenWidth, 0);
    glVertex2f(screenWidth, screenHeight);
    glVertex2f(0, screenHeight);
    glEnd();
    
    glColor4f(1.0f, 1.0f, 1.0f, 0.1f);
    glLineWidth(1.0f);
    
    glBegin(GL_LINES);
    for (int x = 0; x < screenWidth; x += 8) {
        glVertex2f(x, 0);
        glVertex2f(x, screenHeight);
    }
    for (int y = 0; y < screenHeight; y += 8) {
        glVertex2f(0, y);
        glVertex2f(screenWidth, y);
    }
    glEnd();
}

void CycleVisualModes()
{
    static int mode = 0;
    mode = (mode + 1) % 4;
    
    switch (mode) {
        case 0:
            SetMonochromeFilter(0);
            SetThermalMode(0);
            SetIRMode(0);
            gNoiseEnabled = 0;
            gScanLinesEnabled = 0;
            break;
            
        case 1:
            SetMonochromeFilter(1);
            SetThermalMode(0);
            SetIRMode(0);
            gNoiseEnabled = 1;
            gScanLinesEnabled = 1;
            break;
            
        case 2:
            SetMonochromeFilter(0);
            SetThermalMode(1);
            SetIRMode(0);
            gNoiseEnabled = 1;
            gScanLinesEnabled = 0;
            break;
            
        case 3:
            SetMonochromeFilter(1);
            SetThermalMode(0);
            SetIRMode(0);
            gNoiseEnabled = 1;
            gScanLinesEnabled = 1;
            break;
    }
}

void GetVisualEffectsStatus(char* statusBuffer, int bufferSize)
{
    const char* mode = "STANDARD";
    if (gMonochromeEnabled && gThermalEnabled) mode = "ENHANCED";
    else if (gThermalEnabled) mode = "THERMAL";
    else if (gMonochromeEnabled) mode = "MONO";
    
    snprintf(statusBuffer, bufferSize, "VFX: %s", mode);
    statusBuffer[bufferSize - 1] = '\0';
}