Chapitre -112 · Mouse

Inputs — souris & Trackpad

Dans cette partie tu vas mettre en place tout un système pour contrôler ta souris, faire le pont Objective-C → C++ et contrôler ta caméra.

Le bridge

Il n'est pas essentiel de connaitre en détail cette partie, ce n'est pas encore du shading pur.

Complètes ta structure partagée

    /// C++ — InputState.h — update:18/05/26
    bool keyD     = false; // tu avais déjà

    bool          leftMouseDown  = false;
    bool          rightMouseDown = false;
    simd::float2  mousePosition = { 0, 0 }; // position absolue pixel
    
    // État continu — Mouvement (valeurs normalisées -1 à 1)
    simd::float3  moveDirection = { 0, 0, 0 }; // donnée non-partagée avec le GPU, float3 suffisant
    simd::float2  mouseDelta    = { 0, 0 };    // drag x,y accumulés depuis le dernier poll
    float         scrollDelta   = 0.f;
    float         speedMultiplier = 1.0f; // tu avais déjà
};

Le déplacement souris

NSTrackingArea — c'est quoi ?

La plupart des entrées clavier et souris sont reçues par la classe NSWindow dans RenderWindow.

Le déplacement de la souris provient de la classe NSTrackingArea dans ApplicationController.

/// Objective-C++ — Exemple — avait été créé dans createView() :
NSTrackingArea* trackingArea = [[NSTrackingArea alloc]
                                initWithRect:_mtkView.bounds // taille maximale d'une fenêtre
                                options:(NSTrackingMouseMoved | NSTrackingActiveInKeyWindow |
                                                NSTrackingInVisibleRect | NSTrackingActiveAlways)
                                owner:self userInfo:nil];
[_mtkView addTrackingArea:trackingArea];
// Sans ça, mouseMoved n'est appelé que pendant un drag.

Tu stockes ce que la fonction mouseMoved() fournie renvoie via la variable delta.

/// Objective-C++ — AppViewController.mm — update:18/05/26 — ApplicationController
@implementation ApplicationController
{
    RenderWindow*             _window;
    std::unique_ptr<Renderer> _renderer;
    simd::float2              _mousePosition; // compatible event.delta renvoyé par NSTrackingArea
}

Tu calculs les deltas :

\( scaleX = \frac{largeurFenêtre}{largeurLimite} \qquad scaleY = \frac{hauteurFenêtre}{hauteurLimite} \)

\( mouseX = positionCurseurX * scaleX \)

\( mouseY = (hauteurLimite - positionCurseurY) * scaleY \)

/// Objective-C++ — AppViewController.mm — update:18/05/26 — ApplicationController
// cursor libre — nécessite NSTrackingArea
- (void)mouseMoved:(NSEvent *)event // déjà déclarée mais vide pour éviter une erreur
{
    NSPoint p = [_mtkView convertPoint:[event locationInWindow] fromView:nil];
    CGSize  d = _mtkView.drawableSize, b = _mtkView.bounds.size;
    _mousePosition = { static_cast<float>(p.x * (d.width / b.width)),
                       static_cast<float>((b.height - p.y) * (d.height / b.height)) };
} // Conversion points → pixels (Retina : backingScaleFactor ≥ 2) + Y-flip

Tu ajoutes la donnée event.deltaX et la donnée event.deltaY à la structure avant l'envoie vers ton code C++.

/// Objective-C++ — AppViewController.mm — update:18/05/26 — ApplicationController
- (void)drawInMTKView:(nonnull MTKView *)view
{
    static CFTimeInterval lastTime = CACurrentMediaTime();
    CFTimeInterval now = CACurrentMediaTime();
    float delta = (float)(now - lastTime);
    lastTime = now;
    delta = fminf(delta, 0.1f);
    InputState input = [_window pollInputState];
    input.mousePosition = _mousePosition; // ← injecté ici
    _renderer->m_input = input;
    _renderer->draw((__bridge MTK::View *)view, now, delta);
}

Le reste des entrées souris

Retina — points vs pixels

// Un "point" AppKit ≠ un pixel sur Retina
// backingScaleFactor = 2.0 sur Retina → 1 point = 4 pixels
CGFloat scale = screen.backingScaleFactor ?: 1.0; // 1.0 ou 2.0
CGSize sizePts = _mtkView.bounds.size;
_mtkView.drawableSize = CGSizeMake(sizePts.width * scale, sizePts.height * scale);
// → le drawable Metal est toujours en pixels réels
// → les coordonnées NSEvent sont en points — toujours convertir
/// Objective-C++ — AppViewController.mm — update:18/05/26
@implementation RenderWindow
{
    NSMutableSet<NSNumber*>* _pressedKeys;
    simd::float2             _mouseDelta; // accumulé entre deux frames
    float                    _scrollDelta;
    BOOL                     _leftMouseDown;
    BOOL                     _rightMouseDown;
}
/// Objective-C++ — AppViewController.mm — update:18/05/26 — avant: pollInputState
// mouseDragged : bouton gauche enfoncé — accumuler le delta pour la caméra
- (void)mouseDragged:(NSEvent *)event
{
    constexpr float sensitivity = 0.003f;
    _mouseDelta.x += event.deltaX * sensitivity;
    _mouseDelta.y += event.deltaY * sensitivity;
}
/// Objective-C++ — AppViewController.mm — update:18/05/26 — avant: pollInputState
- (void)mouseDown:(NSEvent *)event      { _leftMouseDown  = YES; }
- (void)mouseUp:(NSEvent *)event        { _leftMouseDown  = NO;  }

- (void)rightMouseDown:(NSEvent *)event { _rightMouseDown = YES; }
- (void)rightMouseUp:(NSEvent *)event   { _rightMouseDown = NO;  }
/// Objective-C++ — AppViewController.mm — update:18/05/26 — avant: pollInputState
// zoom ou défilement UI
- (void)scrollWheel:(NSEvent *)event
{
    float delta = event.scrollingDeltaY;
    if (event.hasPreciseScrollingDeltas) // Trackpad multi-touch → précision fine
        delta *= 0.1f;
    _scrollDelta += delta / 4.f;
}

Notre structure avec toutes les entrées claviers, à chaque image. Prête à être envoyée à notre C++ pour être lue.

/// Objective-C++ — AppViewController.mm — update:18/05/26
    InputState state;
    if ([_pressedKeys containsObject:@(0x7B)]) { state.keyLeft  = true; }
    if ([_pressedKeys containsObject:@(0x7C)]) { state.keyRight = true; }
    if ([_pressedKeys containsObject:@(0x31)]) { state.keySpace = true; }
    if ([_pressedKeys containsObject:@(0x80)]) { state.speedMultiplier = 1.89f; }
    if ([_pressedKeys containsObject:@(0x81)]) { /* Do something */ }
    if ([_pressedKeys containsObject:@(0x35)]) { [[NSApplication sharedApplication] terminate:nil]; }
    state.mouseDelta     = _mouseDelta;   _mouseDelta  = {0, 0};
    state.scrollDelta    = _scrollDelta;  _scrollDelta = 0.f;
    state.leftMouseDown  = _leftMouseDown;
    state.rightMouseDown = _rightMouseDown;

Capturer le curseur (mode FPS)

// Dans keyDown ou un bouton :
// Masquer le curseur + le confiner à la fenêtre
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(NO);  // découple curseur OS et position

// Pour le libérer :
[NSCursor unhide];
CGAssociateMouseAndMouseCursorPosition(YES);
Y-flip — coordonnées écran
AppKit place l'origine en bas à gauche (Y vers le haut). Metal et la plupart des systèmes de rendu placent l'origine en haut à gauche (Y vers le bas). La conversion : mouseY = (boundsSize.height - cursorPos.y) * scaleY. L'oubli de ce flip est une source classique de décalage de picking souris.

Lire ces nouvelles données côté C++ et les utiliser dès aujourd'hui !