Index: ps/trunk/source/maths/BoundingBoxAligned.cpp =================================================================== --- ps/trunk/source/maths/BoundingBoxAligned.cpp (revision 22371) +++ ps/trunk/source/maths/BoundingBoxAligned.cpp (revision 22372) @@ -1,322 +1,335 @@ -/* Copyright (C) 2012 Wildfire Games. +/* 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 . */ /* * Axis-aligned bounding box */ #include "precompiled.h" #include "BoundingBoxAligned.h" -#include "lib/ogl.h" - -#include - #include "graphics/Frustum.h" #include "graphics/ShaderProgram.h" +#include "lib/ogl.h" #include "maths/BoundingBoxOriented.h" #include "maths/Brush.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 +bool CBoundingBoxAligned::RayIntersect( + const CVector3D& origin, const CVector3D& dir, float& tmin, float& tmax) const { - float t1,t2; - float tnear,tfar; + float t1, t2; + float tnear, tfar; - if (dir[0]==0) { - if (origin[0]m_Data[1][0]) + if (dir[0] == 0) + { + if (origin[0] < m_Data[0][0] || origin[0] > m_Data[1][0]) return false; - else { - tnear=(float) -FLT_MAX; - tfar=(float) FLT_MAX; + 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]; + } + else + { + t1 = (m_Data[0][0] - origin[0]) / dir[0]; + t2 = (m_Data[1][0] - origin[0]) / dir[0]; - if (dir[0]<0) { + if (dir[0] < 0) + { tnear = t2; tfar = t1; - } else { + } + else + { tnear = t1; tfar = t2; } - if (tfar<0) + if (tfar < 0) return false; } - if (dir[1]==0 && (origin[1]m_Data[1][1])) + 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) + 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 (t1tnear) + } + else + { + if (t1 > tnear) tnear = t1; - if (t2tfar || tfar<0) + if (tnear > tfar || tfar < 0) return false; } - if (dir[2]==0 && (origin[2]m_Data[1][2])) + 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) + 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 (t1tnear) + } + else + { + if (t1 > tnear) tnear = t1; - if (t2tfar || tfar<0) - return false; + if (tnear > tfar || tfar < 0) + return false; } - tmin=tnear; - tmax=tfar; + tmin = tnear; + tmax = tfar; return true; } /////////////////////////////////////////////////////////////////////////////// // SetEmpty: initialise this bound as empty void CBoundingBoxAligned::SetEmpty() { - m_Data[0]=CVector3D( FLT_MAX, FLT_MAX, FLT_MAX); - m_Data[1]=CVector3D(-FLT_MAX,-FLT_MAX,-FLT_MAX); + 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].X == FLT_MAX && m_Data[0].Y == FLT_MAX && m_Data[0].Z == FLT_MAX - && m_Data[1].X == -FLT_MAX && m_Data[1].Y == -FLT_MAX && m_Data[1].Z == -FLT_MAX); + 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); + ENSURE(this != &result); - for (int i=0;i<3;++i) { + for (int i = 0; i < 3; ++i) + { // handle translation - result[0][i]=result[1][i]=m(i,3); + 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.m_aPlanes[0].m_Norm = CVector3D(1, 0, 0); frustum.m_aPlanes[0].m_Dist = -m_Data[0].X; // get the RIGHT plane frustum.m_aPlanes[1].m_Norm = CVector3D(-1, 0, 0); frustum.m_aPlanes[1].m_Dist = m_Data[1].X; // get the BOTTOM plane frustum.m_aPlanes[2].m_Norm = CVector3D(0, 1, 0); frustum.m_aPlanes[2].m_Dist = -m_Data[0].Y; // get the TOP plane frustum.m_aPlanes[3].m_Norm = CVector3D(0, -1, 0); frustum.m_aPlanes[3].m_Dist = m_Data[1].Y; // get the NEAR plane frustum.m_aPlanes[4].m_Norm = CVector3D(0, 0, 1); frustum.m_aPlanes[4].m_Dist = -m_Data[0].Z; // get the FAR plane frustum.m_aPlanes[5].m_Norm = CVector3D(0, 0, -1); frustum.m_aPlanes[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); } /////////////////////////////////////////////////////////////////////////////// // Render the bounding box void CBoundingBoxAligned::Render(CShaderProgramPtr& shader) const { std::vector data; #define ADD_FACE(x, y, z) \ ADD_PT(0, 0, x, y, z); ADD_PT(1, 0, x, y, z); ADD_PT(1, 1, x, y, z); \ ADD_PT(1, 1, x, y, z); ADD_PT(0, 1, x, y, z); ADD_PT(0, 0, x, y, z); #define ADD_PT(u_, v_, x, y, z) \ STMT(int u = u_; int v = v_; \ data.push_back(u); \ data.push_back(v); \ data.push_back(m_Data[x].X); \ data.push_back(m_Data[y].Y); \ data.push_back(m_Data[z].Z); \ ) ADD_FACE(u, v, 0); ADD_FACE(0, u, v); ADD_FACE(u, 0, 1-v); ADD_FACE(u, 1-v, 1); ADD_FACE(1, u, 1-v); ADD_FACE(u, 1, v); #undef ADD_FACE shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 5*sizeof(float), &data[0]); shader->VertexPointer(3, GL_FLOAT, 5*sizeof(float), &data[2]); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6*6); } void CBoundingBoxAligned::RenderOutline(CShaderProgramPtr& shader) const { std::vector data; #define ADD_FACE(x, y, z) \ ADD_PT(0, 0, x, y, z); ADD_PT(1, 0, x, y, z); \ ADD_PT(1, 0, x, y, z); ADD_PT(1, 1, x, y, z); \ ADD_PT(1, 1, x, y, z); ADD_PT(0, 1, x, y, z); \ ADD_PT(0, 1, x, y, z); ADD_PT(0, 0, x, y, z); #define ADD_PT(u_, v_, x, y, z) \ STMT(int u = u_; int v = v_; \ data.push_back(u); \ data.push_back(v); \ data.push_back(m_Data[x].X); \ data.push_back(m_Data[y].Y); \ data.push_back(m_Data[z].Z); \ ) ADD_FACE(u, v, 0); ADD_FACE(0, u, v); ADD_FACE(u, 0, 1-v); ADD_FACE(u, 1-v, 1); ADD_FACE(1, u, 1-v); ADD_FACE(u, 1, v); #undef ADD_FACE shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 5*sizeof(float), &data[0]); shader->VertexPointer(3, GL_FLOAT, 5*sizeof(float), &data[2]); shader->AssertPointersBound(); glDrawArrays(GL_LINES, 0, 6*8); } Index: ps/trunk/source/maths/BoundingBoxAligned.h =================================================================== --- ps/trunk/source/maths/BoundingBoxAligned.h (revision 22371) +++ ps/trunk/source/maths/BoundingBoxAligned.h (revision 22372) @@ -1,171 +1,165 @@ -/* Copyright (C) 2011 Wildfire Games. +/* 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 . */ /* * Axis-aligned bounding box */ #ifndef INCLUDED_BOUND #define INCLUDED_BOUND -// necessary includes -#include "Vector3D.h" +#include "maths/Vector3D.h" #include "graphics/ShaderProgramPtr.h" class CFrustum; class CMatrix3D; class CBoundingBoxOriented; -/////////////////////////////////////////////////////////////////////////////// -// basic axis aligned bounding box (AABB) class +// Basic axis aligned bounding box (AABB) class class CBoundingBoxAligned { public: + static const CBoundingBoxAligned EMPTY; CBoundingBoxAligned() { SetEmpty(); } - CBoundingBoxAligned(const CVector3D& min, const CVector3D& max) { + 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; // 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 centre of this bounding box - void GetCentre(CVector3D& centre) const + // return the center of this bounding box + void GetCenter(CVector3D& center) const { - centre = (m_Data[0] + m_Data[1]) * 0.5f; + 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; /** * Render the surfaces of the bound object as triangles. */ void Render(CShaderProgramPtr& shader) const; /** * Render the outline of the bound object as lines. */ void RenderOutline(CShaderProgramPtr& shader) const; private: // Holds the minimal and maximal coordinate points in m_Data[0] and m_Data[1], respectively. CVector3D m_Data[2]; - -public: - static const CBoundingBoxAligned EMPTY; - }; -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#endif +#endif // INCLUDED_BOUND Index: ps/trunk/source/maths/BoundingBoxOriented.cpp =================================================================== --- ps/trunk/source/maths/BoundingBoxOriented.cpp (revision 22371) +++ ps/trunk/source/maths/BoundingBoxOriented.cpp (revision 22372) @@ -1,97 +1,97 @@ -/* Copyright (C) 2011 Wildfire Games. +/* 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 "precompiled.h" #include "BoundingBoxOriented.h" #include "maths/BoundingBoxAligned.h" #include const CBoundingBoxOriented CBoundingBoxOriented::EMPTY = CBoundingBoxOriented(); CBoundingBoxOriented::CBoundingBoxOriented(const CBoundingBoxAligned& bound) { if (bound.IsEmpty()) { SetEmpty(); } else { - bound.GetCentre(m_Center); + bound.GetCenter(m_Center); // the axes of an AABB are the world-space axes m_Basis[0].X = 1.f; m_Basis[0].Y = 0.f; m_Basis[0].Z = 0.f; m_Basis[1].X = 0.f; m_Basis[1].Y = 1.f; m_Basis[1].Z = 0.f; m_Basis[2].X = 0.f; m_Basis[2].Y = 0.f; m_Basis[2].Z = 1.f; // element-wise division by two to get half sizes (remember, [1] and [0] are the max and min coord points) m_HalfSizes = (bound[1] - bound[0]) * 0.5f; } } bool CBoundingBoxOriented::RayIntersect(const CVector3D& origin, const CVector3D& dir, float& tMin_out, float& tMax_out) const { // See Real-Time Rendering, Third Edition, p. 743 float tMin = -FLT_MAX; float tMax = FLT_MAX; CVector3D p = m_Center - origin; for (int i = 0; i < 3; ++i) { // test the ray for intersections with the slab whose normal vector is m_Basis[i] float e = m_Basis[i].Dot(p); // distance between the ray origin and the box center projected onto the slab normal float f = m_Basis[i].Dot(dir); // cosine of the angle between the slab normal and the ray direction if(fabsf(f) > 1e-10f) { // Determine the distances t1 and t2 from the origin of the ray to the points where it intersects // the slab. See docs/ray_intersect.pdf for why/how this works. float invF = 1.f/f; float t1 = (e + m_HalfSizes[i]) * invF; float t2 = (e - m_HalfSizes[i]) * invF; // make sure t1 <= t2, swap if necessary if (t1 > t2) { float tmp = t1; t1 = t2; t2 = tmp; } // update the overall tMin and tMax if necessary if (t1 > tMin) tMin = t1; if (t2 < tMax) tMax = t2; // try to break out of the loop as fast as possible by checking for some conditions if (tMin > tMax) return false; // ray misses the box if (tMax < 0) return false; // box is behind the ray origin } else { // the ray is parallel to the slab currently being tested, or is as close to parallel // as makes no difference; return false if the ray is outside of the slab. if (e > m_HalfSizes[i] || -e > m_HalfSizes[i]) return false; } } tMin_out = tMin; tMax_out = tMax; return true; } Index: ps/trunk/source/maths/Vector3D.cpp =================================================================== --- ps/trunk/source/maths/Vector3D.cpp (revision 22371) +++ ps/trunk/source/maths/Vector3D.cpp (revision 22372) @@ -1,83 +1,95 @@ -/* Copyright (C) 2010 Wildfire Games. +/* 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 . */ /* * Provides an interface for a vector in R3 and allows vector and * scalar operations on it */ #include "precompiled.h" #include "Vector3D.h" -#include -#include #include "MathUtil.h" #include "FixedVector3D.h" +#include +#include + CVector3D::CVector3D(const CFixedVector3D& v) : X(v.X.ToFloat()), Y(v.Y.ToFloat()), Z(v.Z.ToFloat()) { } +// static +CVector3D CVector3D::Max() +{ + const float max_float = std::numeric_limits::max(); + return CVector3D(max_float, max_float, max_float); +} + +// static +CVector3D CVector3D::Min() +{ + const float max_float = std::numeric_limits::max(); + return CVector3D(-max_float, -max_float, -max_float); +} + int CVector3D::operator ! () const { if (X != 0.0f || Y != 0.0f || Z != 0.0f) return 0; return 1; } float CVector3D::LengthSquared () const { return ( SQR(X) + SQR(Y) + SQR(Z) ); } float CVector3D::Length () const { return sqrtf ( LengthSquared() ); } void CVector3D::Normalize () { float scale = 1.0f/Length (); X *= scale; Y *= scale; Z *= scale; } CVector3D CVector3D::Normalized () const { float scale = 1.0f/Length (); return CVector3D(X * scale, Y * scale, Z * scale); } //----------------------------------------------------------------------------- float MaxComponent(const CVector3D& v) { - float max = -FLT_MAX; - for(int i = 0; i < 3; i++) - max = std::max(max, v[i]); - return max; + return std::max({v.X, v.Y, v.Z}); } Index: ps/trunk/source/maths/Vector3D.h =================================================================== --- ps/trunk/source/maths/Vector3D.h (revision 22371) +++ ps/trunk/source/maths/Vector3D.h (revision 22372) @@ -1,126 +1,130 @@ -/* Copyright (C) 2010 Wildfire Games. +/* 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 . */ /* * Provides an interface for a vector in R3 and allows vector and * scalar operations on it */ #ifndef INCLUDED_VECTOR3D #define INCLUDED_VECTOR3D class CFixedVector3D; class CVector3D { public: float X, Y, Z; public: + // Returns maximum/minimum possible position stored in the CVector3D. + static CVector3D Max(); + static CVector3D Min(); + CVector3D() : X(0.0f), Y(0.0f), Z(0.0f) {} CVector3D(float x, float y, float z) : X(x), Y(y), Z(z) {} CVector3D(const CFixedVector3D& v); int operator!() const; float& operator[](int index) { return *((&X)+index); } const float& operator[](int index) const { return *((&X)+index); } // vector equality (testing float equality, so please be careful if necessary) bool operator==(const CVector3D &vector) const { return (X == vector.X && Y == vector.Y && Z == vector.Z); } bool operator!=(const CVector3D& vector) const { return !operator==(vector); } CVector3D operator+(const CVector3D& vector) const { return CVector3D(X + vector.X, Y + vector.Y, Z + vector.Z); } CVector3D& operator+=(const CVector3D& vector) { X += vector.X; Y += vector.Y; Z += vector.Z; return *this; } CVector3D operator-(const CVector3D& vector) const { return CVector3D(X - vector.X, Y - vector.Y, Z - vector.Z); } CVector3D& operator-=(const CVector3D& vector) { X -= vector.X; Y -= vector.Y; Z -= vector.Z; return *this; } CVector3D operator*(float value) const { return CVector3D(X * value, Y * value, Z * value); } CVector3D& operator*=(float value) { X *= value; Y *= value; Z *= value; return *this; } CVector3D operator-() const { return CVector3D(-X, -Y, -Z); } public: float Dot (const CVector3D &vector) const { return ( X * vector.X + Y * vector.Y + Z * vector.Z ); } CVector3D Cross (const CVector3D &vector) const { CVector3D Temp; Temp.X = (Y * vector.Z) - (Z * vector.Y); Temp.Y = (Z * vector.X) - (X * vector.Z); Temp.Z = (X * vector.Y) - (Y * vector.X); return Temp; } float Length () const; float LengthSquared () const; void Normalize (); CVector3D Normalized () const; // Returns 3 element array of floats, e.g. for glVertex3fv const float* GetFloatArray() const { return &X; } }; extern float MaxComponent(const CVector3D& v); #endif Index: ps/trunk/source/maths/tests/test_Bound.h =================================================================== --- ps/trunk/source/maths/tests/test_Bound.h (revision 22371) +++ ps/trunk/source/maths/tests/test_Bound.h (revision 22372) @@ -1,211 +1,211 @@ -/* Copyright (C) 2012 Wildfire Games. +/* 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 centre; - bound.GetCentre(centre); - TS_ASSERT_EQUALS(centre, 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 centre; - bound.GetCentre(centre); - TS_ASSERT_EQUALS(centre, v); + 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)); } }; Index: ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 22371) +++ ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 22372) @@ -1,472 +1,472 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpUnitRenderer.h" #include "simulation2/MessageTypes.h" #include "ICmpPosition.h" #include "ICmpRangeManager.h" #include "ICmpSelectable.h" #include "ICmpVisibility.h" #include "ICmpVisual.h" #include "graphics/Frustum.h" #include "graphics/ModelAbstract.h" #include "graphics/ObjectEntry.h" #include "graphics/Overlay.h" #include "graphics/Unit.h" #include "maths/BoundingSphere.h" #include "maths/Matrix3D.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "renderer/Scene.h" #include "tools/atlas/GameInterface/GameLoop.h" /** * Efficiently(ish) renders all the units in the world. * * The class maintains a list of all units that currently exist, and the data * needed for frustum-culling them. To minimise the amount of work done per * frame (despite a unit's interpolated position changing every frame), the * culling data is only updated once per turn: we store the position at the * start of the turn, and the position at the end of the turn, and assume the * unit might be anywhere between those two points (linearly). * * (Note this is a slightly invalid assumption: units don't always move linearly, * since their interpolated position depends on terrain and water. But over a * single turn it's probably going to be a good enough approximation, and will * only break for units that both start and end the turn off-screen.) * * We want to ignore rotation entirely, since it's a complex function of * interpolated position and terrain. So we store a bounding sphere, which * is rotation-independent, instead of a bounding box. */ class CCmpUnitRenderer : public ICmpUnitRenderer { public: struct SUnit { CEntityHandle entity; CUnit* actor; int flags; /** * m_FrameNumber from when the model's transform was last updated. * This is used to avoid recomputing it multiple times per frame * if a model is visible in multiple cull groups. */ int lastTransformFrame; /** * Worst-case bounding shape, relative to position. Needs to account * for all possible animations, orientations, etc. */ CBoundingSphere boundsApprox; /** * Cached LOS visibility status. */ ICmpRangeManager::ELosVisibility visibility; bool visibilityDirty; /** * Whether the unit has a valid position. If false, pos0 and pos1 * are meaningless. */ bool inWorld; /** * World-space positions to interpolate between. */ CVector3D pos0; CVector3D pos1; /** * Bounds encompassing the unit's bounds when it is anywhere between * pos0 and pos1. */ CBoundingSphere sweptBounds; /** * For debug overlay. */ bool culled; }; std::vector m_Units; std::vector m_UnitTagsFree; int m_FrameNumber; float m_FrameOffset; bool m_EnableDebugOverlays; std::vector m_DebugSpheres; static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(UnitRenderer) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_FrameNumber = 0; m_FrameOffset = 0.0f; m_EnableDebugOverlays = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_TurnStart: { TurnStart(); break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); break; } } } SUnit* LookupUnit(tag_t tag) { if (tag.n < 1 || tag.n - 1 >= m_Units.size()) return NULL; return &m_Units[tag.n - 1]; } virtual tag_t AddUnit(CEntityHandle entity, CUnit* actor, const CBoundingSphere& boundsApprox, int flags) { ENSURE(actor != NULL); tag_t tag; if (!m_UnitTagsFree.empty()) { tag = m_UnitTagsFree.back(); m_UnitTagsFree.pop_back(); } else { m_Units.push_back(SUnit()); tag.n = m_Units.size(); } SUnit* unit = LookupUnit(tag); unit->entity = entity; unit->actor = actor; unit->lastTransformFrame = -1; unit->flags = flags; unit->boundsApprox = boundsApprox; unit->inWorld = false; unit->visibilityDirty = true; unit->pos0 = unit->pos1 = CVector3D(); return tag; } virtual void RemoveUnit(tag_t tag) { SUnit* unit = LookupUnit(tag); unit->actor = NULL; unit->inWorld = false; m_UnitTagsFree.push_back(tag); } void RecomputeSweptBounds(SUnit* unit) { // Compute the bounding sphere of the capsule formed by // sweeping boundsApprox from pos0 to pos1 CVector3D mid = (unit->pos0 + unit->pos1) * 0.5f + unit->boundsApprox.GetCenter(); float radius = (unit->pos1 - unit->pos0).Length() * 0.5f + unit->boundsApprox.GetRadius(); unit->sweptBounds = CBoundingSphere(mid, radius); } virtual void UpdateUnit(tag_t tag, CUnit* actor, const CBoundingSphere& boundsApprox) { SUnit* unit = LookupUnit(tag); unit->actor = actor; unit->boundsApprox = boundsApprox; RecomputeSweptBounds(unit); } virtual void UpdateUnitPos(tag_t tag, bool inWorld, const CVector3D& pos0, const CVector3D& pos1) { SUnit* unit = LookupUnit(tag); unit->inWorld = inWorld; unit->pos0 = pos0; unit->pos1 = pos1; unit->visibilityDirty = true; RecomputeSweptBounds(unit); } void TurnStart(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); void UpdateVisibility(SUnit& unit) const; virtual float GetFrameOffset() const { return m_FrameOffset; } virtual void SetDebugOverlay(bool enabled) { m_EnableDebugOverlays = enabled; } virtual void PickAllEntitiesAtPoint(std::vector >& outEntities, const CVector3D& origin, const CVector3D& dir, bool allowEditorSelectables) const { // First, make a rough test with the worst-case bounding boxes to pick all // entities/models that could possibly be hit by the ray. std::vector candidates; for (const SUnit& unit : m_Units) { if (!unit.actor || !unit.inWorld) continue; if (unit.sweptBounds.RayIntersect(origin, dir)) candidates.push_back(&unit); } // Now make a more precise test to get rid of the remaining false positives float tmin, tmax; CVector3D center; for (size_t i = 0; i< candidates.size(); ++i) { const SUnit& unit = *candidates[i]; CmpPtr cmpVisual(unit.entity); if (!cmpVisual) continue; CBoundingBoxOriented selectionBox = cmpVisual->GetSelectionBox(); if (selectionBox.IsEmpty()) { if (!allowEditorSelectables) continue; // Fall back to using old AABB selection method for decals // see: http://trac.wildfiregames.com/ticket/1032 // Decals are flat objects without a selectionShape defined, // but they should still be selectable in the editor to move them // around or delete them after they are placed. // Check campaigns/labels/ in the Actors tab of atlas for examples. CBoundingBoxAligned aABBox = cmpVisual->GetBounds(); if (aABBox.IsEmpty()) continue; if (!aABBox.RayIntersect(origin, dir, tmin, tmax)) continue; - aABBox.GetCentre(center); + aABBox.GetCenter(center); } else { if (!selectionBox.RayIntersect(origin, dir, tmin, tmax)) continue; center = selectionBox.m_Center; } outEntities.emplace_back(unit.entity, center); } } }; void CCmpUnitRenderer::TurnStart() { PROFILE3("UnitRenderer::TurnStart"); // Assume units have stopped moving after the previous turn. If that assumption is not // correct, we will get a UpdateUnitPos to tell us about its movement in the new turn. for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; unit.pos0 = unit.pos1; unit.sweptBounds = CBoundingSphere(unit.pos1, unit.boundsApprox.GetRadius()); // Visibility must be recomputed on the first frame during this turn unit.visibilityDirty = true; } } void CCmpUnitRenderer::Interpolate(float frameTime, float frameOffset) { PROFILE3("UnitRenderer::Interpolate"); ++m_FrameNumber; m_FrameOffset = frameOffset; // TODO: we shouldn't update all the animations etc for units that are off-screen // (but need to be careful about e.g. sounds triggered by animations of off-screen // units) for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; if (unit.actor) unit.actor->UpdateModel(frameTime); } m_DebugSpheres.clear(); if (m_EnableDebugOverlays) { for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; if (!(unit.actor && unit.inWorld)) continue; SOverlaySphere sphere; sphere.m_Center = unit.sweptBounds.GetCenter(); sphere.m_Radius = unit.sweptBounds.GetRadius(); if (unit.culled) sphere.m_Color = CColor(1.0f, 0.5f, 0.5f, 0.5f); else sphere.m_Color = CColor(0.5f, 0.5f, 1.0f, 0.5f); m_DebugSpheres.push_back(sphere); } } } void CCmpUnitRenderer::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { // TODO: need a coarse culling pass based on some kind of spatial data // structure - that's the main point of this design. Once we've got a // rough list of possibly-visible units, then we can do the more precise // culling. (And once it's cheap enough, we can do multiple culling passes // per frame - one for shadow generation, one for water reflections, etc.) PROFILE3("UnitRenderer::RenderSubmit"); for (size_t i = 0; i < m_Units.size(); ++i) { SUnit& unit = m_Units[i]; unit.culled = true; if (!unit.actor) continue; if (unit.visibilityDirty) UpdateVisibility(unit); if (unit.visibility == ICmpRangeManager::VIS_HIDDEN) continue; if (!g_AtlasGameLoop->running && !g_RenderActors && (unit.flags & ACTOR_ONLY)) continue; if (!g_AtlasGameLoop->running && (unit.flags & VISIBLE_IN_ATLAS_ONLY)) continue; if (culling && !frustum.IsSphereVisible(unit.sweptBounds.GetCenter(), unit.sweptBounds.GetRadius())) continue; unit.culled = false; CModelAbstract& unitModel = unit.actor->GetModel(); if (unit.lastTransformFrame != m_FrameNumber) { CmpPtr cmpPosition(unit.entity); if (!cmpPosition) continue; CMatrix3D transform(cmpPosition->GetInterpolatedTransform(m_FrameOffset)); unitModel.SetTransform(transform); unit.lastTransformFrame = m_FrameNumber; } if (culling && !frustum.IsBoxVisible(unitModel.GetWorldBoundsRec())) continue; collector.SubmitRecursive(&unitModel); } for (size_t i = 0; i < m_DebugSpheres.size(); ++i) collector.Submit(&m_DebugSpheres[i]); } void CCmpUnitRenderer::UpdateVisibility(SUnit& unit) const { if (unit.inWorld) { // The 'always visible' flag means we should always render the unit // (regardless of whether the LOS system thinks it's visible) CmpPtr cmpVisibility(unit.entity); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) unit.visibility = ICmpRangeManager::VIS_VISIBLE; else { CmpPtr cmpRangeManager(GetSystemEntity()); unit.visibility = cmpRangeManager->GetLosVisibility(unit.entity, GetSimContext().GetCurrentDisplayedPlayer()); } } else unit.visibility = ICmpRangeManager::VIS_HIDDEN; // Change the visibility of the visual actor's selectable if it has one. CmpPtr cmpSelectable(unit.entity); if (cmpSelectable) cmpSelectable->SetVisibility(unit.visibility != ICmpRangeManager::VIS_HIDDEN); unit.visibilityDirty = false; } REGISTER_COMPONENT_TYPE(UnitRenderer) Index: ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 22371) +++ ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 22372) @@ -1,557 +1,557 @@ /* 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 "precompiled.h" #include "ActorViewer.h" #include "View.h" #include "graphics/ColladaManager.h" #include "graphics/LOSTexture.h" #include "graphics/Unit.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ObjectManager.h" #include "graphics/ParticleManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/UnitManager.h" #include "graphics/Overlay.h" #include "maths/MathUtil.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "ps/ProfileViewer.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/SkyManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpUnitMotion.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Render.h" struct ActorViewerImpl : public Scene { NONCOPYABLE(ActorViewerImpl); public: ActorViewerImpl() : Entity(INVALID_ENTITY), Terrain(), ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), UnitManager(), Simulation2(&UnitManager, g_ScriptRuntime, &Terrain), ObjectManager(MeshManager, SkeletonAnimManager, Simulation2), LOSTexture(Simulation2), TerritoryTexture(Simulation2) { UnitManager.SetObjectManager(ObjectManager); } entity_id_t Entity; CStrW CurrentUnitID; CStr CurrentUnitAnim; float CurrentSpeed; bool WalkEnabled; bool GroundEnabled; bool WaterEnabled; bool ShadowsEnabled; // Whether shadows, sky and water are enabled outside of the actor viewer. bool OldShadows; bool OldSky; bool OldWater; bool SelectionBoxEnabled; bool AxesMarkerEnabled; int PropPointsMode; // 0 disabled, 1 for point markers, 2 for point markers + axes SColor4ub Background; CTerrain Terrain; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CUnitManager UnitManager; CSimulation2 Simulation2; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; SOverlayLine SelectionBoxOverlay; SOverlayLine AxesMarkerOverlays[3]; std::vector Props; std::vector PropPointOverlays; // Simplistic implementation of the Scene interface virtual void EnumerateObjects(const CFrustum& frustum, SceneCollector* c) { if (GroundEnabled) { for (ssize_t pj = 0; pj < Terrain.GetPatchesPerSide(); ++pj) for (ssize_t pi = 0; pi < Terrain.GetPatchesPerSide(); ++pi) c->Submit(Terrain.GetPatch(pi, pj)); } CmpPtr cmpVisual(Simulation2, Entity); if (cmpVisual) { // add selection box outlines manually if (SelectionBoxEnabled) { SelectionBoxOverlay.m_Color = CColor(35/255.f, 86/255.f, 188/255.f, .75f); // pretty blue SelectionBoxOverlay.m_Thickness = 2; SimRender::ConstructBoxOutline(cmpVisual->GetSelectionBox(), SelectionBoxOverlay); c->Submit(&SelectionBoxOverlay); } // add origin axis thingy if (AxesMarkerEnabled) { CMatrix3D worldSpaceAxes; // offset from the ground a little bit to prevent fighting with the floor texture (also note: SetTranslation // sets the identity 3x3 transformation matrix, which are the world axes) worldSpaceAxes.SetTranslation(cmpVisual->GetPosition() + CVector3D(0, 0.02f, 0)); SimRender::ConstructAxesMarker(worldSpaceAxes, AxesMarkerOverlays[0], AxesMarkerOverlays[1], AxesMarkerOverlays[2]); c->Submit(&AxesMarkerOverlays[0]); c->Submit(&AxesMarkerOverlays[1]); c->Submit(&AxesMarkerOverlays[2]); } // add prop point overlays if (PropPointsMode > 0 && Props.size() > 0) { PropPointOverlays.clear(); // doesn't clear capacity, but should be ok since the number of prop points is usually pretty limited for (size_t i = 0; i < Props.size(); ++i) { CModel::Prop& prop = Props[i]; if (prop.m_Model) // should always be the case { // prop point positions are automatically updated during animations etc. by CModel::ValidatePosition const CMatrix3D& propCoordSystem = prop.m_Model->GetTransform(); SOverlayLine pointGimbal; pointGimbal.m_Color = CColor(1.f, 0.f, 1.f, 1.f); SimRender::ConstructGimbal(propCoordSystem.GetTranslation(), 0.05f, pointGimbal); PropPointOverlays.push_back(pointGimbal); if (PropPointsMode > 1) { // scale the prop axes coord system down a bit to distinguish them from the main world-space axes markers CMatrix3D displayCoordSystem = propCoordSystem; displayCoordSystem.Scale(0.5f, 0.5f, 0.5f); // revert translation scaling displayCoordSystem._14 = propCoordSystem._14; displayCoordSystem._24 = propCoordSystem._24; displayCoordSystem._34 = propCoordSystem._34; // construct an XYZ axes marker for the prop's coordinate system SOverlayLine xAxis, yAxis, zAxis; SimRender::ConstructAxesMarker(displayCoordSystem, xAxis, yAxis, zAxis); PropPointOverlays.push_back(xAxis); PropPointOverlays.push_back(yAxis); PropPointOverlays.push_back(zAxis); } } } for (size_t i = 0; i < PropPointOverlays.size(); ++i) { c->Submit(&PropPointOverlays[i]); } } } // send a RenderSubmit message so the components can submit their visuals to the renderer Simulation2.RenderSubmit(*c, frustum, false); } virtual CLOSTexture& GetLOSTexture() { return LOSTexture; } virtual CTerritoryTexture& GetTerritoryTexture() { return TerritoryTexture; } /** * Recursively fetches the props of the currently displayed entity model and its submodels, and stores them for rendering. */ void UpdatePropList(); void UpdatePropListRecursive(CModelAbstract* model); }; void ActorViewerImpl::UpdatePropList() { Props.clear(); CmpPtr cmpVisual(Simulation2, Entity); if (cmpVisual) { CUnit* unit = cmpVisual->GetUnit(); if (unit) { CModelAbstract& modelAbstract = unit->GetModel(); UpdatePropListRecursive(&modelAbstract); } } } void ActorViewerImpl::UpdatePropListRecursive(CModelAbstract* modelAbstract) { ENSURE(modelAbstract); CModel* model = modelAbstract->ToCModel(); if (model) { std::vector& modelProps = model->GetProps(); for (CModel::Prop& modelProp : modelProps) { Props.push_back(modelProp); if (modelProp.m_Model) UpdatePropListRecursive(modelProp.m_Model); } } } ActorViewer::ActorViewer() : m(*new ActorViewerImpl()) { m.WalkEnabled = false; m.GroundEnabled = true; m.WaterEnabled = false; m.ShadowsEnabled = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS); m.SelectionBoxEnabled = false; m.AxesMarkerEnabled = false; m.PropPointsMode = 0; m.Background = SColor4ub(0, 0, 0, 255); // Create a tiny empty piece of terrain, just so we can put shadows // on it without having to think too hard m.Terrain.Initialize(2, NULL); CTerrainTextureEntry* tex = g_TexMan.FindTexture("whiteness"); if (tex) { for (ssize_t pi = 0; pi < m.Terrain.GetPatchesPerSide(); ++pi) { for (ssize_t pj = 0; pj < m.Terrain.GetPatchesPerSide(); ++pj) { CPatch* patch = m.Terrain.GetPatch(pi, pj); for (ssize_t i = 0; i < PATCH_SIZE; ++i) { for (ssize_t j = 0; j < PATCH_SIZE; ++j) { CMiniPatch& mp = patch->m_MiniPatches[i][j]; mp.Tex = tex; mp.Priority = 0; } } } } } else { debug_warn(L"Failed to load whiteness texture"); } // Prepare the simulation m.Simulation2.LoadDefaultScripts(); m.Simulation2.ResetState(); // Set player data m.Simulation2.SetMapSettings(m.Simulation2.GetPlayerDefaults()); m.Simulation2.LoadPlayerSettings(true); // Tell the simulation we've already loaded the terrain CmpPtr cmpTerrain(m.Simulation2, SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(false); // Remove FOW since we're in Atlas CmpPtr cmpRangeManager(m.Simulation2, SYSTEM_ENTITY); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(-1, true); m.Simulation2.InitGame(); } ActorViewer::~ActorViewer() { delete &m; } CSimulation2* ActorViewer::GetSimulation2() { return &m.Simulation2; } entity_id_t ActorViewer::GetEntity() { return m.Entity; } void ActorViewer::UnloadObjects() { m.ObjectManager.UnloadObjects(); } void ActorViewer::SetActor(const CStrW& name, const CStr& animation, player_id_t playerID) { bool needsAnimReload = false; CStrW id = name; // Recreate the entity, if we don't have one or if the new one is different if (m.Entity == INVALID_ENTITY || id != m.CurrentUnitID) { // Delete the old entity (if any) if (m.Entity != INVALID_ENTITY) { m.Simulation2.DestroyEntity(m.Entity); m.Simulation2.FlushDestroyedEntities(); m.Entity = INVALID_ENTITY; } // Clear particles associated with deleted entity g_Renderer.GetParticleManager().ClearUnattachedEmitters(); // If there's no actor to display, return with nothing loaded if (id.empty()) return; m.Entity = m.Simulation2.AddEntity(L"preview|" + id); if (m.Entity == INVALID_ENTITY) return; CmpPtr cmpPosition(m.Simulation2, m.Entity); if (cmpPosition) { ssize_t c = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; cmpPosition->JumpTo(entity_pos_t::FromInt(c), entity_pos_t::FromInt(c)); cmpPosition->SetYRotation(entity_angle_t::Pi()); } CmpPtr cmpOwnership(m.Simulation2, m.Entity); if (cmpOwnership) cmpOwnership->SetOwner(playerID); needsAnimReload = true; } if (animation != m.CurrentUnitAnim) needsAnimReload = true; if (needsAnimReload) { CStr anim = animation.LowerCase(); // Emulate the typical simulation animation behaviour float speed; float repeattime = 0.f; if (anim == "walk") { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); else speed = 7.f; // typical unit speed m.CurrentSpeed = speed; } else if (anim == "run") { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) speed = cmpUnitMotion->GetWalkSpeed().ToFloat() * cmpUnitMotion->GetRunMultiplier().ToFloat(); else speed = 12.f; // typical unit speed m.CurrentSpeed = speed; } else if (anim == "melee") { speed = 1.f; // speed will be ignored if we have a repeattime m.CurrentSpeed = 0.f; CStr code = "var cmp = Engine.QueryInterface("+CStr::FromUInt(m.Entity)+", IID_Attack); " + "if (cmp) cmp.GetTimers(cmp.GetBestAttack()).repeat; else 0;"; m.Simulation2.GetScriptInterface().Eval(code.c_str(), repeattime); } else { // Play the animation at normal speed, but movement speed is zero speed = 1.f; m.CurrentSpeed = 0.f; } CStr sound; if (anim == "melee") sound = "attack"; else if (anim == "build") sound = "build"; else if (anim.Find("gather_") == 0) sound = anim; CmpPtr cmpVisual(m.Simulation2, m.Entity); if (cmpVisual) { // TODO: SetEntitySelection(anim) cmpVisual->SelectAnimation(anim, false, fixed::FromFloat(speed)); if (repeattime) cmpVisual->SetAnimationSyncRepeat(fixed::FromFloat(repeattime)); } // update prop list for new entity/animation (relies on needsAnimReload also getting called for entire entity changes) m.UpdatePropList(); } m.CurrentUnitID = id; m.CurrentUnitAnim = animation; } void ActorViewer::SetEnabled(bool enabled) { if (enabled) { // Set shadows, sky and water. m.OldShadows = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, m.ShadowsEnabled); m.OldSky = g_Renderer.GetSkyManager()->GetRenderSky(); g_Renderer.GetSkyManager()->SetRenderSky(false); m.OldWater = g_Renderer.GetWaterManager()->m_RenderWater; g_Renderer.GetWaterManager()->m_RenderWater = m.WaterEnabled; } else { // Restore the old renderer state g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, m.OldShadows); g_Renderer.GetSkyManager()->SetRenderSky(m.OldSky); g_Renderer.GetWaterManager()->m_RenderWater = m.OldWater; } } void ActorViewer::SetBackgroundColor(const SColor4ub& color) { m.Background = color; m.Terrain.SetBaseColor(color); } void ActorViewer::SetWalkEnabled(bool enabled) { m.WalkEnabled = enabled; } void ActorViewer::SetGroundEnabled(bool enabled) { m.GroundEnabled = enabled; } void ActorViewer::SetWaterEnabled(bool enabled) { m.WaterEnabled = enabled; // Adjust water level entity_pos_t waterLevel = entity_pos_t::FromFloat(enabled ? 10.f : 0.f); CmpPtr cmpWaterManager(m.Simulation2, SYSTEM_ENTITY); if (cmpWaterManager) cmpWaterManager->SetWaterLevel(waterLevel); } void ActorViewer::SetShadowsEnabled(bool enabled) { m.ShadowsEnabled = enabled; } void ActorViewer::SetBoundingBoxesEnabled(bool enabled) { m.SelectionBoxEnabled = enabled; } void ActorViewer::SetAxesMarkerEnabled(bool enabled) { m.AxesMarkerEnabled = enabled; } void ActorViewer::SetPropPointsMode(int mode) { m.PropPointsMode = mode; } void ActorViewer::SetStatsEnabled(bool enabled) { if (enabled) g_ProfileViewer.ShowTable("renderer"); else g_ProfileViewer.ShowTable(""); } void ActorViewer::Render() { m.Terrain.MakeDirty(RENDERDATA_UPDATE_COLOR); g_Renderer.SetClearColor(m.Background); // Set simulation context for rendering purposes g_Renderer.SetSimulation(&m.Simulation2); g_Renderer.BeginFrame(); // Find the centre of the interesting region, in the middle of the patch // and half way up the model (assuming there is one) CVector3D centre; CmpPtr cmpVisual(m.Simulation2, m.Entity); if (cmpVisual) - cmpVisual->GetBounds().GetCentre(centre); + cmpVisual->GetBounds().GetCenter(centre); else centre.Y = 0.f; centre.X = centre.Z = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; CCamera camera = AtlasView::GetView_Actor()->GetCamera(); camera.m_Orientation.Translate(centre.X, centre.Y, centre.Z); camera.UpdateFrustum(); g_Renderer.SetSceneCamera(camera, camera); g_Renderer.RenderScene(m); glDisable(GL_DEPTH_TEST); g_Logger->Render(); g_ProfileViewer.RenderProfile(); glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); ogl_WarnIfError(); } void ActorViewer::Update(float simFrameLength, float realFrameLength) { m.Simulation2.Update((int)(simFrameLength*1000)); m.Simulation2.Interpolate(simFrameLength, 0, realFrameLength); if (m.WalkEnabled && m.CurrentSpeed) { CmpPtr cmpPosition(m.Simulation2, m.Entity); if (cmpPosition) { // Move the model by speed*simFrameLength forwards float z = cmpPosition->GetPosition().Z.ToFloat(); z -= m.CurrentSpeed*simFrameLength; // Wrap at the edges, so it doesn't run off into the horizon ssize_t c = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; if (z < c - TERRAIN_TILE_SIZE*PATCH_SIZE * 0.1f) z = c + TERRAIN_TILE_SIZE*PATCH_SIZE * 0.1f; cmpPosition->JumpTo(cmpPosition->GetPosition().X, entity_pos_t::FromFloat(z)); } } }