Loading...
Searching...
No Matches
CameraMath.hpp
1#pragma once
2#include <score_plugin_gfx_export.h>
3#include <QMatrix4x4>
4#include <QSize>
5#include <QVector3D>
6
7#include <cmath>
8#include <cstddef>
9#include <cstdint>
10#include <cstring>
11
12namespace ossia
13{
14struct camera_component;
15}
16
17namespace score::gfx
18{
19
20// std140 layout; must byte-for-byte match every shader's `uniform camera_t`.
21// Packed into ScenePreprocessor's per-camera Camera UBO aux buffer (attached
22// to Geometry Out and auto-bound in consuming shaders by name).
24{
25 float view[16]{};
26 float projection[16]{};
27 float viewProjection[16]{};
28 float cameraPosition[4]{};
29 float renderSize[4]{};
30 float params[4]{};
31};
32static_assert(sizeof(CameraUBOData) == 240, "CameraUBO layout must match shader");
33
34// Byte range the ScenePreprocessor advertises for its `camera` / `camera_prev`
35// auxiliary buffers, given the number of cameras it packed into them.
36inline constexpr int64_t cameraAuxByteSize(std::size_t cameraCount) noexcept
37{
38 // The buffer always holds at least one entry: flattenScene publishes a default
39 // camera when the scene has none, so a consumer never sees a null binding.
40 return (int64_t)((cameraCount < 1 ? 1 : cameraCount) * sizeof(CameraUBOData));
41}
42
43inline void writeMat4(float dst[16], const QMatrix4x4& src)
44{
45 std::memcpy(dst, src.constData(), 16 * sizeof(float));
46}
47
48// Reverse-Z perspective projection in OpenGL NDC convention.
49//
50// Standard OpenGL perspective: view_z ∈ [-far, -near] → NDC z ∈ [-1, +1].
51// Reverse-Z (this function): view_z ∈ [-far, -near] → NDC z ∈ [-1, +1]
52// but inverted: near → +1, far → -1.
53//
54// QRhi's clipSpaceCorrMatrix on Vulkan/Metal/D3D remaps the output NDC z ∈
55// [-1, +1] down to the backend-native [0, 1] without further flipping:
56// near → 1.0, far → 0.0 in the depth buffer.
57//
58// This is paired project-wide with a float (D32F) depth attachment, a
59// GREATER depth compare and a clear-depth of 0.0. Mixing conventions on a
60// single depth buffer produces garbage.
61inline void setReverseZPerspective(
62 QMatrix4x4& out, float fovYDeg, float aspect, float nearPlane,
63 float farPlane)
64{
65 out.setToIdentity();
66 if(nearPlane == farPlane || aspect == 0.f)
67 return;
68
69 const float radians = (fovYDeg * 0.5f) * float(M_PI / 180.0);
70 const float sine = std::sin(radians);
71 if(sine == 0.f)
72 return;
73 const float cotan = std::cos(radians) / sine;
74 const float clip = farPlane - nearPlane;
75
76 out(0, 0) = cotan / aspect;
77 out(1, 1) = cotan;
78 out(2, 2) = (farPlane + nearPlane) / clip;
79 out(2, 3) = (2.f * farPlane * nearPlane) / clip;
80 out(3, 2) = -1.f;
81 out(3, 3) = 0.f;
82}
83
84// Reverse-Z ORTHOGRAPHIC projection, in the same convention as
85// setReverseZPerspective above: view_z = -near -> NDC z = +1,
86// view_z = -far -> NDC z = -1, paired with a D32F attachment, a GREATER depth
87// compare and a clear-depth of 0.0.
88//
89// halfWidth / halfHeight are the half-extents of the view volume in view space
90// — glTF's `xmag` / `ymag`, which is what ossia::camera_component carries.
91//
92// Derivation of the z row: z_ndc = a*z_view + b with a*(-near) + b = +1 and
93// a*(-far) + b = -1 => a = 2/(far-near), b = (far+near)/(far-near). w stays 1
94// (no perspective divide), which is exactly what makes parallel edges stay
95// parallel.
96inline void setReverseZOrthographic(
97 QMatrix4x4& out, float halfWidth, float halfHeight, float nearPlane,
98 float farPlane)
99{
100 out.setToIdentity();
101 if(nearPlane == farPlane || halfWidth == 0.f || halfHeight == 0.f)
102 return;
103
104 const float clip = farPlane - nearPlane;
105 out(0, 0) = 1.f / halfWidth;
106 out(1, 1) = 1.f / halfHeight;
107 out(2, 2) = 2.f / clip;
108 out(2, 3) = (farPlane + nearPlane) / clip;
109 out(3, 2) = 0.f;
110 out(3, 3) = 1.f;
111}
112
113// The value packCameraUBO writes into CameraUBOData::params[3] (`camera.params.w`
114// in a scene shader) so the shader can tell which projection the author asked
115// for. Perspective is 0, so a shader that ignores the field still reads the
116// common case correctly.
117enum class CameraProjectionMode : int
118{
119 Perspective = 0,
120 Orthographic = 1,
121 Fulldome = 2,
122};
123
124// Pack a camera_component's view/projection/position into a CameraUBOData.
125// `worldTransform` is the camera node's accumulated world matrix (its
126// column 3 is the eye position and its inverse is the view matrix).
127// `aspectOverride` of <= 0 falls back to `renderSize.width / renderSize.height`.
128SCORE_PLUGIN_GFX_EXPORT
129void packCameraUBO(
130 CameraUBOData& out, const ossia::camera_component& cam,
131 const QMatrix4x4& worldTransform, QSize renderSize, float timeSeconds,
132 float aspectOverride = -1.f);
133
134}
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
Definition CameraMath.hpp:24