/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.render; import com.jaamsim.math.AABB; import com.jaamsim.math.Mat4d; import com.jaamsim.math.MathUtils; import com.jaamsim.math.Plane; import com.jaamsim.math.Sphere; import com.jaamsim.math.Transform; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; /** * In the JaamRenderer, Camera's represent a single viewer of the world. Camera's are responsible for providing * the Projection and View matrices at render time and are capable of view frustum culling against bounding * spheres and AABBs (both in world coords) * @author Matt Chudleigh * */ public class Camera { // Tuning parameters for logarithmic depth buffer // Thanks to this algorithm, we can have a near distance of 0.1mm, and a far distance of around // 10,000km // This uses the algorithm from the following article: // http://outerra.blogspot.ca/2012/11/maximizing-depth-buffer-range-and.html public static final float C = 1.0f; public static final float FC; public static final float far = 100000000f; static { FC = (float)(1.0/Math.log(far*C + 1)); } /** * All the basic camera configuration information is stored in a CameraInfo, this can be copied and passed * back to the app. All other members are renderer owned information */ private CameraInfo _info; private double _aspectRatio; private final Transform invTrans = new Transform(); private Mat4d _projMat; private boolean _projMatDirty = true; /** * An array of 6 Planes that represent the view frustum in world coordinates */ private final Plane[] _frustum; private boolean _frustumDirty = true; { _frustum = new Plane[4]; for (int i = 0; i < _frustum.length; i++) _frustum[i] = new Plane(); } /** * Construct a new camera, the parameters provided are needed to determine the view frustum and the * project matrix. Default camera is the openGL camera, at the origin looking in the -Z direction * @param FOV * @param aspectRatio * @param near * @param far */ public Camera(double FOV, double aspectRatio, double near, double far) { _info = new CameraInfo(FOV, Transform.ident, null); _aspectRatio = aspectRatio; _info.trans.inverse(invTrans); _frustumDirty = true; _projMatDirty = true; } public Camera(CameraInfo camInfo, double aspectRatio) { _info = camInfo; _aspectRatio = aspectRatio; _info.trans.inverse(invTrans); _frustumDirty = true; _projMatDirty = true; } public void getTransform(Transform out) { out.copyFrom(_info.trans); } public Transform getTransformRef() { return _info.trans; } /** * Update the camera position * @param t */ public void setTransform(Transform t) { _info.trans.copyFrom(t); _frustumDirty = true; _info.trans.inverse(invTrans); } /** * Apply this transform to the existing camera transform. * Useful for incremental changes like rotating in place * @param t */ public void applyTransform(Transform t) { _info.trans.merge(t, _info.trans); _frustumDirty = true; _info.trans.inverse(invTrans); } /** * Fills a Matrix4d with the projection matrix for this camera, lookup gluPerspective to understand the math * @param projOut */ private void updateProjMat() { if (_projMat == null) { _projMat = new Mat4d(); } _projMat.zero(); double f = 1/Math.tan(_info.FOV/2); double fx, fy; if(_aspectRatio > 1) { fx = f; fy = f * _aspectRatio; } else { fy = f; fx = f / _aspectRatio; } _projMat.d00 = fx; _projMat.d11 = fy; _projMat.d22 = -1; _projMat.d32 = -1; _projMat.d23 = -2; _projMatDirty = false; } /** * Gets a reference to the projection matrix for this camera * @return */ public Mat4d getProjMat4d() { if (_projMatDirty) { updateProjMat(); } return _projMat; } /** * Get the view matrix for the current position (defined by the transform) * @param viewOut */ public void getViewMat4d(Mat4d viewOut) { invTrans.getMat4d(viewOut); } public void getRotMat4d(Mat4d rotOut) { rotOut.setRot4(invTrans.getRotRef()); } public void getViewTrans(Transform transOut) { transOut.copyFrom(invTrans); } public double getAspectRatio() { return _aspectRatio; } /** * Get the camera FOV in the y direction in radians * @return */ public double getFOV() { return _info.FOV; } public void setAspectRatio(double aspect) { boolean dirty = !MathUtils.near(_aspectRatio, aspect); _frustumDirty = dirty || _frustumDirty; _projMatDirty = dirty || _projMatDirty; _aspectRatio = aspect; } public void setFOV(double FOV) { boolean dirty = !MathUtils.near(_info.FOV, FOV); _frustumDirty = dirty || _frustumDirty; _projMatDirty = dirty || _projMatDirty; _info.FOV = FOV; } /** * Get a direction vector in the direction of camera view (only accurate for the center of the view, as this * is a perspective transform) * @param dirOut */ public void getViewDir(Vec4d dirOut) { _info.trans.apply(new Vec4d(0 ,0, -1, 0), dirOut); } /** * Test frustum collision with sphere s * @param s * @return */ public boolean collides(Sphere s) { updateFrustum(); // The sphere needs to be inside (or touching) all planes to be in the frustum for (Plane p : _frustum) { double dist = s.getDistance(p); if (dist < - s.radius) { return false; } } return true; } public boolean collides(AABB aabb) { if (aabb.isEmpty()) { return false; } updateFrustum(); for (Plane p : _frustum) { // Check if the AABB is completely outside any frustum plane if (aabb.testToPlane(p) == AABB.PlaneTestResult.NEGATIVE) { return false; } } return true; } // Transform the camera by 'camToBounds' then check collision public boolean collides(AABB aabb, Mat4d camToBounds, Mat4d camNormal) { if (aabb.isEmpty()) { return false; } updateFrustum(); Plane boundsPlane = new Plane(); for (Plane p : _frustum) { boundsPlane.transform(camToBounds, camNormal, p); // Check if the AABB is completely outside any frustum plane if (aabb.testToPlane(boundsPlane) == AABB.PlaneTestResult.NEGATIVE) { return false; } } return true; } /** * Update the stored frustum planes to account for the current parameters and transform */ private void updateFrustum() { if (!_frustumDirty) { return; // Already done } double thetaX, thetaY; if (_aspectRatio > 1) { thetaX = _info.FOV/2; thetaY = Math.atan(Math.tan(thetaX) / _aspectRatio); } else { thetaY = _info.FOV/2; thetaX = Math.atan(Math.tan(thetaY) * _aspectRatio); } double sinX = Math.sin(thetaX); double sinY = Math.sin(thetaY); double cosX = Math.cos(thetaX); double cosY = Math.cos(thetaY); Vec3d v = new Vec3d(); // Create the planes that define the frustum, anything on the positive side // of all planes is in the frustum // +Y v.set3(0, cosY, -sinY); _frustum[0].set(v, 0.0d); // -Y v.set3(0, -cosY, -sinY); _frustum[1].set(v, 0.0d); // +X v.set3( cosX, 0, -sinX); _frustum[2].set(v, 0.0d); // -X v.set3(-cosX, 0, -sinX); _frustum[3].set(v, 0.0d); // Apply the current transform to the planes. Puts the planes in world space for (Plane p : _frustum) { p.transform(_info.trans, p); } _frustumDirty = false; } public CameraInfo getInfo() { return new CameraInfo(_info); } CameraInfo getInfoRef() { return _info; } private final Vec3d centerTemp = new Vec3d(); public double distToBounds(AABB bounds) { invTrans.multAndTrans(bounds.center, centerTemp); return centerTemp.z * -1; // In camera space, Z is in the negative direction } /** * Overwrite all core camera state in one go */ public void setInfo(CameraInfo newInfo) { boolean dirty = !_info.isSame(newInfo); _info = new CameraInfo(newInfo); _info.trans.inverse(invTrans); _frustumDirty = dirty || _frustumDirty; _projMatDirty = dirty || _projMatDirty; } } // class Camera