Release NaraMotion v0.26.0: Structure of Arrays (SoA), Path Arena Allocator, Uniform Grid Indexing, Dual-Buffer Rendering & Centroid Spring Pop-in
b516e00 verified Download tests/test_perspective_camera.py from ArifiIslam/NaraMotion: direct link, hf CLI and curl.
- Browser
- Download file 4.91 kB
-
https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/tests/test_perspective_camera.py
- Command line
-
hf download hf://ArifiIslam/NaraMotion/tests/test_perspective_camera.py
-
curl -L -o test_perspective_camera.py https://huggingface.co/ArifiIslam/NaraMotion/resolve/main/tests/test_perspective_camera.py
4.91 kB
| import os | |
| import math | |
| import pytest | |
| import numpy as np | |
| import naramotion | |
| from naramotion import Image, Context, Format, Path, VideoWriter | |
| from naramotion.motion import PerspectiveCamera, render_globe, Timeline, Easing | |
| def test_perspective_project_point(): | |
| cam = PerspectiveCamera(width=1920, height=1080, fov=60.0, pitch=0.0, distance=1000.0) | |
| # Point at origin (0, 0) should project directly to viewport center (960, 540) | |
| sx, sy, ds, ok = cam.project_point(0.0, 0.0) | |
| assert ok is True | |
| assert math.isclose(sx, 960.0, abs_tol=1e-2) | |
| assert math.isclose(sy, 540.0, abs_tol=1e-2) | |
| assert math.isclose(ds, 1.0, abs_tol=1e-2) | |
| def test_perspective_pitch_and_horizon_clipping(): | |
| # Camera tilted 60 degrees forward | |
| cam = PerspectiveCamera(width=1920, height=1080, fov=60.0, pitch=60.0, distance=500.0) | |
| # Point directly in front | |
| sx1, sy1, ds1, ok1 = cam.project_point(0.0, 200.0) | |
| assert ok1 is True | |
| assert sy1 < 540.0 # Appears higher towards horizon | |
| # Point far behind camera should be clipped by near-plane | |
| sx2, sy2, ds2, ok2 = cam.project_point(0.0, -5000.0) | |
| assert ok2 is False or ds2 <= 0.0 | |
| def test_perspective_transform_points_numpy(): | |
| cam = PerspectiveCamera(width=1920, height=1080, fov=60.0, pitch=45.0, distance=800.0) | |
| # (1000, 2) grid points | |
| grid_x, grid_y = np.meshgrid(np.linspace(-500, 500, 50), np.linspace(-500, 500, 20)) | |
| pts = np.column_stack([grid_x.ravel(), grid_y.ravel()]) | |
| proj_pts, depth_scales, valid_mask = cam.transform_points(pts) | |
| assert proj_pts.shape == (1000, 2) | |
| assert depth_scales.shape == (1000,) | |
| assert valid_mask.shape == (1000,) | |
| assert np.any(valid_mask) | |
| def test_perspective_transform_lines(): | |
| cam = PerspectiveCamera(width=1920, height=1080, fov=60.0, pitch=30.0, distance=600.0) | |
| lines = np.array([ | |
| [-100.0, -100.0, 100.0, -100.0], | |
| [100.0, -100.0, 100.0, 100.0], | |
| [100.0, 100.0, -100.0, 100.0], | |
| [-100.0, 100.0, -100.0, -100.0], | |
| ], dtype=np.float64) | |
| proj_lines, depth_scales, valid_mask = cam.transform_lines(lines) | |
| assert proj_lines.shape == (4, 4) | |
| assert len(depth_scales) == 4 | |
| assert np.all(valid_mask) | |
| def test_perspective_transform_path(): | |
| cam = PerspectiveCamera(width=1920, height=1080, fov=60.0, pitch=45.0, distance=500.0) | |
| path = Path() | |
| path.add_circle(0.0, 0.0, 200.0) | |
| proj_path, avg_ds = cam.transform_path(path) | |
| assert not proj_path.is_empty() | |
| assert proj_path.size >= 10 | |
| assert avg_ds > 0.0 | |
| def test_perspective_render_globe(tmp_path): | |
| # 1. Create dummy world texture image (e.g. 512x256) | |
| tex = Image(512, 256, Format.PRGB32) | |
| with Context(tex) as ctx: | |
| ctx.fill_all("#1E3A8A") # Ocean blue | |
| ctx.fill_circle(150, 100, 60, style="#10B981") # Land green | |
| ctx.fill_circle(350, 120, 70, style="#10B981") | |
| # 2. Render 3D Globe onto canvas | |
| canvas = Image(1000, 1000, Format.PRGB32) | |
| with Context(canvas) as ctx: | |
| ctx.fill_all("#030712") # Space black | |
| ok = render_globe( | |
| dst_image=canvas, | |
| texture_image=tex, | |
| cx=500.0, cy=500.0, radius=350.0, | |
| rot_lon=45.0, rot_lat=15.0, | |
| glow_color="#38BDF8FF" | |
| ) | |
| assert ok is True | |
| out_file = str(tmp_path / "globe_test.png") | |
| canvas.save(out_file) | |
| assert os.path.exists(out_file) | |
| assert os.path.getsize(out_file) > 1000 | |
| def test_perspective_drone_flythrough_timeline(tmp_path): | |
| video_out = str(tmp_path / "drone_perspective_flythrough.mp4") | |
| cam = PerspectiveCamera(width=640, height=360, fov=60.0) | |
| timeline = Timeline(duration=0.5, fps=30) | |
| # Animate pitch from 0 deg (flat) to 70 deg (drone perspective) | |
| timeline.animate(cam, "pitch", 0.0, 70.0, duration=0.5, easing=Easing.cubic_in_out) | |
| timeline.animate(cam, "target_y", 0.0, 300.0, duration=0.5) | |
| # Grid of road lines | |
| grid_lines = [] | |
| for i in range(-5, 6): | |
| grid_lines.append([-500.0, i * 100.0, 500.0, i * 100.0]) | |
| grid_lines.append([i * 100.0, -500.0, i * 100.0, 500.0]) | |
| lines_arr = np.array(grid_lines, dtype=np.float64) | |
| def draw_frame(ctx: Context, t: float, frame_idx: int): | |
| ctx.fill_all("#090D16") | |
| proj_lines, depth_scales, mask = cam.transform_lines(lines_arr) | |
| for line, ds, valid in zip(proj_lines, depth_scales, mask): | |
| if not valid: continue | |
| ctx.stroke_width = max(0.5, 2.0 * ds) | |
| ctx.stroke_line(line[0], line[1], line[2], line[3], style="#38BDF8") | |
| cam.render_fog(ctx, fog_color="#090D16", horizon_y_ratio=0.30) | |
| timeline.render_video(video_out, width=640, height=360, fps=30, draw_fn=draw_frame) | |
| assert os.path.exists(video_out) | |
| assert os.path.getsize(video_out) > 1000 | |