Package se.llbit.chunky.renderer.scene

Source Code of se.llbit.chunky.renderer.scene.Camera$ForwardDisplacementProjector

/* Copyright (c) 2012-2013 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky.  If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.renderer.scene;

import java.util.Random;

import org.apache.commons.math3.util.FastMath;
import org.apache.log4j.Logger;

import se.llbit.chunky.renderer.Refreshable;
import se.llbit.chunky.world.Chunk;
import se.llbit.chunky.world.World;
import se.llbit.json.JsonObject;
import se.llbit.math.Matrix3d;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import se.llbit.math.Vector3d;
import se.llbit.util.JSONifiable;

/**
* Camera model for 3D rendering.
*
* The camera space has x as right vector and z as up vector.
*
* @author Jesper Öqvist <jesper@llbit.se>
* @author TOGoS (projection code)
*/
public class Camera implements JSONifiable {

  private static final Logger logger = Logger.getLogger(Camera.class);

  private static final double HALF_PI = Math.PI/2;
  private static final double TWO_PI = Math.PI*2;

  /**
   * @param fov Field of view, in degrees. Maximum 180.
   * @return tan(fov/2)
   */
  public static double clampedFovTan(double fov) {
    double clampedFoV = Math.max(0, Math.min(180, fov));
    return 2 * FastMath.tan(QuickMath.degToRad(clampedFoV / 2));
  }

  /**
   * Projectors project the view ray using different projection models.
   */
  interface Projector {
    /**
     * @param x pixel X coordinate, where 0 = center and +-0.5 = edges
     * @param y pixel Y coordinate, where 0 = center and +-0.5 = edges
     * @param random Random number stream
     * @param pos will be populated with camera-relative ray origin position
     * @param direction will be populated with camera-relative ray direction
     *            (not necessarily normalized)
     */
    public void apply(double x, double y, Random random, Vector3d pos,
        Vector3d direction);

    public double getMinRecommendedFoV();

    public double getMaxRecommendedFoV();

    public double getDefaultFoV();
  }

  /**
   * Casts parallel rays from different origin points on a plane
   */
  static class ParallelProjector implements Projector {
    protected final double worldWidth;
    protected final double fov;

    public ParallelProjector(double worldWidth, double fov) {
      this.worldWidth = worldWidth;
      this.fov = fov;
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      o.set(fov * x, fov * y, 0);
      d.set(0, 0, 1);
    }

    @Override
    public double getMinRecommendedFoV() {
      return 0.01;
    }

    @Override
    public double getMaxRecommendedFoV() {
      return worldWidth;
    }

    @Override
    public double getDefaultFoV() {
      return worldWidth / 2;
    }
  };

  /**
   * Casts rays like a pinhole camera
   */
  static class PinholeProjector implements Projector {
    protected final double fovTan;

    public PinholeProjector(double fov) {
      this.fovTan = clampedFovTan(fov);
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      o.set(0, 0, 0);
      d.set(fovTan * x, fovTan * y, 1);
    }

    @Override
    public double getMinRecommendedFoV() {
      return 1;
    }

    @Override
    public double getMaxRecommendedFoV() {
      return 175;
    }

    @Override
    public double getDefaultFoV() {
      return 70;
    }
  }

  static class FisheyeProjector implements Projector {
    protected final double fov;

    public FisheyeProjector(double fov) {
      this.fov = fov;
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      double ay = QuickMath.degToRad(y * fov);
      double ax = QuickMath.degToRad(x * fov);
      double avSquared = ay * ay + ax * ax;
      double angleFromCenter = FastMath.sqrt(avSquared);
      double dz = FastMath.cos(angleFromCenter);
      double dv = FastMath.sin(angleFromCenter);
      double dy, dx;
      if (angleFromCenter == 0) {
        dx = dy = 0;
      } else {
        dx = dv * (ax / angleFromCenter);
        dy = dv * (ay / angleFromCenter);
      }
      o.set(0, 0, 0);
      d.set(dx, dy, dz);
    }

    @Override
    public double getMinRecommendedFoV() {
      return 1;
    }

    @Override
    public double getMaxRecommendedFoV() {
      return 180;
    }

    @Override
    public double getDefaultFoV() {
      return 120;
    }
  }

  /**
   * Panoramic equirectangular projector. x is mapped to yaw, y is mapped to
   * pitch.
   */
  static class PanoramicProjector implements Projector {
    protected final double fov;

    public PanoramicProjector(double fov) {
      this.fov = fov;
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      double ay = QuickMath.degToRad(y * fov);
      double ax = QuickMath.degToRad(x * fov);

      double vv = FastMath.cos(ay);

      o.set(0, 0, 0);
      d.set(vv * FastMath.sin(ax), FastMath.sin(ay), vv * FastMath.cos(ax));
    }

    @Override
    public double getMinRecommendedFoV() {
      return 1;
    }

    @Override
    public double getMaxRecommendedFoV() {
      return 180;
    }

    @Override
    public double getDefaultFoV() {
      return 120;
    }
  }

  /**
   * Behaves like a pinhole camera in the vertical direction, but like a
   * spherical one in the horizontal direction.
   */
  static class PanoramicSlotProjector implements Projector {
    protected final double fov;
    protected final double fovTan;

    public PanoramicSlotProjector(double fov) {
      this.fov = fov;
      this.fovTan = clampedFovTan(fov);
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      double ax = QuickMath.degToRad(x * fov);
      double dz = FastMath.cos(ax);
      double dx = FastMath.sin(ax);
      double dy = fovTan * y;

      o.set(0, 0, 0);
      d.set(dx, dy, dz);
    }

    @Override
    public double getMinRecommendedFoV() {
      return 1;
    }

    @Override
    public double getMaxRecommendedFoV() {
      return 90;
    }

    @Override
    public double getDefaultFoV() {
      return 90;
    }
  }

  /**
   * Simulates a non-point aperture to produce a depth-of-field effect.
   * Delegates calculation of base offset/direction to another projector. If
   * apertureSize is 0 this will still work, but it will not have any effect.
   * In that case you should use the wrapped Projector directly.
   */
  static class ApertureProjector implements Projector {
    protected final Projector wrapped;
    protected final double aperture;
    protected final double subjectDistance;

    public ApertureProjector(Projector wrapped, double apertureSize,
        double subjectDistance) {
      this.wrapped = wrapped;
      this.aperture = apertureSize;
      this.subjectDistance = subjectDistance;
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      wrapped.apply(x, y, random, o, d);

      d.scale(subjectDistance/d.z);

      // find random point in aperture
      double rx, ry;
      while (true) {
        rx = 2 * random.nextDouble() - 1;
        ry = 2 * random.nextDouble() - 1;
        double s = rx * rx + ry * ry;
        if (s > Ray.EPSILON && s <= 1) {
          rx *= aperture;
          ry *= aperture;
          break;
        }
      }

      d.sub(rx, ry, 0);
      o.add(rx, ry, 0);
    }

    @Override
    public double getMinRecommendedFoV() {
      return wrapped.getMinRecommendedFoV();
    }

    @Override
    public double getMaxRecommendedFoV() {
      return wrapped.getMaxRecommendedFoV();
    }

    @Override
    public double getDefaultFoV() {
      return wrapped.getDefaultFoV();
    }
  }

  /**
   * A projector for spherical depth of field.
   */
  static class SphericalApertureProjector extends ApertureProjector {
    public SphericalApertureProjector(Projector wrapped, double apertureSize,
        double subjectDistance) {
      super(wrapped, apertureSize, subjectDistance);
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      wrapped.apply(x, y, random, o, d);

      d.scale(subjectDistance);

      // find random point in aperture
      double rx, ry;
      while (true) {
        rx = 2 * random.nextDouble() - 1;
        ry = 2 * random.nextDouble() - 1;
        double s = rx * rx + ry * ry;
        if (s > Ray.EPSILON && s <= 1) {
          rx *= aperture;
          ry *= aperture;
          break;
        }
      }

      d.sub(rx, ry, 0);
      o.add(rx, ry, 0);
    }
  }

  /**
   * Moves the ray origin forward (if displacement is positive) along the
   * direction vector.
   */
  static class ForwardDisplacementProjector implements Projector {
    protected final Projector wrapped;
    protected final double displacementValue;
    protected final double displacementSign;

    public ForwardDisplacementProjector(Projector wrapped,
        double displacement) {
      this.wrapped = wrapped;
      this.displacementValue = QuickMath.abs(displacement);
      this.displacementSign = QuickMath.signum(displacement);
    }

    @Override
    public void apply(double x, double y, Random random, Vector3d o,
        Vector3d d) {
      wrapped.apply(x, y, random, o, d);

      d.normalize();
      d.scale(displacementValue);
      o.scaleAdd(displacementSign, d, o);
    }

    @Override
    public double getMinRecommendedFoV() {
      return wrapped.getMinRecommendedFoV();
    }

    @Override
    public double getMaxRecommendedFoV() {
      return wrapped.getMaxRecommendedFoV();
    }

    @Override
    public double getDefaultFoV() {
      return wrapped.getDefaultFoV();
    }
  }

  /**
   * Minimum DoF
   */
  public static final double MIN_DOF = .5;

  /**
   * Maximum DoF
   */
  public static final double MAX_DOF = 5000;

  /**
   * Minimum recommended subject distance
   */
  public static final double MIN_SUBJECT_DISTANCE = 0.01;

  /**
   * Maximum recommended subject distance
   */
  public static final double MAX_SUBJECT_DISTANCE = 1000;

  private final Refreshable scene;

  Vector3d pos = new Vector3d(0, 0, 0);

  /**
   * Scratch vector
   * NB: protected by synchronized methods (no concurrent modification)
   */
  private final Vector3d u = new Vector3d();

  /**
   * Yaw angle. Down = 0, forward = -PI/2, up = -PI.
   */
  private double yaw = - HALF_PI;

  /**
   * Pitch angle. Pitch = 0 corresponds to the camera pointing along the z axis,
   * pitch = PI/2 corresponds to the negative x axis, etc.
   */
  private double pitch = 0;

  /**
   * Camera roll.
   */
  private double roll = 0;

  /**
   * Transform to rotate from camera space to world space (not including
   * translation).
   */
  private final Matrix3d transform = new Matrix3d();

  private final Matrix3d tmpTransform = new Matrix3d();

  private ProjectionMode projectionMode = ProjectionMode.PINHOLE;
  private Projector projector = createProjector();

  private double dof = Double.POSITIVE_INFINITY;
  private double fov = projector.getDefaultFoV();

  /**
   * Maximum diagonal width of the world
   */
  private double worldWidth = 100;

  private double subjectDistance = 2;

  /**
   * Create a new camera
   * @param sceneDescription The scene which the camera should be attached to
   */
  public Camera(Refreshable sceneDescription) {
    this.scene = sceneDescription;
    transform.setIdentity();
    initProjector();
    updateTransform();
  }

  /**
   * Copy camera configuration from another camera
   * @param other the camera to copy configuration from
   */
  public void set(Camera other) {
    pos.set(other.pos);
    yaw = other.yaw;
    pitch = other.pitch;
    roll = other.roll;
    dof = other.dof;
    projectionMode = other.projectionMode;
    fov = other.fov;
    subjectDistance = other.subjectDistance;
    worldWidth = other.worldWidth;
    initProjector();
    updateTransform();
  }

  private Projector applyDoF(Projector p, double subjectDistance) {
    return infiniteDoF() ? p : new ApertureProjector(p,
        subjectDistance/dof, subjectDistance);
  }

  private Projector applySphericalDoF(Projector p) {
    return infiniteDoF() ? p : new SphericalApertureProjector(p,
        subjectDistance/dof, subjectDistance);
  }

  /**
   * Creates, but does not otherwise use, a projector object
   * based on the current camera settings.
   */
  private Projector createProjector() {
    switch (projectionMode) {
    default:
      logger.error("Unknown projection mode: "
          + projectionMode + ", using standard mode");
    case PINHOLE:
      return applyDoF(new PinholeProjector(fov), subjectDistance);
    case PARALLEL:
      return applyDoF(new ForwardDisplacementProjector(
          new ParallelProjector(worldWidth, fov),
          -worldWidth), subjectDistance+worldWidth);
    case FISHEYE:
      return applySphericalDoF(new FisheyeProjector(fov));
    case PANORAMIC_SLOT:
      return applySphericalDoF(new PanoramicSlotProjector(fov));
    case PANORAMIC:
      return applySphericalDoF(new PanoramicProjector(fov));
    }
  }

  private void initProjector() {
    projector = createProjector();
  }

  /**
   * Set the camera position
   * @param v
   */
  public void setPosition(Vector3d v) {
    pos.set(v);
    scene.refresh();
  }

  /**
   * Set depth of field.
   *
   * @param value
   */
  public synchronized void setDof(double value) {
    if (dof != value) {
      dof = value;
      scene.refresh();
    }
  }

  /**
   * @return Current Depth of Field
   */
  public double getDof() {
    return dof;
  }

  /**
   * @return <code>true</code> if infinite DoF is active
   */
  public boolean infiniteDoF() {
    return dof == Double.POSITIVE_INFINITY;
  }

  /**
   * @return the projection mode
   */
  public ProjectionMode getProjectionMode() {
    return projectionMode;
  }

  /**
   * Set the projection mode
   * @param mode
   */
  public synchronized void setProjectionMode(ProjectionMode mode) {
    if (projectionMode != mode) {
      projectionMode = mode;
      initProjector();
      fov = projector.getDefaultFoV();
      scene.refresh();
    }
  }

  /**
   * Set field of view in degrees.
   *
   * @param value
   */
  public synchronized void setFoV(double value) {
    fov = value;
    initProjector();
    scene.refresh();
  }

  /**
   * @return Current field of view
   */
  public double getFoV() {
    return fov;
  }

  /**
   * Set the subject distance
   * @param value
   */
  public synchronized void setSubjectDistance(double value) {
    subjectDistance = value;
    scene.refresh();
  }

  /**
   * @return Current subject distance
   */
  public double getSubjectDistance() {
    return subjectDistance;
  }

  /**
   * Move camera forward
   * @param v
   */
  public synchronized void moveForward(double v) {
    if (projectionMode != ProjectionMode.PARALLEL) {
      u.set(0, 0, 1);
    } else {
      u.set(0, -1, 0);
    }
    transform.transform(u);
    pos.scaleAdd(v, u, pos);
    scene.refresh();
  }

  /**
   * Move camera backward
   * @param v
   */
  public synchronized void moveBackward(double v) {
    if (projectionMode != ProjectionMode.PARALLEL) {
      u.set(0, 0, 1);
    } else {
      u.set(0, -1, 0);
    }
    transform.transform(u);
    pos.scaleAdd(-v, u, pos);
    scene.refresh();
  }

  /**
   * Move camera up
   * @param v
   */
  public synchronized void moveUp(double v) {
    u.set(0, 1, 0);
    pos.scaleAdd(v, u, pos);
    scene.refresh();
  }

  /**
   * Move camera down
   * @param v
   */
  public synchronized void moveDown(double v) {
    u.set(0, 1, 0);
    pos.scaleAdd(-v, u, pos);
    scene.refresh();
  }

  /**
   * Strafe camera left
   * @param v
   */
  public synchronized void strafeLeft(double v) {
    u.set(1, 0, 0);
    transform.transform(u);
    pos.scaleAdd(-v, u, pos);
    scene.refresh();
  }

  /**
   * Strafe camera right
   * @param v
   */
  public synchronized void strafeRight(double v) {
    u.set(1, 0, 0);
    transform.transform(u);
    pos.scaleAdd(v, u, pos);
    scene.refresh();
  }

  /**
   * Rotate the camera
   * @param yaw
   * @param pitch
   */
  public synchronized void rotateView(double yaw, double pitch) {
    double fovRad = QuickMath.degToRad(fov / 2);
    this.yaw += yaw * fovRad;
    this.pitch += pitch * fovRad;

    this.pitch = QuickMath.min(0, this.pitch);
    this.pitch = QuickMath.max(-Math.PI, this.pitch);

    if (this.yaw > TWO_PI) {
      this.yaw -= TWO_PI;
    } else if (this.yaw < -TWO_PI) {
      this.yaw += TWO_PI;
    }

    updateTransform();
  }

  /**
   * Set the view direction
   * @param yaw Yaw in radians
   * @param pitch Pitch in radians
   * @param roll Roll in radians
   */
  public synchronized void setView(double yaw, double pitch, double roll) {
    this.yaw = yaw;
    this.pitch = pitch;
    this.roll = roll;

    updateTransform();
  }

  /**
   * Update the camera transformation matrix.
   */
  synchronized void updateTransform() {
    transform.setIdentity();

    // yaw (y axis rotation)
    tmpTransform.rotY(HALF_PI + yaw);
    transform.mul(tmpTransform);

    // pitch (x axis rotation)
    tmpTransform.rotX(HALF_PI - pitch);
    transform.mul(tmpTransform);

    // roll (z axis rotation)
    tmpTransform.rotZ(roll);
    transform.mul(tmpTransform);

    scene.refresh();
  }

  /**
   * Attempt to move the camera to the player position.
   * @param world
   */
  public void moveToPlayer(World world) {
    if (world != null) {
      Vector3d playerPos = world.playerPos();
      if (playerPos != null) {
        pitch = HALF_PI * ( (world.playerPitch() / 90) - 1);
        yaw = HALF_PI * ( -(world.playerYaw() / 90) + 1);
        roll = 0;
        pos.x = playerPos.x;
        pos.y = playerPos.y + 1.6;
        pos.z = playerPos.z;
        updateTransform();
        scene.refresh();
      }
    }
  }

  /**
   * Calculate a ray shooting out of the camera based on normalized
   * image coordinates.
   * @param ray result ray
   * @param random random number stream
   * @param x normalized image coordinate [-0.5, 0.5]
   * @param y normalized image coordinate [-0.5, 0.5]
   */
  public void calcViewRay(Ray ray, Random random, double x,
      double y) {

    // reset the ray properties - current material etc.
    ray.setDefault();

    projector.apply(x, y, random, ray.x, ray.d);

    ray.d.normalize();

    // from camera space to world space
    transform.transform(ray.d);
    transform.transform(ray.x);
    ray.x.add(pos);
  }

  /**
   * Rotate vector from camera space to world space (does not translate
   * the vector)
   * @param d Vector to rotate
   */
  public void transform(Vector3d d) {
    transform.transform(d);
  }

  /**
   * @return Current position
   */
  public Vector3d getPosition() {
    return pos;
  }

  /**
   * @return The current yaw angle
   */
  public double getYaw() {
    return yaw;
  }

  /**
   * @return The current pitch angle
   */
  public double getPitch() {
    return pitch;
  }

  /**
   * @return The current roll angle
   */
  public double getRoll() {
    return roll;
  }

  /**
   * @param size World size
   */
  public void setWorldSize(double size) {
    worldWidth = 2*Math.sqrt(2*size*size + Chunk.Y_MAX*Chunk.Y_MAX);
  }

  /**
   * @return Minimum FoV value, depending on projection
   */
  public double getMinFoV() {
    return projector.getMinRecommendedFoV();
  }

  /**
   * @return Maximum FoV value, depending on projection
   */
  public double getMaxFoV() {
    return projector.getMaxRecommendedFoV();
  }

  @Override
  public JsonObject toJson() {
    JsonObject camera = new JsonObject();

    camera.add("position", pos.toJson());

    JsonObject orientation = new JsonObject();
    orientation.add("roll", roll);
    orientation.add("pitch", pitch);
    orientation.add("yaw", yaw);
    camera.add("orientation", orientation);

    camera.add("projectionMode", projectionMode.name());
    camera.add("fov", fov);
    if (dof == Double.POSITIVE_INFINITY) {
      camera.add("dof", "Infinity");
    } else {
      camera.add("dof", dof);
    }
    camera.add("focalOffset", subjectDistance);
    return camera;
  }

  @Override
  public void fromJson(JsonObject obj) {
    pos.fromJson(obj.get("position").object());

    JsonObject orientation = obj.get("orientation").object();
    roll = orientation.get("roll").doubleValue(0);
    pitch = orientation.get("pitch").doubleValue(0);
    yaw = orientation.get("yaw").doubleValue(- HALF_PI);

    fov = obj.get("fov").doubleValue(0);
    subjectDistance = obj.get("focalOffset").doubleValue(0);
    try {
      projectionMode = ProjectionMode.valueOf(
          obj.get("projectionMode").stringValue(""));
    } catch (IllegalArgumentException e) {
      projectionMode = ProjectionMode.PINHOLE;
    }
    if (obj.get("infDof").boolValue(false)) {
      // legacy
      dof = Double.POSITIVE_INFINITY;
    } else {
      dof = obj.get("dof").doubleValue(Double.POSITIVE_INFINITY);
    }
    initProjector();
    updateTransform();
  }
}
TOP

Related Classes of se.llbit.chunky.renderer.scene.Camera$ForwardDisplacementProjector

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.