Aeon Engine c550894
AeonGames Open Source Game Engine
Loading...
Searching...
No Matches
EngineWindow.cpp
1/*
2Copyright (C) 2016-2019,2021,2022,2024,2025,2026 Rodrigo Jose Hernandez Cordoba
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16#include <QResizeEvent>
17#include <QString>
18#include <QStringList>
19#include <cassert>
20#include <cmath>
21#include <iostream>
22#include <exception>
23#include <stdexcept>
24#include "WorldEditor.h"
25#include "EngineWindow.h"
27#include "aeongames/Renderer.hpp"
28#include "aeongames/Model.hpp"
29#include "aeongames/Animation.hpp"
30#include "aeongames/Mesh.hpp"
31#include "aeongames/ResourceCache.hpp"
32#include "aeongames/Frustum.hpp"
33#include "aeongames/CRC.hpp"
34#include "aeongames/Node.hpp"
35
36namespace AeonGames
37{
38 EngineWindow::EngineWindow ( QWindow *parent ) :
39 QWindow{ parent },
40 mTimer{},
41 mStopWatch{},
42 mFrustumVerticalHalfAngle{}, mStep{ 10 },
43 mCameraRotation ( QQuaternion::fromAxisAndAngle ( 0.0f, 0.0f, 1.0f, 45.0f ) * QQuaternion::fromAxisAndAngle ( 1.0f, 0.0f, 0.0f, -30.0f ) ),
44 // Stand back 3 meters.
45 mCameraLocation { -mCameraRotation.rotatedVector ( forward ) * 300.0f },
46 mViewMatrix{}
47 {
48#if defined(__APPLE__)
49 // On macOS, we dont support OpenGL since it is deprecated
50 // and we need a version higher than the one available,
51 // so lets go with Metal.
52 setSurfaceType ( QSurface::MetalSurface );
53#else
54 setSurfaceType ( QSurface::OpenGLSurface );
55#endif
56
57 /* On Windows we want the window to own its device context.
58 This code works on Qt 5.6.0, but if it does not
59 or a new non Qt application needs to set it,
60 a window class style seems to be mutable by using
61 SetClassLongPtr( ..., GCL_STYLE, ... );
62 instead.
63 https://msdn.microsoft.com/en-us/library/windows/desktop/ms633589%28v=vs.85%29.aspx
64 http://www.gamedev.net/topic/319124-ogl-and-windows/#entry3055268
65
66 This flag is Windows specific, so Linux or Mac implementations
67 will probably just ignore it, I have not verified this though.
68 UPDATE: Linux compilation does not complain about this code.
69 */
70 auto current_flags = flags();
71 if ( ! ( current_flags & Qt::MSWindowsOwnDC ) )
72 {
73 setFlags ( current_flags | Qt::MSWindowsOwnDC );
74 }
75
76 // Get the native window handle
77 mWinId = reinterpret_cast<void*> ( winId() );
78
79 qWorldEditorApp->AttachWindowToRenderer ( mWinId );
80 connect ( &mTimer, SIGNAL ( timeout() ), this, SLOT ( requestUpdate() ) );
81
82 QSettings settings{};
83 settings.beginGroup ( "Camera" );
84 mFieldOfView = settings.value ( "FieldOfView", 60.0f ).toFloat();
85 mNear = settings.value ( "Near", 1.0f ).toFloat();
86 mFar = settings.value ( "Far", 1600.0f ).toFloat();
87 settings.endGroup();
88 settings.beginGroup ( "Workspace" );
89 mHorizontalSpacing = settings.value ( "HorizontalSpacing", uint32_t ( 16 ) ).toUInt();
90 mVerticalSpacing = settings.value ( "VerticalSpacing", uint32_t ( 16 ) ).toUInt();
91 QColor background_color = settings.value ( "BackgroundColor", QColor ( 127, 127, 127 ) ).value<QColor>();
92 settings.endGroup();
93
94 qWorldEditorApp->GetRenderer()->SetClearColor ( mWinId, background_color.redF(), background_color.greenF(), background_color.blueF(), 1.0f );
95
96 updateViewMatrix();
97#if 0
98 float half_radius = ( static_cast<float> ( geometry().size().width() ) / static_cast<float> ( geometry().size().height() ) ) / 2;
99 float v1[2] = { 1, 0 };
100 float v2[2] = { 1, half_radius };
101 float length = sqrtf ( ( v2[0] * v2[0] ) + ( v2[1] * v2[1] ) );
102 v2[0] /= length;
103 v2[1] /= length;
104 mFrustumVerticalHalfAngle = acosf ( ( v1[0] * v2[0] ) + ( v1[1] * v2[1] ) );
105#endif
106 }
107
109 {
110 stop();
111 mTimer.disconnect();
112 // Detach from renderer BEFORE Qt destroys the NSView
113 // This ensures Vulkan releases the Metal layer first
114 qWorldEditorApp->DetachWindowFromRenderer ( mWinId );
115 // Reset to prevent use-after-free
116 mWinId = nullptr;
117 std::cout << LogLevel::Info << "EngineWindow destroyed" << std::endl;
118 }
119
121 {
122 if ( mStopWatch.isValid() && mTimer.isActive() )
123 {
124 mTimer.stop();
125 mStopWatch.invalidate();
126 }
127 }
128
130 {
131 if ( !mStopWatch.isValid() && !mTimer.isActive() )
132 {
133 mTimer.start ( 0 );
134 mStopWatch.start();
135 }
136 }
137
138 void EngineWindow::setScene ( const Scene* aScene )
139 {
140 mScene = aScene;
141 }
142 void EngineWindow::SetFieldOfView ( float aFieldOfView )
143 {
144 mFieldOfView = aFieldOfView;
145 Matrix4x4 projection{};
146 projection.Perspective ( mFieldOfView, mAspectRatio, mNear, mFar );
147 qWorldEditorApp->GetRenderer()->SetProjectionMatrix ( mWinId, projection );
148 }
149 void EngineWindow::SetNear ( float aNear )
150 {
151 mNear = aNear;
152 Matrix4x4 projection{};
153 projection.Perspective ( mFieldOfView, mAspectRatio, mNear, mFar );
154 qWorldEditorApp->GetRenderer()->SetProjectionMatrix ( mWinId, projection );
155 }
156 void EngineWindow::SetFar ( float aFar )
157 {
158 mFar = aFar;
159 Matrix4x4 projection{};
160 projection.Perspective ( mFieldOfView, mAspectRatio, mNear, mFar );
161 qWorldEditorApp->GetRenderer()->SetProjectionMatrix ( mWinId, projection );
162 }
163#if 0
164 // Commented pending Refactor
165 void EngineWindow::setModel ( const QString & filename )
166 {
168 mNode->AttachComponent ( ModelInstance::TypeId, {},
169 std::make_shared<ModelInstance> (
170 Get<Model> ( filename.toUtf8().constData(),
171 filename.toUtf8().constData() ) ) );
172 assert ( mNode->GetComponent ( ModelInstance::TypeId ) && "ModelInstance is a nullptr" );
173 mRenderer->Unload ( *mNode );
174 mRenderer->Load ( *mNode );
175 if ( ModelInstance * model_instance = reinterpret_cast<ModelInstance * > ( mNode->GetComponent ( ModelInstance::TypeId ) ) )
176 {
177 // Adjust camera position so model fits the frustum tightly.
178 float diameter = model_instance->GetModel()->GetCenterRadii().GetRadii().GetMaxAxisLenght() * 2;
179 std::cout << "Diameter: " << diameter << std::endl;
180 // Add the near value to the radius just in case the actual object contains the eye position.
181 float half_radius = ( static_cast<float> ( geometry().size().width() ) / static_cast<float> ( geometry().size().height() ) ) / 2;
182 float v1[2] = { 1, 0 };
183 float v2[2] = { 1, half_radius };
184 float length = sqrtf ( ( v2[0] * v2[0] ) + ( v2[1] * v2[1] ) );
185 v2[0] /= length;
186 v2[1] /= length;
187 mFrustumVerticalHalfAngle = acosf ( ( v1[0] * v2[0] ) + ( v1[1] * v2[1] ) );
188
189 if ( mFrustumVerticalHalfAngle == 0.0f )
190 {
191 throw std::runtime_error ( "mFrustumVerticalHalfAngle is zero." );
192 }
193
194 float eye_length = ( diameter ) / std::tan ( mFrustumVerticalHalfAngle );
195 mCameraLocation = QVector4D (
196 QVector3D (
197 model_instance->GetModel()->GetCenterRadii().GetCenter() [0],
198 model_instance->GetModel()->GetCenterRadii().GetCenter() [1],
199 model_instance->GetModel()->GetCenterRadii().GetCenter() [2] ) +
200 ( mCameraRotation.rotatedVector ( -forward ) * eye_length ), 1 );
201 updateViewMatrix();
202 mStep = eye_length / 100.0f;
203 }
204 }
205#endif
206
207 void EngineWindow::resizeEvent ( QResizeEvent * aResizeEvent )
208 {
209 if ( mIsClosing || aResizeEvent->size().isEmpty() )
210 {
211 return;
212 }
213 QWindow::resizeEvent ( aResizeEvent );
214 auto renderer = qWorldEditorApp->GetRenderer();
215 if ( renderer != nullptr )
216 {
217 QSize window_size{ aResizeEvent->size() };
218 renderer->ResizeViewport ( mWinId,
219 0,
220 0,
221 window_size.width() * devicePixelRatio(),
222 window_size.height() * devicePixelRatio() );
223 Matrix4x4 projection {};
224 mAspectRatio = ( static_cast<float> ( window_size.width() ) /
225 static_cast<float> ( window_size.height() ) );
226 projection.Perspective ( mFieldOfView, mAspectRatio, mNear, mFar );
227 renderer->SetProjectionMatrix ( mWinId, projection );
228#if 0
229 static const QMatrix4x4 flipMatrix (
230 1.0f, 0.0f, 0.0f, 0.0f,
231 0.0f, 0.0f, -1.0f, 0.0f,
232 0.0f, -1.0f, 0.0f, 0.0f,
233 0.0f, 0.0f, 0.0f, 1.0f );
234 mProjectionMatrix.setToIdentity();
235 float half_radius = ( static_cast<float> ( aResizeEvent->size().width() ) / static_cast<float> ( aResizeEvent->size().height() ) ) / 2;
236 mProjectionMatrix.frustum ( -half_radius, half_radius, -0.5, 0.5, 1, 1600 );
237 mProjectionMatrix = mProjectionMatrix * flipMatrix;
238 qWorldEditorApp->GetRenderer()->SetProjectionMatrix ( mWinId, mProjectionMatrix.constData() );
239 // Calculate frustum half vertical angle (for fitting nodes into frustum)
240 float v1[2] = { 1, 0 };
241 float v2[2] = { 1, mWindow->GetHalfAspectRatio() };
242 float length = sqrtf ( ( v2[0] * v2[0] ) + ( v2[1] * v2[1] ) );
243 v2[0] /= length;
244 v2[1] /= length;
245 mFrustumVerticalHalfAngle = acosf ( ( v1[0] * v2[0] ) + ( v1[1] * v2[1] ) );
246#endif
247 start();
248 }
249 else
250 {
251 stop();
252 }
253 }
254
255 void EngineWindow::exposeEvent ( QExposeEvent * aExposeEvent )
256 {
257 Q_UNUSED ( aExposeEvent );
258 if ( isExposed() )
259 {
260 start();
261 }
262 else
263 {
264 stop();
265 }
266 }
267
268 bool EngineWindow::event ( QEvent * aEvent )
269 {
270 switch ( aEvent->type() )
271 {
272 case QEvent::UpdateRequest:
273 if ( !geometry().width() || !geometry().height() || !qWorldEditorApp->GetRenderer() )
274 {
275 return QWindow::event ( aEvent );
276 }
277 {
278 double delta = 0.0;
279 if ( mStopWatch.isValid() )
280 {
281 delta = mStopWatch.restart() * 1e-3f;
282 if ( delta > 1e-1f )
283 {
284 delta = 1.0 / 30.0;
285 }
286 }
287 if ( mScene )
288 {
289 const_cast<Scene*> ( mScene )->Update ( delta );
290 }
291 qWorldEditorApp->GetRenderer()->BeginRender ( mWinId );
292 qWorldEditorApp->GetRenderer()->Render (
293 mWinId,
294 Matrix4x4{},
295 qWorldEditorApp->GetGridMesh(),
296 qWorldEditorApp->GetGridPipeline(),
297 &qWorldEditorApp->GetXGridMaterial(), nullptr, AeonGames::Topology::LINE_LIST, 0, 2,
298 mHorizontalSpacing + 1 );
299 qWorldEditorApp->GetRenderer()->Render (
300 mWinId,
301 Matrix4x4{},
302 qWorldEditorApp->GetGridMesh(),
303 qWorldEditorApp->GetGridPipeline(),
304 &qWorldEditorApp->GetYGridMaterial(), nullptr, AeonGames::Topology::LINE_LIST, 2, 2,
305 mVerticalSpacing + 1 );
309 if ( mScene && mScene->GetChildrenCount() )
310 {
311 const Frustum& frustum { qWorldEditorApp->GetRenderer()->GetFrustum ( mWinId ) };
312 mScene->LoopTraverseDFSPreOrder ( [this, &frustum] ( const Node & aNode )
313 {
314 if ( &aNode == mScene->GetCamera() )
315 {
316 /* This renders the scene current camera node's frustum */
317 Matrix4x4 projection_matrix{};
318 projection_matrix.Perspective ( mScene->GetFieldOfView(), mAspectRatio, mScene->GetNear(), mScene->GetFar() );
319 projection_matrix.Invert();
320 qWorldEditorApp->GetRenderer()->Render (
321 mWinId,
322 aNode.GetGlobalTransform() * projection_matrix,
323 qWorldEditorApp->GetAABBWireMesh(),
324 qWorldEditorApp->GetSolidColorPipeline(),
325 &qWorldEditorApp->GetSolidColorMaterial(),
326 nullptr,
328 );
329 }
330
331 AABB transformed_aabb = aNode.GetGlobalTransform() * aNode.GetAABB();
332 if ( frustum.Intersects ( transformed_aabb ) )
333 {
334 // Call Node specific rendering function.
335 aNode.Render ( *qWorldEditorApp->GetRenderer(), mWinId );
336 // Render Node AABBss
337 qWorldEditorApp->GetRenderer()->Render (
338 mWinId,
339 transformed_aabb.GetTransform(),
340 qWorldEditorApp->GetAABBWireMesh(),
341 qWorldEditorApp->GetSolidColorPipeline(),
342 &qWorldEditorApp->GetSolidColorMaterial(),
343 nullptr,
345 );
346 // Render Node Root
347 qWorldEditorApp->GetRenderer()->Render (
348 mWinId,
349 aNode.GetGlobalTransform(),
350 qWorldEditorApp->GetAABBWireMesh(),
351 qWorldEditorApp->GetSolidColorPipeline(),
352 &qWorldEditorApp->GetSolidColorMaterial(),
353 nullptr,
355 );
356 // Render AABB Center
357 qWorldEditorApp->GetRenderer()->Render (
358 mWinId,
359 Transform{Vector3{1, 1, 1},
360 Quaternion{1, 0, 0, 0},
361 Vector3{transformed_aabb.GetCenter() }},
362 qWorldEditorApp->GetAABBWireMesh(),
363 qWorldEditorApp->GetSolidColorPipeline(),
364 &qWorldEditorApp->GetSolidColorMaterial(),
365 nullptr,
367 );
368 }
369 } );
370 }
371 qWorldEditorApp->GetRenderer()->EndRender ( mWinId );
372 return true;
373 }
374 case QEvent::Close:
375 {
376 std::cout << LogLevel::Info << "EngineWindow received Close event" << std::endl;
377 mIsClosing = true;
378 return true;
379 }
380 default:
381 return QWindow::event ( aEvent );
382 }
383 }
384
385 void EngineWindow::updateViewMatrix()
386 {
387 Transform view_transform;
388 view_transform.SetTranslation ( Vector3 ( mCameraLocation.x(), mCameraLocation.y(), mCameraLocation.z() ) );
389 view_transform.SetRotation ( Quaternion ( mCameraRotation.scalar(), mCameraRotation.x(), mCameraRotation.y(), mCameraRotation.z() ) );
390 qWorldEditorApp->GetRenderer()->SetViewMatrix ( mWinId, view_transform.GetInverted().GetMatrix() );
391 }
392
393 void EngineWindow::keyPressEvent ( QKeyEvent * event )
394 {
395 switch ( event->key() )
396 {
397 case Qt::Key_W:
398 mCameraLocation += ( mCameraRotation.rotatedVector ( forward ) * mStep );
399 break;
400 case Qt::Key_S:
401 mCameraLocation -= ( mCameraRotation.rotatedVector ( forward ) * mStep );
402 break;
403 case Qt::Key_D:
404 mCameraLocation += ( mCameraRotation.rotatedVector ( right ) * mStep );
405 break;
406 case Qt::Key_A:
407 mCameraLocation -= ( mCameraRotation.rotatedVector ( right ) * mStep );
408 break;
409 }
410 updateViewMatrix();
411 event->accept();
412 }
413
414 void EngineWindow::keyReleaseEvent ( QKeyEvent * event )
415 {
416 event->accept();
417 }
418
419 void EngineWindow::mouseMoveEvent ( QMouseEvent * event )
420 {
421 if ( event->buttons() & Qt::LeftButton )
422 {
423 QPointF movement = event->globalPosition() - mLastCursorPosition;
424 mLastCursorPosition = event->globalPosition();
425 mCameraRotation = QQuaternion::fromAxisAndAngle ( 0, 0, 1, - ( movement.x() / 2.0f ) ) * mCameraRotation;
426 mCameraRotation = mCameraRotation * QQuaternion::fromAxisAndAngle ( 1, 0, 0, - ( movement.y() / 2.0f ) );
427 updateViewMatrix();
428 }
429 event->accept();
430 }
431
432 void EngineWindow::mousePressEvent ( QMouseEvent * event )
433 {
434 if ( event->button() & Qt::LeftButton )
435 {
436 mLastCursorPosition = event->globalPosition();
437 }
438 event->accept();
439 }
440
441 void EngineWindow::mouseReleaseEvent ( QMouseEvent * event )
442 {
443 event->accept();
444 }
445
446 void EngineWindow::wheelEvent ( QWheelEvent *event )
447 {
448 mCameraLocation += ( ( mCameraRotation.rotatedVector ( forward ) * mStep ) *
449 ( event->angleDelta().y() / std::abs ( event->angleDelta().y() ) ) );
450 updateViewMatrix();
451 event->accept();
452 }
453}
Header for the frustum class.
Defines log severity levels and stream output for the AeonGames engine.
EngineWindow(QWindow *parent=nullptr)
Construct the engine window.
void SetNear(float aNear)
Set the near clipping plane distance.
void start()
Start the rendering loop.
void SetFieldOfView(float aFieldOfView)
Set the camera field of view.
void stop()
Stop the rendering loop.
void setScene(const Scene *aScene)
Set the scene to render.
void SetFar(float aFar)
Set the far clipping plane distance.
4 by 4 matrix in colum mayor order.
Definition Matrix4x4.hpp:35
DLL void Perspective(float aFieldOfVision, float aAspect, float aNear, float aFar)
Set up a symmetric perspective projection matrix.
Scene class. Scene is the container for all elements in a game level, takes care of collision,...
Definition Scene.hpp:40
<- This is here just for the literals
Definition AABB.hpp:31
@ LINE_LIST
Pairs of vertices forming individual lines.
Definition Pipeline.hpp:42
@ Info
General informational messages.
Definition LogLevel.hpp:30