3D viewport, milestone 1
This commit is contained in:
parent
38156dd570
commit
e277663ba0
27 changed files with 7389 additions and 8 deletions
BIN
tests/unit/math3d_test
Executable file
BIN
tests/unit/math3d_test
Executable file
Binary file not shown.
141
tests/unit/math3d_test.cpp
Normal file
141
tests/unit/math3d_test.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// math3d_test.cpp - Quick verification of Math3D library
|
||||
// Compile: g++ -std=c++17 -I../../src math3d_test.cpp -o math3d_test && ./math3d_test
|
||||
|
||||
#include "3d/Math3D.h"
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
using namespace mcrf;
|
||||
|
||||
bool approx(float a, float b, float eps = 0.0001f) {
|
||||
return std::abs(a - b) < eps;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0, failed = 0;
|
||||
|
||||
// vec3 tests
|
||||
{
|
||||
vec3 a(1, 2, 3);
|
||||
vec3 b(4, 5, 6);
|
||||
|
||||
vec3 sum = a + b;
|
||||
if (approx(sum.x, 5) && approx(sum.y, 7) && approx(sum.z, 9)) {
|
||||
std::cout << "[PASS] vec3 addition\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] vec3 addition\n"; failed++;
|
||||
}
|
||||
|
||||
float dot = a.dot(b);
|
||||
if (approx(dot, 32)) { // 1*4 + 2*5 + 3*6 = 32
|
||||
std::cout << "[PASS] vec3 dot product\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] vec3 dot product: " << dot << "\n"; failed++;
|
||||
}
|
||||
|
||||
vec3 c(1, 0, 0);
|
||||
vec3 d(0, 1, 0);
|
||||
vec3 cross = c.cross(d);
|
||||
if (approx(cross.x, 0) && approx(cross.y, 0) && approx(cross.z, 1)) {
|
||||
std::cout << "[PASS] vec3 cross product\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] vec3 cross product\n"; failed++;
|
||||
}
|
||||
|
||||
vec3 n = vec3(3, 4, 0).normalized();
|
||||
if (approx(n.length(), 1.0f)) {
|
||||
std::cout << "[PASS] vec3 normalize\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] vec3 normalize\n"; failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// mat4 tests
|
||||
{
|
||||
mat4 id = mat4::identity();
|
||||
vec3 p(1, 2, 3);
|
||||
vec3 transformed = id.transformPoint(p);
|
||||
if (approx(transformed.x, 1) && approx(transformed.y, 2) && approx(transformed.z, 3)) {
|
||||
std::cout << "[PASS] mat4 identity transform\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 identity transform\n"; failed++;
|
||||
}
|
||||
|
||||
mat4 trans = mat4::translate(10, 20, 30);
|
||||
vec3 moved = trans.transformPoint(p);
|
||||
if (approx(moved.x, 11) && approx(moved.y, 22) && approx(moved.z, 33)) {
|
||||
std::cout << "[PASS] mat4 translation\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 translation\n"; failed++;
|
||||
}
|
||||
|
||||
mat4 scl = mat4::scale(2, 3, 4);
|
||||
vec3 scaled = scl.transformPoint(p);
|
||||
if (approx(scaled.x, 2) && approx(scaled.y, 6) && approx(scaled.z, 12)) {
|
||||
std::cout << "[PASS] mat4 scale\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 scale\n"; failed++;
|
||||
}
|
||||
|
||||
// Test rotation: 90 degrees around Y should swap X and Z
|
||||
mat4 rotY = mat4::rotateY(HALF_PI);
|
||||
vec3 rotated = rotY.transformPoint(vec3(1, 0, 0));
|
||||
if (approx(rotated.x, 0) && approx(rotated.z, -1)) {
|
||||
std::cout << "[PASS] mat4 rotateY\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 rotateY: " << rotated.x << "," << rotated.y << "," << rotated.z << "\n"; failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Projection matrix test
|
||||
{
|
||||
mat4 proj = mat4::perspective(radians(90.0f), 1.0f, 0.1f, 100.0f);
|
||||
// A point at z=-1 (in front of camera) should project to valid NDC
|
||||
vec4 p(0, 0, -1, 1);
|
||||
vec4 clip = proj * p;
|
||||
vec3 ndc = clip.perspectiveDivide();
|
||||
if (ndc.z > -1.0f && ndc.z < 1.0f) {
|
||||
std::cout << "[PASS] mat4 perspective\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 perspective\n"; failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// LookAt matrix test
|
||||
{
|
||||
mat4 view = mat4::lookAt(vec3(0, 0, 5), vec3(0, 0, 0), vec3(0, 1, 0));
|
||||
vec3 origin = view.transformPoint(vec3(0, 0, 0));
|
||||
// Origin should be at z=-5 in view space (5 units in front)
|
||||
if (approx(origin.x, 0) && approx(origin.y, 0) && approx(origin.z, -5)) {
|
||||
std::cout << "[PASS] mat4 lookAt\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] mat4 lookAt: " << origin.x << "," << origin.y << "," << origin.z << "\n"; failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Quaternion tests
|
||||
{
|
||||
quat q = quat::fromAxisAngle(vec3(0, 1, 0), HALF_PI);
|
||||
vec3 rotated = q.rotate(vec3(1, 0, 0));
|
||||
if (approx(rotated.x, 0) && approx(rotated.z, -1)) {
|
||||
std::cout << "[PASS] quat rotation\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] quat rotation: " << rotated.x << "," << rotated.y << "," << rotated.z << "\n"; failed++;
|
||||
}
|
||||
|
||||
quat a = quat::fromAxisAngle(vec3(0, 1, 0), 0);
|
||||
quat b = quat::fromAxisAngle(vec3(0, 1, 0), PI);
|
||||
quat mid = quat::slerp(a, b, 0.5f);
|
||||
vec3 half = mid.rotate(vec3(1, 0, 0));
|
||||
// At t=0.5 between 0 and PI rotation, we should get 90 degrees
|
||||
// Result should be perpendicular to input (x near 0, |z| near 1)
|
||||
if (approx(half.x, 0, 0.01f) && approx(std::abs(half.z), 1, 0.01f)) {
|
||||
std::cout << "[PASS] quat slerp\n"; passed++;
|
||||
} else {
|
||||
std::cout << "[FAIL] quat slerp: " << half.x << "," << half.y << "," << half.z << "\n"; failed++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\n=== Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
212
tests/unit/viewport3d_test.py
Normal file
212
tests/unit/viewport3d_test.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# viewport3d_test.py - Unit test for Viewport3D 3D rendering viewport
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_viewport3d_creation():
|
||||
"""Test basic Viewport3D creation and default properties"""
|
||||
vp = mcrfpy.Viewport3D()
|
||||
|
||||
# Default position
|
||||
assert vp.x == 0.0, f"Expected x=0, got {vp.x}"
|
||||
assert vp.y == 0.0, f"Expected y=0, got {vp.y}"
|
||||
|
||||
# Default size (320x240 - PS1 resolution)
|
||||
assert vp.w == 320.0, f"Expected w=320, got {vp.w}"
|
||||
assert vp.h == 240.0, f"Expected h=240, got {vp.h}"
|
||||
|
||||
# Default render resolution
|
||||
assert vp.render_resolution == (320, 240), f"Expected (320, 240), got {vp.render_resolution}"
|
||||
|
||||
# Default camera position
|
||||
assert vp.camera_pos == (0.0, 0.0, 5.0), f"Expected (0, 0, 5), got {vp.camera_pos}"
|
||||
|
||||
# Default camera target
|
||||
assert vp.camera_target == (0.0, 0.0, 0.0), f"Expected (0, 0, 0), got {vp.camera_target}"
|
||||
|
||||
# Default FOV
|
||||
assert vp.fov == 60.0, f"Expected fov=60, got {vp.fov}"
|
||||
|
||||
# Default PS1 effect flags
|
||||
assert vp.enable_vertex_snap == True, f"Expected vertex_snap=True, got {vp.enable_vertex_snap}"
|
||||
assert vp.enable_affine == True, f"Expected affine=True, got {vp.enable_affine}"
|
||||
assert vp.enable_dither == True, f"Expected dither=True, got {vp.enable_dither}"
|
||||
assert vp.enable_fog == True, f"Expected fog=True, got {vp.enable_fog}"
|
||||
|
||||
# Default fog range
|
||||
assert vp.fog_near == 10.0, f"Expected fog_near=10, got {vp.fog_near}"
|
||||
assert vp.fog_far == 100.0, f"Expected fog_far=100, got {vp.fog_far}"
|
||||
|
||||
print("[PASS] test_viewport3d_creation")
|
||||
|
||||
def test_viewport3d_with_kwargs():
|
||||
"""Test Viewport3D creation with keyword arguments"""
|
||||
vp = mcrfpy.Viewport3D(
|
||||
pos=(100, 200),
|
||||
size=(640, 480),
|
||||
render_resolution=(160, 120),
|
||||
fov=90.0,
|
||||
camera_pos=(10.0, 5.0, 10.0),
|
||||
camera_target=(0.0, 2.0, 0.0),
|
||||
enable_vertex_snap=False,
|
||||
enable_affine=False,
|
||||
enable_dither=False,
|
||||
enable_fog=False,
|
||||
fog_near=5.0,
|
||||
fog_far=50.0
|
||||
)
|
||||
|
||||
assert vp.x == 100.0, f"Expected x=100, got {vp.x}"
|
||||
assert vp.y == 200.0, f"Expected y=200, got {vp.y}"
|
||||
assert vp.w == 640.0, f"Expected w=640, got {vp.w}"
|
||||
assert vp.h == 480.0, f"Expected h=480, got {vp.h}"
|
||||
assert vp.render_resolution == (160, 120), f"Expected (160, 120), got {vp.render_resolution}"
|
||||
assert vp.fov == 90.0, f"Expected fov=90, got {vp.fov}"
|
||||
assert vp.camera_pos == (10.0, 5.0, 10.0), f"Expected (10, 5, 10), got {vp.camera_pos}"
|
||||
assert vp.camera_target == (0.0, 2.0, 0.0), f"Expected (0, 2, 0), got {vp.camera_target}"
|
||||
assert vp.enable_vertex_snap == False, f"Expected vertex_snap=False, got {vp.enable_vertex_snap}"
|
||||
assert vp.enable_affine == False, f"Expected affine=False, got {vp.enable_affine}"
|
||||
assert vp.enable_dither == False, f"Expected dither=False, got {vp.enable_dither}"
|
||||
assert vp.enable_fog == False, f"Expected fog=False, got {vp.enable_fog}"
|
||||
assert vp.fog_near == 5.0, f"Expected fog_near=5, got {vp.fog_near}"
|
||||
assert vp.fog_far == 50.0, f"Expected fog_far=50, got {vp.fog_far}"
|
||||
|
||||
print("[PASS] test_viewport3d_with_kwargs")
|
||||
|
||||
def test_viewport3d_property_modification():
|
||||
"""Test modifying Viewport3D properties after creation"""
|
||||
vp = mcrfpy.Viewport3D()
|
||||
|
||||
# Modify position
|
||||
vp.x = 50
|
||||
vp.y = 75
|
||||
assert vp.x == 50.0, f"Expected x=50, got {vp.x}"
|
||||
assert vp.y == 75.0, f"Expected y=75, got {vp.y}"
|
||||
|
||||
# Modify size
|
||||
vp.w = 800
|
||||
vp.h = 600
|
||||
assert vp.w == 800.0, f"Expected w=800, got {vp.w}"
|
||||
assert vp.h == 600.0, f"Expected h=600, got {vp.h}"
|
||||
|
||||
# Modify render resolution
|
||||
vp.render_resolution = (256, 192)
|
||||
assert vp.render_resolution == (256, 192), f"Expected (256, 192), got {vp.render_resolution}"
|
||||
|
||||
# Modify camera
|
||||
vp.camera_pos = (0.0, 10.0, 20.0)
|
||||
vp.camera_target = (5.0, 0.0, 5.0)
|
||||
vp.fov = 45.0
|
||||
assert vp.camera_pos == (0.0, 10.0, 20.0), f"Expected (0, 10, 20), got {vp.camera_pos}"
|
||||
assert vp.camera_target == (5.0, 0.0, 5.0), f"Expected (5, 0, 5), got {vp.camera_target}"
|
||||
assert vp.fov == 45.0, f"Expected fov=45, got {vp.fov}"
|
||||
|
||||
# Modify PS1 effects
|
||||
vp.enable_vertex_snap = False
|
||||
vp.enable_affine = False
|
||||
vp.enable_dither = True
|
||||
vp.enable_fog = True
|
||||
assert vp.enable_vertex_snap == False
|
||||
assert vp.enable_affine == False
|
||||
assert vp.enable_dither == True
|
||||
assert vp.enable_fog == True
|
||||
|
||||
# Modify fog range
|
||||
vp.fog_near = 1.0
|
||||
vp.fog_far = 200.0
|
||||
assert vp.fog_near == 1.0, f"Expected fog_near=1, got {vp.fog_near}"
|
||||
assert vp.fog_far == 200.0, f"Expected fog_far=200, got {vp.fog_far}"
|
||||
|
||||
print("[PASS] test_viewport3d_property_modification")
|
||||
|
||||
def test_viewport3d_scene_integration():
|
||||
"""Test adding Viewport3D to a scene"""
|
||||
scene = mcrfpy.Scene("viewport3d_test_scene")
|
||||
vp = mcrfpy.Viewport3D(pos=(10, 10), size=(400, 300))
|
||||
|
||||
# Add to scene
|
||||
scene.children.append(vp)
|
||||
|
||||
# Verify it was added
|
||||
assert len(scene.children) == 1, f"Expected 1 child, got {len(scene.children)}"
|
||||
|
||||
# Retrieve and verify type
|
||||
child = scene.children[0]
|
||||
assert type(child).__name__ == "Viewport3D", f"Expected Viewport3D, got {type(child).__name__}"
|
||||
|
||||
# Verify properties match
|
||||
assert child.x == 10.0
|
||||
assert child.y == 10.0
|
||||
assert child.w == 400.0
|
||||
assert child.h == 300.0
|
||||
|
||||
print("[PASS] test_viewport3d_scene_integration")
|
||||
|
||||
def test_viewport3d_visibility():
|
||||
"""Test visibility and opacity properties"""
|
||||
vp = mcrfpy.Viewport3D()
|
||||
|
||||
# Default visibility
|
||||
assert vp.visible == True, f"Expected visible=True, got {vp.visible}"
|
||||
assert vp.opacity == 1.0, f"Expected opacity=1.0, got {vp.opacity}"
|
||||
|
||||
# Modify visibility
|
||||
vp.visible = False
|
||||
assert vp.visible == False, f"Expected visible=False, got {vp.visible}"
|
||||
|
||||
# Modify opacity
|
||||
vp.opacity = 0.5
|
||||
assert vp.opacity == 0.5, f"Expected opacity=0.5, got {vp.opacity}"
|
||||
|
||||
# Opacity clamping
|
||||
vp.opacity = 2.0 # Should clamp to 1.0
|
||||
assert vp.opacity == 1.0, f"Expected opacity=1.0 after clamping, got {vp.opacity}"
|
||||
|
||||
vp.opacity = -0.5 # Should clamp to 0.0
|
||||
assert vp.opacity == 0.0, f"Expected opacity=0.0 after clamping, got {vp.opacity}"
|
||||
|
||||
print("[PASS] test_viewport3d_visibility")
|
||||
|
||||
def test_viewport3d_repr():
|
||||
"""Test Viewport3D string representation"""
|
||||
vp = mcrfpy.Viewport3D(pos=(100, 200), size=(640, 480), render_resolution=(320, 240))
|
||||
repr_str = repr(vp)
|
||||
|
||||
# Check that repr contains expected information
|
||||
assert "Viewport3D" in repr_str, f"Expected 'Viewport3D' in repr, got {repr_str}"
|
||||
assert "100" in repr_str, f"Expected x position in repr, got {repr_str}"
|
||||
assert "200" in repr_str, f"Expected y position in repr, got {repr_str}"
|
||||
|
||||
print("[PASS] test_viewport3d_repr")
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all Viewport3D tests"""
|
||||
tests = [
|
||||
test_viewport3d_creation,
|
||||
test_viewport3d_with_kwargs,
|
||||
test_viewport3d_property_modification,
|
||||
test_viewport3d_scene_integration,
|
||||
test_viewport3d_visibility,
|
||||
test_viewport3d_repr,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"[FAIL] {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {test.__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n=== Results: {passed} passed, {failed} failed ===")
|
||||
return failed == 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue