mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-25 21:01:49 +01:00
89 lines
5.2 KiB
C++
89 lines
5.2 KiB
C++
#pragma once
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <utility>
|
|
#include "vendor/json.hpp"
|
|
using RegionJson=nlohmann::json;
|
|
struct RecordingRegionPlane {vr::HmdMatrix34_t transform{};float width=0;};
|
|
inline void recording_region_foreground(vr::IVROverlay* overlays,vr::VROverlayHandle_t handle){
|
|
overlays->SetOverlayFlag(handle,vr::VROverlayFlags_SortWithNonSceneOverlays,true);
|
|
overlays->SetOverlaySortOrder(handle,1000);
|
|
}
|
|
inline bool recording_region_plane(const RegionJson& view,double tan,double cx,double cy,double aspect,RecordingRegionPlane& plane,double distance=2.){
|
|
try{
|
|
if(!std::isfinite(tan)||tan<=0||!std::isfinite(cx)||!std::isfinite(cy)||!std::isfinite(aspect)||aspect<=0||!std::isfinite(distance)||distance<=0)return false;
|
|
const auto& basis=view.at("captureToHead");
|
|
for(int i=0;i<3;i++){
|
|
double translation=basis.at(i).at(3).get<double>();
|
|
for(int j=0;j<3;j++){
|
|
double value=basis.at(i).at(j).get<double>();if(!std::isfinite(value))return false;
|
|
plane.transform.m[i][j]=value;
|
|
translation+=distance*value*(j==0?cx:j==1?-cy:-1.);
|
|
}
|
|
if(!std::isfinite(translation))return false;plane.transform.m[i][3]=translation;
|
|
}
|
|
plane.width=2*tan*distance;return std::isfinite(plane.width)&&plane.width>0;
|
|
}catch(...){return false;}
|
|
}
|
|
inline std::array<unsigned char,3> recording_region_color(){
|
|
static std::array<unsigned char,3> cached{0,255,255};static auto checked=std::chrono::steady_clock::time_point{};
|
|
auto now=std::chrono::steady_clock::now();if(now-checked<std::chrono::seconds(1))return cached;checked=now;
|
|
std::array<unsigned char,3> color{0,255,255};auto* settings=vr::VRSettings();
|
|
if(!settings)return cached;
|
|
const char* keys[]={vr::k_pch_CollisionBounds_ColorGammaR_Int32,vr::k_pch_CollisionBounds_ColorGammaG_Int32,vr::k_pch_CollisionBounds_ColorGammaB_Int32};
|
|
for(int i=0;i<3;i++){vr::EVRSettingsError error{};auto value=settings->GetInt32(vr::k_pch_CollisionBounds_Section,keys[i],&error);if(error!=vr::VRSettingsError_None||value<0||value>255)return cached;color[i]=static_cast<unsigned char>(value);}
|
|
cached=color;return cached;
|
|
}
|
|
inline std::vector<unsigned char> recording_region_pixels(int w,int h,const std::array<unsigned char,3>& color={0,255,255}){
|
|
std::vector<unsigned char> pixels(w*h*4);
|
|
for(int y=0;y<h;y++)for(int x=0;x<w;x++){
|
|
bool edge=x<2||x>=w-2||y<2||y>=h-2;
|
|
if(edge&&((x<2||x>=w-2?y:x)/14)%2==0){auto i=(y*w+x)*4;for(int c=0;c<3;c++)pixels[i+c]=color[c];pixels[i+3]=72;}
|
|
}
|
|
return pixels;
|
|
}
|
|
class RecordingRegionOverlay {
|
|
vr::IVROverlay* overlays=nullptr;vr::VROverlayHandle_t handle=0;std::string last,last_pixels;
|
|
std::string key;
|
|
public:
|
|
explicit RecordingRegionOverlay(std::string overlay_key="framely.recording.region"):key(std::move(overlay_key)){}
|
|
void destroy(){if(handle&&overlays)overlays->DestroyOverlay(handle);handle=0;last.clear();last_pixels.clear();}
|
|
void tick(vr::IVROverlay* api,const RegionJson& state,double distance=2.,const RegionJson* live_view=nullptr){
|
|
overlays=api;
|
|
if(!state.value("visible",false)){destroy();return;}
|
|
auto color=recording_region_color();auto serialized=state.dump()+std::string(color.begin(),color.end())+std::to_string(distance)+(live_view?live_view->dump():"");if(serialized==last)return;
|
|
try{
|
|
const auto& view=live_view?*live_view:state.at("view");double aspect=view.at("aspect");
|
|
RecordingRegionPlane plane;if(!recording_region_plane(view,view.at("tanHalfHorizontal"),view.at("centerTanX"),view.at("centerTanY"),aspect,plane,distance)){destroy();return;}
|
|
if(!handle){
|
|
if(overlays->CreateOverlay(key.c_str(),"Framely capture region",&handle))return;
|
|
overlays->SetOverlayInputMethod(handle,vr::VROverlayInputMethod_None);
|
|
overlays->SetOverlayFlag(handle,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,false);
|
|
overlays->SetOverlayFlag(handle,vr::VROverlayFlags_HideLaserIntersection,true);
|
|
overlays->SetOverlayFlag(handle,vr::VROverlayFlags_VisibleInDashboard,true);
|
|
recording_region_foreground(overlays,handle);
|
|
}
|
|
int width=state.at("settings").at("width"),height=state.at("settings").at("height");
|
|
if(width<160||height<160||width>4096||height>4096||std::abs(double(width)/height-aspect)>1e-6){destroy();return;}
|
|
// The INNER edge is the capture boundary. Keep the guide outside it so
|
|
// it does not cover pixels inside the intended recorded image.
|
|
// Bound raw-overlay traffic independently of recording resolution.
|
|
// Square texels are explicitly stretched to the exact capture aspect.
|
|
width=height=512;plane.width*=512.f/508.f;
|
|
auto error=overlays->SetOverlayWidthInMeters(handle,plane.width);
|
|
if(!error)error=overlays->SetOverlayTexelAspect(handle,aspect);
|
|
if(!error)error=overlays->SetOverlayTransformTrackedDeviceRelative(handle,vr::k_unTrackedDeviceIndex_Hmd,&plane.transform);
|
|
const auto pixel_key=std::string(color.begin(),color.end());
|
|
if(!error&&(last_pixels.empty()||pixel_key!=last_pixels)){
|
|
auto pixels=recording_region_pixels(width,height,color);
|
|
error=overlays->SetOverlayRaw(handle,pixels.data(),width,height,4);
|
|
if(!error)last_pixels=pixel_key;
|
|
}
|
|
if(error){destroy();return;}overlays->ShowOverlay(handle);last=serialized;
|
|
}catch(...){destroy();}
|
|
}
|
|
};
|