Index: ps/trunk/source/graphics/Camera.cpp =================================================================== --- ps/trunk/source/graphics/Camera.cpp (revision 26120) +++ ps/trunk/source/graphics/Camera.cpp (revision 26121) @@ -1,460 +1,459 @@ /* 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/Vector2D.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 { if (m_ProjType == ProjectionType::CUSTOM) { const CMatrix3D invProjection = m_ProjMat.GetInverse(); const std::array ndcCorners = { CVector2D{-1.0f, -1.0f}, CVector2D{1.0f, -1.0f}, CVector2D{1.0f, 1.0f}, CVector2D{-1.0f, 1.0f}}; for (size_t idx = 0; idx < 4; ++idx) { const CVector2D& corner = ndcCorners[idx]; CVector4D nearCorner = invProjection.Transform(CVector4D(corner.X, corner.Y, -1.0f, 1.0f)); nearCorner /= nearCorner.W; CVector4D farCorner = invProjection.Transform(CVector4D(corner.X, corner.Y, 1.0f, 1.0f)); farCorner /= farCorner.W; const float t = (dist - nearCorner.Z) / (farCorner.Z - nearCorner.Z); const CVector4D quadCorner = nearCorner * (1.0 - t) + farCorner * t; quad[idx].X = quadCorner.X; quad[idx].Y = quadCorner.Y; quad[idx].Z = quadCorner.Z; } return; } 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 { 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 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); } #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/MaterialManager.cpp =================================================================== --- ps/trunk/source/graphics/MaterialManager.cpp (revision 26120) +++ ps/trunk/source/graphics/MaterialManager.cpp (revision 26121) @@ -1,198 +1,197 @@ /* 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 "precompiled.h" #include "MaterialManager.h" #include "graphics/PreprocessorWrapper.h" -#include "lib/ogl.h" #include "maths/MathUtil.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "renderer/RenderingOptions.h" #include CMaterialManager::CMaterialManager() { qualityLevel = 5.0; CFG_GET_VAL("materialmgr.quality", qualityLevel); qualityLevel = Clamp(qualityLevel, 0.0f, 10.0f); if (VfsDirectoryExists(L"art/materials/") && !CXeromyces::AddValidator(g_VFS, "material", "art/materials/material.rng")) LOGERROR("CMaterialManager: failed to load grammar file 'art/materials/material.rng'"); } CMaterial CMaterialManager::LoadMaterial(const VfsPath& pathname) { if (pathname.empty()) return CMaterial(); std::map::iterator iter = m_Materials.find(pathname); if (iter != m_Materials.end()) return iter->second; CXeromyces xeroFile; if (xeroFile.Load(g_VFS, pathname, "material") != PSRETURN_OK) return CMaterial(); #define EL(x) int el_##x = xeroFile.GetElementID(#x) #define AT(x) int at_##x = xeroFile.GetAttributeID(#x) EL(alpha_blending); EL(alternative); EL(define); EL(shader); EL(uniform); EL(renderquery); EL(required_texture); EL(conditional_define); AT(effect); AT(if); AT(define); AT(quality); AT(material); AT(name); AT(value); AT(type); AT(min); AT(max); AT(conf); #undef AT #undef EL CPreprocessorWrapper preprocessor; preprocessor.AddDefine("CFG_FORCE_ALPHATEST", g_RenderingOptions.GetForceAlphaTest() ? "1" : "0"); CMaterial material; material.AddStaticUniform("qualityLevel", CVector4D(qualityLevel, 0, 0, 0)); XMBElement root = xeroFile.GetRoot(); XERO_ITER_EL(root, node) { int token = node.GetNodeName(); XMBAttributeList attrs = node.GetAttributes(); if (token == el_alternative) { CStr cond = attrs.GetNamedItem(at_if); if (cond.empty() || !preprocessor.TestConditional(cond)) { cond = attrs.GetNamedItem(at_quality); if (cond.empty()) continue; else { if (cond.ToFloat() <= qualityLevel) continue; } } material = LoadMaterial(VfsPath("art/materials") / attrs.GetNamedItem(at_material).FromUTF8()); break; } else if (token == el_alpha_blending) { material.SetUsesAlphaBlending(true); } else if (token == el_shader) { material.SetShaderEffect(attrs.GetNamedItem(at_effect)); } else if (token == el_define) { material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_name)), CStrIntern(attrs.GetNamedItem(at_value))); } else if (token == el_conditional_define) { std::vector args; CStr type = attrs.GetNamedItem(at_type).c_str(); int typeID = -1; if (type == CStr("draw_range")) { typeID = DCOND_DISTANCE; float valmin = -1.0f; float valmax = -1.0f; CStr conf = attrs.GetNamedItem(at_conf); if (!conf.empty()) { CFG_GET_VAL("materialmgr." + conf + ".min", valmin); CFG_GET_VAL("materialmgr." + conf + ".max", valmax); } else { CStr dmin = attrs.GetNamedItem(at_min); if (!dmin.empty()) valmin = attrs.GetNamedItem(at_min).ToFloat(); CStr dmax = attrs.GetNamedItem(at_max); if (!dmax.empty()) valmax = attrs.GetNamedItem(at_max).ToFloat(); } args.push_back(valmin); args.push_back(valmax); if (valmin >= 0.0f) { std::stringstream sstr; sstr << valmin; material.AddShaderDefine(CStrIntern(conf + "_MIN"), CStrIntern(sstr.str())); } if (valmax >= 0.0f) { std::stringstream sstr; sstr << valmax; material.AddShaderDefine(CStrIntern(conf + "_MAX"), CStrIntern(sstr.str())); } } material.AddConditionalDefine(attrs.GetNamedItem(at_name).c_str(), attrs.GetNamedItem(at_value).c_str(), typeID, args); } else if (token == el_uniform) { std::stringstream str(attrs.GetNamedItem(at_value)); CVector4D vec; str >> vec.X >> vec.Y >> vec.Z >> vec.W; material.AddStaticUniform(attrs.GetNamedItem(at_name).c_str(), vec); } else if (token == el_renderquery) { material.AddRenderQuery(attrs.GetNamedItem(at_name).c_str()); } else if (token == el_required_texture) { material.AddRequiredSampler(attrs.GetNamedItem(at_name)); if (!attrs.GetNamedItem(at_define).empty()) material.AddShaderDefine(CStrIntern(attrs.GetNamedItem(at_define)), str_1); } } material.RecomputeCombinedShaderDefines(); m_Materials[pathname] = material; return material; } Index: ps/trunk/source/graphics/Model.cpp =================================================================== --- ps/trunk/source/graphics/Model.cpp (revision 26120) +++ ps/trunk/source/graphics/Model.cpp (revision 26121) @@ -1,623 +1,618 @@ /* 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 . */ -/* - * Mesh object with texture and skinning information - */ - #include "precompiled.h" #include "Model.h" -#include "Decal.h" -#include "ModelDef.h" -#include "maths/Quaternion.h" +#include "graphics/Decal.h" +#include "graphics/MeshManager.h" +#include "graphics/ModelDef.h" +#include "graphics/ObjectEntry.h" +#include "graphics/SkeletonAnim.h" +#include "graphics/SkeletonAnimDef.h" +#include "graphics/SkeletonAnimManager.h" #include "maths/BoundingBoxAligned.h" -#include "SkeletonAnim.h" -#include "SkeletonAnimDef.h" -#include "SkeletonAnimManager.h" -#include "MeshManager.h" -#include "ObjectEntry.h" -#include "lib/res/graphics/ogl_tex.h" +#include "maths/Quaternion.h" #include "lib/res/h_mgr.h" #include "lib/sysdep/rtl.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Profile.h" #include "renderer/RenderingOptions.h" -#include "simulation2/Simulation2.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpWaterManager.h" +#include "simulation2/Simulation2.h" ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Constructor CModel::CModel(CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation) : m_Flags(0), m_Anim(NULL), m_AnimTime(0), m_Simulation(simulation), m_BoneMatrices(NULL), m_AmmoPropPoint(NULL), m_AmmoLoadedProp(0), m_SkeletonAnimManager(skeletonAnimManager) { } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Destructor CModel::~CModel() { ReleaseData(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // ReleaseData: delete anything allocated by the model void CModel::ReleaseData() { rtl_FreeAligned(m_BoneMatrices); for (size_t i = 0; i < m_Props.size(); ++i) delete m_Props[i].m_Model; m_Props.clear(); m_pModelDef = CModelDefPtr(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // InitModel: setup model from given geometry bool CModel::InitModel(const CModelDefPtr& modeldef) { // clean up any existing data first ReleaseData(); m_pModelDef = modeldef; size_t numBones = modeldef->GetNumBones(); if (numBones != 0) { size_t numBlends = modeldef->GetNumBlends(); // allocate matrices for bone transformations // (one extra matrix is used for the special case of bind-shape relative weighting) m_BoneMatrices = (CMatrix3D*)rtl_AllocateAligned(sizeof(CMatrix3D) * (numBones + 1 + numBlends), 16); for (size_t i = 0; i < numBones + 1 + numBlends; ++i) { m_BoneMatrices[i].SetIdentity(); } } m_PositionValid = true; return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcBound: calculate the world space bounds of this model void CModel::CalcBounds() { // Need to calculate the object bounds first, if that hasn't already been done if (! (m_Anim && m_Anim->m_AnimDef)) { if (m_ObjectBounds.IsEmpty()) CalcStaticObjectBounds(); } else { if (m_Anim->m_ObjectBounds.IsEmpty()) CalcAnimatedObjectBounds(m_Anim->m_AnimDef, m_Anim->m_ObjectBounds); ENSURE(! m_Anim->m_ObjectBounds.IsEmpty()); // (if this happens, it'll be recalculating the bounds every time) m_ObjectBounds = m_Anim->m_ObjectBounds; } // Ensure the transform is set correctly before we use it ValidatePosition(); // Now transform the object-space bounds to world-space bounds m_ObjectBounds.Transform(GetTransform(), m_WorldBounds); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcObjectBounds: calculate object space bounds of this model, based solely on vertex positions void CModel::CalcStaticObjectBounds() { PROFILE2("CalcStaticObjectBounds"); m_pModelDef->GetMaxBounds(nullptr, !(m_Flags & MODELFLAG_NOLOOPANIMATION), m_ObjectBounds); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcAnimatedObjectBound: calculate bounds encompassing all vertex positions for given animation void CModel::CalcAnimatedObjectBounds(CSkeletonAnimDef* anim, CBoundingBoxAligned& result) { PROFILE2("CalcAnimatedObjectBounds"); m_pModelDef->GetMaxBounds(anim, !(m_Flags & MODELFLAG_NOLOOPANIMATION), result); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// const CBoundingBoxAligned CModel::GetWorldBoundsRec() { CBoundingBoxAligned bounds = GetWorldBounds(); for (size_t i = 0; i < m_Props.size(); ++i) bounds += m_Props[i].m_Model->GetWorldBoundsRec(); return bounds; } const CBoundingBoxAligned CModel::GetObjectSelectionBoundsRec() { CBoundingBoxAligned objBounds = GetObjectBounds(); // updates the (children-not-included) object-space bounds if necessary // now extend these bounds to include the props' selection bounds (if any) for (size_t i = 0; i < m_Props.size(); ++i) { const Prop& prop = m_Props[i]; if (prop.m_Hidden || !prop.m_Selectable) continue; // prop is hidden from rendering, so it also shouldn't be used for selection CBoundingBoxAligned propSelectionBounds = prop.m_Model->GetObjectSelectionBoundsRec(); if (propSelectionBounds.IsEmpty()) continue; // submodel does not wish to participate in selection box, exclude it // We have the prop's bounds in its own object-space; now we need to transform them so they can be properly added // to the bounds in our object-space. For that, we need the transform of the prop attachment point. // // We have the prop point information; however, it's not trivial to compute its exact location in our object-space // since it may or may not be attached to a bone (see SPropPoint), which in turn may or may not be in the middle of // an animation. The bone matrices might be of interest, but they're really only meant to be used for the animation // system and are quite opaque to use from the outside (see @ref ValidatePosition). // // However, a nice side effect of ValidatePosition is that it also computes the absolute world-space transform of // our props and sets it on their respective models. In particular, @ref ValidatePosition will compute the prop's // world-space transform as either // // T' = T x B x O // or // T' = T x O // // where T' is the prop's world-space transform, T is our world-space transform, O is the prop's local // offset/rotation matrix, and B is an optional transformation matrix of the bone the prop is attached to // (taking into account animation and everything). // // From this, it is clear that either O or B x O is the object-space transformation matrix of the prop. So, // all we need to do is apply our own inverse world-transform T^(-1) to T' to get our desired result. Luckily, // this is precomputed upon setting the transform matrix (see @ref SetTransform), so it is free to fetch. CMatrix3D propObjectTransform = prop.m_Model->GetTransform(); // T' propObjectTransform.Concatenate(GetInvTransform()); // T^(-1) x T' // Transform the prop's bounds into our object coordinate space CBoundingBoxAligned transformedPropSelectionBounds; propSelectionBounds.Transform(propObjectTransform, transformedPropSelectionBounds); objBounds += transformedPropSelectionBounds; } return objBounds; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // BuildAnimation: load raw animation frame animation from given file, and build a // animation specific to this model CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, const CStr& ID, int frequency, float speed, float actionpos, float actionpos2, float soundpos) { CSkeletonAnimDef* def = m_SkeletonAnimManager.GetAnimation(pathname); if (!def) return NULL; CSkeletonAnim* anim = new CSkeletonAnim(); anim->m_Name = name; anim->m_ID = ID; anim->m_Frequency = frequency; anim->m_AnimDef = def; anim->m_Speed = speed; if (actionpos == -1.f) anim->m_ActionPos = -1.f; else anim->m_ActionPos = actionpos * anim->m_AnimDef->GetDuration(); if (actionpos2 == -1.f) anim->m_ActionPos2 = -1.f; else anim->m_ActionPos2 = actionpos2 * anim->m_AnimDef->GetDuration(); if (soundpos == -1.f) anim->m_SoundPos = -1.f; else anim->m_SoundPos = soundpos * anim->m_AnimDef->GetDuration(); anim->m_ObjectBounds.SetEmpty(); InvalidateBounds(); return anim; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Update: update this model to the given time, in msec void CModel::UpdateTo(float time) { // update animation time, but don't calculate bone matrices - do that (lazily) when // something requests them; that saves some calculation work for offscreen models, // and also assures the world space, inverted bone matrices (required for normal // skinning) are up to date with respect to m_Transform m_AnimTime = time; // mark vertices as dirty SetDirty(RENDERDATA_UPDATE_VERTICES); // mark matrices as dirty InvalidatePosition(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // InvalidatePosition void CModel::InvalidatePosition() { m_PositionValid = false; for (size_t i = 0; i < m_Props.size(); ++i) m_Props[i].m_Model->InvalidatePosition(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // ValidatePosition: ensure that current transform and bone matrices are both uptodate void CModel::ValidatePosition() { if (m_PositionValid) { ENSURE(!m_Parent || m_Parent->m_PositionValid); return; } if (m_Parent && !m_Parent->m_PositionValid) { // Make sure we don't base our calculations on // a parent animation state that is out of date. m_Parent->ValidatePosition(); // Parent will recursively call our validation. ENSURE(m_PositionValid); return; } if (m_Anim && m_BoneMatrices) { // PROFILE( "generating bone matrices" ); ENSURE(m_pModelDef->GetNumBones() == m_Anim->m_AnimDef->GetNumKeys()); m_Anim->m_AnimDef->BuildBoneMatrices(m_AnimTime, m_BoneMatrices, !(m_Flags & MODELFLAG_NOLOOPANIMATION)); } else if (m_BoneMatrices) { // Bones but no animation - probably a buggy actor forgot to set up the animation, // so just render it in its bind pose for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) { m_BoneMatrices[i].SetIdentity(); m_BoneMatrices[i].Rotate(m_pModelDef->GetBones()[i].m_Rotation); m_BoneMatrices[i].Translate(m_pModelDef->GetBones()[i].m_Translation); } } // For CPU skinning, we precompute as much as possible so that the only // per-vertex work is a single matrix*vec multiplication. // For GPU skinning, we try to minimise CPU work by doing most computation // in the vertex shader instead. // Using g_RenderingOptions to detect CPU vs GPU is a bit hacky, // and this doesn't allow the setting to change at runtime, but there isn't // an obvious cleaner way to determine what data needs to be computed, // and GPU skinning is a rarely-used experimental feature anyway. bool worldSpaceBoneMatrices = !g_RenderingOptions.GetGPUSkinning(); bool computeBlendMatrices = !g_RenderingOptions.GetGPUSkinning(); if (m_BoneMatrices && worldSpaceBoneMatrices) { // add world-space transformation to m_BoneMatrices const CMatrix3D transform = GetTransform(); for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) m_BoneMatrices[i].Concatenate(transform); } // our own position is now valid; now we can safely update our props' positions without fearing // that doing so will cause a revalidation of this model (see recursion above). m_PositionValid = true; CMatrix3D translate; CVector3D objTranslation = m_Transform.GetTranslation(); float objectHeight = 0.0f; CmpPtr cmpTerrain(m_Simulation, SYSTEM_ENTITY); if (cmpTerrain) objectHeight = cmpTerrain->GetExactGroundLevel(objTranslation.X, objTranslation.Z); // Object height is incorrect for floating objects. We use water height instead. CmpPtr cmpWaterManager(m_Simulation, SYSTEM_ENTITY); if (cmpWaterManager) { float waterHeight = cmpWaterManager->GetExactWaterLevel(objTranslation.X, objTranslation.Z); if (waterHeight >= objectHeight && m_Flags & MODELFLAG_FLOATONWATER) objectHeight = waterHeight; } // re-position and validate all props for (const Prop& prop : m_Props) { CMatrix3D proptransform = prop.m_Point->m_Transform; if (prop.m_Point->m_BoneIndex != 0xff) { CMatrix3D boneMatrix = m_BoneMatrices[prop.m_Point->m_BoneIndex]; if (!worldSpaceBoneMatrices) boneMatrix.Concatenate(GetTransform()); proptransform.Concatenate(boneMatrix); } else { // not relative to any bone; just apply world-space transformation (i.e. relative to object-space origin) proptransform.Concatenate(m_Transform); } // Adjust prop height to terrain level when needed if (cmpTerrain && (prop.m_MaxHeight != 0.f || prop.m_MinHeight != 0.f)) { const CVector3D& propTranslation = proptransform.GetTranslation(); const float propTerrain = cmpTerrain->GetExactGroundLevel(propTranslation.X, propTranslation.Z); const float translateHeight = std::min(prop.m_MaxHeight, std::max(prop.m_MinHeight, propTerrain - objectHeight)); translate.SetTranslation(0.f, translateHeight, 0.f); proptransform.Concatenate(translate); } prop.m_Model->SetTransform(proptransform); prop.m_Model->ValidatePosition(); } if (m_BoneMatrices) { for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) { m_BoneMatrices[i] = m_BoneMatrices[i] * m_pModelDef->GetInverseBindBoneMatrices()[i]; } // Note: there is a special case of joint influence, in which the vertex // is influenced by the bind-shape transform instead of a particular bone, // which we indicate with the blending bone ID set to the total number // of bones. But since we're skinning in world space, we use the model's // world space transform and store that matrix in this special index. // (see http://trac.wildfiregames.com/ticket/1012) m_BoneMatrices[m_pModelDef->GetNumBones()] = m_Transform; if (computeBlendMatrices) m_pModelDef->BlendBoneMatrices(m_BoneMatrices); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // SetAnimation: set the given animation as the current animation on this model; // return false on error, else true bool CModel::SetAnimation(CSkeletonAnim* anim, bool once) { m_Anim = nullptr; // in case something fails if (anim) { m_Flags &= ~MODELFLAG_NOLOOPANIMATION; if (once) m_Flags |= MODELFLAG_NOLOOPANIMATION; // Not rigged or animation is not valid. if (!m_BoneMatrices || !anim->m_AnimDef) return false; if (anim->m_AnimDef->GetNumKeys() != m_pModelDef->GetNumBones()) { LOGERROR("Mismatch between model's skeleton and animation's skeleton (%s.dae has %lu model bones while the animation %s has %lu animation keys.)", m_pModelDef->GetName().string8().c_str() , static_cast(m_pModelDef->GetNumBones()), anim->m_Name.c_str(), static_cast(anim->m_AnimDef->GetNumKeys())); return false; } // Reset the cached bounds when the animation is changed. m_ObjectBounds.SetEmpty(); InvalidateBounds(); // Start anim from beginning. m_AnimTime = 0; } m_Anim = anim; return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CopyAnimation void CModel::CopyAnimationFrom(CModel* source) { m_Anim = source->m_Anim; m_AnimTime = source->m_AnimTime; m_ObjectBounds.SetEmpty(); InvalidateBounds(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // AddProp: add a prop to the model on the given point void CModel::AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight, float maxHeight, bool selectable) { // position model according to prop point position // this next call will invalidate the bounds of "model", which will in turn also invalidate the selection box model->SetTransform(point->m_Transform); model->m_Parent = this; Prop prop; prop.m_Point = point; prop.m_Model = model; prop.m_ObjectEntry = objectentry; prop.m_MinHeight = minHeight; prop.m_MaxHeight = maxHeight; prop.m_Selectable = selectable; m_Props.push_back(prop); } void CModel::AddAmmoProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry) { AddProp(point, model, objectentry); m_AmmoPropPoint = point; m_AmmoLoadedProp = m_Props.size() - 1; m_Props[m_AmmoLoadedProp].m_Hidden = true; // we only need to invalidate the selection box here if it is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } void CModel::ShowAmmoProp() { if (m_AmmoPropPoint == NULL) return; // Show the ammo prop, hide all others on the same prop point for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Point == m_AmmoPropPoint) m_Props[i].m_Hidden = (i != m_AmmoLoadedProp); // we only need to invalidate the selection box here if it is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } void CModel::HideAmmoProp() { if (m_AmmoPropPoint == NULL) return; // Hide the ammo prop, show all others on the same prop point for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Point == m_AmmoPropPoint) m_Props[i].m_Hidden = (i == m_AmmoLoadedProp); // we only need to invalidate here if the selection box is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } CModelAbstract* CModel::FindFirstAmmoProp() { if (m_AmmoPropPoint) return m_Props[m_AmmoLoadedProp].m_Model; for (size_t i = 0; i < m_Props.size(); ++i) { CModel* propModel = m_Props[i].m_Model->ToCModel(); if (propModel) { CModelAbstract* model = propModel->FindFirstAmmoProp(); if (model) return model; } } return NULL; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Clone: return a clone of this model CModelAbstract* CModel::Clone() const { CModel* clone = new CModel(m_SkeletonAnimManager, m_Simulation); clone->m_ObjectBounds = m_ObjectBounds; clone->InitModel(m_pModelDef); clone->SetMaterial(m_Material); clone->SetAnimation(m_Anim); clone->SetFlags(m_Flags); for (size_t i = 0; i < m_Props.size(); i++) { // eek! TODO, RC - need to investigate shallow clone here if (m_AmmoPropPoint && i == m_AmmoLoadedProp) clone->AddAmmoProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry); else clone->AddProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry, m_Props[i].m_MinHeight, m_Props[i].m_MaxHeight, m_Props[i].m_Selectable); } return clone; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // SetTransform: set the transform on this object, and reorientate props accordingly void CModel::SetTransform(const CMatrix3D& transform) { // call base class to set transform on this object CRenderableObject::SetTransform(transform); InvalidatePosition(); } ////////////////////////////////////////////////////////////////////////// void CModel::AddFlagsRec(int flags) { m_Flags |= flags; if (flags & MODELFLAG_IGNORE_LOS) { m_Material.AddShaderDefine(str_IGNORE_LOS, str_1); m_Material.RecomputeCombinedShaderDefines(); } for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Model->ToCModel()) m_Props[i].m_Model->ToCModel()->AddFlagsRec(flags); } void CModel::RemoveShadowsRec() { m_Flags &= ~MODELFLAG_CASTSHADOWS; m_Material.AddShaderDefine(str_DISABLE_RECEIVE_SHADOWS, str_1); m_Material.RecomputeCombinedShaderDefines(); for (size_t i = 0; i < m_Props.size(); ++i) { if (m_Props[i].m_Model->ToCModel()) m_Props[i].m_Model->ToCModel()->RemoveShadowsRec(); else if (m_Props[i].m_Model->ToCModelDecal()) m_Props[i].m_Model->ToCModelDecal()->RemoveShadows(); } } void CModel::SetMaterial(const CMaterial &material) { m_Material = material; } void CModel::SetPlayerID(player_id_t id) { CModelAbstract::SetPlayerID(id); for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it) it->m_Model->SetPlayerID(id); } void CModel::SetShadingColor(const CColor& color) { CModelAbstract::SetShadingColor(color); for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it) it->m_Model->SetShadingColor(color); } Index: ps/trunk/source/graphics/Terrain.cpp =================================================================== --- ps/trunk/source/graphics/Terrain.cpp (revision 26120) +++ ps/trunk/source/graphics/Terrain.cpp (revision 26121) @@ -1,848 +1,842 @@ /* 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 . */ -/* - * Describes ground via heightmap and array of CPatch. - */ - #include "precompiled.h" -#include "lib/res/graphics/ogl_tex.h" -#include "lib/sysdep/cpu.h" - -#include "renderer/Renderer.h" - -#include "TerrainProperties.h" -#include "TerrainTextureEntry.h" -#include "TerrainTextureManager.h" +#include "graphics/Terrain.h" -#include -#include "Terrain.h" -#include "Patch.h" +#include "graphics/Patch.h" +#include "graphics/TerrainProperties.h" +#include "graphics/TerrainTextureEntry.h" +#include "graphics/TerrainTextureManager.h" +#include "lib/sysdep/cpu.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" +#include "renderer/Renderer.h" #include "simulation2/helpers/Pathfinding.h" +#include + /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::CTerrain() : m_Heightmap(0), m_Patches(0), m_MapSize(0), m_MapSizePatches(0), m_BaseColor(255, 255, 255, 255) { } /////////////////////////////////////////////////////////////////////////////// // CTerrain constructor CTerrain::~CTerrain() { ReleaseData(); } /////////////////////////////////////////////////////////////////////////////// // ReleaseData: delete any data allocated by this terrain void CTerrain::ReleaseData() { m_HeightMipmap.ReleaseData(); delete[] m_Heightmap; delete[] m_Patches; } /////////////////////////////////////////////////////////////////////////////// // Initialise: initialise this terrain to the given size // using given heightmap to setup elevation data bool CTerrain::Initialize(ssize_t patchesPerSide, const u16* data) { // clean up any previous terrain ReleaseData(); // store terrain size m_MapSize = patchesPerSide * PATCH_SIZE + 1; m_MapSizePatches = patchesPerSide; // allocate data for new terrain m_Heightmap = new u16[m_MapSize * m_MapSize]; m_Patches = new CPatch[m_MapSizePatches * m_MapSizePatches]; // given a heightmap? if (data) { // yes; keep a copy of it memcpy(m_Heightmap, data, m_MapSize*m_MapSize*sizeof(u16)); } else { // build a flat terrain memset(m_Heightmap, 0, m_MapSize*m_MapSize*sizeof(u16)); } // setup patch parents, indices etc InitialisePatches(); // initialise mipmap m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); return true; } /////////////////////////////////////////////////////////////////////////////// CStr8 CTerrain::GetMovementClass(ssize_t i, ssize_t j) const { CMiniPatch* tile = GetTile(i, j); if (tile && tile->GetTextureEntry()) return tile->GetTextureEntry()->GetProperties().GetMovementClass(); return "default"; } /////////////////////////////////////////////////////////////////////////////// // CalcPosition: calculate the world space position of the vertex at (i,j) // If i,j is off the map, it acts as if the edges of the terrain are extended // outwards to infinity void CTerrain::CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const { ssize_t hi = Clamp(i, 0, m_MapSize - 1); ssize_t hj = Clamp(j, 0, m_MapSize - 1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = float(i*TERRAIN_TILE_SIZE); pos.Y = float(height*HEIGHT_SCALE); pos.Z = float(j*TERRAIN_TILE_SIZE); } /////////////////////////////////////////////////////////////////////////////// // CalcPositionFixed: calculate the world space position of the vertex at (i,j) void CTerrain::CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const { ssize_t hi = Clamp(i, 0, m_MapSize - 1); ssize_t hj = Clamp(j, 0, m_MapSize - 1); u16 height = m_Heightmap[hj*m_MapSize + hi]; pos.X = fixed::FromInt(i) * (int)TERRAIN_TILE_SIZE; // fixed max value is 32767, but height is a u16, so divide by two to avoid overflow pos.Y = fixed::FromInt(height/ 2 ) / ((int)HEIGHT_UNITS_PER_METRE / 2); pos.Z = fixed::FromInt(j) * (int)TERRAIN_TILE_SIZE; } /////////////////////////////////////////////////////////////////////////////// // CalcNormal: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const { CVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CVector3D basepos; CalcPosition(i, j, basepos); if (i > 0) { CalcPosition(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPosition(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPosition(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPosition(i, j+1, down); down -= basepos; down.Normalize(); } CVector3D n0 = up.Cross(left); CVector3D n1 = left.Cross(down); CVector3D n2 = down.Cross(right); CVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; float nlen=normal.Length(); if (nlen>0.00001f) normal*=1.0f/nlen; } /////////////////////////////////////////////////////////////////////////////// // CalcNormalFixed: calculate the world space normal of the vertex at (i,j) void CTerrain::CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const { CFixedVector3D left, right, up, down; // Calculate normals of the four half-tile triangles surrounding this vertex: // get position of vertex where normal is being evaluated CFixedVector3D basepos; CalcPositionFixed(i, j, basepos); if (i > 0) { CalcPositionFixed(i-1, j, left); left -= basepos; left.Normalize(); } if (i < m_MapSize-1) { CalcPositionFixed(i+1, j, right); right -= basepos; right.Normalize(); } if (j > 0) { CalcPositionFixed(i, j-1, up); up -= basepos; up.Normalize(); } if (j < m_MapSize-1) { CalcPositionFixed(i, j+1, down); down -= basepos; down.Normalize(); } CFixedVector3D n0 = up.Cross(left); CFixedVector3D n1 = left.Cross(down); CFixedVector3D n2 = down.Cross(right); CFixedVector3D n3 = right.Cross(up); // Compute the mean of the normals normal = n0 + n1 + n2 + n3; normal.Normalize(); } CVector3D CTerrain::CalcExactNormal(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp(floor(x / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const ssize_t zi = Clamp(floor(z / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const float xf = Clamp(x / TERRAIN_TILE_SIZE-xi, 0.0f, 1.0f); const float zf = Clamp(z / TERRAIN_TILE_SIZE-zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the normal of that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Upper-right triangle (don't use h00) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } else { // Lower-right triangle (don't use h01) return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized(); } } } /////////////////////////////////////////////////////////////////////////////// // GetPatch: return the patch at (i,j) in patch space, or null if the patch is // out of bounds CPatch* CTerrain::GetPatch(ssize_t i, ssize_t j) const { // range check (invalid indices are passed in by the culling and // patch blend code because they iterate from 0..#patches and examine // neighbors without checking if they're already on the edge) if( (size_t)i >= (size_t)m_MapSizePatches || (size_t)j >= (size_t)m_MapSizePatches ) return 0; return &m_Patches[(j*m_MapSizePatches)+i]; } /////////////////////////////////////////////////////////////////////////////// // GetTile: return the tile at (i,j) in tile space, or null if the tile is out // of bounds CMiniPatch* CTerrain::GetTile(ssize_t i, ssize_t j) const { // see comment above if( (size_t)i >= (size_t)(m_MapSize-1) || (size_t)j >= (size_t)(m_MapSize-1) ) return 0; CPatch* patch=GetPatch(i/PATCH_SIZE, j/PATCH_SIZE); // can't fail (due to above check) return &patch->m_MiniPatches[j%PATCH_SIZE][i%PATCH_SIZE]; } float CTerrain::GetVertexGroundLevel(ssize_t i, ssize_t j) const { i = Clamp(i, 0, m_MapSize - 1); j = Clamp(j, 0, m_MapSize - 1); return HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i]; } fixed CTerrain::GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const { i = Clamp(i, 0, m_MapSize - 1); j = Clamp(j, 0, m_MapSize - 1); // Convert to fixed metres (being careful to avoid intermediate overflows) return fixed::FromInt(m_Heightmap[j*m_MapSize + i] / 2) / (int)(HEIGHT_UNITS_PER_METRE / 2); } fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = Clamp(i, 0, m_MapSize - 2); j = Clamp(j, 0, m_MapSize - 2); u16 h00 = m_Heightmap[j*m_MapSize + i]; u16 h01 = m_Heightmap[(j+1)*m_MapSize + i]; u16 h10 = m_Heightmap[j*m_MapSize + (i+1)]; u16 h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Difference of highest point from lowest point u16 delta = std::max(std::max(h00, h01), std::max(h10, h11)) - std::min(std::min(h00, h01), std::min(h10, h11)); // Compute fractional slope (being careful to avoid intermediate overflows) return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE; } fixed CTerrain::GetExactSlopeFixed(fixed x, fixed z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const fixed one = fixed::FromInt(1); const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one); const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one); u16 h00 = m_Heightmap[zi*m_MapSize + xi]; u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; u16 delta; if (GetTriangulationDir(xi, zi)) { if (xf + zf <= one) { // Lower-left triangle (don't use h11) delta = std::max(std::max(h00, h01), h10) - std::min(std::min(h00, h01), h10); } else { // Upper-right triangle (don't use h00) delta = std::max(std::max(h01, h10), h11) - std::min(std::min(h01, h10), h11); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) delta = std::max(std::max(h00, h01), h11) - std::min(std::min(h00, h01), h11); } else { // Lower-right triangle (don't use h01) delta = std::max(std::max(h00, h10), h11) - std::min(std::min(h00, h10), h11); } } // Compute fractional slope (being careful to avoid intermediate overflows) return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE; } float CTerrain::GetFilteredGroundLevel(float x, float z, float radius) const { // convert to [0,1] interval float nx = x / (TERRAIN_TILE_SIZE*m_MapSize); float nz = z / (TERRAIN_TILE_SIZE*m_MapSize); float nr = radius / (TERRAIN_TILE_SIZE*m_MapSize); // get trilinear filtered mipmap height return HEIGHT_SCALE * m_HeightMipmap.GetTrilinearGroundLevel(nx, nz, nr); } float CTerrain::GetExactGroundLevel(float x, float z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp(floor(x / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const ssize_t zi = Clamp(floor(z / TERRAIN_TILE_SIZE), 0, m_MapSize - 2); const float xf = Clamp(x / TERRAIN_TILE_SIZE - xi, 0.0f, 1.0f); const float zf = Clamp(z / TERRAIN_TILE_SIZE - zi, 0.0f, 1.0f); float h00 = m_Heightmap[zi*m_MapSize + xi]; float h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; float h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Determine which terrain triangle this point is on, // then compute the linearly-interpolated height on that triangle's plane if (GetTriangulationDir(xi, zi)) { if (xf + zf <= 1.f) { // Lower-left triangle (don't use h11) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h01-h00)*zf); } else { // Upper-right triangle (don't use h00) return HEIGHT_SCALE * (h11 + (h01-h11)*(1-xf) + (h10-h11)*(1-zf)); } } else { if (xf <= zf) { // Upper-left triangle (don't use h10) return HEIGHT_SCALE * (h00 + (h11-h01)*xf + (h01-h00)*zf); } else { // Lower-right triangle (don't use h01) return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h11-h10)*zf); } } } fixed CTerrain::GetExactGroundLevelFixed(fixed x, fixed z) const { // Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1) const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2); const fixed one = fixed::FromInt(1); const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one); const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one); u16 h00 = m_Heightmap[zi*m_MapSize + xi]; u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi]; u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)]; u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)]; // Intermediate scaling of xf, so we don't overflow in the multiplications below // (h00 <= 65535, xf <= 1, max fixed is < 32768; divide by 2 here so xf1*h00 <= 32767.5) const fixed xf0 = xf / 2; const fixed xf1 = (one - xf) / 2; // Linearly interpolate return ((one - zf).Multiply(xf1 * h00 + xf0 * h10) + zf.Multiply(xf1 * h01 + xf0 * h11)) / (int)(HEIGHT_UNITS_PER_METRE / 2); // TODO: This should probably be more like GetExactGroundLevel() // in handling triangulation properly } bool CTerrain::GetTriangulationDir(ssize_t i, ssize_t j) const { // Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1) i = Clamp(i, 0, m_MapSize - 2); j = Clamp(j, 0, m_MapSize - 2); int h00 = m_Heightmap[j*m_MapSize + i]; int h01 = m_Heightmap[(j+1)*m_MapSize + i]; int h10 = m_Heightmap[j*m_MapSize + (i+1)]; int h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)]; // Prefer triangulating in whichever direction means the midpoint of the diagonal // will be the highest. (In particular this means a diagonal edge will be straight // along the top, and jagged along the bottom, which makes sense for terrain.) int mid1 = h00+h11; int mid2 = h01+h10; return (mid1 < mid2); } void CTerrain::ResizeAndOffset(ssize_t size, ssize_t horizontalOffset, ssize_t verticalOffset) { if (size == m_MapSizePatches && horizontalOffset == 0 && verticalOffset == 0) { // Inexplicable request to resize terrain to the same size, ignore it. return; } if (!m_Heightmap || std::abs(horizontalOffset) >= size / 2 + m_MapSizePatches / 2 || std::abs(verticalOffset) >= size / 2 + m_MapSizePatches / 2) { // We have not yet created a terrain, or we are offsetting outside the current source. // Let's build a default terrain of the given size now. Initialize(size, 0); return; } // Allocate data for new terrain. const ssize_t newMapSize = size * PATCH_SIZE + 1; u16* newHeightmap = new u16[newMapSize * newMapSize]; memset(newHeightmap, 0, newMapSize * newMapSize * sizeof(u16)); CPatch* newPatches = new CPatch[size * size]; // O--------------------+ // | Source | // | | // | Source Center (SC) | // | X | // | A------+----------------+ // | | | Destination | // | | | | // +-------------+------B | // | Dest. Center (DC) | // | X | // | | // | | // | | // | | // +-----------------------+ // // Calculations below should also account cases like: // // +----------+ +----------+ +----------+ +---+--+---+ +------+ // |S | |D | |S | |S | | D| |D | // | +---+ | | +---+ | +-+-+ | | | | | | +---+--+ // | | D | | | | S | | |D| | | +---+--+---+ +--+---+ | // | +---+ | | +---+ | +-+-+ | | S| // +----------+ +----------+ +----------+ +------+ // // O = (0, 0) // SC = (m_MapSizePatches / 2, m_MapSizePatches / 2) // DC - SC = (horizontalOffset, verticalOffset) // // Source upper left: // A = (max(0, (m_MapSizePatches - size) / 2 + horizontalOffset), // max(0, (m_MapSizePatches - size) / 2 + verticalOffset)) // Source bottom right: // B = (min(m_MapSizePatches, (m_MapSizePatches + size) / 2 + horizontalOffset), // min(m_MapSizePatches, (m_MapSizePatches + size) / 2 + verticalOffset)) // // A-B is the area that we have to copy from the source to the destination. // Restate center offset as a window over destination. // This has the effect of always considering the source to be the same size or smaller. const ssize_t sourceUpperLeftX = std::max( static_cast(0), m_MapSizePatches / 2 - size / 2 + horizontalOffset); const ssize_t sourceUpperLeftZ = std::max( static_cast(0), m_MapSizePatches / 2 - size / 2 + verticalOffset); const ssize_t destUpperLeftX = std::max( static_cast(0), (size / 2 - m_MapSizePatches / 2 - horizontalOffset)); const ssize_t destUpperLeftZ = std::max( static_cast(0), (size / 2 - m_MapSizePatches / 2 - verticalOffset)); const ssize_t width = std::min(m_MapSizePatches, m_MapSizePatches / 2 + horizontalOffset + size / 2) - sourceUpperLeftX; const ssize_t depth = std::min(m_MapSizePatches, m_MapSizePatches / 2 + verticalOffset + size / 2) - sourceUpperLeftZ; for (ssize_t j = 0; j < depth * PATCH_SIZE; ++j) { // Copy the main part from the source. Destination heightmap: // +----------+ // | | // | 1234 | < current j-th row for example. // | 5678 | // | | // +----------+ u16* dst = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize + destUpperLeftX * PATCH_SIZE; u16* src = m_Heightmap + (j + sourceUpperLeftZ * PATCH_SIZE) * m_MapSize + sourceUpperLeftX * PATCH_SIZE; std::copy_n(src, width * PATCH_SIZE, dst); if (destUpperLeftX > 0) { // Fill the preceding part by copying the first elements of the // main part. Destination heightmap: // +----------+ // | | // |1111234 | < current j-th row for example. // | 5678 | // | | // +----------+ u16* dst_prefix = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize; std::fill_n(dst_prefix, destUpperLeftX * PATCH_SIZE, dst[0]); } if ((destUpperLeftX + width) * PATCH_SIZE < newMapSize) { // Fill the succeeding part by copying the last elements of the // main part. Destination heightmap: // +----------+ // | | // |1111234444| < current j-th row for example. // | 5678 | // | | // +----------+ u16* dst_suffix = dst + width * PATCH_SIZE; std::fill_n( dst_suffix, newMapSize - (width + destUpperLeftX) * PATCH_SIZE, dst[width * PATCH_SIZE - 1]); } } // Copy over heights from the preceding row. Destination heightmap: // +----------+ // |1111234444| < copied from the row below // |1111234444| // |5555678888| // | | // +----------+ for (ssize_t j = 0; j < destUpperLeftZ * PATCH_SIZE; ++j) { u16* dst = newHeightmap + j * newMapSize; u16* src = newHeightmap + destUpperLeftZ * PATCH_SIZE * newMapSize; std::copy_n(src, newMapSize, dst); } // Copy over heights from the succeeding row. Destination heightmap: // +----------+ // |1111234444| // |1111234444| // |5555678888| // |5555678888| < copied from the row above // +----------+ for (ssize_t j = (destUpperLeftZ + depth) * PATCH_SIZE; j < newMapSize; ++j) { u16* dst = newHeightmap + j * newMapSize; u16* src = newHeightmap + ((destUpperLeftZ + depth) * PATCH_SIZE - 1) * newMapSize; std::copy_n(src, newMapSize, dst); } // Now build new patches. The same process as for the heightmap. for (ssize_t j = 0; j < depth; ++j) { for (ssize_t i = 0; i < width; ++i) { const CPatch& src = m_Patches[(sourceUpperLeftZ + j) * m_MapSizePatches + sourceUpperLeftX + i]; CPatch& dst = newPatches[(destUpperLeftZ + j) * size + destUpperLeftX + i]; std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); } for (ssize_t i = 0; i < destUpperLeftX; ++i) for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) { const CMiniPatch& src = newPatches[(destUpperLeftZ + j) * size + destUpperLeftX] .m_MiniPatches[jPatch][0]; for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) { CMiniPatch& dst = newPatches[(destUpperLeftZ + j) * size + i] .m_MiniPatches[jPatch][iPatch]; dst = src; } } for (ssize_t i = destUpperLeftX + width; i < size; ++i) { for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) { const CMiniPatch& src = newPatches[(destUpperLeftZ + j) * size + destUpperLeftX + width - 1] .m_MiniPatches[jPatch][PATCH_SIZE - 1]; for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) { CMiniPatch& dst = newPatches[(destUpperLeftZ + j) * size + i].m_MiniPatches[jPatch][iPatch]; dst = src; } } } } for (ssize_t j = 0; j < destUpperLeftZ; ++j) for (ssize_t i = 0; i < size; ++i) for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) { const CMiniPatch& src = newPatches[destUpperLeftZ * size + i].m_MiniPatches[0][iPatch]; for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) { CMiniPatch& dst = newPatches[j * size + i].m_MiniPatches[jPatch][iPatch]; dst = src; } } for (ssize_t j = destUpperLeftZ + depth; j < size; ++j) for (ssize_t i = 0; i < size; ++i) for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch) { const CMiniPatch& src = newPatches[(destUpperLeftZ + depth - 1) * size + i].m_MiniPatches[0][iPatch]; for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch) { CMiniPatch& dst = newPatches[j * size + i].m_MiniPatches[jPatch][iPatch]; dst = src; } } // Release all the original data. ReleaseData(); // Store new data. m_Heightmap = newHeightmap; m_Patches = newPatches; m_MapSize = newMapSize; m_MapSizePatches = size; // Initialise all the new patches. InitialisePatches(); // Initialise mipmap. m_HeightMipmap.Initialize(m_MapSize, m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// // InitialisePatches: initialise patch data void CTerrain::InitialisePatches() { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->Initialize(this, i, j); } } } /////////////////////////////////////////////////////////////////////////////// // SetHeightMap: set up a new heightmap from 16-bit source data; // assumes heightmap matches current terrain size void CTerrain::SetHeightMap(u16* heightmap) { // keep a copy of the given heightmap memcpy(m_Heightmap, heightmap, m_MapSize*m_MapSize*sizeof(u16)); // recalculate patch bounds, invalidate vertices for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail patch->InvalidateBounds(); patch->SetDirty(RENDERDATA_UPDATE_VERTICES); } } // update mipmap m_HeightMipmap.Update(m_Heightmap); } /////////////////////////////////////////////////////////////////////////////// void CTerrain::MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags) { // Finds the inclusive limits of the patches that include the specified range of tiles ssize_t pi0 = Clamp( i0 /PATCH_SIZE, 0, m_MapSizePatches-1); ssize_t pi1 = Clamp((i1-1)/PATCH_SIZE, 0, m_MapSizePatches-1); ssize_t pj0 = Clamp( j0 /PATCH_SIZE, 0, m_MapSizePatches-1); ssize_t pj1 = Clamp((j1-1)/PATCH_SIZE, 0, m_MapSizePatches-1); for (ssize_t j = pj0; j <= pj1; j++) { for (ssize_t i = pi0; i <= pi1; i++) { CPatch* patch = GetPatch(i, j); // can't fail (i,j were clamped) if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } if (m_Heightmap) { m_HeightMipmap.Update(m_Heightmap, Clamp(i0, 0, m_MapSize - 1), Clamp(j0, 0, m_MapSize - 1), Clamp(i1, 1, m_MapSize), Clamp(j1, 1, m_MapSize) ); } } void CTerrain::MakeDirty(int dirtyFlags) { for (ssize_t j = 0; j < m_MapSizePatches; j++) { for (ssize_t i = 0; i < m_MapSizePatches; i++) { CPatch* patch = GetPatch(i, j); // can't fail if (dirtyFlags & RENDERDATA_UPDATE_VERTICES) patch->CalcBounds(); patch->SetDirty(dirtyFlags); } } if (m_Heightmap) m_HeightMipmap.Update(m_Heightmap); } CBoundingBoxAligned CTerrain::GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) { i0 = Clamp(i0, 0, m_MapSize - 1); j0 = Clamp(j0, 0, m_MapSize - 1); i1 = Clamp(i1, 0, m_MapSize - 1); j1 = Clamp(j1, 0, m_MapSize - 1); u16 minH = 65535; u16 maxH = 0; for (ssize_t j = j0; j <= j1; ++j) { for (ssize_t i = i0; i <= i1; ++i) { minH = std::min(minH, m_Heightmap[j*m_MapSize + i]); maxH = std::max(maxH, m_Heightmap[j*m_MapSize + i]); } } CBoundingBoxAligned bound; bound[0].X = (float)(i0*TERRAIN_TILE_SIZE); bound[0].Y = (float)(minH*HEIGHT_SCALE); bound[0].Z = (float)(j0*TERRAIN_TILE_SIZE); bound[1].X = (float)(i1*TERRAIN_TILE_SIZE); bound[1].Y = (float)(maxH*HEIGHT_SCALE); bound[1].Z = (float)(j1*TERRAIN_TILE_SIZE); return bound; } Index: ps/trunk/source/graphics/TerrainTextureManager.cpp =================================================================== --- ps/trunk/source/graphics/TerrainTextureManager.cpp (revision 26120) +++ ps/trunk/source/graphics/TerrainTextureManager.cpp (revision 26121) @@ -1,148 +1,146 @@ /* 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 "precompiled.h" #include "TerrainTextureManager.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainProperties.h" -#include "lib/ogl.h" #include "lib/timer.h" -#include "lib/res/graphics/ogl_tex.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include #include #include CTerrainTextureManager::CTerrainTextureManager() : m_LastGroupIndex(0) { if (!VfsDirectoryExists(L"art/terrains/")) return; if (!CXeromyces::AddValidator(g_VFS, "terrain", "art/terrains/terrain.rng")) LOGERROR("CTerrainTextureManager: failed to load grammar file 'art/terrains/terrain.rng'"); if (!CXeromyces::AddValidator(g_VFS, "terrain_texture", "art/terrains/terrain_texture.rng")) LOGERROR("CTerrainTextureManager: failed to load grammar file 'art/terrains/terrain_texture.rng'"); } CTerrainTextureManager::~CTerrainTextureManager() { UnloadTerrainTextures(); for (std::pair& ta : m_TerrainAlphas) ta.second.m_CompositeAlphaMap.reset(); } void CTerrainTextureManager::UnloadTerrainTextures() { for (CTerrainTextureEntry* const& te : m_TextureEntries) delete te; m_TextureEntries.clear(); for (const std::pair& tg : m_TerrainGroups) delete tg.second; m_TerrainGroups.clear(); m_LastGroupIndex = 0; } CTerrainTextureEntry* CTerrainTextureManager::FindTexture(const CStr& tag_) const { CStr tag = tag_.BeforeLast("."); // Strip extension for (CTerrainTextureEntry* const& te : m_TextureEntries) if (te->GetTag() == tag) return te; LOGWARNING("CTerrainTextureManager: Couldn't find terrain %s", tag.c_str()); return 0; } CTerrainTextureEntry* CTerrainTextureManager::AddTexture(const CTerrainPropertiesPtr& props, const VfsPath& path) { CTerrainTextureEntry* entry = new CTerrainTextureEntry(props, path); m_TextureEntries.push_back(entry); return entry; } void CTerrainTextureManager::DeleteTexture(CTerrainTextureEntry* entry) { std::vector::iterator it = std::find(m_TextureEntries.begin(), m_TextureEntries.end(), entry); if (it != m_TextureEntries.end()) m_TextureEntries.erase(it); delete entry; } struct AddTextureCallbackData { CTerrainTextureManager* self; CTerrainPropertiesPtr props; }; static Status AddTextureDirCallback(const VfsPath& pathname, const uintptr_t cbData) { AddTextureCallbackData& data = *(AddTextureCallbackData*)cbData; VfsPath path = pathname / L"terrains.xml"; if (!VfsFileExists(path)) LOGMESSAGE("'%s' does not exist. Using previous properties.", path.string8()); else data.props = CTerrainProperties::FromXML(data.props, path); return INFO::OK; } static Status AddTextureCallback(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { AddTextureCallbackData& data = *(AddTextureCallbackData*)cbData; if (pathname.Basename() != L"terrains") data.self->AddTexture(data.props, pathname); return INFO::OK; } int CTerrainTextureManager::LoadTerrainTextures() { AddTextureCallbackData data = {this, CTerrainPropertiesPtr(new CTerrainProperties(CTerrainPropertiesPtr()))}; vfs::ForEachFile(g_VFS, L"art/terrains/", AddTextureCallback, (uintptr_t)&data, L"*.xml", vfs::DIR_RECURSIVE, AddTextureDirCallback, (uintptr_t)&data); return 0; } CTerrainGroup* CTerrainTextureManager::FindGroup(const CStr& name) { TerrainGroupMap::const_iterator it = m_TerrainGroups.find(name); if (it != m_TerrainGroups.end()) return it->second; else return m_TerrainGroups[name] = new CTerrainGroup(name, ++m_LastGroupIndex); } void CTerrainGroup::AddTerrain(CTerrainTextureEntry* pTerrain) { m_Terrains.push_back(pTerrain); } void CTerrainGroup::RemoveTerrain(CTerrainTextureEntry* pTerrain) { std::vector::iterator it = find(m_Terrains.begin(), m_Terrains.end(), pTerrain); if (it != m_Terrains.end()) m_Terrains.erase(it); } Index: ps/trunk/source/graphics/TerrainTextureManager.h =================================================================== --- ps/trunk/source/graphics/TerrainTextureManager.h (revision 26120) +++ ps/trunk/source/graphics/TerrainTextureManager.h (revision 26121) @@ -1,135 +1,134 @@ /* 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 . */ #ifndef INCLUDED_TERRAINTEXTUREMANAGER #define INCLUDED_TERRAINTEXTUREMANAGER -#include -#include -#include - -#include "lib/res/graphics/ogl_tex.h" -#include "lib/res/handle.h" #include "lib/file/vfs/vfs_path.h" +#include "lib/res/handle.h" #include "ps/CStr.h" #include "ps/Singleton.h" #include "renderer/backend/gl/Texture.h" +#include +#include +#include + // access to sole CTerrainTextureManager object #define g_TexMan CTerrainTextureManager::GetSingleton() #define NUM_ALPHA_MAPS 14 class CTerrainTextureEntry; class CTerrainProperties; typedef std::shared_ptr CTerrainPropertiesPtr; class CTerrainGroup { // name of this terrain group (as specified by the terrain XML) CStr m_Name; // "index".. basically a bogus integer that can be used by ScEd to set texture // priorities size_t m_Index; // list of textures of this type (found from the texture directory) std::vector m_Terrains; public: CTerrainGroup(CStr name, size_t index): m_Name(name), m_Index(index) {} // Add a texture entry to this terrain type void AddTerrain(CTerrainTextureEntry*); // Remove a texture entry void RemoveTerrain(CTerrainTextureEntry*); size_t GetIndex() const { return m_Index; } CStr GetName() const { return m_Name; } const std::vector &GetTerrains() const { return m_Terrains; } }; struct TerrainAlpha { - // ogl_tex handle of composite alpha map (all the alpha maps packed into one texture) + // Composite alpha map (all the alpha maps packed into one texture). std::unique_ptr m_CompositeAlphaMap; - // coordinates of each (untransformed) alpha map within the packed texture + // Coordinates of each (untransformed) alpha map within the packed texture. struct { float u0, u1, v0, v1; } m_AlphaMapCoords[NUM_ALPHA_MAPS]; }; /////////////////////////////////////////////////////////////////////////////////////////// // CTerrainTextureManager : manager class for all terrain texture objects class CTerrainTextureManager : public Singleton { friend class CTerrainTextureEntry; public: typedef std::map TerrainGroupMap; typedef std::map TerrainAlphaMap; private: // All texture entries created by this class, for easy freeing now that // textures may be in several STextureType's std::vector m_TextureEntries; TerrainGroupMap m_TerrainGroups; TerrainAlphaMap m_TerrainAlphas; size_t m_LastGroupIndex; public: // constructor, destructor CTerrainTextureManager(); ~CTerrainTextureManager(); // Find all XML's in the directory (with subdirs) and try to load them as // terrain XML's int LoadTerrainTextures(); void UnloadTerrainTextures(); CTerrainTextureEntry* FindTexture(const CStr& tag) const; // Create a texture object for a new terrain texture at path, using the // property sheet props. CTerrainTextureEntry* AddTexture(const CTerrainPropertiesPtr& props, const VfsPath& path); // Remove the texture from all our maps and lists and delete it afterwards. void DeleteTexture(CTerrainTextureEntry* entry); // Find or create a new texture group. All terrain groups are owned by the // texturemanager (TerrainTypeManager) CTerrainGroup* FindGroup(const CStr& name); const TerrainGroupMap& GetGroups() const { return m_TerrainGroups; } }; #endif Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 26120) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 26121) @@ -1,1547 +1,1546 @@ /* 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 "precompiled.h" -#include "lib/app_hooks.h" -#include "lib/config2.h" -#include "lib/input.h" -#include "lib/ogl.h" -#include "lib/timer.h" -#include "lib/external_libraries/libsdl.h" -#include "lib/file/common/file_stats.h" -#include "lib/res/h_mgr.h" +#include "ps/GameSetup/GameSetup.h" #include "graphics/Canvas2D.h" #include "graphics/CinemaManager.h" #include "graphics/Color.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" -#include "graphics/ModelDef.h" #include "graphics/MaterialManager.h" +#include "graphics/ModelDef.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" +#include "lib/app_hooks.h" +#include "lib/config2.h" +#include "lib/external_libraries/libsdl.h" +#include "lib/file/common/file_stats.h" +#include "lib/input.h" +#include "lib/ogl.h" +#include "lib/res/graphics/ogl_tex.h" +#include "lib/res/h_mgr.h" +#include "lib/timer.h" +#include "lobby/IXmppClient.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" - #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" -#include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" - +#include "renderer/ModelRenderer.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" -#include "renderer/ModelRenderer.h" #include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" -#include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" -#include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; thread_local std::shared_ptr g_ScriptContext; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); Script::ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) { g_Game->GetView()->Render(); ogl_WarnIfError(); } g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) { g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); } glDisable(GL_DEPTH_TEST); if (g_DoRenderGui) { // All GUI elements are drawn in Z order to render semi-transparent // objects correctly. g_GUI->Draw(); ogl_WarnIfError(); } // If we're in Atlas game view, render special overlays (e.g. editor bandbox). if (g_AtlasGameLoop && g_AtlasGameLoop->view) { CCanvas2D canvas; g_AtlasGameLoop->view->DrawOverlays(canvas); ogl_WarnIfError(); } g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) { g_Logger->Render(); ogl_WarnIfError(); } // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); // Mods will be mounted later. // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->Init(); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(rq.cx); Script::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); Script::SetProperty(rq, playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(rq.cx); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). in_add_handler(HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN // SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false // breaking the behavior on Windows. // https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710 // https://github.com/libsdl-org/SDL/issues/5033 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); CConfigDB::Shutdown(); TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); // On the first Init (INIT_MODS), check for command-line arguments // or use the default mods from the config and enable those. // On later engine restarts (e.g. the mod selector), we will skip this path, // to avoid overwriting the newly selected mods. if (flags & INIT_MODS) { ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); std::vector mods; if (args.Has("mod")) mods = args.GetMultiple("mod"); else { CStr modsStr; CFG_GET_VAL("mod.enabledmods", modsStr); boost::split(mods, modsStr, boost::algorithm::is_space(), boost::token_compress_on); } if (!g_Mods.EnableMods(mods, flags & INIT_MODS_PUBLIC)) { // In non-visual mode, fail entirely. if (args.Has("autostart-nonvisual")) { LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods.GetIncompatibleMods(), ", ")); return false; } } } // If there are incompatible mods, switch to the mod selector so players can resolve the problem. if (g_Mods.GetIncompatibleMods().empty()) MountMods(Paths(args), g_Mods.GetEnabledMods()); else MountMods(Paths(args), { "mod" }); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); if (g_AtlasGameLoop && g_AtlasGameLoop->view) SetTextureQuality(SANE_TEX_QUALITY_DEFAULT); else { int textureQuality = SANE_TEX_QUALITY_DEFAULT; CFG_GET_VAL("texturequality", textureQuality); SetTextureQuality(textureQuality); } ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if (RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } ogl_WarnIfError(); g_RenderingOptions.ReadConfigAndSetupHooks(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup std::shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { Script::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) Script::SetProperty(rq, data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); Script::CreateObject(rq, &attrs); Script::CreateObject(rq, &settings); Script::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(rq.cx); Script::ReadJSONFile(rq, scriptPath, &scriptData); if (!scriptData.isUndefined() && Script::GetProperty(rq, scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; if (!Script::GetProperty(rq, settings, "Script", scriptFile)) { LOGERROR("Autostart: random map '%s' data has no 'Script' property.", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } Script::SetProperty(rq, attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } Script::SetProperty(rq, settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway Script::CreateObject(rq, &player, "Civ", "athen"); Script::SetPropertyInt(rq, playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); Script::ParseJSON(rq, mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: Script::GetProperty(rq, settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } Script::SetProperty(rq, attrs, "mapType", mapType); Script::SetProperty(rq, attrs, "map", "maps/" + autoStartName); Script::SetProperty(rq, settings, "mapType", mapType); Script::SetProperty(rq, settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } Script::SetProperty(rq, settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (Script::GetPropertyInt(rq, playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } Script::CreateObject(rq, ¤tPlayer); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; Script::SetProperty(rq, currentPlayer, "Team", teamID); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); Script::SetProperty(rq, settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } Script::CreateObject(rq, ¤tPlayer); } Script::SetProperty(rq, currentPlayer, "AI", aiArgs[i].AfterFirst(":")); Script::SetProperty(rq, currentPlayer, "AIDiff", 3); Script::SetProperty(rq, currentPlayer, "AIBehavior", "balanced"); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } Script::CreateObject(rq, ¤tPlayer); } Script::SetProperty(rq, currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } Script::CreateObject(rq, ¤tPlayer); } Script::SetProperty(rq, currentPlayer, "Civ", civArgs[i].AfterFirst(":")); Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings Script::SetProperty(rq, settings, "PlayerData", playerData); // Add map settings to game attributes Script::SetProperty(rq, attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (Script::HasProperty(rq, settings, "TriggerScripts")) { Script::GetProperty(rq, settings, "TriggerScripts", &triggerScripts); Script::FromJSVal(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); Script::SetProperty(rq, settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(rq.cx); JS::RootedValue data(rq.cx); JS::RootedValue victoryScripts(rq.cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; Script::ReadJSONFile(rq, scriptPath, &scriptData); if (!scriptData.isUndefined() && Script::GetProperty(rq, scriptData, "Data", &data) && !data.isUndefined() && Script::GetProperty(rq, data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; Script::FromJSVal(rq, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } Script::ToJSVal(rq, &triggerScripts, triggerScriptsVector); Script::SetProperty(rq, settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); Script::SetProperty(rq, settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); Script::SetProperty(rq, settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); Script::SetProperty(rq, settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->SetControllerSecret(secret); g_NetServer->UpdateInitAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); g_NetClient->SetControllerSecret(secret); g_NetClient->SetupConnection(nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false); ENSURE(g_NetClient->SetupConnection(nullptr)); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && Script::HasProperty(rq, global, "cancelOnLoadGameError")) ScriptFunction::CallVoid(rq, global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/GameSetup/GameSetup.h =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.h (revision 26120) +++ ps/trunk/source/ps/GameSetup/GameSetup.h (revision 26121) @@ -1,100 +1,105 @@ /* 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 . */ #ifndef INCLUDED_GAMESETUP #define INCLUDED_GAMESETUP +#include "ps/CStr.h" + +#include + +class CmdLineArgs; +class Paths; + // // GUI integration // // display progress / description in loading screen extern void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task); extern void Render(); extern bool ShouldRender(); /** * initialize global modules that are be needed before Init. * must be called from the very beginning of main. **/ extern void EarlyInit(); extern void EndGame(); enum InitFlags { // avoid setting a video mode / initializing OpenGL; assume that has // already been done and everything is ready for rendering. // needed by map editor because it creates its own window. INIT_HAVE_VMODE = 1, // skip initializing the in-game GUI. // needed by map editor because it uses its own GUI. INIT_NO_GUI = 2, // avoid setting display_error app hook // needed by map editor because it has its own wx error display INIT_HAVE_DISPLAY_ERROR = 4, // initialize the mod folders from command line parameters INIT_MODS = 8, // mount the public mod // needed by the map editor as "mod" does not provide everything it needs INIT_MODS_PUBLIC = 16 }; enum ShutdownFlags { // start shutdown from config down // needed for loading mods as specified in the config // without having to go through a full init-shutdown cycle SHUTDOWN_FROM_CONFIG = 1 }; /** * enable/disable rendering of the GUI (intended mainly for screenshots) */ extern void RenderGui(bool RenderingState); extern void RenderLogger(bool RenderingState); -class CmdLineArgs; -class Paths; extern const std::vector& GetMods(const CmdLineArgs& args, int flags); /** * Mounts all files of the given mods in the global VFS. * Make sure to call CacheEnabledModVersions after every call to this. */ extern void MountMods(const Paths& paths, const std::vector& mods); /** * Returns true if successful, false if mods changed and restart_engine was called. * In the latter case the caller should call Shutdown() with SHUTDOWN_FROM_CONFIG. */ extern bool Init(const CmdLineArgs& args, int flags); extern void InitInput(); extern void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods = std::vector()); extern void InitNonVisual(const CmdLineArgs& args); extern void Shutdown(int flags); extern void CancelLoad(const CStrW& message); extern bool InDevelopmentCopy(); #endif // INCLUDED_GAMESETUP Index: ps/trunk/source/renderer/SkyManager.cpp =================================================================== --- ps/trunk/source/renderer/SkyManager.cpp (revision 26120) +++ ps/trunk/source/renderer/SkyManager.cpp (revision 26121) @@ -1,303 +1,303 @@ /* 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 . */ /* * Sky settings, texture management and rendering. */ #include "precompiled.h" #include "renderer/SkyManager.h" #include "graphics/LightEnv.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" +#include "lib/ogl.h" #include "lib/tex/tex.h" #include "lib/timer.h" -#include "lib/res/graphics/ogl_tex.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStr.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include SkyManager::SkyManager() : m_RenderSky(true), m_SkyCubeMap(0) { CFG_GET_VAL("showsky", m_RenderSky); } /////////////////////////////////////////////////////////////////// // Load all sky textures void SkyManager::LoadSkyTextures() { static const CStrW images[NUMBER_OF_TEXTURES + 1] = { L"front", L"back", L"right", L"left", L"top", L"top" }; /*for (size_t i = 0; i < ARRAY_SIZE(m_SkyTexture); ++i) { VfsPath path = VfsPath("art/textures/skies") / m_SkySet / (Path::String(s_imageNames[i])+L".dds"); CTextureProperties textureProps(path); textureProps.SetWrap(GL_CLAMP_TO_EDGE); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_SkyTexture[i] = texture; }*/ /////////////////////////////////////////////////////////////////////////// // HACK: THE HORRIBLENESS HERE IS OVER 9000. The following code is a HUGE hack and will be removed completely // as soon as all the hardcoded GL_TEXTURE_2D references are corrected in the TextureManager/OGL/tex libs. glGenTextures(1, &m_SkyCubeMap); glBindTexture(GL_TEXTURE_CUBE_MAP, m_SkyCubeMap); static const int types[] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y }; for (size_t i = 0; i < NUMBER_OF_TEXTURES + 1; ++i) { VfsPath path = VfsPath("art/textures/skies") / m_SkySet / (Path::String(images[i])+L".dds"); std::shared_ptr file; size_t fileSize; if (g_VFS->LoadFile(path, file, fileSize) != INFO::OK) { path = VfsPath("art/textures/skies") / m_SkySet / (Path::String(images[i])+L".dds.cached.dds"); if (g_VFS->LoadFile(path, file, fileSize) != INFO::OK) { glDeleteTextures(1, &m_SkyCubeMap); LOGERROR("Error creating sky cubemap."); return; } } Tex tex; tex.decode(file, fileSize); tex.transform_to((tex.m_Flags | TEX_BOTTOM_UP | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS)); u8* data = tex.get_data(); if (types[i] == GL_TEXTURE_CUBE_MAP_NEGATIVE_Y || types[i] == GL_TEXTURE_CUBE_MAP_POSITIVE_Y) { std::vector rotated(tex.m_DataSize); for (size_t y = 0; y < tex.m_Height; ++y) { for (size_t x = 0; x < tex.m_Width; ++x) { size_t invx = y, invy = tex.m_Width-x-1; rotated[(y*tex.m_Width + x) * 4 + 0] = data[(invy*tex.m_Width + invx) * 4 + 0]; rotated[(y*tex.m_Width + x) * 4 + 1] = data[(invy*tex.m_Width + invx) * 4 + 1]; rotated[(y*tex.m_Width + x) * 4 + 2] = data[(invy*tex.m_Width + invx) * 4 + 2]; rotated[(y*tex.m_Width + x) * 4 + 3] = data[(invy*tex.m_Width + invx) * 4 + 3]; } } glTexImage2D(types[i], 0, GL_RGBA, tex.m_Width, tex.m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &rotated[0]); } else { glTexImage2D(types[i], 0, GL_RGBA, tex.m_Width, tex.m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); } } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); #if CONFIG2_GLES #warning TODO: fix SkyManager::LoadSkyTextures for GLES #else glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); #endif glBindTexture(GL_TEXTURE_CUBE_MAP, 0); /////////////////////////////////////////////////////////////////////////// } /////////////////////////////////////////////////////////////////// // Switch to a different sky set (while the game is running) void SkyManager::SetSkySet(const CStrW& newSet) { if (newSet == m_SkySet) return; if (m_SkyCubeMap) { glDeleteTextures(1, &m_SkyCubeMap); m_SkyCubeMap = 0; } m_SkySet = newSet; LoadSkyTextures(); } /////////////////////////////////////////////////////////////////// // Generate list of available skies std::vector SkyManager::GetSkySets() const { std::vector skies; // Find all subdirectories in art/textures/skies const VfsPath path(L"art/textures/skies/"); DirectoryNames subdirectories; if (g_VFS->GetDirectoryEntries(path, 0, &subdirectories) != INFO::OK) { LOGERROR("Error opening directory '%s'", path.string8()); return std::vector(1, GetSkySet()); // just return what we currently have } for(size_t i = 0; i < subdirectories.size(); i++) skies.push_back(subdirectories[i].string()); sort(skies.begin(), skies.end()); return skies; } /////////////////////////////////////////////////////////////////// // Render sky void SkyManager::RenderSky() { #if CONFIG2_GLES #warning TODO: implement SkyManager::RenderSky for GLES #else if (!m_RenderSky) return; // Draw the sky as a small box around the map, with depth write enabled. // This will be done before anything else is drawn so we'll be overlapped by // everything else. // Do nothing unless SetSkySet was called if (m_SkySet.empty()) return; glDepthMask(GL_FALSE); const CCamera& camera = g_Renderer.GetViewCamera(); CShaderTechniquePtr skytech = g_Renderer.GetShaderManager().LoadEffect(str_sky_simple); skytech->BeginPass(); CShaderProgramPtr shader = skytech->GetShader(); shader->BindTexture(str_baseTex, m_SkyCubeMap); // Translate so the sky center is at the camera space origin. CMatrix3D translate; translate.SetTranslation(camera.GetOrientation().GetTranslation()); // Currently we have a hardcoded near plane in the projection matrix. CMatrix3D scale; scale.SetScaling(10.0f, 10.0f, 10.0f); // Rotate so that the "left" face, which contains the brightest part of // each skymap, is in the direction of the sun from our light // environment. CMatrix3D rotate; rotate.SetYRotation(M_PI + g_Renderer.GetLightEnv().GetRotation()); shader->Uniform( str_transform, camera.GetViewProjection() * translate * rotate * scale); std::vector vertexData; // 6 sides of cube with 4 vertices with 6 floats (3 uv and 3 position). vertexData.reserve(6 * 4 * 6); #define ADD_VERTEX(U, V, W, X, Y, Z) \ STMT( \ vertexData.push_back(X); \ vertexData.push_back(Y); \ vertexData.push_back(Z); \ vertexData.push_back(U); \ vertexData.push_back(V); \ vertexData.push_back(W);) // GL_TEXTURE_CUBE_MAP_NEGATIVE_X ADD_VERTEX(+1, +1, +1, -1.0f, -1.0f, -1.0f); ADD_VERTEX(+1, +1, -1, -1.0f, -1.0f, +1.0f); ADD_VERTEX(+1, -1, -1, -1.0f, +1.0f, +1.0f); ADD_VERTEX(+1, -1, +1, -1.0f, +1.0f, -1.0f); // GL_TEXTURE_CUBE_MAP_POSITIVE_X ADD_VERTEX(-1, +1, -1, +1.0f, -1.0f, +1.0f); ADD_VERTEX(-1, +1, +1, +1.0f, -1.0f, -1.0f); ADD_VERTEX(-1, -1, +1, +1.0f, +1.0f, -1.0f); ADD_VERTEX(-1, -1, -1, +1.0f, +1.0f, +1.0f); // GL_TEXTURE_CUBE_MAP_NEGATIVE_Y ADD_VERTEX(-1, +1, +1, +1.0f, -1.0f, -1.0f); ADD_VERTEX(-1, +1, -1, +1.0f, -1.0f, +1.0f); ADD_VERTEX(+1, +1, -1, -1.0f, -1.0f, +1.0f); ADD_VERTEX(+1, +1, +1, -1.0f, -1.0f, -1.0f); // GL_TEXTURE_CUBE_MAP_POSITIVE_Y ADD_VERTEX(+1, -1, +1, -1.0f, +1.0f, -1.0f); ADD_VERTEX(+1, -1, -1, -1.0f, +1.0f, +1.0f); ADD_VERTEX(-1, -1, -1, +1.0f, +1.0f, +1.0f); ADD_VERTEX(-1, -1, +1, +1.0f, +1.0f, -1.0f); // GL_TEXTURE_CUBE_MAP_NEGATIVE_Z ADD_VERTEX(-1, +1, +1, +1.0f, -1.0f, -1.0f); ADD_VERTEX(+1, +1, +1, -1.0f, -1.0f, -1.0f); ADD_VERTEX(+1, -1, +1, -1.0f, +1.0f, -1.0f); ADD_VERTEX(-1, -1, +1, +1.0f, +1.0f, -1.0f); // GL_TEXTURE_CUBE_MAP_POSITIVE_Z ADD_VERTEX(+1, +1, -1, -1.0f, -1.0f, +1.0f); ADD_VERTEX(-1, +1, -1, +1.0f, -1.0f, +1.0f); ADD_VERTEX(-1, -1, -1, +1.0f, +1.0f, +1.0f); ADD_VERTEX(+1, -1, -1, -1.0f, +1.0f, +1.0f); #undef ADD_VERTEX shader->VertexPointer(3, GL_FLOAT, sizeof(GLfloat) * 6, &vertexData[0]); shader->TexCoordPointer( GL_TEXTURE0, 3, GL_FLOAT, sizeof(GLfloat) * 6, &vertexData[3]); shader->AssertPointersBound(); glDrawArrays(GL_QUADS, 0, 6 * 4); skytech->EndPass(); glDepthMask(GL_TRUE); #endif } Index: ps/trunk/source/renderer/VertexBuffer.h =================================================================== --- ps/trunk/source/renderer/VertexBuffer.h (revision 26120) +++ ps/trunk/source/renderer/VertexBuffer.h (revision 26121) @@ -1,159 +1,159 @@ /* 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 . */ /* * encapsulation of VBOs with batching and sharing */ #ifndef INCLUDED_VERTEXBUFFER #define INCLUDED_VERTEXBUFFER -#include "lib/res/graphics/ogl_tex.h" +#include "lib/ogl.h" #include /** * CVertexBuffer: encapsulation of ARB_vertex_buffer_object, also supplying * some additional functionality for sharing buffers between multiple objects. * * The class can be used in two modes, depending on the usage parameter: * * GL_STATIC_DRAW: Call Allocate() with backingStore = NULL. Then call * UpdateChunkVertices() with any pointer - the data will be immediately copied * to the VBO. This should be used for vertex data that rarely changes. * * GL_DYNAMIC_DRAW, GL_STREAM_DRAW: Call Allocate() with backingStore pointing * at some memory that will remain valid for the lifetime of the CVertexBuffer. * This should be used for vertex data that may change every frame. * Rendering is expected to occur in two phases: * - "Prepare" phase: * If this chunk is going to be used for rendering during the next Bind phase, * you must call PrepareForRendering(). * If the vertex data in backingStore has been modified since the last Bind phase, * you must call UpdateChunkVertices(). * - "Bind" phase: * Bind() can be called (multiple times). The vertex data will be uploaded * to the GPU if necessary. * It is okay to have multiple prepare/bind cycles per frame (though slightly less * efficient), but they must occur sequentially. */ class CVertexBuffer { NONCOPYABLE(CVertexBuffer); public: /// VBChunk: describes a portion of this vertex buffer struct VBChunk { /// Owning (parent) vertex buffer CVertexBuffer* m_Owner; /// Start index of this chunk in owner size_t m_Index; /// Number of vertices used by chunk size_t m_Count; /// If UseStreaming() is true, points at the data for this chunk void* m_BackingStore; /// If true, the VBO is not consistent with the chunk's backing store /// (and will need to be re-uploaded before rendering with this chunk) bool m_Dirty; /// If true, we have been told this chunk is going to be used for /// rendering in the next bind phase and will need to be uploaded bool m_Needed; private: // Only CVertexBuffer can construct/delete these // (Other people should use g_VBMan.Allocate, g_VBMan.Release) friend class CVertexBuffer; VBChunk() {} ~VBChunk() {} }; public: // constructor, destructor CVertexBuffer(size_t vertexSize, GLenum usage, GLenum target); CVertexBuffer(size_t vertexSize, GLenum usage, GLenum target, size_t maximumBufferSize); ~CVertexBuffer(); /// Bind to this buffer; return pointer to address required as parameter /// to glVertexPointer ( + etc) calls u8* Bind(); /// Unbind any currently-bound buffer, so glVertexPointer etc calls will not attempt to use it static void Unbind(); /// Make the vertex data available for the next call to Bind() void PrepareForRendering(VBChunk* chunk); /// Update vertex data for given chunk. Transfers the provided data to the actual OpenGL vertex buffer. void UpdateChunkVertices(VBChunk* chunk, void* data); size_t GetVertexSize() const { return m_VertexSize; } size_t GetBytesReserved() const; size_t GetBytesAllocated() const; /// Returns true if this vertex buffer is compatible with the specified vertex type and intended usage. bool CompatibleVertexType(size_t vertexSize, GLenum usage, GLenum target) const; void DumpStatus() const; /** * Given the usage flags of a buffer that has been (or will be) allocated: * * If true, we assume the buffer is going to be modified on every frame, * so we will re-upload the entire buffer every frame using glMapBuffer. * This requires the buffer's owner to hold onto its backing store. * * If false, we assume it will change rarely, and use glSubBufferData to * update it incrementally. The backing store can be freed to save memory. */ static bool UseStreaming(GLenum usage); protected: friend class CVertexBufferManager; // allow allocate only via CVertexBufferManager /// Try to allocate a buffer of given number of vertices (each of given size), /// and with the given type - return null if no free chunks available VBChunk* Allocate(size_t vertexSize, size_t numVertices, GLenum usage, GLenum target, void* backingStore); /// Return given chunk to this buffer void Release(VBChunk* chunk); private: /// Vertex size of this vertex buffer size_t m_VertexSize; /// Number of vertices of above size in this buffer size_t m_MaxVertices; /// List of free chunks in this buffer std::vector m_FreeList; /// List of allocated chunks std::vector m_AllocList; /// Available free vertices - total of all free vertices in the free list size_t m_FreeVertices; /// Handle to the actual GL vertex buffer object GLuint m_Handle; /// Usage type of the buffer (GL_STATIC_DRAW etc) GLenum m_Usage; /// Buffer target (GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER) GLenum m_Target; bool m_HasNeededChunks; }; #endif Index: ps/trunk/source/tools/atlas/GameInterface/Brushes.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Brushes.cpp (revision 26120) +++ ps/trunk/source/tools/atlas/GameInterface/Brushes.cpp (revision 26121) @@ -1,131 +1,130 @@ -/* Copyright (C) 2019 Wildfire Games. +/* 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 "precompiled.h" #include "Brushes.h" #include "graphics/Color.h" #include "graphics/Terrain.h" -#include "lib/ogl.h" #include "maths/MathUtil.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/TerrainOverlay.h" #include "simulation2/Simulation2.h" #include "simulation2/system/SimContext.h" using namespace AtlasMessage; class BrushTerrainOverlay : public TerrainOverlay { public: BrushTerrainOverlay(const Brush* brush) : TerrainOverlay(g_Game->GetSimulation2()->GetSimContext(), 300), m_Brush(brush) { } void GetTileExtents( ssize_t& min_i_inclusive, ssize_t& min_j_inclusive, ssize_t& max_i_inclusive, ssize_t& max_j_inclusive) { m_Brush->GetBottomLeft(min_i_inclusive, min_j_inclusive); m_Brush->GetTopRight(max_i_inclusive, max_j_inclusive); // But since brushes deal with vertices instead of tiles, // we don't want to include the top/right row --max_i_inclusive; --max_j_inclusive; } void ProcessTile(ssize_t i, ssize_t j) { ssize_t i0, j0; m_Brush->GetBottomLeft(i0, j0); // Color this tile based on the average of the surrounding vertices float avg = ( m_Brush->Get(i-i0, j-j0) + m_Brush->Get(i-i0+1, j-j0) + m_Brush->Get(i-i0, j-j0+1) + m_Brush->Get(i-i0+1, j-j0+1) ) / 4.f; RenderTile(CColor(0, 1, 0, avg*0.8f), false); if (avg > 0.1f) RenderTileOutline(CColor(1, 1, 1, std::min(0.4f, avg-0.1f)), 1, true); } const AtlasMessage::Brush* m_Brush; }; Brush::Brush() : m_W(0), m_H(0), m_TerrainOverlay(NULL) { } Brush::~Brush() { delete m_TerrainOverlay; } void Brush::SetData(ssize_t w, ssize_t h, const std::vector& data) { m_W = w; m_H = h; m_Data = data; ENSURE(data.size() == (size_t)(w*h)); } void Brush::GetCentre(ssize_t& x, ssize_t& y) const { CVector3D c = m_Centre; if (m_W % 2) c.X += TERRAIN_TILE_SIZE/2.f; if (m_H % 2) c.Z += TERRAIN_TILE_SIZE/2.f; ssize_t cx, cy; CTerrain::CalcFromPosition(c, cx, cy); x = cx; y = cy; } void Brush::GetBottomLeft(ssize_t& x, ssize_t& y) const { GetCentre(x, y); x -= (m_W-1)/2; y -= (m_H-1)/2; } void Brush::GetTopRight(ssize_t& x, ssize_t& y) const { GetBottomLeft(x, y); x += m_W-1; y += m_H-1; } void Brush::SetRenderEnabled(bool enabled) { if (enabled && !m_TerrainOverlay) { m_TerrainOverlay = new BrushTerrainOverlay(this); } else if (!enabled && m_TerrainOverlay) { delete m_TerrainOverlay; m_TerrainOverlay = NULL; } } Brush AtlasMessage::g_CurrentBrush; Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp (revision 26120) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/CinemaHandler.cpp (revision 26121) @@ -1,544 +1,544 @@ -/* Copyright (C) 2020 Wildfire Games. +/* 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 "precompiled.h" #include "MessageHandler.h" #include "../CommandProc.h" #include "../GameLoop.h" #include "../View.h" #include "graphics/Camera.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "ps/Game.h" #include "ps/CStr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "maths/Vector2D.h" #include "maths/Vector3D.h" -#include "lib/res/graphics/ogl_tex.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpCinemaManager.h" -namespace AtlasMessage { +namespace AtlasMessage +{ const float MINIMAL_SCREEN_DISTANCE = 5.f; sCinemaPath ConstructCinemaPath(const CCinemaPath* source) { sCinemaPath path; const CCinemaData* data = source->GetData(); //path.mode = data->m_Mode; //path.style = data->m_Style; path.growth = data->m_Growth; path.timescale = data->m_Timescale.ToFloat(); path.change = data->m_Switch; return path; } CCinemaData ConstructCinemaData(const sCinemaPath& path) { CCinemaData data; data.m_Growth = data.m_GrowthCount = path.growth; data.m_Switch = path.change; //data.m_Mode = path.mode; //data.m_Style = path.style; return data; } sCinemaSplineNode ConstructCinemaNode(const SplineData& data) { sCinemaSplineNode node; node.px = data.Position.X.ToFloat(); node.py = data.Position.Y.ToFloat(); node.pz = data.Position.Z.ToFloat(); node.rx = data.Rotation.X.ToFloat(); node.ry = data.Rotation.Y.ToFloat(); node.rz = data.Rotation.Z.ToFloat(); node.t = data.Distance.ToFloat(); return node; } std::vector GetCurrentPaths() { std::vector atlasPaths; CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return atlasPaths; const std::map& paths = cmpCinemaManager->GetPaths(); for ( std::map::const_iterator it=paths.begin(); it!=paths.end(); ++it ) { sCinemaPath path = ConstructCinemaPath(&it->second); path.name = it->first; const std::vector& nodes = it->second.GetAllNodes(); std::vector atlasNodes; for ( size_t i=0; i 2 ) { for ( size_t i=atlasNodes.size()-2; i>0; --i ) atlasNodes[i].t = atlasNodes[i-1].t; } atlasNodes.back().t = atlasNodes.front().t; atlasNodes.front().t = back; } path.nodes = atlasNodes; atlasPaths.push_back(path); } return atlasPaths; } void SetCurrentPaths(const std::vector& atlasPaths) { std::map paths; for ( std::vector::const_iterator it=atlasPaths.begin(); it!=atlasPaths.end(); ++it ) { CStrW pathName(*it->name); paths[pathName] = CCinemaPath(); paths[pathName].SetTimescale(fixed::FromFloat(it->timescale)); const sCinemaPath& atlasPath = *it; const std::vector nodes = *atlasPath.nodes; TNSpline spline; CCinemaData data = ConstructCinemaData(atlasPath); for ( size_t j=0; j cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpCinemaManager) cmpCinemaManager->SetPaths(paths); } QUERYHANDLER(GetCameraInfo) { sCameraInfo info; const CMatrix3D& cameraOrientation = g_Game->GetView()->GetCamera()->GetOrientation(); CQuaternion quatRot = cameraOrientation.GetRotation(); quatRot.Normalize(); CVector3D rotation = quatRot.ToEulerAngles(); rotation.X = RADTODEG(rotation.X); rotation.Y = RADTODEG(rotation.Y); rotation.Z = RADTODEG(rotation.Z); CVector3D translation = cameraOrientation.GetTranslation(); info.pX = translation.X; info.pY = translation.Y; info.pZ = translation.Z; info.rX = rotation.X; info.rY = rotation.Y; info.rZ = rotation.Z; msg->info = info; } MESSAGEHANDLER(CinemaEvent) { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; if (msg->mode == eCinemaEventMode::SMOOTH) { cmpCinemaManager->AddCinemaPathToQueue(*msg->path); } else if ( msg->mode == eCinemaEventMode::RESET ) { // g_Game->GetView()->ResetCamera(); } else ENSURE(false); } BEGIN_COMMAND(AddCinemaPath) { void Do() { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; CCinemaData pathData; pathData.m_Name = *msg->pathName; pathData.m_Timescale = fixed::FromInt(1); pathData.m_Orientation = L"target"; pathData.m_Mode = L"ease_inout"; pathData.m_Style = L"default"; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CFixedVector3D target( fixed::FromFloat(focus.X), fixed::FromFloat(focus.Y), fixed::FromFloat(focus.Z) ); CVector3D camera = g_Game->GetView()->GetCamera()->GetOrientation().GetTranslation(); CFixedVector3D position( fixed::FromFloat(camera.X), fixed::FromFloat(camera.Y), fixed::FromFloat(camera.Z) ); TNSpline positionSpline; positionSpline.AddNode(position, CFixedVector3D(), fixed::FromInt(0)); TNSpline targetSpline; targetSpline.AddNode(target, CFixedVector3D(), fixed::FromInt(0)); cmpCinemaManager->AddPath(CCinemaPath(pathData, positionSpline, targetSpline)); } void Redo() { } void Undo() { } }; END_COMMAND(AddCinemaPath) BEGIN_COMMAND(DeleteCinemaPath) { void Do() { CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) return; cmpCinemaManager->DeletePath(*msg->pathName); } void Redo() { } void Undo() { } }; END_COMMAND(DeleteCinemaPath) BEGIN_COMMAND(SetCinemaPaths) { std::vector m_oldPaths, m_newPaths; void Do() { m_oldPaths = GetCurrentPaths(); m_newPaths = *msg->paths; Redo(); } void Redo() { SetCurrentPaths(m_newPaths); } void Undo() { SetCurrentPaths(m_oldPaths); } }; END_COMMAND(SetCinemaPaths) BEGIN_COMMAND(SetCinemaPathsDrawing) { void Do() { if (g_Game && g_Game->GetView() && g_Game->GetView()->GetCinema()) g_Game->GetView()->GetCinema()->SetPathsDrawing(msg->drawPaths); } void Redo() { } void Undo() { } }; END_COMMAND(SetCinemaPathsDrawing) static CVector3D GetNearestPointToScreenCoords(const CVector3D& base, const CVector3D& dir, const CVector2D& screen, float lower = -1e5, float upper = 1e5) { // It uses a ternary search, because an intersection of cylinders is the complex task for (int i = 0; i < 64; ++i) { float delta = (upper - lower) / 3.0; float middle1 = lower + delta, middle2 = lower + 2.0f * delta; CVector3D p1 = base + dir * middle1, p2 = base + dir * middle2; CVector2D s1, s2; g_Game->GetView()->GetCamera()->GetScreenCoordinates(p1, s1.X, s1.Y); g_Game->GetView()->GetCamera()->GetScreenCoordinates(p2, s2.X, s2.Y); if ((s1 - screen).Length() < (s2 - screen).Length()) upper = middle2; else lower = middle1; } return base + dir * upper; } #define GET_PATH_NODE_WITH_VALIDATION() \ int index = msg->node->index; \ if (index < 0) \ return; \ CStrW name = *msg->node->name; \ CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); \ if (!cmpCinemaManager || !cmpCinemaManager->HasPath(name)) \ return; \ const CCinemaPath& path = cmpCinemaManager->GetPaths().find(name)->second; \ if (!msg->node->targetNode) \ { \ if (index >= (int)path.GetAllNodes().size()) \ return; \ } \ else \ { \ if (index >= (int)path.GetTargetSpline().GetAllNodes().size()) \ return; \ } BEGIN_COMMAND(AddPathNode) { void Do() { GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CFixedVector3D target( fixed::FromFloat(focus.X), fixed::FromFloat(focus.Y), fixed::FromFloat(focus.Z) ); spline.InsertNode(index + 1, target, CFixedVector3D(), fixed::FromInt(1)); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); } void Redo() { } void Undo() { } }; END_COMMAND(AddPathNode) BEGIN_COMMAND(DeletePathNode) { void Do() { GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; if (spline.GetAllNodes().size() <= 1) return; spline.RemoveNode(index); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); g_AtlasGameLoop->view->SetParam(L"movetool", false); } void Redo() { } void Undo() { } }; END_COMMAND(DeletePathNode) BEGIN_COMMAND(MovePathNode) { void Do() { int axis = msg->axis; if (axis == AXIS_INVALID) return; GET_PATH_NODE_WITH_VALIDATION(); CCinemaData data = *path.GetData(); TNSpline positionSpline = path; TNSpline targetSpline = path.GetTargetSpline(); TNSpline& spline = msg->node->targetNode ? targetSpline : positionSpline; // Get shift of the tool by the cursor movement CFixedVector3D pos = spline.GetAllNodes()[index].Position; CVector3D position( pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat() ); CVector3D axisDirection(axis & AXIS_X, axis & AXIS_Y, axis & AXIS_Z); CVector2D from, to; msg->from->GetScreenSpace(from.X, from.Y); msg->to->GetScreenSpace(to.X, to.Y); CVector3D shift( GetNearestPointToScreenCoords(position, axisDirection, to) - GetNearestPointToScreenCoords(position, axisDirection, from) ); // Change, rebuild and update the path position += shift; pos += CFixedVector3D( fixed::FromFloat(shift.X), fixed::FromFloat(shift.Y), fixed::FromFloat(shift.Z) ); spline.UpdateNodePos(index, pos); spline.BuildSpline(); cmpCinemaManager->DeletePath(name); cmpCinemaManager->AddPath(CCinemaPath(data, positionSpline, targetSpline)); // Update visual tool coordinates g_AtlasGameLoop->view->SetParam(L"movetool_x", position.X); g_AtlasGameLoop->view->SetParam(L"movetool_y", position.Y); g_AtlasGameLoop->view->SetParam(L"movetool_z", position.Z); } void Redo() { } void Undo() { } }; END_COMMAND(MovePathNode) QUERYHANDLER(GetCinemaPaths) { msg->paths = GetCurrentPaths(); } static bool isPathNodePicked(const TNSpline& spline, const CVector2D& cursor, AtlasMessage::sCinemaPathNode& node, bool targetNode) { for (size_t i = 0; i < spline.GetAllNodes().size(); ++i) { const SplineData& data = spline.GetAllNodes()[i]; CVector3D position( data.Position.X.ToFloat(), data.Position.Y.ToFloat(), data.Position.Z.ToFloat() ); CVector2D screen_pos; g_Game->GetView()->GetCamera()->GetScreenCoordinates(position, screen_pos.X, screen_pos.Y); if ((screen_pos - cursor).Length() < MINIMAL_SCREEN_DISTANCE) { node.index = i; node.targetNode = targetNode; g_AtlasGameLoop->view->SetParam(L"movetool", true); g_AtlasGameLoop->view->SetParam(L"movetool_x", position.X); g_AtlasGameLoop->view->SetParam(L"movetool_y", position.Y); g_AtlasGameLoop->view->SetParam(L"movetool_z", position.Z); return true; } } return false; } QUERYHANDLER(PickPathNode) { AtlasMessage::sCinemaPathNode node; CmpPtr cmpCinemaManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpCinemaManager) { msg->node = node; return; } CVector2D cursor; msg->pos->GetScreenSpace(cursor.X, cursor.Y); for (const std::pair& p : cmpCinemaManager->GetPaths()) { const CCinemaPath& path = p.second; if (isPathNodePicked(path, cursor, node, false) || isPathNodePicked(path.GetTargetSpline(), cursor, node, true)) { node.name = path.GetName(); msg->node = node; return; } } msg->node = node; g_AtlasGameLoop->view->SetParam(L"movetool", false); } static bool isAxisPicked(const CVector3D& base, const CVector3D& direction, float length, const CVector2D& cursor) { CVector3D position = GetNearestPointToScreenCoords(base, direction, cursor, 0, length); CVector2D screen_position; g_Game->GetView()->GetCamera()->GetScreenCoordinates(position, screen_position.X, screen_position.Y); return (cursor - screen_position).Length() < MINIMAL_SCREEN_DISTANCE; } QUERYHANDLER(PickAxis) { msg->axis = AXIS_INVALID; GET_PATH_NODE_WITH_VALIDATION(); const TNSpline& spline = msg->node->targetNode ? path.GetTargetSpline() : path; CFixedVector3D pos = spline.GetAllNodes()[index].Position; CVector3D position(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()); CVector3D camera = g_Game->GetView()->GetCamera()->GetOrientation().GetTranslation(); float scale = (position - camera).Length() / 10.0; CVector2D cursor; msg->pos->GetScreenSpace(cursor.X, cursor.Y); if (isAxisPicked(position, CVector3D(1, 0, 0), scale, cursor)) msg->axis = AXIS_X; else if (isAxisPicked(position, CVector3D(0, 1, 0), scale, cursor)) msg->axis = AXIS_Y; else if (isAxisPicked(position, CVector3D(0, 0, 1), scale, cursor)) msg->axis = AXIS_Z; } MESSAGEHANDLER(ClearPathNodePreview) { UNUSED2(msg); g_AtlasGameLoop->view->SetParam(L"movetool", false); } -} +} // namespace AtlasMessage Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 26120) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 26121) @@ -1,1131 +1,1132 @@ -/* Copyright (C) 2020 Wildfire Games. +/* 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 "precompiled.h" #include #include #include "MessageHandler.h" #include "../CommandProc.h" #include "../SimState.h" #include "../View.h" #include "graphics/GameView.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/ObjectManager.h" #include "graphics/Terrain.h" #include "graphics/Unit.h" -#include "lib/ogl.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Selection.h" #include "ps/XML/XMLWriter.h" -namespace AtlasMessage { +namespace AtlasMessage +{ namespace { bool SortObjectsList(const sObjectsListItem& a, const sObjectsListItem& b) { return wcscmp(a.name.c_str(), b.name.c_str()) < 0; } -} +} // anonymous namespace // Helpers for object constraints bool CheckEntityObstruction(entity_id_t ent) { CmpPtr cmpObstruction(*g_Game->GetSimulation2(), ent); if (cmpObstruction) { ICmpObstruction::EFoundationCheck result = cmpObstruction->CheckFoundation("default"); if (result != ICmpObstruction::FOUNDATION_CHECK_SUCCESS) return false; } return true; } void CheckObstructionAndUpdateVisual(entity_id_t id) { CmpPtr cmpVisual(*g_Game->GetSimulation2(), id); if (cmpVisual) { if (!CheckEntityObstruction(id)) cmpVisual->SetShadingColor(fixed::FromDouble(1.4), fixed::FromDouble(0.4), fixed::FromDouble(0.4), fixed::FromDouble(1)); else cmpVisual->SetShadingColor(fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1)); } } QUERYHANDLER(GetObjectsList) { std::vector objects; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTemplateManager) { std::vector names = cmpTemplateManager->FindAllTemplates(true); for (std::vector::iterator it = names.begin(); it != names.end(); ++it) { std::wstring name(it->begin(), it->end()); sObjectsListItem e; e.id = name; if (name.substr(0, 6) == L"actor|") { e.name = name.substr(6); e.type = 1; } else { e.name = name; e.type = 0; } objects.push_back(e); } } std::sort(objects.begin(), objects.end(), SortObjectsList); msg->objects = objects; } static std::vector g_Selection; typedef std::map PlayerColorMap; // Helper function to find color of player owning the given entity, // returns white if entity has no owner. Uses caching to avoid // expensive script calls. static CColor GetOwnerPlayerColor(PlayerColorMap& colorMap, entity_id_t id) { // Default color - white CColor color(1.0f, 1.0f, 1.0f, 1.0f); CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpOwnership(sim, id); if (cmpOwnership) { player_id_t owner = cmpOwnership->GetOwner(); if (colorMap.find(owner) != colorMap.end()) return colorMap[owner]; else { CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); entity_id_t playerEnt = cmpPlayerManager->GetPlayerByID(owner); CmpPtr cmpPlayer(sim, playerEnt); if (cmpPlayer) { colorMap[owner] = cmpPlayer->GetDisplayedColor(); color = colorMap[owner]; } } } return color; } MESSAGEHANDLER(SetSelectionPreview) { CSimulation2& sim = *g_Game->GetSimulation2(); // Cache player colors for performance PlayerColorMap playerColors; // Clear old selection rings for (size_t i = 0; i < g_Selection.size(); ++i) { // We can't set only alpha here, because that won't trigger desaturation // so we set the complete color (not too evil since it's cached) CmpPtr cmpSelectable(sim, g_Selection[i]); if (cmpSelectable) { CColor color = GetOwnerPlayerColor(playerColors, g_Selection[i]); color.a = 0.0f; cmpSelectable->SetSelectionHighlight(color, false); } } g_Selection = *msg->ids; // Set new selection rings for (size_t i = 0; i < g_Selection.size(); ++i) { CmpPtr cmpSelectable(sim, g_Selection[i]); if (cmpSelectable) cmpSelectable->SetSelectionHighlight(GetOwnerPlayerColor(playerColors, g_Selection[i]), true); } } QUERYHANDLER(GetObjectSettings) { AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); sObjectSettings settings; settings.player = 0; CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); if (cmpOwnership) { int32_t player = cmpOwnership->GetOwner(); if (player != -1) settings.player = player; } // TODO: selections /* // Get the unit's possible variants and selected variants std::vector > groups = unit->GetObject().m_Base->GetVariantGroups(); const std::set& selections = unit->GetActorSelections(); // Iterate over variant groups std::vector > variantgroups; std::set selections_set; variantgroups.reserve(groups.size()); for (size_t i = 0; i < groups.size(); ++i) { // Copy variants into output structure std::vector group; group.reserve(groups[i].size()); int choice = -1; for (size_t j = 0; j < groups[i].size(); ++j) { group.push_back(CStrW(groups[i][j])); // Find the first string in 'selections' that matches one of this // group's variants if (choice == -1) if (selections.find(groups[i][j]) != selections.end()) choice = (int)j; } // Assuming one of the variants was selected (which it really ought // to be), remember that one's name if (choice != -1) selections_set.insert(CStrW(groups[i][choice])); variantgroups.push_back(group); } settings.variantgroups = variantgroups; settings.selections = std::vector (selections_set.begin(), selections_set.end()); // convert set->vector */ msg->settings = settings; } QUERYHANDLER(GetObjectMapSettings) { std::vector ids = *msg->ids; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); XMLWriter_File exampleFile; { XMLWriter_Element entitiesTag(exampleFile, "Entities"); { for (entity_id_t id : ids) { XMLWriter_Element entityTag(exampleFile, "Entity"); { //Template name entityTag.Setting("Template", cmpTemplateManager->GetCurrentTemplateName(id)); //Player CmpPtr cmpOwnership(*g_Game->GetSimulation2(), id); if (cmpOwnership) entityTag.Setting("Player", static_cast(cmpOwnership->GetOwner())); //Adding position to make some relative position later CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (cmpPosition) { CFixedVector3D pos = cmpPosition->GetPosition(); CFixedVector3D rot = cmpPosition->GetRotation(); { XMLWriter_Element positionTag(exampleFile, "Position"); positionTag.Attribute("x", pos.X); positionTag.Attribute("z", pos.Z); // TODO: height offset etc } { XMLWriter_Element orientationTag(exampleFile, "Orientation"); orientationTag.Attribute("y", rot.Y); // TODO: X, Z maybe } } // Adding actor seed CmpPtr cmpVisual(*g_Game->GetSimulation2(), id); if (cmpVisual) entityTag.Setting("ActorSeed", static_cast(cmpVisual->GetActorSeed())); } } } } const CStr& data = exampleFile.GetOutput(); msg->xmldata = data.FromUTF8(); } BEGIN_COMMAND(SetObjectSettings) { player_id_t m_PlayerOld, m_PlayerNew; std::set m_SelectionsOld, m_SelectionsNew; void Do() { sObjectSettings settings = msg->settings; AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); m_PlayerOld = 0; if (cmpOwnership) { int32_t player = cmpOwnership->GetOwner(); if (player != -1) m_PlayerOld = player; } // TODO: selections // m_SelectionsOld = unit->GetActorSelections(); m_PlayerNew = (player_id_t)settings.player; std::vector selections = *settings.selections; for (std::vector::iterator it = selections.begin(); it != selections.end(); ++it) { m_SelectionsNew.insert(CStrW(*it).ToUTF8()); } Redo(); } void Redo() { Set(m_PlayerNew, m_SelectionsNew); } void Undo() { Set(m_PlayerOld, m_SelectionsOld); } private: void Set(player_id_t player, const std::set& UNUSED(selections)) { AtlasView* view = AtlasView::GetView(msg->view); CSimulation2* simulation = view->GetSimulation2(); CmpPtr cmpOwnership(*simulation, view->GetEntityId(msg->id)); if (cmpOwnership) cmpOwnership->SetOwner(player); // TODO: selections // unit->SetActorSelections(selections); } }; END_COMMAND(SetObjectSettings); ////////////////////////////////////////////////////////////////////////// static CStrW g_PreviewUnitName; static entity_id_t g_PreviewEntityID = INVALID_ENTITY; static std::vector g_PreviewEntitiesID; static CVector3D GetUnitPos(const Position& pos, bool floating) { static CVector3D vec; vec = pos.GetWorldSpace(vec, floating); // if msg->pos is 'Unchanged', use the previous pos // Clamp the position to the edges of the world: // Use 'Clamp' with a value slightly less than the width, so that converting // to integer (rounding towards zero) will put it on the tile inside the edge // instead of just outside float mapWidth = (g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide()-1)*TERRAIN_TILE_SIZE; float delta = 1e-6f; // fraction of map width - must be > FLT_EPSILON float xOnMap = Clamp(vec.X, 0.f, mapWidth * (1.f - delta)); float zOnMap = Clamp(vec.Z, 0.f, mapWidth * (1.f - delta)); // Don't waste time with GetExactGroundLevel unless we've changed if (xOnMap != vec.X || zOnMap != vec.Z) { vec.X = xOnMap; vec.Z = zOnMap; vec.Y = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(xOnMap, zOnMap); } return vec; } QUERYHANDLER(GetCurrentSelection) { msg->ids = g_Selection; } MESSAGEHANDLER(ObjectPreviewToEntity) { UNUSED2(msg); if (g_PreviewEntitiesID.size() == 0) return; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); PlayerColorMap playerColor; //I need to re create the objects finally delete preview objects for (entity_id_t ent : g_PreviewEntitiesID) { //Get template name (without the "preview|" prefix) std::wstring wTemplateName = wstring_from_utf8(cmpTemplateManager->GetCurrentTemplateName(ent).substr(8)); //Create new entity entity_id_t new_ent = g_Game->GetSimulation2()->AddEntity(wTemplateName); if (new_ent == INVALID_ENTITY) continue; //get position, get rotation CmpPtr cmpPositionNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpPositionOld(*g_Game->GetSimulation2(), ent); if (cmpPositionNew && cmpPositionOld) { CVector3D pos = cmpPositionOld->GetPosition(); cmpPositionNew->JumpTo(entity_pos_t::FromFloat(pos.X), entity_pos_t::FromFloat(pos.Z)); //now rotate CFixedVector3D rotation = cmpPositionOld->GetRotation(); cmpPositionNew->SetYRotation(rotation.Y); } //get owner CmpPtr cmpOwnershipNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpOwnershipOld(*g_Game->GetSimulation2(), ent); if (cmpOwnershipNew && cmpOwnershipOld) cmpOwnershipNew->SetOwner(cmpOwnershipOld->GetOwner()); //getVisual CmpPtr cmpVisualNew(*g_Game->GetSimulation2(), new_ent); CmpPtr cmpVisualOld(*g_Game->GetSimulation2(), ent); if (cmpVisualNew && cmpVisualOld) cmpVisualNew->SetActorSeed(cmpVisualOld->GetActorSeed()); //Update g_selectedObject and higligth g_Selection.push_back(new_ent); CmpPtr cmpSelectable(*g_Game->GetSimulation2(), new_ent); if (cmpSelectable) cmpSelectable->SetSelectionHighlight(GetOwnerPlayerColor(playerColor, new_ent), true); g_Game->GetSimulation2()->DestroyEntity(ent); } g_PreviewEntitiesID.clear(); } MESSAGEHANDLER(MoveObjectPreview) { if (g_PreviewEntitiesID.size()==0) return; //TODO:Change pivot entity_id_t referenceEntity = *g_PreviewEntitiesID.begin(); // All selected objects move relative to a pivot object, // so get its position and whether it's floating CFixedVector3D referencePos; CmpPtr cmpPosition(*g_Game->GetSimulation2(), referenceEntity); if (cmpPosition && cmpPosition->IsInWorld()) referencePos = cmpPosition->GetPosition(); // Calculate directional vector of movement for pivot object, // we apply the same movement to all objects CVector3D targetPos = GetUnitPos(msg->pos, true); CFixedVector3D fTargetPos(entity_pos_t::FromFloat(targetPos.X), entity_pos_t::FromFloat(targetPos.Y), entity_pos_t::FromFloat(targetPos.Z)); CFixedVector3D dir = fTargetPos - referencePos; for (const entity_id_t id : g_PreviewEntitiesID) { CmpPtr cmpPreviewPosition(*g_Game->GetSimulation2(), id); if (cmpPreviewPosition) { CFixedVector3D posFinal; if (cmpPreviewPosition->IsInWorld()) { // Calculate this object's position CFixedVector3D posFixed = cmpPreviewPosition->GetPosition(); posFinal = posFixed + dir; } cmpPreviewPosition->JumpTo(posFinal.X, posFinal.Z); } CheckObstructionAndUpdateVisual(id); } } MESSAGEHANDLER(ObjectPreview) { // If the selection has changed... if (*msg->id != g_PreviewUnitName || (!msg->cleanObjectPreviews)) { // Delete old entity if (g_PreviewEntityID != INVALID_ENTITY && msg->cleanObjectPreviews) { //Time to delete all preview objects for (entity_id_t ent : g_PreviewEntitiesID) g_Game->GetSimulation2()->DestroyEntity(ent); g_PreviewEntitiesID.clear(); } // Create the new entity if ((*msg->id).empty()) g_PreviewEntityID = INVALID_ENTITY; else { g_PreviewEntityID = g_Game->GetSimulation2()->AddLocalEntity(L"preview|" + *msg->id); g_PreviewEntitiesID.push_back(g_PreviewEntityID); } g_PreviewUnitName = *msg->id; } if (g_PreviewEntityID != INVALID_ENTITY) { // Update the unit's position and orientation: CmpPtr cmpPosition(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpPosition) { CVector3D pos = GetUnitPos(msg->pos, cmpPosition->CanFloat()); cmpPosition->JumpTo(entity_pos_t::FromFloat(pos.X), entity_pos_t::FromFloat(pos.Z)); float angle; if (msg->usetarget) { // Aim from pos towards msg->target CVector3D target = msg->target->GetWorldSpace(pos.Y); angle = atan2(target.X-pos.X, target.Z-pos.Z); } else { angle = msg->angle; } cmpPosition->SetYRotation(entity_angle_t::FromFloat(angle)); } // TODO: handle random variations somehow CmpPtr cmpVisual(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpVisual) cmpVisual->SetActorSeed(msg->actorseed); CmpPtr cmpOwnership(*g_Game->GetSimulation2(), g_PreviewEntityID); if (cmpOwnership) cmpOwnership->SetOwner((player_id_t)msg->settings->player); CheckObstructionAndUpdateVisual(g_PreviewEntityID); } } BEGIN_COMMAND(CreateObject) { CVector3D m_Pos; float m_Angle; player_id_t m_Player; entity_id_t m_EntityID; u32 m_ActorSeed; void Do() { // Calculate the position/orientation to create this unit with m_Pos = GetUnitPos(msg->pos, true); // don't really care about floating if (msg->usetarget) { // Aim from m_Pos towards msg->target CVector3D target = msg->target->GetWorldSpace(m_Pos.Y); m_Angle = atan2(target.X-m_Pos.X, target.Z-m_Pos.Z); } else { m_Angle = msg->angle; } m_Player = (player_id_t)msg->settings->player; m_ActorSeed = msg->actorseed; // TODO: variation/selection strings Redo(); } void Redo() { m_EntityID = g_Game->GetSimulation2()->AddEntity(*msg->id); if (m_EntityID == INVALID_ENTITY) return; CmpPtr cmpPosition(*g_Game->GetSimulation2(), m_EntityID); if (cmpPosition) { cmpPosition->JumpTo(entity_pos_t::FromFloat(m_Pos.X), entity_pos_t::FromFloat(m_Pos.Z)); cmpPosition->SetYRotation(entity_angle_t::FromFloat(m_Angle)); } CmpPtr cmpOwnership(*g_Game->GetSimulation2(), m_EntityID); if (cmpOwnership) cmpOwnership->SetOwner(m_Player); CmpPtr cmpVisual(*g_Game->GetSimulation2(), m_EntityID); if (cmpVisual) { cmpVisual->SetActorSeed(m_ActorSeed); // TODO: variation/selection strings } } void Undo() { if (m_EntityID != INVALID_ENTITY) { g_Game->GetSimulation2()->DestroyEntity(m_EntityID); m_EntityID = INVALID_ENTITY; } } }; END_COMMAND(CreateObject) QUERYHANDLER(PickObject) { float x, y; msg->pos->GetScreenSpace(x, y); // Normally this function would be called with a player ID to check LOS, // but in Atlas the entire map is revealed, so just pass INVALID_PLAYER entity_id_t ent = EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, INVALID_PLAYER, msg->selectActors);; if (ent == INVALID_ENTITY) msg->id = INVALID_ENTITY; else { msg->id = ent; // Calculate offset of object from original mouse click position // so it gets moved by that offset CmpPtr cmpPosition(*g_Game->GetSimulation2(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) { // error msg->offsetx = msg->offsety = 0; } else { CFixedVector3D fixed = cmpPosition->GetPosition(); CVector3D centre = CVector3D(fixed.X.ToFloat(), fixed.Y.ToFloat(), fixed.Z.ToFloat()); float cx, cy; g_Game->GetView()->GetCamera()->GetScreenCoordinates(centre, cx, cy); msg->offsetx = (int)(cx - x); msg->offsety = (int)(cy - y); } } } QUERYHANDLER(PickObjectsInRect) { float x0, y0, x1, y1; msg->start->GetScreenSpace(x0, y0); msg->end->GetScreenSpace(x1, y1); // Since owner selections are meaningless in Atlas, use INVALID_PLAYER msg->ids = EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, INVALID_PLAYER, msg->selectActors); } QUERYHANDLER(PickSimilarObjects) { CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); entity_id_t ent = msg->id; std::string templateName = cmpTemplateManager->GetCurrentTemplateName(ent); // If unit has ownership, only pick units from the same player player_id_t owner = INVALID_PLAYER; CmpPtr cmpOwnership(*g_Game->GetSimulation2(), ent); if (cmpOwnership) owner = cmpOwnership->GetOwner(); msg->ids = EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, owner, false, true, true, false); } MESSAGEHANDLER(ResetSelectionColor) { UNUSED2(msg); for (entity_id_t ent : g_Selection) { CmpPtr cmpVisual(*g_Game->GetSimulation2(), ent); if (cmpVisual) cmpVisual->SetShadingColor(fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1), fixed::FromDouble(1)); } } BEGIN_COMMAND(MoveObjects) { // Mapping from object to position std::map m_PosOld, m_PosNew; void Do() { std::vector ids = *msg->ids; // All selected objects move relative to a pivot object, // so get its position and whether it's floating CVector3D pivotPos(0, 0, 0); bool pivotFloating = false; CmpPtr cmpPositionPivot(*g_Game->GetSimulation2(), (entity_id_t)msg->pivot); if (cmpPositionPivot && cmpPositionPivot->IsInWorld()) { pivotFloating = cmpPositionPivot->CanFloat(); CFixedVector3D pivotFixed = cmpPositionPivot->GetPosition(); pivotPos = CVector3D(pivotFixed.X.ToFloat(), pivotFixed.Y.ToFloat(), pivotFixed.Z.ToFloat()); } // Calculate directional vector of movement for pivot object, // we apply the same movement to all objects CVector3D targetPos = GetUnitPos(msg->pos, pivotFloating); CVector3D dir = targetPos - pivotPos; for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition || !cmpPosition->IsInWorld()) { // error m_PosOld[id] = m_PosNew[id] = CVector3D(0, 0, 0); } else { // Calculate this object's position CFixedVector3D posFixed = cmpPosition->GetPosition(); CVector3D pos = CVector3D(posFixed.X.ToFloat(), posFixed.Y.ToFloat(), posFixed.Z.ToFloat()); m_PosNew[id] = pos + dir; m_PosOld[id] = pos; } } SetPos(m_PosNew); } void SetPos(const std::map& map) { for (const std::pair& p : map) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; // Set 2D position, ignoring height cmpPosition->JumpTo(entity_pos_t::FromFloat(p.second.X), entity_pos_t::FromFloat(p.second.Z)); CheckObstructionAndUpdateVisual(p.first); } } void Redo() { SetPos(m_PosNew); } void Undo() { SetPos(m_PosOld); } void MergeIntoPrevious(cMoveObjects* prev) { // TODO: do something valid if prev selection != this selection ENSURE(*(prev->msg->ids) == *(msg->ids)); prev->m_PosNew = m_PosNew; } }; END_COMMAND(MoveObjects) BEGIN_COMMAND(RotateObjectsFromCenterPoint) { std::map m_PosOld, m_PosNew; std::map m_AngleOld, m_AngleNew; CVector3D m_CenterPoint; float m_AngleInitialRotation; void Do() { std::vector ids = *msg->ids; CVector3D minPos; CVector3D maxPos; bool first = true; // Compute min position and max position for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition) continue; CVector3D pos = cmpPosition->GetPosition(); m_PosOld[id] = cmpPosition->GetPosition(); m_AngleOld[id] = cmpPosition->GetRotation().Y.ToFloat(); if (first) { first = false; minPos = pos; maxPos = pos; m_CenterPoint.Y = pos.Y; continue; } if (pos.X < minPos.X) minPos.X = pos.X; if (pos.X > maxPos.X) maxPos.X = pos.X; if (pos.Z < minPos.Z) minPos.Z = pos.Z; if (pos.Z > maxPos.Z) maxPos.Z = pos.Z; } // Calculate objects center point m_CenterPoint.X = minPos.X + ((maxPos.X - minPos.X) * 0.5); m_CenterPoint.Z = minPos.Z + ((maxPos.Z - minPos.Z) * 0.5); CVector3D target = msg->target->GetWorldSpace(m_CenterPoint.Y); m_AngleInitialRotation = atan2(target.X-m_CenterPoint.X, target.Z-m_CenterPoint.Z); } void SetPos(const std::map& position, const std::map& angle) { for (const std::pair& p : position) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; // Set 2D position, ignoring height cmpPosition->JumpTo(entity_pos_t::FromFloat(p.second.X), entity_pos_t::FromFloat(p.second.Z)); if (msg->rotateObject) cmpPosition->SetYRotation(entity_angle_t::FromFloat(angle.at(p.first))); } for (const std::pair& p : position) CheckObstructionAndUpdateVisual(p.first); } void Redo() { SetPos(m_PosNew, m_AngleNew); } void RecalculateRotation(Position newPoint) { std::vector ids = *msg->ids; CVector3D target = newPoint.GetWorldSpace(m_CenterPoint.Y); float newAngle = atan2(target.X-m_CenterPoint.X, target.Z-m_CenterPoint.Z); float globalAngle = m_AngleInitialRotation - newAngle; // Recalculate positions for (entity_id_t id : ids) { CVector3D pos = m_PosOld[id]; float angle = atan2(pos.X - m_CenterPoint.X, pos.Z - m_CenterPoint.Z); float localAngle = angle + (globalAngle - angle); float xCos = cosf(localAngle); float xSin = sinf(localAngle); pos.X -= m_CenterPoint.X; pos.Z -= m_CenterPoint.Z; float newX = pos.X * xCos - pos.Z * xSin; float newZ = pos.X * xSin + pos.Z * xCos; pos.X = newX + m_CenterPoint.X; pos.Z = newZ + m_CenterPoint.Z; m_PosNew[id] = pos; m_AngleNew[id] = m_AngleOld[id] - globalAngle; } SetPos(m_PosNew, m_AngleNew); } void Undo() { SetPos(m_PosOld, m_AngleOld); } void MergeIntoPrevious(cRotateObjectsFromCenterPoint* prev) { // TODO: do something valid if prev unit != this unit ENSURE(*prev->msg->ids == *msg->ids); m_PosOld = prev->m_PosOld; m_AngleInitialRotation = prev->m_AngleInitialRotation; m_AngleOld = prev->m_AngleOld; m_CenterPoint = prev->m_CenterPoint; RecalculateRotation(msg->target); } }; END_COMMAND(RotateObjectsFromCenterPoint) BEGIN_COMMAND(RotateObject) { std::map m_AngleOld, m_AngleNew; void Do() { std::vector ids = *msg->ids; for (entity_id_t id : ids) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); if (!cmpPosition) return; m_AngleOld[id] = cmpPosition->GetRotation().Y.ToFloat(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(0.f); CVector3D pos = transform.GetTranslation(); CVector3D target = msg->target->GetWorldSpace(pos.Y); m_AngleNew[id] = atan2(target.X-pos.X, target.Z-pos.Z); } SetAngle(m_AngleNew); } void SetAngle(const std::map& angles) { for (const std::pair& p : angles) { CmpPtr cmpPosition(*g_Game->GetSimulation2(), p.first); if (!cmpPosition) return; cmpPosition->SetYRotation(entity_angle_t::FromFloat(p.second)); } } void Redo() { SetAngle(m_AngleNew); } void Undo() { SetAngle(m_AngleOld); } void MergeIntoPrevious(cRotateObject* prev) { // TODO: do something valid if prev unit != this unit ENSURE(*prev->msg->ids == *msg->ids); prev->m_AngleNew = m_AngleNew; } }; END_COMMAND(RotateObject) BEGIN_COMMAND(DeleteObjects) { // Saved copy of the important aspects of a unit, to allow undo struct OldObject { entity_id_t entityID; CStr templateName; player_id_t owner; CFixedVector3D pos; CFixedVector3D rot; u32 actorSeed; }; std::vector oldObjects; cDeleteObjects() { } void Do() { Redo(); } void Redo() { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); ENSURE(cmpTemplateManager); std::vector ids = *msg->ids; for (size_t i = 0; i < ids.size(); ++i) { OldObject obj; obj.entityID = (entity_id_t)ids[i]; obj.templateName = cmpTemplateManager->GetCurrentTemplateName(obj.entityID); CmpPtr cmpOwnership(sim, obj.entityID); if (cmpOwnership) obj.owner = cmpOwnership->GetOwner(); CmpPtr cmpPosition(sim, obj.entityID); if (cmpPosition) { obj.pos = cmpPosition->GetPosition(); obj.rot = cmpPosition->GetRotation(); } CmpPtr cmpVisual(sim, obj.entityID); if (cmpVisual) obj.actorSeed = cmpVisual->GetActorSeed(); oldObjects.push_back(obj); g_Game->GetSimulation2()->DestroyEntity(obj.entityID); } g_Game->GetSimulation2()->FlushDestroyedEntities(); } void Undo() { CSimulation2& sim = *g_Game->GetSimulation2(); for (size_t i = 0; i < oldObjects.size(); ++i) { entity_id_t ent = sim.AddEntity(oldObjects[i].templateName.FromUTF8(), oldObjects[i].entityID); if (ent == INVALID_ENTITY) { LOGERROR("Failed to load entity template '%s'", oldObjects[i].templateName.c_str()); } else { CmpPtr cmpPosition(sim, oldObjects[i].entityID); if (cmpPosition) { cmpPosition->JumpTo(oldObjects[i].pos.X, oldObjects[i].pos.Z); cmpPosition->SetXZRotation(oldObjects[i].rot.X, oldObjects[i].rot.Z); cmpPosition->SetYRotation(oldObjects[i].rot.Y); } CmpPtr cmpOwnership(sim, oldObjects[i].entityID); if (cmpOwnership) cmpOwnership->SetOwner(oldObjects[i].owner); CmpPtr cmpVisual(sim, oldObjects[i].entityID); if (cmpVisual) cmpVisual->SetActorSeed(oldObjects[i].actorSeed); } } oldObjects.clear(); } }; END_COMMAND(DeleteObjects) QUERYHANDLER(GetPlayerObjects) { std::vector ids; player_id_t playerID = msg->player; const CSimulation2::InterfaceListUnordered& cmps = g_Game->GetSimulation2()->GetEntitiesWithInterfaceUnordered(IID_Ownership); for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit) { if (static_cast(eit->second)->GetOwner() == playerID) { ids.push_back(eit->first); } } msg->ids = ids; } MESSAGEHANDLER(SetBandbox) { AtlasView::GetView_Game()->SetBandbox(msg->show, (float)msg->sx0, (float)msg->sy0, (float)msg->sx1, (float)msg->sy1); } QUERYHANDLER(GetSelectedObjectsTemplateNames) { std::vector ids = *msg->ids; std::vector names; CmpPtr cmpTemplateManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); ENSURE(cmpTemplateManager); for (size_t i = 0; i < ids.size(); ++i) { entity_id_t id = (entity_id_t)ids[i]; std::string templateName = cmpTemplateManager->GetCurrentTemplateName(id); names.push_back(templateName); } std::sort(names.begin(), names.end()); msg->names = names; } -} + +} // namespace AtlasMessage Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26120) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26121) @@ -1,549 +1,549 @@ -/* Copyright (C) 2019 Wildfire Games. +/* 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 "precompiled.h" #include "MessageHandler.h" #include "../CommandProc.h" #include "graphics/Patch.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/Terrain.h" #include "ps/Game.h" #include "ps/World.h" #include "lib/ogl.h" -#include "lib/res/graphics/ogl_tex.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/helpers/Grid.h" #include "../Brushes.h" #include "../DeltaArray.h" #include "../View.h" #include -namespace AtlasMessage { +namespace AtlasMessage +{ QUERYHANDLER(GetTerrainGroups) { const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups(); std::vector groupNames; for (CTerrainTextureManager::TerrainGroupMap::const_iterator it = groups.begin(); it != groups.end(); ++it) groupNames.push_back(it->first.FromUTF8()); msg->groupNames = groupNames; } static bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b) { return (wcscmp(a.name.c_str(), b.name.c_str()) < 0); } static sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, int width, int height) { sTerrainTexturePreview preview; preview.name = tex->GetTag().FromUTF8(); std::vector buf (width*height*3); #if !CONFIG2_GLES // It's not good to shrink the entire texture to fit the small preview // window, since it's the fine details in the texture that are // interesting; so just go down one mipmap level, then crop a chunk // out of the middle. // Read the size of the texture. (Usually loads the texture from // disk, which is slow.) tex->GetTexture()->Bind(); int level = 1; // level 0 is the original size int w = std::max(1, (int)tex->GetTexture()->GetWidth() >> level); int h = std::max(1, (int)tex->GetTexture()->GetHeight() >> level); if (w >= width && h >= height) { // Read the whole texture into a new buffer unsigned char* texdata = new unsigned char[w*h*3]; glGetTexImage(GL_TEXTURE_2D, level, GL_RGB, GL_UNSIGNED_BYTE, texdata); // Extract the middle section (as a representative preview), // and copy into buf unsigned char* texdata_ptr = texdata + (w*(h - height)/2 + (w - width)/2) * 3; unsigned char* buf_ptr = &buf[0]; for (ssize_t y = 0; y < height; ++y) { memcpy(buf_ptr, texdata_ptr, width*3); buf_ptr += width*3; texdata_ptr += w*3; } delete[] texdata; } else #endif { // Too small to preview, or glGetTexImage not supported (on GLES) // Just use a flat color instead u32 c = tex->GetBaseColor(); for (ssize_t i = 0; i < width*height; ++i) { buf[i*3+0] = (c>>16) & 0xff; buf[i*3+1] = (c>>8) & 0xff; buf[i*3+2] = (c>>0) & 0xff; } } preview.loaded = tex->GetTexture()->IsLoaded(); preview.imageWidth = width; preview.imageHeight = height; preview.imageData = buf; return preview; } QUERYHANDLER(GetTerrainGroupPreviews) { std::vector previews; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) { previews.push_back(GetPreview(*it, msg->imageWidth, msg->imageHeight)); } // Sort the list alphabetically by name std::sort(previews.begin(), previews.end(), CompareTerrain); msg->previews = previews; } QUERYHANDLER(GetTerrainPassabilityClasses) { CmpPtr cmpPathfinder(*AtlasView::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY); if (cmpPathfinder) { std::map nonPathfindingClasses, pathfindingClasses; cmpPathfinder->GetPassabilityClasses(nonPathfindingClasses, pathfindingClasses); std::vector classNames; for (std::map::iterator it = nonPathfindingClasses.begin(); it != nonPathfindingClasses.end(); ++it) classNames.push_back(CStr(it->first).FromUTF8()); msg->classNames = classNames; } } QUERYHANDLER(GetTerrainTexture) { ssize_t x, y; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); g_CurrentBrush.GetCentre(x, y); CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CMiniPatch* tile = terrain->GetTile(x, y); if (tile) { CTerrainTextureEntry* tex = tile->GetTextureEntry(); msg->texture = tex->GetTag().FromUTF8(); } else { msg->texture = std::wstring(); } } QUERYHANDLER(GetTerrainTexturePreview) { CTerrainTextureEntry* tex = g_TexMan.FindTexture(CStrW(*msg->name).ToUTF8()); if (tex) { msg->preview = GetPreview(tex, msg->imageWidth, msg->imageHeight); } else { sTerrainTexturePreview noPreview{}; noPreview.name = std::wstring(); noPreview.loaded = false; noPreview.imageHeight = 0; noPreview.imageWidth = 0; msg->preview = noPreview; } } ////////////////////////////////////////////////////////////////////////// namespace { struct TerrainTile { TerrainTile(CTerrainTextureEntry* t, ssize_t p) : tex(t), priority(p) {} CTerrainTextureEntry* tex; ssize_t priority; }; class TerrainArray : public DeltaArray2D { public: void Init() { m_Terrain = g_Game->GetWorld()->GetTerrain(); m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide(); } void UpdatePriority(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priorityScale, ssize_t& priority) { CMiniPatch* tile = m_Terrain->GetTile(x, y); if (!tile) return; // tile was out-of-bounds // If this tile matches the current texture, we just want to match its // priority; otherwise we want to exceed its priority if (tile->GetTextureEntry() == tex) priority = std::max(priority, tile->GetPriority()*priorityScale); else priority = std::max(priority, tile->GetPriority()*priorityScale + 1); } CTerrainTextureEntry* GetTexEntry(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return NULL; return get(x, y).tex; } ssize_t GetPriority(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return 0; return get(x, y).priority; } void PaintTile(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priority) { // Ignore out-of-bounds tiles if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return; set(x,y, TerrainTile(tex, priority)); } ssize_t GetTilesPerSide() { return m_VertsPerSide-1; } protected: TerrainTile getOld(ssize_t x, ssize_t y) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); return TerrainTile(mp->Tex, mp->Priority); } void setNew(ssize_t x, ssize_t y, const TerrainTile& val) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); mp->Tex = val.tex; mp->Priority = val.priority; } CTerrain* m_Terrain; ssize_t m_VertsPerSide; }; } BEGIN_COMMAND(PaintTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cPaintTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } // Priority system: If the new tile should have a high priority, // set it to one plus the maximum priority of all surrounding tiles // that aren't included in the brush (so that it's definitely the highest). // Similar for low priority. ssize_t priorityScale = (msg->priority == ePaintTerrainPriority::HIGH ? +1 : -1); ssize_t priority = 0; for (ssize_t dy = -1; dy < g_CurrentBrush.m_H+1; ++dy) { for (ssize_t dx = -1; dx < g_CurrentBrush.m_W+1; ++dx) { if (!(g_CurrentBrush.Get(dx, dy) > 0.5f)) // ignore tiles that will be painted over m_TerrainDelta.UpdatePriority(x0+dx, y0+dy, texentry, priorityScale, priority); } } for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { if (g_CurrentBrush.Get(dx, dy) > 0.5f) // TODO: proper solid brushes m_TerrainDelta.PaintTile(x0+dx, y0+dy, texentry, priority*priorityScale); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W + 1; m_j1 = y0 + g_CurrentBrush.m_H + 1; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cPaintTerrain* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(PaintTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(ReplaceTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cReplaceTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); for (ssize_t j = 0; j < tiles; ++j) { for (ssize_t i = 0; i < tiles; ++i) { if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { m_i0 = std::min(m_i0, i-1); m_j0 = std::min(m_j0, j-1); m_i1 = std::max(m_i1, i+2); m_j1 = std::max(m_j1, j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(ReplaceTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(FillTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cFillTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); // Simple 4-way flood fill algorithm using queue and a grid to keep track of visited tiles, // almost as fast as loop for filling whole map, much faster for small patches SparseGrid visited(tiles, tiles); std::queue > queue; // Initial tile queue.push(std::make_pair((u16)x0, (u16)y0)); visited.set(x0, y0, true); while(!queue.empty()) { // Check front of queue std::pair t = queue.front(); queue.pop(); u16 i = t.first; u16 j = t.second; if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { // Found a tile to replace: adjust bounds and paint it m_i0 = std::min(m_i0, (ssize_t)i-1); m_j0 = std::min(m_j0, (ssize_t)j-1); m_i1 = std::max(m_i1, (ssize_t)i+2); m_j1 = std::max(m_j1, (ssize_t)j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); // Visit 4 adjacent tiles (could visit 8 if we want to count diagonal adjacency) if (i > 0 && !visited.get(i-1, j)) { visited.set(i-1, j, true); queue.push(std::make_pair(i-1, j)); } if (i < (tiles-1) && !visited.get(i+1, j)) { visited.set(i+1, j, true); queue.push(std::make_pair(i+1, j)); } if (j > 0 && !visited.get(i, j-1)) { visited.set(i, j-1, true); queue.push(std::make_pair(i, j-1)); } if (j < (tiles-1) && !visited.get(i, j+1)) { visited.set(i, j+1, true); queue.push(std::make_pair(i, j+1)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(FillTerrain) -} +} // namespace AtlasMessage