001/*
002 * Renderer 9. The MIT License.
003 * Copyright (c) 2022 rlkraft@pnw.edu
004 * See LICENSE for details.
005*/
006
007package renderer.scene;
008
009/**
010   This {@code Camera} data structure represents a camera
011   that can be translated around within world coordinates.
012<p>
013   This {@code Camera} has a {@link viewVector} which associates
014   to this {@code Camera} a location in the world coordinate system.
015   A {@code Camera} object is moved within world coordinates by
016   changing its {@link viewVector}.
017<p>
018   A {@code Camera}'s {@link translate} method is used to move
019   the {@code Camera} in world coordinates exactly as the
020   {@link Position#translate} method is used to move a
021   {@link Model} in world coordinates.
022<p>
023   The renderer uses the camera's {@link viewVector} in its
024   {@link renderer.pipeline.World2View} pipeline stage. In that stage,
025   the renderer subtracts the camera's {@link viewVector} from every
026   vertex of every model in the scene to convert each vertex from
027   world coordinates to the camera's view coordinate system. The reason
028   for using subtraction is that when the camera moves forward by 5 units
029   in the z-direction, every vertex in the scene will appear 5 units
030   closer to the camera. So every vertex in the scene needs a translation
031   by -5 in the z-direction. Similarly, if the camera moves up 5 units
032   in the y-direction, every vertex in the scene will appear to the
033   camera to be 5 units lower. So every vertex in the scene needs a
034   translation by -5 in the y-direction.
035<p>
036   This {@code Camera} has a configurable "view volume" that
037   determines what part of world space the camera "sees" when
038   we use the camera to take a picture (that is, when we render
039   a {@link Scene}).
040<p>
041   This {@code Camera} can "take a picture" two ways, using
042   a perspective projection or a parallel (orthographic)
043   projection. Each way of taking a picture has a different
044   shape for its view volume. The data in this data structure
045   determines the shape of each of the two view volumes.
046<p>
047   For the perspective projection, the view volume (in view
048   coordinates!) is the infinitely long frustum that is formed
049   by cutting at the near clipping plane, {@code z = -near},
050   the infinitely long pyramid with its apex at the origin
051   and its cross section in the image plane, {@code z = -1},
052   with edges {@code x = -1}, {@code x = +1}, {@code y = -1},
053   and {@code y = +1}. The perspective view volume's shape is
054   set by the {@link projPerspective} method.
055<p>
056   For the orthographic projection, the view volume (in view
057   coordinates!) is the semi-infinite rectangular cylinder
058   parallel to the z-axis, with base in the near clipping plane,
059   {@code z = -near}, and with edges {@code x = left},
060   {@code x = right}, {@code y = bottom}, {@code y = top} (a
061   semi-infinite parallelepiped). The orthographic view volume's
062   shape is set by the {@link projOrtho} method.
063<p>
064   When the graphics rendering {@link renderer.pipeline.Pipeline}
065   uses this {@code Camera} to render a {@link Scene}, the renderer
066   only "sees" the geometry from the scene that is contained in this
067   camera's view volume. (Notice that this means the orthographic
068   camera can see geometry that is behind the camera. In fact, the
069   perspective camera can also sees geometry that is behind the
070   camera.) The renderer's {@link renderer.pipeline.NearClip} and
071   {@link renderer.pipeline.Clip} pipeline stages are responsible
072   for making sure that the scene's geometry that is outside of
073   this camera's view volume is not visible.
074<p>
075   The plane {@code z = -1} (in view coordinates) is the camera's
076   "image plane". The rectangle in the image plane with corners
077   {@code (left, bottom, -1)} and {@code (right, top, -1)} is the
078   camera's "view rectangle". The view rectangle is like the film
079   in a real camera, it is where the camera's image appears when you
080   take a picture. The contents of the camera's view rectangle (after
081   it gets "normalized" to camera coordinates by the renderer's
082   {@link renderer.pipeline.View2Camera} stage) is what gets rasterized,
083   by the renderer's {@link renderer.pipeline.Rasterize}
084   pipeline stage, into a {@link renderer.framebuffer.FrameBuffer}'s
085   {@link renderer.framebuffer.FrameBuffer.Viewport}.
086<p>
087   For both the perspective and the parallel projections, the camera's
088   near plane is there to prevent the camera from seeing what is "behind"
089   the near plane. For the perspective projection, the near plane also
090   prevents the renderer from incorrectly rasterizing line segments that
091   cross the camera plane, {@code z = 0}.
092*/
093public final class Camera
094{
095   // Choose either perspective or parallel projection.
096   public final boolean perspective;
097   // The following five numbers define the camera's view volume.
098   // The first four will be encoded into the camera's normalization matrix.
099   public final double left;
100   public final double right;
101   public final double bottom;
102   public final double top;
103   public final double n;  // near clipping plane
104   // This Vector determines the Camera's location in world space.
105   private Vector viewVector;
106
107   /**
108      A private {@code Camera} constructor for
109      use by the static factory methods.
110   */
111   private Camera(final boolean perspective,
112                  final double left,
113                  final double right,
114                  final double bottom,
115                  final double top,
116                  final double n,
117                  final Vector viewVector)
118   {
119      this.perspective = perspective;
120      this.left = left;
121      this.right = right;
122      this.bottom = bottom;
123      this.top = top;
124      this.n = n;
125      this.viewVector = viewVector;
126   }
127
128
129   /**
130      This is a static factory method.
131      <p>
132      Set up this {@code Camera}'s view volume as a perspective projection
133      of the normalized infinite view pyramid extending along the negative
134      z-axis.
135
136      @return a new {@code Camera} object with the default perspective parameters
137   */
138   public static Camera projPerspective()
139   {
140      return projPerspective(-1.0, +1.0, -1.0, +1.0); // left, right, bottom, top
141   }
142
143
144   /**
145      This is a static factory method.
146      <p>
147      Set up this {@code Camera}'s view volume as a perspective projection
148      of an infinite view pyramid extending along the negative z-axis.
149
150      @param left    left edge of view rectangle in the image plane
151      @param right   right edge of view rectangle in the image plane
152      @param bottom  bottom edge of view rectangle in the image plane
153      @param top     top edge of view rectangle in the image plane
154      @return a new {@code Camera} object with the given parameters
155   */
156   public static Camera projPerspective(final double left,   final double right,
157                                        final double bottom, final double top)
158   {
159      return projPerspective(left, right, bottom, top, 1.0);
160   }
161
162
163   /**
164      This is a static factory method.
165      <p>
166      Set up this {@code Camera}'s view volume as a perspective projection
167      of an infinite view pyramid extending along the negative z-axis.
168      <p>
169      Use {@code focalLength} to determine the image plane. So the
170      {@code left}, {@code right}, {@code bottom}, {@code top}
171      parameters are used in the plane {@code z = -focalLength}.
172      <p>
173      The {@code focalLength} parameter can be used to zoom an
174      asymmetric view volume, much like the {@code fovy} parameter
175      for the symmetric view volume, or the "near" parameter for
176      the OpenGL gluPerspective() function.
177
178      @param left    left edge of view rectangle in the image plane
179      @param right   right edge of view rectangle in the image plane
180      @param bottom  bottom edge of view rectangle in the image plane
181      @param top     top edge of view rectangle in the image plane
182      @param focalLength  distance from the origin to the image plane
183      @return a new {@code Camera} object with the given parameters
184   */
185   public static Camera projPerspective(final double left,   final double right,
186                                        final double bottom, final double top,
187                                        final double focalLength)
188   {
189      return new Camera(true,
190                        left / focalLength,
191                        right / focalLength,
192                        bottom / focalLength,
193                        top / focalLength,
194                        -0.1,  // near clipping plane (near = +0.1)
195                        new Vector(0,0,0));  // viewVector
196   }
197
198
199   /**
200      This is a static factory method.
201      <p>
202      Set up this {@code Camera}'s view volume as a symmetric infinite
203      view pyramid extending along the negative z-axis.
204      <p>
205      Here, the view volume is determined by a vertical "field of view"
206      angle and an aspect ratio for the view rectangle in the image plane.
207
208      @param fovy    angle in the y-direction subtended by the view rectangle in the image plane
209      @param aspect  aspect ratio of the view rectangle in the image plane
210      @return a new {@code Camera} object with the given parameters
211   */
212   public static Camera projPerspective(final double fovy, final double aspect)
213   {
214      final double top    =  Math.tan((Math.PI/180.0)*fovy/2.0);
215      final double bottom = -top;
216      final double right  =  top * aspect;
217      final double left   = -right;
218
219      return projPerspective(left, right, bottom, top);
220   }
221
222
223   /**
224      This is a static factory method.
225      <p>
226      Set up this {@code Camera}'s view volume as a parallel (orthographic)
227      projection of the normalized infinite view parallelepiped extending
228      along the z-axis.
229
230      @return a new {@code Camera} object with the default orthographic parameters
231   */
232   public static Camera projOrtho()
233   {
234      return projOrtho(-1.0, +1.0, -1.0, +1.0); // left, right, bottom, top
235   }
236
237
238   /**
239      This is a static factory method.
240      <p>
241      Set up this {@code Camera}'s view volume as a parallel (orthographic)
242      projection of an infinite view parallelepiped extending along the
243      z-axis.
244
245      @param left    left edge of view rectangle in the xy-plane
246      @param right   right edge of view rectangle in the xy-plane
247      @param bottom  bottom edge of view rectangle in the xy-plane
248      @param top     top edge of view rectangle in the xy-plane
249      @return a new {@code Camera} object with the given parameters
250   */
251   public static Camera projOrtho(final double left,   final double right,
252                                  final double bottom, final double top)
253   {
254      return new Camera(false,
255                        left,
256                        right,
257                        bottom,
258                        top,
259                        +1.0,  // near clipping plane (near = -1.0)
260                        new Vector(0,0,0));  // viewVector
261   }
262
263
264   /**
265      This is a static factory method.
266      <p>
267      Set up this {@code Camera}'s view volume as a symmetric infinite
268      view parallelepiped extending along the z-axis.
269      <p>
270      Here, the view volume is determined by a vertical "field-of-view"
271      angle and an aspect ratio for the view rectangle in the image plane.
272
273      @param fovy    angle in the y-direction subtended by the view rectangle in the image plane
274      @param aspect  aspect ratio of the view rectangle in the image plane
275      @return a new {@code Camera} object with the given parameters
276   */
277   public static Camera projOrtho(final double fovy, final double aspect)
278   {
279      final double top    =  Math.tan((Math.PI/180.0)*fovy/2.0);
280      final double bottom = -top;
281      final double right  =  top * aspect;
282      final double left   = -right;
283
284      return projOrtho(left, right, bottom, top);
285   }
286
287
288   /**
289      Create a new {@code Camera} that is essentially the same as this
290      {@code Camera} but with the given distance from the camera to
291      the near clipping plane.
292      <p>
293      When {@code near} is positive, the near clipping plane is in
294      front of the camera. When {@code near} is negative, the near
295      clipping plane is behind the camera.
296
297      @param near  distance from the new {@code Camera} to its near clipping plane
298      @return a new {@code Camera} object with the given value for near
299   */
300   public Camera changeNear(final double near)
301   {
302      return new Camera(this.perspective,
303                        this.left,
304                        this.right,
305                        this.bottom,
306                        this.top,
307                        -near,
308                        this.viewVector);
309   }
310
311
312   /**
313      Create a new {@code Camera} that is essentially the same as
314      this {@code Camera} but with the given location.
315
316      @param x  translated location, in the x-direction, for the new {@code Camera}
317      @param y  translated location, in the y-direction, for the new {@code Camera}
318      @param z  translated location, in the z-direction, for the new {@code Camera}
319      @return a new {@code Camera} object with the given translated location
320   */
321   public Camera translate(final double x,
322                           final double y,
323                           final double z)
324   {
325      return new Camera(this.perspective,
326                        this.left,
327                        this.right,
328                        this.bottom,
329                        this.top,
330                        this.n,
331                        new Vector(x, y, z));  // viewVector
332   }
333
334
335   /**
336      Get a reference to this {@code Camera}'s view {@link Vector}.
337
338      @return a reference to this {@code Camera}'s {@link Vector} object
339   */
340   public Vector getViewVector()
341   {
342      return this.viewVector;
343   }
344
345
346   /**
347      Get a reference to this {@code Camera}'s normalization {@link Matrix}.
348
349      @return a reference to this {@code Camera}'s normalizing {@link Matrix} object
350   */
351   public Matrix getNormalizeMatrix()
352   {
353      if (perspective)
354      {
355         return PerspectiveNormalizeMatrix.build(left, right, bottom, top);
356      }
357      else
358      {
359         return OrthographicNormalizeMatrix.build(left, right, bottom, top);
360      }
361   }
362
363
364   /**
365      For debugging.
366
367      @return {@link String} representation of this {@code Camera} object
368   */
369   public String toString()
370   {
371      final double fovy = (180./Math.PI) * Math.atan(top)
372                        + (180./Math.PI) * Math.atan(-bottom);
373      final double ratio = (right - left) / (top - bottom);
374      String result = "";
375      result += "Camera: \n";
376      result += "  perspective = " + perspective + "\n";
377      result += "  left = "   + left + ", "
378             +  "  right = "  + right + "\n"
379             +  "  bottom = " + bottom + ", "
380             +  "  top = "    + top + "\n"
381             +  "  near = "   + -n + "\n"
382             +  "  (fovy = " + fovy
383             +  ", aspect ratio = " + String.format("%.2f", ratio) + ")\n"
384             +  "Normalization Matrix\n"
385             +  getNormalizeMatrix() + "\n"
386             +  "Translation Vector\n"
387             +  "  " + viewVector;
388      return result;
389   }
390}