Index: ps/trunk/source/graphics/Camera.cpp
===================================================================
--- ps/trunk/source/graphics/Camera.cpp (revision 25439)
+++ ps/trunk/source/graphics/Camera.cpp (revision 25440)
@@ -1,433 +1,437 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
/*
* CCamera holds a view and a projection matrix. It also has a frustum
* which can be used to cull objects for rendering.
*/
#include "precompiled.h"
#include "Camera.h"
#include "graphics/HFTracer.h"
#include "graphics/Terrain.h"
#include "lib/ogl.h"
#include "maths/MathUtil.h"
#include "maths/Vector4D.h"
#include "ps/Game.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/WaterManager.h"
CCamera::CCamera()
{
// Set viewport to something anything should handle, but should be initialised
// to window size before use.
m_ViewPort.m_X = 0;
m_ViewPort.m_Y = 0;
m_ViewPort.m_Width = 800;
m_ViewPort.m_Height = 600;
}
CCamera::~CCamera() = default;
void CCamera::SetProjection(const CMatrix3D& matrix)
{
m_ProjType = ProjectionType::CUSTOM;
m_ProjMat = matrix;
}
void CCamera::SetProjectionFromCamera(const CCamera& camera)
{
m_ProjType = camera.m_ProjType;
m_NearPlane = camera.m_NearPlane;
m_FarPlane = camera.m_FarPlane;
if (m_ProjType == ProjectionType::PERSPECTIVE)
{
m_FOV = camera.m_FOV;
}
else if (m_ProjType == ProjectionType::ORTHO)
{
m_OrthoScale = camera.m_OrthoScale;
}
m_ProjMat = camera.m_ProjMat;
}
void CCamera::SetOrthoProjection(float nearp, float farp, float scale)
{
m_ProjType = ProjectionType::ORTHO;
m_NearPlane = nearp;
m_FarPlane = farp;
m_OrthoScale = scale;
const float halfHeight = 0.5f * m_OrthoScale;
const float halfWidth = halfHeight * GetAspectRatio();
m_ProjMat.SetOrtho(-halfWidth, halfWidth, -halfHeight, halfHeight, m_NearPlane, m_FarPlane);
}
void CCamera::SetPerspectiveProjection(float nearp, float farp, float fov)
{
m_ProjType = ProjectionType::PERSPECTIVE;
m_NearPlane = nearp;
m_FarPlane = farp;
m_FOV = fov;
m_ProjMat.SetPerspective(m_FOV, GetAspectRatio(), m_NearPlane, m_FarPlane);
}
// Updates the frustum planes. Should be called
// everytime the view or projection matrices are
// altered.
void CCamera::UpdateFrustum(const CBoundingBoxAligned& scissor)
{
CMatrix3D MatFinal;
CMatrix3D MatView;
m_Orientation.GetInverse(MatView);
MatFinal = m_ProjMat * MatView;
m_ViewFrustum.SetNumPlanes(6);
// get the RIGHT plane
m_ViewFrustum[0].m_Norm.X = scissor[1].X*MatFinal._41 - MatFinal._11;
m_ViewFrustum[0].m_Norm.Y = scissor[1].X*MatFinal._42 - MatFinal._12;
m_ViewFrustum[0].m_Norm.Z = scissor[1].X*MatFinal._43 - MatFinal._13;
m_ViewFrustum[0].m_Dist = scissor[1].X*MatFinal._44 - MatFinal._14;
// get the LEFT plane
m_ViewFrustum[1].m_Norm.X = -scissor[0].X*MatFinal._41 + MatFinal._11;
m_ViewFrustum[1].m_Norm.Y = -scissor[0].X*MatFinal._42 + MatFinal._12;
m_ViewFrustum[1].m_Norm.Z = -scissor[0].X*MatFinal._43 + MatFinal._13;
m_ViewFrustum[1].m_Dist = -scissor[0].X*MatFinal._44 + MatFinal._14;
// get the BOTTOM plane
m_ViewFrustum[2].m_Norm.X = -scissor[0].Y*MatFinal._41 + MatFinal._21;
m_ViewFrustum[2].m_Norm.Y = -scissor[0].Y*MatFinal._42 + MatFinal._22;
m_ViewFrustum[2].m_Norm.Z = -scissor[0].Y*MatFinal._43 + MatFinal._23;
m_ViewFrustum[2].m_Dist = -scissor[0].Y*MatFinal._44 + MatFinal._24;
// get the TOP plane
m_ViewFrustum[3].m_Norm.X = scissor[1].Y*MatFinal._41 - MatFinal._21;
m_ViewFrustum[3].m_Norm.Y = scissor[1].Y*MatFinal._42 - MatFinal._22;
m_ViewFrustum[3].m_Norm.Z = scissor[1].Y*MatFinal._43 - MatFinal._23;
m_ViewFrustum[3].m_Dist = scissor[1].Y*MatFinal._44 - MatFinal._24;
// get the FAR plane
m_ViewFrustum[4].m_Norm.X = scissor[1].Z*MatFinal._41 - MatFinal._31;
m_ViewFrustum[4].m_Norm.Y = scissor[1].Z*MatFinal._42 - MatFinal._32;
m_ViewFrustum[4].m_Norm.Z = scissor[1].Z*MatFinal._43 - MatFinal._33;
m_ViewFrustum[4].m_Dist = scissor[1].Z*MatFinal._44 - MatFinal._34;
// get the NEAR plane
m_ViewFrustum[5].m_Norm.X = -scissor[0].Z*MatFinal._41 + MatFinal._31;
m_ViewFrustum[5].m_Norm.Y = -scissor[0].Z*MatFinal._42 + MatFinal._32;
m_ViewFrustum[5].m_Norm.Z = -scissor[0].Z*MatFinal._43 + MatFinal._33;
m_ViewFrustum[5].m_Dist = -scissor[0].Z*MatFinal._44 + MatFinal._34;
for (size_t i = 0; i < 6; ++i)
m_ViewFrustum[i].Normalize();
}
void CCamera::ClipFrustum(const CPlane& clipPlane)
{
CPlane normClipPlane = clipPlane;
normClipPlane.Normalize();
m_ViewFrustum.AddPlane(normClipPlane);
}
void CCamera::SetViewPort(const SViewPort& viewport)
{
m_ViewPort.m_X = viewport.m_X;
m_ViewPort.m_Y = viewport.m_Y;
m_ViewPort.m_Width = viewport.m_Width;
m_ViewPort.m_Height = viewport.m_Height;
}
float CCamera::GetAspectRatio() const
{
return static_cast(m_ViewPort.m_Width) / static_cast(m_ViewPort.m_Height);
}
void CCamera::GetViewQuad(float dist, Quad& quad) const
{
ENSURE(m_ProjType == ProjectionType::PERSPECTIVE || m_ProjType == ProjectionType::ORTHO);
const float y = m_ProjType == ProjectionType::PERSPECTIVE ? dist * tanf(m_FOV * 0.5f) : m_OrthoScale * 0.5f;
const float x = y * GetAspectRatio();
quad[0].X = -x;
quad[0].Y = -y;
quad[0].Z = dist;
quad[1].X = x;
quad[1].Y = -y;
quad[1].Z = dist;
quad[2].X = x;
quad[2].Y = y;
quad[2].Z = dist;
quad[3].X = -x;
quad[3].Y = y;
quad[3].Z = dist;
}
void CCamera::BuildCameraRay(int px, int py, CVector3D& origin, CVector3D& dir) const
{
ENSURE(m_ProjType == ProjectionType::PERSPECTIVE || m_ProjType == ProjectionType::ORTHO);
// Coordinates relative to the camera plane.
const float dx = static_cast(px) / m_ViewPort.m_Width;
const float dy = 1.0f - static_cast(py) / m_ViewPort.m_Height;
Quad points;
GetViewQuad(m_FarPlane, points);
// Transform from camera space to world space.
for (CVector3D& point : points)
point = m_Orientation.Transform(point);
// Get world space position of mouse point at the far clipping plane.
const CVector3D basisX = points[1] - points[0];
const CVector3D basisY = points[3] - points[0];
if (m_ProjType == ProjectionType::PERSPECTIVE)
{
// Build direction for the camera origin to the target point.
origin = m_Orientation.GetTranslation();
CVector3D targetPoint = points[0] + (basisX * dx) + (basisY * dy);
dir = targetPoint - origin;
}
else if (m_ProjType == ProjectionType::ORTHO)
{
origin = m_Orientation.GetTranslation() + (basisX * (dx - 0.5f)) + (basisY * (dy - 0.5f));
dir = m_Orientation.GetIn();
}
dir.Normalize();
}
void CCamera::GetScreenCoordinates(const CVector3D& world, float& x, float& y) const
{
CMatrix3D transform = m_ProjMat * m_Orientation.GetInverse();
CVector4D screenspace = transform.Transform(CVector4D(world.X, world.Y, world.Z, 1.0f));
x = screenspace.X / screenspace.W;
y = screenspace.Y / screenspace.W;
x = (x + 1) * 0.5f * m_ViewPort.m_Width;
y = (1 - y) * 0.5f * m_ViewPort.m_Height;
}
CVector3D CCamera::GetWorldCoordinates(int px, int py, bool aboveWater) const
{
CHFTracer tracer(g_Game->GetWorld()->GetTerrain());
int x, z;
CVector3D origin, dir, delta, terrainPoint, waterPoint;
BuildCameraRay(px, py, origin, dir);
bool gotTerrain = tracer.RayIntersect(origin, dir, x, z, terrainPoint);
if (!aboveWater)
{
if (gotTerrain)
return terrainPoint;
// Off the edge of the world?
// Work out where it /would/ hit, if the map were extended out to infinity with average height.
return GetWorldCoordinates(px, py, 50.0f);
}
CPlane plane;
plane.Set(CVector3D(0.f, 1.f, 0.f), // upwards normal
CVector3D(0.f, g_Renderer.GetWaterManager()->m_WaterHeight, 0.f)); // passes through water plane
bool gotWater = plane.FindRayIntersection( origin, dir, &waterPoint );
// Clamp the water intersection to within the map's bounds, so that
// we'll always return a valid position on the map
ssize_t mapSize = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
if (gotWater)
{
waterPoint.X = Clamp(waterPoint.X, 0.f, (mapSize - 1) * TERRAIN_TILE_SIZE);
waterPoint.Z = Clamp(waterPoint.Z, 0.f, (mapSize - 1) * TERRAIN_TILE_SIZE);
}
if (gotTerrain)
{
if (gotWater)
{
// Intersecting both heightmap and water plane; choose the closest of those
if ((origin - terrainPoint).LengthSquared() < (origin - waterPoint).LengthSquared())
return terrainPoint;
else
return waterPoint;
}
else
{
// Intersecting heightmap but parallel to water plane
return terrainPoint;
}
}
else
{
if (gotWater)
{
// Only intersecting water plane
return waterPoint;
}
else
{
// Not intersecting terrain or water; just return 0,0,0.
return CVector3D(0.f, 0.f, 0.f);
}
}
}
CVector3D CCamera::GetWorldCoordinates(int px, int py, float h) const
{
CPlane plane;
plane.Set(CVector3D(0.f, 1.f, 0.f), CVector3D(0.f, h, 0.f)); // upwards normal, passes through h
CVector3D origin, dir, delta, currentTarget;
BuildCameraRay(px, py, origin, dir);
if (plane.FindRayIntersection(origin, dir, ¤tTarget))
return currentTarget;
// No intersection with the infinite plane - nothing sensible can be returned,
// so just choose an arbitrary point on the plane
return CVector3D(0.f, h, 0.f);
}
CVector3D CCamera::GetFocus() const
{
// Basically the same as GetWorldCoordinates
CHFTracer tracer(g_Game->GetWorld()->GetTerrain());
int x, z;
CVector3D origin, dir, delta, terrainPoint, waterPoint;
origin = m_Orientation.GetTranslation();
dir = m_Orientation.GetIn();
bool gotTerrain = tracer.RayIntersect(origin, dir, x, z, terrainPoint);
CPlane plane;
plane.Set(CVector3D(0.f, 1.f, 0.f), // upwards normal
CVector3D(0.f, g_Renderer.GetWaterManager()->m_WaterHeight, 0.f)); // passes through water plane
bool gotWater = plane.FindRayIntersection( origin, dir, &waterPoint );
// Clamp the water intersection to within the map's bounds, so that
// we'll always return a valid position on the map
ssize_t mapSize = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
if (gotWater)
{
waterPoint.X = Clamp(waterPoint.X, 0.f, (mapSize - 1) * TERRAIN_TILE_SIZE);
waterPoint.Z = Clamp(waterPoint.Z, 0.f, (mapSize - 1) * TERRAIN_TILE_SIZE);
}
if (gotTerrain)
{
if (gotWater)
{
// Intersecting both heightmap and water plane; choose the closest of those
if ((origin - terrainPoint).LengthSquared() < (origin - waterPoint).LengthSquared())
return terrainPoint;
else
return waterPoint;
}
else
{
// Intersecting heightmap but parallel to water plane
return terrainPoint;
}
}
else
{
if (gotWater)
{
// Only intersecting water plane
return waterPoint;
}
else
{
// Not intersecting terrain or water; just return 0,0,0.
return CVector3D(0.f, 0.f, 0.f);
}
}
}
CBoundingBoxAligned CCamera::GetBoundsInViewPort(const CBoundingBoxAligned& boundigBox) const
{
- CVector4D v1 = GetViewProjection().Transform(CVector4D(boundigBox[0].X, boundigBox[1].Y, boundigBox[0].Z, 1.0f));
- CVector4D v2 = GetViewProjection().Transform(CVector4D(boundigBox[1].X, boundigBox[1].Y, boundigBox[0].Z, 1.0f));
- CVector4D v3 = GetViewProjection().Transform(CVector4D(boundigBox[0].X, boundigBox[1].Y, boundigBox[1].Z, 1.0f));
- CVector4D v4 = GetViewProjection().Transform(CVector4D(boundigBox[1].X, boundigBox[1].Y, boundigBox[1].Z, 1.0f));
+ const CVector3D cameraPosition = GetOrientation().GetTranslation();
+ if (boundigBox.IsPointInside(cameraPosition))
+ return CBoundingBoxAligned(CVector3D(-1.0f, -1.0f, 0.0f), CVector3D(1.0f, 1.0f, 0.0f));
+
+ const CMatrix3D viewProjection = GetViewProjection();
CBoundingBoxAligned viewPortBounds;
- #define ADDBOUND(v1, v2, v3, v4) \
- if (v1.Z >= -v1.W) \
- viewPortBounds += CVector3D(v1.X, v1.Y, v1.Z) * (1.0f / v1.W); \
- else \
- { \
- float t = v1.Z + v1.W; \
- if (v2.Z > -v2.W) \
- { \
- CVector4D c2 = v1 + (v2 - v1) * (t / (t - (v2.Z + v2.W))); \
- viewPortBounds += CVector3D(c2.X, c2.Y, c2.Z) * (1.0f / c2.W); \
- } \
- if (v3.Z > -v3.W) \
- { \
- CVector4D c3 = v1 + (v3 - v1) * (t / (t - (v3.Z + v3.W))); \
- viewPortBounds += CVector3D(c3.X, c3.Y, c3.Z) * (1.0f / c3.W); \
- } \
- if (v4.Z > -v4.W) \
- { \
- CVector4D c4 = v1 + (v4 - v1) * (t / (t - (v4.Z + v4.W))); \
- viewPortBounds += CVector3D(c4.X, c4.Y, c4.Z) * (1.0f / c4.W); \
- } \
+#define ADD_VISIBLE_POINT_TO_VIEWBOUNDS(POSITION) STMT( \
+ CVector4D v = viewProjection.Transform(CVector4D((POSITION).X, (POSITION).Y, (POSITION).Z, 1.0f)); \
+ if (v.W != 0.0f) \
+ viewPortBounds += CVector3D(v.X, v.Y, v.Z) * (1.0f / v.W); )
+
+ std::array worldPositions;
+ std::array isBehindNearPlane;
+ const CVector3D lookDirection = GetOrientation().GetIn();
+ // Check corners.
+ for (size_t idx = 0; idx < 8; ++idx)
+ {
+ worldPositions[idx] = CVector3D(boundigBox[(idx >> 0) & 0x1].X, boundigBox[(idx >> 1) & 0x1].Y, boundigBox[(idx >> 2) & 0x1].Z);
+ isBehindNearPlane[idx] = lookDirection.Dot(worldPositions[idx]) < lookDirection.Dot(cameraPosition) + GetNearPlane();
+ if (!isBehindNearPlane[idx])
+ ADD_VISIBLE_POINT_TO_VIEWBOUNDS(worldPositions[idx]);
+ }
+ // Check edges for intersections with the near plane.
+ for (size_t idxBegin = 0; idxBegin < 8; ++idxBegin)
+ for (size_t nextComponent = 0; nextComponent < 3; ++nextComponent)
+ {
+ const size_t idxEnd = idxBegin | (1u << nextComponent);
+ if (idxBegin == idxEnd || isBehindNearPlane[idxBegin] == isBehindNearPlane[idxEnd])
+ continue;
+ CVector3D intersection;
+ // Intersect the segment with the near plane.
+ if (!m_ViewFrustum[5].FindLineSegIntersection(worldPositions[idxBegin], worldPositions[idxEnd], &intersection))
+ continue;
+ ADD_VISIBLE_POINT_TO_VIEWBOUNDS(intersection);
}
- ADDBOUND(v1, v2, v3, v4);
- ADDBOUND(v2, v1, v3, v4);
- ADDBOUND(v3, v1, v2, v4);
- ADDBOUND(v4, v1, v2, v3);
- #undef ADDBOUND
+#undef ADD_VISIBLE_POINT_TO_VIEWBOUNDS
if (viewPortBounds[0].X >= 1.0f || viewPortBounds[1].X <= -1.0f || viewPortBounds[0].Y >= 1.0f || viewPortBounds[1].Y <= -1.0f)
return CBoundingBoxAligned{};
return viewPortBounds;
}
void CCamera::LookAt(const CVector3D& camera, const CVector3D& focus, const CVector3D& up)
{
CVector3D delta = focus - camera;
LookAlong(camera, delta, up);
}
void CCamera::LookAlong(const CVector3D& camera, CVector3D orientation, CVector3D up)
{
orientation.Normalize();
up.Normalize();
CVector3D s = orientation.Cross(up);
m_Orientation._11 = -s.X; m_Orientation._12 = up.X; m_Orientation._13 = orientation.X; m_Orientation._14 = camera.X;
m_Orientation._21 = -s.Y; m_Orientation._22 = up.Y; m_Orientation._23 = orientation.Y; m_Orientation._24 = camera.Y;
m_Orientation._31 = -s.Z; m_Orientation._32 = up.Z; m_Orientation._33 = orientation.Z; m_Orientation._34 = camera.Z;
m_Orientation._41 = 0.0f; m_Orientation._42 = 0.0f; m_Orientation._43 = 0.0f; m_Orientation._44 = 1.0f;
}
Index: ps/trunk/source/graphics/tests/test_Camera.h
===================================================================
--- ps/trunk/source/graphics/tests/test_Camera.h (revision 25439)
+++ ps/trunk/source/graphics/tests/test_Camera.h (revision 25440)
@@ -1,344 +1,424 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "lib/self_test.h"
#include "graphics/Camera.h"
#include "maths/MathUtil.h"
+#include "maths/Vector2D.h"
#include "maths/Vector3D.h"
+#include "maths/Vector4D.h"
#include
#include
class TestCamera : public CxxTest::TestSuite
{
public:
void test_frustum_perspective()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAlong(
CVector3D(0.0f, 0.0f, 0.0f),
CVector3D(0.0f, 0.0f, 1.0f),
CVector3D(0.0f, 1.0f, 0.0f)
);
camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
TS_ASSERT_EQUALS(camera.GetProjectionType(), CCamera::ProjectionType::PERSPECTIVE);
camera.UpdateFrustum();
const float sqrt2 = sqrtf(2.0f) / 2.0f;
const std::vector expectedPlanes = {
CVector4D(sqrt2, 0.0f, sqrt2, 0.0f),
CVector4D(-sqrt2, 0.0f, sqrt2, 0.0f),
CVector4D(0.0f, sqrt2, sqrt2, 0.0f),
CVector4D(0.0f, -sqrt2, sqrt2, 0.0f),
CVector4D(0.0f, 0.0f, -1.0f, 101.0f),
CVector4D(0.0f, 0.0f, 1.0f, -1.0f),
};
CheckFrustumPlanes(camera.GetFrustum(), expectedPlanes);
}
void test_frustum_ortho()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAlong(
CVector3D(0.0f, 0.0f, 0.0f),
CVector3D(0.0f, 0.0f, 1.0f),
CVector3D(0.0f, 1.0f, 0.0f)
);
CMatrix3D projection;
projection.SetOrtho(-10.0f, 10.0f, -10.0f, 10.0f, -10.0f, 10.0f);
camera.SetProjection(projection);
TS_ASSERT_EQUALS(camera.GetProjectionType(), CCamera::ProjectionType::CUSTOM);
camera.UpdateFrustum();
const std::vector expectedPlanes = {
CVector4D(1.0f, 0.0f, 0.0f, 10.0f),
CVector4D(-1.0f, 0.0f, 0.0f, 10.0f),
CVector4D(0.0f, 1.0f, 0.0f, 10.0f),
CVector4D(0.0f, -1.0f, 0.0f, 10.0f),
CVector4D(0.0f, 0.0f, 1.0f, 10.0f),
CVector4D(0.0f, 0.0f, -1.0f, 10.0f)
};
CheckFrustumPlanes(camera.GetFrustum(), expectedPlanes);
}
// Order of planes is unknown. So use interactive checker.
void CheckFrustumPlanes(const CFrustum& frustum, const std::vector& expectedPlanes)
{
TS_ASSERT_EQUALS(frustum.GetNumPlanes(), expectedPlanes.size());
std::set indices;
for (size_t i = 0; i < expectedPlanes.size(); ++i)
indices.insert(i);
for (size_t i = 0; i < frustum.GetNumPlanes(); ++i)
{
bool found = false;
for (size_t j : indices)
{
if (EqualPlanes(frustum[i], expectedPlanes[j]))
{
found = true;
indices.erase(j);
break;
}
}
if (!found)
TS_FAIL(frustum[i]);
}
}
bool EqualPlanes(const CPlane& p1, const CPlane& p2) const
{
const float EPS = 1e-3f;
if (std::fabs(p1.m_Dist - p2.m_Dist) >= EPS)
return false;
return
std::fabs(p1.m_Norm.X - p2.m_Norm.X) < EPS &&
std::fabs(p1.m_Norm.Y - p2.m_Norm.Y) < EPS &&
std::fabs(p1.m_Norm.Z - p2.m_Norm.Z) < EPS;
}
void CompareVectors(const CVector3D& vector1, const CVector3D& vector2, const float EPS)
{
TS_ASSERT_DELTA(vector1.X, vector2.X, EPS);
TS_ASSERT_DELTA(vector1.Y, vector2.Y, EPS);
TS_ASSERT_DELTA(vector1.Z, vector2.Z, EPS);
}
void CompareQuads(const CCamera::Quad& quad, const CCamera::Quad& expectedQuad)
{
const float EPS = 1e-4f;
for (size_t index = 0; index < expectedQuad.size(); ++index)
CompareVectors(quad[index], expectedQuad[index], EPS);
}
void CompareQuadsInWorldSpace(const CCamera& camera, const CCamera::Quad& quad, const CCamera::Quad& expectedQuad)
{
const float EPS = 1e-4f;
for (size_t index = 0; index < expectedQuad.size(); ++index)
{
// Transform quad points from camera space to world space.
CVector3D point = camera.GetOrientation().Transform(quad[index]);
CompareVectors(point, expectedQuad[index], EPS);
}
}
void test_perspective_plane_points()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAt(
CVector3D(10.0f, 20.0f, 10.0f),
CVector3D(10.0f, 10.0f, 20.0f),
CVector3D(0.0f, 1.0f, 1.0f).Normalized()
);
camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
CCamera::Quad quad;
// Zero distance point is the origin of all camera rays,
// so all plane points should stay there.
camera.GetViewQuad(0.0f, quad);
for (const CVector3D& point : quad)
TS_ASSERT_EQUALS(point, CVector3D(0.0f, 0.0f, 0.0f));
// Points lying on the near plane.
CCamera::Quad expectedNearQuad = {
CVector3D(-1.0f, -1.0f, 1.0f),
CVector3D(1.0f, -1.0f, 1.0f),
CVector3D(1.0f, 1.0f, 1.0f),
CVector3D(-1.0f, 1.0f, 1.0f)
};
CCamera::Quad nearQuad;
camera.GetViewQuad(camera.GetNearPlane(), nearQuad);
CompareQuads(nearQuad, expectedNearQuad);
CCamera::Quad expectedWorldSpaceNearQuad = {
CVector3D(9.0f, 18.5857868f, 10.0f),
CVector3D(11.0f, 18.5857868f, 10.0f),
CVector3D(11.0f, 20.0f, 11.4142132f),
CVector3D(9.0f, 20.0f, 11.4142132f)
};
CompareQuadsInWorldSpace(camera, nearQuad, expectedWorldSpaceNearQuad);
// Points lying on the far plane.
CCamera::Quad expectedFarQuad = {
CVector3D(-101.0f, -101.0f, 101.0f),
CVector3D(101.0f, -101.0f, 101.0f),
CVector3D(101.0f, 101.0f, 101.0f),
CVector3D(-101.0f, 101.0f, 101.0f)
};
CCamera::Quad farQuad;
camera.GetViewQuad(camera.GetFarPlane(), farQuad);
CompareQuads(farQuad, expectedFarQuad);
CCamera::Quad expectedWorldSpaceFarQuad = {
CVector3D(-91.0000153f, -122.8355865f, 10.0f),
CVector3D(111.0000153f, -122.8355865f, 10.0f),
CVector3D(111.0000153f, 20.0f, 152.8355865f),
CVector3D(-91.0000153f, 20.0f, 152.8355865f)
};
CompareQuadsInWorldSpace(camera, farQuad, expectedWorldSpaceFarQuad);
}
void test_ortho_plane_points()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAt(
CVector3D(10.0f, 20.0f, 10.0f),
CVector3D(10.0f, 10.0f, 20.0f),
CVector3D(0.0f, 1.0f, 1.0f).Normalized()
);
camera.SetOrthoProjection(2.0f, 128.0f, 10.0f);
// Zero distance is the origin plane of all camera rays,
// so all plane points should stay there.
CCamera::Quad quad;
camera.GetViewQuad(0.0f, quad);
for (const CVector3D& point : quad)
{
constexpr float EPS = 1e-4f;
TS_ASSERT_DELTA(point.Z, 0.0f, EPS);
}
// Points lying on the near plane.
CCamera::Quad expectedNearQuad = {
CVector3D(-5.0f, -5.0f, 2.0f),
CVector3D(5.0f, -5.0f, 2.0f),
CVector3D(5.0f, 5.0f, 2.0f),
CVector3D(-5.0f, 5.0f, 2.0f)
};
CCamera::Quad nearQuad;
camera.GetViewQuad(camera.GetNearPlane(), nearQuad);
CompareQuads(nearQuad, expectedNearQuad);
CCamera::Quad expectedWorldSpaceNearQuad = {
CVector3D(4.9999995f, 15.0502520f, 7.8786793f),
CVector3D(15.0f, 15.0502520f, 7.8786793f),
CVector3D(15.0f, 22.1213207f, 14.9497480f),
CVector3D(4.9999995f, 22.1213207f, 14.9497480f)
};
CompareQuadsInWorldSpace(camera, nearQuad, expectedWorldSpaceNearQuad);
// Points lying on the far plane.
CCamera::Quad expectedFarQuad = {
CVector3D(-5.0f, -5.0f, 128.0f),
CVector3D(5.0f, -5.0f, 128.0f),
CVector3D(5.0f, 5.0f, 128.0f),
CVector3D(-5.0f, 5.0f, 128.0f)
};
CCamera::Quad farQuad;
camera.GetViewQuad(camera.GetFarPlane(), farQuad);
CompareQuads(farQuad, expectedFarQuad);
CCamera::Quad expectedWorldSpaceFarQuad = {
CVector3D(4.9999995f, -74.0452118f, 96.9741364f),
CVector3D(15.0f, -74.0452118f, 96.9741364f),
CVector3D(15.0f, -66.9741364f, 104.0452118f),
CVector3D(4.9999995f, -66.9741364f, 104.0452118f)
};
CompareQuadsInWorldSpace(camera, farQuad, expectedWorldSpaceFarQuad);
}
void test_perspective_screen_rays()
{
const float EPS = 1e-4f;
const std::vector viewPorts = {
SViewPort{0, 0, 512, 512},
SViewPort{0, 0, 1024, 768},
SViewPort{0, 0, 1440, 2536},
};
for (const SViewPort& viewPort : viewPorts)
{
const CVector3D cameraPosition(10.0f, 20.0f, 10.0f);
const CVector3D cameraDirection(CVector3D(0.0f, -1.0f, 1.0f).Normalized());
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAt(
cameraPosition,
cameraPosition + cameraDirection * 10.0f,
CVector3D(0.0f, 1.0f, 1.0f).Normalized()
);
camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
CVector3D origin, dir;
camera.BuildCameraRay(viewPort.m_Width / 2, viewPort.m_Height / 2, origin, dir);
const CVector3D expectedOrigin = cameraPosition;
const CVector3D expectedDir = cameraDirection;
CompareVectors(origin, expectedOrigin, EPS);
CompareVectors(dir, expectedDir, EPS);
}
}
void test_ortho_screen_rays()
{
const float EPS = 1e-4f;
const std::vector viewPorts = {
SViewPort{0, 0, 512, 512},
SViewPort{0, 0, 1024, 768},
SViewPort{0, 0, 1440, 2536},
};
for (const SViewPort& viewPort : viewPorts)
{
const CVector3D cameraPosition(10.0f, 20.0f, 10.0f);
const CVector3D cameraDirection(CVector3D(0.0f, -1.0f, 1.0f).Normalized());
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAt(
cameraPosition,
cameraPosition + cameraDirection * 10.0f,
CVector3D(0.0f, 1.0f, 1.0f).Normalized()
);
camera.SetOrthoProjection(2.0f, 128.0f, 10.0f);
CVector3D origin, dir;
camera.BuildCameraRay(viewPort.m_Width / 2, viewPort.m_Height / 2, origin, dir);
const CVector3D expectedOrigin = cameraPosition;
const CVector3D expectedDir = cameraDirection;
CompareVectors(origin, expectedOrigin, EPS);
CompareVectors(dir, expectedDir, EPS);
}
}
+
+ void CompareBoundingBoxes(const CBoundingBoxAligned& bb1, const CBoundingBoxAligned& bb2)
+ {
+ constexpr float EPS = 1e-3f;
+ CompareVectors(bb1[0], bb2[0], EPS);
+ CompareVectors(bb1[1], bb2[1], EPS);
+ }
+
+ void test_viewport_bounds_perspective()
+ {
+ SViewPort viewPort;
+ viewPort.m_X = 0;
+ viewPort.m_Y = 0;
+ viewPort.m_Width = 512;
+ viewPort.m_Height = 512;
+
+ CCamera camera;
+ camera.SetViewPort(viewPort);
+ camera.LookAlong(
+ CVector3D(0.0f, 0.0f, 0.0f),
+ CVector3D(0.0f, 0.0f, 1.0f),
+ CVector3D(0.0f, 1.0f, 0.0f)
+ );
+ camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
+ camera.UpdateFrustum();
+
+ struct TestCase
+ {
+ CBoundingBoxAligned worldSpaceBoundingBox;
+ CBoundingBoxAligned expectedViewPortBoundingBox;
+ };
+ const TestCase testCases[] = {
+ // Box is in front of the camera.
+ {
+ {{-1.0f, 0.0f, 5.0f}, {1.0f, 0.0f, 7.0f}},
+ {{-0.2f, 0.0f, 0.616f}, {0.2f, 0.0f, 0.731429f}}
+ },
+ // Box is out of the camera view.
+ {
+ {{-10.0f, -1.0f, 5.0f}, {-8.0f, 1.0f, 7.0f}},
+ {}
+ },
+ {
+ {{-1.0f, -10.0f, 5.0f}, {1.0f, -8.0f, 7.0f}},
+ {}
+ },
+ // Box is in the bottom part of the camera view.
+ {
+ {{-1.0f, -3.0f, 5.0f}, {1.0f, -3.0f, 7.0f}},
+ {{-0.2f, -0.6f, 0.616f}, {0.2f, -0.428571f, 0.731429f}}
+ },
+ {
+ {{-1.0f, -3.0f, 0.0f}, {1.0f, -3.0f, 7.0f}},
+ {{-1.0f, -3.0f, -1.0f}, {1.0f, -0.428571f, 0.731429f}}
+ },
+ {
+ {{-1.0f, -3.0f, -7.0f}, {1.0f, -3.0f, 7.0f}},
+ {{-1.0f, -3.0f, -1.0f}, {1.0f, -0.428571f, 0.731429f}}
+ },
+ };
+
+ for (const TestCase& testCase : testCases)
+ {
+ TS_ASSERT(testCase.worldSpaceBoundingBox[0].X <= testCase.worldSpaceBoundingBox[1].X);
+ TS_ASSERT(testCase.worldSpaceBoundingBox[0].Y <= testCase.worldSpaceBoundingBox[1].Y);
+ TS_ASSERT(testCase.worldSpaceBoundingBox[0].Z <= testCase.worldSpaceBoundingBox[1].Z);
+
+ const CBoundingBoxAligned result =
+ camera.GetBoundsInViewPort(testCase.worldSpaceBoundingBox);
+ if (testCase.expectedViewPortBoundingBox.IsEmpty())
+ {
+ TS_ASSERT(result.IsEmpty());
+ }
+ else
+ CompareBoundingBoxes(result, testCase.expectedViewPortBoundingBox);
+ }
+ }
+
};
Index: ps/trunk/source/maths/BoundingBoxAligned.cpp
===================================================================
--- ps/trunk/source/maths/BoundingBoxAligned.cpp (revision 25439)
+++ ps/trunk/source/maths/BoundingBoxAligned.cpp (revision 25440)
@@ -1,265 +1,273 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
/*
* Axis-aligned bounding box
*/
#include "precompiled.h"
#include "BoundingBoxAligned.h"
#include "maths/BoundingBoxOriented.h"
#include "maths/Brush.h"
#include "maths/Frustum.h"
#include "maths/Matrix3D.h"
#include
const CBoundingBoxAligned CBoundingBoxAligned::EMPTY = CBoundingBoxAligned(); // initializes to an empty bound
///////////////////////////////////////////////////////////////////////////////
// RayIntersect: intersect ray with this bound; return true
// if ray hits (and store entry and exit times), or false
// otherwise
// note: incoming ray direction must be normalised
bool CBoundingBoxAligned::RayIntersect(
const CVector3D& origin, const CVector3D& dir, float& tmin, float& tmax) const
{
float t1, t2;
float tnear, tfar;
if (dir[0] == 0)
{
if (origin[0] < m_Data[0][0] || origin[0] > m_Data[1][0])
return false;
else
{
tnear = -std::numeric_limits::max();
tfar = std::numeric_limits::max();
}
}
else
{
t1 = (m_Data[0][0] - origin[0]) / dir[0];
t2 = (m_Data[1][0] - origin[0]) / dir[0];
if (dir[0] < 0)
{
tnear = t2;
tfar = t1;
}
else
{
tnear = t1;
tfar = t2;
}
if (tfar < 0)
return false;
}
if (dir[1] == 0 && (origin[1] < m_Data[0][1] || origin[1] > m_Data[1][1]))
return false;
else
{
t1 = (m_Data[0][1] - origin[1]) / dir[1];
t2 = (m_Data[1][1] - origin[1]) / dir[1];
if (dir[1] < 0)
{
if (t2 > tnear)
tnear = t2;
if (t1 < tfar)
tfar = t1;
}
else
{
if (t1 > tnear)
tnear = t1;
if (t2 < tfar)
tfar = t2;
}
if (tnear > tfar || tfar < 0)
return false;
}
if (dir[2] == 0 && (origin[2] < m_Data[0][2] || origin[2] > m_Data[1][2]))
return false;
else
{
t1 = (m_Data[0][2] - origin[2]) / dir[2];
t2 = (m_Data[1][2] - origin[2]) / dir[2];
if (dir[2] < 0)
{
if (t2 > tnear)
tnear = t2;
if (t1 < tfar)
tfar = t1;
}
else
{
if (t1 > tnear)
tnear = t1;
if (t2 < tfar)
tfar = t2;
}
if (tnear > tfar || tfar < 0)
return false;
}
tmin = tnear;
tmax = tfar;
return true;
}
///////////////////////////////////////////////////////////////////////////////
// SetEmpty: initialise this bound as empty
void CBoundingBoxAligned::SetEmpty()
{
m_Data[0] = CVector3D::Max();
m_Data[1] = CVector3D::Min();
}
///////////////////////////////////////////////////////////////////////////////
// IsEmpty: tests whether this bound is empty
bool CBoundingBoxAligned::IsEmpty() const
{
return m_Data[0] == CVector3D::Max() && m_Data[1] == CVector3D::Min();
}
///////////////////////////////////////////////////////////////////////////////
// Transform: transform this bound by given matrix; return transformed bound
// in 'result' parameter - slightly modified version of code in Graphic Gems
// (can't remember which one it was, though)
void CBoundingBoxAligned::Transform(const CMatrix3D& m, CBoundingBoxAligned& result) const
{
ENSURE(this != &result);
for (int i = 0; i < 3; ++i)
{
// handle translation
result[0][i] = result[1][i] = m(i, 3);
// Now find the extreme points by considering the product of the
// min and max with each component of matrix
for (int j = 0; j < 3; ++j)
{
float a = m(i, j) * m_Data[0][j];
float b = m(i, j) * m_Data[1][j];
if (a >= b)
std::swap(a, b);
result[0][i] += a;
result[1][i] += b;
}
}
}
void CBoundingBoxAligned::Transform(const CMatrix3D& transform, CBoundingBoxOriented& result) const
{
const CVector3D& pMin = m_Data[0];
const CVector3D& pMax = m_Data[1];
// the basis vectors of the OBB are the normalized versions of the transformed AABB basis vectors, which
// are the columns of the identity matrix, so the unnormalized OBB basis vectors are the transformation
// matrix columns:
CVector3D u(transform._11, transform._21, transform._31);
CVector3D v(transform._12, transform._22, transform._32);
CVector3D w(transform._13, transform._23, transform._33);
// the half-sizes are scaled by whatever factor the AABB unit vectors end up scaled by
result.m_HalfSizes = CVector3D(
(pMax.X - pMin.X) / 2.f * u.Length(),
(pMax.Y - pMin.Y) / 2.f * v.Length(),
(pMax.Z - pMin.Z) / 2.f * w.Length()
);
u.Normalize();
v.Normalize();
w.Normalize();
result.m_Basis[0] = u;
result.m_Basis[1] = v;
result.m_Basis[2] = w;
result.m_Center = transform.Transform((pMax + pMin) * 0.5f);
}
///////////////////////////////////////////////////////////////////////////////
// Intersect with the given frustum in a conservative manner
void CBoundingBoxAligned::IntersectFrustumConservative(const CFrustum& frustum)
{
// if this bound is empty, then the result must be empty (we should not attempt to intersect with
// a brush, may cause crashes due to the numeric representation of empty bounds -- see
// http://trac.wildfiregames.com/ticket/1027)
if (IsEmpty())
return;
CBrush brush(*this);
CBrush buf;
brush.Intersect(frustum, buf);
buf.Bounds(*this);
}
///////////////////////////////////////////////////////////////////////////////
CFrustum CBoundingBoxAligned::ToFrustum() const
{
CFrustum frustum;
frustum.SetNumPlanes(6);
// get the LEFT plane
frustum[0].m_Norm = CVector3D(1, 0, 0);
frustum[0].m_Dist = -m_Data[0].X;
// get the RIGHT plane
frustum[1].m_Norm = CVector3D(-1, 0, 0);
frustum[1].m_Dist = m_Data[1].X;
// get the BOTTOM plane
frustum[2].m_Norm = CVector3D(0, 1, 0);
frustum[2].m_Dist = -m_Data[0].Y;
// get the TOP plane
frustum[3].m_Norm = CVector3D(0, -1, 0);
frustum[3].m_Dist = m_Data[1].Y;
// get the NEAR plane
frustum[4].m_Norm = CVector3D(0, 0, 1);
frustum[4].m_Dist = -m_Data[0].Z;
// get the FAR plane
frustum[5].m_Norm = CVector3D(0, 0, -1);
frustum[5].m_Dist = m_Data[1].Z;
return frustum;
}
///////////////////////////////////////////////////////////////////////////////
void CBoundingBoxAligned::Expand(float amount)
{
m_Data[0] -= CVector3D(amount, amount, amount);
m_Data[1] += CVector3D(amount, amount, amount);
}
+
+bool CBoundingBoxAligned::IsPointInside(const CVector3D& point) const
+{
+ return
+ m_Data[0].X <= point.X && point.X <= m_Data[1].X &&
+ m_Data[0].Y <= point.Y && point.Y <= m_Data[1].Y &&
+ m_Data[0].Z <= point.Z && point.Z <= m_Data[1].Z;
+}
Index: ps/trunk/source/maths/BoundingBoxAligned.h
===================================================================
--- ps/trunk/source/maths/BoundingBoxAligned.h (revision 25439)
+++ ps/trunk/source/maths/BoundingBoxAligned.h (revision 25440)
@@ -1,155 +1,157 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
/*
* Axis-aligned bounding box
*/
#ifndef INCLUDED_BOUND
#define INCLUDED_BOUND
#include "maths/Vector3D.h"
#include "graphics/ShaderProgramPtr.h"
class CFrustum;
class CMatrix3D;
class CBoundingBoxOriented;
// Basic axis aligned bounding box (AABB) class
class CBoundingBoxAligned
{
public:
static const CBoundingBoxAligned EMPTY;
CBoundingBoxAligned() { SetEmpty(); }
CBoundingBoxAligned(const CVector3D& min, const CVector3D& max)
{
m_Data[0] = min;
m_Data[1] = max;
}
/**
* Transforms these bounds according to the specified transformation matrix @p m, and writes the axis-aligned bounds
* of that result to @p result.
*/
void Transform(const CMatrix3D& m, CBoundingBoxAligned& result) const;
/**
* Transform these bounds using the matrix @p transform, and write out the result as an oriented (i.e. non-axis-aligned) box.
* The difference with @ref Transform(const CMatrix3D&, CBoundingBoxAligned&) is that that method is equivalent to first
* computing this result, and then taking the axis-aligned bounding boxes from the result again.
*/
void Transform(const CMatrix3D& m, CBoundingBoxOriented& result) const;
/**
* Translates these bounds by @p v, and writes the result to @p result.
*/
void Translate(const CVector3D& v, CBoundingBoxAligned& result) const
{
result.m_Data[0] = m_Data[0] + v;
result.m_Data[1] = m_Data[1] + v;
}
CVector3D& operator[](int index) { return m_Data[index]; }
const CVector3D& operator[](int index) const { return m_Data[index]; }
void SetEmpty();
bool IsEmpty() const;
void Extend(const CVector3D& min, const CVector3D& max)
{
if (min.X < m_Data[0].X) m_Data[0].X = min.X;
if (min.Y < m_Data[0].Y) m_Data[0].Y = min.Y;
if (min.Z < m_Data[0].Z) m_Data[0].Z = min.Z;
if (max.X > m_Data[1].X) m_Data[1].X = max.X;
if (max.Y > m_Data[1].Y) m_Data[1].Y = max.Y;
if (max.Z > m_Data[1].Z) m_Data[1].Z = max.Z;
}
// operator+=: extend this bound to include given bound
CBoundingBoxAligned& operator+=(const CBoundingBoxAligned& b)
{
Extend(b.m_Data[0], b.m_Data[1]);
return *this;
}
// operator+=: extend this bound to include given point
CBoundingBoxAligned& operator+=(const CVector3D& pt)
{
Extend(pt, pt);
return *this;
}
/**
* Check if a given ray intersects this AABB.
* See also Real-Time Rendering, Third Edition by T. Akenine-Moller, p. 741--742.
*
* @param[in] origin Origin of the ray.
* @param[in] dir Direction vector of the ray, defining the positive direction of the ray. Must be of unit length.
* @param[out] tmin,tmax distance in the positive direction from the origin of the ray to the entry and exit points in
* the bounding box. If the origin is inside the box, then this is counted as an intersection and one of @p tMin and @p tMax may be negative.
*
* @return true if the ray originating in @p origin and with unit direction vector @p dir intersects this AABB, false otherwise.
*/
bool RayIntersect(const CVector3D& origin, const CVector3D& dir, float& tmin, float& tmax) const;
+ bool IsPointInside(const CVector3D& point) const;
+
// return the volume of this bounding box
float GetVolume() const
{
CVector3D v = m_Data[1] - m_Data[0];
return (std::max(v.X, 0.0f) * std::max(v.Y, 0.0f) * std::max(v.Z, 0.0f));
}
// return the center of this bounding box
void GetCenter(CVector3D& center) const
{
center = (m_Data[0] + m_Data[1]) * 0.5f;
}
/**
* Expand the bounding box by the given amount in every direction.
*/
void Expand(float amount);
/**
* IntersectFrustumConservative: Approximate the intersection of this bounds object
* with the given frustum. The bounds object is overwritten with the results.
*
* The approximation is conservative in the sense that the result will always contain
* the actual intersection, but it may be larger than the intersection itself.
* The result will always be fully contained within the original bounds.
*
* @note While not in the spirit of this function's purpose, a no-op would be a correct
* implementation of this function.
* @note If this bound is empty, the result is the empty bound.
*
* @param frustum the frustum to intersect with
*/
void IntersectFrustumConservative(const CFrustum& frustum);
/**
* Construct a CFrustum that describes the same volume as this bounding box.
* Only valid for non-empty bounding boxes - check IsEmpty() first.
*/
CFrustum ToFrustum() const;
private:
// Holds the minimal and maximal coordinate points in m_Data[0] and m_Data[1], respectively.
CVector3D m_Data[2];
};
#endif // INCLUDED_BOUND
Index: ps/trunk/source/maths/tests/test_Bound.h
===================================================================
--- ps/trunk/source/maths/tests/test_Bound.h (revision 25439)
+++ ps/trunk/source/maths/tests/test_Bound.h (revision 25440)
@@ -1,211 +1,241 @@
/* Copyright (C) 2019 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
#include "lib/self_test.h"
#include "lib/posix/posix.h"
#include "maths/BoundingBoxAligned.h"
#include "maths/BoundingBoxOriented.h"
#include "maths/Matrix3D.h"
#define TS_ASSERT_VEC_DELTA(v, x, y, z, delta) \
TS_ASSERT_DELTA(v.X, x, delta); \
TS_ASSERT_DELTA(v.Y, y, delta); \
TS_ASSERT_DELTA(v.Z, z, delta);
class TestBound : public CxxTest::TestSuite
{
public:
void setUp()
{
CxxTest::setAbortTestOnFail(true);
}
void test_empty_aabb()
{
CBoundingBoxAligned bound;
TS_ASSERT(bound.IsEmpty());
bound += CVector3D(1, 2, 3);
TS_ASSERT(! bound.IsEmpty());
bound.SetEmpty();
TS_ASSERT(bound.IsEmpty());
}
void test_empty_obb()
{
CBoundingBoxOriented bound;
TS_ASSERT(bound.IsEmpty());
bound.m_Basis[0] = CVector3D(1,0,0);
bound.m_Basis[1] = CVector3D(0,1,0);
bound.m_Basis[2] = CVector3D(0,0,1);
bound.m_HalfSizes = CVector3D(1,2,3);
TS_ASSERT(!bound.IsEmpty());
bound.SetEmpty();
TS_ASSERT(bound.IsEmpty());
}
void test_extend_vector()
{
CBoundingBoxAligned bound;
CVector3D v (1, 2, 3);
bound += v;
CVector3D center;
bound.GetCenter(center);
TS_ASSERT_EQUALS(center, v);
}
void test_extend_bound()
{
CBoundingBoxAligned bound;
CVector3D v (1, 2, 3);
CBoundingBoxAligned b (v, v);
bound += b;
CVector3D center;
bound.GetCenter(center);
TS_ASSERT_EQUALS(center, v);
}
void test_aabb_to_obb_translation()
{
CBoundingBoxAligned aabb(CVector3D(-1,-2,-1), CVector3D(1,2,1));
CMatrix3D translation;
translation.SetTranslation(CVector3D(1,3,7));
CBoundingBoxOriented result;
aabb.Transform(translation, result);
TS_ASSERT_VEC_DELTA(result.m_Center, 1.f, 3.f, 7.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 1.f, 0.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[1], 0.f, 1.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_HalfSizes, 1.f, 2.f, 1.f, 1e-7f);
}
void test_aabb_to_obb_rotation_around_origin()
{
// rotate a 4x3x3 AABB centered at (5,0,0) 90 degrees CCW around the Z axis, and verify that the
// resulting OBB is correct
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
CMatrix3D rotation;
rotation.SetZRotation(float(M_PI)/2.f);
CBoundingBoxOriented result;
aabb.Transform(rotation, result);
TS_ASSERT_VEC_DELTA(result.m_Center, 0.f, 5.f, 0.f, 1e-6f); // involves some trigonometry, lower precision
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 0.f, 1.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[1], -1.f, 0.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
}
void test_aabb_to_obb_rotation_around_point()
{
// rotate a 4x3x3 AABB centered at (5,0,0) 45 degrees CW around the Z axis through (2,0,0)
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
// move everything so (2,0,0) becomes the origin, do the rotation, then move everything back
CMatrix3D translate;
CMatrix3D rotate;
CMatrix3D translateBack;
translate.SetTranslation(-2.f, 0, 0);
rotate.SetZRotation(-float(M_PI)/4.f);
translateBack.SetTranslation(2.f, 0, 0);
CMatrix3D transform;
transform.SetIdentity();
transform.Concatenate(translate);
transform.Concatenate(rotate);
transform.Concatenate(translateBack);
CBoundingBoxOriented result;
aabb.Transform(transform, result);
const float invSqrt2 = 1.f/sqrtf(2.f);
TS_ASSERT_VEC_DELTA(result.m_Center, 3*invSqrt2 + 2, -3*invSqrt2, 0.f, 1e-6f); // involves some trigonometry, lower precision
TS_ASSERT_VEC_DELTA(result.m_Basis[0], invSqrt2, -invSqrt2, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[1], invSqrt2, invSqrt2, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
}
void test_aabb_to_obb_scale()
{
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
CMatrix3D scale;
scale.SetScaling(1.f, 3.f, 7.f);
CBoundingBoxOriented result;
aabb.Transform(scale, result);
TS_ASSERT_VEC_DELTA(result.m_Center, 5.f, 0.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_HalfSizes, 2.f, 4.5f, 10.5f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 1.f, 0.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[1], 0.f, 1.f, 0.f, 1e-7f);
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
}
// Verify that ray/OBB intersection is correctly determined in degenerate case where the
// box has zero size in one of its dimensions.
void test_degenerate_obb_ray_intersect()
{
// create OBB of a flat 1x1 square in the X/Z plane, with 0 size in the Y dimension
CBoundingBoxOriented bound;
bound.m_Basis[0] = CVector3D(1,0,0); // X
bound.m_Basis[1] = CVector3D(0,1,0); // Y
bound.m_Basis[2] = CVector3D(0,0,1); // Z
bound.m_HalfSizes[0] = 1.f;
bound.m_HalfSizes[1] = 0.f; // no height, i.e. a "flat" OBB
bound.m_HalfSizes[2] = 1.f;
bound.m_Center = CVector3D(0,0,0);
// create two rays; one that should hit the OBB, and one that should miss it
CVector3D ray1origin(-3.5f, 3.f, 0.f);
CVector3D ray1direction(1.f, -1.f, 0.f);
CVector3D ray2origin(-4.5f, 3.f, 0.f);
CVector3D ray2direction(1.f, -1.f, 0.f);
float tMin, tMax;
TSM_ASSERT("Ray 1 should intersect the OBB", bound.RayIntersect(ray1origin, ray1direction, tMin, tMax));
TSM_ASSERT("Ray 2 should not intersect the OBB", !bound.RayIntersect(ray2origin, ray2direction, tMin, tMax));
}
// Verify that transforming a flat AABB to an OBB does not produce NaN basis vectors in the
// resulting OBB (see http://trac.wildfiregames.com/ticket/1121)
void test_degenerate_aabb_to_obb_transform()
{
// create a flat AABB, transform it with some matrix (can even be the identity matrix),
// and verify that the result does not contain any NaN values in its basis vectors
// and/or half-sizes
CBoundingBoxAligned flatAabb(CVector3D(-1,0,-1), CVector3D(1,0,1));
CMatrix3D transform;
transform.SetIdentity();
CBoundingBoxOriented result;
flatAabb.Transform(transform, result);
TS_ASSERT(!isnan(result.m_Basis[0].X) && !isnan(result.m_Basis[0].Y) && !isnan(result.m_Basis[0].Z));
TS_ASSERT(!isnan(result.m_Basis[1].X) && !isnan(result.m_Basis[1].Y) && !isnan(result.m_Basis[1].Z));
TS_ASSERT(!isnan(result.m_Basis[2].X) && !isnan(result.m_Basis[2].Y) && !isnan(result.m_Basis[2].Z));
}
+ void test_point_visibility()
+ {
+ const CBoundingBoxAligned bb(CVector3D(1.0f, -10.0f, 3.0f), CVector3D(3.0f, -8.0f, 5.0f));
+
+ TS_ASSERT(!bb.IsPointInside(CVector3D(0.0f, 0.0f, 0.0f)));
+ TS_ASSERT(bb.IsPointInside(bb[0]));
+ TS_ASSERT(bb.IsPointInside(bb[1]));
+
+ CVector3D center;
+ bb.GetCenter(center);
+ TS_ASSERT(bb.IsPointInside(center));
+
+ for (int offsetX = -1; offsetX <= 1; ++offsetX)
+ for (int offsetY = -1; offsetY <= 1; ++offsetY)
+ for (int offsetZ = -1; offsetZ <= 1; ++offsetZ)
+ {
+ TS_ASSERT(bb.IsPointInside(
+ center + CVector3D(offsetX, offsetY, offsetZ) * 0.9f));
+ const bool isInside = bb.IsPointInside(
+ center + CVector3D(offsetX, offsetY, offsetZ) * 1.1f);
+ if (offsetX == 0 && offsetY == 0 && offsetZ == 0)
+ {
+ TS_ASSERT(isInside);
+ }
+ else
+ {
+ TS_ASSERT(!isInside);
+ }
+ }
+ }
};
Index: ps/trunk/source/renderer/Renderer.cpp
===================================================================
--- ps/trunk/source/renderer/Renderer.cpp (revision 25439)
+++ ps/trunk/source/renderer/Renderer.cpp (revision 25440)
@@ -1,1936 +1,1943 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. 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 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 0 A.D. If not, see .
*/
/*
* higher level interface on top of OpenGL to render basic objects:
* terrain, models, sprites, particles etc.
*/
#include "precompiled.h"
#include "Renderer.h"
#include "lib/bits.h" // is_pow2
#include "lib/res/graphics/ogl_tex.h"
#include "lib/allocators/shared_ptr.h"
#include "maths/Matrix3D.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "ps/Filesystem.h"
#include "ps/World.h"
#include "ps/Loader.h"
#include "ps/ProfileViewer.h"
#include "graphics/Camera.h"
#include "graphics/Decal.h"
#include "graphics/FontManager.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/LOSTexture.h"
#include "graphics/MaterialManager.h"
#include "graphics/Model.h"
#include "graphics/ModelDef.h"
#include "graphics/ParticleManager.h"
#include "graphics/Patch.h"
#include "graphics/ShaderManager.h"
#include "graphics/Terrain.h"
#include "graphics/Texture.h"
#include "graphics/TextureManager.h"
#include "renderer/DebugRenderer.h"
#include "renderer/HWLightingModelRenderer.h"
#include "renderer/InstancingModelRenderer.h"
#include "renderer/ModelRenderer.h"
#include "renderer/OverlayRenderer.h"
#include "renderer/ParticleRenderer.h"
#include "renderer/PostprocManager.h"
#include "renderer/RenderingOptions.h"
#include "renderer/RenderModifiers.h"
#include "renderer/ShadowMap.h"
#include "renderer/SilhouetteRenderer.h"
#include "renderer/SkyManager.h"
#include "renderer/TerrainOverlay.h"
#include "renderer/TerrainRenderer.h"
#include "renderer/TimeManager.h"
#include "renderer/VertexBufferManager.h"
#include "renderer/WaterManager.h"
#include
#include
#include