fareyi kullanarak yön kontrolünü etkinleştirerek kameranın uygulamasını tamamlıyoruz. Kameranın tasarımıyla ilgili çeşitli özgürlük seviyeleri vardır. Bir birinci şahıs oyununda (nişancı veya başka bir şekilde) beklediğiniz kontrol seviyesini etkinleştireceğiz. Bu, kamerayı 360 derece (pozitif Y ekseni etrafında) çevirebileceğimiz anlamına gelir; bu, başınızı sola veya sağa döndürmeye ve vücudunuzla tam bir daire oluşturmaya karşılık gelir. Ek olarak, yukarı ve aşağı daha iyi bir görüntü elde etmek için kamerayı yukarı ve aşağı eğebileceğiz. Kamerayı tam bir daireyi tamamlayana veya bir dönüş sırasında uçak gövdesinin yana yatacağı şekilde eğene kadar açamayız. Bu serbestlik seviyeleri, bu eğitimin kapsamı dışında olan uçuş simülatörlerinin etki alanı içindedir. Herhangi bir oranda,

Aşağıdaki 2.Dünya Savaşı uçaksavar silahı, inşa edeceğimiz kamera türünü göstermektedir:



Silahın iki kontrol ekseni vardır:

Vektör (0,1,0) etrafında 360 derece dönebilir. Bu açıya 'yatay açı' ve vektör 'dikey eksen' olarak adlandırılır.
Yere paralel olan bir vektör etrafında yukarı ve aşağı eğilebilir. Bu hareket biraz sınırlıdır ve tabanca tam bir daire tamamlayamaz. Bu açıya 'dikey açı' ve vektör 'yatay eksen' olarak adlandırılır. Dikey eksen sabitken (0,1,0) yatay eksenin tabanca ile döndüğünü ve her zaman tabanca hedefine dik olduğunu unutmayın. Bu, matematiği doğru bir şekilde elde etmek için anlaşılması gereken önemli bir noktadır.
Plan, farenin hareketini takip etmek ve fare sola ve sağa hareket ettiğinde yatay açıyı, fare yukarı ve aşağı hareket ettiğinde dikey açıyı değiştirmektir. Bu iki açı göz önüne alındığında, hedef ve yukarı vektörleri hesaplamak istiyoruz.

Hedef vektörü yatay açıyla döndürmek oldukça basittir. Temel trigonometri kullanarak, hedef vektörün Z bileşeninin yatay vektörün sinüsü olduğunu ve X bileşeninin yatay açının kosinüsü olduğunu görebiliriz (bu aşamada kamera dümdüz ileri bakar, böylece Y sıfırdır). Bunun bir şemasını görmek için 7. öğreticiyi tekrar ziyaret edin.

Hedef vektörü dikey açıya göre döndürmek, yatay eksen kamera ile birlikte döndüğünden daha karmaşıktır. Yatay eksen, yatay açı ile döndürüldükten sonra dikey eksen ile hedef vektör arasında bir çapraz çarpım kullanılarak hesaplanabilir, ancak rastgele bir vektör etrafında dönme (tabancayı yukarı ve aşağı kaldırma) zor olabilir.

Neyse ki, bu problem için son derece yararlı bir matematiksel araca sahibiz - kuaterniyon. Kuaterniyonlar, 1843'te İrlandalı bir matematikçi olan Sir Willilam Rowan Hamilton tarafından keşfedildi ve karmaşık sayı sistemine dayanıyor. 'Q' kuaterniyonu şu şekilde tanımlanır:



İ, j ve k'nin karmaşık sayılar olduğu ve aşağıdaki denklemin geçerli olduğu durumlarda:



Pratikte, 4-vektör (x, y, z, w) olarak bir kuaterniyon belirleriz. Kuaterniyon 'Q' eşleniği şu şekilde tanımlanır:



Bir kuaterniyonu normalleştirmek, bir vektörü normalleştirmekle aynıdır. Bir vektörü bir kuaterniyon kullanarak rasgele bir vektör etrafında döndürmek için gereken adımları açıklayacağım. Adımların ardındaki matematiksel kanıt hakkında daha fazla ayrıntı web'de bulunabilir.

Döndürülmüş 'V' vektörünü 'a' açısıyla temsil eden bir 'W' kuaterniyonunu hesaplamanın genel işlevi şudur:



Q, şu şekilde tanımlanan rotasyon kuaterniyonudur:



'W' hesaplandıktan sonra döndürülen vektör basittir (Wx, Wy, Wz). 'W' hesaplamasında dikkat edilmesi gereken önemli bir nokta, ilk önce bir kuaterniyon ile sonuçlanan bir kuaterniyon-vektör çarpımı olan 'V' ile 'Q'yu çarpmamız ve sonra bir kuaterniyon yapmamız gerektiğidir. ile kuaterniyon çarpımı (Q * V'nin sonucu 'Q' eşleniği ile çarpılır). İki tür çarpma aynı değildir. Math_3d.cpp dosyası, bu çarpma türlerinin uygulamalarını içerir.

Kullanıcı fareyi ekranda hareket ettirdikçe yatay ve dikey açıları sürekli güncel tutmamız gerekecek ve bunları nasıl başlatacağımıza karar vermemiz gerekecek. Mantıksal seçim, onları kameranın kurucusuna sağlanan hedef vektöre göre başlatmaktır. Yatay açı ile başlayalım. XZ düzlemine yukarıdan bakan aşağıdaki şemaya bir göz atın:



Hedef vektör (x, z) ve alfa harfiyle temsil edilen yatay açıyı bulmak istiyoruz (Y bileşeni yalnızca dikey açı ile ilgilidir). Dairenin yarıçapının uzunluğu 1 olduğundan, alfa'nın sinüs fonksiyonunun tam olarak z olduğunu görmek çok kolaydır. Bu nedenle, z'nin asinini hesaplamak alfa sağlayacaktır. Tamam mıyız? - henüz değil. Z [-1,1] aralığında olabileceğinden, asine sonucu -90 derece ile +90 derece arasındadır. Ancak yatay açının aralığı 360 derecedir. Ek olarak, kuaterniyonumuz saat yönünde dönmektedir. Bu, kuaterniyonla 90 derece döndürdüğümüzde, 90 derecelik gerçek sinüsün (1 olan) zıttı olan Z ekseninde -1 ile son bulduğumuz anlamına gelir. BENİM NACİZANE FİKRİME GÖRE, bu hakkı elde etmenin en kolay yolu, her zaman Z'nin pozitif değerini kullanarak asin fonksiyonunu hesaplamak ve sonucu, vektörün bulunduğu çemberin belirli çeyreği ile birleştirmektir. Örneğin, hedef vektörümüz (0,1) olduğunda, 1'in asini olan 90'ı hesaplıyoruz ve bunu 360'tan çıkarıyoruz. Sonuç 270'tir. 0 - 1 arasındaki asin aralığı 0 - 90 derecedir. Bunu dairenin belirli çeyreği ile birleştirin ve son yatay açıyı elde edin.

Dikey açıyı hesaplamak biraz daha basittir. Hareket aralığını -90 dereceyle (270 dereceye eşit - düz yukarı bakarken) +90 dereceyle (aşağı bakarken) sınırlayacağız. Bu, hedef vektördeki Y bileşeninin asin fonksiyonunun yalnızca negatif değerine ihtiyacımız olduğu anlamına gelir. Y 1'e eşit olduğunda (yukarı baktığımızda) asin 90'dır, bu yüzden sadece işareti tersine çevirmemiz gerekiyor. Y -1'e eşit olduğunda (düz aşağı bakarken) asin -90'dır ve işareti ters çevirmek bizi 90'a götürür. Kafanız karışırsa diyagramı tekrar kontrol edin ve Z'yi Y ve X'i Z ile değiştirin.

Kaynak çözüm yolu

(kamera.cpp: 38)

Kod:
Camera::Camera(int WindowWidth, int WindowHeight, const Vector3f& Pos, const Vector3f& Target, const Vector3f& Up)
{
    m_windowWidth = WindowWidth;
    m_windowHeight = WindowHeight;
    m_pos = Pos;

    m_target = Target;
    m_target.Normalize();

    m_up = Up;
    m_up.Normalize();

    Init();
}
Kameranın kurucusu artık pencerenin boyutlarını alır. Fareyi ekranın ortasına taşımak için buna ihtiyacımız var. Ek olarak, dahili kamera özelliklerini ayarlayan Init () çağrısına dikkat edin.

(kamera.cpp: 54)

Kod:
void Camera::Init()
{
    Vector3f HTarget(m_target.x, 0.0, m_target.z);
    HTarget.Normalize();

    if (HTarget.z >= 0.0f)
    {
        if (HTarget.x >= 0.0f)
        {
            m_AngleH = 360.0f - ToDegree(asin(HTarget.z));
        }
        else
        {
            m_AngleH = 180.0f + ToDegree(asin(HTarget.z));
        }
    }
    else
    {
        if (HTarget.x >= 0.0f)
        {
            m_AngleH = ToDegree(asin(-HTarget.z));
        }
        else
        {
            m_AngleH = 180.0f - ToDegree(asin(-HTarget.z));
        }
    }

    m_AngleV = -ToDegree(asin(m_target.y));

    m_OnUpperEdge = false;
    m_OnLowerEdge = false;
    m_OnLeftEdge = false;
    m_OnRightEdge = false;
    m_mousePos.x = m_windowWidth / 2;
    m_mousePos.y = m_windowHeight / 2;

    glutWarpPointer(m_mousePos.x, m_mousePos.y);
}
Init () fonksiyonunda yatay açıyı hesaplayarak başlarız. Orijinal hedef vektörün XZ düzlemindeki izdüşümü olan HTarget (yatay hedef) adında yeni bir hedef vektör oluşturuyoruz. Sonra onu normalize ederiz (çünkü daha önce açıklanan matematik XZ düzleminde bir birim vektörü varsayar). Ardından hedef vektörün hangi çeyreğe ait olduğunu kontrol edip Z bileşeninin pozitif değerine göre son açıyı hesaplıyoruz. Daha sonra çok daha basit olan dikey açıyı hesaplıyoruz.

Kameranın, farenin ekranın kenarlarından birine konumlandırılıp konumlandırılmadığını gösteren 4 yeni bayrak vardır. Bu gerçekleştiğinde, karşılık gelen yönde otomatik bir dönüş gerçekleştireceğiz. Bu, 360 derece dönmemizi sağlayacaktır. Fare ekranın ortasından başladığı için bayrakları FALSE olarak başlatıyoruz. Sonraki iki kod satırı, ekranın merkezinin nerede olduğunu (pencere boyutuna göre) hesaplar ve yeni glutWarpPointer işlevi fareyi hareket ettirir. Farenin ekranın ortasına gelmesiyle başlamak hayatı çok daha basit hale getiriyor.

(kamera.cpp: 140)

Kod:
void Camera::OnMouse(int x, int y)
{
    const int DeltaX = x - m_mousePos.x;
    const int DeltaY = y - m_mousePos.y;

    m_mousePos.x = x;
    m_mousePos.y = y;

    m_AngleH += (float)DeltaX / 20.0f;
    m_AngleV += (float)DeltaY / 20.0f;

    if (DeltaX == 0) {
        if (x <= MARGIN) {
            m_OnLeftEdge = true;
        }
        else if (x >= (m_windowWidth - MARGIN)) {
            m_OnRightEdge = true;
        }
    }
    else {
        m_OnLeftEdge = false;
        m_OnRightEdge = false;
    }

    if (DeltaY == 0) {
        if (y <= MARGIN) {
            m_OnUpperEdge = true;
        }
        else if (y >= (m_windowHeight - MARGIN)) {
            m_OnLowerEdge = true;
        }
    }
    else {
        m_OnUpperEdge = false;
        m_OnLowerEdge = false;
    }

    Update();
}
Bu işlev, kameraya farenin hareket ettiğini bildirmek için kullanılır. Parametreler, farenin yeni ekran konumudur. Deltayı hem X hem de Y ekseninde önceki konumdan hesaplayarak başlıyoruz. Daha sonra, işleve sonraki çağrı için yeni değerleri saklıyoruz. Deltaları küçülterek mevcut yatay ve dikey açıları güncelliyoruz. Benim için iyi çalışan bir ölçekleme değeri kullanıyorum, ancak farklı bilgisayarlarda farklı ölçekleme değerleri isteyebilirsiniz. Uygulamanın kare hızını bir faktör olarak eklediğimizde, bunu gelecekteki bir öğreticide geliştireceğiz.

Sonraki test grubu, farenin konumuna göre 'm_On * Edge' bayraklarını günceller. Fare ekranın kenarlarından birine yaklaştığında "kenar" davranışını tetikleyen varsayılan olarak 10 piksellik bir kenar boşluğu vardır. Son olarak, hedefi ve yukarı vektörleri yeni yatay ve dikey açılara göre yeniden hesaplamak için Update () 'yi çağırıyoruz.

(kamera.cpp: 183)

Kod:
void Camera::OnRender()
{
    bool ShouldUpdate = false;

    if (m_OnLeftEdge) {
        m_AngleH -= 0.1f;
        ShouldUpdate = true;
    }
    else if (m_OnRightEdge) {
        m_AngleH += 0.1f;
        ShouldUpdate = true;
    }

    if (m_OnUpperEdge) {
        if (m_AngleV > -90.0f) {
            m_AngleV -= 0.1f;
            ShouldUpdate = true;
        }
    }
    else if (m_OnLowerEdge) {
        if (m_AngleV < 90.0f) {
            m_AngleV += 0.1f;
            ShouldUpdate = true;
        }
    }

    if (ShouldUpdate) {
        Update();
    }
}
Bu işlev, ana oluşturma döngüsünden çağrılır. Farenin ekranın kenarlarından birinde bulunduğu ve hareket etmediği durumlar için buna ihtiyacımız var. Bu durumda fare olayı yoktur, ancak yine de kameranın sürekli hareket etmesini istiyoruz (fare kenardan uzaklaşana kadar). Bayraklardan birinin ayarlanıp ayarlanmadığını kontrol edip ilgili açıyı buna göre güncelliyoruz. Açılardan birinde bir değişiklik varsa, hedefi ve yukarı vektörleri güncellemek için Update () adını veriyoruz. Fare ekrandan uzaklaştığında, fare olay işleyicisinde onu algılar ve bayrağı temizleriz. Dikey açının -90 derece ve +90 derece aralıkları arasında sınırlı olduğuna dikkat edin. Bu yukarı veya aşağı bakarken tam bir daire oluşmasını önlemek içindir.

(kamera.cpp: 214)

Kod:
void Camera::Update()
{
    const Vector3f Vaxis(0.0f, 1.0f, 0.0f);

    // Rotate the view vector by the horizontal angle around the vertical axis
    Vector3f View(1.0f, 0.0f, 0.0f);
    View.Rotate(m_AngleH, Vaxis);
    View.Normalize();

    // Rotate the view vector by the vertical angle around the horizontal axis
    Vector3f Haxis = Vaxis.Cross(View);
    Haxis.Normalize();
    View.Rotate(m_AngleV, Haxis);
    View.Normalize();

    m_target = View;
    m_target.Normalize();

    m_up = m_target.Cross(Haxis);
    m_up.Normalize();
}
Bu fonksiyon hedef ve yukarı vektörü yatay ve dikey açılara göre günceller. Görünüm vektörüyle "sıfırlama" durumunda başlarız. Bu, sapa paralel olduğu (dikey açı sıfırdır) ve doğrudan sağa baktığı anlamına gelir (yatay açı sıfırdır - yukarıdaki şemaya bakın). Dikey ekseni düz yukarı bakacak şekilde ayarlıyoruz ve ardından görünüm vektörünü etrafındaki yatay açıyla döndürüyoruz. Sonuç, amaçlanan hedefin genel yönünü gösteren ancak doğru yükseklikte olması gerekmeyen bir vektördür (yani, XZ düzleminde). Dikey eksenle bu vektörün çapraz çarpımını yaparak, XZ düzleminde, görünüm vektörü ve dikey eksen tarafından oluşturulan düzleme dik olan başka bir vektör elde ederiz. Bu bizim yeni yatay eksenimiz ve şimdi vektörü dikey açıya göre etrafında yukarı veya aşağı döndürme zamanı. Sonuç, nihai hedef vektördür ve onu ilgili üye niteliğine yerleştiririz. Şimdi yukarı vektörünü düzeltmeliyiz. Örneğin, kamera yukarı bakıyorsa, telafi etmek için yukarı vektörün geriye eğilmesi gerekir (hedef vektöre göre 90 derece olmalıdır). Bu, gökyüzüne baktığınızda başınızın arkasına eğilme şekline benzer. Yeni yukarı vektör, son hedef vektör ile yatay eksen arasında başka bir çapraz çarpım yapılarak hesaplanır. Dikey açı hala sıfırsa, hedef vektör XZ düzleminde kalır ve yukarı vektör (0,1,0) kalır. Hedef vektör yukarı veya aşağı eğilirse, yukarı vektör sırasıyla geriye veya ileriye doğru eğilir.

(tutorial15.cpp: 209)

Kod:
glutGameModeString("1920x1200@32");
glutEnterGameMode();
Bu bolluk fonksiyonları, uygulamamızın yüksek performanslı 'oyun modu' adı verilen tam ekranda çalışmasını sağlar. Kamerayı 360 derece döndürmeyi kolaylaştırır çünkü yapmanız gereken tek şey fareyi ekranın kenarlarından birine doğru çekmektir. Oyun modu dizisi aracılığıyla yapılandırılan çözünürlüğü ve piksel başına bit sayısını not edin. Piksel başına 32 bit, işleme için maksimum renk sayısını sağlar.

(tutorial15.cpp: 214)

Kod:
pGameCamera = new Camera(WINDOW_WIDTH, WINDOW_HEIGHT);
Kamera, bir aşırı çağrı (glutWarpPointer) gerçekleştirdiği için artık bu konuma dinamik olarak tahsis edilmiştir. Glut henüz başlatılmadıysa bu çağrı başarısız olacaktır.

(tutorial15.cpp: 99)

Kod:
glutPassiveMotionFunc(PassiveMouseCB);
glutKeyboardFunc(KeyboardCB);
Burada iki yeni glut callback fonksiyonu kaydediyoruz. Biri fare için, diğeri ise normal klavye tıklamaları içindir (özel klavye geri arama, yön ve işlev tuşlarını yakalar). Pasif hareket, farenin herhangi bir düğmesine basılmadan hareket etmesi anlamına gelir.

(tutorial15.cpp: 81)

Kod:
static void KeyboardCB(unsigned char Key, int x, int y)
{
    switch (Key) {
        case 'q':
            exit(0);
    }
}

static void PassiveMouseCB(int x, int y)
{
    pGameCamera->OnMouse(x, y);
}
Artık tam ekran modunu kullandığımıza göre, uygulamadan çıkmak daha zor. Klavye geri arama, 'q' tuşunu yakalar ve çıkar. Fare geri araması, farenin konumunu kameraya aktarır.

(tutorial15.cpp: 44)

Kod:
static void RenderSceneCB()
{
    pGameCamera->OnRender();
Ana oluşturma döngüsünde olduğumuzda kameraya haber vermeliyiz. Bu, farenin hareket etmediği ve ekranın kenarlarından birinin üzerindeyken kameraya dönme şansı verir.