Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
World3DProjection.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <vector>
7
8namespace monopoly::engine
9{
10 namespace
11 {
12 struct Point3D { float x{}, y{}, z{}; };
13
14 float dot(const std::array<float, 3>& a,
15 const std::array<float, 3>& b) noexcept
16 { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
17
18 std::array<float, 3> subtract(const std::array<float, 3>& a,
19 const std::array<float, 3>& b) noexcept
20 { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; }
21
22 std::array<float, 3> scale(const std::array<float, 3>& a, float value) noexcept
23 { return {a[0] * value, a[1] * value, a[2] * value}; }
24
25 float magnitude(const std::array<float, 3>& value) noexcept
26 { return std::sqrt(dot(value, value)); }
27
28 std::array<float, 3> normalize(const std::array<float, 3>& value) noexcept
29 {
30 const auto length = magnitude(value);
31 return {value[0] / length, value[1] / length, value[2] / length};
32 }
33
34 std::array<float, 3> cross(const std::array<float, 3>& a,
35 const std::array<float, 3>& b) noexcept
36 {
37 return {
38 a[1] * b[2] - a[2] * b[1],
39 a[2] * b[0] - a[0] * b[2],
40 a[0] * b[1] - a[1] * b[0]};
41 }
42
43 Point3D transformPoint(Point3D point,
44 const sequence::Matrix3D& matrix) noexcept
45 {
46 const auto& m = matrix.values;
47 const float x = m[0] * point.x + m[4] * point.y + m[8] * point.z + m[12];
48 const float y = m[1] * point.x + m[5] * point.y + m[9] * point.z + m[13];
49 const float z = m[2] * point.x + m[6] * point.y + m[10] * point.z + m[14];
50 const float w = m[3] * point.x + m[7] * point.y + m[11] * point.z + m[15];
51 if (w == 0.0F) return {};
52 return {x / w, y / w, z / w};
53 }
54
55 std::optional<std::int32_t> coord(float value) noexcept
56 {
57 if (!std::isfinite(value)) return std::nullopt;
58 constexpr auto low = static_cast<float>(std::numeric_limits<std::int32_t>::min());
59 constexpr auto high = static_cast<float>(std::numeric_limits<std::int32_t>::max());
60 if (value < low || value > high) return std::nullopt;
61 return static_cast<std::int32_t>(value);
62 }
63
64 sequence::Matrix3D makeView(const World3DCamera& camera) noexcept
65 {
66 const auto forward = normalize(camera.forward);
67 const auto projected = scale(forward, dot(camera.up, forward));
68 const auto up = normalize(subtract(camera.up, projected));
69 const auto right = cross(up, forward);
70 auto result = sequence::identity3D();
71 result.values[0] = right[0]; result.values[1] = up[0]; result.values[2] = forward[0];
72 result.values[4] = right[1]; result.values[5] = up[1]; result.values[6] = forward[1];
73 result.values[8] = right[2]; result.values[9] = up[2]; result.values[10] = forward[2];
74 result.values[12] = -dot(camera.location, right);
75 result.values[13] = -dot(camera.location, up);
76 result.values[14] = -dot(camera.location, forward);
77 return result;
78 }
79
80 sequence::Matrix3D makeProjection(const World3DCamera& camera) noexcept
81 {
82 sequence::Matrix3D result{};
83 const float cotangent = std::cos(camera.fieldOfView * 0.5F) /
84 std::sin(camera.fieldOfView * 0.5F);
85 result.values[0] = cotangent;
86 result.values[5] = cotangent;
87 result.values[10] = camera.farPlane / (camera.farPlane - camera.nearPlane);
88 result.values[11] = 1.0F;
89 result.values[14] = -result.values[10] * camera.nearPlane;
90 return result;
91 }
92
93 sequence::Matrix3D makeViewport(World3DRect viewport) noexcept
94 {
95 auto result = sequence::identity3D();
96 const auto width = viewport.right - viewport.left;
97 const auto height = viewport.bottom - viewport.top;
98 const float aspect = static_cast<float>(width) / static_cast<float>(height);
99 result.values[0] = static_cast<float>(width / 2);
100 result.values[5] = aspect * static_cast<float>((viewport.top - viewport.bottom) / 2);
101 result.values[12] = static_cast<float>(viewport.left + width / 2);
102 result.values[13] = static_cast<float>(viewport.top + height / 2);
103 return result;
104 }
105
106 bool finiteVector(const std::array<float, 3>& value) noexcept
107 { return std::isfinite(value[0]) && std::isfinite(value[1]) && std::isfinite(value[2]); }
108 }
109
110 std::expected<World3DProjectionState, World3DProjectionError>
112 {
113 if (viewport.empty())
114 return std::unexpected(World3DProjectionError{
116 if (!std::isfinite(camera.nearPlane) || !std::isfinite(camera.farPlane) ||
117 camera.nearPlane <= 0.0F || camera.farPlane <= camera.nearPlane)
118 return std::unexpected(World3DProjectionError{
120 if (!std::isfinite(camera.fieldOfView) || camera.fieldOfView <= 0.0F ||
121 camera.fieldOfView >= 3.14159265358979323846F)
122 return std::unexpected(World3DProjectionError{
124 if (!finiteVector(camera.location) || !finiteVector(camera.forward) ||
125 !finiteVector(camera.up) || magnitude(camera.forward) <= 0.000001F)
126 return std::unexpected(World3DProjectionError{
128
129 const auto forward = normalize(camera.forward);
130 const auto projectedUp = subtract(camera.up, scale(forward, dot(camera.up, forward)));
131 if (magnitude(projectedUp) <= 0.000001F)
132 return std::unexpected(World3DProjectionError{
134
136 result.viewport = viewport;
137 result.camera = camera;
138 result.view = makeView(camera);
139 result.projection = makeProjection(camera);
140 result.rasterProjection = result.projection;
141 // PC3D/view.cpp encoded aspect preservation in D3DVIEWPORT2:
142 // dvClipHeight = 2 * (height / width). SDL_GPU has a standard
143 // [-1,1] clip volume, so fold the inverse clip aspect into Y here.
144 const float rasterAspect = static_cast<float>(viewport.right - viewport.left) /
145 static_cast<float>(viewport.bottom - viewport.top);
146 result.rasterProjection.values[5] *= rasterAspect;
147 result.viewportMatrix = makeViewport(viewport);
148 result.cameraToScreen = sequence::multiply(result.projection, result.viewportMatrix);
149 return result;
150 }
151
152 std::optional<World3DRect> intersectWorld3DRect(
154 {
155 World3DRect result{
156 std::max(left.left, right.left),
157 std::max(left.top, right.top),
158 std::min(left.right, right.right),
159 std::min(left.bottom, right.bottom)};
160 return result.empty() ? std::nullopt : std::optional<World3DRect>{result};
161 }
162
163 std::optional<World3DRect> world3DMeshScreenRect(
164 const data::MeshBounds& bounds,
165 const sequence::Matrix3D& worldTransform,
166 const World3DProjectionState& projection) noexcept
167 {
168 const auto modelToCamera = sequence::multiply(worldTransform, projection.view);
169 std::array<Point3D, 8> corners{};
170 for (std::size_t i = 0; i < corners.size(); ++i)
171 {
172 const Point3D point{
173 (i & 4U) ? bounds.maximum[0] : bounds.minimum[0],
174 (i & 2U) ? bounds.maximum[1] : bounds.minimum[1],
175 (i & 1U) ? bounds.maximum[2] : bounds.minimum[2]};
176 corners[i] = transformPoint(point, modelToCamera);
177 }
178
179 std::vector<Point3D> visible;
180 visible.reserve(16);
181 for (const auto& point : corners)
182 if (point.z >= projection.camera.nearPlane)
183 visible.push_back(point);
184 if (visible.empty()) return std::nullopt;
185
186 if (visible.size() < corners.size())
187 {
188 for (std::size_t i = 0; i < corners.size(); ++i)
189 {
190 for (std::size_t dimension = 0; dimension < 3; ++dimension)
191 {
192 const auto j = i ^ (std::size_t{1} << dimension);
193 if (j < i) continue;
194 const bool firstVisible = corners[i].z >= projection.camera.nearPlane;
195 const bool secondVisible = corners[j].z >= projection.camera.nearPlane;
196 if (firstVisible == secondVisible) continue;
197 const float denominator = corners[j].z - corners[i].z;
198 if (denominator == 0.0F) continue;
199 const float proportion =
200 (projection.camera.nearPlane - corners[i].z) / denominator;
201 visible.push_back({
202 corners[i].x + (corners[j].x - corners[i].x) * proportion,
203 corners[i].y + (corners[j].y - corners[i].y) * proportion,
204 projection.camera.nearPlane});
205 }
206 }
207 }
208
209 std::vector<Point3D> screen;
210 screen.reserve(visible.size());
211 for (const auto& point : visible)
212 screen.push_back(transformPoint(point, projection.cameraToScreen));
213 if (screen.empty()) return std::nullopt;
214
215 auto firstX = coord(screen.front().x);
216 auto firstY = coord(screen.front().y);
217 if (!firstX || !firstY) return std::nullopt;
219 for (const auto& point : screen)
220 {
221 auto x = coord(point.x);
222 auto y = coord(point.y);
223 if (!x || !y) return std::nullopt;
224 rectangle.left = std::min(rectangle.left, *x);
225 rectangle.right = std::max(rectangle.right, *x);
226 rectangle.top = std::min(rectangle.top, *y);
227 rectangle.bottom = std::max(rectangle.bottom, *y);
228 }
229
230 if (rectangle.left > std::numeric_limits<std::int32_t>::min()) --rectangle.left;
231 if (rectangle.top > std::numeric_limits<std::int32_t>::min()) --rectangle.top;
232 if (rectangle.right < std::numeric_limits<std::int32_t>::max()) ++rectangle.right;
233 if (rectangle.bottom < std::numeric_limits<std::int32_t>::max()) ++rectangle.bottom;
234 return intersectWorld3DRect(rectangle, projection.viewport);
235 }
236}
std::atomic< float > left
std::atomic< float > right
std::uint32_t height
std::uint32_t width
unsigned index
int z
std::uint16_t value
Definition Timers.cpp:16
std::optional< World3DRect > world3DMeshScreenRect(const data::MeshBounds &bounds, const sequence::Matrix3D &worldTransform, const World3DProjectionState &projection) noexcept
std::optional< World3DRect > intersectWorld3DRect(World3DRect left, World3DRect right) noexcept
std::expected< World3DProjectionState, World3DProjectionError > makeWorld3DProjectionState(World3DRect viewport, World3DCamera camera)
Matrix3D identity3D() noexcept
Matrix2D multiply(const Matrix2D &left, const Matrix2D &right) noexcept