import { detectVr360Profile, normalizeVr360Profile, sameVr360Profile, vr360FrameUv, type Vr360Profile, } from './vr360.ts' import { applyDirection, applyViewOffset, cameraForward, cameraFromDeviceOrientation, cameraFromYawPitch, forwardPitch, forwardYaw, multiply3, perspectiveMatrix, rotationY, rotationZ, transpose3, viewMatrixFromCamera, } from './vr360Math.ts' function check(name: string, condition: boolean) { if (!condition) throw new Error(`vr360: ${name}`) } function close(a: number, b: number, tolerance = 1e-6): boolean { return Math.abs(a - b) <= tolerance } function closeAll(a: number[], b: number[], tolerance = 1e-6): boolean { return a.length === b.length && a.every((value, index) => close(value, b[index], tolerance)) } function profileOf(profile: Vr360Profile): string { return `${profile.projection}/${profile.stereo}` } // ---------- 命名/画幅识别 ---------- const plain = detectVr360Profile({ title: '普通电影', path: '/media/电影/movie.mkv', width: 1920, height: 1080 }) check('普通素材不自动进入 VR', !plain.confident) check('普通素材默认 360 单眼', profileOf(plain.profile) === 'equirect360/mono') const pano360 = detectVr360Profile({ title: '全景演示', path: '/media/360/equirect_demo.mp4', width: 3840, height: 1920, }) check('360 关键词可识别', pano360.confident && pano360.profile.projection === 'equirect360') check('2:1 的 360 保持单眼', pano360.profile.stereo === 'mono') const vr360Sbs = detectVr360Profile({ path: '/media/vr/vr360-sbs-8k.mp4', width: 7680, height: 1920 }) check('4:1 画幅识别为左右并排', vr360Sbs.profile.stereo === 'sbs') const ou360 = detectVr360Profile({ path: '/media/vr/pano360-ou.mp4', width: 3840, height: 3840 }) check('1:1 的 360 识别为上下并排', ou360.profile.stereo === 'ou') const fisheye = detectVr360Profile({ path: '/media/vr/abc-fisheye-180.mp4', width: 1920, height: 1920 }) check('鱼眼关键词优先于其它信号', fisheye.profile.projection === 'fisheye180') const vr180Sbs = detectVr360Profile({ path: '/media/云下载/sivr-270/sivr-270-1.mp4', width: 3840, height: 1920, }) check('编号里的 vr 可识别为 VR180', vr180Sbs.confident && vr180Sbs.profile.projection === 'equirect180') check('VR180 的 2:1 画幅为左右并排', vr180Sbs.profile.stereo === 'sbs') const vr180Mono = detectVr360Profile({ path: '/media/vr/vr180-mono.mp4', width: 1920, height: 1920 }) check('VR180 的 1:1 画幅为单眼', vr180Mono.profile.stereo === 'mono') const vr180Ou = detectVr360Profile({ path: '/media/vr/vr180-tb.mp4', width: 1920, height: 3840 }) check('VR180 的 1:2 画幅为上下并排', vr180Ou.profile.stereo === 'ou') const topBottom = detectVr360Profile({ path: '/media/vr/vr360-top-bottom.mp4' }) check('top-bottom 关键词为上下并排', topBottom.profile.stereo === 'ou') // ---------- 立体裁切与偏好归一化 ---------- check('单眼不裁切', closeAll(vr360FrameUv('mono').scale, [1, 1])) check('左右并排取左半张', closeAll(vr360FrameUv('sbs').scale, [0.5, 1])) check('上下并排取上半张', closeAll(vr360FrameUv('ou').scale, [1, 0.5])) check('并排画面不做偏移', closeAll(vr360FrameUv('sbs').offset, [0, 0])) check('未知投影回退到 360 全景', normalizeVr360Profile({ projection: 'nope', stereo: 'nope' }).projection === 'equirect360') check('未知布局回退到单眼', normalizeVr360Profile({}).stereo === 'mono') check( '相同配置判定相等', sameVr360Profile({ projection: 'equirect180', stereo: 'sbs' }, { projection: 'equirect180', stereo: 'sbs' }), ) check( '不同配置判定不等', !sameVr360Profile({ projection: 'equirect180', stereo: 'sbs' }, { projection: 'equirect180', stereo: 'ou' }), ) // ---------- 相机姿态与设备陀螺仪 ---------- const identityForward = cameraForward(new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1])) check('单位姿态朝向地面(设备平放)', closeAll([...identityForward], [0, 0, -1])) const level = cameraFromYawPitch(0, 0) check('默认视角水平看向正前方', closeAll([...cameraForward(level)], [0, 1, 0])) const east = cameraFromYawPitch(Math.PI / 2, 0) check('方位角 90° 转向正东', closeAll([...cameraForward(east)], [1, 0, 0], 1e-6)) const up = cameraFromYawPitch(0, Math.PI / 2) check('俯仰角 90° 抬头看天', closeAll([...cameraForward(up)], [0, 0, 1], 1e-6)) const northEast = cameraFromYawPitch(Math.PI / 4, 0) check('方位角 45° 落在东北方向', close(forwardYaw(cameraForward(northEast)), Math.PI / 4)) const tilted = cameraFromYawPitch(0.3, -0.4) check('朝向角可反解出方位角', close(forwardYaw(cameraForward(tilted)), 0.3, 1e-6)) check('朝向角可反解出俯仰角', close(forwardPitch(cameraForward(tilted)), -0.4, 1e-6)) check( '旋转矩阵转置等于反向旋转', closeAll([...transpose3(rotationY(0.7))], [...rotationY(-0.7)], 1e-6), ) check( '正反旋转相乘为单位矩阵', closeAll( [...multiply3(rotationZ(1.1), rotationZ(-1.1))], [1, 0, 0, 0, 1, 0, 0, 0, 1], 1e-6, ), ) // 视图矩阵 = 相机姿态的逆(纯旋转) const view = viewMatrixFromCamera(east) check('视图矩阵为相机矩阵的转置', closeAll([...view], [...transpose3(east)], 1e-6)) // 设备姿态约定(与 W3C 规范一致):相机看向设备背面, // 因此设备平放时(alpha 任意、beta = 0)视线朝下。 const flat = cameraFromDeviceOrientation(Math.PI / 3, 0, 0, 0) check('设备平放时看向地面', closeAll([...cameraForward(flat)], [0, 0, -1], 1e-6)) // 竖持手机(beta = 90°)时视线水平 const portrait = cameraFromDeviceOrientation(0, Math.PI / 2, 0, 0) check('竖持手机时视线水平', close(cameraForward(portrait)[2], 0, 1e-6)) check('竖持手机时视线朝北', close(forwardYaw(cameraForward(portrait)), 0, 1e-6)) // 横屏(屏幕旋转 90°)时相机「上方向」跟随屏幕 const landscape = cameraFromDeviceOrientation(0, Math.PI / 2, 0, Math.PI / 2) check('横屏时画面随屏幕转向', closeAll([...applyDirection(landscape, 0, 1, 0)], [1, 0, 0], 1e-6)) // 陀螺仪开启瞬间的视角对齐:叠加偏移后朝向应等于对齐前记录的拖拽视角 const aligned = applyViewOffset(cameraFromDeviceOrientation(0, Math.PI / 2, 0, 0), 0.5, 0.25) check('叠加偏移后方位角对齐', close(forwardYaw(cameraForward(aligned)), 0.5, 1e-6)) check('叠加偏移后俯仰角对齐', close(forwardPitch(cameraForward(aligned)), 0.25, 1e-6)) // ---------- 透视投影 ---------- const projection = perspectiveMatrix(Math.PI / 2, 2, 0.1, 10) check('透视矩阵缩放项随视场角变化', close(projection[0], 0.5)) check('透视矩阵写入 w = -1(右手系)', projection[11] === -1) console.log('vr360.test.ts: ok')