UsdLayerManager/tests/viewport_display_test.cpp

608 lines
22 KiB
C++

#include "../src/utils/Logger.h"
#include "../src/core/UsdSceneRenderer.h"
#include "../src/core/ViewportCamera.h"
#include "../src/utils/GLExt.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/cube.h>
#include <pxr/usd/usdGeom/sphere.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <Windows.h>
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
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<UsdGeomCube>(), "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<float>(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;
}