/** @file GCamera.cpp @author Morgan McGuire, http://graphics.cs.williams.edu @author Jeff Marsceill, 08jcm@williams.edu @created 2005-07-20 @edited 2010-02-22 */ #include "G3D/GCamera.h" #include "G3D/platform.h" #include "G3D/Rect2D.h" #include "G3D/BinaryInput.h" #include "G3D/BinaryOutput.h" #include "G3D/Ray.h" #include "G3D/Matrix4.h" #include "G3D/Any.h" #include "G3D/stringutils.h" namespace G3D { GCamera::GCamera(const Any& any) { any.verifyName("GCamera"); any.verifyType(Any::TABLE); *this = GCamera(); const Any::AnyTable& table = any.table(); Any::AnyTable::Iterator it = table.begin(); while (it.hasMore()) { const std::string& k = toUpper(it->key); if (k == "FOVDIRECTION") { const std::string& v = toUpper(it->value); if (v == "HORIZONTAL") { m_direction = HORIZONTAL; } else if (v == "VERTICAL") { m_direction = VERTICAL; } else { any.verify(false, "fovDirection must be \"HORIZONTAL\" or \"VERTICAL\""); } } else if (k == "COORDINATEFRAME") { m_cframe = it->value; } else if (k == "FOVDEGREES") { m_fieldOfView = toRadians(it->value.number()); } else if (k == "NEARPLANEZ") { m_nearPlaneZ = it->value; } else if (k == "FARPLANEZ") { m_farPlaneZ = it->value; } else if (k == "PIXELOFFSET") { m_pixelOffset = it->value; } else { any.verify(false, std::string("Illegal key in table: ") + it->key); } ++it; } } GCamera::operator Any() const { Any any(Any::TABLE, "GCamera"); any.set("fovDirection", std::string((m_direction == HORIZONTAL) ? "HORIZONTAL" : "VERTICAL")); any.set("fovDegrees", toDegrees(m_fieldOfView)); any.set("nearPlaneZ", nearPlaneZ()); any.set("farPlaneZ", farPlaneZ()); any.set("coordinateFrame", coordinateFrame()); any.set("pixelOffset", pixelOffset()); return any; } GCamera::GCamera() { setNearPlaneZ(-0.2f); setFarPlaneZ(-150.0f); setFieldOfView((float)toRadians(90.0f), HORIZONTAL); } GCamera::GCamera(const Matrix4& proj, const CFrame& frame) { float left, right, bottom, top, nearval, farval; proj.getPerspectiveProjectionParameters(left, right, bottom, top, nearval, farval); setNearPlaneZ(-nearval); setFarPlaneZ(-farval); float x = right; // Assume horizontal field of view setFieldOfView(atan2(x, -m_nearPlaneZ) * 2.0f, HORIZONTAL); setCoordinateFrame(frame); } GCamera::~GCamera() { } void GCamera::getCoordinateFrame(CoordinateFrame& c) const { c = m_cframe; } void GCamera::setCoordinateFrame(const CoordinateFrame& c) { m_cframe = c; } void GCamera::setFieldOfView(float angle, FOVDirection dir) { debugAssert((angle < pi()) && (angle > 0)); m_fieldOfView = angle; m_direction = dir; } float GCamera::imagePlaneDepth() const{ return -m_nearPlaneZ; } float GCamera::viewportWidth(const Rect2D& viewport) const { // Compute the side of a square at the near plane based on our field of view float s = 2.0f * -m_nearPlaneZ * tan(m_fieldOfView * 0.5f); if (m_direction == VERTICAL) { s *= viewport.width() / viewport.height(); } return s; } float GCamera::viewportHeight(const Rect2D& viewport) const { // Compute the side of a square at the near plane based on our field of view float s = 2.0f * -m_nearPlaneZ * tan(m_fieldOfView * 0.5f); debugAssert(m_fieldOfView < toRadians(180)); if (m_direction == HORIZONTAL) { s *= viewport.height() / viewport.width(); } return s; } Ray GCamera::worldRay(float x, float y, const Rect2D& viewport) const { int screenWidth = iFloor(viewport.width()); int screenHeight = iFloor(viewport.height()); Vector3 origin = m_cframe.translation; float cx = screenWidth / 2.0f; float cy = screenHeight / 2.0f; float vw = viewportWidth(viewport); float vh = viewportHeight(viewport); Vector3 direction = Vector3( (x - cx) * vw / screenWidth, -(y - cy) * vh / screenHeight, m_nearPlaneZ); direction = m_cframe.vectorToWorldSpace(direction); // Normalize the direction (we didn't do it before) direction = direction.direction(); return Ray::fromOriginAndDirection(origin, direction); } void GCamera::getProjectPixelMatrix(const Rect2D& viewport, Matrix4& P) const { getProjectUnitMatrix(viewport, P); float screenWidth = viewport.width(); float screenHeight = viewport.height(); float sx = screenWidth / 2.0; float sy = screenHeight / 2.0; P = Matrix4(sx, 0, 0, sx + viewport.x0() - m_pixelOffset.x, 0, -sy, 0, sy + viewport.y0() + m_pixelOffset.y, 0, 0, 1, 0, 0, 0, 0, 1) * P; } void GCamera::getProjectUnitMatrix(const Rect2D& viewport, Matrix4& P) const { float screenWidth = viewport.width(); float screenHeight = viewport.height(); float r, l, t, b, n, f, x, y; float s = 1.0f; if (m_direction == VERTICAL) { y = -m_nearPlaneZ * tan(m_fieldOfView / 2); x = y * (screenWidth / screenHeight); s = screenHeight; } else { //m_direction == HORIZONTAL x = -m_nearPlaneZ * tan(m_fieldOfView / 2); y = x * (screenHeight / screenWidth); s = screenWidth; } n = -m_nearPlaneZ; f = -m_farPlaneZ; r = x - m_pixelOffset.x/s; l = -x - m_pixelOffset.x/s; t = y + m_pixelOffset.y/s; b = -y + m_pixelOffset.y/s; P = Matrix4::perspectiveProjection(l, r, b, t, n, f); } Vector3 GCamera::projectUnit(const Vector3& point, const Rect2D& viewport) const { Matrix4 M; getProjectUnitMatrix(viewport, M); Vector4 cameraSpacePoint(coordinateFrame().pointToObjectSpace(point), 1.0f); const Vector4& screenSpacePoint = M * cameraSpacePoint; return Vector3(screenSpacePoint.xyz() / screenSpacePoint.w); } Vector3 GCamera::project(const Vector3& point, const Rect2D& viewport) const { // Find the point in the homogeneous cube const Vector3& cube = projectUnit(point, viewport); return convertFromUnitToNormal(cube, viewport); } Vector3 GCamera::unprojectUnit(const Vector3& v, const Rect2D& viewport) const { const Vector3& projectedPoint = convertFromUnitToNormal(v, viewport); return unproject(projectedPoint, viewport); } Vector3 GCamera::unproject(const Vector3& v, const Rect2D& viewport) const { const float n = m_nearPlaneZ; const float f = m_farPlaneZ; float z; if (-f >= finf()) { // Infinite far plane z = 1.0f / (((-1.0f / n) * v.z) + 1.0f / n); } else { z = 1.0f / ((((1.0f / f) - (1.0f / n)) * v.z) + 1.0f / n); } const Ray& ray = worldRay(v.x - m_pixelOffset.x, v.y - m_pixelOffset.y, viewport); // Find out where the ray reaches the specified depth. const Vector3& out = ray.origin() + ray.direction() * -z / (ray.direction().dot(m_cframe.lookVector())); return out; } float GCamera::worldToScreenSpaceArea(float area, float z, const Rect2D& viewport) const { (void)viewport; if (z >= 0) { return finf(); } return area * (float)square(imagePlaneDepth() / z); } void GCamera::getClipPlanes( const Rect2D& viewport, Array& clip) const { Frustum fr; frustum(viewport, fr); clip.resize(fr.faceArray.size(), DONT_SHRINK_UNDERLYING_ARRAY); for (int f = 0; f < clip.size(); ++f) { clip[f] = fr.faceArray[f].plane; } } GCamera::Frustum GCamera::frustum(const Rect2D& viewport) const { Frustum f; frustum(viewport, f); return f; } void GCamera::frustum(const Rect2D& viewport, Frustum& fr) const { // The volume is the convex hull of the vertices definining the view // frustum and the light source point at infinity. const float x = viewportWidth(viewport) / 2; const float y = viewportHeight(viewport) / 2; const float zn = m_nearPlaneZ; const float zf = m_farPlaneZ; float xx, zz, yy; float halfFOV = m_fieldOfView * 0.5f; // This computes the normal, which is based on the complement of the // halfFOV angle, so the equations are "backwards" if (m_direction == VERTICAL) { yy = -cosf(halfFOV); xx = yy * viewport.height() / viewport.width(); zz = -sinf(halfFOV); } else { xx = -cosf(halfFOV); yy = xx * viewport.width() / viewport.height(); zz = -sinf(halfFOV); } // Near face (ccw from UR) fr.vertexPos.append( Vector4( x, y, zn, 1), Vector4(-x, y, zn, 1), Vector4(-x, -y, zn, 1), Vector4( x, -y, zn, 1)); // Far face (ccw from UR, from origin) if (m_farPlaneZ == -finf()) { fr.vertexPos.append(Vector4( x, y, zn, 0), Vector4(-x, y, zn, 0), Vector4(-x, -y, zn, 0), Vector4( x, -y, zn, 0)); } else { // Finite const float s = zf / zn; fr.vertexPos.append(Vector4( x * s, y * s, zf, 1), Vector4(-x * s, y * s, zf, 1), Vector4(-x * s, -y * s, zf, 1), Vector4( x * s, -y * s, zf, 1)); } Frustum::Face face; // Near plane (wind backwards so normal faces into frustum) // Recall that nearPlane, farPlane are positive numbers, so // we need to negate them to produce actual z values. face.plane = Plane(Vector3(0,0,-1), Vector3(0,0,m_nearPlaneZ)); face.vertexIndex[0] = 3; face.vertexIndex[1] = 2; face.vertexIndex[2] = 1; face.vertexIndex[3] = 0; fr.faceArray.append(face); // Right plane face.plane = Plane(Vector3(xx, 0, zz), Vector3::zero()); face.vertexIndex[0] = 0; face.vertexIndex[1] = 4; face.vertexIndex[2] = 7; face.vertexIndex[3] = 3; fr.faceArray.append(face); // Left plane face.plane = Plane(Vector3(-fr.faceArray.last().plane.normal().x, 0, fr.faceArray.last().plane.normal().z), Vector3::zero()); face.vertexIndex[0] = 5; face.vertexIndex[1] = 1; face.vertexIndex[2] = 2; face.vertexIndex[3] = 6; fr.faceArray.append(face); // Top plane face.plane = Plane(Vector3(0, yy, zz), Vector3::zero()); face.vertexIndex[0] = 1; face.vertexIndex[1] = 5; face.vertexIndex[2] = 4; face.vertexIndex[3] = 0; fr.faceArray.append(face); // Bottom plane face.plane = Plane(Vector3(0, -fr.faceArray.last().plane.normal().y, fr.faceArray.last().plane.normal().z), Vector3::zero()); face.vertexIndex[0] = 2; face.vertexIndex[1] = 3; face.vertexIndex[2] = 7; face.vertexIndex[3] = 6; fr.faceArray.append(face); // Far plane if (-m_farPlaneZ < finf()) { face.plane = Plane(Vector3(0, 0, 1), Vector3(0, 0, m_farPlaneZ)); face.vertexIndex[0] = 4; face.vertexIndex[1] = 5; face.vertexIndex[2] = 6; face.vertexIndex[3] = 7; fr.faceArray.append(face); } // Transform vertices to world space for (int v = 0; v < fr.vertexPos.size(); ++v) { fr.vertexPos[v] = m_cframe.toWorldSpace(fr.vertexPos[v]); } // Transform planes to world space for (int p = 0; p < fr.faceArray.size(); ++p) { // Since there is no scale factor, we don't have to // worry about the inverse transpose of the normal. Vector3 normal; float d; fr.faceArray[p].plane.getEquation(normal, d); Vector3 newNormal = m_cframe.rotation * normal; if (isFinite(d)) { d = (newNormal * -d + m_cframe.translation).dot(newNormal); fr.faceArray[p].plane = Plane(newNormal, newNormal * d); } else { // When d is infinite, we can't multiply 0's by it without // generating NaNs. fr.faceArray[p].plane = Plane::fromEquation(newNormal.x, newNormal.y, newNormal.z, d); } } } void GCamera::getNearViewportCorners (const Rect2D& viewport, Vector3& outUR, Vector3& outUL, Vector3& outLL, Vector3& outLR) const { // Must be kept in sync with getFrustum() const float w = viewportWidth(viewport) / 2.0f; const float h = viewportHeight(viewport) / 2.0f; const float z = nearPlaneZ(); // Compute the points outUR = Vector3( w, h, z); outUL = Vector3(-w, h, z); outLL = Vector3(-w, -h, z); outLR = Vector3( w, -h, z); // Take to world space outUR = m_cframe.pointToWorldSpace(outUR); outUL = m_cframe.pointToWorldSpace(outUL); outLR = m_cframe.pointToWorldSpace(outLR); outLL = m_cframe.pointToWorldSpace(outLL); } void GCamera::getFarViewportCorners( const Rect2D& viewport, Vector3& outUR, Vector3& outUL, Vector3& outLL, Vector3& outLR) const { // Must be kept in sync with getFrustum() const float w = viewportWidth(viewport) * m_farPlaneZ / m_nearPlaneZ; const float h = viewportHeight(viewport) * m_farPlaneZ / m_nearPlaneZ; const float z = m_farPlaneZ; // Compute the points outUR = Vector3( w/2, h/2, z); outUL = Vector3(-w/2, h/2, z); outLL = Vector3(-w/2, -h/2, z); outLR = Vector3( w/2, -h/2, z); // Take to world space outUR = m_cframe.pointToWorldSpace(outUR); outUL = m_cframe.pointToWorldSpace(outUL); outLR = m_cframe.pointToWorldSpace(outLR); outLL = m_cframe.pointToWorldSpace(outLL); } void GCamera::setPosition(const Vector3& t) { m_cframe.translation = t; } void GCamera::lookAt(const Vector3& position, const Vector3& up) { m_cframe.lookAt(position, up); } void GCamera::serialize(BinaryOutput& bo) const { bo.writeFloat32(m_fieldOfView); bo.writeFloat32(imagePlaneDepth()); debugAssert(nearPlaneZ() < 0.0f); bo.writeFloat32(nearPlaneZ()); debugAssert(farPlaneZ() < 0.0f); bo.writeFloat32(farPlaneZ()); m_cframe.serialize(bo); bo.writeInt8(m_direction); m_pixelOffset.serialize(bo); } void GCamera::deserialize(BinaryInput& bi) { m_fieldOfView = bi.readFloat32(); m_nearPlaneZ = bi.readFloat32(); debugAssert(m_nearPlaneZ < 0.0f); m_farPlaneZ = bi.readFloat32(); debugAssert(m_farPlaneZ < 0.0f); m_cframe.deserialize(bi); m_direction = (FOVDirection)bi.readInt8(); m_pixelOffset.deserialize(bi); } Vector3 GCamera::convertFromUnitToNormal(const Vector3& in, const Rect2D& viewport) const{ return (in + Vector3(1,1,1)) * 0.5 * Vector3(viewport.width(), -viewport.height(), 1) + Vector3(viewport.x0(), viewport.y1(), 0); } } // namespace