/* Copyright (c) 2012-2015 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 org.apache.commons.math3.util.FastMath; import se.llbit.chunky.renderer.Refreshable; import se.llbit.chunky.renderer.projection.ApertureProjector; import se.llbit.chunky.renderer.projection.FisheyeProjector; import se.llbit.chunky.renderer.projection.ForwardDisplacementProjector; import se.llbit.chunky.renderer.projection.OmniDirectionalStereoProjector; import se.llbit.chunky.renderer.projection.OmniDirectionalStereoProjector.Eye; import se.llbit.chunky.renderer.projection.PanoramicProjector; import se.llbit.chunky.renderer.projection.PanoramicSlotProjector; import se.llbit.chunky.renderer.projection.ParallelProjector; import se.llbit.chunky.renderer.projection.PinholeProjector; import se.llbit.chunky.renderer.projection.ProjectionMode; import se.llbit.chunky.renderer.projection.Projector; import se.llbit.chunky.renderer.projection.SphericalApertureProjector; import se.llbit.chunky.renderer.projection.StereographicProjector; import se.llbit.chunky.world.Chunk; import se.llbit.chunky.world.entity.PlayerEntity; import se.llbit.json.JsonObject; import se.llbit.log.Log; import se.llbit.math.Matrix3; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector2; import se.llbit.math.Vector3; import se.llbit.util.JsonSerializable; import java.util.Random; /** * Camera model for 3D rendering. * <p> * 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 JsonSerializable { private Runnable directionListener = () -> {}; private Runnable positionListener = () -> {}; private Runnable projectionListener = () -> {}; /** * @param fov Field of view, in degrees. Maximum 180. * @return {@code 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)); } /** * Minimum DoF */ public static final double MIN_DOF = .05; /** * 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; Vector3 pos = new Vector3(0, 0, 0); /** * Scratch vector * NB: protected by synchronized methods (no concurrent modification) */ private final Vector3 u = new Vector3(); /** * Yaw angle. Down = 0, forward = -PI/2, up = -PI. */ private double yaw = -QuickMath.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 Matrix3 transform = new Matrix3(); private final Matrix3 tmpTransform = new Matrix3(); private ProjectionMode projectionMode = ProjectionMode.PINHOLE; private Projector projector = createProjector(); private double dof = Double.POSITIVE_INFINITY; private double fov = projector.getDefaultFoV(); /** * Target location. Position is relative to center of view, normalized by scene height. */ private Vector2 target = new Vector2(0, 0); /** * Maximum diagonal width of the world. Recalculated when world is loaded. */ private double worldWidth = 100; private double subjectDistance = 2; public String name = "camera 1"; /** * 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: Log.errorf("Unknown projection mode: %s, using standard mode", projectionMode); 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)); case STEREOGRAPHIC: return new StereographicProjector(fov); case ODS_LEFT: return new OmniDirectionalStereoProjector(Eye.LEFT); case ODS_RIGHT: return new OmniDirectionalStereoProjector(Eye.RIGHT); } } private void initProjector() { projector = createProjector(); } /** * Set the camera position */ public void setPosition(Vector3 v) { pos.set(v); onViewChange(); } /** * Set depth of field. */ 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 */ public synchronized void setProjectionMode(ProjectionMode mode) { if (projectionMode != mode) { projectionMode = mode; initProjector(); fov = projector.getDefaultFoV(); onViewChange(); } } /** * Set field of view in degrees. */ public synchronized void setFoV(double value) { fov = value; initProjector(); onViewChange(); projectionListener.run(); } /** * @return Current field of view */ public double getFov() { return fov; } /** * Set the subject distance */ public synchronized void setSubjectDistance(double value) { subjectDistance = value; scene.refresh(); } /** * @return Current subject distance */ public double getSubjectDistance() { return subjectDistance; } /** * Move camera forward */ 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); onViewChange(); positionListener.run(); } /** * Move camera backward */ 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); onViewChange(); positionListener.run(); } /** * Move camera up */ public synchronized void moveUp(double v) { u.set(0, 1, 0); pos.scaleAdd(v, u); onViewChange(); positionListener.run(); } /** * Move camera down */ public synchronized void moveDown(double v) { u.set(0, 1, 0); pos.scaleAdd(-v, u); onViewChange(); positionListener.run(); } /** * Strafe camera left */ public synchronized void strafeLeft(double v) { u.set(1, 0, 0); transform.transform(u); pos.scaleAdd(-v, u); onViewChange(); positionListener.run(); } /** * Strafe camera right */ public synchronized void strafeRight(double v) { u.set(1, 0, 0); transform.transform(u); pos.scaleAdd(v, u); onViewChange(); positionListener.run(); } /** * Called when the view is changed (translated or rotated), and * when the projection mode changes, and when the field of view * changes. */ private void onViewChange() { scene.refresh(); target.set(0, 0); } /** * Rotate the camera */ 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 > QuickMath.TAU) { this.yaw -= QuickMath.TAU; } else if (this.yaw < -QuickMath.TAU) { this.yaw += QuickMath.TAU; } updateTransform(); onViewChange(); directionListener.run(); } /** * 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(); onViewChange(); } /** * Update the camera transformation matrix. */ synchronized void updateTransform() { transform.setIdentity(); // Yaw (y axis rotation). tmpTransform.rotY(QuickMath.HALF_PI + yaw); transform.mul(tmpTransform); // Pitch (x axis rotation). tmpTransform.rotX(QuickMath.HALF_PI - pitch); transform.mul(tmpTransform); // Roll (z axis rotation). tmpTransform.rotZ(roll); transform.mul(tmpTransform); } /** * 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.o, ray.d); ray.d.normalize(); // From camera space to world space. transform.transform(ray.d); transform.transform(ray.o); ray.o.add(pos); } /** * Calculate a ray shooting out of the camera based on normalized * image coordinates. * * @param ray result ray * @param x normalized image coordinate [-0.5, 0.5] * @param y normalized image coordinate [-0.5, 0.5] */ public void calcViewRay(Ray ray, double x, double y) { // Reset the ray properties - current material etc. ray.setDefault(); projector.apply(x, y, ray.o, ray.d); ray.d.normalize(); // From camera space to world space. transform.transform(ray.d); transform.transform(ray.o); ray.o.add(pos); } /** * Rotate vector from camera space to world space (does not translate * the vector) * * @param d Vector to rotate */ public void transform(Vector3 d) { transform.transform(d); } /** * @return Current position */ public Vector3 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; } /** * Update the world size. This is the maximum X/Z dimension value. The world size affects * the parallel projector which uses the world size to avoid clipping chunks. * * @param size World size */ public void setWorldSize(double size) { worldWidth = 2 * Math.sqrt(2 * size * size + Chunk.Y_MAX * Chunk.Y_MAX); if (projectionMode == ProjectionMode.PARALLEL) { initProjector(); } } /** * @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("name", name); 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; } public void importFromJson(JsonObject json) { name = json.get("name").stringValue(name); if (json.get("position").isObject()) { pos.fromJson(json.get("position").object()); } JsonObject orientation = json.get("orientation").object(); roll = orientation.get("roll").doubleValue(roll); pitch = orientation.get("pitch").doubleValue(pitch); yaw = orientation.get("yaw").doubleValue(yaw); fov = json.get("fov").doubleValue(fov); subjectDistance = json.get("focalOffset").doubleValue(subjectDistance); projectionMode = ProjectionMode.get( json.get("projectionMode").stringValue(projectionMode.name())); if (json.get("infDof").boolValue(false)) { // The infDof setting is deprecated. dof = Double.POSITIVE_INFINITY; } else { dof = json.get("dof").doubleValue(dof); } initProjector(); updateTransform(); } /** * Move the camera to the player location. */ public void moveToPlayer(PlayerEntity player) { pitch = QuickMath.degToRad(player.pitch - 90); yaw = QuickMath.degToRad(-player.yaw + 90); roll = 0; pos.x = player.position.x; pos.y = player.position.y + 1.6; pos.z = player.position.z; updateTransform(); onViewChange(); } public void setDirectionListener(Runnable directionListener) { this.directionListener = directionListener; } public void setPositionListener(Runnable positionListener) { this.positionListener = positionListener; } public void setProjectionListener(Runnable projectionListener) { this.projectionListener = projectionListener; } /** * Update the argument ray to point toward the current target. */ public void getTargetDirection(Ray ray) { calcViewRay(ray, target.x, target.y); } /** Sets the target position (where autofocus is calculated). */ public void setTarget(double x, double y) { target.set(x, y); } /** * Copy transient state from another camera. */ public void copyTransients(Camera other) { name = other.name; target.set(other.target); } }