#include "../src/utils/Logger.h" #include "../src/core/UsdSceneRenderer.h" #include "../src/core/ViewportCamera.h" #include "../src/utils/GLExt.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace UsdLayerManager; using namespace pxr; static HWND g_hwnd = nullptr; static HDC g_hdc = nullptr; static HGLRC g_hglrc = nullptr; static int g_testsPassed = 0; static int g_testsFailed = 0; static int g_testsSkipped = 0; #define TEST_ASSERT(cond, msg) \ do { \ if (!(cond)) { \ std::cout << "[FAIL] " << (msg) << " (at " << __FILE__ << ":" << __LINE__ << ")" << std::endl; \ g_testsFailed++; \ return false; \ } else { \ std::cout << "[PASS] " << (msg) << std::endl; \ g_testsPassed++; \ } \ } while(0) #define TEST_SKIP(msg) \ do { \ std::cout << "[SKIP] " << (msg) << std::endl; \ g_testsSkipped++; \ return true; \ } while(0) static bool CreateGLContext() { WNDCLASSEXW wc = { sizeof(wc), CS_OWNDC, DefWindowProcW, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr, L"ViewportTest", nullptr }; RegisterClassExW(&wc); g_hwnd = CreateWindowW(wc.lpszClassName, L"ViewportTest", WS_OVERLAPPEDWINDOW, 100, 100, 800, 600, nullptr, nullptr, wc.hInstance, nullptr); if (!g_hwnd) { std::cerr << "[FAIL] CreateWindowW failed" << std::endl; return false; } g_hdc = GetDC(g_hwnd); PIXELFORMATDESCRIPTOR pfd = {}; pfd.nSize = sizeof(pfd); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 32; pfd.cDepthBits = 24; pfd.cStencilBits = 8; int pixelFormat = ChoosePixelFormat(g_hdc, &pfd); if (pixelFormat == 0) { std::cerr << "[FAIL] ChoosePixelFormat failed" << std::endl; return false; } if (!SetPixelFormat(g_hdc, pixelFormat, &pfd)) { std::cerr << "[FAIL] SetPixelFormat failed" << std::endl; return false; } g_hglrc = wglCreateContext(g_hdc); if (!g_hglrc) { std::cerr << "[FAIL] wglCreateContext failed" << std::endl; return false; } if (!wglMakeCurrent(g_hdc, g_hglrc)) { std::cerr << "[FAIL] wglMakeCurrent failed" << std::endl; return false; } return true; } static void DestroyGLContext() { if (g_hglrc) { wglMakeCurrent(nullptr, nullptr); wglDeleteContext(g_hglrc); g_hglrc = nullptr; } if (g_hdc) { ReleaseDC(g_hwnd, g_hdc); g_hdc = nullptr; } if (g_hwnd) { DestroyWindow(g_hwnd); UnregisterClassW(L"ViewportTest", GetModuleHandle(nullptr)); g_hwnd = nullptr; } } static UsdStageRefPtr CreateTestStageWithCube() { UsdStageRefPtr stage = UsdStage::CreateInMemory(); if (!stage) return nullptr; UsdGeomXform xform = UsdGeomXform::Define(stage, SdfPath("/TestXform")); UsdGeomCube cube = UsdGeomCube::Define(stage, SdfPath("/TestXform/TestCube")); cube.GetSizeAttr().Set(50.0); cube.GetDisplayColorAttr().Set(VtVec3fArray{{1.0f, 0.0f, 0.0f}}); cube.GetDisplayOpacityAttr().Set(VtFloatArray{1.0f}); UsdGeomSphere sphere = UsdGeomSphere::Define(stage, SdfPath("/TestSphere")); sphere.GetRadiusAttr().Set(25.0); sphere.GetDisplayColorAttr().Set(VtVec3fArray{{0.0f, 1.0f, 0.0f}}); sphere.GetDisplayOpacityAttr().Set(VtFloatArray{1.0f}); return stage; } static GfVec4d MulMatrixVec4(const GfMatrix4d& m, const GfVec4d& v) { GfVec4d result; for (int j = 0; j < 4; ++j) { result[j] = 0; for (int i = 0; i < 4; ++i) { result[j] += v[i] * m[i][j]; } } return result; } bool Test_StageCreation() { std::cout << "\n=== Test 1: Stage Creation ===" << std::endl; UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "In-memory stage created"); size_t primCount = 0; for ([[maybe_unused]] const auto& prim : stage->Traverse()) { ++primCount; } std::cout << " Prim count: " << primCount << std::endl; TEST_ASSERT(primCount >= 3, "Stage has at least 3 prims (xform, cube, sphere)"); UsdPrim cubePrim = stage->GetPrimAtPath(SdfPath("/TestXform/TestCube")); TEST_ASSERT(cubePrim.IsValid(), "Cube prim found at /TestXform/TestCube"); TEST_ASSERT(cubePrim.IsA(), "Prim is a UsdGeomCube"); UsdPrim spherePrim = stage->GetPrimAtPath(SdfPath("/TestSphere")); TEST_ASSERT(spherePrim.IsValid(), "Sphere prim found at /TestSphere"); return true; } bool Test_StageBounds() { std::cout << "\n=== Test 2: Stage Bounds Computation ===" << std::endl; UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Stage created for bounds test"); UsdSceneRenderer renderer; renderer.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); TEST_ASSERT(!bounds.IsEmpty(), "Stage bounds are not empty"); GfVec3d min = bounds.GetMin(); GfVec3d max = bounds.GetMax(); std::cout << " Bounds min: (" << min[0] << ", " << min[1] << ", " << min[2] << ")" << std::endl; std::cout << " Bounds max: (" << max[0] << ", " << max[1] << ", " << max[2] << ")" << std::endl; GfVec3d size = bounds.GetSize(); double maxSize = size.GetLength(); std::cout << " Bounds size length: " << maxSize << std::endl; TEST_ASSERT(maxSize > 0.0, "Bounds size is greater than zero"); return true; } bool Test_CameraSetup() { std::cout << "\n=== Test 3: Camera Setup ===" << std::endl; ViewportCamera camera; TEST_ASSERT(camera.GetMode() == ViewportCamera::CameraMode::Free, "Camera starts in Free mode"); GfVec3d eye = camera.GetEye(); GfVec3d focal = camera.GetFocalPoint(); std::cout << " Default eye: (" << eye[0] << ", " << eye[1] << ", " << eye[2] << ")" << std::endl; std::cout << " Default focal: (" << focal[0] << ", " << focal[1] << ", " << focal[2] << ")" << std::endl; std::cout << " Near/Far: " << camera.GetNearClip() << " / " << camera.GetFarClip() << std::endl; std::cout << " FOV: " << camera.GetFOV() << ", Aspect: " << camera.GetAspectRatio() << std::endl; UsdStageRefPtr stage = CreateTestStageWithCube(); camera.SetStage(stage); GfRange3d bounds = GfRange3d(GfVec3d(-25, -25, -25), GfVec3d(25, 25, 25)); camera.FrameBoundingBox(bounds); eye = camera.GetEye(); focal = camera.GetFocalPoint(); std::cout << " After FrameBB eye: (" << eye[0] << ", " << eye[1] << ", " << eye[2] << ")" << std::endl; std::cout << " After FrameBB focal: (" << focal[0] << ", " << focal[1] << ", " << focal[2] << ")" << std::endl; std::cout << " After FrameBB Near/Far: " << camera.GetNearClip() << " / " << camera.GetFarClip() << std::endl; TEST_ASSERT(camera.GetNearClip() > 0.0f, "Near clip is positive after framing"); TEST_ASSERT(camera.GetFarClip() > camera.GetNearClip(), "Far clip > near clip after framing"); GfMatrix4d viewMatrix = camera.GetViewMatrix(); GfMatrix4d projMatrix = camera.GetProjectionMatrix(); double detView = viewMatrix.GetDeterminant(); double detProj = projMatrix.GetDeterminant(); std::cout << " View matrix determinant: " << detView << std::endl; std::cout << " Proj matrix determinant: " << detProj << std::endl; TEST_ASSERT(std::abs(detView) > 0.001, "View matrix is non-degenerate"); TEST_ASSERT(std::abs(detProj) > 0.000001, "Projection matrix is non-degenerate"); return true; } bool Test_CameraAspectRatio() { std::cout << "\n=== Test 4: Camera Aspect Ratio Update ===" << std::endl; ViewportCamera camera; float defaultAspect = camera.GetAspectRatio(); std::cout << " Default aspect: " << defaultAspect << std::endl; camera.SetAspectRatio(800.0f / 600.0f); float newAspect = camera.GetAspectRatio(); std::cout << " After update: " << newAspect << std::endl; TEST_ASSERT(std::abs(newAspect - (800.0f / 600.0f)) < 0.01f, "Aspect ratio updated correctly"); GfMatrix4d proj = camera.GetProjectionMatrix(); double det = proj.GetDeterminant(); std::cout << " Projection determinant with new aspect: " << det << std::endl; TEST_ASSERT(std::abs(det) > 0.000001, "Projection matrix valid after aspect update"); return true; } bool Test_ViewProjectionSanity() { std::cout << "\n=== Test 5: View/Projection Matrix Sanity ===" << std::endl; ViewportCamera camera; camera.SetAspectRatio(800.0f / 600.0f); GfRange3d bounds(GfVec3d(-25, -25, -25), GfVec3d(25, 25, 25)); camera.FrameBoundingBox(bounds); GfMatrix4d view = camera.GetViewMatrix(); GfMatrix4d proj = camera.GetProjectionMatrix(); std::cout << " View matrix:" << std::endl; for (int r = 0; r < 4; ++r) { std::cout << " [" << view[r][0] << ", " << view[r][1] << ", " << view[r][2] << ", " << view[r][3] << "]" << std::endl; } std::cout << " Projection matrix:" << std::endl; for (int r = 0; r < 4; ++r) { std::cout << " [" << proj[r][0] << ", " << proj[r][1] << ", " << proj[r][2] << ", " << proj[r][3] << "]" << std::endl; } GfVec3d testPoint(0, 0, 0); GfVec4d viewSpacePoint = MulMatrixVec4(view, GfVec4d(testPoint[0], testPoint[1], testPoint[2], 1.0)); std::cout << " Origin in view space: (" << viewSpacePoint[0] << ", " << viewSpacePoint[1] << ", " << viewSpacePoint[2] << ", " << viewSpacePoint[3] << ")" << std::endl; TEST_ASSERT(viewSpacePoint[3] != 0.0, "Origin has valid w component in view space"); GfVec4d clipSpacePoint = MulMatrixVec4(proj, viewSpacePoint); std::cout << " Origin in clip space: (" << clipSpacePoint[0] << ", " << clipSpacePoint[1] << ", " << clipSpacePoint[2] << ", " << clipSpacePoint[3] << ")" << std::endl; if (clipSpacePoint[3] != 0.0) { double ndcX = clipSpacePoint[0] / clipSpacePoint[3]; double ndcY = clipSpacePoint[1] / clipSpacePoint[3]; double ndcZ = clipSpacePoint[2] / clipSpacePoint[3]; std::cout << " Origin in NDC: (" << ndcX << ", " << ndcY << ", " << ndcZ << ")" << std::endl; bool inFrustum = (ndcX >= -1.0 && ndcX <= 1.0 && ndcY >= -1.0 && ndcY <= 1.0 && ndcZ >= -1.0 && ndcZ <= 1.0); if (inFrustum) { std::cout << " [PASS] Scene origin is inside the view frustum" << std::endl; g_testsPassed++; } else { std::cout << " [FAIL] Scene origin is OUTSIDE the view frustum - this explains nothing visible!" << std::endl; std::cout << " This likely means the camera framing calculation is wrong." << std::endl; g_testsFailed++; } } else { std::cout << " [FAIL] Clip space w=0 - projection is broken" << std::endl; g_testsFailed++; } return true; } bool Test_RendererInitAndRender() { std::cout << "\n=== Test 6: Renderer Init and Render ===" << std::endl; if (!GL::InitExtensions()) { TEST_SKIP("OpenGL extensions not available"); } UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Test stage created"); UsdSceneRenderer renderer; renderer.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); TEST_ASSERT(!bounds.IsEmpty(), "Stage bounds not empty"); ViewportCamera camera; camera.SetStage(stage); camera.FrameBoundingBox(bounds); camera.SetAspectRatio(800.0f / 600.0f); renderer.SetCameraState(camera.GetViewMatrix(), camera.GetProjectionMatrix()); int width = 800, height = 600; renderer.Render(width, height); uint32_t texID = renderer.GetColorTextureID(); std::cout << " Color texture ID: " << texID << std::endl; TEST_ASSERT(texID != 0, "Renderer produced a valid texture ID"); GfVec3f bgColor = renderer.GetBackgroundColor(); std::cout << " Background color: (" << bgColor[0] << ", " << bgColor[1] << ", " << bgColor[2] << ")" << std::endl; return true; } bool Test_RendererDrawTargetValid() { std::cout << "\n=== Test 7: Renderer DrawTarget Validity ===" << std::endl; if (!GL::InitExtensions()) { TEST_SKIP("OpenGL extensions not available"); } UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Test stage created"); UsdSceneRenderer renderer; renderer.SetStage(stage); ViewportCamera camera; camera.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); if (!bounds.IsEmpty()) { camera.FrameBoundingBox(bounds); } camera.SetAspectRatio(400.0f / 300.0f); renderer.SetCameraState(camera.GetViewMatrix(), camera.GetProjectionMatrix()); renderer.Render(400, 300); auto drawTarget = renderer.GetDrawTargetForTest(); TEST_ASSERT(drawTarget != nullptr, "DrawTarget is not null after render"); if (drawTarget) { GLuint fboId = drawTarget->GetFramebufferId(); auto colorAttach = drawTarget->GetAttachment("color"); auto depthAttach = drawTarget->GetAttachment("depth"); std::cout << " FBO ID: " << fboId << std::endl; std::cout << " Color attachment: " << (colorAttach ? "present" : "MISSING") << std::endl; std::cout << " Depth attachment: " << (depthAttach ? "present" : "MISSING") << std::endl; TEST_ASSERT(fboId != 0, "FBO ID is non-zero"); TEST_ASSERT(colorAttach != nullptr, "Color attachment exists"); TEST_ASSERT(depthAttach != nullptr, "Depth attachment exists"); if (colorAttach) { GLuint texId = colorAttach->GetGlTextureName(); std::cout << " Color texture ID: " << texId << std::endl; TEST_ASSERT(texId != 0, "Color attachment has valid GL texture"); GfVec2i texSize = drawTarget->GetSize(); std::cout << " DrawTarget size: " << texSize[0] << "x" << texSize[1] << std::endl; TEST_ASSERT(texSize[0] > 0 && texSize[1] > 0, "DrawTarget has non-zero size"); } } return true; } bool Test_MultipleRenderFrames() { std::cout << "\n=== Test 8: Multiple Render Frames (Stability) ===" << std::endl; if (!GL::InitExtensions()) { TEST_SKIP("OpenGL extensions not available"); } UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Stage created for stability test"); UsdSceneRenderer renderer; renderer.SetStage(stage); ViewportCamera camera; camera.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); if (!bounds.IsEmpty()) { camera.FrameBoundingBox(bounds); } camera.SetAspectRatio(400.0f / 300.0f); renderer.SetCameraState(camera.GetViewMatrix(), camera.GetProjectionMatrix()); for (int i = 0; i < 5; ++i) { renderer.Render(400, 300); uint32_t texID = renderer.GetColorTextureID(); if (texID == 0) { std::cout << "[FAIL] Render frame " << i << " produced invalid texture" << std::endl; g_testsFailed++; return false; } } std::cout << " [PASS] 5 consecutive renders completed successfully" << std::endl; g_testsPassed++; return true; } bool Test_CameraOrbitAndRender() { std::cout << "\n=== Test 9: Camera Orbit + Render ===" << std::endl; if (!GL::InitExtensions()) { TEST_SKIP("OpenGL extensions not available"); } UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Stage created for orbit test"); UsdSceneRenderer renderer; renderer.SetStage(stage); ViewportCamera camera; camera.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); if (!bounds.IsEmpty()) { camera.FrameBoundingBox(bounds); } camera.SetAspectRatio(400.0f / 300.0f); for (int angle = 0; angle < 4; ++angle) { camera.Orbit(45.0f, 0.0f); renderer.SetCameraState(camera.GetViewMatrix(), camera.GetProjectionMatrix()); renderer.Render(400, 300); uint32_t texID = renderer.GetColorTextureID(); if (texID == 0) { std::cout << "[FAIL] Render after orbit " << angle << " failed" << std::endl; g_testsFailed++; return false; } } std::cout << " [PASS] Renders after camera orbit completed" << std::endl; g_testsPassed++; return true; } bool Test_EmptyStageRender() { std::cout << "\n=== Test 10: Empty Stage Render ===" << std::endl; if (!GL::InitExtensions()) { TEST_SKIP("OpenGL extensions not available"); } UsdStageRefPtr stage = UsdStage::CreateInMemory(); TEST_ASSERT(stage != nullptr, "Empty in-memory stage created"); UsdSceneRenderer renderer; renderer.SetBackgroundColor(GfVec3f(0.1f, 0.1f, 0.15f)); renderer.SetStage(stage); ViewportCamera camera; camera.SetStage(stage); camera.SetAspectRatio(400.0f / 300.0f); renderer.SetCameraState(camera.GetViewMatrix(), camera.GetProjectionMatrix()); renderer.Render(400, 300); uint32_t texID = renderer.GetColorTextureID(); TEST_ASSERT(texID != 0, "Empty stage still produces a valid texture"); auto drawTarget = renderer.GetDrawTargetForTest(); if (drawTarget) { std::cout << " Empty stage FBO: " << drawTarget->GetFramebufferId() << std::endl; std::cout << " [PASS] Empty stage render produces valid FBO" << std::endl; g_testsPassed++; } return true; } bool Test_CameraFrameSceneIntegration() { std::cout << "\n=== Test 11: Camera + FrameScene Integration ===" << std::endl; UsdStageRefPtr stage = CreateTestStageWithCube(); TEST_ASSERT(stage != nullptr, "Stage created for integration test"); ViewportCamera camera; camera.SetStage(stage); UsdSceneRenderer renderer; renderer.SetStage(stage); GfRange3d bounds = renderer.ComputeStageBounds(); TEST_ASSERT(!bounds.IsEmpty(), "Stage has bounds for framing"); camera.FrameBoundingBox(bounds); float nearClip = camera.GetNearClip(); float farClip = camera.GetFarClip(); float distance = 0.0f; GfVec3d eye = camera.GetEye(); GfVec3d focal = camera.GetFocalPoint(); GfVec3d diff = eye - focal; distance = static_cast(diff.GetLength()); std::cout << " Camera distance after FrameBoundingBox: " << distance << std::endl; std::cout << " Near clip: " << nearClip << ", Far clip: " << farClip << std::endl; std::cout << " Eye: (" << eye[0] << ", " << eye[1] << ", " << eye[2] << ")" << std::endl; TEST_ASSERT(distance > 0.0f, "Camera has non-zero distance from focal point"); TEST_ASSERT(nearClip > 0.0f, "Near clip is positive"); TEST_ASSERT(farClip > nearClip, "Far clip is beyond near clip"); TEST_ASSERT(nearClip < distance, "Near clip is less than camera distance (scene is in front of camera)"); GfMatrix4d view = camera.GetViewMatrix(); GfMatrix4d proj = camera.GetProjectionMatrix(); GfVec4d originView = MulMatrixVec4(view, GfVec4d(0, 0, 0, 1)); if (originView[3] != 0.0) { GfVec4d originClip = MulMatrixVec4(proj, originView); if (originClip[3] != 0.0) { double ndcZ = originClip[2] / originClip[3]; std::cout << " Scene origin NDC Z: " << ndcZ << std::endl; bool zInRange = (ndcZ >= -1.0 && ndcZ <= 1.0); TEST_ASSERT(zInRange, "Scene origin is within NDC Z range [-1,1] - visible in depth"); } } return true; } int main() { std::cout << "============================================" << std::endl; std::cout << " Viewport Display Logic Test Suite" << std::endl; std::cout << "============================================" << std::endl; char exePath[MAX_PATH]; GetModuleFileNameA(nullptr, exePath, MAX_PATH); std::string exeDir(exePath); size_t lastSlash = exeDir.find_last_of("\\/"); if (lastSlash != std::string::npos) { exeDir = exeDir.substr(0, lastSlash); } std::string pluginPath = exeDir + "\\usd"; SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str()); Logger::Instance().SetLogLevel(LogLevel::Warning); if (!CreateGLContext()) { std::cerr << "FATAL: Cannot create OpenGL context. Tests require a valid GL context." << std::endl; return 1; } GL::InitExtensions(); Test_StageCreation(); Test_StageBounds(); Test_CameraSetup(); Test_CameraAspectRatio(); Test_ViewProjectionSanity(); Test_CameraFrameSceneIntegration(); Test_RendererInitAndRender(); Test_RendererDrawTargetValid(); Test_MultipleRenderFrames(); Test_CameraOrbitAndRender(); Test_EmptyStageRender(); std::cout << "\n============================================" << std::endl; std::cout << " Results: " << g_testsPassed << " passed, " << g_testsFailed << " failed, " << g_testsSkipped << " skipped" << std::endl; std::cout << "============================================" << std::endl; if (g_testsFailed > 0) { std::cout << "\n DIAGNOSIS: If Test 5 (View/Projection Sanity) shows the origin" << std::endl; std::cout << " is outside the frustum, the camera framing logic in" << std::endl; std::cout << " ViewportCamera::FrameBoundingBox() is broken. Check:" << std::endl; std::cout << " 1. The view matrix construction in ComputeFreeViewMatrix()" << std::endl; std::cout << " 2. The projection matrix in ComputeFreeProjectionMatrix()" << std::endl; std::cout << " 3. Whether GfMatrix4d row/column conventions match USD's expectations" << std::endl; } DestroyGLContext(); return g_testsFailed > 0 ? 1 : 0; }