Index: ps/trunk/source/graphics/Camera.cpp =================================================================== --- ps/trunk/source/graphics/Camera.cpp (revision 27860) +++ ps/trunk/source/graphics/Camera.cpp (revision 27861) @@ -1,456 +1,456 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "Camera.h" #include "graphics/HFTracer.h" #include "graphics/Terrain.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/SceneRenderer.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.GetSceneRenderer().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(); + const 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.GetSceneRenderer().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(); + const 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(); const CVector3D s = orientation.Cross(up); up = s.Cross(orientation); 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/CameraController.cpp =================================================================== --- ps/trunk/source/graphics/CameraController.cpp (revision 27860) +++ ps/trunk/source/graphics/CameraController.cpp (revision 27861) @@ -1,699 +1,701 @@ -/* Copyright (C) 2022 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 . -*/ +/* Copyright (C) 2023 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 "CameraController.h" #include "graphics/HFTracer.h" #include "graphics/Terrain.h" #include "i18n/L10n.h" #include "lib/input.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" extern int g_xres, g_yres; // Maximum distance outside the edge of the map that the camera's // focus point can be moved static const float CAMERA_EDGE_MARGIN = 2.0f * TERRAIN_TILE_SIZE; CCameraController::CCameraController(CCamera& camera) : ICameraController(camera), m_ConstrainCamera(true), m_FollowEntity(INVALID_ENTITY), m_FollowFirstPerson(false), // Dummy values (these will be filled in by the config file) m_ViewScrollSpeed(0), m_ViewScrollSpeedModifier(1), m_ViewScrollMouseDetectDistance(3), m_ViewRotateXSpeed(0), m_ViewRotateXMin(0), m_ViewRotateXMax(0), m_ViewRotateXDefault(0), m_ViewRotateYSpeed(0), m_ViewRotateYSpeedWheel(0), m_ViewRotateYDefault(0), m_ViewRotateSpeedModifier(1), m_ViewDragSpeed(0), m_ViewZoomSpeed(0), m_ViewZoomSpeedWheel(0), m_ViewZoomMin(0), m_ViewZoomMax(0), m_ViewZoomDefault(0), m_ViewZoomSpeedModifier(1), m_ViewFOV(DEGTORAD(45.f)), m_ViewNear(2.f), m_ViewFar(4096.f), m_HeightSmoothness(0.5f), m_HeightMin(16.f), m_PosX(0, 0, 0.01f), m_PosY(0, 0, 0.01f), m_PosZ(0, 0, 0.01f), m_Zoom(0, 0, 0.1f), m_RotateX(0, 0, 0.001f), m_RotateY(0, 0, 0.001f) { SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; m_Camera.SetViewPort(vp); SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CCameraController::~CCameraController() = default; void CCameraController::LoadConfig() { CFG_GET_VAL("view.scroll.speed", m_ViewScrollSpeed); CFG_GET_VAL("view.scroll.speed.modifier", m_ViewScrollSpeedModifier); CFG_GET_VAL("view.scroll.mouse.detectdistance", m_ViewScrollMouseDetectDistance); CFG_GET_VAL("view.rotate.x.speed", m_ViewRotateXSpeed); CFG_GET_VAL("view.rotate.x.min", m_ViewRotateXMin); CFG_GET_VAL("view.rotate.x.max", m_ViewRotateXMax); CFG_GET_VAL("view.rotate.x.default", m_ViewRotateXDefault); CFG_GET_VAL("view.rotate.y.speed", m_ViewRotateYSpeed); CFG_GET_VAL("view.rotate.y.speed.wheel", m_ViewRotateYSpeedWheel); CFG_GET_VAL("view.rotate.y.default", m_ViewRotateYDefault); CFG_GET_VAL("view.rotate.speed.modifier", m_ViewRotateSpeedModifier); CFG_GET_VAL("view.drag.speed", m_ViewDragSpeed); CFG_GET_VAL("view.zoom.speed", m_ViewZoomSpeed); CFG_GET_VAL("view.zoom.speed.wheel", m_ViewZoomSpeedWheel); CFG_GET_VAL("view.zoom.min", m_ViewZoomMin); CFG_GET_VAL("view.zoom.max", m_ViewZoomMax); CFG_GET_VAL("view.zoom.default", m_ViewZoomDefault); CFG_GET_VAL("view.zoom.speed.modifier", m_ViewZoomSpeedModifier); CFG_GET_VAL("view.height.smoothness", m_HeightSmoothness); CFG_GET_VAL("view.height.min", m_HeightMin); #define SETUP_SMOOTHNESS(CFG_PREFIX, SMOOTHED_VALUE) \ { \ float smoothness = SMOOTHED_VALUE.GetSmoothness(); \ CFG_GET_VAL(CFG_PREFIX ".smoothness", smoothness); \ SMOOTHED_VALUE.SetSmoothness(smoothness); \ } SETUP_SMOOTHNESS("view.pos", m_PosX); SETUP_SMOOTHNESS("view.pos", m_PosY); SETUP_SMOOTHNESS("view.pos", m_PosZ); SETUP_SMOOTHNESS("view.zoom", m_Zoom); SETUP_SMOOTHNESS("view.rotate.x", m_RotateX); SETUP_SMOOTHNESS("view.rotate.y", m_RotateY); #undef SETUP_SMOOTHNESS CFG_GET_VAL("view.near", m_ViewNear); CFG_GET_VAL("view.far", m_ViewFar); CFG_GET_VAL("view.fov", m_ViewFOV); // Convert to radians m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_ViewFOV = DEGTORAD(m_ViewFOV); } void CCameraController::SetViewport(const SViewPort& vp) { m_Camera.SetViewPort(vp); SetCameraProjection(); } void CCameraController::Update(const float deltaRealTime) { // Calculate mouse movement static int mouse_last_x = 0; static int mouse_last_y = 0; int mouse_dx = g_mouse_x - mouse_last_x; int mouse_dy = g_mouse_y - mouse_last_y; mouse_last_x = g_mouse_x; mouse_last_y = g_mouse_y; if (HotkeyIsPressed("camera.rotate.cw")) m_RotateY.AddSmoothly(m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.ccw")) m_RotateY.AddSmoothly(-m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.up")) m_RotateX.AddSmoothly(-m_ViewRotateXSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.down")) m_RotateX.AddSmoothly(m_ViewRotateXSpeed * deltaRealTime); float moveRightward = 0.f; float moveForward = 0.f; if (HotkeyIsPressed("camera.pan")) { moveRightward += m_ViewDragSpeed * mouse_dx; moveForward += m_ViewDragSpeed * -mouse_dy; } if (g_mouse_active && m_ViewScrollMouseDetectDistance > 0) { if (g_mouse_x >= g_xres - m_ViewScrollMouseDetectDistance && g_mouse_x < g_xres) moveRightward += m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_x < m_ViewScrollMouseDetectDistance && g_mouse_x >= 0) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (g_mouse_y >= g_yres - m_ViewScrollMouseDetectDistance && g_mouse_y < g_yres) moveForward -= m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_y < m_ViewScrollMouseDetectDistance && g_mouse_y >= 0) moveForward += m_ViewScrollSpeed * deltaRealTime; } if (HotkeyIsPressed("camera.right")) moveRightward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.left")) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.up")) moveForward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.down")) moveForward -= m_ViewScrollSpeed * deltaRealTime; if (moveRightward || moveForward) { // Break out of following mode when the user starts scrolling m_FollowEntity = INVALID_ENTITY; float s = sin(m_RotateY.GetSmoothedValue()); float c = cos(m_RotateY.GetSmoothedValue()); m_PosX.AddSmoothly(c * moveRightward); m_PosZ.AddSmoothly(-s * moveRightward); m_PosX.AddSmoothly(s * moveForward); m_PosZ.AddSmoothly(c * moveForward); } if (m_FollowEntity) { CmpPtr cmpPosition(*(g_Game->GetSimulation2()), m_FollowEntity); CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpPosition && cmpPosition->IsInWorld() && cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FollowEntity, g_Game->GetViewedPlayerID()) == LosVisibility::VISIBLE) { // Get the most recent interpolated position float frameOffset = g_Game->GetSimulation2()->GetLastFrameOffset(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset); CVector3D pos = transform.GetTranslation(); if (m_FollowFirstPerson) { float x, z, angle; cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle); float height = 4.f; m_Camera.m_Orientation.SetIdentity(); m_Camera.m_Orientation.RotateX(static_cast(M_PI) / 24.f); m_Camera.m_Orientation.RotateY(angle); m_Camera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z); m_Camera.UpdateFrustum(); return; } else { // Move the camera to match the unit CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pos - pivot; m_PosX.AddSmoothly(delta.X); m_PosY.AddSmoothly(delta.Y); m_PosZ.AddSmoothly(delta.Z); } } else { // The unit disappeared (died or garrisoned etc), so stop following it m_FollowEntity = INVALID_ENTITY; } } if (HotkeyIsPressed("camera.zoom.in")) m_Zoom.AddSmoothly(-m_ViewZoomSpeed * deltaRealTime); if (HotkeyIsPressed("camera.zoom.out")) m_Zoom.AddSmoothly(m_ViewZoomSpeed * deltaRealTime); if (m_ConstrainCamera) m_Zoom.ClampSmoothly(m_ViewZoomMin, m_ViewZoomMax); float zoomDelta = -m_Zoom.Update(deltaRealTime); if (zoomDelta) { CVector3D forwards = m_Camera.GetOrientation().GetIn(); m_PosX.AddSmoothly(forwards.X * zoomDelta); m_PosY.AddSmoothly(forwards.Y * zoomDelta); m_PosZ.AddSmoothly(forwards.Z * zoomDelta); } if (m_ConstrainCamera) m_RotateX.ClampSmoothly(DEGTORAD(m_ViewRotateXMin), DEGTORAD(m_ViewRotateXMax)); FocusHeight(true); // Ensure the ViewCamera focus is inside the map with the chosen margins // if not so - apply margins to the camera if (m_ConstrainCamera) { CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); - CTerrain* pTerrain = g_Game->GetWorld()->GetTerrain(); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CVector3D desiredPivot = pivot; CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpRangeManager && cmpRangeManager->GetLosCircular()) { // Clamp to a circular region around the center of the map - float r = pTerrain->GetMaxX() / 2; + const float r = terrain.GetMaxX() / 2.f; CVector3D center(r, desiredPivot.Y, r); float dist = (desiredPivot - center).Length(); if (dist > r - CAMERA_EDGE_MARGIN) desiredPivot = center + (desiredPivot - center).Normalized() * (r - CAMERA_EDGE_MARGIN); } else { // Clamp to the square edges of the map - desiredPivot.X = Clamp(desiredPivot.X, pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN); - desiredPivot.Z = Clamp(desiredPivot.Z, pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN); + desiredPivot.X = Clamp(desiredPivot.X, terrain.GetMinX() + CAMERA_EDGE_MARGIN, + terrain.GetMaxX() - CAMERA_EDGE_MARGIN); + desiredPivot.Z = Clamp(desiredPivot.Z, terrain.GetMinZ() + CAMERA_EDGE_MARGIN, + terrain.GetMaxZ() - CAMERA_EDGE_MARGIN); } // Update the position so that pivot is within the margin m_PosX.SetValueSmoothly(desiredPivot.X + delta.X); m_PosZ.SetValueSmoothly(desiredPivot.Z + delta.Z); } m_PosX.Update(deltaRealTime); m_PosY.Update(deltaRealTime); m_PosZ.Update(deltaRealTime); // Handle rotation around the Y (vertical) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateYDelta = m_RotateY.Update(deltaRealTime); if (rotateYDelta) { // We've updated RotateY, and need to adjust Pos so that it's still // facing towards the original focus point (the terrain in the center // of the screen). CVector3D upwards(0.0f, 1.0f, 0.0f); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(upwards, rotateYDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } // Handle rotation around the X (sideways, relative to camera) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateXDelta = m_RotateX.Update(deltaRealTime); if (rotateXDelta) { CVector3D rightwards = targetCam.GetOrientation().GetLeft() * -1.0f; CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(rightwards, rotateXDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } /* This is disabled since it doesn't seem necessary: // Ensure the camera's near point is never inside the terrain if (m_ConstrainCamera) { CMatrix3D target; target.SetIdentity(); target.RotateX(m_RotateX.GetValue()); target.RotateY(m_RotateY.GetValue()); target.Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); CVector3D nearPoint = target.GetTranslation() + target.GetIn() * defaultNear; float ground = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z); float limit = ground + 16.f; if (nearPoint.Y < limit) m_PosY.AddSmoothly(limit - nearPoint.Y); } */ m_RotateY.Wrap(-static_cast(M_PI), static_cast(M_PI)); // Update the camera matrix SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CVector3D CCameraController::GetSmoothPivot(CCamera& camera) const { return camera.GetOrientation().GetTranslation() + camera.GetOrientation().GetIn() * m_Zoom.GetSmoothedValue(); } CVector3D CCameraController::GetCameraPivot() const { return GetSmoothPivot(m_Camera); } CVector3D CCameraController::GetCameraPosition() const { return CVector3D(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } CVector3D CCameraController::GetCameraRotation() const { // The angle of rotation around the Z axis is not used. return CVector3D(m_RotateX.GetValue(), m_RotateY.GetValue(), 0.0f); } float CCameraController::GetCameraZoom() const { return m_Zoom.GetValue(); } void CCameraController::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom) { m_PosX.SetValue(pos.X); m_PosY.SetValue(pos.Y); m_PosZ.SetValue(pos.Z); m_RotateX.SetValue(rotX); m_RotateY.SetValue(rotY); m_Zoom.SetValue(zoom); FocusHeight(false); SetupCameraMatrixNonSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::MoveCameraTarget(const CVector3D& target) { // Maintain the same orientation and level of zoom, if we can // (do this by working out the point the camera is looking at, saving // the difference between that position and the camera point, and restoring // that difference to our new target) CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = target - pivot; m_PosX.SetValueSmoothly(delta.X + m_PosX.GetValue()); m_PosZ.SetValueSmoothly(delta.Z + m_PosZ.GetValue()); FocusHeight(false); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::ResetCameraTarget(const CVector3D& target) { CMatrix3D orientation; orientation.SetIdentity(); orientation.RotateX(DEGTORAD(m_ViewRotateXDefault)); orientation.RotateY(DEGTORAD(m_ViewRotateYDefault)); CVector3D delta = orientation.GetIn() * m_ViewZoomDefault; m_PosX.SetValue(target.X - delta.X); m_PosY.SetValue(target.Y - delta.Y); m_PosZ.SetValue(target.Z - delta.Z); m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_Zoom.SetValue(m_ViewZoomDefault); FocusHeight(false); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::FollowEntity(entity_id_t entity, bool firstPerson) { m_FollowEntity = entity; m_FollowFirstPerson = firstPerson; } entity_id_t CCameraController::GetFollowedEntity() { return m_FollowEntity; } void CCameraController::SetCameraProjection() { m_Camera.SetPerspectiveProjection(m_ViewNear, m_ViewFar, m_ViewFOV); } void CCameraController::ResetCameraAngleZoom() { CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); // Compute the zoom adjustment to get us back to the default CVector3D forwards = targetCam.GetOrientation().GetIn(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pivot - targetCam.GetOrientation().GetTranslation(); float dist = delta.Dot(forwards); m_Zoom.AddSmoothly(m_ViewZoomDefault - dist); // Reset orientations to default m_RotateX.SetValueSmoothly(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValueSmoothly(DEGTORAD(m_ViewRotateYDefault)); } void CCameraController::SetupCameraMatrixSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetSmoothedValue(), m_PosY.GetSmoothedValue(), m_PosZ.GetSmoothedValue()); } void CCameraController::SetupCameraMatrixSmoothRot(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::SetupCameraMatrixNonSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetValue()); orientation->RotateY(m_RotateY.GetValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::FocusHeight(bool smooth) { /* The camera pivot height is moved towards ground level. To prevent excessive zoom when looking over a cliff, the target ground level is the maximum of the ground level at the camera's near and pivot points. The ground levels are filtered to achieve smooth camera movement. The filter radius is proportional to the zoom level. The camera height is clamped to prevent map penetration. */ if (!m_ConstrainCamera) return; CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); const CVector3D position = targetCam.GetOrientation().GetTranslation(); const CVector3D forwards = targetCam.GetOrientation().GetIn(); // horizontal view radius const float radius = sqrtf(forwards.X * forwards.X + forwards.Z * forwards.Z) * m_Zoom.GetSmoothedValue(); const float near_radius = radius * m_HeightSmoothness; const float pivot_radius = radius * m_HeightSmoothness; const CVector3D nearPoint = position + forwards * m_ViewNear; const CVector3D pivotPoint = position + forwards * m_Zoom.GetSmoothedValue(); const float ground = std::max( - g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z), + g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(nearPoint.X, nearPoint.Z), g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight); // filter ground levels for smooth camera movement - const float filtered_near_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius); - const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius); + const float filtered_near_ground = g_Game->GetWorld()->GetTerrain().GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius); + const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain().GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius); // filtered maximum visible ground level in view const float filtered_ground = std::max( std::max(filtered_near_ground, filtered_pivot_ground), g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight); // target camera height above pivot point const float pivot_height = -forwards.Y * (m_Zoom.GetSmoothedValue() - m_ViewNear); // minimum camera height above filtered ground level const float min_height = (m_HeightMin + ground - filtered_ground); const float target_height = std::max(pivot_height, min_height); const float height = (nearPoint.Y - filtered_ground); const float diff = target_height - height; if (fabsf(diff) < 0.0001f) return; if (smooth) m_PosY.AddSmoothly(diff); else m_PosY.Add(diff); } InReaction CCameraController::HandleEvent(const SDL_Event_* ev) { switch (ev->ev.type) { case SDL_HOTKEYPRESS: { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "camera.reset") { ResetCameraAngleZoom(); return IN_HANDLED; } return IN_PASS; } case SDL_HOTKEYDOWN: { std::string hotkey = static_cast(ev->ev.user.data1); // Mouse wheel must be treated using events instead of polling, // because SDL auto-generates a sequence of mousedown/mouseup events // and we never get to see the "down" state inside Update(). if (hotkey == "camera.zoom.wheel.in") { m_Zoom.AddSmoothly(-m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.zoom.wheel.out") { m_Zoom.AddSmoothly(m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.cw") { m_RotateY.AddSmoothly(m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.ccw") { m_RotateY.AddSmoothly(-m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.increase") { m_ViewScrollSpeed *= m_ViewScrollSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Scroll speed increased to %.1f"), m_ViewScrollSpeed); return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.decrease") { m_ViewScrollSpeed /= m_ViewScrollSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Scroll speed decreased to %.1f"), m_ViewScrollSpeed); return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.increase") { m_ViewRotateXSpeed *= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed *= m_ViewRotateSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Rotate speed increased to X=%.3f, Y=%.3f"), m_ViewRotateXSpeed, m_ViewRotateYSpeed); return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.decrease") { m_ViewRotateXSpeed /= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed /= m_ViewRotateSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Rotate speed decreased to X=%.3f, Y=%.3f"), m_ViewRotateXSpeed, m_ViewRotateYSpeed); return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.increase") { m_ViewZoomSpeed *= m_ViewZoomSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Zoom speed increased to %.1f"), m_ViewZoomSpeed); return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.decrease") { m_ViewZoomSpeed /= m_ViewZoomSpeedModifier; LOGMESSAGERENDER(g_L10n.Translate("Zoom speed decreased to %.1f"), m_ViewZoomSpeed); return IN_HANDLED; } return IN_PASS; } } return IN_PASS; } Index: ps/trunk/source/graphics/CinemaManager.cpp =================================================================== --- ps/trunk/source/graphics/CinemaManager.cpp (revision 27860) +++ ps/trunk/source/graphics/CinemaManager.cpp (revision 27861) @@ -1,147 +1,147 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "graphics/CinemaManager.h" #include "graphics/Camera.h" #include "graphics/Color.h" #include "graphics/GameView.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Hotkey.h" #include "ps/World.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "simulation2/components/ICmpCinemaManager.h" #include "simulation2/components/ICmpOverlayRenderer.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/CinemaPath.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/Simulation2.h" CCinemaManager::CCinemaManager() : m_DrawPaths(false) { } void CCinemaManager::Update(const float deltaRealTime) const { CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity()); if (!cmpCinemaManager) return; if (IsPlaying()) cmpCinemaManager->PlayQueue(deltaRealTime, g_Game->GetView()->GetCamera()); } void CCinemaManager::Render() const { if (!IsEnabled() && m_DrawPaths) DrawPaths(); } void CCinemaManager::DrawPaths() const { CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity()); if (!cmpCinemaManager) return; for (const std::pair& p : cmpCinemaManager->GetPaths()) { DrawSpline(p.second, CColor(0.2f, 0.2f, 1.f, 0.9f), 128); DrawNodes(p.second, CColor(0.1f, 1.f, 0.f, 1.f)); if (p.second.GetTargetSpline().GetAllNodes().empty()) continue; DrawSpline(p.second.GetTargetSpline(), CColor(1.f, 0.3f, 0.4f, 0.9f), 128); DrawNodes(p.second.GetTargetSpline(), CColor(1.f, 0.1f, 0.f, 1.f)); } } void CCinemaManager::DrawSpline(const RNSpline& spline, const CColor& splineColor, int smoothness) const { if (spline.GetAllNodes().size() < 2) return; if (spline.GetAllNodes().size() == 2) smoothness = 2; const float start = spline.MaxDistance.ToFloat() / smoothness; std::vector line; for (int i = 0; i <= smoothness; ++i) { const float time = start * i / spline.MaxDistance.ToFloat(); line.emplace_back(spline.GetPosition(time)); } g_Renderer.GetDebugRenderer().DrawLine(line, splineColor, 0.2f, false); // Height indicator - if (g_Game && g_Game->GetWorld() && g_Game->GetWorld()->GetTerrain()) + if (g_Game && g_Game->GetWorld()) { for (int i = 0; i <= smoothness; ++i) { const float time = start * i / spline.MaxDistance.ToFloat(); const CVector3D tmp = spline.GetPosition(time); - const float groundY = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(tmp.X, tmp.Z); + const float groundY = g_Game->GetWorld()->GetTerrain().GetExactGroundLevel(tmp.X, tmp.Z); g_Renderer.GetDebugRenderer().DrawLine(tmp, CVector3D(tmp.X, groundY, tmp.Z), splineColor, 0.1f, false); } } } void CCinemaManager::DrawNodes(const RNSpline& spline, const CColor& nodeColor) const { for (const SplineData& node : spline.GetAllNodes()) { g_Renderer.GetDebugRenderer().DrawCircle( CVector3D(node.Position.X.ToFloat(), node.Position.Y.ToFloat(), node.Position.Z.ToFloat()), 0.5f, nodeColor); } } bool CCinemaManager::IsEnabled() const { CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity()); return cmpCinemaManager && cmpCinemaManager->IsEnabled(); } bool CCinemaManager::IsPlaying() const { return IsEnabled() && g_Game && !g_Game->m_Paused; } bool CCinemaManager::GetPathsDrawing() const { return m_DrawPaths; } void CCinemaManager::SetPathsDrawing(const bool drawPath) { m_DrawPaths = drawPath; } Index: ps/trunk/source/graphics/GameView.cpp =================================================================== --- ps/trunk/source/graphics/GameView.cpp (revision 27860) +++ ps/trunk/source/graphics/GameView.cpp (revision 27861) @@ -1,407 +1,407 @@ /* Copyright (C) 2023 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 "GameView.h" #include "graphics/CameraController.h" #include "graphics/CinemaManager.h" #include "graphics/ColladaManager.h" #include "graphics/HFTracer.h" #include "graphics/LOSTexture.h" #include "graphics/LightEnv.h" #include "graphics/MiniMapTexture.h" #include "graphics/Model.h" #include "graphics/ObjectManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/SmoothedValue.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/Unit.h" #include "graphics/UnitManager.h" #include "lib/input.h" #include "lib/timer.h" #include "lobby/IXmppClient.h" #include "maths/BoundingBoxAligned.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include class CGameViewImpl { NONCOPYABLE(CGameViewImpl); public: CGameViewImpl(Renderer::Backend::IDevice* device, CGame* game) : Game(game), ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()), LOSTexture(*game->GetSimulation2()), TerritoryTexture(*game->GetSimulation2()), MiniMapTexture(device, *game->GetSimulation2()), ViewCamera(), CullCamera(), LockCullCamera(false), Culling(true), CameraController(new CCameraController(ViewCamera)) { } CGame* Game; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; CMiniMapTexture MiniMapTexture; /** * this camera controls the eye position when rendering */ CCamera ViewCamera; /** * this camera controls the frustum that is used for culling * and shadow calculations * * Note that all code that works with camera movements should only change * m_ViewCamera. The render functions automatically sync the cull camera to * the view camera depending on the value of m_LockCullCamera. */ CCamera CullCamera; /** * When @c true, the cull camera is locked in place. * When @c false, the cull camera follows the view camera. * * Exposed to JS as gameView.lockCullCamera */ bool LockCullCamera; /** * When @c true, culling is enabled so that only models that have a chance of * being visible are sent to the renderer. * Otherwise, the entire world is sent to the renderer. * * Exposed to JS as gameView.culling */ bool Culling; CCinemaManager CinemaManager; /** * Controller of the view's camera. We use a std::unique_ptr for an easy * on the fly replacement. It's guaranteed that the pointer is never nulllptr. */ std::unique_ptr CameraController; }; #define IMPLEMENT_BOOLEAN_SETTING(NAME) \ bool CGameView::Get##NAME##Enabled() const \ { \ return m->NAME; \ } \ \ void CGameView::Set##NAME##Enabled(bool Enabled) \ { \ m->NAME = Enabled; \ } IMPLEMENT_BOOLEAN_SETTING(Culling); IMPLEMENT_BOOLEAN_SETTING(LockCullCamera); bool CGameView::GetConstrainCameraEnabled() const { return m->CameraController->GetConstrainCamera(); } void CGameView::SetConstrainCameraEnabled(bool enabled) { m->CameraController->SetConstrainCamera(enabled); } #undef IMPLEMENT_BOOLEAN_SETTING CGameView::CGameView(Renderer::Backend::IDevice* device, CGame *pGame): m(new CGameViewImpl(device, pGame)) { m->CullCamera = m->ViewCamera; g_Renderer.GetSceneRenderer().SetSceneCamera(m->ViewCamera, m->CullCamera); } CGameView::~CGameView() { UnloadResources(); delete m; } void CGameView::SetViewport(const SViewPort& vp) { m->CameraController->SetViewport(vp); } CObjectManager& CGameView::GetObjectManager() { return m->ObjectManager; } CCamera* CGameView::GetCamera() { return &m->ViewCamera; } CCinemaManager* CGameView::GetCinema() { return &m->CinemaManager; } CLOSTexture& CGameView::GetLOSTexture() { return m->LOSTexture; } CTerritoryTexture& CGameView::GetTerritoryTexture() { return m->TerritoryTexture; } CMiniMapTexture& CGameView::GetMiniMapTexture() { return m->MiniMapTexture; } void CGameView::RegisterInit() { // CGameView init LDR_Register([this](const double) { m->CameraController->LoadConfig(); return 0; }, L"CGameView init", 1); LDR_Register([](const double) { return g_TexMan.LoadTerrainTextures(); }, L"LoadTerrainTextures", 60); } void CGameView::BeginFrame() { if (m->LockCullCamera == false) { // Set up cull camera m->CullCamera = m->ViewCamera; } g_Renderer.GetSceneRenderer().SetSceneCamera(m->ViewCamera, m->CullCamera); m->Game->CachePlayerColors(); } void CGameView::Prepare( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { g_Renderer.GetSceneRenderer().PrepareScene(deviceCommandContext, *this); } void CGameView::Render( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { g_Renderer.GetSceneRenderer().RenderScene(deviceCommandContext); } void CGameView::RenderOverlays( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { g_Renderer.GetSceneRenderer().RenderSceneOverlays(deviceCommandContext); } /////////////////////////////////////////////////////////// // This callback is part of the Scene interface // Submit all objects visible in the given frustum void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c) { { PROFILE3("submit terrain"); - CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); + const CTerrain& terrain = m->Game->GetWorld()->GetTerrain(); float waterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight + 0.001f; - const ssize_t patchesPerSide = pTerrain->GetPatchesPerSide(); + const ssize_t patchesPerSide = terrain.GetPatchesPerSide(); // find out which patches will be drawn for (ssize_t j=0; jGetPatch(i,j); // can't fail + CPatch* const patch = terrain.GetPatch(i,j); // can't fail // If the patch is underwater, calculate a bounding box that also contains the water plane CBoundingBoxAligned bounds = patch->GetWorldBounds(); if(bounds[1].Y < waterHeight) bounds[1].Y = waterHeight; if (!m->Culling || frustum.IsBoxVisible(bounds)) c->Submit(patch); } } } m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling); } void CGameView::UnloadResources() { g_TexMan.UnloadTerrainTextures(); g_Renderer.GetSceneRenderer().GetWaterManager().UnloadWaterTextures(); } void CGameView::Update(const float deltaRealTime) { m->MiniMapTexture.Update(deltaRealTime); // If camera movement is being handled by the touch-input system, // then we should stop to avoid conflicting with it if (g_TouchInput.IsEnabled()) return; if (!g_app_has_focus) return; m->CinemaManager.Update(deltaRealTime); if (m->CinemaManager.IsEnabled()) return; m->CameraController->Update(deltaRealTime); } CVector3D CGameView::GetCameraPivot() const { return m->CameraController->GetCameraPivot(); } CVector3D CGameView::GetCameraPosition() const { return m->CameraController->GetCameraPosition(); } CVector3D CGameView::GetCameraRotation() const { return m->CameraController->GetCameraRotation(); } float CGameView::GetCameraZoom() const { return m->CameraController->GetCameraZoom(); } void CGameView::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom) { m->CameraController->SetCamera(pos, rotX, rotY, zoom); } void CGameView::MoveCameraTarget(const CVector3D& target) { m->CameraController->MoveCameraTarget(target); } void CGameView::ResetCameraTarget(const CVector3D& target) { m->CameraController->ResetCameraTarget(target); } void CGameView::FollowEntity(entity_id_t entity, bool firstPerson) { m->CameraController->FollowEntity(entity, firstPerson); } entity_id_t CGameView::GetFollowedEntity() { return m->CameraController->GetFollowedEntity(); } InReaction game_view_handler(const SDL_Event_* ev) { // put any events that must be processed even if inactive here if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsEnabled()) return IN_PASS; CGameView *pView=g_Game->GetView(); return pView->HandleEvent(ev); } InReaction CGameView::HandleEvent(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_HOTKEYPRESS: { std::string hotkey = static_cast(ev->ev.user.data1); CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer(); if (hotkey == "wireframe") { if (g_XmppClient && g_rankedGame == true) break; else if (sceneRenderer.GetModelRenderMode() == SOLID) { sceneRenderer.SetTerrainRenderMode(EDGED_FACES); sceneRenderer.SetWaterRenderMode(EDGED_FACES); sceneRenderer.SetModelRenderMode(EDGED_FACES); sceneRenderer.SetOverlayRenderMode(EDGED_FACES); } else if (sceneRenderer.GetModelRenderMode() == EDGED_FACES) { sceneRenderer.SetTerrainRenderMode(WIREFRAME); sceneRenderer.SetWaterRenderMode(WIREFRAME); sceneRenderer.SetModelRenderMode(WIREFRAME); sceneRenderer.SetOverlayRenderMode(WIREFRAME); } else { sceneRenderer.SetTerrainRenderMode(SOLID); sceneRenderer.SetWaterRenderMode(SOLID); sceneRenderer.SetModelRenderMode(SOLID); sceneRenderer.SetOverlayRenderMode(SOLID); } return IN_HANDLED; } } } return m->CameraController->HandleEvent(ev); } Index: ps/trunk/source/graphics/HFTracer.cpp =================================================================== --- ps/trunk/source/graphics/HFTracer.cpp (revision 27860) +++ ps/trunk/source/graphics/HFTracer.cpp (revision 27861) @@ -1,360 +1,360 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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 . */ /* * Determine intersection of rays with a heightfield. */ #include "precompiled.h" #include "HFTracer.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "maths/BoundingBoxAligned.h" #include "maths/MathUtil.h" #include "maths/Vector3D.h" #include // To cope well with points that are slightly off the edge of the map, // we act as if there's an N-tile margin around the edges of the heightfield. // (N shouldn't be too huge else it'll hurt performance a little when // RayIntersect loops through it all.) // CTerrain::CalcPosition implements clamp-to-edge behaviour so the tracer // will have that behaviour. static const int MARGIN_SIZE = 64; /////////////////////////////////////////////////////////////////////////////// // CHFTracer constructor -CHFTracer::CHFTracer(CTerrain *pTerrain): - m_pTerrain(pTerrain), - m_Heightfield(m_pTerrain->GetHeightMap()), - m_MapSize(m_pTerrain->GetVerticesPerSide()), +CHFTracer::CHFTracer(CTerrain& terrain): + m_Terrain(terrain), + m_Heightfield(m_Terrain.GetHeightMap()), + m_MapSize(m_Terrain.GetVerticesPerSide()), m_CellSize((float)TERRAIN_TILE_SIZE), m_HeightScale(HEIGHT_SCALE) { } /////////////////////////////////////////////////////////////////////////////// // RayTriIntersect: intersect a ray with triangle defined by vertices // v0,v1,v2; return true if ray hits triangle at distance less than dist, // or false otherwise static bool RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2, const CVector3D& origin, const CVector3D& dir, float& dist) { const float EPSILON=0.00001f; // calculate edge vectors CVector3D edge0=v1-v0; CVector3D edge1=v2-v0; // begin calculating determinant - also used to calculate U parameter CVector3D pvec=dir.Cross(edge1); // if determinant is near zero, ray lies in plane of triangle float det = edge0.Dot(pvec); if (fabs(det)1.01f) return false; // prepare to test V parameter CVector3D qvec=tvec.Cross(edge0); // calculate V parameter and test bounds float v=dir.Dot(qvec)*inv_det; if (v<0.0f || u+v>1.0f) return false; // calculate distance to intersection point from ray origin float d=edge1.Dot(qvec)*inv_det; if (d>=0 && dCalcPosition(cx,cz,vpos[0]); - m_pTerrain->CalcPosition(cx+1,cz,vpos[1]); - m_pTerrain->CalcPosition(cx+1,cz+1,vpos[2]); - m_pTerrain->CalcPosition(cx,cz+1,vpos[3]); + m_Terrain.CalcPosition(cx,cz,vpos[0]); + m_Terrain.CalcPosition(cx+1,cz,vpos[1]); + m_Terrain.CalcPosition(cx+1,cz+1,vpos[2]); + m_Terrain.CalcPosition(cx,cz+1,vpos[3]); dist=1.0e30f; if (RayTriIntersect(vpos[0],vpos[1],vpos[2],origin,dir,dist)) { res=true; } if (RayTriIntersect(vpos[0],vpos[2],vpos[3],origin,dir,dist)) { res=true; } return res; } /////////////////////////////////////////////////////////////////////////////// // RayIntersect: intersect ray with this heightfield; return true if // intersection occurs (and fill in grid coordinates of intersection), or false // otherwise bool CHFTracer::RayIntersect(const CVector3D& origin, const CVector3D& dir, int& x, int& z, CVector3D& ipt) const { // If the map is empty (which should never happen), // return early before we crash when reading zero-sized heightmaps if (!m_MapSize) { debug_warn(L"CHFTracer::RayIntersect called with zero-size map"); return false; } // intersect first against bounding box CBoundingBoxAligned bound; bound[0] = CVector3D(-MARGIN_SIZE * m_CellSize, 0, -MARGIN_SIZE * m_CellSize); bound[1] = CVector3D((m_MapSize + MARGIN_SIZE) * m_CellSize, 65535 * m_HeightScale, (m_MapSize + MARGIN_SIZE) * m_CellSize); float tmin,tmax; if (!bound.RayIntersect(origin,dir,tmin,tmax)) { // ray missed world bounds; no intersection return false; } // project origin onto grid, if necessary, to get starting point for traversal CVector3D traversalPt; if (tmin>0) { traversalPt=origin+dir*tmin; } else { traversalPt=origin; } // setup traversal variables int sx=dir.X<0 ? -1 : 1; int sz=dir.Z<0 ? -1 : 1; float invCellSize=1.0f/float(m_CellSize); float fcx=traversalPt.X*invCellSize; int cx=(int)floor(fcx); float fcz=traversalPt.Z*invCellSize; int cz=(int)floor(fcz); float invdx = 1.0e20f; float invdz = 1.0e20f; if (fabs(dir.X) > 1.0e-20) invdx = float(1.0/fabs(dir.X)); if (fabs(dir.Z) > 1.0e-20) invdz = float(1.0/fabs(dir.Z)); do { // test current cell if (cx >= -MARGIN_SIZE && cx < int(m_MapSize + MARGIN_SIZE - 1) && cz >= -MARGIN_SIZE && cz < int(m_MapSize + MARGIN_SIZE - 1)) { float dist; if (CellIntersect(cx,cz,origin,dir,dist)) { x=cx; z=cz; ipt=origin+dir*dist; return true; } } else { // Degenerate case: y close to zero // catch travelling off the map if ((cx < -MARGIN_SIZE) && (sx < 0)) return false; if ((cx >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sx > 0)) return false; if ((cz < -MARGIN_SIZE) && (sz < 0)) return false; if ((cz >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sz > 0)) return false; } // get coords of current cell fcx=traversalPt.X*invCellSize; fcz=traversalPt.Z*invCellSize; // get distance to next cell in x,z float dx=(sx==-1) ? fcx-float(cx) : 1-(fcx-float(cx)); dx*=invdx; float dz=(sz==-1) ? fcz-float(cz) : 1-(fcz-float(cz)); dz*=invdz; // advance .. float dist; if (dx=0); // fell off end of heightmap with no intersection; return a miss return false; } static bool TestTile(u16* heightmap, int stride, int i, int j, const CVector3D& pos, const CVector3D& dir, CVector3D& isct) { u16 y00 = heightmap[i + j*stride]; u16 y10 = heightmap[i+1 + j*stride]; u16 y01 = heightmap[i + (j+1)*stride]; u16 y11 = heightmap[i+1 + (j+1)*stride]; CVector3D p00( i * TERRAIN_TILE_SIZE, y00 * HEIGHT_SCALE, j * TERRAIN_TILE_SIZE); CVector3D p10((i+1) * TERRAIN_TILE_SIZE, y10 * HEIGHT_SCALE, j * TERRAIN_TILE_SIZE); CVector3D p01( i * TERRAIN_TILE_SIZE, y01 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE); CVector3D p11((i+1) * TERRAIN_TILE_SIZE, y11 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE); int mid1 = y00+y11; int mid2 = y01+y10; int triDir = (mid1 < mid2); float dist = FLT_MAX; if (triDir) { if (RayTriIntersect(p00, p10, p01, pos, dir, dist) || // lower-left triangle RayTriIntersect(p11, p01, p10, pos, dir, dist)) // upper-right triangle { isct = pos + dir * dist; return true; } } else { if (RayTriIntersect(p00, p11, p01, pos, dir, dist) || // upper-left triangle RayTriIntersect(p00, p10, p11, pos, dir, dist)) // lower-right triangle { isct = pos + dir * dist; return true; } } return false; } bool CHFTracer::PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out) { // (TODO: This largely duplicates RayIntersect - some refactoring might be // nice in the future.) // General approach: // Given the ray defined by origin + dir * t, we increase t until it // enters the patch's bounding box. The x,z coordinates identify which // tile it is currently above/below. Do an intersection test vs the tile's // two triangles. If it doesn't hit, do a 2D line rasterisation to find // the next tiles the ray will pass through, and test each of them. // Start by jumping to the point where the ray enters the bounding box CBoundingBoxAligned bound = patch->GetWorldBounds(); float tmin, tmax; if (!bound.RayIntersect(origin, dir, tmin, tmax)) { // Ray missed patch; no intersection return false; } int heightmapStride = patch->m_Parent->GetVerticesPerSide(); // Get heightmap, offset to start at this patch u16* heightmap = patch->m_Parent->GetHeightMap() + patch->m_X * PATCH_SIZE + patch->m_Z * PATCH_SIZE * heightmapStride; // Get patch-space position of ray origin and bbox entry point CVector3D patchPos( patch->m_X * PATCH_SIZE * TERRAIN_TILE_SIZE, 0.0f, patch->m_Z * PATCH_SIZE * TERRAIN_TILE_SIZE); CVector3D originPatch = origin - patchPos; CVector3D entryPatch = originPatch + dir * tmin; // We want to do a simple 2D line rasterisation (with the 3D ray projected // down onto the Y plane). That will tell us which cells are intersected // in 2D dimensions, then we can do a more precise 3D intersection test. // // WLOG, assume the ray has direction dir.x > 0, dir.z > 0, and starts in // cell (i,j). The next cell intersecting the line must be either (i+1,j) // or (i,j+1). To tell which, just check whether the point (i+1,j+1) is // above or below the ray. Advance into that cell and repeat. // // (If the ray passes precisely through (i+1,j+1), we can pick either. // If the ray is parallel to Y, only the first cell matters, then we can // carry on rasterising in any direction (a bit of a waste of time but // should be extremely rare, and it's safe and simple).) // Work out which tile we're starting in int i = Clamp(entryPatch.X / TERRAIN_TILE_SIZE, 0, PATCH_SIZE - 1); int j = Clamp(entryPatch.Z / TERRAIN_TILE_SIZE, 0, PATCH_SIZE - 1); // Work out which direction the ray is going in int di = (dir.X >= 0 ? 1 : 0); int dj = (dir.Z >= 0 ? 1 : 0); do { CVector3D isct; if (TestTile(heightmap, heightmapStride, i, j, originPatch, dir, isct)) { if (out) *out = isct + patchPos; return true; } // Get the vertex between the two possible next cells float nx = (i + di) * (int)TERRAIN_TILE_SIZE; float nz = (j + dj) * (int)TERRAIN_TILE_SIZE; // Test which side of the ray the vertex is on, and advance into the // appropriate cell, using a test that works for all 4 combinations // of di,dj float dot = dir.Z * (nx - originPatch.X) - dir.X * (nz - originPatch.Z); if ((di == dj) == (dot > 0.0f)) j += dj*2-1; else i += di*2-1; } while (i >= 0 && j >= 0 && i < PATCH_SIZE && j < PATCH_SIZE); // Ran off the edge of the patch, so no intersection return false; } Index: ps/trunk/source/graphics/HFTracer.h =================================================================== --- ps/trunk/source/graphics/HFTracer.h (revision 27860) +++ ps/trunk/source/graphics/HFTracer.h (revision 27861) @@ -1,74 +1,74 @@ -/* Copyright (C) 2014 Wildfire Games. +/* Copyright (C) 2023 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 . */ /* * Determine intersection of rays with a heightfield. */ #ifndef INCLUDED_HFTRACER #define INCLUDED_HFTRACER class CPatch; class CVector3D; class CTerrain; /////////////////////////////////////////////////////////////////////////////// // CHFTracer: a class for determining ray intersections with a heightfield class CHFTracer { public: // constructor; setup data - CHFTracer(CTerrain *pTerrain); + CHFTracer(CTerrain& terrain); // intersect ray with this heightfield; return true if intersection // occurs (and fill in grid coordinates and point of intersection), or false otherwise bool RayIntersect(const CVector3D& origin, const CVector3D& dir, int& x, int& z, CVector3D& ipt) const; /** * Intersects ray with a single patch. * The ray is a half-infinite line starting at @p origin with direction @p dir * (not required to be a unit vector).. The patch is treated as a collection * of two-sided triangles, corresponding to the terrain tiles. * * If there is an intersection, returns true; and if @p out is not NULL, it * is set to the intersection point. This is guaranteed to be the earliest * tile intersected (starting at @p origin), but not necessarily the earlier * triangle inside that tile. * * This partly duplicates RayIntersect, but it only operates on a single * patch, and it's more precise (it uses the same tile triangulation as the * renderer), and tries to be more numerically robust. */ static bool PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out); private: // test if ray intersects either of the triangles in the given bool CellIntersect(int cx, int cz, const CVector3D& origin, const CVector3D& dir, float& dist) const; // The terrain we're operating on - CTerrain *m_pTerrain; + CTerrain& m_Terrain; // the heightfield were tracing const u16* m_Heightfield; // size of the heightfield size_t m_MapSize; // cell size - size of each cell in x and z float m_CellSize; // vertical scale - size of each cell in y float m_HeightScale; }; #endif Index: ps/trunk/source/graphics/MiniMapTexture.cpp =================================================================== --- ps/trunk/source/graphics/MiniMapTexture.cpp (revision 27860) +++ ps/trunk/source/graphics/MiniMapTexture.cpp (revision 27861) @@ -1,878 +1,877 @@ /* Copyright (C) 2023 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 "MiniMapTexture.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniPatch.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgramPtr.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/TextureManager.h" #include "lib/bits.h" #include "lib/code_generation.h" #include "lib/hash.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Vector2D.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/system/ParamNode.h" #include #include #include namespace { // Set max drawn entities to 64K / 4 for now, which is more than enough. // 4 is the number of vertices per entity. // TODO: we should be cleverer about drawing them to reduce clutter, // f.e. use instancing. constexpr size_t MAX_ENTITIES_DRAWN = 65536 / 4; constexpr size_t MAX_ICON_COUNT = 256; constexpr size_t MAX_UNIQUE_ICON_COUNT = 64; constexpr size_t ICON_COMBINING_GRID_SIZE = 10; constexpr size_t FINAL_TEXTURE_SIZE = 512; unsigned int ScaleColor(unsigned int color, float x) { unsigned int r = unsigned(float(color & 0xff) * x); unsigned int g = unsigned(float((color >> 8) & 0xff) * x); unsigned int b = unsigned(float((color >> 16) & 0xff) * x); return (0xff000000 | b | g << 8 | r << 16); } void DrawTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IVertexInputLayout* quadVertexInputLayout) { const float quadUVs[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f }; const float quadVertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, }; deviceCommandContext->SetVertexInputLayout(quadVertexInputLayout); deviceCommandContext->SetVertexBufferData( 0, quadVertices, std::size(quadVertices) * sizeof(quadVertices[0])); deviceCommandContext->SetVertexBufferData( 1, quadUVs, std::size(quadUVs) * sizeof(quadUVs[0])); deviceCommandContext->Draw(0, 6); } struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. u8 r, g, b, a; CVector2D position; }; // Adds a vertex to the passed VertexArray inline void AddEntity(const MinimapUnitVertex& v, VertexArrayIterator& attrColor, VertexArrayIterator& attrPos, const float entityRadius, const bool useInstancing) { if (useInstancing) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.position.X; (*attrPos)[1] = v.position.Y; ++attrPos; return; } const CVector2D offsets[4] = { {-entityRadius, 0.0f}, {0.0f, -entityRadius}, {entityRadius, 0.0f}, {0.0f, entityRadius} }; for (const CVector2D& offset : offsets) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.position.X + offset.X; (*attrPos)[1] = v.position.Y + offset.Y; ++attrPos; } } } // anonymous namespace size_t CMiniMapTexture::CellIconKeyHash::operator()( const CellIconKey& key) const { size_t seed = 0; hash_combine(seed, key.path); hash_combine(seed, key.r); hash_combine(seed, key.g); hash_combine(seed, key.b); return seed; } bool CMiniMapTexture::CellIconKeyEqual::operator()( const CellIconKey& lhs, const CellIconKey& rhs) const { return lhs.path == rhs.path && lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b; } CMiniMapTexture::CMiniMapTexture(Renderer::Backend::IDevice* device, CSimulation2& simulation) : m_Simulation(simulation), m_IndexArray(false), m_VertexArray(Renderer::Backend::IBuffer::Type::VERTEX, true), m_InstanceVertexArray(Renderer::Backend::IBuffer::Type::VERTEX, false) { // Register Relax NG validator. CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); m_ShallowPassageHeight = GetShallowPassageHeight(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); } m_HalfBlinkDuration = blinkDuration / 2.0; m_AttributePos.format = Renderer::Backend::Format::R32G32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumberOfVertices(MAX_ENTITIES_DRAWN * 4); m_VertexArray.Layout(); m_IndexArray.SetNumberOfVertices(MAX_ENTITIES_DRAWN * 6); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (size_t i = 0; i < m_IndexArray.GetNumberOfVertices(); ++i) *index++ = 0; m_IndexArray.Upload(); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); for (size_t i = 0; i < m_VertexArray.GetNumberOfVertices(); ++i) { (*attrColor)[0] = 0; (*attrColor)[1] = 0; (*attrColor)[2] = 0; (*attrColor)[3] = 0; ++attrColor; (*attrPos)[0] = -10000.0f; (*attrPos)[1] = -10000.0f; ++attrPos; } m_VertexArray.Upload(); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1} }}; m_QuadVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); m_Flipped = device->GetBackend() == Renderer::Backend::Backend::VULKAN; const uint32_t stride = m_VertexArray.GetStride(); if (device->GetCapabilities().instancing) { m_UseInstancing = true; const size_t numberOfCircleSegments = 8; m_InstanceAttributePosition.format = Renderer::Backend::Format::R32G32_SFLOAT; m_InstanceVertexArray.AddAttribute(&m_InstanceAttributePosition); m_InstanceVertexArray.SetNumberOfVertices(numberOfCircleSegments * 3); m_InstanceVertexArray.Layout(); VertexArrayIterator attributePosition = m_InstanceAttributePosition.GetIterator(); for (size_t segment = 0; segment < numberOfCircleSegments; ++segment) { const float currentAngle = static_cast(segment) / numberOfCircleSegments * 2.0f * M_PI; const float nextAngle = static_cast(segment + 1) / numberOfCircleSegments * 2.0f * M_PI; (*attributePosition)[0] = 0.0f; (*attributePosition)[1] = 0.0f; ++attributePosition; (*attributePosition)[0] = std::cos(currentAngle); (*attributePosition)[1] = std::sin(currentAngle); ++attributePosition; (*attributePosition)[0] = std::cos(nextAngle); (*attributePosition)[1] = std::sin(nextAngle); ++attributePosition; } m_InstanceVertexArray.Upload(); m_InstanceVertexArray.FreeBackingStore(); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, m_InstanceAttributePosition.format, m_InstanceAttributePosition.offset, m_InstanceVertexArray.GetStride(), Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, m_AttributePos.format, m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1}, {Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1}, }}; m_EntitiesVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } else { const std::array entitiesAttributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; m_EntitiesVertexInputLayout = g_Renderer.GetVertexInputLayout(entitiesAttributes); } CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); m_TerritoryTechnique = g_Renderer.GetShaderManager().LoadEffect( str_minimap, baseDefines, [](Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc) { pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; pipelineStateDesc.blendState.colorWriteMask = Renderer::Backend::ColorWriteMask::RED | Renderer::Backend::ColorWriteMask::GREEN | Renderer::Backend::ColorWriteMask::BLUE; }); } CMiniMapTexture::~CMiniMapTexture() { DestroyTextures(); } void CMiniMapTexture::Update(const float UNUSED(deltaRealTime)) { if (m_WaterHeight != g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight) { m_TerrainTextureDirty = true; m_FinalTextureDirty = true; } } void CMiniMapTexture::Render( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CLOSTexture& losTexture, CTerritoryTexture& territoryTexture) { - const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - if (!terrain) - return; - + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); if (!m_TerrainTexture) CreateTextures(deviceCommandContext, terrain); if (m_TerrainTextureDirty) RebuildTerrainTexture(deviceCommandContext, terrain); RenderFinalTexture(deviceCommandContext, losTexture, territoryTexture); } void CMiniMapTexture::CreateTextures( - Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CTerrain* terrain) + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CTerrain& terrain) { DestroyTextures(); - m_MapSize = terrain->GetVerticesPerSide(); + m_MapSize = terrain.GetVerticesPerSide(); const size_t textureSize = round_up_to_pow2(static_cast(m_MapSize)); const Renderer::Backend::Sampler::Desc defaultSamplerDesc = Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); Renderer::Backend::IDevice* backendDevice = deviceCommandContext->GetDevice(); // Create terrain texture m_TerrainTexture = backendDevice->CreateTexture2D("MiniMapTerrainTexture", Renderer::Backend::ITexture::Usage::TRANSFER_DST | Renderer::Backend::ITexture::Usage::SAMPLED, Renderer::Backend::Format::R8G8B8A8_UNORM, textureSize, textureSize, defaultSamplerDesc); // Initialise texture with solid black, for the areas we don't // overwrite with uploading later. std::unique_ptr texData = std::make_unique(textureSize * textureSize); for (size_t i = 0; i < textureSize * textureSize; ++i) texData[i] = 0xFF000000; deviceCommandContext->UploadTexture( m_TerrainTexture.get(), Renderer::Backend::Format::R8G8B8A8_UNORM, texData.get(), textureSize * textureSize * 4); texData.reset(); m_TerrainData = std::make_unique((m_MapSize - 1) * (m_MapSize - 1)); m_FinalTexture = g_Renderer.GetTextureManager().WrapBackendTexture( backendDevice->CreateTexture2D("MiniMapFinalTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, FINAL_TEXTURE_SIZE, FINAL_TEXTURE_SIZE, defaultSamplerDesc)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_FinalTexture->GetBackendTexture(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::DONT_CARE; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; m_FinalTextureFramebuffer = backendDevice->CreateFramebuffer( "MiniMapFinalFramebuffer", &colorAttachment, nullptr); ENSURE(m_FinalTextureFramebuffer); } void CMiniMapTexture::DestroyTextures() { m_TerrainTexture.reset(); m_FinalTexture.reset(); m_TerrainData.reset(); } void CMiniMapTexture::RebuildTerrainTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, - const CTerrain* terrain) + const CTerrain& terrain) { const u32 x = 0; const u32 y = 0; const u32 width = m_MapSize - 1; const u32 height = m_MapSize - 1; m_WaterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight; m_TerrainTextureDirty = false; for (u32 j = 0; j < height; ++j) { u32* dataPtr = m_TerrainData.get() + ((y + j) * width) + x; for (u32 i = 0; i < width; ++i) { - const float avgHeight = ( terrain->GetVertexGroundLevel((int)i, (int)j) - + terrain->GetVertexGroundLevel((int)i+1, (int)j) - + terrain->GetVertexGroundLevel((int)i, (int)j+1) - + terrain->GetVertexGroundLevel((int)i+1, (int)j+1) + const float avgHeight = ( + terrain.GetVertexGroundLevel(static_cast(i), static_cast(j)) + + terrain.GetVertexGroundLevel(static_cast(i+1), static_cast(j)) + + terrain.GetVertexGroundLevel(static_cast(i), static_cast(j+1)) + + terrain.GetVertexGroundLevel(static_cast(i+1), static_cast(j+1)) ) / 4.0f; if (avgHeight < m_WaterHeight && avgHeight > m_WaterHeight - m_ShallowPassageHeight) { // shallow water *dataPtr++ = 0xffc09870; } else if (avgHeight < m_WaterHeight) { // Set water as constant color for consistency on different maps *dataPtr++ = 0xffa07850; } else { - int hmap = ((int)terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; + const int hmap = + static_cast(terrain.GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; int val = (hmap / 3) + 170; u32 color = 0xFFFFFFFF; - CMiniPatch* mp = terrain->GetTile(x + i, y + j); + CMiniPatch* const mp = terrain.GetTile(x + i, y + j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) { // If the texture can't be loaded yet, set the dirty flags // so we'll try regenerating the terrain texture again soon if (!tex->GetTexture()->TryLoad()) m_TerrainTextureDirty = true; color = tex->GetBaseColor(); } } *dataPtr++ = ScaleColor(color, float(val) / 255.0f); } } } // Upload the texture deviceCommandContext->UploadTextureRegion( m_TerrainTexture.get(), Renderer::Backend::Format::R8G8B8A8_UNORM, m_TerrainData.get(), width * height * 4, 0, 0, width, height); } void CMiniMapTexture::RenderFinalTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CLOSTexture& losTexture, CTerritoryTexture& territoryTexture) { // only update 2x / second // (note: since units only move a few pixels per second on the minimap, // we can get away with infrequent updates; this is slow) // TODO: Update all but camera at same speed as simulation const double currentTime = timer_Time(); const bool doUpdate = (currentTime - m_LastFinalTextureUpdate > 0.5) || m_FinalTextureDirty; if (!doUpdate) return; m_LastFinalTextureUpdate = currentTime; m_FinalTextureDirty = false; // We might scale entities properly in the vertex shader but it requires // additional space in the vertex buffer. So we assume that we don't need // to change an entity size so often. // Radius with instancing is lower because an entity has a more round shape. const float entityRadius = static_cast(m_MapSize) / 128.0f * (m_UseInstancing ? 5.0 : 6.0f); UpdateAndUploadEntities(deviceCommandContext, entityRadius, currentTime); PROFILE3("Render minimap texture"); GPU_SCOPED_LABEL(deviceCommandContext, "Render minimap texture"); deviceCommandContext->BeginFramebufferPass(m_FinalTextureFramebuffer.get()); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = FINAL_TEXTURE_SIZE; viewportRect.height = FINAL_TEXTURE_SIZE; deviceCommandContext->SetViewports(1, &viewportRect); const float texCoordMax = m_TerrainTexture ? static_cast(m_MapSize - 1) / m_TerrainTexture->GetWidth() : 1.0f; Renderer::Backend::IShaderProgram* shader = nullptr; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, baseDefines); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); shader = tech->GetShader(); if (m_TerrainTexture) { deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), m_TerrainTexture.get()); } CMatrix3D baseTransform; baseTransform.SetIdentity(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); CMatrix3D terrainTransform; terrainTransform.SetIdentity(); terrainTransform.Scale(texCoordMax, texCoordMax, 1.0f); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), terrainTransform._11, terrainTransform._21, terrainTransform._12, terrainTransform._22); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), baseTransform._14, baseTransform._24, terrainTransform._14, terrainTransform._24); if (m_TerrainTexture) DrawTexture(deviceCommandContext, m_QuadVertexInputLayout); deviceCommandContext->EndPass(); deviceCommandContext->SetGraphicsPipelineState( m_TerritoryTechnique->GetGraphicsPipelineState()); shader = m_TerritoryTechnique->GetShader(); deviceCommandContext->BeginPass(); // Draw territory boundaries deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), territoryTexture.GetTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22); const CMatrix3D& territoryTransform = territoryTexture.GetMinimapTextureMatrix(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), territoryTransform._11, territoryTransform._21, territoryTransform._12, territoryTransform._22); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), baseTransform._14, baseTransform._24, territoryTransform._14, territoryTransform._24); DrawTexture(deviceCommandContext, m_QuadVertexInputLayout); deviceCommandContext->EndPass(); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap_los, CShaderDefines()); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), losTexture.GetTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22); const CMatrix3D& losTransform = losTexture.GetMinimapTextureMatrix(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), losTransform._11, losTransform._21, losTransform._12, losTransform._22); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), baseTransform._14, baseTransform._24, losTransform._14, losTransform._24); DrawTexture(deviceCommandContext, m_QuadVertexInputLayout); deviceCommandContext->EndPass(); if (m_EntitiesDrawn > 0) DrawEntities(deviceCommandContext, entityRadius); deviceCommandContext->EndFramebufferPass(); } void CMiniMapTexture::UpdateAndUploadEntities( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const float entityRadius, const double& currentTime) { const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); m_Icons.clear(); m_IconsCache.clear(); CSimulation2::InterfaceList ents = m_Simulation.GetEntitiesWithInterface(IID_Minimap); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); m_EntitiesDrawn = 0; MinimapUnitVertex v; std::vector pingingVertices; pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2); CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); if (currentTime > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = currentTime + m_HalfBlinkDuration; } bool iconsEnabled = false; CFG_GET_VAL("gui.session.minimap.icons.enabled", iconsEnabled); float iconsOpacity = 1.0f; CFG_GET_VAL("gui.session.minimap.icons.opacity", iconsOpacity); float iconsSizeScale = 1.0f; CFG_GET_VAL("gui.session.minimap.icons.sizescale", iconsSizeScale); bool iconsCountOverflow = false; entity_pos_t posX, posZ; for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) { ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, m_Simulation.GetSimContext().GetCurrentDisplayedPlayer()); if (vis != LosVisibility::HIDDEN) { v.a = 255; v.position.X = posX.ToFloat(); v.position.Y = posZ.ToFloat(); // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(currentTime, m_PingDuration)) { v.r = 255; // ping color is white v.g = 255; v.b = 255; pingingVertices.push_back(v); } else if (m_EntitiesDrawn < MAX_ENTITIES_DRAWN) { AddEntity(v, attrColor, attrPos, entityRadius, m_UseInstancing); ++m_EntitiesDrawn; } if (!iconsEnabled || !cmpMinimap->HasIcon()) continue; const CellIconKey key{ cmpMinimap->GetIconPath(), v.r, v.g, v.b}; const u16 gridX = Clamp( (v.position.X * invTileMapSize) * ICON_COMBINING_GRID_SIZE, 0, ICON_COMBINING_GRID_SIZE - 1); const u16 gridY = Clamp( (v.position.Y * invTileMapSize) * ICON_COMBINING_GRID_SIZE, 0, ICON_COMBINING_GRID_SIZE - 1); CellIcon icon{ gridX, gridY, cmpMinimap->GetIconSize() * iconsSizeScale * 0.5f, v.position}; if (m_IconsCache.find(key) == m_IconsCache.end() && m_IconsCache.size() >= MAX_UNIQUE_ICON_COUNT) { iconsCountOverflow = true; } else { m_IconsCache[key].emplace_back(std::move(icon)); } } } } // We need to combine too close icons with the same path, we use a grid for // that. But to save some allocations and space we store only the current // row. struct Cell { u32 count; float maxHalfSize; CVector2D averagePosition; }; std::array gridRow; for (auto& [key, icons] : m_IconsCache) { CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture( CTextureProperties(key.path)); const CColor color(key.r / 255.0f, key.g / 255.0f, key.b / 255.0f, iconsOpacity); std::sort(icons.begin(), icons.end(), [](const CellIcon& lhs, const CellIcon& rhs) -> bool { if (lhs.gridY != rhs.gridY) return lhs.gridY < rhs.gridY; return lhs.gridX < rhs.gridX; }); for (auto beginIt = icons.begin(); beginIt != icons.end();) { auto endIt = std::next(beginIt); while (endIt != icons.end() && beginIt->gridY == endIt->gridY) ++endIt; gridRow.fill({0, 0.0f, {}}); for (; beginIt != endIt; ++beginIt) { Cell& cell = gridRow[beginIt->gridX]; const float previousPositionWeight = static_cast(cell.count) / (cell.count + 1); cell.averagePosition = cell.averagePosition * previousPositionWeight + beginIt->worldPosition / static_cast(cell.count + 1); cell.maxHalfSize = std::max(cell.maxHalfSize, beginIt->halfSize); ++cell.count; } for (const Cell& cell : gridRow) { if (cell.count == 0) continue; if (m_Icons.size() < MAX_ICON_COUNT) { m_Icons.emplace_back(Icon{ texture, color, cell.averagePosition, cell.maxHalfSize}); } else iconsCountOverflow = true; } } } if (iconsCountOverflow) LOGWARNING("Too many minimap icons to draw."); // Add the pinged vertices at the end, so they are drawn on top for (const MinimapUnitVertex& vertex : pingingVertices) { AddEntity(vertex, attrColor, attrPos, entityRadius, m_UseInstancing); ++m_EntitiesDrawn; if (m_EntitiesDrawn == MAX_ENTITIES_DRAWN) break; } if (m_EntitiesDrawn == MAX_ENTITIES_DRAWN) ONCE(LOGERROR("Too many entities, some of them will be hidden on the minimap.")); if (!m_UseInstancing) { VertexArrayIterator index = m_IndexArray.GetIterator(); for (size_t entityIndex = 0; entityIndex < m_EntitiesDrawn; ++entityIndex) { index[entityIndex * 6 + 0] = static_cast(entityIndex * 4 + 0); index[entityIndex * 6 + 1] = static_cast(entityIndex * 4 + 1); index[entityIndex * 6 + 2] = static_cast(entityIndex * 4 + 2); index[entityIndex * 6 + 3] = static_cast(entityIndex * 4 + 0); index[entityIndex * 6 + 4] = static_cast(entityIndex * 4 + 2); index[entityIndex * 6 + 5] = static_cast(entityIndex * 4 + 3); } m_IndexArray.Upload(); } m_VertexArray.Upload(); m_VertexArray.PrepareForRendering(); m_VertexArray.UploadIfNeeded(deviceCommandContext); if (!m_UseInstancing) m_IndexArray.UploadIfNeeded(deviceCommandContext); } void CMiniMapTexture::DrawEntities( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const float entityRadius) { const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); if (m_UseInstancing) pointDefines.Add(str_USE_GPU_INSTANCING, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, pointDefines); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Convert world space coordinates into [0, 2]. const float unitScale = invTileMapSize; unitMatrix.Scale(unitScale * 2.0f, unitScale * 2.0f, 1.0f); // Offset the coordinates to [-1, 1]. unitMatrix.Translate(CVector3D(-1.0f, -1.0f, 0.0f)); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), unitMatrix._11, unitMatrix._21, unitMatrix._12, unitMatrix._22); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), unitMatrix._14, unitMatrix._24, 0.0f, 0.0f); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = scissorRect.y = 1; scissorRect.width = scissorRect.height = FINAL_TEXTURE_SIZE - 2; deviceCommandContext->SetScissors(1, &scissorRect); const uint32_t stride = m_VertexArray.GetStride(); const uint32_t firstVertexOffset = m_VertexArray.GetOffset() * stride; deviceCommandContext->SetVertexInputLayout(m_EntitiesVertexInputLayout); if (m_UseInstancing) { deviceCommandContext->SetVertexBuffer( 0, m_InstanceVertexArray.GetBuffer(), m_InstanceVertexArray.GetOffset()); deviceCommandContext->SetVertexBuffer( 1, m_VertexArray.GetBuffer(), firstVertexOffset); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_width), entityRadius); deviceCommandContext->DrawInstanced(0, m_InstanceVertexArray.GetNumberOfVertices(), 0, m_EntitiesDrawn); } else { deviceCommandContext->SetVertexBuffer( 0, m_VertexArray.GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_IndexArray.GetBuffer()); deviceCommandContext->DrawIndexed(m_IndexArray.GetOffset(), m_EntitiesDrawn * 6, 0); } g_Renderer.GetStats().m_DrawCalls++; deviceCommandContext->SetScissors(0, nullptr); deviceCommandContext->EndPass(); } // static float CMiniMapTexture::GetShallowPassageHeight() { float shallowPassageHeight = 0.0f; CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses"); if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk()) shallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); return shallowPassageHeight; } Index: ps/trunk/source/graphics/MiniMapTexture.h =================================================================== --- ps/trunk/source/graphics/MiniMapTexture.h (revision 27860) +++ ps/trunk/source/graphics/MiniMapTexture.h (revision 27861) @@ -1,169 +1,169 @@ /* Copyright (C) 2023 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_MINIMAPTEXTURE #define INCLUDED_MINIMAPTEXTURE #include "graphics/Color.h" #include "graphics/ShaderTechniquePtr.h" #include "graphics/Texture.h" #include "maths/Vector2D.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IShaderProgram.h" #include "renderer/backend/ITexture.h" #include "renderer/VertexArray.h" #include #include #include #include class CLOSTexture; class CSimulation2; class CTerrain; class CTerritoryTexture; class CMiniMapTexture { NONCOPYABLE(CMiniMapTexture); public: CMiniMapTexture(Renderer::Backend::IDevice* device, CSimulation2& simulation); ~CMiniMapTexture(); /** * Marks the texture as dirty if it's old enough to redraw it on Render. */ void Update(const float deltaRealTime); /** * Redraws the texture if it's dirty. */ void Render( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CLOSTexture& losTexture, CTerritoryTexture& territoryTexture); const CTexturePtr& GetTexture() const { return m_FinalTexture; } bool IsFlipped() const { return m_Flipped; } /** * @return The maximum height for unit passage in water. */ static float GetShallowPassageHeight(); struct Icon { CTexturePtr texture; CColor color; CVector2D worldPosition; float halfSize; }; // Returns icons for corresponding entities on the minimap texture. const std::vector& GetIcons() { return m_Icons; } private: void CreateTextures( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, - const CTerrain* terrain); + const CTerrain& terrain); void DestroyTextures(); void RebuildTerrainTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, - const CTerrain* terrain); + const CTerrain& terrain); void RenderFinalTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CLOSTexture& losTexture, CTerritoryTexture& territoryTexture); void UpdateAndUploadEntities( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const float entityRadius, const double& currentTime); void DrawEntities( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const float entityRadius); CSimulation2& m_Simulation; bool m_TerrainTextureDirty = true; bool m_FinalTextureDirty = true; double m_LastFinalTextureUpdate = 0.0; bool m_Flipped = false; // minimap texture handles std::unique_ptr m_TerrainTexture; CTexturePtr m_FinalTexture; std::unique_ptr m_FinalTextureFramebuffer; // texture data std::unique_ptr m_TerrainData; // map size ssize_t m_MapSize = 0; // Maximal water height to allow the passage of a unit (for underwater shallows). float m_ShallowPassageHeight = 0.0f; float m_WaterHeight = 0.0f; Renderer::Backend::IVertexInputLayout* m_QuadVertexInputLayout = nullptr; Renderer::Backend::IVertexInputLayout* m_EntitiesVertexInputLayout = nullptr; VertexIndexArray m_IndexArray; VertexArray m_VertexArray; VertexArray::Attribute m_AttributePos; VertexArray::Attribute m_AttributeColor; bool m_UseInstancing = false; // Vertex data if instancing is supported. VertexArray m_InstanceVertexArray; VertexArray::Attribute m_InstanceAttributePosition; CShaderTechniquePtr m_TerritoryTechnique; size_t m_EntitiesDrawn = 0; double m_PingDuration = 25.0; double m_HalfBlinkDuration = 0.0; double m_NextBlinkTime = 0.0; bool m_BlinkState = false; std::vector m_Icons; // We store the map as a member to avoid redundant reallocations on each // update. We use a grid approach to combine icons by distance. struct CellIconKey { std::string path; u8 r, g, b; }; struct CellIconKeyHash { size_t operator()(const CellIconKey& key) const; }; struct CellIconKeyEqual { bool operator()(const CellIconKey& lhs, const CellIconKey& rhs) const; }; struct CellIcon { // TODO: use CVector2DI. u16 gridX, gridY; float halfSize; CVector2D worldPosition; }; std::unordered_map, CellIconKeyHash, CellIconKeyEqual> m_IconsCache; }; #endif // INCLUDED_MINIMAPTEXTURE Index: ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp =================================================================== --- ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp (revision 27860) +++ ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp (revision 27861) @@ -1,211 +1,211 @@ /* Copyright (C) 2023 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 "JSInterface_GameView.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/Terrain.h" #include "maths/FixedVector3D.h" #include "ps/Game.h" #include "ps/World.h" #include "ps/CLogger.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "simulation2/helpers/Position.h" namespace JSI_GameView { #define IMPLEMENT_BOOLEAN_SCRIPT_SETTING(NAME) \ bool Get##NAME##Enabled() \ { \ if (!g_Game || !g_Game->GetView()) \ { \ LOGERROR("Trying to get a setting from GameView when it's not initialized!"); \ return false; \ } \ return g_Game->GetView()->Get##NAME##Enabled(); \ } \ \ void Set##NAME##Enabled(bool Enabled) \ { \ if (!g_Game || !g_Game->GetView()) \ { \ LOGERROR("Trying to set a setting of GameView when it's not initialized!"); \ return; \ } \ g_Game->GetView()->Set##NAME##Enabled(Enabled); \ } IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Culling); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(LockCullCamera); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(ConstrainCamera); #undef IMPLEMENT_BOOLEAN_SCRIPT_SETTING #define REGISTER_BOOLEAN_SCRIPT_SETTING(NAME) \ ScriptFunction::Register<&Get##NAME##Enabled>(rq, "GameView_Get" #NAME "Enabled"); \ ScriptFunction::Register<&Set##NAME##Enabled>(rq, "GameView_Set" #NAME "Enabled"); void RegisterScriptFunctions_Settings(const ScriptRequest& rq) { REGISTER_BOOLEAN_SCRIPT_SETTING(Culling); REGISTER_BOOLEAN_SCRIPT_SETTING(LockCullCamera); REGISTER_BOOLEAN_SCRIPT_SETTING(ConstrainCamera); } #undef REGISTER_BOOLEAN_SCRIPT_SETTING JS::Value GetCameraRotation(const ScriptRequest& rq) { if (!g_Game || !g_Game->GetView()) return JS::UndefinedValue(); const CVector3D rotation = g_Game->GetView()->GetCameraRotation(); JS::RootedValue val(rq.cx); Script::CreateObject(rq, &val, "x", rotation.X, "y", rotation.Y); return val; } JS::Value GetCameraZoom() { if (!g_Game || !g_Game->GetView()) return JS::UndefinedValue(); return JS::NumberValue(g_Game->GetView()->GetCameraZoom()); } JS::Value GetCameraPivot(const ScriptRequest& rq) { if (!g_Game || !g_Game->GetView()) return JS::UndefinedValue(); const CVector3D pivot = g_Game->GetView()->GetCameraPivot(); JS::RootedValue pivotValue(rq.cx); Script::CreateObject(rq, &pivotValue, "x", pivot.X, "z", pivot.Z); return pivotValue; } JS::Value GetCameraPosition(const ScriptRequest& rq) { if (!g_Game || !g_Game->GetView()) return JS::UndefinedValue(); const CVector3D position = g_Game->GetView()->GetCameraPosition(); JS::RootedValue positionValue(rq.cx); Script::CreateObject(rq, &positionValue, "x", position.X, "y", position.Y, "z", position.Z); return positionValue; } /** * Move camera to a 2D location. */ void CameraMoveTo(entity_pos_t x, entity_pos_t z) { - if (!g_Game || !g_Game->GetWorld() || !g_Game->GetView() || !g_Game->GetWorld()->GetTerrain()) + if (!g_Game || !g_Game->GetWorld() || !g_Game->GetView()) return; - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; target.X = x.ToFloat(); target.Z = z.ToFloat(); - target.Y = terrain->GetExactGroundLevel(target.X, target.Z); + target.Y = terrain.GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } /** * Set the camera to look at the given location. */ void SetCameraTarget(float x, float y, float z) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->MoveCameraTarget(CVector3D(x, y, z)); } /** * Set the data (position, orientation and zoom) of the camera. */ void SetCameraData(entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom) { if (!g_Game || !g_Game->GetView()) return; CVector3D pos(x.ToFloat(), y.ToFloat(), z.ToFloat()); g_Game->GetView()->SetCamera(pos, rotx.ToFloat(), roty.ToFloat(), zoom.ToFloat()); } /** * Start / stop camera following mode. * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollow(entity_id_t entityid) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->FollowEntity(entityid, false); } /** * Start / stop first-person camera following mode. * @param entityid unit id to follow. If zero, stop following mode. */ void CameraFollowFPS(entity_id_t entityid) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->FollowEntity(entityid, true); } entity_id_t GetFollowedEntity() { if (!g_Game || !g_Game->GetView()) return INVALID_ENTITY; return g_Game->GetView()->GetFollowedEntity(); } CFixedVector3D GetTerrainAtScreenPoint(int x, int y) { CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true); return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z)); } void RegisterScriptFunctions(const ScriptRequest& rq) { RegisterScriptFunctions_Settings(rq); ScriptFunction::Register<&GetCameraRotation>(rq, "GetCameraRotation"); ScriptFunction::Register<&GetCameraZoom>(rq, "GetCameraZoom"); ScriptFunction::Register<&GetCameraPivot>(rq, "GetCameraPivot"); ScriptFunction::Register<&GetCameraPosition>(rq, "GetCameraPosition"); ScriptFunction::Register<&CameraMoveTo>(rq, "CameraMoveTo"); ScriptFunction::Register<&SetCameraTarget>(rq, "SetCameraTarget"); ScriptFunction::Register<&SetCameraData>(rq, "SetCameraData"); ScriptFunction::Register<&CameraFollow>(rq, "CameraFollow"); ScriptFunction::Register<&CameraFollowFPS>(rq, "CameraFollowFPS"); ScriptFunction::Register<&GetFollowedEntity>(rq, "GetFollowedEntity"); ScriptFunction::Register<&GetTerrainAtScreenPoint>(rq, "GetTerrainAtScreenPoint"); } } Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 27860) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 27861) @@ -1,416 +1,416 @@ /* Copyright (C) 2023 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 "CMiniMap.h" #include "graphics/Canvas2D.h" #include "graphics/GameView.h" #include "graphics/MiniMapTexture.h" #include "graphics/MiniPatch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/system/ParamNode.h" #include #include #include namespace { // Adds segments pieces lying inside the circle to lines. void CropPointsByCircle(const std::array& points, const CVector3D& center, const float radius, std::vector* lines) { constexpr float EPS = 1e-3f; lines->reserve(points.size() * 2); for (size_t idx = 0; idx < points.size(); ++idx) { const CVector3D& currentPoint = points[idx]; const CVector3D& nextPoint = points[(idx + 1) % points.size()]; const CVector3D direction = (nextPoint - currentPoint).Normalized(); const CVector3D normal(direction.Z, 0.0f, -direction.X); const float offset = normal.Dot(currentPoint) - normal.Dot(center); // We need to have lines only inside the circle. if (std::abs(offset) + EPS >= radius) continue; const CVector3D closestPoint = center + normal * offset; const float halfChordLength = sqrt(radius * radius - offset * offset); const CVector3D intersectionA = closestPoint - direction * halfChordLength; const CVector3D intersectionB = closestPoint + direction * halfChordLength; // We have no intersection if the segment is lying outside of the circle. if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) || direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA)) continue; lines->emplace_back( direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA); lines->emplace_back( direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB); } } } // anonymous namespace const CStr CMiniMap::EventNameWorldClick = "WorldClick"; CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_MapSize(0), m_MapScale(1.f), m_Mask(this, "mask", false), m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0), m_FlareInterleave(this, "flare_interleave", false), m_FlareAnimationSpeed(this, "flare_animation_speed", 0.0f), m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0.0f), m_FlareStartFadeSeconds(this, "flare_start_fade_seconds", 0.0f), m_FlareStopFadeSeconds(this, "flare_stop_fade_seconds", 0.0f) { m_Clicking = false; m_MouseHovering = false; } CMiniMap::~CMiniMap() = default; void CMiniMap::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); switch (Message.type) { case GUIM_LOAD: RecreateFlareTextures(); break; case GUIM_SETTINGS_UPDATED: if (Message.value == "flare_texture_count") RecreateFlareTextures(); break; case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1)) { SetCameraPositionFromMousePosition(); m_Clicking = true; } } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_ENTER: m_MouseHovering = true; break; case GUIM_MOUSE_LEAVE: m_Clicking = false; m_MouseHovering = false; break; case GUIM_MOUSE_RELEASE_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1); break; case GUIM_MOUSE_DBLCLICK_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2); break; case GUIM_MOUSE_MOTION: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } void CMiniMap::RecreateFlareTextures() { // Catch invalid values. if (m_FlareTextureCount > 99) { LOGERROR("Invalid value for flare texture count. Valid range is 0-99."); return; } const CStr textureNumberingFormat = "art/textures/animated/minimap-flare/frame%02u.png"; m_FlareTextures.clear(); m_FlareTextures.reserve(m_FlareTextureCount); for (u32 i = 0; i < m_FlareTextureCount; ++i) { CTextureProperties textureProps(fmt::sprintf(textureNumberingFormat, i).c_str()); textureProps.SetIgnoreQuality(true); m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps)); } } bool CMiniMap::IsMouseOver() const { const CVector2D& mousePos = m_pGUI.GetMousePos(); // Take the magnitude of the difference of the mouse position and minimap center. const float distanceFromCenter = (mousePos - m_CachedActualSize.CenterPoint()).Length(); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. return distanceFromCenter < m_CachedActualSize.GetWidth() / 2.0; } void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const { // Determine X and Z according to proportion of mouse position and minimap. const CVector2D& mousePos = m_pGUI.GetMousePos(); float px = (mousePos.X - m_CachedActualSize.left) / m_CachedActualSize.GetWidth(); float py = (m_CachedActualSize.bottom - mousePos.Y) / m_CachedActualSize.GetHeight(); float angle = GetAngle(); // Scale world coordinates for shrunken square map x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5); z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } void CMiniMap::SetCameraPositionFromMousePosition() { - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; GetMouseWorldCoordinates(target.X, target.Z); - target.Y = terrain->GetExactGroundLevel(target.X, target.Z); + target.Y = terrain.GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } float CMiniMap::GetAngle() const { CVector3D cameraIn = g_Game->GetView()->GetCamera()->GetOrientation().GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } CVector2D CMiniMap::WorldSpaceToMiniMapSpace(const CVector3D& worldPosition) const { // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max. const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); const float relativeX = (worldPosition.X * invTileMapSize - 0.5) / m_MapScale; const float relativeY = (worldPosition.Z * invTileMapSize - 0.5) / m_MapScale; // Rotate coordinates. const float angle = GetAngle(); const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY; const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY; // Calculate coordinates in GUI space. return CVector2D( m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(), m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight()); } bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks)) { ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface()); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(rq.cx); Script::CreateObject(rq, &coords, "x", x, "z", z); JS::RootedValue buttonJs(rq.cx); Script::ToJSVal(rq, &buttonJs, button); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(coords)); ignore_result(paramData.append(buttonJs)); return ScriptEventWithReturn(EventNameWorldClick, paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. void CMiniMap::DrawViewRect(CCanvas2D& canvas) const { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go const float sampleHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight; const CCamera* camera = g_Game->GetView()->GetCamera(); const std::array hitPoints = { camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), sampleHeight), camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), sampleHeight), camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, sampleHeight), camera->GetWorldCoordinates(0, 0, sampleHeight) }; std::vector worldSpaceLines; // We need to prevent drawing view bounds out of the map. const float halfMapSize = static_cast((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f; CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &worldSpaceLines); if (worldSpaceLines.empty()) return; for (size_t index = 0; index < worldSpaceLines.size() && index + 1 < worldSpaceLines.size(); index += 2) { const CVector2D from = WorldSpaceToMiniMapSpace(worldSpaceLines[index]); const CVector2D to = WorldSpaceToMiniMapSpace(worldSpaceLines[index + 1]); canvas.DrawLine({from, to}, 2.0f, CColor(1.0f, 0.3f, 0.3f, 1.0f)); } } void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const { if (m_FlareTextures.empty()) return; const CVector2D flareCenter = WorldSpaceToMiniMapSpace(CVector3D(flare.pos.X, 0.0f, flare.pos.Y)); const CRect destination( flareCenter.X - m_FlareRenderSize, flareCenter.Y - m_FlareRenderSize, flareCenter.X + m_FlareRenderSize, flareCenter.Y + m_FlareRenderSize); const double deltaTime = currentTime - flare.time; const double remainingTime = m_FlareLifetimeSeconds - deltaTime; const u32 flooredStep = floor(deltaTime * m_FlareAnimationSpeed); const float startFadeAlpha = m_FlareStartFadeSeconds > 0.0f ? deltaTime / m_FlareStartFadeSeconds : 1.0f; const float stopFadeAlpha = m_FlareStopFadeSeconds > 0.0f ? remainingTime / m_FlareStopFadeSeconds : 1.0f; const float alpha = Clamp(std::min( SmoothStep(0.0f, 1.0f, startFadeAlpha), SmoothStep(0.0f, 1.0f, stopFadeAlpha)), 0.0f, 1.0f); DrawFlareFrame(canvas, flooredStep % m_FlareTextures.size(), destination, flare.color, alpha); // Draw a second circle if the first has reached half of the animation. if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2) { DrawFlareFrame(canvas, (flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size(), destination, flare.color, alpha); } } void CMiniMap::DrawFlareFrame(CCanvas2D& canvas, const u32 frameIndex, const CRect& destination, const CColor& color, float alpha) const { // TODO: Only draw inside the minimap circle. CTexturePtr texture = m_FlareTextures[frameIndex % m_FlareTextures.size()]; CColor finalColor = color; finalColor.a *= alpha; canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), finalColor, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); } void CMiniMap::Draw(CCanvas2D& canvas) { PROFILE3("render minimap"); // The terrain isn't actually initialized until the map is loaded, which // happens when the game is started, so abort until then. if (!g_Game || !g_Game->IsGameStarted()) return; if (!m_Mask) canvas.DrawRect(m_CachedActualSize, CColor(0.0f, 0.0f, 0.0f, 1.0f)); CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); // Set our globals in case they hadn't been set before - const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - m_MapSize = terrain->GetVerticesPerSide(); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + m_MapSize = terrain.GetVerticesPerSide(); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); // Draw the main textured quad CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture(); if (miniMapTexture.GetTexture()) { const CVector2D center = m_CachedActualSize.CenterPoint(); const CRect source( 0, miniMapTexture.IsFlipped() ? 0 : miniMapTexture.GetTexture()->GetHeight(), miniMapTexture.GetTexture()->GetWidth(), miniMapTexture.IsFlipped() ? miniMapTexture.GetTexture()->GetHeight() : 0); const CSize2D size(m_CachedActualSize.GetSize() / m_MapScale); const CRect destination(center - size / 2.0f, size); canvas.DrawRotatedTexture( miniMapTexture.GetTexture(), destination, source, CColor(1.0f, 1.0f, 1.0f, 1.0f), CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, center, GetAngle()); } for (const CMiniMapTexture::Icon& icon : miniMapTexture.GetIcons()) { const CVector2D center = WorldSpaceToMiniMapSpace( CVector3D(icon.worldPosition.X, 0.0f, icon.worldPosition.Y)); const CRect destination( center.X - icon.halfSize, center.Y - icon.halfSize, center.X + icon.halfSize, center.Y + icon.halfSize); const CRect source(0, 0, icon.texture->GetWidth(), icon.texture->GetHeight()); canvas.DrawTexture( icon.texture, destination, source, icon.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); } PROFILE_START("minimap flares"); DrawViewRect(canvas); const double currentTime = timer_Time(); while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < currentTime) m_MapFlares.pop_front(); for (const MapFlare& flare : m_MapFlares) DrawFlare(canvas, flare, currentTime); PROFILE_END("minimap flares"); } bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr) { CColor color; if (!color.ParseString(colorStr)) { LOGERROR("CMiniMap::Flare: Couldn't parse color string"); return false; } m_MapFlares.push_back({ pos, color, timer_Time() }); return true; } Index: ps/trunk/source/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 27860) +++ ps/trunk/source/ps/Game.cpp (revision 27861) @@ -1,477 +1,477 @@ /* Copyright (C) 2023 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 "Game.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleManager.h" #include "graphics/UnitManager.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "lib/config2.h" #include "lib/timer.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Replay.h" #include "ps/World.h" #include "ps/VideoMode.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/system/ReplayTurnManager.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include extern GameLoopState* g_AtlasGameLoop; /** * Globally accessible pointer to the CGame object. **/ CGame *g_Game=NULL; const CStr CGame::EventNameSimulationUpdate = "SimulationUpdate"; /** * Constructor * **/ CGame::CGame(bool replayLog): m_World(new CWorld(this)), - m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptContext, m_World->GetTerrain())), + m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptContext, &m_World->GetTerrain())), // TODO: we need to remove that global dependency. Maybe the game view // should be created outside only if needed. m_GameView(CRenderer::IsInitialised() ? new CGameView(g_VideoMode.GetBackendDevice(), this) : nullptr), m_GameStarted(false), m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1), m_ViewedPlayerID(-1), m_IsSavedGame(false), m_IsVisualReplay(false), m_ReplayStream(NULL) { // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? if (replayLog) m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); else m_ReplayLogger = new CDummyReplayLogger(); // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); } /** * Destructor * **/ CGame::~CGame() { // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); if (m_ReplayLogger && m_GameStarted) m_ReplayLogger->SaveMetadata(*m_Simulation2); delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_World; delete m_ReplayLogger; delete m_ReplayStream; } void CGame::SetTurnManager(CTurnManager* turnManager) { if (m_TurnManager) delete m_TurnManager; m_TurnManager = turnManager; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::LoadVisualReplayData() { ENSURE(m_IsVisualReplay); ENSURE(!m_ReplayPath.empty()); ENSURE(m_ReplayStream); CReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager()); u32 currentTurn = 0; std::string type; while ((*m_ReplayStream >> type).good()) { if (type == "turn") { u32 turn = 0; u32 turnLength = 0; *m_ReplayStream >> turn >> turnLength; ENSURE(turn == currentTurn && "You tried to replay a commands.txt file of a rejoined client. Please use the host's file."); replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); } else if (type == "cmd") { player_id_t player; *m_ReplayStream >> player; std::string line; std::getline(*m_ReplayStream, line); replayTurnMgr->StoreReplayCommand(currentTurn, player, line); } else if (type == "hash" || type == "hash-quick") { bool quick = (type == "hash-quick"); std::string replayHash; *m_ReplayStream >> replayHash; replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick); } else if (type == "end") ++currentTurn; else CancelLoad(L"Failed to load replay data (unrecognized content)"); } SAFE_DELETE(m_ReplayStream); m_FinalReplayTurn = currentTurn > 0 ? currentTurn - 1 : 0; replayTurnMgr->StoreFinalReplayTurn(m_FinalReplayTurn); return 0; } bool CGame::StartVisualReplay(const OsPath& replayPath) { debug_printf("Starting to replay %s\n", replayPath.string8().c_str()); m_IsVisualReplay = true; SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); m_ReplayPath = replayPath; m_ReplayStream = new std::ifstream(OsString(replayPath).c_str()); std::string type; ENSURE((*m_ReplayStream >> type).good() && type == "start"); std::string line; std::getline(*m_ReplayStream, line); const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); Script::ParseJSON(rq, line, &attribs); StartGame(&attribs, ""); return true; } /** * Initializes the game with the set of attributes provided. * Makes calls to initialize the game view, world, and simulation objects. * Calls are made to facilitate progress reporting of the initialization. **/ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState) { const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); m_IsSavedGame = !savedState.empty(); m_Simulation2->SetInitAttributes(attribs); std::string mapType; Script::GetProperty(rq, attribs, "mapType", mapType); float speed; if (Script::HasProperty(rq, attribs, "gameSpeed")) { if (Script::GetProperty(rq, attribs, "gameSpeed", speed)) SetSimRate(speed); else LOGERROR("GameSpeed could not be parsed."); } LDR_BeginRegistering(); LDR_Register([this](const double) { return m_Simulation2->ProgressiveLoad(); }, L"Simulation init", 1000); // RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization // overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default // values. At the minute, it's just lighting settings, but could be extended to store camera position. // Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at // some point to be stored in the world object? if (m_GameView) m_GameView->RegisterInit(); if (mapType == "random") { // Load random map attributes std::wstring scriptFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "script", scriptFile); Script::GetProperty(rq, attribs, "settings", &settings); m_World->RegisterInitRMS(scriptFile, *scriptInterface.GetContext(), settings, m_PlayerID); } else { std::wstring mapFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "map", mapFile); Script::GetProperty(rq, attribs, "settings", &settings); m_World->RegisterInit(mapFile, *scriptInterface.GetContext(), settings, m_PlayerID); } if (m_GameView) LDR_Register([&waterManager = g_Renderer.GetSceneRenderer().GetWaterManager()](const double) { return waterManager.LoadWaterTextures(); }, L"LoadWaterTextures", 80); if (m_IsSavedGame) LDR_Register([this, savedState](const double) { return LoadInitialState(savedState); }, L"Loading game", 1000); if (m_IsVisualReplay) LDR_Register([this](const double) { return LoadVisualReplayData(); }, L"Loading visual replay data", 1000); LDR_EndRegistering(); } int CGame::LoadInitialState(const std::string& savedState) { ENSURE(m_IsSavedGame); std::stringstream stream(savedState); bool ok = m_Simulation2->DeserializeState(stream); if (!ok) { CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game."); return 0; } return 0; } /** * Game initialization has been completed. Set game started flag and start the session. * * @return PSRETURN 0 **/ PSRETURN CGame::ReallyStartGame() { // Call the script function InitGame only for new games, not saved games if (!m_IsSavedGame) { // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) // that needs to be done before setting up the AI and shouldn't be done in Atlas if (!g_AtlasGameLoop->running) m_Simulation2->PreInitGame(); m_Simulation2->InitGame(); } // We need to do an initial Interpolate call to set up all the models etc, // because Update might never interpolate (e.g. if the game starts paused) // and we could end up rendering before having set up any models (so they'd // all be invisible) Interpolate(0, 0); m_GameStarted = true; // Preload resources to avoid blinking on a first game frame. if (CRenderer::IsInitialised()) g_Renderer.PreloadResourcesBeforeNextFrame(); if (g_NetClient) g_NetClient->LoadFinished(); // Call the reallyStartGame GUI function, but only if it exists if (g_GUI && g_GUI->GetPageCount()) { std::shared_ptr scriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); if (Script::HasProperty(rq, global, "reallyStartGame")) ScriptFunction::CallVoid(rq, global, "reallyStartGame"); } debug_printf("GAME STARTED, ALL INIT COMPLETE\n"); // The call tree we've built for pregame probably isn't useful in-game. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); return 0; } int CGame::GetPlayerID() { return m_PlayerID; } void CGame::SetPlayerID(player_id_t playerID) { m_PlayerID = playerID; m_ViewedPlayerID = playerID; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::GetViewedPlayerID() { return m_ViewedPlayerID; } void CGame::SetViewedPlayerID(player_id_t playerID) { m_ViewedPlayerID = playerID; } void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { if (m_ReplayLogger) m_ReplayLogger->StartGame(attribs); RegisterInit(attribs, savedState); } // TODO: doInterpolate is optional because Atlas interpolates explicitly, // so that it has more control over the update rate. The game might want to // do the same, and then doInterpolate should be redundant and removed. void CGame::Update(const double deltaRealTime, bool doInterpolate) { if (m_Paused || !m_TurnManager) return; const double deltaSimTime = deltaRealTime * m_SimRate; if (deltaSimTime) { // At the normal sim rate, we currently want to render at least one // frame per simulation turn, so let maxTurns be 1. But for fast-forward // sim rates we want to allow more, so it's not bounded by framerate, // so just use the sim rate itself as the number of turns per frame. size_t maxTurns = (size_t)m_SimRate; if (m_TurnManager->Update(deltaSimTime, maxTurns)) { { PROFILE3("gui sim update"); g_GUI->SendEventToAll(EventNameSimulationUpdate); } GetView()->GetLOSTexture().MakeDirty(); } if (CRenderer::IsInitialised()) g_Renderer.GetTimeManager().Update(deltaSimTime); } if (doInterpolate) m_TurnManager->Interpolate(deltaSimTime, deltaRealTime); } void CGame::Interpolate(float simFrameLength, float realFrameLength) { if (!m_TurnManager) return; m_TurnManager->Interpolate(simFrameLength, realFrameLength); } static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f); void CGame::CachePlayerColors() { m_PlayerColors.clear(); CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); if (!cmpPlayerManager) return; int numPlayers = cmpPlayerManager->GetNumPlayers(); m_PlayerColors.resize(numPlayers); for (int i = 0; i < numPlayers; ++i) { CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i)); if (!cmpPlayer) m_PlayerColors[i] = BrokenColor; else m_PlayerColors[i] = cmpPlayer->GetDisplayedColor(); } } const CColor& CGame::GetPlayerColor(player_id_t player) const { if (player < 0 || player >= (int)m_PlayerColors.size()) return BrokenColor; return m_PlayerColors[player]; } bool CGame::IsGameFinished() const { for (const std::pair& p : m_Simulation2->GetEntitiesWithInterface(IID_Player)) { CmpPtr cmpPlayer(*m_Simulation2, p.first); if (cmpPlayer && cmpPlayer->GetState() == "won") return true; } return false; } Index: ps/trunk/source/ps/World.h =================================================================== --- ps/trunk/source/ps/World.h (revision 27860) +++ ps/trunk/source/ps/World.h (revision 27861) @@ -1,107 +1,111 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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 . */ /** * File : World.h * Project : engine * Description : Contains the CWorld Class which contains all the entities and represents them at a specific moment in time. * **/ #ifndef INCLUDED_WORLD #define INCLUDED_WORLD #include "ps/CStrForward.h" #include "ps/Errors.h" #ifndef ERROR_GROUP_GAME_DEFINED #define ERROR_GROUP_GAME_DEFINED ERROR_GROUP(Game); #endif ERROR_SUBGROUP(Game, World); ERROR_TYPE(Game_World, MapLoadFailed); class CGame; class CUnitManager; class CTerrain; class CMapReader; class ScriptContext; /** * CWorld is a general data class containing whatever is needed to accurately represent the world. * This includes the map, entities, influence maps, tiles, heightmap, etc. **/ class CWorld { NONCOPYABLE(CWorld); /** * pointer to the CGame object representing the game. **/ CGame *m_pGame; /** * pointer to the CTerrain object representing the height map. **/ CTerrain *m_Terrain; /** * pointer to the CUnitManager that holds all the units in the world. **/ CUnitManager *m_UnitManager; CMapReader* m_MapReader; public: CWorld(CGame *pGame); ~CWorld(); /* Initialize the World - load the map and all objects */ void RegisterInit(const CStrW& mapFile, const ScriptContext& cx, JS::HandleValue settings, int playerID); /* Initialize the World - generate and load the random map */ void RegisterInitRMS(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, int playerID); /** * Explicitly delete m_MapReader once the map has finished loading. **/ int DeleteMapReader(); /** - * Get the pointer to the terrain object. + * Get a reference to the terrain object. * - * @return CTerrain * the value of m_Terrain. + * @return CTerrain& dereferenced m_Terrain. **/ - inline CTerrain *GetTerrain() - { return m_Terrain; } + CTerrain& GetTerrain() + { + return *m_Terrain; + } /** * Get a reference to the unit manager object. * - * @return CUnitManager & dereferenced m_UnitManager. + * @return CUnitManager& dereferenced m_UnitManager. **/ - inline CUnitManager &GetUnitManager() - { return *m_UnitManager; } + CUnitManager& GetUnitManager() + { + return *m_UnitManager; + } }; // rationale: see definition. class CLightEnv; extern CLightEnv g_LightEnv; #endif Index: ps/trunk/source/ps/scripting/JSInterface_Game.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 27860) +++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 27861) @@ -1,199 +1,199 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "JSInterface_Game.h" #include "graphics/Terrain.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Replay.h" #include "ps/World.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/StructuredClone.h" #include "simulation2/system/TurnManager.h" #include "simulation2/Simulation2.h" #include "soundmanager/SoundManager.h" namespace JSI_Game { bool IsGameStarted() { return g_Game; } void StartGame(const ScriptInterface& guiInterface, JS::HandleValue attribs, int playerID, bool storeReplay) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(storeReplay); // Convert from GUI script context to sim script context/ CSimulation2* sim = g_Game->GetSimulation2(); ScriptRequest rqSim(sim->GetScriptInterface()); JS::RootedValue gameAttribs(rqSim.cx, Script::CloneValueFromOtherCompartment(sim->GetScriptInterface(), guiInterface, attribs)); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameAttribs, ""); } void Script_EndGame() { EndGame(); } int GetPlayerID() { if (!g_Game) return -1; return g_Game->GetPlayerID(); } void SetPlayerID(int id) { if (!g_Game) return; g_Game->SetPlayerID(id); } void SetViewedPlayer(int id) { if (!g_Game) return; g_Game->SetViewedPlayerID(id); } float GetSimRate() { return g_Game->GetSimRate(); } void SetSimRate(float rate) { g_Game->SetSimRate(rate); } int GetPendingTurns() { if (!g_Game || !g_Game->GetTurnManager()) return 0; return g_Game->GetTurnManager()->GetPendingTurns(); } bool IsPaused(const ScriptRequest& rq) { if (!g_Game) { ScriptException::Raise(rq, "Game is not started"); return false; } return g_Game->m_Paused; } void SetPaused(const ScriptRequest& rq, bool pause, bool sendMessage) { if (!g_Game) { ScriptException::Raise(rq, "Game is not started"); return; } g_Game->m_Paused = pause; #if CONFIG2_AUDIO if (g_SoundManager) { g_SoundManager->PauseAmbient(pause); g_SoundManager->PauseAction(pause); } #endif if (g_NetClient && sendMessage) g_NetClient->SendPausedMessage(pause); } bool IsVisualReplay() { if (!g_Game) return false; return g_Game->IsVisualReplay(); } std::wstring GetCurrentReplayDirectory() { if (!g_Game) return std::wstring(); if (g_Game->IsVisualReplay()) return g_Game->GetReplayPath().Parent().Filename().string(); return g_Game->GetReplayLogger().GetDirectory().Filename().string(); } void EnableTimeWarpRecording(unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void RewindTimeWarp() { g_Game->GetTurnManager()->RewindTimeWarp(); } void DumpTerrainMipmap() { VfsPath filename(L"screenshots/terrainmipmap.png"); - g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename); + g_Game->GetWorld()->GetTerrain().GetHeightMipmap().DumpToDisk(filename); OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8()); } void RegisterScriptFunctions(const ScriptRequest& rq) { ScriptFunction::Register<&IsGameStarted>(rq, "IsGameStarted"); ScriptFunction::Register<&StartGame>(rq, "StartGame"); ScriptFunction::Register<&Script_EndGame>(rq, "EndGame"); ScriptFunction::Register<&GetPlayerID>(rq, "GetPlayerID"); ScriptFunction::Register<&SetPlayerID>(rq, "SetPlayerID"); ScriptFunction::Register<&SetViewedPlayer>(rq, "SetViewedPlayer"); ScriptFunction::Register<&GetSimRate>(rq, "GetSimRate"); ScriptFunction::Register<&SetSimRate>(rq, "SetSimRate"); ScriptFunction::Register<&GetPendingTurns>(rq, "GetPendingTurns"); ScriptFunction::Register<&IsPaused>(rq, "IsPaused"); ScriptFunction::Register<&SetPaused>(rq, "SetPaused"); ScriptFunction::Register<&IsVisualReplay>(rq, "IsVisualReplay"); ScriptFunction::Register<&GetCurrentReplayDirectory>(rq, "GetCurrentReplayDirectory"); ScriptFunction::Register<&EnableTimeWarpRecording>(rq, "EnableTimeWarpRecording"); ScriptFunction::Register<&RewindTimeWarp>(rq, "RewindTimeWarp"); ScriptFunction::Register<&DumpTerrainMipmap>(rq, "DumpTerrainMipmap"); } } Index: ps/trunk/source/renderer/TerrainOverlay.cpp =================================================================== --- ps/trunk/source/renderer/TerrainOverlay.cpp (revision 27860) +++ ps/trunk/source/renderer/TerrainOverlay.cpp (revision 27861) @@ -1,410 +1,410 @@ /* Copyright (C) 2023 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 "TerrainOverlay.h" #include "graphics/Color.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgram.h" #include "graphics/Terrain.h" #include "lib/bits.h" #include "maths/MathUtil.h" #include "maths/Vector2D.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TerrainRenderer.h" #include "simulation2/system/SimContext.h" #include namespace { // Global overlay list management: std::vector> g_TerrainOverlayList; void AdjustOverlayGraphicsPipelineState( Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc, const bool drawHidden) { pipelineStateDesc.depthStencilState.depthTestEnabled = !drawHidden; pipelineStateDesc.blendState.enabled = true; pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor = Renderer::Backend::BlendFactor::SRC_ALPHA; pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor = Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA; pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp = Renderer::Backend::BlendOp::ADD; pipelineStateDesc.rasterizationState.cullMode = drawHidden ? Renderer::Backend::CullMode::NONE : Renderer::Backend::CullMode::BACK; } CShaderTechniquePtr CreateOverlayTileShaderTechnique(const bool drawHidden) { return g_Renderer.GetShaderManager().LoadEffect( str_debug_line, {}, [drawHidden](Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc) { AdjustOverlayGraphicsPipelineState(pipelineStateDesc, drawHidden); // To ensure that outlines are drawn on top of the terrain correctly (and // don't Z-fight and flicker nastily), use detph bias to pull them towards // the camera. pipelineStateDesc.rasterizationState.depthBiasEnabled = true; pipelineStateDesc.rasterizationState.depthBiasConstantFactor = -1.0f; pipelineStateDesc.rasterizationState.depthBiasSlopeFactor = -1.0f; }); } CShaderTechniquePtr CreateOverlayOutlineShaderTechnique(const bool drawHidden) { return g_Renderer.GetShaderManager().LoadEffect( str_debug_line, {}, [drawHidden](Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc) { AdjustOverlayGraphicsPipelineState(pipelineStateDesc, drawHidden); pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; }); } } // anonymous namespace ITerrainOverlay::ITerrainOverlay(int priority) { // Add to global list of overlays g_TerrainOverlayList.emplace_back(this, priority); // Sort by overlays by priority. Do stable sort so that adding/removing // overlays doesn't randomly disturb all the existing ones (which would // be noticeable if they have the same priority and overlap). std::stable_sort(g_TerrainOverlayList.begin(), g_TerrainOverlayList.end(), [](const std::pair& a, const std::pair& b) { return a.second < b.second; }); } ITerrainOverlay::~ITerrainOverlay() { std::vector >::iterator newEnd = std::remove_if(g_TerrainOverlayList.begin(), g_TerrainOverlayList.end(), [this](const std::pair& a) { return a.first == this; }); g_TerrainOverlayList.erase(newEnd, g_TerrainOverlayList.end()); } void ITerrainOverlay::RenderOverlaysBeforeWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (g_TerrainOverlayList.empty()) return; PROFILE3_GPU("terrain overlays (before)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain overlays before water"); for (size_t i = 0; i < g_TerrainOverlayList.size(); ++i) g_TerrainOverlayList[i].first->RenderBeforeWater(deviceCommandContext); } void ITerrainOverlay::RenderOverlaysAfterWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { if (g_TerrainOverlayList.empty()) return; PROFILE3_GPU("terrain overlays (after)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain overlays after water"); for (size_t i = 0; i < g_TerrainOverlayList.size(); ++i) g_TerrainOverlayList[i].first->RenderAfterWater(deviceCommandContext, cullGroup); } ////////////////////////////////////////////////////////////////////////// TerrainOverlay::TerrainOverlay( const CSimContext& simContext, int priority /* = 100 */) : ITerrainOverlay(priority), m_Terrain(&simContext.GetTerrain()) { m_OverlayTechTile = CreateOverlayTileShaderTechnique(false); m_OverlayTechTileHidden = CreateOverlayTileShaderTechnique(true); m_OverlayTechOutline = CreateOverlayOutlineShaderTechnique(false); m_OverlayTechOutlineHidden = CreateOverlayOutlineShaderTechnique(true); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, sizeof(float) * 3, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } void TerrainOverlay::StartRender() { } void TerrainOverlay::EndRender() { } void TerrainOverlay::GetTileExtents( ssize_t& min_i_inclusive, ssize_t& min_j_inclusive, ssize_t& max_i_inclusive, ssize_t& max_j_inclusive) { // Default to whole map min_i_inclusive = min_j_inclusive = 0; max_i_inclusive = max_j_inclusive = m_Terrain->GetTilesPerSide()-1; } void TerrainOverlay::RenderBeforeWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (!m_Terrain) return; // should never happen, but let's play it safe StartRender(); ssize_t min_i, min_j, max_i, max_j; GetTileExtents(min_i, min_j, max_i, max_j); // Clamp the min to 0, but the max to -1 - so tile -1 can never be rendered, // but if unclamped_max<0 then no tiles at all will be rendered. And the same // for the upper limit. min_i = Clamp(min_i, 0, m_Terrain->GetTilesPerSide()); min_j = Clamp(min_j, 0, m_Terrain->GetTilesPerSide()); max_i = Clamp(max_i, -1, m_Terrain->GetTilesPerSide()-1); max_j = Clamp(max_j, -1, m_Terrain->GetTilesPerSide()-1); for (m_j = min_j; m_j <= max_j; ++m_j) for (m_i = min_i; m_i <= max_i; ++m_i) ProcessTile(deviceCommandContext, m_i, m_j); EndRender(); } void TerrainOverlay::RenderTile( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CColor& color, bool drawHidden) { RenderTile(deviceCommandContext, color, drawHidden, m_i, m_j); } void TerrainOverlay::RenderTile( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CColor& color, bool drawHidden, ssize_t i, ssize_t j) { // TODO: unnecessary computation calls has been removed but we should use // a vertex buffer or a vertex shader with a texture. // Not sure if it's possible on old OpenGL. CVector3D pos[2][2]; for (int di = 0; di < 2; ++di) for (int dj = 0; dj < 2; ++dj) m_Terrain->CalcPosition(i + di, j + dj, pos[di][dj]); std::vector vertices; #define ADD(position) \ vertices.emplace_back((position).X); \ vertices.emplace_back((position).Y); \ vertices.emplace_back((position).Z); if (m_Terrain->GetTriangulationDir(i, j)) { ADD(pos[0][0]); ADD(pos[1][0]); ADD(pos[0][1]); ADD(pos[1][0]); ADD(pos[1][1]); ADD(pos[0][1]); } else { ADD(pos[0][0]); ADD(pos[1][0]); ADD(pos[1][1]); ADD(pos[1][1]); ADD(pos[0][1]); ADD(pos[0][0]); } #undef ADD const CShaderTechniquePtr& shaderTechnique = drawHidden ? m_OverlayTechTileHidden : m_OverlayTechTile; deviceCommandContext->SetGraphicsPipelineState( shaderTechnique->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* overlayShader = shaderTechnique->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( overlayShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( overlayShader->GetBindingSlot(str_color), color.AsFloatArray()); deviceCommandContext->SetVertexInputLayout(m_VertexInputLayout); deviceCommandContext->SetVertexBufferData( 0, vertices.data(), vertices.size() * sizeof(vertices[0])); deviceCommandContext->Draw(0, vertices.size() / 3); deviceCommandContext->EndPass(); } void TerrainOverlay::RenderTileOutline( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CColor& color, bool drawHidden) { RenderTileOutline(deviceCommandContext, color, drawHidden, m_i, m_j); } void TerrainOverlay::RenderTileOutline( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CColor& color, bool drawHidden, ssize_t i, ssize_t j) { std::vector vertices; #define ADD(i, j) \ m_Terrain->CalcPosition(i, j, position); \ vertices.emplace_back(position.X); \ vertices.emplace_back(position.Y); \ vertices.emplace_back(position.Z); CVector3D position; ADD(i, j); ADD(i + 1, j); ADD(i + 1, j + 1); ADD(i, j); ADD(i + 1, j + 1); ADD(i, j + 1); #undef ADD const CShaderTechniquePtr& shaderTechnique = drawHidden ? m_OverlayTechOutlineHidden : m_OverlayTechOutline; deviceCommandContext->SetGraphicsPipelineState( shaderTechnique->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* overlayShader = shaderTechnique->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( overlayShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( overlayShader->GetBindingSlot(str_color), color.AsFloatArray()); deviceCommandContext->SetVertexInputLayout(m_VertexInputLayout); deviceCommandContext->SetVertexBufferData( 0, vertices.data(), vertices.size() * sizeof(vertices[0])); deviceCommandContext->Draw(0, vertices.size() / 3); deviceCommandContext->EndPass(); } ////////////////////////////////////////////////////////////////////////// TerrainTextureOverlay::TerrainTextureOverlay(float texelsPerTile, int priority) : ITerrainOverlay(priority), m_TexelsPerTile(texelsPerTile) { } TerrainTextureOverlay::~TerrainTextureOverlay() = default; void TerrainTextureOverlay::RenderAfterWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); - ssize_t w = (ssize_t)(terrain->GetTilesPerSide() * m_TexelsPerTile); - ssize_t h = (ssize_t)(terrain->GetTilesPerSide() * m_TexelsPerTile); + const ssize_t w = static_cast(terrain.GetTilesPerSide() * m_TexelsPerTile); + const ssize_t h = static_cast(terrain.GetTilesPerSide() * m_TexelsPerTile); const uint32_t requiredWidth = round_up_to_pow2(w); const uint32_t requiredHeight = round_up_to_pow2(h); // Recreate the texture with new size if necessary if (!m_Texture || m_Texture->GetWidth() != requiredWidth || m_Texture->GetHeight() != requiredHeight) { m_Texture = deviceCommandContext->GetDevice()->CreateTexture2D("TerrainOverlayTexture", Renderer::Backend::ITexture::Usage::TRANSFER_DST | Renderer::Backend::ITexture::Usage::SAMPLED, Renderer::Backend::Format::R8G8B8A8_UNORM, requiredWidth, requiredHeight, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::NEAREST, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); } u8* data = (u8*)calloc(w * h, 4); BuildTextureRGBA(data, w, h); deviceCommandContext->UploadTextureRegion( m_Texture.get(), Renderer::Backend::Format::R8G8B8A8_UNORM, data, w * h * 4, 0, 0, w, h); free(data); const CVector2D textureTransform{ m_TexelsPerTile / (m_Texture->GetWidth() * TERRAIN_TILE_SIZE), m_TexelsPerTile / (m_Texture->GetHeight() * TERRAIN_TILE_SIZE)}; g_Renderer.GetSceneRenderer().GetTerrainRenderer().RenderTerrainOverlayTexture( deviceCommandContext, cullGroup, textureTransform, m_Texture.get()); } SColor4ub TerrainTextureOverlay::GetColor(size_t idx, u8 alpha) const { static u8 colors[][3] = { { 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, { 255, 255, 0 }, { 255, 0, 255 }, { 0, 255, 255 }, { 255, 255, 255 }, { 127, 0, 0 }, { 0, 127, 0 }, { 0, 0, 127 }, { 127, 127, 0 }, { 127, 0, 127 }, { 0, 127, 127 }, { 127, 127, 127}, { 255, 127, 0 }, { 127, 255, 0 }, { 255, 0, 127 }, { 127, 0, 255}, { 0, 255, 127 }, { 0, 127, 255}, { 255, 127, 127}, { 127, 255, 127}, { 127, 127, 255}, { 127, 255, 255 }, { 255, 127, 255 }, { 255, 255, 127 }, }; size_t c = idx % ARRAY_SIZE(colors); return SColor4ub(colors[c][0], colors[c][1], colors[c][2], alpha); } Index: ps/trunk/source/renderer/WaterManager.cpp =================================================================== --- ps/trunk/source/renderer/WaterManager.cpp (revision 27860) +++ ps/trunk/source/renderer/WaterManager.cpp (revision 27861) @@ -1,1144 +1,1153 @@ /* Copyright (C) 2023 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 "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgram.h" #include "lib/bits.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Vector2D.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/components/ICmpRangeManager.h" #include struct CoastalPoint { CoastalPoint(int idx, CVector2D pos) : index(idx), position(pos) {}; int index; CVector2D position; }; struct SWavesVertex { // vertex position CVector3D m_BasePosition; CVector3D m_ApexPosition; CVector3D m_SplashPosition; CVector3D m_RetreatPosition; CVector2D m_PerpVect; float m_UV[2]; }; cassert(sizeof(SWavesVertex) == 64); struct WaveObject { CVertexBufferManager::Handle m_VBVertices; CBoundingBoxAligned m_AABB; size_t m_Width; float m_TimeDiff; }; WaterManager::WaterManager(Renderer::Backend::IDevice* device) : m_Device(device) { // water m_RenderWater = false; // disabled until textures are successfully loaded m_WaterHeight = 5.0f; m_RefTextureSize = 0; m_WaterTexTimer = 0.0; m_WindAngle = 0.0f; m_Waviness = 8.0f; m_WaterColor = CColor(0.3f, 0.35f, 0.7f, 1.0f); m_WaterTint = CColor(0.28f, 0.3f, 0.59f, 1.0f); m_Murkiness = 0.45f; m_RepeatPeriod = 16.0f; m_WaterEffects = true; m_WaterFancyEffects = false; m_WaterRealDepth = false; m_WaterRefraction = false; m_WaterReflection = false; m_WaterType = L"ocean"; m_NeedsReloading = false; m_NeedInfoUpdate = true; m_MapSize = 0; m_updatei0 = 0; m_updatej0 = 0; m_updatei1 = 0; m_updatej1 = 0; } WaterManager::~WaterManager() { // Cleanup if the caller messed up UnloadWaterTextures(); m_ShoreWaves.clear(); m_ShoreWavesVBIndices.Reset(); m_DistanceHeightmap.reset(); m_WindStrength.reset(); m_FancyEffectsFramebuffer.reset(); m_FancyEffectsOccludersFramebuffer.reset(); m_RefractionFramebuffer.reset(); m_ReflectionFramebuffer.reset(); m_FancyTexture.reset(); m_FancyTextureDepth.reset(); m_ReflFboDepthTexture.reset(); m_RefrFboDepthTexture.reset(); } void WaterManager::Initialize() { const uint32_t stride = sizeof(SWavesVertex); const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_BasePosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::NORMAL, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWavesVertex, m_PerpVect), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, offsetof(SWavesVertex, m_UV), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV1, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_ApexPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV2, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_SplashPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV3, Renderer::Backend::Format::R32G32B32_SFLOAT, offsetof(SWavesVertex, m_RetreatPosition), stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0} }}; m_ShoreVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); } /////////////////////////////////////////////////////////////////// // Progressive load of water textures int WaterManager::LoadWaterTextures() { // TODO: this doesn't need to be progressive-loading any more // (since texture loading is async now) wchar_t pathname[PATH_MAX]; // Load diffuse grayscale images (for non-fancy water) for (size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); ++i) { swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/default/diffuse%02d.dds", (int)i+1); CTextureProperties textureProps(pathname); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_WaterTexture[i] = texture; } m_RenderWater = true; // Load normalmaps (for fancy water) ReloadWaterNormalTextures(); // Load CoastalWaves { CTextureProperties textureProps(L"art/textures/terrain/types/water/coastalWave.png"); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_WaveTex = texture; } // Load Foam { CTextureProperties textureProps(L"art/textures/terrain/types/water/foam.png"); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_FoamTex = texture; } RecreateOrLoadTexturesIfNeeded(); return 0; } void WaterManager::RecreateOrLoadTexturesIfNeeded() { // Use screen-sized textures for minimum artifacts. const size_t newRefTextureSize = round_up_to_pow2(g_Renderer.GetHeight()); if (m_RefTextureSize != newRefTextureSize) { m_ReflectionFramebuffer.reset(); m_ReflectionTexture.reset(); m_ReflFboDepthTexture.reset(); m_RefractionFramebuffer.reset(); m_RefractionTexture.reset(); m_RefrFboDepthTexture.reset(); m_RefTextureSize = newRefTextureSize; } const Renderer::Backend::Format depthFormat = m_Device->GetPreferredDepthStencilFormat( Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, true, false); // Create reflection textures. const bool needsReflectionTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterReflection(); if (needsReflectionTextures && !m_ReflectionTexture) { m_ReflectionTexture = m_Device->CreateTexture2D("WaterReflectionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); m_ReflFboDepthTexture = m_Device->CreateTexture2D("WaterReflectionDepthTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::NEAREST, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_ReflectionTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.5f, 0.5f, 1.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_ReflFboDepthTexture.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_ReflectionFramebuffer = m_Device->CreateFramebuffer("ReflectionFramebuffer", &colorAttachment, &depthStencilAttachment); if (!m_ReflectionFramebuffer) { g_RenderingOptions.SetWaterReflection(false); UpdateQuality(); } } // Create refraction textures. const bool needsRefractionTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterRefraction(); if (needsRefractionTextures && !m_RefractionTexture) { m_RefractionTexture = m_Device->CreateTexture2D("WaterRefractionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); m_RefrFboDepthTexture = m_Device->CreateTexture2D("WaterRefractionDepthTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::NEAREST, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_RefractionTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{1.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_RefrFboDepthTexture.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_RefractionFramebuffer = m_Device->CreateFramebuffer("RefractionFramebuffer", &colorAttachment, &depthStencilAttachment); if (!m_RefractionFramebuffer) { g_RenderingOptions.SetWaterRefraction(false); UpdateQuality(); } } const uint32_t newWidth = static_cast(g_Renderer.GetWidth()); const uint32_t newHeight = static_cast(g_Renderer.GetHeight()); if (m_FancyTexture && (m_FancyTexture->GetWidth() != newWidth || m_FancyTexture->GetHeight() != newHeight)) { m_FancyEffectsFramebuffer.reset(); m_FancyEffectsOccludersFramebuffer.reset(); m_FancyTexture.reset(); m_FancyTextureDepth.reset(); } // Create the Fancy Effects textures. const bool needsFancyTextures = g_RenderingOptions.GetWaterEffects() && g_RenderingOptions.GetWaterFancyEffects(); if (needsFancyTextures && !m_FancyTexture) { m_FancyTexture = m_Device->CreateTexture2D("WaterFancyTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, Renderer::Backend::Format::R8G8B8A8_UNORM, g_Renderer.GetWidth(), g_Renderer.GetHeight(), Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::REPEAT)); m_FancyTextureDepth = m_Device->CreateTexture2D("WaterFancyDepthTexture", Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, depthFormat, g_Renderer.GetWidth(), g_Renderer.GetHeight(), Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::REPEAT)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_FancyTexture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_FancyTextureDepth.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; // We need to store depth for later rendering occluders. depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_FancyEffectsFramebuffer = m_Device->CreateFramebuffer("FancyEffectsFramebuffer", &colorAttachment, &depthStencilAttachment); Renderer::Backend::SColorAttachment occludersColorAttachment{}; occludersColorAttachment.texture = m_FancyTexture.get(); occludersColorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; occludersColorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; occludersColorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment occludersDepthStencilAttachment{}; occludersDepthStencilAttachment.texture = m_FancyTextureDepth.get(); occludersDepthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; occludersDepthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::DONT_CARE; m_FancyEffectsOccludersFramebuffer = m_Device->CreateFramebuffer("FancyEffectsOccludersFramebuffer", &occludersColorAttachment, &occludersDepthStencilAttachment); if (!m_FancyEffectsFramebuffer || !m_FancyEffectsOccludersFramebuffer) { g_RenderingOptions.SetWaterRefraction(false); UpdateQuality(); } } } void WaterManager::ReloadWaterNormalTextures() { wchar_t pathname[PATH_MAX]; for (size_t i = 0; i < ARRAY_SIZE(m_NormalMap); ++i) { swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/%ls/normal00%02d.png", m_WaterType.c_str(), static_cast(i) + 1); CTextureProperties textureProps(pathname); textureProps.SetAddressMode( Renderer::Backend::Sampler::AddressMode::REPEAT); textureProps.SetAnisotropicFilter(true); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); m_NormalMap[i] = texture; } } /////////////////////////////////////////////////////////////////// // Unload water textures void WaterManager::UnloadWaterTextures() { for (size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); i++) m_WaterTexture[i].reset(); for (size_t i = 0; i < ARRAY_SIZE(m_NormalMap); i++) m_NormalMap[i].reset(); m_RefractionFramebuffer.reset(); m_ReflectionFramebuffer.reset(); m_ReflectionTexture.reset(); m_RefractionTexture.reset(); } template static inline void ComputeDirection(float* distanceMap, const u16* heightmap, float waterHeight, size_t SideSize, size_t maxLevel) { #define ABOVEWATER(x, z) (HEIGHT_SCALE * heightmap[z*SideSize + x] >= waterHeight) #define UPDATELOOKAHEAD \ for (; lookahead <= id2+maxLevel && lookahead < SideSize && \ ((!Transpose && !ABOVEWATER(lookahead, id1)) || (Transpose && !ABOVEWATER(id1, lookahead))); ++lookahead) // Algorithm: // We want to know the distance to the closest shore point. Go through each line/column, // keep track of when we encountered the last shore point and how far ahead the next one is. for (size_t id1 = 0; id1 < SideSize; ++id1) { size_t id2 = 0; const size_t& x = Transpose ? id1 : id2; const size_t& z = Transpose ? id2 : id1; size_t level = ABOVEWATER(x, z) ? 0 : maxLevel; size_t lookahead = (size_t)(level > 0); UPDATELOOKAHEAD; // start moving for (; id2 < SideSize; ++id2) { // update current level if (ABOVEWATER(x, z)) level = 0; else level = std::min(level+1, maxLevel); // move lookahead if (lookahead == id2) ++lookahead; UPDATELOOKAHEAD; // This is the important bit: set the distance to either: // - the distance to the previous shore point (level) // - the distance to the next shore point (lookahead-id2) distanceMap[z*SideSize + x] = std::min(distanceMap[z*SideSize + x], (float)std::min(lookahead-id2, level)); } } #undef ABOVEWATER #undef UPDATELOOKAHEAD } /////////////////////////////////////////////////////////////////// // Calculate our binary heightmap from the terrain heightmap. void WaterManager::RecomputeDistanceHeightmap() { - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - if (!terrain || !terrain->GetHeightMap()) + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + if (!terrain.GetHeightMap()) return; size_t SideSize = m_MapSize; // we want to look ahead some distance, but not too much (less efficient and not interesting). This is our lookahead. const size_t maxLevel = 5; if (!m_DistanceHeightmap) { m_DistanceHeightmap = std::make_unique(SideSize * SideSize); std::fill(m_DistanceHeightmap.get(), m_DistanceHeightmap.get() + SideSize * SideSize, static_cast(maxLevel)); } // Create a manhattan-distance heightmap. // This could be refined to only be done near the coast itself, but it's probably not necessary. - u16* heightmap = terrain->GetHeightMap(); + const u16* const heightmap = terrain.GetHeightMap(); ComputeDirection(m_DistanceHeightmap.get(), heightmap, m_WaterHeight, SideSize, maxLevel); ComputeDirection(m_DistanceHeightmap.get(), heightmap, m_WaterHeight, SideSize, maxLevel); } // This requires m_DistanceHeightmap to be defined properly. void WaterManager::CreateWaveMeshes() { if (m_MapSize == 0) return; - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - if (!terrain || !terrain->GetHeightMap()) + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + if (!terrain.GetHeightMap()) return; m_ShoreWaves.clear(); m_ShoreWavesVBIndices.Reset(); if (m_Waviness < 5.0f && m_WaterType != L"ocean") return; size_t SideSize = m_MapSize; // First step: get the points near the coast. std::set CoastalPointsSet; for (size_t z = 1; z < SideSize-1; ++z) for (size_t x = 1; x < SideSize-1; ++x) // get the points not on the shore but near it, ocean-side if (m_DistanceHeightmap[z*m_MapSize + x] > 0.5f && m_DistanceHeightmap[z*m_MapSize + x] < 1.5f) CoastalPointsSet.insert((z)*SideSize + x); // Second step: create chains out of those coastal points. static const int around[8][2] = { { -1,-1 }, { -1,0 }, { -1,1 }, { 0,1 }, { 1,1 }, { 1,0 }, { 1,-1 }, { 0,-1 } }; std::vector > CoastalPointsChains; while (!CoastalPointsSet.empty()) { int index = *(CoastalPointsSet.begin()); int x = index % SideSize; int y = (index - x ) / SideSize; std::deque Chain; Chain.push_front(CoastalPoint(index,CVector2D(x*4,y*4))); // Erase us. CoastalPointsSet.erase(CoastalPointsSet.begin()); // We're our starter points. At most we can have 2 points close to us. // We'll pick the first one and look for its neighbors (he can only have one new) // Up until we either reach the end of the chain, or ourselves. // Then go down the other direction if there is any. int neighbours[2] = { -1, -1 }; int nbNeighb = 0; for (int i = 0; i < 8; ++i) { if (CoastalPointsSet.count(x + around[i][0] + (y + around[i][1])*SideSize)) { if (nbNeighb < 2) neighbours[nbNeighb] = x + around[i][0] + (y + around[i][1])*SideSize; ++nbNeighb; } } if (nbNeighb > 2) continue; for (int i = 0; i < 2; ++i) { if (neighbours[i] == -1) continue; // Move to our neighboring point int xx = neighbours[i] % SideSize; int yy = (neighbours[i] - xx ) / SideSize; int indexx = xx + yy*SideSize; int endedChain = false; if (i == 0) Chain.push_back(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); else Chain.push_front(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); // If there's a loop we'll be the "other" neighboring point already so check for that. // We'll readd at the end/front the other one to have full squares. if (CoastalPointsSet.count(indexx) == 0) break; CoastalPointsSet.erase(indexx); // Start checking from there. while(!endedChain) { bool found = false; nbNeighb = 0; for (int p = 0; p < 8; ++p) { if (CoastalPointsSet.count(xx+around[p][0] + (yy + around[p][1])*SideSize)) { if (nbNeighb >= 2) { CoastalPointsSet.erase(xx + yy*SideSize); continue; } ++nbNeighb; // We've found a new point around us. // Move there xx = xx + around[p][0]; yy = yy + around[p][1]; indexx = xx + yy*SideSize; if (i == 0) Chain.push_back(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); else Chain.push_front(CoastalPoint(indexx,CVector2D(xx*4,yy*4))); CoastalPointsSet.erase(xx + yy*SideSize); found = true; break; } } if (!found) endedChain = true; } } if (Chain.size() > 10) CoastalPointsChains.push_back(Chain); } // (optional) third step: Smooth chains out. // This is also really dumb. for (size_t i = 0; i < CoastalPointsChains.size(); ++i) { // Bump 1 for smoother. for (int p = 0; p < 3; ++p) { for (size_t j = 1; j < CoastalPointsChains[i].size()-1; ++j) { CVector2D realPos = CoastalPointsChains[i][j-1].position + CoastalPointsChains[i][j+1].position; CoastalPointsChains[i][j].position = (CoastalPointsChains[i][j].position + realPos/2.0f)/2.0f; } } } // Fourth step: create waves themselves, using those chains. We basically create subchains. u16 waveSizes = 14; // maximal size in width. // Construct indices buffer (we can afford one for all of them) std::vector water_indices; for (u16 a = 0; a < waveSizes - 1; ++a) { for (u16 rect = 0; rect < 7; ++rect) { water_indices.push_back(a * 9 + rect); water_indices.push_back(a * 9 + 9 + rect); water_indices.push_back(a * 9 + 1 + rect); water_indices.push_back(a * 9 + 9 + rect); water_indices.push_back(a * 9 + 10 + rect); water_indices.push_back(a * 9 + 1 + rect); } } // Generic indexes, max-length m_ShoreWavesVBIndices = g_VBMan.AllocateChunk( sizeof(u16), water_indices.size(), Renderer::Backend::IBuffer::Type::INDEX, false, nullptr, CVertexBufferManager::Group::WATER); m_ShoreWavesVBIndices->m_Owner->UpdateChunkVertices(m_ShoreWavesVBIndices.Get(), &water_indices[0]); float diff = (rand() % 50) / 5.0f; std::vector vertices, reversed; for (size_t i = 0; i < CoastalPointsChains.size(); ++i) { for (size_t j = 0; j < CoastalPointsChains[i].size()-waveSizes; ++j) { if (CoastalPointsChains[i].size()- 1 - j < waveSizes) break; u16 width = waveSizes; // First pass to get some parameters out. float outmost = 0.0f; // how far to move on the shore. float avgDepth = 0.0f; int sign = 1; CVector2D firstPerp(0,0), perp(0,0), lastPerp(0,0); for (u16 a = 0; a < waveSizes;++a) { lastPerp = perp; perp = CVector2D(0,0); int nb = 0; CVector2D pos = CoastalPointsChains[i][j+a].position; CVector2D posPlus; CVector2D posMinus; if (a > 0) { ++nb; posMinus = CoastalPointsChains[i][j+a-1].position; perp += pos-posMinus; } if (a < waveSizes-1) { ++nb; posPlus = CoastalPointsChains[i][j+a+1].position; perp += posPlus-pos; } perp /= nb; perp = CVector2D(-perp.Y,perp.X).Normalized(); if (a == 0) firstPerp = perp; if ( a > 1 && perp.Dot(lastPerp) < 0.90f && perp.Dot(firstPerp) < 0.70f) { width = a+1; break; } - if (terrain->GetExactGroundLevel(pos.X+perp.X*1.5f, pos.Y+perp.Y*1.5f) > m_WaterHeight) + if (terrain.GetExactGroundLevel(pos.X+perp.X*1.5f, pos.Y+perp.Y*1.5f) + > m_WaterHeight) sign = -1; - avgDepth += terrain->GetExactGroundLevel(pos.X+sign*perp.X*20.0f, pos.Y+sign*perp.Y*20.0f) - m_WaterHeight; + avgDepth += terrain.GetExactGroundLevel(pos.X+sign*perp.X*20.0f, + pos.Y+sign*perp.Y*20.0f) - m_WaterHeight; float localOutmost = -2.0f; while (localOutmost < 0.0f) { - float depth = terrain->GetExactGroundLevel(pos.X+sign*perp.X*localOutmost, pos.Y+sign*perp.Y*localOutmost) - m_WaterHeight; + const float depth = terrain.GetExactGroundLevel( + pos.X+sign*perp.X*localOutmost, + pos.Y+sign*perp.Y*localOutmost) - m_WaterHeight; if (depth < 0.0f || depth > 0.6f) localOutmost += 0.2f; else break; } outmost += localOutmost; } if (width < 5) { j += 6; continue; } outmost /= width; if (outmost > -0.5f) { j += 3; continue; } outmost = -2.5f + outmost * m_Waviness/10.0f; avgDepth /= width; if (avgDepth > -1.3f) { j += 3; continue; } // we passed the checks, we can create a wave of size "width". std::unique_ptr shoreWave = std::make_unique(); vertices.clear(); vertices.reserve(9 * width); shoreWave->m_Width = width; shoreWave->m_TimeDiff = diff; diff += (rand() % 100) / 25.0f + 4.0f; for (u16 a = 0; a < width;++a) { perp = CVector2D(0,0); int nb = 0; CVector2D pos = CoastalPointsChains[i][j+a].position; CVector2D posPlus; CVector2D posMinus; if (a > 0) { ++nb; posMinus = CoastalPointsChains[i][j+a-1].position; perp += pos-posMinus; } if (a < waveSizes-1) { ++nb; posPlus = CoastalPointsChains[i][j+a+1].position; perp += posPlus-pos; } perp /= nb; perp = CVector2D(-perp.Y,perp.X).Normalized(); SWavesVertex point[9]; float baseHeight = 0.04f; float halfWidth = (width-1.0f)/2.0f; float sideNess = sqrtf(Clamp( (halfWidth - fabsf(a - halfWidth)) / 3.0f, 0.0f, 1.0f)); point[0].m_UV[0] = a; point[0].m_UV[1] = 8; point[1].m_UV[0] = a; point[1].m_UV[1] = 7; point[2].m_UV[0] = a; point[2].m_UV[1] = 6; point[3].m_UV[0] = a; point[3].m_UV[1] = 5; point[4].m_UV[0] = a; point[4].m_UV[1] = 4; point[5].m_UV[0] = a; point[5].m_UV[1] = 3; point[6].m_UV[0] = a; point[6].m_UV[1] = 2; point[7].m_UV[0] = a; point[7].m_UV[1] = 1; point[8].m_UV[0] = a; point[8].m_UV[1] = 0; point[0].m_PerpVect = perp; point[1].m_PerpVect = perp; point[2].m_PerpVect = perp; point[3].m_PerpVect = perp; point[4].m_PerpVect = perp; point[5].m_PerpVect = perp; point[6].m_PerpVect = perp; point[7].m_PerpVect = perp; point[8].m_PerpVect = perp; static const float perpT1[9] = { 6.0f, 6.05f, 6.1f, 6.2f, 6.3f, 6.4f, 6.5f, 6.6f, 9.7f }; static const float perpT2[9] = { 2.0f, 2.1f, 2.2f, 2.3f, 2.4f, 3.0f, 3.3f, 3.6f, 9.5f }; static const float perpT3[9] = { 1.1f, 0.7f, -0.2f, 0.0f, 0.6f, 1.3f, 2.2f, 3.6f, 9.0f }; static const float perpT4[9] = { 2.0f, 2.1f, 1.2f, 1.5f, 1.7f, 1.9f, 2.7f, 3.8f, 9.0f }; static const float heightT1[9] = { 0.0f, 0.2f, 0.5f, 0.8f, 0.9f, 0.85f, 0.6f, 0.2f, 0.0 }; static const float heightT2[9] = { -0.8f, -0.4f, 0.0f, 0.1f, 0.1f, 0.03f, 0.0f, 0.0f, 0.0 }; static const float heightT3[9] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0 }; for (size_t t = 0; t < 9; ++t) { - float terrHeight = 0.05f + terrain->GetExactGroundLevel(pos.X+sign*perp.X*(perpT1[t]+outmost), + const float terrHeight = 0.05f + terrain.GetExactGroundLevel( + pos.X+sign*perp.X*(perpT1[t]+outmost), pos.Y+sign*perp.Y*(perpT1[t]+outmost)); point[t].m_BasePosition = CVector3D(pos.X+sign*perp.X*(perpT1[t]+outmost), baseHeight + heightT1[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT1[t]+outmost)); } for (size_t t = 0; t < 9; ++t) { - float terrHeight = 0.05f + terrain->GetExactGroundLevel(pos.X+sign*perp.X*(perpT2[t]+outmost), + const float terrHeight = 0.05f + terrain.GetExactGroundLevel( + pos.X+sign*perp.X*(perpT2[t]+outmost), pos.Y+sign*perp.Y*(perpT2[t]+outmost)); point[t].m_ApexPosition = CVector3D(pos.X+sign*perp.X*(perpT2[t]+outmost), baseHeight + heightT1[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT2[t]+outmost)); } for (size_t t = 0; t < 9; ++t) { - float terrHeight = 0.05f + terrain->GetExactGroundLevel(pos.X+sign*perp.X*(perpT3[t]+outmost*sideNess), + const float terrHeight = 0.05f + terrain.GetExactGroundLevel( + pos.X+sign*perp.X*(perpT3[t]+outmost*sideNess), pos.Y+sign*perp.Y*(perpT3[t]+outmost*sideNess)); point[t].m_SplashPosition = CVector3D(pos.X+sign*perp.X*(perpT3[t]+outmost*sideNess), baseHeight + heightT2[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT3[t]+outmost*sideNess)); } for (size_t t = 0; t < 9; ++t) { - float terrHeight = 0.05f + terrain->GetExactGroundLevel(pos.X+sign*perp.X*(perpT4[t]+outmost), + const float terrHeight = 0.05f + terrain.GetExactGroundLevel( + pos.X+sign*perp.X*(perpT4[t]+outmost), pos.Y+sign*perp.Y*(perpT4[t]+outmost)); point[t].m_RetreatPosition = CVector3D(pos.X+sign*perp.X*(perpT4[t]+outmost), baseHeight + heightT3[t]*sideNess + std::max(m_WaterHeight,terrHeight), pos.Y+sign*perp.Y*(perpT4[t]+outmost)); } vertices.push_back(point[8]); vertices.push_back(point[7]); vertices.push_back(point[6]); vertices.push_back(point[5]); vertices.push_back(point[4]); vertices.push_back(point[3]); vertices.push_back(point[2]); vertices.push_back(point[1]); vertices.push_back(point[0]); shoreWave->m_AABB += point[8].m_SplashPosition; shoreWave->m_AABB += point[8].m_BasePosition; shoreWave->m_AABB += point[0].m_SplashPosition; shoreWave->m_AABB += point[0].m_BasePosition; shoreWave->m_AABB += point[4].m_ApexPosition; } if (sign == 1) { // Let's do some fancy reversing. reversed.clear(); reversed.reserve(vertices.size()); for (int a = width - 1; a >= 0; --a) { for (size_t t = 0; t < 9; ++t) reversed.push_back(vertices[a * 9 + t]); } std::swap(vertices, reversed); } j += width/2-1; shoreWave->m_VBVertices = g_VBMan.AllocateChunk( sizeof(SWavesVertex), vertices.size(), Renderer::Backend::IBuffer::Type::VERTEX, false, nullptr, CVertexBufferManager::Group::WATER); shoreWave->m_VBVertices->m_Owner->UpdateChunkVertices(shoreWave->m_VBVertices.Get(), &vertices[0]); m_ShoreWaves.emplace_back(std::move(shoreWave)); } } } void WaterManager::RenderWaves( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CFrustum& frustrum) { if (!m_WaterFancyEffects) return; m_WaveTex->UploadBackendTextureIfNeeded(deviceCommandContext); m_FoamTex->UploadBackendTextureIfNeeded(deviceCommandContext); GPU_SCOPED_LABEL(deviceCommandContext, "Render Waves"); Renderer::Backend::IFramebuffer* framebuffer = m_FancyEffectsFramebuffer.get(); deviceCommandContext->BeginFramebufferPass(framebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = framebuffer->GetWidth(); viewportRect.height = framebuffer->GetHeight(); deviceCommandContext->SetViewports(1, &viewportRect); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_water_waves); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_waveTex), m_WaveTex->GetBackendTexture()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_foamTex), m_FoamTex->GetBackendTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_time), static_cast(m_WaterTexTimer)); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); for (size_t a = 0; a < m_ShoreWaves.size(); ++a) { if (!frustrum.IsBoxVisible(m_ShoreWaves[a]->m_AABB)) continue; CVertexBuffer::VBChunk* VBchunk = m_ShoreWaves[a]->m_VBVertices.Get(); ENSURE(!VBchunk->m_Owner->GetBuffer()->IsDynamic()); ENSURE(!m_ShoreWavesVBIndices->m_Owner->GetBuffer()->IsDynamic()); const uint32_t stride = sizeof(SWavesVertex); const uint32_t firstVertexOffset = VBchunk->m_Index * stride; deviceCommandContext->SetVertexInputLayout(m_ShoreVertexInputLayout); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_translation), m_ShoreWaves[a]->m_TimeDiff); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_width), static_cast(m_ShoreWaves[a]->m_Width)); deviceCommandContext->SetVertexBuffer( 0, VBchunk->m_Owner->GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_ShoreWavesVBIndices->m_Owner->GetBuffer()); const uint32_t indexCount = (m_ShoreWaves[a]->m_Width - 1) * (7 * 6); deviceCommandContext->DrawIndexed(m_ShoreWavesVBIndices->m_Index, indexCount, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_WaterTris += indexCount / 3; } deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); } void WaterManager::RecomputeWaterData() { if (!m_MapSize) return; RecomputeDistanceHeightmap(); RecomputeWindStrength(); CreateWaveMeshes(); } /////////////////////////////////////////////////////////////////// // Calculate the strength of the wind at a given point on the map. void WaterManager::RecomputeWindStrength() { if (m_MapSize <= 0) return; if (!m_WindStrength) m_WindStrength = std::make_unique(m_MapSize * m_MapSize); - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - if (!terrain || !terrain->GetHeightMap()) + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + if (!terrain.GetHeightMap()) return; CVector2D windDir = CVector2D(cos(m_WindAngle), sin(m_WindAngle)); int stepSize = 10; ssize_t windX = -round(stepSize * windDir.X); ssize_t windY = -round(stepSize * windDir.Y); struct SWindPoint { SWindPoint(size_t x, size_t y, float strength) : X(x), Y(y), windStrength(strength) {} ssize_t X; ssize_t Y; float windStrength; }; std::vector startingPoints; std::vector> movement; // Every increment, move each starting point by all of these. // Compute starting points (one or two edges of the map) and how much to move each computation increment. if (fabs(windDir.X) < 0.01f) { movement.emplace_back(0, windY > 0.f ? 1 : -1); startingPoints.reserve(m_MapSize); size_t start = windY > 0 ? 0 : m_MapSize - 1; for (size_t x = 0; x < m_MapSize; ++x) startingPoints.emplace_back(x, start, 0.f); } else if (fabs(windDir.Y) < 0.01f) { movement.emplace_back(windX > 0.f ? 1 : - 1, 0); startingPoints.reserve(m_MapSize); size_t start = windX > 0 ? 0 : m_MapSize - 1; for (size_t z = 0; z < m_MapSize; ++z) startingPoints.emplace_back(start, z, 0.f); } else { startingPoints.reserve(m_MapSize * 2); // Points along X. size_t start = windY > 0 ? 0 : m_MapSize - 1; for (size_t x = 0; x < m_MapSize; ++x) startingPoints.emplace_back(x, start, 0.f); // Points along Z, avoid repeating the corner point. start = windX > 0 ? 0 : m_MapSize - 1; if (windY > 0) for (size_t z = 1; z < m_MapSize; ++z) startingPoints.emplace_back(start, z, 0.f); else for (size_t z = 0; z < m_MapSize-1; ++z) startingPoints.emplace_back(start, z, 0.f); // Compute movement array. movement.reserve(std::max(std::abs(windX),std::abs(windY))); while (windX != 0 || windY != 0) { std::pair move = { windX == 0 ? 0 : windX > 0 ? +1 : -1, windY == 0 ? 0 : windY > 0 ? +1 : -1 }; windX -= move.first; windY -= move.second; movement.push_back(move); } } // We have all starting points ready, move them all until the map is covered. for (SWindPoint& point : startingPoints) { // Starting velocity is 1.0 unless in shallow water. m_WindStrength[point.Y * m_MapSize + point.X] = 1.f; - float depth = m_WaterHeight - terrain->GetVertexGroundLevel(point.X, point.Y); + const float depth = m_WaterHeight - terrain.GetVertexGroundLevel(point.X, point.Y); if (depth > 0.f && depth < 2.f) m_WindStrength[point.Y * m_MapSize + point.X] = depth / 2.f; point.windStrength = m_WindStrength[point.Y * m_MapSize + point.X]; bool onMap = true; while (onMap) for (size_t step = 0; step < movement.size(); ++step) { // Move wind speed towards the mean. point.windStrength = 0.15f + point.windStrength * 0.85f; // Adjust speed based on height difference, a positive height difference slowly increases speed (simulate venturi effect) // and a lower height reduces speed (wind protection from hills/...) - float heightDiff = std::max(m_WaterHeight, terrain->GetVertexGroundLevel(point.X + movement[step].first, point.Y + movement[step].second)) - - std::max(m_WaterHeight, terrain->GetVertexGroundLevel(point.X, point.Y)); + const float heightDiff = std::max(m_WaterHeight, terrain.GetVertexGroundLevel( + point.X + movement[step].first, point.Y + movement[step].second)) - + std::max(m_WaterHeight, terrain.GetVertexGroundLevel(point.X, point.Y)); if (heightDiff > 0.f) point.windStrength = std::min(2.f, point.windStrength + std::min(4.f, heightDiff) / 40.f); else point.windStrength = std::max(0.f, point.windStrength + std::max(-4.f, heightDiff) / 5.f); point.X += movement[step].first; point.Y += movement[step].second; if (point.X < 0 || point.X >= static_cast(m_MapSize) || point.Y < 0 || point.Y >= static_cast(m_MapSize)) { onMap = false; break; } m_WindStrength[point.Y * m_MapSize + point.X] = point.windStrength; } } // TODO: should perhaps blur a little, or change the above code to incorporate neighboring tiles a bit. } //////////////////////////////////////////////////////////////////////// // TODO: This will always recalculate for now void WaterManager::SetMapSize(size_t size) { // TODO: Im' blindly trusting the user here. m_MapSize = size; m_NeedInfoUpdate = true; m_updatei0 = 0; m_updatei1 = size; m_updatej0 = 0; m_updatej1 = size; m_DistanceHeightmap.reset(); m_WindStrength.reset(); } //////////////////////////////////////////////////////////////////////// // This will set the bools properly void WaterManager::UpdateQuality() { if (g_RenderingOptions.GetWaterEffects() != m_WaterEffects) { m_WaterEffects = g_RenderingOptions.GetWaterEffects(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterFancyEffects() != m_WaterFancyEffects) { m_WaterFancyEffects = g_RenderingOptions.GetWaterFancyEffects(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterRealDepth() != m_WaterRealDepth) { m_WaterRealDepth = g_RenderingOptions.GetWaterRealDepth(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterRefraction() != m_WaterRefraction) { m_WaterRefraction = g_RenderingOptions.GetWaterRefraction(); m_NeedsReloading = true; } if (g_RenderingOptions.GetWaterReflection() != m_WaterReflection) { m_WaterReflection = g_RenderingOptions.GetWaterReflection(); m_NeedsReloading = true; } } bool WaterManager::WillRenderFancyWater() const { return m_RenderWater && m_Device->GetBackend() != Renderer::Backend::Backend::GL_ARB && g_RenderingOptions.GetWaterEffects(); } size_t WaterManager::GetCurrentTextureIndex(const double& period) const { ENSURE(period > 0.0); return static_cast(m_WaterTexTimer * ARRAY_SIZE(m_WaterTexture) / period) % ARRAY_SIZE(m_WaterTexture); } size_t WaterManager::GetNextTextureIndex(const double& period) const { ENSURE(period > 0.0); return (GetCurrentTextureIndex(period) + 1) % ARRAY_SIZE(m_WaterTexture); } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/CameraCtrlHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/CameraCtrlHandlers.cpp (revision 27860) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/CameraCtrlHandlers.cpp (revision 27861) @@ -1,261 +1,261 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2023 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 "../GameLoop.h" #include "../View.h" #include "maths/MathUtil.h" #include "maths/Vector3D.h" #include "maths/Quaternion.h" #include "ps/Game.h" #include "renderer/Renderer.h" #include "graphics/GameView.h" #include "graphics/CinemaManager.h" #include "ps/World.h" #include "graphics/Terrain.h" #include namespace AtlasMessage { MESSAGEHANDLER(CameraReset) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) return; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CVector3D target; - if (!g_Game->GetWorld()->GetTerrain()->IsOnMap(focus.X, focus.Z)) + if (!g_Game->GetWorld()->GetTerrain().IsOnMap(focus.X, focus.Z)) { target = CVector3D( - g_Game->GetWorld()->GetTerrain()->GetMaxX()/2.f, + g_Game->GetWorld()->GetTerrain().GetMaxX()/2.f, focus.Y, - g_Game->GetWorld()->GetTerrain()->GetMaxZ()/2.f); + g_Game->GetWorld()->GetTerrain().GetMaxZ()/2.f); } else { target = focus; } g_Game->GetView()->ResetCameraTarget(target); UNUSED2(msg); } MESSAGEHANDLER(ScrollConstant) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) return; if (msg->dir < 0 || msg->dir > 5) { debug_warn(L"ScrollConstant: invalid direction"); } else { g_AtlasGameLoop->input.scrollSpeed[msg->dir] = msg->speed; } } // TODO: change all these g_Game->...GetCamera() bits to use the current AtlasView's // camera instead. MESSAGEHANDLER(Scroll) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) // TODO: do this better (probably a separate AtlasView class for cinematics) return; static CVector3D targetPos; static float targetDistance = 0.f; CMatrix3D& camera = g_Game->GetView()->GetCamera()->m_Orientation; static CVector3D lastCameraPos = camera.GetTranslation(); // Ensure roughly correct motion when dragging is combined with other // movements. if (lastCameraPos != camera.GetTranslation()) targetPos += camera.GetTranslation() - lastCameraPos; // General operation: // // When selecting a target point to drag, remember targetPos (a world-space // point on the terrain, underneath the mouse) and targetDistance (from the // camera to the target point). // // When dragging to a different position, the target point should remain // under the moved mouse; so calculate the ray through the camera and mouse, // multiply by targetDistance and add to targetPos, resulting in the required // camera position. if (msg->type == eScrollType::FROM) { targetPos = msg->pos->GetWorldSpace(); targetDistance = (targetPos - camera.GetTranslation()).Length(); } else if (msg->type == eScrollType::TO) { CVector3D origin, dir; float x, y; msg->pos->GetScreenSpace(x, y); g_Game->GetView()->GetCamera()->BuildCameraRay((int)x, (int)y, origin, dir); dir *= targetDistance; camera.Translate(targetPos - dir - origin); g_Game->GetView()->GetCamera()->UpdateFrustum(); } else { debug_warn(L"Scroll: Invalid type"); } lastCameraPos = camera.GetTranslation(); } MESSAGEHANDLER(SmoothZoom) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) return; g_AtlasGameLoop->input.zoomDelta += msg->amount; } MESSAGEHANDLER(RotateAround) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) return; static CVector3D focusPos; static float lastX = 0.f, lastY = 0.f; CMatrix3D& camera = g_Game->GetView()->GetCamera()->m_Orientation; if (msg->type == eRotateAroundType::FROM) { msg->pos->GetScreenSpace(lastX, lastY); // get mouse position focusPos = msg->pos->GetWorldSpace(); // get point on terrain under mouse } else if (msg->type == eRotateAroundType::TO) { float x, y; msg->pos->GetScreenSpace(x, y); // get mouse position // Rotate around X and Y axes by amounts depending on the mouse delta float rotX = 6.f * (y-lastY) / g_Renderer.GetHeight(); float rotY = 6.f * (x-lastX) / g_Renderer.GetWidth(); CQuaternion q0, q1; q0.FromAxisAngle(camera.GetLeft(), -rotX); q1.FromAxisAngle(CVector3D(0.f, 1.f, 0.f), rotY); CQuaternion q = q0*q1; CVector3D origin = camera.GetTranslation(); CVector3D offset = q.Rotate(origin - focusPos); q *= camera.GetRotation(); q.Normalize(); // to avoid things blowing up when turning upside-down, for some reason I don't understand q.ToMatrix(camera); // Make sure up is still pointing up, regardless of any rounding errors. // (Maybe this distorts the camera in other ways, but at least the errors // are far less noticeable to me.) camera._21 = 0.f; // (_21 = Y component returned by GetLeft()) camera.Translate(focusPos + offset); g_Game->GetView()->GetCamera()->UpdateFrustum(); lastX = x; lastY = y; } else { debug_warn(L"RotateAround: Invalid type"); } } MESSAGEHANDLER(LookAt) { // TODO: different camera depending on msg->view CCamera& camera = AtlasView::GetView_Actor()->GetCamera(); CVector3D tgt = msg->target->GetWorldSpace(); CVector3D eye = msg->pos->GetWorldSpace(); tgt.Y = -tgt.Y; // ??? why is this needed? eye.Y = -eye.Y; // ??? // Based on http://www.opengl.org/documentation/specs/man_pages/hardcopy/GL/html/glu/lookat.html CVector3D f = tgt - eye; f.Normalize(); CVector3D s = f.Cross(CVector3D(0, 1, 0)); CVector3D u = s.Cross(f); s.Normalize(); // (not in that man page, but necessary for correctness, and done by Mesa) u.Normalize(); CMatrix3D M ( s[0], s[1], s[2], 0, u[0], u[1], u[2], 0, -f[0], -f[1], -f[2], 0, 0, 0, 0, 1 ); camera.m_Orientation = M.GetTranspose(); camera.m_Orientation.Translate(-eye); camera.UpdateFrustum(); } QUERYHANDLER(GetView) { if (!g_Game) return; CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); sCameraInfo info; info.pX = focus.X; info.pY = focus.Y; info.pZ = focus.Z; CQuaternion quatRot = g_Game->GetView()->GetCamera()->GetOrientation().GetRotation(); quatRot.Normalize(); CVector3D rotation = quatRot.ToEulerAngles(); info.rX = RADTODEG(rotation.X); info.rY = RADTODEG(rotation.Y); info.rZ = RADTODEG(rotation.Z); msg->info = info; } MESSAGEHANDLER(SetView) { if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) return; CGameView* view = g_Game->GetView(); view->ResetCameraTarget(view->GetCamera()->GetFocus()); sCameraInfo cam = msg->info; view->ResetCameraTarget(CVector3D(cam.pX, cam.pY, cam.pZ)); // TODO: Rotation } } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp (revision 27860) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/ElevationHandlers.cpp (revision 27861) @@ -1,459 +1,459 @@ /* Copyright (C) 2022 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/RenderableObject.h" #include "graphics/Terrain.h" #include "graphics/UnitManager.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/World.h" #include "maths/MathUtil.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpTerrain.h" #include "../Brushes.h" #include "../DeltaArray.h" namespace AtlasMessage { class TerrainArray : public DeltaArray2D { public: void Init() { - m_Heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); - m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide(); + m_Heightmap = g_Game->GetWorld()->GetTerrain().GetHeightMap(); + m_VertsPerSide = g_Game->GetWorld()->GetTerrain().GetVerticesPerSide(); } void RaiseVertex(ssize_t x, ssize_t y, int amount) { // Ignore out-of-bounds vertices if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; set(x, y, static_cast(Clamp(get(x,y) + amount, 0, 65535))); } void MoveVertexTowards(ssize_t x, ssize_t y, int target, int amount) { if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; int h = get(x,y); if (h < target) h = std::min(target, h + amount); else if (h > target) h = std::max(target, h - amount); else return; set(x, y, static_cast(Clamp(h, 0, 65535))); } void SetVertex(ssize_t x, ssize_t y, u16 value) { if (size_t(x) >= size_t(m_VertsPerSide) || size_t(y) >= size_t(m_VertsPerSide)) return; set(x,y, value); } u16 GetVertex(ssize_t x, ssize_t y) { return get(Clamp(x, 0, m_VertsPerSide - 1), Clamp(y, 0, m_VertsPerSide - 1)); } protected: u16 getOld(ssize_t x, ssize_t y) { return m_Heightmap[y*m_VertsPerSide + x]; } void setNew(ssize_t x, ssize_t y, const u16& val) { m_Heightmap[y*m_VertsPerSide + x] = val; } u16* m_Heightmap; ssize_t m_VertsPerSide; }; ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(AlterElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cAlterElevation() { m_TerrainDelta.Init(); } void MakeDirty() { - g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetTerrain().MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { // TODO: proper variable raise amount (store floats in terrain delta array?) float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.RaiseVertex(x0+dx, y0+dy, (int)(amount*b)); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cAlterElevation* 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(AlterElevation) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(SmoothElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cSmoothElevation() { m_TerrainDelta.Init(); } void MakeDirty() { - g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetTerrain().MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); if (g_CurrentBrush.m_H > 2) { std::vector terrainDeltas; ssize_t num = (g_CurrentBrush.m_H - 2) * (g_CurrentBrush.m_W - 2); terrainDeltas.resize(num); // For each vertex, compute the average of the 9 adjacent vertices for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float delta = m_TerrainDelta.GetVertex(x0+dx, y0+dy) / 9.0f; ssize_t x1_min = std::max((ssize_t)1, dx - 1); ssize_t x1_max = std::min(dx + 1, g_CurrentBrush.m_W - 2); ssize_t y1_min = std::max((ssize_t)1, dy - 1); ssize_t y1_max = std::min(dy + 1, g_CurrentBrush.m_H - 2); for (ssize_t yy = y1_min; yy <= y1_max; ++yy) { for (ssize_t xx = x1_min; xx <= x1_max; ++xx) { ssize_t index = (yy-1)*(g_CurrentBrush.m_W-2) + (xx-1); terrainDeltas[index] += delta; } } } } // Move each vertex towards the computed average of its neighbours for (ssize_t dy = 1; dy < g_CurrentBrush.m_H - 1; ++dy) { for (ssize_t dx = 1; dx < g_CurrentBrush.m_W - 1; ++dx) { ssize_t index = (dy-1)*(g_CurrentBrush.m_W-2) + (dx-1); float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.MoveVertexTowards(x0+dx, y0+dy, (int)terrainDeltas[index], (int)(amount*b)); } } } m_i0 = x0; m_j0 = y0; 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(cSmoothElevation* 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(SmoothElevation) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(FlattenElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cFlattenElevation() { m_TerrainDelta.Init(); } void MakeDirty() { - g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetTerrain().MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t xc, yc; g_CurrentBrush.GetCentre(xc, yc); u16 height = m_TerrainDelta.GetVertex(xc, yc); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float b = g_CurrentBrush.Get(dx, dy); if (b) m_TerrainDelta.MoveVertexTowards(x0+dx, y0+dy, height, 1 + (int)(b*amount)); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cFlattenElevation* 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(FlattenElevation) BEGIN_COMMAND(PikeElevation) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cPikeElevation() { m_TerrainDelta.Init(); } void MakeDirty() { - g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); + g_Game->GetWorld()->GetTerrain().MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); g_Game->GetWorld()->GetUnitManager().MakeTerrainDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_VERTICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { int amount = (int)msg->amount; // If the framerate is very high, 'amount' is often very // small (even zero) so the integer truncation is significant static float roundingError = 0.0; roundingError += msg->amount - (float)amount; if (roundingError >= 1.f) { amount += (int)roundingError; roundingError -= (float)(int)roundingError; } static CVector3D previousPosition; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(previousPosition); previousPosition = g_CurrentBrush.m_Centre; ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); float h = ((float) g_CurrentBrush.m_H - 1) / 2.f; for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { float b = g_CurrentBrush.Get(dx, dy); if (b) { float x = (float)dx - ((float)g_CurrentBrush.m_H - 1) / 2.f; float y = (float)dy - ((float)g_CurrentBrush.m_W - 1) / 2.f; float distance = Clamp(1 - static_cast(sqrt(x * x + y * y)) / h, 0.01f, 1.0f); distance *= distance; m_TerrainDelta.RaiseVertex(x0 + dx, y0 + dy, (int)(amount * distance)); } } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W; m_j1 = y0 + g_CurrentBrush.m_H; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cPikeElevation* 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(PikeElevation) } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 27860) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 27861) @@ -1,643 +1,643 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 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 "../MessagePasser.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MapIO.h" #include "graphics/MapWriter.h" #include "graphics/MiniMapTexture.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "lib/bits.h" #include "lib/file/vfs/vfs_path.h" #include "lib/status.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/system/ParamNode.h" #ifdef _MSC_VER # pragma warning(disable: 4458) // Declaration hides class member. #endif namespace { void InitGame() { if (g_Game) { delete g_Game; g_Game = NULL; } g_Game = new CGame(false); // Default to player 1 for playtesting g_Game->SetPlayerID(1); } void StartGame(JS::MutableHandleValue attrs) { g_Game->StartGame(attrs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); // Disable fog-of-war - this must be done before starting the game, // as visual actors cache their visibility state on first render. CmpPtr cmpRangeManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(-1, true); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } } namespace AtlasMessage { QUERYHANDLER(GenerateMap) { try { InitGame(); // Random map const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); Script::ParseJSON(rq, *msg->settings, &settings); Script::SetProperty(rq, settings, "mapType", "random"); JS::RootedValue attrs(rq.cx); Script::CreateObject( rq, &attrs, "mapType", "random", "script", *msg->filename, "settings", settings); StartGame(&attrs); msg->status = 0; } catch (PSERROR_Game_World_MapLoadFailed&) { // Cancel loading LDR_Cancel(); // Since map generation failed and we don't know why, use the blank map as a fallback InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); // Set up 8-element array of empty objects to satisfy init JS::RootedValue playerData(rq.cx); Script::CreateArray(rq, &playerData); for (int i = 0; i < 8; ++i) { JS::RootedValue player(rq.cx); Script::CreateObject(rq, &player); Script::SetPropertyInt(rq, playerData, i, player); } JS::RootedValue settings(rq.cx); Script::CreateObject( rq, &settings, "mapType", "scenario", "PlayerData", playerData); JS::RootedValue attrs(rq.cx); Script::CreateObject( rq, &attrs, "mapType", "scenario", "map", "maps/scenarios/_default", "settings", settings); StartGame(&attrs); msg->status = -1; } } MESSAGEHANDLER(LoadMap) { InitGame(); const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); // Scenario CStrW map = *msg->filename; CStrW mapBase = map.BeforeLast(L".pmp"); // strip the file extension, if any JS::RootedValue attrs(rq.cx); Script::CreateObject( rq, &attrs, "mapType", "scenario", "map", mapBase); StartGame(&attrs); } MESSAGEHANDLER(ImportHeightmap) { std::vector heightmap_source; if (LoadHeightmapImageOs(*msg->filename, heightmap_source) != INFO::OK) { LOGERROR("Failed to decode heightmap."); return; } // resize terrain to heightmap size // Notice that the number of tiles/pixels per side of the heightmap image is // one less than the number of vertices per side of the heightmap. - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); + CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); const ssize_t newSize = (sqrt(heightmap_source.size()) - 1) / PATCH_SIZE; - const ssize_t offset = (newSize - terrain->GetPatchesPerSide()) / 2; - terrain->ResizeAndOffset(newSize, offset, offset); + const ssize_t offset = (newSize - terrain.GetPatchesPerSide()) / 2; + terrain.ResizeAndOffset(newSize, offset, offset); // copy heightmap data into map - u16* heightmap = g_Game->GetWorld()->GetTerrain()->GetHeightMap(); - ENSURE(heightmap_source.size() == (std::size_t) SQR(g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide())); + u16* const heightmap = g_Game->GetWorld()->GetTerrain().GetHeightMap(); + ENSURE(heightmap_source.size() == (std::size_t) SQR(g_Game->GetWorld()->GetTerrain().GetVerticesPerSide())); std::copy(heightmap_source.begin(), heightmap_source.end(), heightmap); // update simulation CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); g_Game->GetView()->GetLOSTexture().MakeDirty(); } MESSAGEHANDLER(SaveMap) { CMapWriter writer; VfsPath pathname = VfsPath(*msg->filename).ChangeExtension(L".pmp"); writer.SaveMap(pathname, - g_Game->GetWorld()->GetTerrain(), + &g_Game->GetWorld()->GetTerrain(), &g_Renderer.GetSceneRenderer().GetWaterManager(), &g_Renderer.GetSceneRenderer().GetSkyManager(), &g_LightEnv, g_Game->GetView()->GetCamera(), g_Game->GetView()->GetCinema(), &g_Renderer.GetPostprocManager(), g_Game->GetSimulation2()); } QUERYHANDLER(GetMapSettings) { msg->settings = g_Game->GetSimulation2()->GetMapSettingsString(); } BEGIN_COMMAND(SetMapSettings) { std::string m_OldSettings, m_NewSettings; void SetSettings(const std::string& settings) { g_Game->GetSimulation2()->SetMapSettings(settings); } void Do() { m_OldSettings = g_Game->GetSimulation2()->GetMapSettingsString(); m_NewSettings = *msg->settings; SetSettings(m_NewSettings); } // TODO: we need some way to notify the Atlas UI when the settings are changed // externally, otherwise this will have no visible effect void Undo() { // SetSettings(m_OldSettings); } void Redo() { // SetSettings(m_NewSettings); } void MergeIntoPrevious(cSetMapSettings* prev) { prev->m_NewSettings = m_NewSettings; } }; END_COMMAND(SetMapSettings) MESSAGEHANDLER(LoadPlayerSettings) { g_Game->GetSimulation2()->LoadPlayerSettings(msg->newplayers); } QUERYHANDLER(GetMapSizes) { msg->sizes = g_Game->GetSimulation2()->GetMapSizes(); } QUERYHANDLER(RasterizeMinimap) { // TODO: remove the code duplication of the rasterization algorithm, using // CMinimap version. - const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - const ssize_t dimension = terrain->GetVerticesPerSide() - 1; + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + const ssize_t dimension = terrain.GetVerticesPerSide() - 1; const ssize_t bpp = 24; const ssize_t imageDataSize = dimension * dimension * (bpp / 8); std::vector imageBytes(imageDataSize); float shallowPassageHeight = CMiniMapTexture::GetShallowPassageHeight(); ssize_t w = dimension; ssize_t h = dimension; const float waterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight; for (ssize_t j = 0; j < h; ++j) { // Work backwards to vertically flip the image. ssize_t position = 3 * (h - j - 1) * dimension; for (ssize_t i = 0; i < w; ++i) { - float avgHeight = (terrain->GetVertexGroundLevel(i, j) - + terrain->GetVertexGroundLevel(i + 1, j) - + terrain->GetVertexGroundLevel(i, j + 1) - + terrain->GetVertexGroundLevel(i + 1, j + 1) + const float avgHeight = (terrain.GetVertexGroundLevel(i, j) + + terrain.GetVertexGroundLevel(i + 1, j) + + terrain.GetVertexGroundLevel(i, j + 1) + + terrain.GetVertexGroundLevel(i + 1, j + 1) ) / 4.0f; if (avgHeight < waterHeight && avgHeight > waterHeight - shallowPassageHeight) { // shallow water imageBytes[position++] = 0x70; imageBytes[position++] = 0x98; imageBytes[position++] = 0xc0; } else if (avgHeight < waterHeight) { // Set water as constant color for consistency on different maps imageBytes[position++] = 0x50; imageBytes[position++] = 0x78; imageBytes[position++] = 0xa0; } else { u32 color = std::numeric_limits::max(); - u32 hmap = static_cast(terrain->GetHeightMap()[j * dimension + i]) >> 8; + const u32 hmap = static_cast(terrain.GetHeightMap()[j * dimension + i]) >> 8; float scale = hmap / 3.0f + 170.0f / 255.0f; - CMiniPatch* mp = terrain->GetTile(i, j); + CMiniPatch* const mp = terrain.GetTile(i, j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) color = tex->GetBaseColor(); } // Convert imageBytes[position++] = static_cast(static_cast(color & 0xff) * scale); imageBytes[position++] = static_cast(static_cast((color >> 8) & 0xff) * scale); imageBytes[position++] = static_cast(static_cast((color >> 16) & 0xff) * scale); } } } msg->imageBytes = std::move(imageBytes); msg->dimension = dimension; } QUERYHANDLER(GetRMSData) { msg->data = g_Game->GetSimulation2()->GetRMSData(); } QUERYHANDLER(GetCurrentMapSize) { - msg->size = g_Game->GetWorld()->GetTerrain()->GetTilesPerSide(); + msg->size = g_Game->GetWorld()->GetTerrain().GetTilesPerSide(); } BEGIN_COMMAND(ResizeMap) { bool Within(const CFixedVector3D& pos, const int centerX, const int centerZ, const int radius) { int dx = abs(pos.X.ToInt_RoundToZero() - centerX); if (dx > radius) return false; int dz = abs(pos.Z.ToInt_RoundToZero() - centerZ); if (dz > radius) return false; if (dx + dz <= radius) return true; return dx * dx + dz * dz <= radius * radius; } struct DeletedObject { entity_id_t entityId; CStr templateName; player_id_t owner; CFixedVector3D pos; CFixedVector3D rot; u32 actorSeed; }; ssize_t m_OldPatches, m_NewPatches; int m_OffsetX, m_OffsetY; u16* m_Heightmap; CPatch* m_Patches; std::vector m_DeletedObjects; std::vector> m_OldPositions; std::vector> m_NewPositions; cResizeMap() : m_Heightmap(nullptr), m_Patches(nullptr) { } ~cResizeMap() { delete[] m_Heightmap; delete[] m_Patches; } void MakeDirty() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); // The LOS texture won't normally get updated when running Atlas // (since there's no simulation updates), so explicitly dirty it g_Game->GetView()->GetLOSTexture().MakeDirty(); } void ResizeTerrain(ssize_t patches, int offsetX, int offsetY) { - CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); - terrain->ResizeAndOffset(patches, -offsetX, -offsetY); + CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + terrain.ResizeAndOffset(patches, -offsetX, -offsetY); } void DeleteObjects(const std::vector& deletedObjects) { for (const DeletedObject& deleted : deletedObjects) g_Game->GetSimulation2()->DestroyEntity(deleted.entityId); g_Game->GetSimulation2()->FlushDestroyedEntities(); } void RestoreObjects(const std::vector& deletedObjects) { CSimulation2& sim = *g_Game->GetSimulation2(); for (const DeletedObject& deleted : deletedObjects) { entity_id_t ent = sim.AddEntity(deleted.templateName.FromUTF8(), deleted.entityId); if (ent == INVALID_ENTITY) { LOGERROR("Failed to load entity template '%s'", deleted.templateName.c_str()); } else { CmpPtr cmpPosition(sim, deleted.entityId); if (cmpPosition) { cmpPosition->JumpTo(deleted.pos.X, deleted.pos.Z); cmpPosition->SetXZRotation(deleted.rot.X, deleted.rot.Z); cmpPosition->SetYRotation(deleted.rot.Y); } CmpPtr cmpOwnership(sim, deleted.entityId); if (cmpOwnership) cmpOwnership->SetOwner(deleted.owner); CmpPtr cmpVisual(sim, deleted.entityId); if (cmpVisual) cmpVisual->SetActorSeed(deleted.actorSeed); } } } void SetMovedEntitiesPosition(const std::vector>& movedObjects) { for (const std::pair& obj : movedObjects) { const entity_id_t id = obj.first; const CFixedVector3D position = obj.second; CmpPtr cmpPosition(*g_Game->GetSimulation2(), id); ENSURE(cmpPosition); cmpPosition->JumpTo(position.X, position.Z); } } void Do() { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); ENSURE(cmpTemplateManager); CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); if (!cmpTerrain) { m_OldPatches = m_NewPatches = 0; m_OffsetX = m_OffsetY = 0; } else { m_OldPatches = static_cast(cmpTerrain->GetTilesPerSide() / PATCH_SIZE); m_NewPatches = msg->tiles / PATCH_SIZE; m_OffsetX = msg->offsetX / PATCH_SIZE; // Need to flip direction of vertical offset, due to screen mapping order. m_OffsetY = -(msg->offsetY / PATCH_SIZE); CTerrain* terrain = cmpTerrain->GetCTerrain(); m_Heightmap = new u16[(m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1)]; std::copy_n(terrain->GetHeightMap(), (m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1), m_Heightmap); m_Patches = new CPatch[m_OldPatches * m_OldPatches]; for (ssize_t j = 0; j < m_OldPatches; ++j) for (ssize_t i = 0; i < m_OldPatches; ++i) { CPatch& src = *(terrain->GetPatch(i, j)); CPatch& dst = m_Patches[j * m_OldPatches + i]; std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); } } const int radiusInTerrainUnits = m_NewPatches * PATCH_SIZE * TERRAIN_TILE_SIZE / 2 * (1.f - 1e-6f); // Opposite direction offset, as we move the destination onto the source, not the source into the destination. const int mapCenterX = (m_OldPatches / 2 - m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; const int mapCenterZ = (m_OldPatches / 2 - m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; // The offset to move units by is opposite the direction the map is moved, and from the corner. const int offsetX = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE; const int offsetZ = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE; const CFixedVector3D offset = CFixedVector3D(fixed::FromInt(offsetX), fixed::FromInt(0), fixed::FromInt(offsetZ)); const CSimulation2::InterfaceListUnordered& ents = sim.GetEntitiesWithInterfaceUnordered(IID_Selectable); for (const std::pair& ent : ents) { const entity_id_t entityId = ent.first; CmpPtr cmpPosition(sim, entityId); if (cmpPosition && cmpPosition->IsInWorld() && Within(cmpPosition->GetPosition(), mapCenterX, mapCenterZ, radiusInTerrainUnits)) { CFixedVector3D position = cmpPosition->GetPosition(); m_NewPositions.emplace_back(entityId, position + offset); m_OldPositions.emplace_back(entityId, position); } else { DeletedObject deleted; deleted.entityId = entityId; deleted.templateName = cmpTemplateManager->GetCurrentTemplateName(entityId); // If the entity has a position, but the ending position is not valid; if (cmpPosition) { deleted.pos = cmpPosition->GetPosition(); deleted.rot = cmpPosition->GetRotation(); } CmpPtr cmpOwnership(sim, entityId); if (cmpOwnership) deleted.owner = cmpOwnership->GetOwner(); CmpPtr cmpVisual(sim, deleted.entityId); if (cmpVisual) deleted.actorSeed = cmpVisual->GetActorSeed(); m_DeletedObjects.push_back(deleted); } } DeleteObjects(m_DeletedObjects); ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); SetMovedEntitiesPosition(m_NewPositions); MakeDirty(); } void Undo() { if (m_Heightmap == nullptr || m_Patches == nullptr) { // If there previously was no data, just resize to old (probably not originally valid). ResizeTerrain(m_OldPatches, -m_OffsetX, -m_OffsetY); } else { CSimulation2& sim = *g_Game->GetSimulation2(); CmpPtr cmpTerrain(sim, SYSTEM_ENTITY); CTerrain* terrain = cmpTerrain->GetCTerrain(); terrain->Initialize(m_OldPatches, m_Heightmap); // Copy terrain data back. for (ssize_t j = 0; j < m_OldPatches; ++j) for (ssize_t i = 0; i < m_OldPatches; ++i) { CPatch& src = m_Patches[j * m_OldPatches + i]; CPatch& dst = *(terrain->GetPatch(i, j)); std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]); } } RestoreObjects(m_DeletedObjects); SetMovedEntitiesPosition(m_OldPositions); MakeDirty(); } void Redo() { DeleteObjects(m_DeletedObjects); ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY); SetMovedEntitiesPosition(m_NewPositions); MakeDirty(); } }; END_COMMAND(ResizeMap) QUERYHANDLER(VFSFileExists) { msg->exists = VfsFileExists(*msg->path); } QUERYHANDLER(VFSFileRealPath) { VfsPath pathname(*msg->path); if (pathname.empty()) return; OsPath realPathname; if (g_VFS->GetRealPath(pathname, realPathname) == INFO::OK) msg->realPath = realPathname.string(); } static Status AddToFilenames(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& filenames = *(std::vector*)cbData; filenames.push_back(pathname.string().c_str()); return INFO::OK; } QUERYHANDLER(GetMapList) { #define GET_FILE_LIST(path, list) \ std::vector list; \ vfs::ForEachFile(g_VFS, path, AddToFilenames, (uintptr_t)&list, L"*.xml", vfs::DIR_RECURSIVE); \ msg->list = list; GET_FILE_LIST(L"maps/scenarios/", scenarioFilenames); GET_FILE_LIST(L"maps/skirmishes/", skirmishFilenames); GET_FILE_LIST(L"maps/tutorials/", tutorialFilenames); #undef GET_FILE_LIST } QUERYHANDLER(GetVictoryConditionData) { msg->data = g_Game->GetSimulation2()->GetVictoryConditiondData(); } } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 27860) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/ObjectHandlers.cpp (revision 27861) @@ -1,1132 +1,1132 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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/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 { 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; + const 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); + 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 27860) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 27861) @@ -1,587 +1,590 @@ /* Copyright (C) 2023 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 "graphics/TextureManager.h" #include "ps/Game.h" #include "ps/World.h" #include "lib/tex/tex.h" #include "ps/Filesystem.h" #include "renderer/Renderer.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 { sTerrainTexturePreview MakeEmptyTerrainTexturePreview() { sTerrainTexturePreview preview{}; preview.name = std::wstring(); preview.loaded = false; preview.imageHeight = 0; preview.imageWidth = 0; preview.imageData = {}; return preview; } bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b) { return (wcscmp(a.name.c_str(), b.name.c_str()) < 0); } sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, size_t width, size_t height) { sTerrainTexturePreview preview; preview.name = tex->GetTag().FromUTF8(); const size_t previewBPP = 3; std::vector buffer(width * height * previewBPP); // 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. VfsPath texturePath; if (!tex->GetDiffuseTexturePath().empty()) { const VfsPath cachedTexturePath = g_Renderer.GetTextureManager().GetCachedPath( tex->GetDiffuseTexturePath()); if (g_VFS->GetFileInfo(cachedTexturePath, nullptr) == INFO::OK) texturePath = cachedTexturePath; else texturePath = tex->GetDiffuseTexturePath(); } std::shared_ptr fileData; size_t fileSize; Tex texture; const bool canUsePreview = !texturePath.empty() && g_VFS->LoadFile(texturePath, fileData, fileSize) == INFO::OK && texture.decode(fileData, fileSize) == INFO::OK && // Check that we can fit the texture into the preview size before any transform. texture.m_Width >= width && texture.m_Height >= height && // Transform to a single format that we can process. texture.transform_to((texture.m_Flags | TEX_MIPMAPS) & ~(TEX_DXT | TEX_GREY | TEX_BGR)) == INFO::OK && (texture.m_Bpp == 24 || texture.m_Bpp == 32); if (canUsePreview) { size_t level = 0; while ((texture.m_Width >> (level + 1)) >= width && (texture.m_Height >> (level + 1)) >= height && level < texture.GetMIPLevels().size()) ++level; // Extract the middle section (as a representative preview), // and copy into buffer. u8* data = texture.GetMIPLevels()[level].data; ENSURE(data); const size_t levelWidth = texture.m_Width >> level; const size_t levelHeight = texture.m_Height >> level; const size_t dataShiftX = (levelWidth - width) / 2; const size_t dataShiftY = (levelHeight - height) / 2; for (size_t y = 0; y < height; ++y) for (size_t x = 0; x < width; ++x) { const size_t bufferOffset = (y * width + x) * previewBPP; const size_t dataOffset = ((y + dataShiftY) * levelWidth + x + dataShiftX) * texture.m_Bpp / 8; buffer[bufferOffset + 0] = data[dataOffset + 0]; buffer[bufferOffset + 1] = data[dataOffset + 1]; buffer[bufferOffset + 2] = data[dataOffset + 2]; } preview.loaded = true; } else { // Too small to preview. Just use a flat color instead. const u32 baseColor = tex->GetBaseColor(); for (size_t i = 0; i < width * height; ++i) { buffer[i * previewBPP + 0] = (baseColor >> 16) & 0xff; buffer[i * previewBPP + 1] = (baseColor >> 8) & 0xff; buffer[i * previewBPP + 2] = (baseColor >> 0) & 0xff; } preview.loaded = tex->GetTexture()->IsLoaded(); } preview.imageWidth = width; preview.imageHeight = height; preview.imageData = buffer; return preview; } } // anonymous namespace 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; } QUERYHANDLER(GetTerrainGroupTextures) { std::vector names; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); if (group) { for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) names.emplace_back((*it)->GetTag().FromUTF8()); } std::sort(names.begin(), names.end()); msg->names = names; } 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); + const CTerrain& terrain = g_Game->GetWorld()->GetTerrain(); + CMiniPatch* const 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 msg->preview = MakeEmptyTerrainTexturePreview(); } ////////////////////////////////////////////////////////////////////////// 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(); + m_Terrain = &g_Game->GetWorld()->GetTerrain(); + m_VertsPerSide = m_Terrain->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); + 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); + 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); 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); + 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); 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