Files
2025-09-18 17:55:52 -04:00

487 lines
14 KiB
C++

/**
Roblox
This is not the G3D 8.0 GCamera, instead it is from the old G3D release modified by Roblox.
@file RbxCamera.cpp
@author Morgan McGuire, matrix@graphics3d.com
@created 2001-04-15
@edited 2006-01-11
*/
#include "RbxG3D/RbxCamera.h"
#include "G3D/platform.h"
#include "G3D/Ray.h"
#include "G3D/Matrix4.h"
namespace RBX {
RbxCamera::RbxCamera() {
nearPlane = 0.1f;
farPlane = (float)inf();
setFieldOfView((float)G3D::toRadians(55.0f));
}
RbxCamera::~RbxCamera() {
}
CoordinateFrame RbxCamera::coordinateFrame() const {
return cframe;
}
void RbxCamera::getCoordinateFrame(CoordinateFrame& c) const {
c = cframe;
}
void RbxCamera::setCoordinateFrame(const CoordinateFrame& c) {
cframe = c;
}
void RbxCamera::setFieldOfView(float angle) {
debugAssert((angle < G3D::pi()) && (angle > 0));
fieldOfView = angle;
// Solve for the corresponding image plane depth, as if the extent
// of the film was 1x1.
imagePlaneDepth = 1.0f / (2.0f * tanf(angle / 2.0f));
}
void RbxCamera::setImagePlaneDepth(
float depth,
const class G3D::Rect2D& viewport) {
debugAssert(depth > 0);
setFieldOfView(2.0f * atanf(viewport.height() / (2.0f * depth)));
}
float RbxCamera::getImagePlaneDepth(
const class G3D::Rect2D& viewport) const {
// The image plane depth has been pre-computed for
// a 1x1 image. Now that the image is width x height,
// we need to scale appropriately.
return imagePlaneDepth * viewport.height();
}
float RbxCamera::getViewportWidth(const G3D::Rect2D& viewport) const {
return getViewportHeight(viewport) * viewport.width() / viewport.height();
}
float RbxCamera::getViewportHeight(const G3D::Rect2D& viewport) const {
(void)viewport;
return nearPlane / imagePlaneDepth;
}
// ROBLOX
RBX::RbxRay RbxCamera::worldRay(
float x,
float y,
const G3D::Rect2D& viewport) const {
int screenWidth = G3D::iFloor(viewport.width());
int screenHeight = G3D::iFloor(viewport.height());
Vector3 origin = cframe.translation;
float cx = screenWidth / 2.0f;
float cy = screenHeight / 2.0f;
Vector3 direction = Vector3( (x - cx),
-(y - cy),
- (getImagePlaneDepth(viewport)));
direction = cframe.vectorToWorldSpace(direction);
// Normalize the direction (we didn't do it before)
direction = direction.direction();
return RBX::RbxRay::fromOriginAndDirection(origin, direction);
}
// ==================
Vector3 RbxCamera::project(
const Vector3& point,
const G3D::Rect2D& viewport) const {
int screenWidth = (int)viewport.width();
int screenHeight = (int)viewport.height();
Vector3 out = cframe.pointToObjectSpace(point);
float w = out.z * (-1.0f);
if (w <= 0) {
// provide at least basic quadrant information.
// (helps with clipping)
return Vector3(((out.x < 0) ? -std::numeric_limits<float>::infinity() : std::numeric_limits<float>::infinity()),
((out.y > 0) ? -std::numeric_limits<float>::infinity() : std::numeric_limits<float>::infinity()),
std::numeric_limits<float>::infinity());
}
debugAssert(w > 0);
// Find where it hits an image plane of these dimensions
float zImagePlane = getImagePlaneDepth(viewport);
// Recover the distance
float rhw = zImagePlane / w;
// Add the image center, flip the y axis
out.x = screenWidth / 2.0f - (rhw * out.x * (-1.0f));
out.y = screenHeight / 2.0f - (rhw * out.y);
out.z = rhw;
return out;
}
Vector3 RbxCamera::inverseProject(
const Vector3& point,
const G3D::Rect2D& viewport) const
{
int screenWidth = (int)viewport.width();
int screenHeight = (int)viewport.height();
float rhw = point.z;
Vector3 p;
// back to [-1..1][-1..1] coords ?
p.x = (screenWidth/2.0f - point.x) / (rhw * (-1.0f) );
p.y = (screenHeight/2.0f - point.y) / (rhw);
float zImagePlane = getImagePlaneDepth(viewport);
float w = zImagePlane / rhw;
p.z = w * (-1.0f);
Vector3 out = cframe.pointToWorldSpace(p);
return out;
}
Matrix4 RbxCamera::projectionMatrix(const class G3D::Rect2D& viewport) const
{
double pixelAspect = viewport.width() / viewport.height();
// Half extents of viewport
double y = -nearPlaneZ() * tan(getFieldOfView() / 2.0);
double x = y * pixelAspect;
double r, l, t, b, n, f;
n = -nearPlaneZ();
f = -farPlaneZ();
r = x;
l = -x;
t = y;
b = -y;
return Matrix4::perspectiveProjection(l, r, b, t, n, f);
}
float RbxCamera::worldToScreenSpaceArea(float area, float z, const G3D::Rect2D& viewport) const {
if (z >= 0) {
return (float)inf();
}
float zImagePlane = getImagePlaneDepth(viewport);
return area * (float)G3D::square(zImagePlane / z);
}
/*
double RbxCamera::getZValue(
double x,
double y,
const class G3D::Rect2D& viewport int width,
int height,
double lineOffset) const {
double depth = renderDevice->getDepthBufferValue((int)x, (int)(height - y));
double n = -nearPlane;
double f = -farPlane;
// Undo the hyperbolic scaling.
// Derivation:
// a = ((1/out) - (1/n)) / ((1/f) - (1/n))
// depth = (1-a) * lineOffset) + (a * 1)
//
// depth = lineOffset + a * (-lineOffset + 1)
// depth = lineOffset + (((1/z) - (1/n)) / ((1/f) - (1/n))) * (1 - lineOffset)
// depth - lineOffset = (((1/z) - (1/n)) / ((1/f) - (1/n))) * (1 - lineOffset)
//
//(depth - lineOffset) / (1 - lineOffset) = (((1/z) - (1/n)) / ((1/f) - (1/n)))
//((1/f) - (1/n)) * (depth - lineOffset) / (1 - lineOffset) = ((1/z) - (1/n))
//(((1/f) - (1/n)) * (depth - lineOffset) / (1 - lineOffset)) + 1/n = (1/z)
//
// z = 1/( (((1/f) - (1/n)) * (depth - lineOffset) / (1 - lineOffset)) + 1/n)
if (f >= inf) {
// Infinite far plane
return 1 / (((-1/n) * (depth - lineOffset) / (1 - lineOffset)) + 1/n);
} else {
return 1 / ((((1/f) - (1/n)) * (depth - lineOffset) / (1 - lineOffset)) + 1/n);
}
}
*/
void RbxCamera::getClipPlanes(
const G3D::Rect2D& viewport,
Array<Plane>& clip) const {
Frustum fr;
frustum(viewport, fr);
clip.resize(fr.faceArray.size(), G3D::DONT_SHRINK_UNDERLYING_ARRAY);
for (int f = 0; f < clip.size(); ++f) {
clip[f] = fr.faceArray[f].plane;
}
/*
clip.resize(0, DONT_SHRINK_UNDERLYING_ARRAY);
double screenWidth = viewport.width();
double screenHeight = viewport.height();
// First construct the planes. Do this in the order of near, left,
// right, bottom, top, far so that early out clipping tests are likely
// to end quickly.
double fovx = screenWidth * fieldOfView / screenHeight;
// Near (recall that nearPlane, farPlane are positive numbers, so
// we need to negate them to produce actual z values.)
clip.append(Plane(Vector3(0,0,-1), Vector3(0,0,-nearPlane)));
// Right
clip.append(Plane(Vector3(-cos(fovx/2), 0, -sin(fovx/2)), Vector3::zero()));
// Left
clip.append(Plane(Vector3(-clip.last().normal().x, 0, clip.last().normal().z), Vector3::zero()));
// Top
clip.append(Plane(Vector3(0, -cos(fieldOfView/2), -sin(fieldOfView/2)), Vector3::zero()));
// Bottom
clip.append(Plane(Vector3(0, -clip.last().normal().y, clip.last().normal().z), Vector3::zero()));
// Far
if (farPlane < inf()) {
clip.append(Plane(Vector3(0, 0, 1), Vector3(0, 0, -farPlane)));
}
// Now transform the planes to world space
for (int p = 0; p < clip.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;
clip[p].getEquation(normal, d);
Vector3 newNormal = cframe.rotation * normal;
if (isFinite(d)) {
d = (newNormal * -d + cframe.translation).dot(newNormal);
clip[p] = Plane(newNormal, newNormal * d);
} else {
// When d is infinite, we can't multiply 0's by it without
// generating NaNs.
clip[p] = Plane::fromEquation(newNormal.x, newNormal.y, newNormal.z, d);
}
}
*/
}
RbxCamera::Frustum RbxCamera::frustum(const G3D::Rect2D& viewport) const {
Frustum f;
frustum(viewport, f);
return f;
}
void RbxCamera::frustum(const G3D::Rect2D& viewport, Frustum& fr) const {
fr.vertexPos.fastClear();
fr.faceArray.fastClear();
// The volume is the convex hull of the vertices definining the view
// frustum and the light source point at infinity.
const float x = getViewportWidth(viewport) / 2;
const float y = getViewportHeight(viewport) / 2;
const float z = nearPlaneZ();
const float w = z / farPlaneZ();
const float fovx = x * fieldOfView / y;
// Near face (ccw from UR)
fr.vertexPos.append(
Vector4( x, y, z, 1),
Vector4(-x, y, z, 1),
Vector4(-x, -y, z, 1),
Vector4( x, -y, z, 1));
// Far face (ccw from UR, from origin)
fr.vertexPos.append(
Vector4( x, y, z, w),
Vector4(-x, y, z, w),
Vector4(-x, -y, z, w),
Vector4( x, -y, z, w));
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,-nearPlane));
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(-cosf(fovx/2), 0, -sinf(fovx/2)), 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, -cosf(fieldOfView/2.0f), -sinf(fieldOfView/2.0f)), 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 (farPlane < inf()) {
face.plane = Plane(Vector3(0, 0, 1), Vector3(0, 0, -farPlane));
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] = 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 = cframe.rotation * normal;
if (G3D::isFinite(d)) {
d = (newNormal * -d + 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);
}
}
}
bool RbxCamera::Frustum::containsPoint(const Vector3& point) const
{
for (int i = 0; i < 6; ++i) { // ignore front, back
const G3D::Plane& plane = faceArray[i].plane;
if (!plane.halfSpaceContains(point)) {
return false;
}
}
return true;
}
bool RbxCamera::Frustum::intersectsSphere(const Vector3& center, float radius) const
{
for (int i = 0; i < 6; ++i) { // ignore front, back
const G3D::Plane& p = faceArray[i].plane;
G3D::Plane offsetplane(p.normal(), p.distance()- radius);
if (!offsetplane.halfSpaceContains(center)) {
return false;
}
}
return true;
}
void RbxCamera::get3DViewportCorners(
const G3D::Rect2D& viewport,
Vector3& outUR,
Vector3& outUL,
Vector3& outLL,
Vector3& outLR) const {
// Must be kept in sync with frustum()
const float sign = (-1.0f);
const float w = -sign * getViewportWidth(viewport) / 2.0f;
const float h = getViewportHeight(viewport) / 2.0f;
const float z = -sign * 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 = cframe.pointToWorldSpace(outUR);
outUL = cframe.pointToWorldSpace(outUL);
outLR = cframe.pointToWorldSpace(outLR);
outLL = cframe.pointToWorldSpace(outLL);
}
void RbxCamera::setPosition(const Vector3& t) {
cframe.translation = t;
}
void RbxCamera::lookAt(const Vector3& position, const Vector3& up) {
cframe.lookAt(position, up);
}
} // namespace