xp12camera

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

FLIR_Camera.cpp

/*
 * X-Plane 12 FLIR camera simulation plugin with realistic belly-mounted camera positioning,
 * optical zoom, pan/tilt controls, military-style targeting reticles, and thermal overlay effects
 *
 * 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 <windows.h>
#include <GL/gl.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#include "XPLMDisplay.h"
#include "XPLMUtilities.h"
#include "XPLMCamera.h"
#include "XPLMDataAccess.h"
#include "XPLMGraphics.h"
#include "XPLMProcessing.h"
#include "XPLMMenus.h"
#include "FLIR_SimpleLock.h"
#include "FLIR_VisualEffects.h"
static XPLMHotKeyID gActivateKey = NULL;
static XPLMHotKeyID gZoomInKey = NULL;
static XPLMHotKeyID gZoomOutKey = NULL;
static XPLMHotKeyID gThermalToggleKey = NULL;
static XPLMHotKeyID gFocusLockKey = NULL;

static XPLMDataRef gPlaneX = NULL;
static XPLMDataRef gPlaneY = NULL;
static XPLMDataRef gPlaneZ = NULL;
static XPLMDataRef gPlaneHeading = NULL;
static XPLMDataRef gPlanePitch = NULL;
static XPLMDataRef gPlaneRoll = NULL;
static XPLMDataRef gManipulatorDisabled = NULL;

static int gCameraActive = 0;
static int gDrawCallbackRegistered = 0;
static float gZoomLevel = 1.0f;
static float gCameraPan = 0.0f;
static float gCameraTilt = -15.0f;
static float gCameraHeight = -5.0f;
static float gCameraDistance = 3.0f;
static int gLastMouseX = 0;
static int gLastMouseY = 0;
static float gMouseSensitivity = 0.2f;
static void ActivateFLIRCallback(void* inRefcon);
static void ZoomInCallback(void* inRefcon);
static void ZoomOutCallback(void* inRefcon);
static void ThermalToggleCallback(void* inRefcon);
static void FocusLockCallback(void* inRefcon);
static int FLIRCameraFunc(XPLMCameraPosition_t* outCameraPosition, int inIsLosingControl, void* inRefcon);
static int DrawThermalOverlay(XPLMDrawingPhase inPhase, int inIsBefore, void* inRefcon);
static void DrawRealisticThermalOverlay(void);
 
PLUGIN_API int XPluginStart(char* outName, char* outSig, char* outDesc)
{
    strcpy(outName, "FLIR Camera System");
    strcpy(outSig, "flir.camera.system");
    strcpy(outDesc, "Realistic FLIR camera with zoom and thermal overlay");

    gPlaneX = XPLMFindDataRef("sim/flightmodel/position/local_x");
    gPlaneY = XPLMFindDataRef("sim/flightmodel/position/local_y");
    gPlaneZ = XPLMFindDataRef("sim/flightmodel/position/local_z");
    gPlaneHeading = XPLMFindDataRef("sim/flightmodel/position/psi");
    gPlanePitch = XPLMFindDataRef("sim/flightmodel/position/theta");
    gPlaneRoll = XPLMFindDataRef("sim/flightmodel/position/phi");
    gManipulatorDisabled = XPLMFindDataRef("sim/operation/prefs/misc/manipulator_disabled");

    InitializeSimpleLock();
    InitializeVisualEffects();
    gActivateKey = XPLMRegisterHotKey(XPLM_VK_F9, xplm_DownFlag, "Activate FLIR Camera", ActivateFLIRCallback, NULL);
    gZoomInKey = XPLMRegisterHotKey(XPLM_VK_EQUAL, xplm_DownFlag, "FLIR Zoom In", ZoomInCallback, NULL);
    gZoomOutKey = XPLMRegisterHotKey(XPLM_VK_MINUS, xplm_DownFlag, "FLIR Zoom Out", ZoomOutCallback, NULL);
    gThermalToggleKey = XPLMRegisterHotKey(XPLM_VK_T, xplm_DownFlag, "FLIR Visual Effects Toggle", ThermalToggleCallback, NULL);
    gFocusLockKey = XPLMRegisterHotKey(XPLM_VK_SPACE, xplm_DownFlag, "FLIR Focus/Lock Target", FocusLockCallback, NULL);

    return 1;
}
PLUGIN_API void XPluginStop(void)
{
    if (gActivateKey) XPLMUnregisterHotKey(gActivateKey);
    if (gZoomInKey) XPLMUnregisterHotKey(gZoomInKey);
    if (gZoomOutKey) XPLMUnregisterHotKey(gZoomOutKey);
    if (gThermalToggleKey) XPLMUnregisterHotKey(gThermalToggleKey);
    if (gFocusLockKey) XPLMUnregisterHotKey(gFocusLockKey);

    if (gCameraActive) {
        XPLMDontControlCamera();
        gCameraActive = 0;
    }
    
    CleanupVisualEffects();
}
PLUGIN_API void XPluginDisable(void) { }
PLUGIN_API int XPluginEnable(void) { return 1; }
PLUGIN_API void XPluginReceiveMessage(XPLMPluginID inFromWho, int inMessage, void* inParam) { }
 
static void ActivateFLIRCallback(void* inRefcon)
{
    if (!gCameraActive) {
        XPLMControlCamera(xplm_ControlCameraUntilViewChanges, FLIRCameraFunc, NULL);
        gCameraActive = 1;
        
        if (gManipulatorDisabled) {
            XPLMSetDatai(gManipulatorDisabled, 1);
        }
        
        if (!gDrawCallbackRegistered) {
            XPLMRegisterDrawCallback(DrawThermalOverlay, xplm_Phase_Window, 0, NULL);
            gDrawCallbackRegistered = 1;
        }
    } else {
        XPLMDontControlCamera();
        gCameraActive = 0;
        
        if (gManipulatorDisabled) {
            XPLMSetDatai(gManipulatorDisabled, 0);
        }
        
        DisableSimpleLock();
        
        if (gDrawCallbackRegistered) {
            XPLMUnregisterDrawCallback(DrawThermalOverlay, xplm_Phase_Window, 0, NULL);
            gDrawCallbackRegistered = 0;
        }
    }
}
 
static void ZoomInCallback(void* inRefcon)
{
    if (gCameraActive) {
        if (gZoomLevel < 1.5f) gZoomLevel = 1.5f;
        else if (gZoomLevel < 2.0f) gZoomLevel = 2.0f;
        else if (gZoomLevel < 3.0f) gZoomLevel = 3.0f;
        else if (gZoomLevel < 4.0f) gZoomLevel = 4.0f;
        else if (gZoomLevel < 6.0f) gZoomLevel = 6.0f;
        else if (gZoomLevel < 8.0f) gZoomLevel = 8.0f;
        else if (gZoomLevel < 12.0f) gZoomLevel = 12.0f;
        else if (gZoomLevel < 16.0f) gZoomLevel = 16.0f;
        else if (gZoomLevel < 24.0f) gZoomLevel = 24.0f;
        else if (gZoomLevel < 32.0f) gZoomLevel = 32.0f;
        else if (gZoomLevel < 48.0f) gZoomLevel = 48.0f;
        else if (gZoomLevel < 64.0f) gZoomLevel = 64.0f;
        else gZoomLevel = 64.0f;
    }
}
 
static void ZoomOutCallback(void* inRefcon)
{
    if (gCameraActive) {
        if (gZoomLevel > 48.0f) gZoomLevel = 48.0f;
        else if (gZoomLevel > 32.0f) gZoomLevel = 32.0f;
        else if (gZoomLevel > 24.0f) gZoomLevel = 24.0f;
        else if (gZoomLevel > 16.0f) gZoomLevel = 16.0f;
        else if (gZoomLevel > 12.0f) gZoomLevel = 12.0f;
        else if (gZoomLevel > 8.0f) gZoomLevel = 8.0f;
        else if (gZoomLevel > 6.0f) gZoomLevel = 6.0f;
        else if (gZoomLevel > 4.0f) gZoomLevel = 4.0f;
        else if (gZoomLevel > 3.0f) gZoomLevel = 3.0f;
        else if (gZoomLevel > 2.0f) gZoomLevel = 2.0f;
        else if (gZoomLevel > 1.5f) gZoomLevel = 1.5f;
        else gZoomLevel = 1.0f;
    }
}
 
static float GetZoomBasedSensitivity(float baseSpeed)
{
    // Enhanced zoom sensitivity algorithm for all zoom levels
    // Uses logarithmic scaling for more natural feel
    
    // Clamp zoom level to reasonable range
    float clampedZoom = fmaxf(1.0f, fminf(gZoomLevel, 64.0f));
    
    // Logarithmic scaling feels more natural for zoom sensitivity
    // log(1) = 0, log(64) ≈ 4.16
    float logZoom = logf(clampedZoom);
    float maxLogZoom = logf(64.0f); // ≈ 4.16
    
    // Normalize to 0-1 range
    float normalizedZoom = logZoom / maxLogZoom;
    
    // Apply sensitivity curve - more aggressive at higher zooms
    // At 1x zoom: 100% sensitivity
    // At 2x zoom: ~85% sensitivity  
    // At 8x zoom: ~50% sensitivity
    // At 32x zoom: ~15% sensitivity
    // At 64x zoom: ~3% sensitivity
    float sensitivityMultiplier = 1.0f - (normalizedZoom * normalizedZoom * 0.97f);
    
    // Apply minimum threshold for usability
    float minSensitivity = baseSpeed * 0.03f; // 3% minimum
    float finalSensitivity = baseSpeed * sensitivityMultiplier;
    
    return fmaxf(finalSensitivity, minSensitivity);
}

 
static void ThermalToggleCallback(void* inRefcon)
{
    if (gCameraActive) {
        CycleVisualModes();
    }
}

static void FocusLockCallback(void* inRefcon)
{
    if (gCameraActive) {
        if (!IsSimpleLockActive()) {
            LockCurrentDirection(gCameraPan, gCameraTilt);
        } else {
            DisableSimpleLock();
        }
    }
}
 
static int FLIRCameraFunc(XPLMCameraPosition_t* outCameraPosition, int inIsLosingControl, void* inRefcon)
{
    if (inIsLosingControl) {
        gCameraActive = 0;
        if (gDrawCallbackRegistered) {
            XPLMUnregisterDrawCallback(DrawThermalOverlay, xplm_Phase_Window, 0, NULL);
            gDrawCallbackRegistered = 0;
        }
        if (gManipulatorDisabled) {
            XPLMSetDatai(gManipulatorDisabled, 0);
        }
        DisableSimpleLock();
        return 0;
    }
    
    if (!gPlaneX || !gPlaneY || !gPlaneZ || !gPlaneHeading || !gPlanePitch || !gPlaneRoll) {
        return 1;
    }
    
    float planeX = XPLMGetDataf(gPlaneX);
    float planeY = XPLMGetDataf(gPlaneY);
    float planeZ = XPLMGetDataf(gPlaneZ);
    float planeHeading = XPLMGetDataf(gPlaneHeading);
    
    float headingRad = planeHeading * M_PI / 180.0f;
    
    outCameraPosition->x = planeX + gCameraDistance * sin(headingRad);
    outCameraPosition->y = planeY + gCameraHeight;
    outCameraPosition->z = planeZ + gCameraDistance * cos(headingRad);
    
    if (!IsSimpleLockActive()) {
        int mouseX, mouseY;
        XPLMGetMouseLocation(&mouseX, &mouseY);
        
        if (gLastMouseX != 0 || gLastMouseY != 0) {
            float zoomBasedMouseSensitivity = GetZoomBasedSensitivity(gMouseSensitivity);
            float deltaX = (mouseX - gLastMouseX) * zoomBasedMouseSensitivity;
            float deltaY = (mouseY - gLastMouseY) * zoomBasedMouseSensitivity;
            
            gCameraPan += deltaX;
            gCameraTilt -= deltaY;
            
            while (gCameraPan > 180.0f) gCameraPan -= 360.0f;
            while (gCameraPan < -180.0f) gCameraPan += 360.0f;
            
            if (gCameraTilt > 45.0f) gCameraTilt = 45.0f;
            if (gCameraTilt < -90.0f) gCameraTilt = -90.0f;
        }
        
        gLastMouseX = mouseX;
        gLastMouseY = mouseY;
    } else {
        GetLockedAngles(&gCameraPan, &gCameraTilt);
    }
    
    outCameraPosition->heading = planeHeading + gCameraPan;
    outCameraPosition->pitch = gCameraTilt;
    outCameraPosition->roll = 0.0f;
    outCameraPosition->zoom = gZoomLevel;
    
    return 1;
}
 
static int DrawThermalOverlay(XPLMDrawingPhase inPhase, int inIsBefore, void* inRefcon)
{
    if (!gCameraActive) return 1;
    
    DrawRealisticThermalOverlay();
    
    return 1;
}

static void DrawRealisticThermalOverlay(void)
{
    int screenWidth, screenHeight;
    XPLMGetScreenSize(&screenWidth, &screenHeight);
    
    RenderVisualEffects(screenWidth, screenHeight);
    
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0, screenWidth, screenHeight, 0, -1, 1);
    
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    float centerX = screenWidth / 2.0f;
    float centerY = screenHeight / 2.0f;
    
    if (IsSimpleLockActive()) {
        glColor4f(1.0f, 0.0f, 0.0f, 0.9f);
    } else {
        glColor4f(0.0f, 1.0f, 0.0f, 0.9f);
    }
    
    glLineWidth(2.0f);
    
    glBegin(GL_LINES);
    glVertex2f(centerX - 20, centerY);
    glVertex2f(centerX + 20, centerY);
    glVertex2f(centerX, centerY - 20);
    glVertex2f(centerX, centerY + 20);
    glEnd();
    
    float bracketSize = 50.0f;
    float bracketLength = 20.0f;
    
    glBegin(GL_LINES);
    glVertex2f(centerX - bracketSize, centerY - bracketSize);
    glVertex2f(centerX - bracketSize + bracketLength, centerY - bracketSize);
    glVertex2f(centerX - bracketSize, centerY - bracketSize);
    glVertex2f(centerX - bracketSize, centerY - bracketSize + bracketLength);
    
    glVertex2f(centerX + bracketSize, centerY - bracketSize);
    glVertex2f(centerX + bracketSize - bracketLength, centerY - bracketSize);
    glVertex2f(centerX + bracketSize, centerY - bracketSize);
    glVertex2f(centerX + bracketSize, centerY - bracketSize + bracketLength);
    
    glVertex2f(centerX - bracketSize, centerY + bracketSize);
    glVertex2f(centerX - bracketSize + bracketLength, centerY + bracketSize);
    glVertex2f(centerX - bracketSize, centerY + bracketSize);
    glVertex2f(centerX - bracketSize, centerY + bracketSize - bracketLength);
    
    glVertex2f(centerX + bracketSize, centerY + bracketSize);
    glVertex2f(centerX + bracketSize - bracketLength, centerY + bracketSize);
    glVertex2f(centerX + bracketSize, centerY + bracketSize);
    glVertex2f(centerX + bracketSize, centerY + bracketSize - bracketLength);
    glEnd();
    
    glPointSize(3.0f);
    glBegin(GL_POINTS);
    glVertex2f(centerX, centerY);
    glEnd();
    
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_BLEND);
    glLineWidth(1.0f);
    glPointSize(1.0f);
    
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
}