Panda3D
Loading...
Searching...
No Matches
pssmCameraRig.cxx
1/**
2 *
3 * RenderPipeline
4 *
5 * Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 *
25 */
26
27
28#include "pssmCameraRig.h"
29
30#ifndef _USE_MATH_DEFINES
31#define _USE_MATH_DEFINES
32#endif
33#include <math.h>
34#include "orthographicLens.h"
35
36
37PStatCollector PSSMCameraRig::_update_collector("App:Show code:RP_PSSM_update");
38
39/**
40 * @brief Constructs a new PSSM camera rig
41 * @details This constructs a new camera rig, with a given amount of splits.
42 * The splits can not be changed later on. Splits are also called Cascades.
43 *
44 * An assertion will be triggered if the splits are below zero.
45 *
46 * @param num_splits Amount of PSSM splits
47 */
48PSSMCameraRig::PSSMCameraRig(size_t num_splits) {
49 nassertv(num_splits > 0);
50 _num_splits = num_splits;
51 _pssm_distance = 100.0;
52 _sun_distance = 500.0;
53 _use_fixed_film_size = false;
54 _use_stable_csm = true;
55 _logarithmic_factor = 1.0;
56 _resolution = 512;
57 _border_bias = 0.1;
58 _camera_mvps = PTA_LMatrix4::empty_array(num_splits);
59 _camera_nearfar = PTA_LVecBase2::empty_array(num_splits);
60 init_cam_nodes();
61}
62
63/**
64 * @brief Destructs the camera rig
65 * @details This destructs the camera rig, cleaning up all used resources.
66 */
68 // TODO: Detach all cameras and call remove_node. Most likely this is not
69 // an issue tho, because the camera rig will never get destructed.
70}
71
72/**
73 * @brief Internal method to init the cameras
74 * @details This method constructs all cameras and their required lens nodes
75 * for all splits. It also resets the film size array.
76 */
77void PSSMCameraRig::init_cam_nodes() {
78 _cam_nodes.reserve(_num_splits);
79 _max_film_sizes.resize(_num_splits);
80 _cameras.resize(_num_splits);
81 for (size_t i = 0; i < _num_splits; ++i)
82 {
83 // Construct a new lens
84 Lens *lens = new OrthographicLens();
85 lens->set_film_size(1, 1);
86 lens->set_near_far(1, 1000);
87
88 // Construct a new camera
89 _cameras[i] = new Camera("pssm-cam-" + format_string(i), lens);
90 _cam_nodes.push_back(NodePath(_cameras[i]));
91 _max_film_sizes[i].fill(0);
92 }
93}
94
95/**
96 * @brief Reparents the camera rig
97 * @details This reparents all cameras to the given parent. Usually the parent
98 * will be ShowBase.render. The parent should be the same node where the
99 * main camera is located in, too.
100 *
101 * If an empty parrent is passed, an assertion will get triggered.
102 *
103 * @param parent Parent node path
104 */
106 nassertv(!parent.is_empty());
107 for (size_t i = 0; i < _num_splits; ++i) {
108 _cam_nodes[i].reparent_to(parent);
109 }
110 _parent = parent;
111}
112
113/**
114 * @brief Internal method to compute the view-projection matrix of a camera
115 * @details This returns the view-projection matrix of the given split. No bounds
116 * checking is done. If an invalid index is passed, undefined behaviour occurs.
117 *
118 * @param split_index Index of the split
119 * @return view-projection matrix of the split
120 */
121LMatrix4 PSSMCameraRig::compute_mvp(size_t split_index) {
122 LMatrix4 transform = _parent.get_transform(_cam_nodes[split_index])->get_mat();
123 return transform * _cameras[split_index]->get_lens()->get_projection_mat();
124}
125
126/**
127 * @brief Internal method used for stable CSM
128 * @details This method is used when stable CSM is enabled. It ensures that each
129 * source only moves in texel-steps, thus preventing flickering. This works by
130 * projecting the point (0, 0, 0) to NDC space, making sure that it gets projected
131 * to a texel center, and then projecting that texel back.
132 *
133 * This only works if the camera does not rotate, change its film size, or change
134 * its angle.
135 *
136 * @param mat view-projection matrix of the camera
137 * @param resolution resolution of the split
138 *
139 * @return Offset to add to the camera position to achieve stable snapping
140 */
141LVecBase3 PSSMCameraRig::get_snap_offset(const LMatrix4& mat, size_t resolution) {
142 // Transform origin to camera space
143 LPoint4 base_point = mat.get_row(3) * 0.5 + 0.5;
144
145 // Compute the snap offset
146 float texel_size = 1.0 / (float)(resolution);
147 float offset_x = fmod(base_point.get_x(), texel_size);
148 float offset_y = fmod(base_point.get_y(), texel_size);
149
150 // Reproject the offset back, for that we need the inverse MVP
151 LMatrix4 inv_mat(mat);
152 inv_mat.invert_in_place();
153 LVecBase3 new_base_point = inv_mat.xform_point(LVecBase3(
154 (base_point.get_x() - offset_x) * 2.0 - 1.0,
155 (base_point.get_y() - offset_y) * 2.0 - 1.0,
156 base_point.get_z() * 2.0 - 1.0
157 ));
158 return -new_base_point;
159}
160
161/**
162 * @brief Computes the average of a list of points
163 * @details This computes the average over a given set of points in 3D space.
164 * It returns the average of those points, namely sum_of_points / num_points.
165 *
166 * It is designed to work with a frustum, which is why it takes two arrays
167 * with a dimension of 4. Usually the first array are the camera near points,
168 * and the second array are the camera far points.
169 *
170 * @param starts First array of points
171 * @param ends Second array of points
172 * @return Average of points
173 */
174LPoint3 get_average_of_points(LVecBase3 const (&starts)[4], LVecBase3 const (&ends)[4]) {
175 LPoint3 mid_point(0, 0, 0);
176 for (size_t k = 0; k < 4; ++k) {
177 mid_point += starts[k];
178 mid_point += ends[k];
179 }
180 return mid_point / 8.0;
181}
182
183/**
184 * @brief Finds the minimum and maximum extends of the given projection
185 * @details This projects each point of the given array of points using the
186 * cameras view-projection matrix, and computes the minimum and maximum
187 * of the projected points.
188 *
189 * @param min_extent Will store the minimum extent of the projected points in NDC space
190 * @param max_extent Will store the maximum extent of the projected points in NDC space
191 * @param transform The transformation matrix of the camera
192 * @param proj_points The array of points to project
193 * @param cam The camera to be used to project the points
194 */
195void find_min_max_extents(LVecBase3 &min_extent, LVecBase3 &max_extent, const LMatrix4 &transform, LVecBase3 const (&proj_points)[8], Camera *cam) {
196
197 min_extent.fill(1e10);
198 max_extent.fill(-1e10);
199 LPoint2 screen_points[8];
200
201 // Now project all points to the screen space of the current camera and also
202 // find the minimum and maximum extents
203 for (size_t k = 0; k < 8; ++k) {
204 LVecBase4 point(proj_points[k], 1);
205 LPoint4 proj_point = transform.xform(point);
206 LPoint3 proj_point_3d(proj_point.get_x(), proj_point.get_y(), proj_point.get_z());
207 cam->get_lens()->project(proj_point_3d, screen_points[k]);
208
209 // Find min / max extents
210 if (screen_points[k].get_x() > max_extent.get_x()) max_extent.set_x(screen_points[k].get_x());
211 if (screen_points[k].get_y() > max_extent.get_y()) max_extent.set_y(screen_points[k].get_y());
212
213 if (screen_points[k].get_x() < min_extent.get_x()) min_extent.set_x(screen_points[k].get_x());
214 if (screen_points[k].get_y() < min_extent.get_y()) min_extent.set_y(screen_points[k].get_y());
215
216 // Find min / max projected depth to adjust far plane
217 if (proj_point.get_y() > max_extent.get_z()) max_extent.set_z(proj_point.get_y());
218 if (proj_point.get_y() < min_extent.get_z()) min_extent.set_z(proj_point.get_y());
219 }
220}
221
222/**
223 * @brief Computes a film size from a given minimum and maximum extend
224 * @details This takes a minimum and maximum extent in NDC space and computes
225 * the film size and film offset needed to cover that extent.
226 *
227 * @param film_size Output film size, can be used for Lens::set_film_size
228 * @param film_offset Output film offset, can be used for Lens::set_film_offset
229 * @param min_extent Minimum extent
230 * @param max_extent Maximum extent
231 */
232inline void get_film_properties(LVecBase2 &film_size, LVecBase2 &film_offset, const LVecBase3 &min_extent, const LVecBase3 &max_extent) {
233 float x_center = (min_extent.get_x() + max_extent.get_x()) * 0.5;
234 float y_center = (min_extent.get_y() + max_extent.get_y()) * 0.5;
235 float x_size = max_extent.get_x() - x_center;
236 float y_size = max_extent.get_y() - y_center;
237 film_size.set(x_size, y_size);
238 film_offset.set(x_center * 0.5, y_center * 0.5);
239}
240
241/**
242 * @brief Merges two arrays
243 * @details This takes two arrays which each 4 members and produces an array
244 * with both arrays contained.
245 *
246 * @param dest Destination array
247 * @param array1 First array
248 * @param array2 Second array
249 */
250inline void merge_points_interleaved(LVecBase3 (&dest)[8], LVecBase3 const (&array1)[4], LVecBase3 const (&array2)[4]) {
251 for (size_t k = 0; k < 4; ++k) {
252 dest[k] = array1[k];
253 dest[k+4] = array2[k];
254 }
255}
256
257
258/**
259 * @brief Internal method to compute the splits
260 * @details This is the internal update method to update the PSSM splits.
261 * It distributes the camera splits over the frustum, and updates the
262 * MVP array aswell as the nearfar array.
263 *
264 * @param transform Main camera transform
265 * @param max_distance Maximum pssm distance, relative to the camera far plane
266 * @param light_vector Sun-Vector
267 */
268void PSSMCameraRig::compute_pssm_splits(const LMatrix4& transform, float max_distance, const LVecBase3& light_vector) {
269 nassertv(!_parent.is_empty());
270
271 // PSSM Distance should never be smaller than camera far plane.
272 nassertv(max_distance <= 1.0);
273
274 float filmsize_bias = 1.0 + _border_bias;
275
276 // Compute the positions of all cameras
277 for (size_t i = 0; i < _cam_nodes.size(); ++i) {
278 float split_start = get_split_start(i) * max_distance;
279 float split_end = get_split_start(i + 1) * max_distance;
280
281 LVecBase3 start_points[4];
282 LVecBase3 end_points[4];
283 LVecBase3 proj_points[8];
284
285 // Get split bounding box, and collect all points which define the frustum
286 for (size_t k = 0; k < 4; ++k) {
287 start_points[k] = get_interpolated_point((CoordinateOrigin)k, split_start);
288 end_points[k] = get_interpolated_point((CoordinateOrigin)k, split_end);
289 proj_points[k] = start_points[k];
290 proj_points[k + 4] = end_points[k];
291 }
292
293 // Compute approximate split mid point
294 LPoint3 split_mid = get_average_of_points(start_points, end_points);
295 LPoint3 cam_start = split_mid + light_vector * _sun_distance;
296
297 // Reset the film size, offset and far-plane
298 Camera* cam = DCAST(Camera, _cam_nodes[i].node());
299 Lens *lens = cam->get_lens();
300 lens->set_film_size(1, 1);
301 lens->set_film_offset(0, 0);
302 lens->set_near_far(1, 100);
303
304 // Find a good initial position
305 _cam_nodes[i].set_pos(cam_start);
306 _cam_nodes[i].look_at(split_mid);
307
308 LVecBase3 best_min_extent, best_max_extent;
309
310 // Find minimum and maximum extents of the points
311 LMatrix4 merged_transform = _parent.get_transform(_cam_nodes[i])->get_mat();
312 find_min_max_extents(best_min_extent, best_max_extent, merged_transform, proj_points, cam);
313
314 // Find the film size to cover all points
315 LVecBase2 film_size, film_offset;
316 get_film_properties(film_size, film_offset, best_min_extent, best_max_extent);
317
318 if (_use_fixed_film_size) {
319 // In case we use a fixed film size, store the maximum film size, and
320 // only change the film size if a new maximum is there
321 if (_max_film_sizes[i].get_x() < film_size.get_x()) _max_film_sizes[i].set_x(film_size.get_x());
322 if (_max_film_sizes[i].get_y() < film_size.get_y()) _max_film_sizes[i].set_y(film_size.get_y());
323
324 lens->set_film_size(_max_film_sizes[i] * filmsize_bias);
325 } else {
326 // If we don't use a fixed film size, we can just set the film size
327 // on the lens.
328 lens->set_film_size(film_size * filmsize_bias);
329 }
330
331 // Compute new film offset
332 lens->set_film_offset(film_offset);
333 lens->set_near_far(10, best_max_extent.get_z());
334 _camera_nearfar[i] = LVecBase2(10, best_max_extent.get_z());
335
336 // Compute the camera MVP
337 LMatrix4 mvp = compute_mvp(i);
338
339 // Stable CSM Snapping
340 if (_use_stable_csm) {
341 LPoint3 snap_offset = get_snap_offset(mvp, _resolution);
342 _cam_nodes[i].set_pos(_cam_nodes[i].get_pos() + snap_offset);
343
344 // Compute the new mvp, since we changed the snap offset
345 mvp = compute_mvp(i);
346 }
347
348 _camera_mvps.set_element(i, mvp);
349 }
350}
351
352
353/**
354 * @brief Updates the PSSM camera rig
355 * @details This updates the rig with an updated camera position, and a given
356 * light vector. This should be called on a per-frame basis. It will reposition
357 * all camera sources to fit the frustum based on the pssm distribution.
358 *
359 * The light vector should be the vector from the light source, not the
360 * vector to the light source.
361 *
362 * @param cam_node Target camera node
363 * @param light_vector The vector from the light to any point
364 */
365void PSSMCameraRig::update(NodePath cam_node, const LVecBase3 &light_vector) {
366 nassertv(!cam_node.is_empty());
367 _update_collector.start();
368
369 // Get camera node transform
370 LMatrix4 transform = cam_node.get_transform()->get_mat();
371
372 // Get Camera and Lens pointers
373 Camera *cam;
374 PandaNode *node = cam_node.node();
375 if (node->is_of_type(Camera::get_class_type())) {
376 cam = (Camera *)node;
377 }
378 else {
379 // Perhaps we passed in something like base.camera ?
380 cam = DCAST(Camera, cam_node.get_child(0).node());
381 nassertv(cam != nullptr);
382 }
383 Lens* lens = cam->get_lens();
384
385 // Extract near and far points:
386 lens->extrude(LPoint2(-1, 1), _curr_near_points[UpperLeft], _curr_far_points[UpperLeft]);
387 lens->extrude(LPoint2(1, 1), _curr_near_points[UpperRight], _curr_far_points[UpperRight]);
388 lens->extrude(LPoint2(-1, -1), _curr_near_points[LowerLeft], _curr_far_points[LowerLeft]);
389 lens->extrude(LPoint2(1, -1), _curr_near_points[LowerRight], _curr_far_points[LowerRight]);
390
391 // Construct MVP to project points to world space
392 LMatrix4 mvp = transform * lens->get_view_mat();
393
394 // Project all points to world space
395 for (size_t i = 0; i < 4; ++i) {
396 LPoint4 ws_near = mvp.xform(_curr_near_points[i]);
397 LPoint4 ws_far = mvp.xform(_curr_far_points[i]);
398 _curr_near_points[i].set(ws_near.get_x(), ws_near.get_y(), ws_near.get_z());
399 _curr_far_points[i].set(ws_far.get_x(), ws_far.get_y(), ws_far.get_z());
400 }
401
402 // Do the actual PSSM
403 double far_recip = std::max(1.0 / (double)lens->get_far(), (double)lens_far_limit);
404 compute_pssm_splits( transform, _pssm_distance * far_recip, light_vector );
405
406 _update_collector.stop();
407}
408
A node that can be positioned around in the scene graph to represent a point of view for rendering a ...
Definition camera.h:35
Lens * get_lens(int index=0) const
Returns a pointer to the particular Lens associated with this LensNode, or NULL if there is not yet a...
Definition lensNode.I:47
A base class for any number of different kinds of lenses, linear and otherwise.
Definition lens.h:41
bool project(const LPoint3 &point3d, LPoint3 &point2d) const
Given a 3-d point in space, determine the 2-d point this maps to, in the range (-1,...
Definition lens.I:131
bool extrude(const LPoint2 &point2d, LPoint3 &near_point, LPoint3 &far_point) const
Given a 2-d point in the range (-1,1) in both dimensions, where (0,0) is the center of the lens and (...
Definition lens.I:24
get_view_mat
Returns the direction in which the lens is facing.
Definition lens.h:141
set_film_offset
Sets the horizontal and vertical offset amounts of this Lens.
Definition lens.h:87
get_far
Returns the position of the far plane (or cylinder, sphere, whatever).
Definition lens.h:114
set_film_size
Sets the horizontal size of the film without changing its shape.
Definition lens.h:82
void set_near_far(PN_stdfloat near_distance, PN_stdfloat far_distance)
Simultaneously changes the near and far planes.
Definition lens.I:419
NodePath is the fundamental system for disambiguating instances, and also provides a higher-level int...
Definition nodePath.h:159
bool is_empty() const
Returns true if the NodePath contains no nodes.
Definition nodePath.I:188
PandaNode * node() const
Returns the referenced node of the path.
Definition nodePath.I:227
NodePath get_child(int n, Thread *current_thread=Thread::get_current_thread()) const
Returns a NodePath representing the nth child of the referenced node.
Definition nodePath.I:337
const TransformState * get_transform(Thread *current_thread=Thread::get_current_thread()) const
Returns the complete transform object set on this node.
Definition nodePath.cxx:794
An orthographic lens.
void update(NodePath cam_node, const LVecBase3 &light_vector)
Updates the PSSM camera rig.
~PSSMCameraRig()
Destructs the camera rig.
PSSMCameraRig(size_t num_splits)
Constructs a new PSSM camera rig.
void reparent_to(NodePath parent)
Reparents the camera rig.
A lightweight class that represents a single element that may be timed and/or counted via stats.
A basic node of the scene graph or data graph.
Definition pandaNode.h:65
get_mat
Returns the matrix that describes the transform.
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition typedObject.I:28
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.