#pragma once #include #include #include #include #include #include #include namespace observatory { struct Vec3 { float x{}, y{}, z{}; }; inline Vec3 subtract(Vec3 a, Vec3 b) { return {a.x-b.x,a.y-b.y,a.z-b.z}; } inline float dot(Vec3 a, Vec3 b) { return a.x*b.x+a.y*b.y+a.z*b.z; } inline Vec3 cross(Vec3 a, Vec3 b) { return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x}; } inline std::expected unit(Vec3 value) { const float squared=dot(value,value); if (!std::isfinite(squared)||squared<1e-12F) return std::unexpected("Direction must have a supported finite nonzero length."); const float scale=1.0F/std::sqrt(squared); return Vec3{value.x*scale,value.y*scale,value.z*scale}; } // Column-major storage, column vectors, right-handed view looking along -Z. struct Mat4 { std::array data{}; }; inline Mat4 identity() { return {{{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}}}; } inline std::array transform(const Mat4& matrix,const std::array& point) { std::array result{}; for(std::size_t row=0;row<4;++row) for(std::size_t column=0;column<4;++column) result[row]+=matrix.data[column*4+row]*point[column]; return result; } inline Mat4 translation(Vec3 offset) { auto result=identity(); result.data[12]=offset.x;result.data[13]=offset.y;result.data[14]=offset.z; return result; } inline std::expected look_at(Vec3 eye,Vec3 target,Vec3 up) { const auto bounded=[](Vec3 value){return std::isfinite(value.x)&&std::isfinite(value.y)&& std::isfinite(value.z)&&std::abs(value.x)<=1000&&std::abs(value.y)<=1000&&std::abs(value.z)<=1000;}; if(!bounded(eye)||!bounded(target)||!bounded(up))return std::unexpected("Unsupported view inputs."); const auto forward=unit(subtract(target,eye)); const auto up_unit=unit(up); if(!forward||!up_unit)return std::unexpected("Eye and target must differ; up must be nonzero."); const auto right=unit(cross(*forward,*up_unit)); if(!right)return std::unexpected("Up must not be parallel to the viewing direction."); const auto camera_up=cross(*right,*forward); return Mat4{{{right->x,camera_up.x,-forward->x,0, right->y,camera_up.y,-forward->y,0, right->z,camera_up.z,-forward->z,0, -dot(*right,eye),-dot(camera_up,eye),dot(*forward,eye),1}}}; } inline Mat4 multiply(const Mat4& a,const Mat4& b) { Mat4 result; for(std::size_t column=0;column<4;++column) for(std::size_t row=0;row<4;++row) for(std::size_t k=0;k<4;++k) result.data[column*4+row]+=a.data[k*4+row]*b.data[column*4+k]; return result; } inline Mat4 rotate_z(float radians) { auto result=identity(); const float c=std::cos(radians),s=std::sin(radians); result.data[0]=c;result.data[1]=s;result.data[4]=-s;result.data[5]=c; return result; } inline std::expected perspective(float vertical_degrees,float aspect,float near_z,float far_z) { if(!std::isfinite(vertical_degrees)||vertical_degrees<10||vertical_degrees>120|| !std::isfinite(aspect)||aspect<0.05F||aspect>20|| !std::isfinite(near_z)||!std::isfinite(far_z)||near_z<0.001F||far_z>10000||far_z<=near_z) return std::unexpected("Unsupported perspective parameters."); const float scale=1/std::tan(vertical_degrees*std::numbers::pi_v/360); Mat4 result; result.data[0]=scale/aspect;result.data[5]=scale; result.data[10]=(far_z+near_z)/(near_z-far_z); result.data[11]=-1; result.data[14]=(2*far_z*near_z)/(near_z-far_z); return result; } inline std::expected rgb_bytes(int width,int height) { if(width<=0||height<=0||width>8192||height>8192) return std::unexpected("Framebuffer dimensions are unsupported."); const auto w=static_cast(width),h=static_cast(height); constexpr std::size_t max_pixels=16*1024*1024; if(h>max_pixels/w) return std::unexpected("Framebuffer exceeds the capture pixel budget."); return w*h*3; } } // namespace observatory