/*
 * Renderer 5. The MIT License.
 * Copyright (c) 2022 rlkraft@pnw.edu
 * See LICENSE for details.
*/

package renderer.pipeline;

import renderer.scene.*;

import java.util.List;
import java.util.ArrayList;

/**
   Transform each {@link Vertex} of a {@link Model} from the
   {@link Camera}'s (shared) view coordinates to normalized
   camera coordinates.
<p>
   This stage transforms the {@link Camera}'s view volume
   from a user defined shape (in the view coordinate system)
   into the standard normalized view volume (in the camera
   coordinate system) used by the {@link Clip} pipeline stage.
<p>
   There are two standard normalized view volumes, one for
   perspective projection and one for orthographic projection.
<p>
   The standard normalized perspective view volume is the infinitely
   long pyramid with its apex at the origin and intersecting the
   image plane {@code z = -1} at the corners {@code (-1, -1, -1)}
   and {@code (+1, +1, -1)}.
<p>
   The standard normalized orthographic view volume is the infinitely
   long parallelepiped centered on the z-axis and intersecting the
   image plane {@code z = -1} at the corners {@code (-1, -1, -1)}
   and {@code (+1, +1, -1)}.
<p>
   The user defined view volume determined by the {@link Scene}'s
   {@link Camera} object is either the infinitely long pyramid with its
   apex at the origin and intersecting the image plane {@code z = -1} at
   the corners {@code (left, bottom, -1)} and {@code (right, top, -1)},
   or it is the infinitely long parallelepiped parallel to the z-axis
   and intersecting the image plane {@code z = -1} at the corners
   {@code (left, bottom, -1)} and {@code (right, top, -1)}.
<p>
   The view coordinate system is relative to the user defined view volume.
<p>
   The normalized camera coordinate system is relative to the normalized
   view volume.
<p>
   The transformation formulas that transform the user defined view volume
   into the normalized view volume also transform the view coordinate
   system into the normalized camera coordinate system.
<p>
   We use two steps to transform the camera's perspective view volume
   into the standard perspective view volume. The first step skews the
   camera's view volume so that its center line is the negative z-axis
   (this takes an asymmetric view volume and makes it symmetric). The
   second step scales the skewed view volume so that it intersects the
   image plane, {@code z = -1}, with corners {@code (-1, -1, -1)} and
   {@code (+1, +1, -1)} (this gives the symmetric view volume a 90 degree
   field-of-view).
<p>
   We also use two steps to transform the camera's orthographic view volume
   into the standard orthographic view volume. The first step translates the
   camera's view volume so that its center line is the negative z-axis (this
   takes an asymmetric view volume and makes it symmetric). The second step
   scales the translated view volume so that it intersects the image plane
   {@code z = -1} with corners {@code (-1, -1, -1)} and {@code (+1, +1, -1)}.
<p>
   Let us derive the formulas for transforming the camera's perspective
   view volume into the standard perspective view volume. Suppose the
   camera's perspective view volume has an asymmetrical cross section
   in the yz-plane that is determined by the top and bottom points
   {@code (t, -1)} and {@code (b, -1)}. The center line of this cross
   section is determined by the point {@code ((t+b)/2, -1)}. We want to
   skew the yz-plane in the y-direction along the z-axis so that the
   field-of-view's center line becomes the z-axis. So we need to solve
   this matrix equation for the value of the skew factor {@code s}.
   <pre>{@code
      [ 1  s ] * [ (t+b)/2 ] = [  0 ]
      [ 0  1 ]   [    -1   ]   [ -1 ]
   }</pre>
   This simplifies to
   <pre>{@code
      (t + b)/2 - s = 0,
      s = (t + b)/2.
   }</pre>
<p>
   A similar calculation can be made for skewing the field-of-view in the
   xz-plane.
<p>
   The following matrix equation skews the camera's view volume along the
   z-axis so that the transformed view volume will be centered on the
   negative z-axis.
   <pre>{@code
     [ 1  0  (r+l)/2 ]   [ x ]   [ x + z * (r + l)/2 ]
     [ 0  1  (t+b)/2 ] * [ y ] = [ y + z * (t + b)/2 ]
     [ 0  0     1    ]   [ z ]   [     z             ]
   }</pre>
<p>
   Once the field-of-view in the yz-plane has been made symmetric with
   respect to the z-axis, we want to scale it in the y-direction so that
   the scaled field-of-view has an angle at the origin of 90 degrees. We
   need to scale the point {@code ((t-b)/2, -1)} to the point {@code (1, -1)}
   (and the point {@code ((b-t)/2, -1)} to the point {@code (-1, -1)}). So
   we need to solve this matrix equation for the value of the scale factor
   {@code s}.
   <pre>{@code
      [ s  0 ] * [ (t-b)/2 ] = [  1 ]
      [ 0  1 ]   [    -1   ]   [ -1 ]
   }</pre>
   This simplifies to
   <pre>{@code
      s * (t - b)/2 = 1,
      s = 2/(t - b).
   }</pre>
<p>
   A similar calculation can be made for scaling the skewed field-of-view
   in the xz-plane.
<p>
   The following matrix equation scales the skewed view volume to be 2 units
   wide and 2 units tall at the image plane {@code z = -1}.
   <pre>{@code
     [ 2/(r-l)     0     0 ]   [ x ]   [ (2 * x)/(r - l) ]
     [    0     2/(t-b)  0 ] * [ y ] = [ (2 * y)/(t - b) ]
     [    0        0     1 ]   [ z ]   [        z        ]
   }</pre>
<p>
   The formulas for transforming the camera's orthographic view volume into
   the standard orthographic view volume (a translation followed by a scale)
   are similar but a bit easier to derive. The derivation is left as an
   exercise for the reader.
*/
public final class View2Camera
{
   /**
      Use the {@link Camera}'s view volume data to transform each
      {@link Vertex} from the {@link Camera}'s view coordinate system
      to the normalized camera coordinate system.

      @param model  {@link Model} with {@link Vertex} objects in the camera's view coordinate system
      @param camera  the {@link Scene}'s {@link Camera} with the view volume data
      @return a new {@link Model} with {@link Vertex} objects in the normalized camera coordinate system
   */
   public static Model view2camera(final Model model, final Camera camera)
   {
      final double l = camera.left; // This data defines the camera's view volume.
      final double r = camera.right;
      final double b = camera.bottom;
      final double t = camera.top;

      // A new vertex list to hold the transformed vertices.
      final List<Vertex> newVertexList =
                            new ArrayList<>(model.vertexList.size());

      // Replace each Vertex object with one that
      // contains normalized camera coordinates.
      for (final Vertex v : model.vertexList)
      {
         double v_x, v_y, v_z;

         if ( camera.perspective )
         {
            // We use two steps to transform the camera's perspective
            // view volume into the standard perspective view volume.
            // The first step skews the current view volume so that
            // its center line is the negative z-axis (this takes an
            // asymmetric view volume and makes it symmetric). The second
            // step scales the skewed view volume so that it intersects the
            // image plane z = -1 with corners (-1, -1, -1) and (+1, +1, -1)
            // (this gives the symmetric view volume a 90 degree field-of-view).

            // For each vertex, skew its coordinates so that the
            // negative z-axis is at the center of the view volume.
            v_z = v.z;
            v_x = v.x + v_z * (r + l)/2.0;
            v_y = v.y + v_z * (t + b)/2.0;

            // For each vertex, scale its coordinates so that the
            // view volume has corners (-1, -1, -1) and (+1, +1, -1).
            v_x = (2.0 * v_x)/(r - l);
            v_y = (2.0 * v_y)/(t - b);
         }
         else
         {
            // We use two steps to transform the camera's orthographic
            // view volume into the standard orthographic view volume.
            // The first step translates the current view volume so that
            // its center line is the z-axis. The second step scales the
            // translated view volume so that it intersects the image plane
            // z = -1 with corners (-1, -1, -1) and (+1, +1, -1).

            // For each vertex, translate its coordinates so that
            // the z-axis is at the center of the view volume.
            v_z = v.z;
            v_x = v.x - (r + l)/2.0;
            v_y = v.y - (t + b)/2.0;

            // For each vertex, scale its coordinates so that the
            // view volume has corners (-1, -1, -1) and (+1, +1, -1).
            v_x = (2.0 * v_x)/(r - l);
            v_y = (2.0 * v_y)/(t - b);
         }

         newVertexList.add( new Vertex(v_x, v_y, v_z) );
      }

      return new Model(newVertexList,
                       model.primitiveList,
                       model.colorList,
                       model.name,
                       model.visible);
   }



   // Private default constructor to enforce noninstantiable class.
   // See Item 4 in "Effective Java", 3rd Ed, Joshua Bloch.
   private View2Camera() {
      throw new AssertionError();
   }
}
